ReAct 패턴의 원리와 구조를 이해하고, 추론-행동-관찰 루프를 직접 구현하여 LLM의 문제 해결 능력을 극대화하는 방법을 다룹니다.
ReAct는 Reasoning + Acting의 합성어로, 2022년 Yao et al.이 발표한 논문 "ReAct: Synergizing Reasoning and Acting in Language Models"에서 제안된 패턴입니다. 핵심 아이디어는 단순합니다. LLM이 행동을 취하기 전에 먼저 생각하도록 하고, 행동의 결과를 관찰한 후 다시 생각하는 과정을 반복하는 것입니다.
기존 LLM 사용 방식에서는 모델에게 질문을 하면 즉시 답변을 생성합니다. 이 방식은 모델이 이미 알고 있는 정보에 대해서는 잘 작동하지만, 외부 정보가 필요하거나 여러 단계의 추론이 필요한 경우에는 한계가 있습니다. ReAct 패턴은 이 한계를 극복합니다.
ReAct 패턴의 핵심은 세 단계의 반복입니다.
Thought (추론): 현재 상황을 분석하고, 목표를 달성하기 위해 다음에 무엇을 해야 하는지 결정합니다. 이 단계에서 LLM은 자연어로 자신의 사고 과정을 명시적으로 기술합니다.
Action (행동): 추론 결과에 따라 구체적인 행동을 취합니다. 도구를 호출하거나, 검색을 수행하거나, 코드를 실행하는 등의 행동이 이에 해당합니다.
Observation (관찰): 행동의 결과를 확인합니다. 검색 결과, API 응답, 코드 실행 결과 등이 관찰에 해당합니다.
추론만 하는 방식(Chain-of-Thought)과 ReAct의 차이를 예시로 비교해 보겠습니다.
Chain-of-Thought (추론만):
질문: 2026년 현재 대한민국 대통령은 누구이며, 그 대통령의 출생지는?
생각: 2026년 현재 대한민국 대통령은... 제가 알고 있는 정보로는...
(학습 데이터에 없는 정보에 대해 잘못된 답을 생성할 수 있음)
ReAct (추론 + 행동):
Thought: 2026년 현재 대한민국 대통령을 알아야 합니다. 검색으로 확인하겠습니다.
Action: search("2026년 대한민국 대통령")
Observation: [검색 결과]
Thought: 검색 결과를 확인했습니다. 이제 출생지를 검색하겠습니다.
Action: search("[대통령 이름] 출생지")
Observation: [검색 결과]
Thought: 두 가지 정보를 모두 확인했으므로 답변할 수 있습니다.
Answer: ...
ReAct는 실시간 정보를 활용하므로 정확도가 높고, 사고 과정이 투명하게 드러나므로 디버깅과 검증이 용이합니다.
실제 코드로 ReAct 패턴을 구현해 보겠습니다. Anthropic의 Claude API를 사용합니다.
먼저 에이전트가 사용할 도구들을 정의합니다.
import anthropic
import json
import httpx
client = anthropic.Anthropic()
# 도구 정의
tools = [
{
"name": "web_search",
"description": "웹에서 정보를 검색합니다. 최신 정보나 사실 확인이 필요할 때 사용합니다.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색 쿼리"
}
},
"required": ["query"]
}
},
{
"name": "calculator",
"description": "수학 계산을 수행합니다. 사칙연산, 거듭제곱 등을 처리합니다.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "계산할 수학 표현식 (예: '2 + 3 * 4')"
}
},
"required": ["expression"]
}
}
]각 도구의 실제 실행 로직을 구현합니다.
def execute_tool(name: str, input_data: dict) -> str:
if name == "web_search":
return web_search(input_data["query"])
elif name == "calculator":
return calculator(input_data["expression"])
else:
return f"알 수 없는 도구: {name}"
def web_search(query: str) -> str:
# 실제 구현에서는 검색 API를 호출합니다
# 여기서는 예시 데이터를 반환합니다
return f"'{query}'에 대한 검색 결과: [검색 결과 데이터]"
def calculator(expression: str) -> str:
try:
# 안전한 수학 표현식만 평가
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return "허용되지 않는 문자가 포함되어 있습니다."
result = eval(expression)
return str(result)
except Exception as e:
return f"계산 오류: {str(e)}"ReAct 패턴의 핵심인 에이전트 루프를 구현합니다.
SYSTEM_PROMPT = """당신은 문제를 단계적으로 해결하는 AI 에이전트입니다.
각 단계에서 먼저 현재 상황을 분석하고(Thought),
필요한 행동을 취하며(Action), 결과를 관찰합니다(Observation).
문제를 해결하기 위해 제공된 도구를 적극 활용하십시오.
충분한 정보를 수집한 후에 최종 답변을 제공하십시오."""
def react_agent(user_message: str, max_iterations: int = 10) -> str:
messages = [{"role": "user", "content": user_message}]
iteration = 0
while iteration < max_iterations:
iteration += 1
print(f"\n--- 반복 {iteration} ---")
# LLM 추론 (Thought + Action 결정)
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system=SYSTEM_PROMPT,
tools=tools,
messages=messages,
)
# 응답 내용 출력 (디버깅용)
for block in response.content:
if hasattr(block, "text"):
print(f"[Thought] {block.text}")
elif block.type == "tool_use":
print(f"[Action] {block.name}({json.dumps(block.input, ensure_ascii=False)})")
# 최종 응답인 경우 루프 종료
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return "응답을 생성할 수 없습니다."
# 도구 호출 처리
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
print(f"[Observation] {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
# 대화 히스토리에 추가
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
return "최대 반복 횟수에 도달했습니다."result = react_agent(
"파이썬의 최신 안정 버전은 무엇이고, "
"해당 버전이 출시된 지 며칠이 지났는지 계산해 주세요."
)
print(f"\n최종 답변: {result}")실행하면 에이전트는 다음과 같은 과정을 거칩니다.
--- 반복 1 ---
[Thought] 파이썬의 최신 안정 버전을 먼저 검색해야 합니다.
[Action] web_search({"query": "Python latest stable version 2026"})
[Observation] 'Python latest stable version 2026'에 대한 검색 결과: ...
--- 반복 2 ---
[Thought] 검색 결과로 최신 버전과 출시일을 확인했습니다. 이제 경과 일수를 계산하겠습니다.
[Action] calculator({"expression": "(2026 - 2025) * 365 + 30"})
[Observation] 395
--- 반복 3 ---
[Thought] 필요한 정보를 모두 수집했으므로 최종 답변을 제공하겠습니다.
최종 답변: ...
투명성: 사고 과정이 명시적으로 기록되므로, 에이전트가 왜 특정 행동을 선택했는지 추적할 수 있습니다. 이는 디버깅과 품질 관리에 결정적인 도움이 됩니다.
정확성: 추론만으로 답변을 생성하는 것보다, 실제 데이터를 기반으로 답변하므로 사실적 정확성이 높습니다. 특히 최신 정보가 필요한 경우 그 차이가 큽니다.
유연성: 도구를 조합하여 다양한 문제를 해결할 수 있습니다. 새로운 도구를 추가하면 에이전트의 능력이 자연스럽게 확장됩니다.
비용과 지연: 여러 번의 LLM 호출이 필요하므로, 단일 호출보다 비용과 응답 시간이 증가합니다. 반복 횟수가 많아질수록 이 문제가 심화됩니다.
무한 루프 위험: 에이전트가 목표를 달성했다고 판단하지 못하면 같은 행동을 반복할 수 있습니다. 최대 반복 횟수 제한이 필수적입니다.
도구 선택 오류: LLM이 잘못된 도구를 선택하거나, 올바른 도구를 잘못된 인자로 호출할 수 있습니다. 도구 설명의 품질이 성능에 직접적인 영향을 미칩니다.
ReAct 에이전트의 성능은 시스템 프롬프트의 품질에 크게 의존합니다. 효과적인 시스템 프롬프트는 다음 요소를 포함합니다.
OPTIMIZED_SYSTEM_PROMPT = """당신은 문제를 체계적으로 해결하는 AI 에이전트입니다.
## 행동 원칙
1. 행동하기 전에 반드시 현재 상황을 분석하십시오.
2. 한 번에 하나의 도구만 사용하고, 결과를 확인한 후 다음 단계를 결정하십시오.
3. 불확실한 정보는 추측하지 말고 도구를 사용하여 확인하십시오.
4. 충분한 정보를 수집한 후에만 최종 답변을 제공하십시오.
## 도구 사용 가이드
- web_search: 최신 정보, 사실 확인, 통계 데이터가 필요할 때
- calculator: 수치 계산, 날짜 계산, 단위 변환이 필요할 때
## 답변 형식
- 근거를 먼저 제시하고, 결론을 명확히 제시하십시오.
- 출처가 있는 정보는 출처를 함께 제공하십시오."""도구 설명(description)을 작성할 때는 "언제 이 도구를 사용해야 하는지"를 명확히 기술하는 것이 중요합니다. 모호한 설명은 LLM의 도구 선택 정확도를 떨어뜨립니다.
도구의 반환 값이 너무 길면 컨텍스트 윈도우를 불필요하게 소모합니다. 관찰 결과를 적절히 요약하거나 잘라내는 전처리가 필요합니다.
def preprocess_observation(result: str, max_length: int = 2000) -> str:
"""관찰 결과를 적절한 길이로 전처리합니다."""
if len(result) <= max_length:
return result
# 핵심 정보를 보존하면서 잘라냄
truncated = result[:max_length]
last_period = truncated.rfind(".")
if last_period > max_length * 0.5:
truncated = truncated[:last_period + 1]
return truncated + "\n\n[결과가 잘렸습니다. 추가 정보가 필요하면 더 구체적인 검색을 수행하십시오.]"에이전트가 무한 루프에 빠지는 것을 방지하기 위한 여러 전략이 있습니다.
class LoopController:
def __init__(self, max_iterations: int = 10, max_tokens: int = 50000):
self.max_iterations = max_iterations
self.max_tokens = max_tokens
self.iteration = 0
self.total_tokens = 0
self.action_history = []
def should_continue(self, action: str) -> bool:
self.iteration += 1
# 최대 반복 횟수 초과
if self.iteration > self.max_iterations:
return False
# 동일한 행동 반복 감지
self.action_history.append(action)
if len(self.action_history) >= 3:
last_three = self.action_history[-3:]
if len(set(last_three)) == 1:
return False # 같은 행동을 3번 반복
return True
def get_status(self) -> str:
return f"반복: {self.iteration}/{self.max_iterations}"토큰 사용량 제한은 비용 관리뿐 아니라 성능에도 중요합니다. 컨텍스트가 너무 길어지면 LLM의 추론 품질이 저하될 수 있습니다. 실무에서는 반복 횟수와 함께 총 토큰 사용량도 모니터링하는 것을 권장합니다.
도구 호출의 정확도를 높이기 위해, LLM의 출력을 구조화하는 변형입니다.
STRUCTURED_SYSTEM_PROMPT = """각 단계를 다음 형식으로 출력하십시오:
Thought: [현재 상황 분석과 다음 행동 계획]
Action: [사용할 도구 이름]
Action Input: [도구에 전달할 입력]
도구 사용이 필요 없으면 다음 형식으로 최종 답변을 제공하십시오:
Thought: [최종 분석]
Final Answer: [최종 답변]"""도구 호출이 실패했을 때 자동으로 수정하는 변형입니다. 이 변형은 4장에서 다룰 리플렉션 패턴과 밀접한 관련이 있습니다.
def execute_with_retry(name: str, input_data: dict, max_retries: int = 2) -> str:
"""도구 실행 실패 시 재시도합니다."""
for attempt in range(max_retries + 1):
try:
result = execute_tool(name, input_data)
if "오류" not in result and "에러" not in result:
return result
if attempt < max_retries:
return f"[실패] {result}\n재시도를 위해 입력을 수정해 주세요. (시도 {attempt + 1}/{max_retries})"
except Exception as e:
if attempt < max_retries:
return f"[예외] {str(e)}\n재시도를 위해 입력을 수정해 주세요."
return f"[최종 실패] {str(e)}"
return result3장에서는 ReAct 패턴의 핵심 구성 요소인 도구 사용(Tool Use) 패턴을 깊이 다룹니다. 도구를 어떻게 정의하고, LLM이 도구를 어떻게 선택하며, 결과를 어떻게 통합하는지를 체계적으로 살펴보겠습니다. 특히 도구 스키마 설계의 모범 사례와 복잡한 도구 조합 전략을 집중적으로 다룹니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
AI 에이전트의 도구 정의, 호출, 결과 통합의 전 과정을 다루고, 효과적인 도구 스키마 설계와 복합 도구 조합 전략을 살펴봅니다.
AI 에이전트가 무엇이고 왜 중요한지, 그리고 이 시리즈에서 다룰 핵심 설계 패턴들의 전체 지도를 살펴봅니다.
에이전트가 자신의 출력을 평가하고 반복적으로 개선하는 리플렉션 패턴의 원리, 구현 방법, 실전 활용 전략을 다룹니다.