ReAct: Synergizing Reasoning and Acting in Language Models - 에이전트의 기초

동진·2026년 3월 9일

논문 리뷰

목록 보기
7/9

저번에는 많이 사용되는 모델인 Llama-3모델의 소개논문을 읽었다. 이번에는 에이전트 설계의 기본이되는 ReAct 기법 소개논문을 읽어볼 예정이다.

핵심은 프롬프트 구현 방식, 적용 효과 정도만 파악해보려고 한다.

ReAct: Synergizing Reasoning and Acting in Language Models

요약

LLM은 자연어 이해, 의사결정 task에서 인상적인 성능을 보였다. 하지만 추론관련 능력(CoT), 행동관련 능력(액션 플랜 생성)에 대한 연구는 별개의 영역으로 연구되어 왔고 이 논문에서 이를 통합했다.

이 논문에서 LLM이 추론trace가 액션 플랜을 유도, 추적, 갱신 하고 예외처리를 하도록 하고, 테스크별 액션은 지식베이스, 환경등 외부 소스에서 추가 정보를 수집하도록 하는 방법ReAct를 소개하고 있다.

ReAct라 명명한 이 접근법을 다양한 언어 및 의사결정 태스크에 적용했고, SOTA 베이스라인 대비 우수한 성능을 입증했다. 더불어 사람이 해석하기 쉽고(interpretability), 신뢰할 수 있는(trustworthiness) 결과를 보였다.

구체적으로, HotpotQA(질의응답)와 Fever(사실 검증) 태스크에서 Wikipedia API와의 상호작용을 통해 CoT 추론의 고질적 문제인 환각과 오류 전파를 극복했다.

방법

논문에서 테스트를 진행하기 위해 강화학습처럼 설정을 하였다.

시간 t에서:
- ot  : 환경으로부터 받은 관측값 (Observation)
- at  : 에이전트가 취하는 행동 (Action)
- ct  : 에이전트의 컨텍스트 = (o1, a1, ..., ot-1, at-1, ot)  ← 전체 히스토리
- π(at|ct) : 컨텍스트를 보고 액션을 결정하는 정책

왜 어렵냐면: ct → at 매핑이 매우 암묵적(implicit)(입력 → 출력 사이의 연산 과정이 불투명하고 해석 불가한 것) 이고 복잡한 연산이 필요하기 때문. 예를 들어 Figure 1(1c)의 Act-only 에이전트는 긴 trajectory 히스토리에서 복잡한 추론이 필요한 Act 4를 못 생성하고, Figure 1(2a)의 에이전트는 "sinkbasin에 peppershaker가 없다"는 걸 컨텍스트에서 파악 못 하고 계속 환각 액션을 반복한다.

이 설명을 좀 더 쉽게 설명하면

"컨텍스트는 있는데, 그걸 올바른 액션으로 변환하는 연산이 너무 복잡해서 모델이 실패한다"

우리가 아무리 프롬프트 길게 쓴다고 해도 모델이 우리가 원하는대로 행동하지 않는 경우가 대부분이다.

그래서 이 논문에서 추론 과정(thought)을 추가해서 언어 모델이 컨텍스트만 갖고 정답을 생성하지 않고 일정 절차를 따라 사고를하고 정답을 출력하게하여 환각, 오답 생성을 줄이려고 했다.

ReAct 예시1: 정보 검색

예를 들어 이런 질문을 했다고 가정하자.

"Apple Remote 말고, Apple Remote가 원래 상호작용하도록 설계된 프로그램을 제어할 수 있는 다른 기기는?"

1. 일반적 출력(오답 ❌)

Answer: iPod

일반적으로 그냥 LLM에 물어보면 이런 오답을 출력했다.

2. CoT(오답 ❌)

Thought: Apple Remote는 원래 Apple TV와 상호작용하도록 설계됨.
         Apple TV는 iPhone, iPad, iPod Touch로 제어 가능.
Answer: iPhone, iPad, iPod Touch

추론 과정을 추가했어서 좀 더 정확한 방향으로 생각하는 것 처럼 보이긴 하나, 모델이 알고 있는 정보만 갖고 추론하는 한계로 중간에 이상한 방향으로 빠져 정답을 출력하지 못했다.

3. Action 만 주어졌을 때 (오답 ❌)

