ReAct(Reasoning+Acting) 패턴의 두 가지 구현 방식—텍스트 파싱 기반과 Tool Use API 기반—을 비교하고, Anthropic API로 실제 ReAct 에이전트를 파이썬으로 구현합니다.
🎯 학습 목표
이 강을 마치면 다음을 할 수 있습니다.
- ReAct 패턴의 두 가지 구현 방식(텍스트 파싱 기반 vs Tool Use API 기반)의 차이를 설명할 수 있습니다.
- Thought→Action→Observation 루프를 프롬프트로 설계할 수 있습니다.
- Anthropic Python SDK의
messages.create()API와 Tool Use 기능을 사용해 실제로 동작하는 ReAct 에이전트를 구현할 수 있습니다. - 파싱 실패, 무한 루프 등 흔한 구현 실수를 사전에 방지하는 방법을 알 수 있습니다.
💡 ReAct 패턴의 핵심 아이디어
이 섹션에서는 1강에서 개념으로 소개한 ReAct가 실제로 무엇을 해결하는지 다시 한번 짚어보겠습니다.
1강에서 배운 것처럼 ReAct는 Reasoning(추론)과 Acting(행동)을 결합한 에이전트 패턴으로, 2022년 Shunyu Yao 외 연구진이 발표한 논문(ICLR 2023)에서 제안됐습니다. 핵심 통찰은 단순합니다. LLM이 행동만 하면 왜 그 행동을 했는지 추적하기 어렵고, 추론만 하면 실제 세계의 정보를 반영하지 못합니다. ReAct는 이 두 가지를 교대로 수행하도록 만들어 두 단점을 동시에 해결합니다.
구체적으로 LLM은 세 가지 유형의 텍스트를 교대로 출력합니다.
- Thought(생각): 현재 상황을 분석하고 다음에 무엇을 해야 할지 추론합니다. 이 단계는 LLM 내부에서만 일어나며, 외부 시스템에 직접 영향을 주지 않습니다.
- Action(행동): 어떤 도구를 어떤 인수로 호출할지 결정합니다. 텍스트 형식으로 표현되거나(원형 ReAct), 구조화된 JSON으로 표현됩니다(현대적 Tool Use).
- Observation(관찰): 도구 실행 결과를 받아 컨텍스트에 추가합니다. 이 관찰이 다음 Thought의 입력이 됩니다.
ReAct가 강력한 이유는 LLM이 생각 과정을 텍스트로 명시화하기 때문에 체인 오브 쏘트(Chain-of-Thought) 추론의 장점을 그대로 누리면서, 동시에 외부 도구를 통해 실시간 정보와 계산 능력을 확보할 수 있다는 데 있습니다. 순수 추론 모델과 도구 사용 모델의 강점을 모두 가집니다.
📋 텍스트 파싱 기반 ReAct — 논문 원형 이해하기
이 섹션에서는 ReAct 논문의 원형 방식인 텍스트 파싱 기반 구현을 살펴보겠습니다. 현대의 Tool Use API를 이해하기 위한 기초 개념입니다.
ReAct 논문의 원형 구현에서는 LLM에게 다음과 같은 형식으로 출력하도록 프롬프트를 설계합니다.
# ReAct 프롬프트 템플릿 (논문 원형)
REACT_SYSTEM_PROMPT = """
당신은 도구를 사용해 질문에 답하는 에이전트입니다.
반드시 아래 형식으로만 응답하세요:
Thought: [현재 상황을 분석하고 다음 단계를 추론합니다]
Action: tool_name[입력값]
또는 답을 알았을 때:
Thought: [최종 답을 도출했습니다]
Action: finish[최종 답변]
사용 가능한 도구:
- search[질의]: 웹에서 정보를 검색합니다
- calculator[수식]: 수식을 계산합니다
- finish[답변]: 최종 답변을 반환하고 종료합니다
Observation은 시스템이 제공합니다. 직접 작성하지 마세요.
"""
# LLM 출력 파싱 함수
import re
def parse_react_output(text: str) -> dict:
"""LLM이 출력한 ReAct 텍스트를 파싱합니다."""
thought_match = re.search(r"Thought:\s*(.+?)(?=Action:|$)", text, re.DOTALL)
action_match = re.search(r"Action:\s*(\w+)\[(.+?)\]", text, re.DOTALL)
if not thought_match or not action_match:
raise ValueError(f"ReAct 형식 파싱 실패: {text[:100]}")
return {
"thought": thought_match.group(1).strip(),
"tool": action_match.group(1).strip(),
"input": action_match.group(2).strip(),
}
이 방식의 핵심은 LLM 출력 전체가 하나의 텍스트 블록이라는 점입니다. Thought:와 Action: 레이블을 정규표현식으로 파싱해 도구 이름과 인수를 추출합니다. 단순하지만 LLM이 형식을 정확히 따르지 않으면 파싱이 실패합니다. 이것이 텍스트 기반 방식의 가장 큰 약점입니다.
예를 들어 LLM이 Action: search[파이썬 최신 버전] 대신 Action: 파이썬 최신 버전을 검색합니다라고 출력하면 파싱이 완전히 깨집니다. 실제 프로덕션에서 텍스트 파싱 기반 ReAct를 사용할 경우, 파싱 실패에 대한 재시도 로직과 오류 복구 전략이 필수입니다.
🔧 현대적 접근 — Tool Use API 기반 ReAct
이 섹션에서는 텍스트 파싱 기반의 한계를 해결한 현대적 방식인 Tool Use API를 살펴보겠습니다.
오늘날 대부분의 LLM API는 도구 정의를 JSON 스키마로 받아, 모델이 도구를 호출할 때 구조화된 JSON을 직접 생성하도록 지원합니다. Anthropic Claude의 경우 응답의 stop_reason이 "tool_use"가 되면 모델이 도구 호출을 결정했다는 뜻이고, content 배열에 tool_use 타입의 블록이 포함됩니다.
이 방식에서 ReAct의 Thought 단계는 LLM 내부 추론에 내재됩니다. 모델은 도구를 언제 호출할지 스스로 결정하며, 개발자가 텍스트를 파싱할 필요가 없습니다. 도구 호출 인수는 JSON으로 제공되므로 타입 검증도 자동으로 이루어집니다.
두 방식을 비교하면 다음과 같습니다.
| 구분 | 텍스트 파싱 기반 (논문 원형) | Tool Use API 기반 (현대적) |
|---|---|---|
| 파싱 안정성 | 낮음 (형식 위반 시 실패) | 높음 (구조화 JSON 보장) |
| Thought 가시성 | 텍스트로 명시적 출력 | 내부 추론 (확장 thinking으로 접근 가능) |
| 도구 인수 검증 | 수동 구현 필요 | JSON Schema로 자동 검증 |
| 구현 복잡도 | 낮음 (프롬프트만) | 중간 (도구 스키마 정의 필요) |
| 프로덕션 적합성 | 낮음 | 높음 |
💻 파이썬 완전 구현 — Anthropic API로 ReAct 에이전트 만들기
이 섹션에서는 Anthropic Python SDK를 사용해 Tool Use 기반 ReAct 에이전트를 처음부터 구현합니다. 실제로 동작하는 코드입니다.
import anthropic
import json
# 1. 클라이언트 초기화
client = anthropic.Anthropic(api_key="YOUR_API_KEY") # 환경 변수 권장
# 2. 도구 정의 (JSON Schema)
TOOLS = [
{
"name": "search_web",
"description": "웹에서 최신 정보를 검색합니다. 사실 확인이나 최신 데이터가 필요할 때 사용하세요.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색할 질의문. 구체적일수록 좋은 결과를 얻습니다."
}
},
"required": ["query"]
}
},
{
"name": "calculate",
"description": "수식을 계산합니다. Python eval() 형식의 수식을 입력하세요.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "계산할 수식. 예: '(100 * 1.1) / 12'"
}
},
"required": ["expression"]
}
}
]
# 3. 실제 도구 구현 (프로덕션에서는 실제 API 연결)
def search_web(query: str) -> str:
"""웹 검색 도구 (예시용 Mock 구현)"""
# 실제 구현 시 SerpAPI, Tavily 등 연결
return f"[검색 결과] '{query}'에 대한 정보: ... (실제 API 응답)"
def calculate(expression: str) -> str:
"""수식 계산 도구"""
try:
# 보안 주의: 프로덕션에서는 eval 대신 안전한 파서 사용
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"계산 오류: {e}"
# 도구 이름 → 함수 매핑
TOOL_FUNCTIONS = {
"search_web": search_web,
"calculate": calculate,
}
# 4. ReAct 에이전트 루프
def run_react_agent(user_goal: str, max_steps: int = 10) -> str:
"""Tool Use API 기반 ReAct 에이전트"""
system_prompt = (
"당신은 도구를 사용해 사용자의 질문에 정확하게 답하는 에이전트입니다. "
"필요한 정보는 반드시 도구를 통해 확인하고, 추측으로 답하지 마세요."
)
messages = [{"role": "user", "content": user_goal}]
for step in range(max_steps):
# LLM 호출
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=system_prompt,
tools=TOOLS,
messages=messages,
)
print(f"\n[스텝 {step + 1}] stop_reason: {response.stop_reason}")
# 종료 조건: 더 이상 도구가 필요 없으면 최종 답변 반환
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return "응답을 생성하지 못했습니다."
# 도구 호출 처리
if response.stop_reason == "tool_use":
# 모델 응답을 대화 히스토리에 추가
messages.append({"role": "assistant", "content": response.content})
# 각 tool_use 블록 처리
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
print(f" → 도구 호출: {tool_name}({tool_input})")
# 도구 실행
fn = TOOL_FUNCTIONS.get(tool_name)
if fn:
observation = fn(**tool_input)
else:
observation = f"오류: '{tool_name}'은 존재하지 않는 도구입니다."
print(f" ← 관찰: {observation[:80]}...")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # 반드시 매칭된 ID 사용
"content": observation,
})
# 관찰 결과를 메시지로 추가 (user 역할)
messages.append({"role": "user", "content": tool_results})
return f"최대 {max_steps}스텝에 도달했습니다. 목표를 완료하지 못했습니다."
# 5. 실행
if __name__ == "__main__":
result = run_react_agent("파이썬 최신 안정 버전이 언제 출시됐는지 알아보고, 3.12와 3.13의 주요 차이점도 비교해줘.")
print(f"\n최종 답변:\n{result}")
코드의 핵심 포인트를 하나씩 설명하겠습니다.
- 도구 정의의
description: LLM이 도구를 언제, 왜 사용해야 하는지 결정하는 기준이 됩니다. 모호한 설명은 잘못된 도구 선택으로 이어집니다. "최신 정보가 필요할 때 사용하세요"처럼 사용 조건을 명시하는 것이 좋습니다. stop_reason == "tool_use": 모델이 도구를 호출하기로 결정했다는 신호입니다. 이 조건을 체크하지 않으면 루프가 올바르게 동작하지 않습니다.tool_use_id매칭:tool_result의tool_use_id는 반드시 대응하는tool_use블록의id와 일치해야 합니다. 불일치하면 API 오류가 발생합니다.messages.append({"role": "assistant", ...}): 도구 결과를 보내기 전에 반드시 어시스턴트의 응답을 먼저 히스토리에 추가해야 합니다. 순서가 바뀌면 오류가 납니다.
⚠️ 자주 발생하는 문제와 해결법
이 섹션에서는 ReAct 에이전트를 처음 구현할 때 흔히 마주치는 문제와 해결법을 살펴보겠습니다.
문제 1: 무한 tool_use 루프. 에이전트가 도구를 계속 호출하면서 끝나지 않는 경우입니다. 보통 도구 설명이 불명확하거나, 에이전트가 언제 도구 사용을 멈춰야 하는지 모르는 상황에서 발생합니다. 해결책은 두 가지입니다. 첫째, max_steps로 반드시 최대 반복 횟수를 제한합니다. 둘째, 시스템 프롬프트에 "충분한 정보를 수집했으면 도구를 더 이상 호출하지 말고 답변하세요"라고 명시합니다.
문제 2: tool_result ID 불일치. tool_use_id가 실제 tool_use 블록의 id와 다를 때 발생하는 API 오류입니다. block.id를 직접 참조해 tool_use_id에 전달하면 해결됩니다. 절대 ID를 직접 생성하거나 임의 값을 넣지 마세요.
문제 3: 메시지 순서 오류. tool_result를 메시지로 추가하기 전에 어시스턴트 응답을 먼저 추가하지 않으면 오류가 발생합니다. 대화 히스토리는 항상 user → assistant → user(tool_result) → assistant → ... 순서를 유지해야 합니다.
문제 4: max_tokens 부족. 에이전트가 복잡한 추론을 한 번에 처리해야 할 때 토큰이 부족해 stop_reason == "max_tokens"로 중단되는 경우입니다. 에이전트용 LLM 호출은 일반 챗봇보다 max_tokens를 넉넉히 설정하세요. 2,048 토큰 이상을 권장합니다.
문제 5: 도구 실행 예외 미처리. 도구 함수에서 예외가 발생했을 때 처리 없이 에이전트가 중단되는 경우입니다. 도구 함수는 예외를 외부로 던지지 말고, 오류를 문자열로 반환해 에이전트가 다른 방법을 시도할 수 있도록 설계해야 합니다.
📝 핵심 요약 + 다음 강 예고
이 강에서 배운 내용을 정리하겠습니다.
| 개념 | 핵심 내용 |
|---|---|
| ReAct 두 가지 방식 | 텍스트 파싱(논문 원형) vs Tool Use API(현대적, 프로덕션 권장) |
| Tool Use API 흐름 | messages.create() → stop_reason 체크 → tool_use 블록 처리 → tool_result 반환 → 반복 |
| 핵심 코드 패턴 | tool_use_id 매칭, 메시지 순서 준수, max_steps 제한 |
| 도구 설명의 중요성 | description이 LLM의 도구 선택 판단 기준 — 명확하고 구체적으로 작성 |
| 흔한 실수 | 무한 루프, ID 불일치, 메시지 순서 오류, max_tokens 부족 |
이제 ReAct 에이전트를 실제로 돌릴 수 있게 됐습니다. 하지만 아직 한 가지 큰 질문이 남아 있습니다. 도구를 어떻게 설계해야 에이전트가 올바르게 사용하는가? 다음 강인 3강 「도구 정의와 스키마 설계」에서는 JSON Schema로 도구를 정의하는 방법, 좋은 description 작성 원칙, 필수 파라미터와 선택 파라미터 설계 전략을 깊이 있게 다룹니다.
📚 참고 자료
관련 주제
- ReAct 패턴
- Reasoning Acting
- Thought Action Observation
- Tool Use API
- Anthropic Python SDK
- 무한 루프 방지
- AI 기술
- AI 기술 강의
- LLM 에이전트 설계와 구현 20강
- NUGUNA
- 누구나
📚 시리즈 전체 공유
LLM 에이전트 설계와 구현 20강
이 강의가 속한 시리즈는 총 20강(수강생 전용 보너스 · 1~3강 무료)입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
불러오는 중...