Act 1: Search[Apple Remote]  →  Obs: Apple Remote is a remote control...
Act 2: Search[Front Row]     →  Obs: Could not find [Front Row]
Act 3: Search[Front Row software] → Obs: discontinued media center software
Act 4: Finish[yes]  ← ???

이건 이제 모르는 정보나 키워드가 나오면 검색을 하도록 프롬프르틀 작성한 것이다. 하지만 우리도 이유없이 지시를 하면 의도가 잘 전달 되지 않는 것처럼 Obs를 보면 정보를 잘 모으고 말도 안되는 Finish[yes]라는 답을 출력했다.

4. ReAct: Thoght + Action(정답 ✅)

Thought 1: Apple Remote를 검색해서 원래 상호작용하던 프로그램을 찾자
Act 1: Search[Apple Remote]
Obs 1: ...Front Row media center program...

Thought 2: Front Row를 찾았다. 다음엔 Front Row를 제어하는 다른 기기를 찾자
Act 2: Search[Front Row]
Obs 2: Could not find. Similar: ['Front Row software'...]

Thought 3: Front Row 못 찾음. 'Front Row software'로 바꿔서 검색하자  ← 오류 교정!
Act 3: Search[Front Row software]
Obs 3: discontinued media center software...

Thought 4: Apple Remote 또는 keyboard function keys로 제어 가능. 답은 keyboard function keys
Act 4: Finish[keyboard function keys]

이런식으로 추론 과정(thought)을 명시하고 그에 대한 행동(Action)을 명시하도록 수정하였다. 특히 3번째를 보면 원하는 정보를 찾지 못하자 다른 유사한 단어로 재검색을 하였다. 그 결과 올바른 정답을 출력하였다.

예시2: ALFWorld — 의사결정 태스크

마찬가지 이번에는 "pepper shaker를 drawer에 놓아라"라는 task를 실행해보자.

Act Only(오답 ❌)

Act 1: Go to drawer 1    → Obs: closed
Act 2: Open drawer 1     → Obs: dish sponge, spoon만 있음 (pepper shaker 없음)
Act 3: Go to sinkbasin 1 → Obs: dish sponge, spatula, spoon
Act 4: Take peppershaker from sinkbasin  → Obs: Nothing happens  ← 없는데 집으려함
Act 5: Take peppershaker from sinkbasin  → Obs: Nothing happens  ← 또 반복!

가장 일반적인 방법일 것이다.

이미 drawer에 후추통이 없는데 sinkbasin가서 찾고 있고 당연히 없으니 아무일도 일어나지 않아 계속 반복 중이다.

ReAct: Thoght + Action(정답 ✅)

Act 1: Think[pepper shaker는 cabinet(1-6), countertop(1-3)에 있을 가능성이 높다]
       → 계획부터 세움!

Act 2~5: cabinet 1, 2, 3 탐색
Act 6: Go to countertop 3
Obs 6: apple, bread, pepper shaker 1, vase 발견!

Act 7: Take pepper shaker from countertop 3
Obs 7: 집음 성공

Act 8: Think[pepper shaker를 찾았다. 이제 drawer에 넣자]
       → 중간 체크포인트 추론!

Act 9~10: Go to drawer 1, Open drawer 1
Act 11: Put pepper shaker in drawer 1  → ✅ 성공

매 단계마다 추론 과정(thought)를 두진 않았다. 필요한 시점에 생각을 하도록 통제하여 추론을 바탕으로 행동을하고 목표인 후추통을 잘 찾아서 drawer에 잘 집어넣었다.

이런식으로 매 단계마다 추론을 하는 것이 비효율적이니 필요할때만 비동기적으로 thought를 생성하는 방식으로 구성하며 사람의 행동과 같이통제 할 수 있었다.

장점

그래서 논문에서는 ReAct의 장점을 다음과 같이 정의 하였다.

1. 직관적이고 설계 쉬움

사람이 액션 위에 자연어로 thought를 타이핑하면 끝. 특별한 포맷이나 복잡한 설계 불필요

2. 범용적이고 유연함

유연한 thought 공간 덕분에 다양한 액션 스페이스와 태스크에 적용 가능

3. 성능 + 로버스트함

외부 정보로 추론을 보완 → 환각 줄고, 오류 전파 차단

4. 사람이 정렬됨

thought trace가 사람의 사고 흐름과 유사 → 해석 가능하고 신뢰할 수 있음

구현

대략적으로 ReAct의 루프를 다음과 같이 표현할 수 있다.

컨텍스트(ct) 입력
      ↓
   LLM 생성
   ↙        ↘
Thought      Action
(환경 영향 X)  (환경 상호작용)
ct 업데이트    Obs 받아서 ct 업데이트
      ↓
  종료 조건?
  ↙       ↘
Yes        No
Finish     다음 루프

여기서 환경이라하면 LLM바깥에 있는 무언가를 호출하고 결과를 받는 것으로 이해하면 될 것이다. 이는 API, 데이터, 링크 등 여러가지가 될 수 있다.

실제 구현에 필요한 것들

1. LLM(Policy)

GPT, Claude 계열의 큰 모델이면 좋다. Thought를 제대로 생성할 수 있는 능력만 있다면 어떤 모델이던지 상관 없을 것이다.

2. Few-shot Prompt (ReAct의 핵심 설계)

[System]
당신은 도구를 사용해 문제를 푸는 에이전트입니다.
Thought → Action → Observation 형식으로 응답하세요.

[Few-shot 예시]
Question: 서울 날씨는?

Thought 1: 서울 현재 날씨를 검색해야겠다
Action 1: Search[서울 현재 날씨]
Obs 1: 서울 5도, 비

Thought 2: 결과를 얻었다. 답변하자
Action 2: Finish[서울은 현재 5도이며 비가 옵니다]

[실제 질문]
Question: {user_input}

논문과 동일하게 사람이 사고과정을 작성해줘야 한다. 이것이 있어 Fine-Tuning없이도 모델이 학습하지 않은 정보를 검색해서 찾을 수 있는 것이다.

3. Tool(환경)

모델이 호출할 수 있는 외부 시스템들이다. 이는 에이전트의 task에따라 바뀐다.

4. Action Parser

LLM 출력에서 Thought와 Action을 분리해서 파싱하는 로직

import re

def parse_output(llm_output):
    # Thought 추출
    thought = re.search(r"Thought \d+: (.+)", llm_output)
    # Action 추출
    action = re.search(r"Action \d+: (\w+)\[(.+)\]", llm_output)
    
    if action:
        return {
            "thought": thought.group(1) if thought else None,
            "tool": action.group(1),   # "Search"
            "input": action.group(2)   # "서울 날씨"
        }

5. ReAct 루프 (Orchestrator)

def react_agent(question, max_steps=10):
    context = [{"role": "user", "content": question}]
    
    for step in range(max_steps):
        # 1. LLM 호출
        output = llm(context)
        context.append({"role": "assistant", "content": output})
        
        # 2. 파싱
        parsed = parse_output(output)
        
        # 3. 종료 조건 체크
        if parsed["tool"] == "Finish":
            return parsed["input"]
        
        # 4. 툴 실행 → Observation
        obs = execute_action(parsed)
        
        # 5. 컨텍스트 업데이트
        context.append({"role": "user", "content": f"Obs: {obs}"})
    
    return "max steps reached"

4가지 요소를 종합해서 원하는 결과가 나올 때까지 반복시키도록하는 루프이다. 무한히 반복되면 안되기 때문에 max_steps를 정해주는 것도 좋은 방법이다.

중요한 것은 이것을 우리는 모두 직접 할 필요가 없다. LangChain, LangGraph가 이를 쉽게 구현하기 위해 기능들이 준비가 되어있어 이를 활용하면된다.

마무리

뒷 내용은 개선 사항에 관한 내용이고 이미 ReAct를 개선한 방법론들이 많이 연구 되어있어 이정도로 마무리 지어보려한다.

그리고 논문에도 이 방법론의 명확한 한계를 시사한 것이 있다.

  • Reasoning Error: 구조적 제약으로 추론 유연성 감소
  • Repetitive Loop: 실패 시 탈출 방법 없음
  • Few-shot 한계: 복잡한 태스크 대응 부족
  • 외부 환경 의존: 툴 품질에 성능이 종속

4가지 정도를 한계로 분석했는데 결국 정해진 업무는 완벽하게 수행 할 때 사용하는 것으로 한정지을 수 있다. 그럼에도 여전히 기존의 실수를 반복하거나 다양하게 생각하지 못하는 한계는 명확하다.

그래서 다음에는 이를 극복하려는 시도를 한 논문 읽어보려고 한다.

profile
AI 개발공부 일지

0개의 댓글