1 들어가며
LLM은 강력하지만, 행동할 수 없다는 근본적인 한계가 있다. 질문에 답할 수는 있지만, 웹 검색을 하거나, 계산을 수행하거나, 데이터베이스를 조회할 수는 없다.
ReAct (Reasoning + Acting)는 이 한계를 극복한다. LLM이 추론(Reasoning)과 행동(Acting)을 번갈아 수행하며, 외부 도구를 활용하여 복잡한 문제를 단계적으로 해결한다.
1.1 기존 접근법의 한계
1.1.1 순수 추론만 (Chain-of-Thought)
질문: "2024년 노벨 물리학상 수상자는?"
CoT 응답:
"단계 1: 노벨상은 매년 10월에 발표됩니다.
단계 2: 물리학상은 중요한 발견에 수여됩니다.
단계 3: 2024년 수상자는... (할루시네이션 가능)"
문제점: 실제 최신 정보 없이 추측
1.1.2 순수 행동만 (Tool Use)
질문: "2024년 노벨 물리학상 수상자의 주요 업적을 설명해주세요"
Tool Use:
Action: search("2024 Nobel Physics")
Observation: "John Hopfield and Geoffrey Hinton"
응답: "John Hopfield and Geoffrey Hinton"
문제점: 컨텍스트 없이 단순 정보만 반환
1.1.3 ReAct (추론 + 행동)
질문: "2024년 노벨 물리학상 수상자의 주요 업적을 설명해주세요"
Thought 1: 먼저 2024년 수상자가 누구인지 검색해야 함
Action 1: search("2024 Nobel Prize Physics winner")
Observation 1: "John Hopfield and Geoffrey Hinton won for foundational discoveries in artificial neural networks"
Thought 2: 이제 각 수상자의 구체적인 업적을 찾아야 함
Action 2: search("John Hopfield neural networks contribution")
Observation 2: "Hopfield networks (1982), associative memory..."
Thought 3: Geoffrey Hinton의 업적도 확인 필요
Action 3: search("Geoffrey Hinton backpropagation deep learning")
Observation 3: "Backpropagation algorithm, deep learning pioneer..."
Thought 4: 이제 충분한 정보를 모았으므로 종합 답변 가능
Action 4: finish("2024년 노벨 물리학상은 John Hopfield와 Geoffrey Hinton이 수상했습니다. Hopfield는 1982년 Hopfield 네트워크를 개발하여 연상 메모리의 기초를 마련했고, Hinton은 역전파 알고리즘과 심층학습 발전에 기여했습니다...")
차이점: - ✅ 단계별 추론 (왜 이 행동을 하는지) - ✅ 외부 도구 활용 (실제 정보 획득) - ✅ 관찰 결과 반영 (동적 조정) - ✅ 종합적 답변 (컨텍스트 포함)
2 ReAct란?
Yao et al. (2022)이 제안한 ReAct는 Reasoning (추론)과 Acting (행동)을 번갈아 수행하는 프롬프팅 패러다임이다.
2.1 핵심 구조
┌─────────────────────────────────────────┐
│ Thought (추론) │
│ "다음에 무엇을 해야 하는가?" │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Action (행동) │
│ 도구 호출: search, calculate, etc. │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Observation (관찰) │
│ 행동의 결과 │
└─────────────────────────────────────────┘
↓
(Thought로 돌아가거나 완료)
2.2 Thought-Action-Observation 사이클
import anthropic
from typing import List, Dict, Callable
import re
class ReActAgent:
"""
ReAct Agent 구현
"""
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = "claude-sonnet-4-20250514"
# 사용 가능한 도구들
self.tools = {}
self.register_default_tools()
# 실행 이력
self.history = []
def register_tool(self, name: str, function: Callable, description: str):
"""
도구 등록
Args:
name: 도구 이름
function: 실행할 함수
description: 도구 설명
"""
self.tools[name] = {
'function': function,
'description': description
}
def register_default_tools(self):
"""
기본 도구 등록
"""
# 검색 도구
def search(query: str) -> str:
"""웹 검색 (시뮬레이션)"""
# 실제로는 Google Search API 등 사용
return f"검색 결과: {query}에 대한 정보..."
self.register_tool(
name="search",
function=search,
description="웹에서 정보를 검색합니다. 입력: 검색 쿼리"
)
# 계산기 도구
def calculate(expression: str) -> str:
"""수학 계산"""
try:
result = eval(expression)
return str(result)
except Exception as e:
return f"계산 오류: {e}"
self.register_tool(
name="calculate",
function=calculate,
description="수학 계산을 수행합니다. 입력: 수식 (예: '2+2', '10*5')"
)
# 완료 도구
def finish(answer: str) -> str:
"""작업 완료"""
return answer
self.register_tool(
name="finish",
function=finish,
description="최종 답변을 제공하고 종료합니다. 입력: 최종 답변"
)
def create_prompt(self, question: str) -> str:
"""
ReAct 프롬프트 생성
"""
# 도구 목록
tools_desc = "\n".join([
f"- {name}: {info['description']}"
for name, info in self.tools.items()
])
# 기본 프롬프트
prompt = f"""다음 질문에 답하기 위해 Thought-Action-Observation 사이클을 사용하세요.
사용 가능한 도구:
{tools_desc}
형식:
Thought: [다음 단계에 대한 추론]
Action: [도구명][입력]
Observation: [도구 실행 결과]
... (필요한 만큼 반복)
Thought: 이제 최종 답변 가능
Action: finish[최종 답변]
질문: {question}
Thought 1:"""
# 기존 이력 추가
if self.history:
prompt += "\n\n" + self._format_history()
return prompt
def _format_history(self) -> str:
"""
실행 이력 포맷팅
"""
formatted = []
for i, step in enumerate(self.history, 1):
formatted.append(f"Thought {i}: {step['thought']}")
formatted.append(f"Action {i}: {step['action']}")
formatted.append(f"Observation {i}: {step['observation']}")
return "\n".join(formatted)
def parse_action(self, text: str) -> tuple:
"""
Action 파싱
형식: "Action: tool_name[input]"
Returns:
(tool_name, input)
"""
# "Action: " 이후 텍스트 추출
action_match = re.search(r'Action\s*\d*:\s*(\w+)\[(.*?)\]', text, re.IGNORECASE)
if action_match:
tool_name = action_match.group(1)
tool_input = action_match.group(2)
return tool_name, tool_input
return None, None
def execute_action(self, tool_name: str, tool_input: str) -> str:
"""
도구 실행
"""
if tool_name not in self.tools:
return f"오류: '{tool_name}'는 사용 가능한 도구가 아닙니다."
try:
tool_func = self.tools[tool_name]['function']
result = tool_func(tool_input)
return result
except Exception as e:
return f"도구 실행 오류: {e}"
def run(self, question: str, max_steps: int = 10, verbose: bool = True) -> str:
"""
ReAct 실행
Args:
question: 질문
max_steps: 최대 단계 수
verbose: 진행 과정 출력 여부
Returns:
최종 답변
"""
if verbose:
print(f"🤔 질문: {question}\n")
print("="*80)
self.history = []
for step in range(1, max_steps + 1):
if verbose:
print(f"\n📍 Step {step}")
print("-"*80)
# Prompt 생성
prompt = self.create_prompt(question)
# LLM 호출
message = self.client.messages.create(
model=self.model,
max_tokens=500,
temperature=0,
messages=[{"role": "user", "content": prompt}]
)
response = message.content[0].text
# Thought 추출
thought_match = re.search(
r'Thought\s*\d*:\s*(.*?)(?=Action|$)',
response,
re.IGNORECASE | re.DOTALL
)
thought = thought_match.group(1).strip() if thought_match else ""
if verbose:
print(f"💭 Thought: {thought}")
# Action 파싱
tool_name, tool_input = self.parse_action(response)
if tool_name is None:
if verbose:
print("⚠️ Action을 파싱할 수 없습니다. 종료합니다.")
break
if verbose:
print(f"🔧 Action: {tool_name}[{tool_input}]")
# Action 실행
observation = self.execute_action(tool_name, tool_input)
if verbose:
print(f"👁️ Observation: {observation}")
# 이력 저장
self.history.append({
'thought': thought,
'action': f"{tool_name}[{tool_input}]",
'observation': observation
})
# finish 도구면 종료
if tool_name == 'finish':
if verbose:
print("\n" + "="*80)
print("✅ 완료!")
print("="*80)
return observation
if verbose:
print("\n⚠️ 최대 단계 수 도달. 종료합니다.")
return "최대 단계 수에 도달했습니다."
# 사용 예시
def main():
agent = ReActAgent(api_key="your-api-key")
question = "15와 23의 합에 3을 곱한 값은?"
answer = agent.run(question, max_steps=10, verbose=True)
print(f"\n최종 답변: {answer}")
if __name__ == "__main__":
main()3 실행 예시
🤔 질문: 15와 23의 합에 3을 곱한 값은?
================================================================================
📍 Step 1
--------------------------------------------------------------------------------
💭 Thought: 먼저 15와 23을 더해야 합니다.
🔧 Action: calculate[15+23]
👁️ Observation: 38
📍 Step 2
--------------------------------------------------------------------------------
💭 Thought: 이제 38에 3을 곱해야 합니다.
🔧 Action: calculate[38*3]
👁️ Observation: 114
📍 Step 3
--------------------------------------------------------------------------------
💭 Thought: 계산이 완료되었습니다. 최종 답변을 제공할 수 있습니다.
🔧 Action: finish[15와 23의 합은 38이고, 이에 3을 곱하면 114입니다.]
👁️ Observation: 15와 23의 합은 38이고, 이에 3을 곱하면 114입니다.
================================================================================
✅ 완료!
================================================================================
최종 답변: 15와 23의 합은 38이고, 이에 3을 곱하면 114입니다.
4 외부 도구 통합
4.1 도구 1: 웹 검색
def register_real_search_tool(agent: ReActAgent):
"""
실제 웹 검색 도구 등록
"""
def web_search(query: str) -> str:
"""
Google Search API 사용
"""
import requests
# Google Custom Search API
api_key = "your-google-api-key"
search_engine_id = "your-search-engine-id"
url = "https://www.googleapis.com/customsearch/v1"
params = {
'key': api_key,
'cx': search_engine_id,
'q': query,
'num': 3 # 상위 3개 결과
}
try:
response = requests.get(url, params=params)
results = response.json()
# 결과 포맷팅
if 'items' in results:
summaries = []
for item in results['items'][:3]:
title = item.get('title', '')
snippet = item.get('snippet', '')
summaries.append(f"{title}: {snippet}")
return "\n".join(summaries)
else:
return "검색 결과를 찾을 수 없습니다."
except Exception as e:
return f"검색 오류: {e}"
agent.register_tool(
name="search",
function=web_search,
description="웹에서 최신 정보를 검색합니다. 입력: 검색 쿼리"
)4.2 도구 2: Wikipedia
def register_wikipedia_tool(agent: ReActAgent):
"""
Wikipedia 도구 등록
"""
def wikipedia_lookup(title: str) -> str:
"""
Wikipedia 페이지 요약 가져오기
"""
import wikipedia
try:
# 페이지 검색
page = wikipedia.page(title, auto_suggest=True)
# 요약 (처음 500자)
summary = page.summary[:500]
return f"{page.title}:\n{summary}..."
except wikipedia.exceptions.DisambiguationError as e:
# 동음이의어 페이지
options = e.options[:5]
return f"여러 의미가 있습니다: {', '.join(options)}"
except wikipedia.exceptions.PageError:
return f"'{title}' 페이지를 찾을 수 없습니다."
except Exception as e:
return f"Wikipedia 오류: {e}"
agent.register_tool(
name="wikipedia",
function=wikipedia_lookup,
description="Wikipedia에서 정보를 가져옵니다. 입력: 페이지 제목"
)4.3 도구 3: 데이터베이스 쿼리
def register_database_tool(agent: ReActAgent):
"""
데이터베이스 쿼리 도구
"""
def query_database(sql: str) -> str:
"""
SQL 쿼리 실행 (안전하게)
"""
import sqlite3
# 안전성 체크
sql_lower = sql.lower().strip()
# SELECT만 허용
if not sql_lower.startswith('select'):
return "오류: SELECT 쿼리만 허용됩니다."
# 위험한 키워드 체크
dangerous = ['drop', 'delete', 'update', 'insert', 'alter']
if any(kw in sql_lower for kw in dangerous):
return "오류: 데이터 수정 쿼리는 허용되지 않습니다."
try:
# 연결 (예시)
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute(sql)
results = cursor.fetchall()
conn.close()
# 결과 포맷팅
if results:
formatted = "\n".join([str(row) for row in results[:10]])
return formatted
else:
return "결과 없음"
except Exception as e:
return f"데이터베이스 오류: {e}"
agent.register_tool(
name="query_db",
function=query_database,
description="데이터베이스에서 정보를 조회합니다 (SELECT만 가능). 입력: SQL 쿼리"
)4.4 도구 4: Python 코드 실행
def register_python_exec_tool(agent: ReActAgent):
"""
Python 코드 실행 도구 (샌드박스)
"""
def execute_python(code: str) -> str:
"""
Python 코드 실행 (안전하게)
"""
import sys
from io import StringIO
import contextlib
# 출력 캡처
output = StringIO()
# 안전성 체크
dangerous_imports = [
'os', 'sys', 'subprocess', 'eval', 'exec',
'__import__', 'open', 'file'
]
code_lower = code.lower()
if any(danger in code_lower for danger in dangerous_imports):
return "오류: 위험한 코드가 감지되었습니다."
try:
# 코드 실행 (타임아웃 필요)
with contextlib.redirect_stdout(output):
# 제한된 환경에서 실행
allowed_globals = {
'__builtins__': {
'print': print,
'range': range,
'len': len,
'sum': sum,
'max': max,
'min': min,
}
}
exec(code, allowed_globals)
result = output.getvalue()
return result if result else "실행 완료 (출력 없음)"
except Exception as e:
return f"실행 오류: {e}"
agent.register_tool(
name="python",
function=execute_python,
description="Python 코드를 실행합니다 (제한된 환경). 입력: Python 코드"
)4.5 도구 5: API 호출
def register_api_tool(agent: ReActAgent):
"""
외부 API 호출 도구
"""
def call_api(endpoint: str) -> str:
"""
REST API 호출
형식: "GET https://api.example.com/endpoint"
"""
import requests
parts = endpoint.split(' ', 1)
if len(parts) != 2:
return "오류: 형식은 'METHOD URL' 입니다."
method, url = parts
method = method.upper()
if method not in ['GET', 'POST']:
return "오류: GET 또는 POST만 지원됩니다."
try:
if method == 'GET':
response = requests.get(url, timeout=5)
else:
response = requests.post(url, timeout=5)
# 응답 (처음 500자)
content = response.text[:500]
return f"Status: {response.status_code}\n{content}"
except Exception as e:
return f"API 호출 오류: {e}"
agent.register_tool(
name="api_call",
function=call_api,
description="외부 API를 호출합니다. 입력: 'METHOD URL' (예: 'GET https://api.example.com/data')"
)5 복잡한 예시: 다단계 문제 해결
def complex_example():
"""
복잡한 다단계 문제
"""
agent = ReActAgent(api_key="your-api-key")
# 추가 도구 등록
register_wikipedia_tool(agent)
question = """
2024년 노벨 물리학상 수상자 중 한 명의 출생 연도를 찾고,
그 사람이 몇 살에 수상했는지 계산해주세요.
"""
answer = agent.run(question, max_steps=15, verbose=True)
return answer실행 과정:
📍 Step 1
💭 Thought: 먼저 2024년 노벨 물리학상 수상자가 누구인지 검색해야 합니다.
🔧 Action: search[2024 Nobel Prize Physics winner]
👁️ Observation: John Hopfield and Geoffrey Hinton won the 2024 Nobel Prize in Physics...
📍 Step 2
💭 Thought: John Hopfield의 출생 연도를 Wikipedia에서 찾겠습니다.
🔧 Action: wikipedia[John Hopfield]
👁️ Observation: John Hopfield: Born July 15, 1933, American scientist...
📍 Step 3
💭 Thought: 1933년생이고 2024년에 수상했으므로, 나이를 계산해야 합니다.
🔧 Action: calculate[2024-1933]
👁️ Observation: 91
📍 Step 4
💭 Thought: 이제 충분한 정보를 모았습니다. 최종 답변을 작성할 수 있습니다.
🔧 Action: finish[John Hopfield는 1933년 7월 15일생으로, 2024년 노벨 물리학상을 91세에 수상했습니다.]
👁️ Observation: John Hopfield는 1933년 7월 15일생으로, 2024년 노벨 물리학상을 91세에 수상했습니다.
✅ 완료!
6 실험 결과 분석
Yao et al. (2022)의 논문 결과를 분석하자.
6.1 벤치마크 성능
6.1.1 HotpotQA (Multi-hop Question Answering)
설정: - 두 개 이상의 Wikipedia 페이지 정보 필요 - 예: “The director of the 2003 film which has a song Frozen, directed 5 films in which year?”
결과:
| 방법 | 정확도 (EM) | F1 Score |
|---|---|---|
| Standard Prompting | 28.7% | 35.1% |
| CoT (Chain-of-Thought) | 29.4% | 36.6% |
| ReAct | 27.4% | 34.2% |
| ReAct + Self-Consistency | 34.2% | 42.9% |
관찰: - ReAct 단독: CoT와 유사 - ReAct + Self-Consistency: 최고 성능 (+19% vs Standard)
6.1.2 FEVER (Fact Verification)
설정: - 주장이 사실인지 검증 - Wikipedia 검색 필요
결과:
| 방법 | 정확도 |
|---|---|
| Standard Prompting | 56.3% |
| CoT | 58.1% |
| ReAct | 64.6% |
개선폭: +8.3% (vs Standard)
ReAct가 특히 효과적인 이유: - Wikipedia 검색으로 실제 정보 확인 - 단계별로 증거 수집 - 모순 발견 시 추가 검색
6.2 행동 유형 분석
ReAct가 사용한 행동들:
| 행동 유형 | 빈도 | 예시 |
|---|---|---|
| 검색 (Search) | 58% | “search[John Hopfield]” |
| 조회 (Lookup) | 23% | “lookup[birth year]” |
| 계산 (Calculate) | 12% | “calculate[2024-1933]” |
| 완료 (Finish) | 7% | “finish[answer]” |
6.3 추론 유형 분석
Thought의 목적:
| 목적 | 비율 | 예시 |
|---|---|---|
| 계획 | 35% | “먼저 X를 찾아야 함” |
| 관찰 해석 | 28% | “이 정보에 따르면…” |
| 전략 수정 | 19% | “이 방법이 안 되니 다른 접근” |
| 결론 | 18% | “충분한 정보를 모았음” |
6.4 오류 회복 능력
시나리오: 검색이 실패한 경우
CoT (추론만):
질문: "X의 생년월일은?"
Thought: X는 유명인이다.
Thought: 유명인은 보통 1900-2000년에 태어난다.
Answer: 약 1950년 (추측)
→ 오류 회복 불가능
ReAct:
질문: "X의 생년월일은?"
Thought: X에 대해 검색해야 한다.
Action: search[X]
Observation: X에 대한 정보를 찾을 수 없음.
Thought: 검색어가 모호할 수 있다. 전체 이름으로 다시 검색.
Action: search[X full name]
Observation: X (1965년 3월 10일생)은...
→ 오류 회복 가능!
오류 회복 성공률: - CoT: 12% - ReAct: 67%
7 고급 ReAct 패턴
7.1 패턴 1: 계층적 ReAct
복잡한 작업을 하위 작업으로 분해하여 처리.
class HierarchicalReAct:
"""
계층적 ReAct Agent
고수준 계획 → 저수준 실행
"""
def __init__(self, api_key: str):
self.planner = ReActAgent(api_key) # 고수준 계획자
self.executor = ReActAgent(api_key) # 저수준 실행자
# 계획자용 특수 도구
self.register_planner_tools()
def register_planner_tools(self):
"""
계획자용 도구: 하위 작업 생성
"""
def create_subtask(task_description: str) -> str:
"""하위 작업 생성"""
return f"하위 작업 생성: {task_description}"
self.planner.register_tool(
name="create_subtask",
function=create_subtask,
description="복잡한 작업을 하위 작업으로 분해합니다."
)
def solve(self, complex_question: str) -> str:
"""
계층적 문제 해결
1. 계획자: 고수준 계획 수립
2. 실행자: 각 하위 작업 실행
3. 통합: 결과 종합
"""
print("🏗️ 계층적 ReAct 시작\n")
# Phase 1: 고수준 계획
print("=" * 80)
print("Phase 1: 계획 수립")
print("=" * 80)
planning_prompt = f"""다음 복잡한 질문을 해결하기 위한 계획을 세우세요.
질문: {complex_question}
단계별 하위 작업으로 분해하세요:
1."""
plan = self.planner.run(planning_prompt, max_steps=5, verbose=False)
# 하위 작업 추출
subtasks = self._extract_subtasks(plan)
print(f"\n📋 계획된 하위 작업:")
for i, task in enumerate(subtasks, 1):
print(f" {i}. {task}")
# Phase 2: 각 하위 작업 실행
print(f"\n{'=' * 80}")
print("Phase 2: 하위 작업 실행")
print("=" * 80)
subtask_results = []
for i, task in enumerate(subtasks, 1):
print(f"\n🔹 하위 작업 {i}: {task}")
print("-" * 80)
result = self.executor.run(task, max_steps=10, verbose=True)
subtask_results.append({
'task': task,
'result': result
})
# Phase 3: 결과 통합
print(f"\n{'=' * 80}")
print("Phase 3: 결과 통합")
print("=" * 80)
final_answer = self._synthesize_results(
complex_question,
subtask_results
)
return final_answer
def _extract_subtasks(self, plan: str) -> List[str]:
"""계획에서 하위 작업 추출"""
import re
# "1. ...", "2. ..." 형식 찾기
subtasks = []
for line in plan.split('\n'):
match = re.match(r'^\d+\.\s*(.+)$', line.strip())
if match:
subtasks.append(match.group(1))
return subtasks
def _synthesize_results(
self,
question: str,
results: List[Dict]
) -> str:
"""하위 작업 결과 통합"""
results_str = "\n\n".join([
f"작업: {r['task']}\n결과: {r['result']}"
for r in results
])
synthesis_prompt = f"""다음 하위 작업 결과들을 종합하여 원래 질문에 답하세요.
원래 질문: {question}
하위 작업 결과들:
{results_str}
종합 답변:"""
# LLM 호출
import anthropic
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1500,
temperature=0,
messages=[{"role": "user", "content": synthesis_prompt}]
)
return message.content[0].text사용 예시:
hierarchical = HierarchicalReAct(api_key="your-api-key")
complex_question = """
2024년 노벨 물리학상 수상자들의 주요 업적을 비교하고,
현대 AI 발전에 미친 영향을 분석하세요.
"""
answer = hierarchical.solve(complex_question)
# 출력:
# 📋 계획된 하위 작업:
# 1. 2024년 노벨 물리학상 수상자 확인
# 2. 각 수상자의 주요 업적 조사
# 3. 업적 간 공통점과 차이점 비교
# 4. AI 발전에 미친 구체적 영향 분석
# 5. 종합 결론 도출7.2 패턴 2: 반성적 ReAct (Reflexion)
실패에서 학습하여 전략을 개선.
class ReflexiveReAct:
"""
반성적 ReAct Agent
실패 → 반성 → 개선 → 재시도
"""
def __init__(self, api_key: str):
self.agent = ReActAgent(api_key)
self.memory = [] # 과거 시도 기록
def solve_with_reflection(
self,
question: str,
max_attempts: int = 3
) -> str:
"""
반성적 문제 해결
"""
print(f"🔄 반성적 ReAct 시작 (최대 {max_attempts}회 시도)\n")
for attempt in range(1, max_attempts + 1):
print(f"{'=' * 80}")
print(f"시도 {attempt}/{max_attempts}")
print(f"{'=' * 80}\n")
# 이전 실패 반성 추가
if self.memory:
reflection = self._generate_reflection()
print(f"💡 반성:")
print(f" {reflection}\n")
# 반성을 프롬프트에 포함
augmented_question = f"""{question}
이전 시도의 반성:
{reflection}
이를 고려하여 다시 시도하세요."""
else:
augmented_question = question
# 실행
try:
answer = self.agent.run(
augmented_question,
max_steps=15,
verbose=True
)
# 성공 여부 평가
is_correct = self._evaluate_answer(question, answer)
if is_correct:
print(f"\n✅ 시도 {attempt}에서 성공!")
return answer
else:
print(f"\n❌ 시도 {attempt} 실패")
# 실패 기록
self.memory.append({
'attempt': attempt,
'question': question,
'answer': answer,
'history': self.agent.history.copy()
})
except Exception as e:
print(f"\n⚠️ 오류 발생: {e}")
self.memory.append({
'attempt': attempt,
'question': question,
'error': str(e)
})
print(f"\n❌ {max_attempts}회 시도 후에도 해결 실패")
return "문제를 해결할 수 없습니다."
def _generate_reflection(self) -> str:
"""
이전 실패에서 반성 생성
"""
last_attempt = self.memory[-1]
reflection_prompt = f"""이전 시도가 실패했습니다. 무엇이 잘못되었는지 분석하세요.
실패한 시도:
{self._format_attempt(last_attempt)}
반성 (무엇이 잘못되었고, 어떻게 개선할 수 있는가?):"""
import anthropic
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=300,
temperature=0,
messages=[{"role": "user", "content": reflection_prompt}]
)
return message.content[0].text
def _format_attempt(self, attempt: Dict) -> str:
"""시도 기록 포맷팅"""
if 'error' in attempt:
return f"오류: {attempt['error']}"
history = attempt.get('history', [])
formatted = []
for step in history:
formatted.append(f"Thought: {step['thought']}")
formatted.append(f"Action: {step['action']}")
formatted.append(f"Observation: {step['observation']}")
formatted.append(f"\n최종 답변: {attempt.get('answer', 'N/A')}")
return "\n".join(formatted)
def _evaluate_answer(self, question: str, answer: str) -> bool:
"""
답변 정확성 평가
실제로는 더 정교한 평가 필요
"""
eval_prompt = f"""다음 답변이 질문에 올바르게 답했는지 평가하세요.
질문: {question}
답변: {answer}
올바른가? (yes/no):"""
import anthropic
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=10,
temperature=0,
messages=[{"role": "user", "content": eval_prompt}]
)
response = message.content[0].text.strip().lower()
return 'yes' in response실행 예시:
🔄 반성적 ReAct 시작 (최대 3회 시도)
================================================================================
시도 1/3
================================================================================
[... ReAct 실행 ...]
❌ 시도 1 실패
================================================================================
시도 2/3
================================================================================
💡 반성:
이전 시도에서는 검색 쿼리가 너무 일반적이었습니다.
더 구체적인 쿼리를 사용하고, 연도를 명시해야 합니다.
[... 개선된 ReAct 실행 ...]
✅ 시도 2에서 성공!
7.3 패턴 3: 멀티 에이전트 협력
여러 Agent가 협력하여 문제 해결.
class MultiAgentSystem:
"""
멀티 에이전트 협력 시스템
"""
def __init__(self, api_key: str):
# 전문화된 Agent들
self.researcher = self._create_researcher(api_key)
self.analyst = self._create_analyst(api_key)
self.writer = self._create_writer(api_key)
def _create_researcher(self, api_key: str) -> ReActAgent:
"""연구자 Agent: 정보 수집 전문"""
agent = ReActAgent(api_key)
# 검색 도구만 사용
register_wikipedia_tool(agent)
register_real_search_tool(agent)
return agent
def _create_analyst(self, api_key: str) -> ReActAgent:
"""분석가 Agent: 데이터 분석 전문"""
agent = ReActAgent(api_key)
# 계산, Python 실행 도구
register_python_exec_tool(agent)
return agent
def _create_writer(self, api_key: str) -> ReActAgent:
"""작가 Agent: 최종 작성 전문"""
agent = ReActAgent(api_key)
# 작성 도구만
# (finish 도구는 기본 등록됨)
return agent
def collaborate(self, task: str) -> str:
"""
협력적 문제 해결
1. Researcher: 정보 수집
2. Analyst: 데이터 분석
3. Writer: 최종 작성
"""
print("🤝 멀티 에이전트 협력 시작\n")
# Phase 1: 정보 수집
print("=" * 80)
print("Phase 1: Researcher - 정보 수집")
print("=" * 80)
research_task = f"다음 주제에 대한 정보를 수집하세요: {task}"
research_results = self.researcher.run(
research_task,
max_steps=10,
verbose=True
)
print(f"\n📚 수집된 정보:\n{research_results}\n")
# Phase 2: 데이터 분석
print("=" * 80)
print("Phase 2: Analyst - 데이터 분석")
print("=" * 80)
analysis_task = f"""다음 정보를 분석하세요:
수집된 정보:
{research_results}
원래 작업: {task}"""
analysis_results = self.analyst.run(
analysis_task,
max_steps=10,
verbose=True
)
print(f"\n📊 분석 결과:\n{analysis_results}\n")
# Phase 3: 최종 작성
print("=" * 80)
print("Phase 3: Writer - 최종 작성")
print("=" * 80)
writing_task = f"""다음 정보와 분석을 바탕으로 최종 답변을 작성하세요:
원래 작업: {task}
수집된 정보:
{research_results}
분석 결과:
{analysis_results}"""
final_answer = self.writer.run(
writing_task,
max_steps=5,
verbose=True
)
return final_answer8 Agent 아키텍처 설계
8.1 아키텍처 1: 단순 루프 (Simple Loop)
┌──────────────────────────┐
│ User Question │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ ReAct Loop │
│ ┌──────────────────┐ │
│ │ Thought │ │
│ │ ↓ │ │
│ │ Action │ │
│ │ ↓ │ │
│ │ Observation │ │
│ └──────────────────┘ │
│ ↓ │
│ (반복 또는 종료) │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ Final Answer │
└──────────────────────────┘
장점: 간단, 이해하기 쉬움 단점: 복잡한 작업에 한계
8.2 아키텍처 2: 계획-실행-반성 (Plan-Execute-Reflect)
┌──────────────────────────┐
│ User Question │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ Planner │
│ - 고수준 계획 수립 │
│ - 하위 작업 생성 │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ Executor │
│ - 각 하위 작업 실행 │
│ - ReAct Loop │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ Reflector │
│ - 결과 평가 │
│ - 재계획 필요 시 반복 │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ Final Answer │
└──────────────────────────┘
장점: 복잡한 작업 처리 가능, 오류 회복 단점: 더 많은 LLM 호출, 복잡도 증가
8.3 아키텍처 3: 도구 중심 (Tool-Centric)
class ToolCentricAgent:
"""
도구 중심 Agent
각 도구가 언제 사용되어야 하는지 명시적으로 정의
"""
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = "claude-sonnet-4-20250514"
# 도구와 사용 조건
self.tools = {
'search': {
'function': self._search,
'when_to_use': '최신 정보나 사실 확인이 필요할 때',
'examples': ['누가 2024년에 X를 했나?', 'Y의 최근 소식은?']
},
'calculate': {
'function': self._calculate,
'when_to_use': '수학 계산이 필요할 때',
'examples': ['15 + 23은?', '면적 계산']
},
'code_exec': {
'function': self._execute_code,
'when_to_use': '복잡한 데이터 처리나 알고리즘 실행이 필요할 때',
'examples': ['리스트 정렬', '통계 계산']
}
}
def select_tool(self, question: str, context: str = "") -> str:
"""
질문과 컨텍스트를 바탕으로 적절한 도구 선택
"""
# 도구 설명 생성
tools_desc = ""
for name, info in self.tools.items():
tools_desc += f"""
도구: {name}
사용 시기: {info['when_to_use']}
예시: {', '.join(info['examples'])}
"""
# 도구 선택 프롬프트
prompt = f"""다음 질문을 해결하기 위해 어떤 도구를 사용해야 할까요?
사용 가능한 도구:
{tools_desc}
질문: {question}
{f'현재 컨텍스트: {context}' if context else ''}
가장 적절한 도구 이름만 답하세요 (또는 'none' if 도구 불필요):"""
message = self.client.messages.create(
model=self.model,
max_tokens=50,
temperature=0,
messages=[{"role": "user", "content": prompt}]
)
tool_name = message.content[0].text.strip().lower()
return tool_name if tool_name in self.tools else None
def run(self, question: str, max_steps: int = 10) -> str:
"""
도구 중심 실행
"""
context = ""
for step in range(1, max_steps + 1):
print(f"\n📍 Step {step}")
# 도구 선택
tool_name = self.select_tool(question, context)
if tool_name is None or tool_name == 'none':
# 도구 불필요 - 직접 답변
print("💬 도구 불필요 - 직접 답변")
return self._generate_final_answer(question, context)
print(f"🔧 선택된 도구: {tool_name}")
# 도구 입력 생성
tool_input = self._generate_tool_input(question, tool_name, context)
print(f"📥 입력: {tool_input}")
# 도구 실행
tool_func = self.tools[tool_name]['function']
observation = tool_func(tool_input)
print(f"👁️ 관찰: {observation}")
# 컨텍스트 업데이트
context += f"\n[{tool_name}] {observation}"
# 완료 조건 체크
if self._is_sufficient(question, context):
print("\n✅ 충분한 정보 수집 완료")
return self._generate_final_answer(question, context)
return self._generate_final_answer(question, context)
def _generate_tool_input(
self,
question: str,
tool_name: str,
context: str
) -> str:
"""도구 입력 생성"""
prompt = f"""질문: {question}
도구: {tool_name}
{f'현재까지 수집한 정보: {context}' if context else ''}
이 도구에 어떤 입력을 제공해야 할까요? (입력만 답하세요):"""
message = self.client.messages.create(
model=self.model,
max_tokens=100,
temperature=0,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text.strip()
def _is_sufficient(self, question: str, context: str) -> bool:
"""충분한 정보를 모았는지 확인"""
if not context:
return False
prompt = f"""질문: {question}
수집한 정보:
{context}
이 정보로 질문에 충분히 답할 수 있습니까? (yes/no):"""
message = self.client.messages.create(
model=self.model,
max_tokens=10,
temperature=0,
messages=[{"role": "user", "content": prompt}]
)
response = message.content[0].text.strip().lower()
return 'yes' in response
def _generate_final_answer(self, question: str, context: str) -> str:
"""최종 답변 생성"""
prompt = f"""질문: {question}
수집한 정보:
{context}
이를 바탕으로 질문에 답하세요:"""
message = self.client.messages.create(
model=self.model,
max_tokens=1000,
temperature=0,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
def _search(self, query: str) -> str:
"""검색 구현"""
return f"[검색 결과: {query}]"
def _calculate(self, expression: str) -> str:
"""계산 구현"""
try:
return str(eval(expression))
except:
return "계산 오류"
def _execute_code(self, code: str) -> str:
"""코드 실행 구현"""
return f"[코드 실행: {code}]"9 도구 선택 전략
9.1 전략 1: 규칙 기반 (Rule-Based)
def rule_based_tool_selection(question: str) -> str:
"""
규칙 기반 도구 선택
키워드 매칭
"""
question_lower = question.lower()
# 검색 키워드
search_keywords = ['누구', 'who', '언제', 'when', '최신', 'latest', '뉴스']
if any(kw in question_lower for kw in search_keywords):
return 'search'
# 계산 키워드
calc_keywords = ['계산', 'calculate', '+', '-', '*', '/', '=']
if any(kw in question_lower for kw in calc_keywords):
return 'calculate'
# 코드 실행 키워드
code_keywords = ['정렬', 'sort', '필터', 'filter', '알고리즘']
if any(kw in question_lower for kw in code_keywords):
return 'code_exec'
# 기본값
return 'none'장점: 빠름, 예측 가능 단점: 유연성 부족, 모호한 경우 처리 어려움
9.2 전략 2: LLM 기반 (LLM-Based)
def llm_based_tool_selection(
question: str,
tools: Dict,
context: str = ""
) -> str:
"""
LLM을 사용한 도구 선택
더 지능적이지만 비용 증가
"""
tools_desc = "\n".join([
f"{name}: {info['description']}"
for name, info in tools.items()
])
prompt = f"""질문을 해결하기 위해 어떤 도구가 필요한가요?
도구 목록:
{tools_desc}
질문: {question}
{f'컨텍스트: {context}' if context else ''}
가장 적절한 도구 이름 (하나만):"""
# LLM 호출
# ...
return tool_name장점: 유연함, 컨텍스트 고려 단점: 비용, 지연 시간
9.3 전략 3: 하이브리드 (Hybrid)
def hybrid_tool_selection(
question: str,
tools: Dict,
context: str = ""
) -> str:
"""
규칙 + LLM 하이브리드
1. 규칙으로 빠른 선택 시도
2. 불확실하면 LLM 사용
"""
# Step 1: 규칙 기반 시도
rule_result = rule_based_tool_selection(question)
# Step 2: 확신도 평가
confidence = evaluate_rule_confidence(question, rule_result)
if confidence > 0.8:
# 높은 확신 → 규칙 결과 사용
return rule_result
else:
# 낮은 확신 → LLM 사용
return llm_based_tool_selection(question, tools, context)장점: 비용 효율적, 높은 정확도 단점: 복잡도 증가
10 실무 적용 사례
10.1 사례 1: 고객 지원 Agent
class CustomerSupportAgent:
"""
고객 지원 ReAct Agent
"""
def __init__(self, api_key: str):
self.agent = ReActAgent(api_key)
# 고객 지원 특화 도구
self.register_support_tools()
def register_support_tools(self):
"""고객 지원 도구 등록"""
# 주문 조회
def lookup_order(order_id: str) -> str:
"""주문 정보 조회"""
# 데이터베이스 쿼리
return f"주문 #{order_id}: 배송 중 (예상 도착: 2일 후)"
self.agent.register_tool(
name="lookup_order",
function=lookup_order,
description="주문 번호로 주문 정보를 조회합니다."
)
# 환불 정책 조회
def get_refund_policy(category: str) -> str:
"""환불 정책 조회"""
policies = {
'electronics': '구매 후 14일 이내 미개봉 상태',
'clothing': '구매 후 30일 이내, 택 부착',
'food': '환불 불가 (단, 불량 시 교환)'
}
return policies.get(category, '일반 정책: 7일 이내')
self.agent.register_tool(
name="refund_policy",
function=get_refund_policy,
description="상품 카테고리별 환불 정책을 조회합니다."
)
# 티켓 생성
def create_ticket(issue: str) -> str:
"""고객 지원 티켓 생성"""
ticket_id = f"TK-{random.randint(1000, 9999)}"
return f"티켓 {ticket_id} 생성 완료. 담당자가 24시간 내 연락드립니다."
self.agent.register_tool(
name="create_ticket",
function=create_ticket,
description="복잡한 문제에 대해 지원 티켓을 생성합니다."
)
def handle_inquiry(self, customer_message: str) -> str:
"""고객 문의 처리"""
return self.agent.run(customer_message, max_steps=10, verbose=True)
# 사용
support = CustomerSupportAgent(api_key="your-api-key")
inquiry = "주문 번호 12345의 배송 상태를 알려주세요. 그리고 마음에 안 들면 환불 가능한가요?"
response = support.handle_inquiry(inquiry)
# 실행:
# Thought 1: 먼저 주문 상태를 확인해야 합니다.
# Action 1: lookup_order[12345]
# Observation 1: 주문 #12345: 배송 중 (예상 도착: 2일 후)
#
# Thought 2: 환불 정책을 확인해야 합니다. 상품 카테고리가 필요합니다.
# Action 2: lookup_order[12345]
# Observation 2: ... (카테고리: electronics)
#
# Thought 3: 전자제품 환불 정책을 조회합니다.
# Action 3: refund_policy[electronics]
# Observation 3: 구매 후 14일 이내 미개봉 상태
#
# Thought 4: 충분한 정보를 모았습니다.
# Action 4: finish[주문 #12345는 현재 배송 중이며 2일 후 도착 예정입니다. 전자제품은 구매 후 14일 이내 미개봉 상태로 환불 가능합니다.]10.2 사례 2: 데이터 분석 Agent
class DataAnalysisAgent:
"""
데이터 분석 ReAct Agent
"""
def __init__(self, api_key: str, data_path: str):
self.agent = ReActAgent(api_key)
self.data_path = data_path
# 데이터 분석 도구
self.register_analysis_tools()
def register_analysis_tools(self):
"""데이터 분석 도구 등록"""
# 데이터 로드
def load_data(filename: str) -> str:
"""CSV 데이터 로드"""
import pandas as pd
try:
df = pd.read_csv(f"{self.data_path}/{filename}")
info = f"""데이터 로드 완료:
- 행 수: {len(df)}
- 열 수: {len(df.columns)}
- 열 이름: {', '.join(df.columns)}
- 처음 5행:
{df.head().to_string()}"""
# 전역 변수에 저장 (다른 도구에서 사용)
globals()['current_df'] = df
return info
except Exception as e:
return f"데이터 로드 실패: {e}"
self.agent.register_tool(
name="load_data",
function=load_data,
description="CSV 파일을 로드합니다. 입력: 파일명"
)
# 통계 계산
def compute_statistics(column: str) -> str:
"""열의 기술 통계"""
df = globals().get('current_df')
if df is None:
return "먼저 데이터를 로드하세요."
if column not in df.columns:
return f"'{column}' 열이 존재하지 않습니다."
stats = df[column].describe()
return f"{column} 통계:\n{stats.to_string()}"
self.agent.register_tool(
name="statistics",
function=compute_statistics,
description="특정 열의 기술 통계를 계산합니다. 입력: 열 이름"
)
# 데이터 필터링
def filter_data(condition: str) -> str:
"""조건에 맞는 데이터 필터링"""
df = globals().get('current_df')
if df is None:
return "먼저 데이터를 로드하세요."
try:
# 조건 평가 (주의: 실제로는 더 안전한 방법 필요)
filtered = df.query(condition)
result = f"""필터링 결과:
- 매칭 행 수: {len(filtered)}
- 처음 5행:
{filtered.head().to_string()}"""
return result
except Exception as e:
return f"필터링 실패: {e}"
self.agent.register_tool(
name="filter",
function=filter_data,
description="조건에 맞는 데이터를 필터링합니다. 입력: pandas query 조건"
)
def analyze(self, question: str) -> str:
"""데이터 분석 실행"""
return self.agent.run(question, max_steps=15, verbose=True)
# 사용
analyst = DataAnalysisAgent(
api_key="your-api-key",
data_path="./data"
)
question = "sales.csv를 로드하고, 매출이 1000 이상인 제품들의 평균 가격을 구해주세요."
answer = analyst.analyze(question)
# 실행:
# Thought 1: 먼저 sales.csv를 로드해야 합니다.
# Action 1: load_data[sales.csv]
# Observation 1: 데이터 로드 완료: 행 수: 150, 열: product, price, sales...
#
# Thought 2: 매출이 1000 이상인 데이터를 필터링합니다.
# Action 2: filter[sales >= 1000]
# Observation 2: 필터링 결과: 매칭 행 수: 45...
#
# Thought 3: 이제 price 열의 통계를 계산합니다.
# Action 3: statistics[price]
# Observation 3: price 통계: mean: 1250.5, std: 320.2...
#
# Thought 4: 답변 가능합니다.
# Action 4: finish[매출이 1000 이상인 제품은 45개이며, 평균 가격은 $1,250.5입니다.]10.3 사례 3: 연구 보조 Agent
class ResearchAssistantAgent:
"""
연구 보조 Agent
논문 검색, 요약, 비교
"""
def __init__(self, api_key: str):
self.agent = ReActAgent(api_key)
# 연구 도구
self.register_research_tools()
def register_research_tools(self):
"""연구 도구 등록"""
# arXiv 검색
def search_arxiv(query: str) -> str:
"""arXiv 논문 검색"""
import arxiv
search = arxiv.Search(
query=query,
max_results=5,
sort_by=arxiv.SortCriterion.Relevance
)
results = []
for paper in search.results():
results.append(f"""제목: {paper.title}
저자: {', '.join([a.name for a in paper.authors])}
게시일: {paper.published.date()}
요약: {paper.summary[:200]}...
URL: {paper.entry_id}
""")
return "\n\n".join(results)
self.agent.register_tool(
name="search_papers",
function=search_arxiv,
description="arXiv에서 논문을 검색합니다. 입력: 검색 쿼리"
)
# 논문 다운로드 및 요약
def summarize_paper(arxiv_id: str) -> str:
"""논문 요약"""
import arxiv
paper = next(arxiv.Search(id_list=[arxiv_id]).results())
# 전문 다운로드 및 요약 (간략화)
summary = f"""논문: {paper.title}
주요 내용:
{paper.summary}
기여:
(여기서는 초록 기반, 실제로는 전문 분석 필요)"""
return summary
self.agent.register_tool(
name="summarize",
function=summarize_paper,
description="arXiv 논문을 요약합니다. 입력: arXiv ID"
)
def research(self, research_question: str) -> str:
"""연구 질문에 답변"""
return self.agent.run(research_question, max_steps=20, verbose=True)
# 사용
researcher = ResearchAssistantAgent(api_key="your-api-key")
question = "ReAct 프롬프팅에 대한 최근 논문을 찾고, 핵심 아이디어를 요약해주세요."
answer = researcher.research(question)11 한계점 및 해결책
11.1 한계 1: 무한 루프
문제: Agent가 같은 행동을 반복
# 나쁜 예
Thought 1: X를 검색해야 함
Action 1: search[X]
Observation 1: X에 대한 정보...
Thought 2: X를 검색해야 함 ← 반복!
Action 2: search[X]
Observation 2: X에 대한 정보...
(무한 반복...)해결책: 중복 행동 감지 및 방지
class LoopPreventionReAct(ReActAgent):
"""
무한 루프 방지 ReAct
"""
def __init__(self, api_key: str):
super().__init__(api_key)
self.action_history = [] # 행동 이력
def execute_action(self, tool_name: str, tool_input: str) -> str:
"""
행동 실행 (중복 체크 포함)
"""
action_key = f"{tool_name}[{tool_input}]"
# 중복 체크
if action_key in self.action_history:
# 이미 실행한 행동
count = self.action_history.count(action_key)
if count >= 2:
# 3번 이상 반복 → 경고
return f"경고: 이 행동을 이미 {count}번 실행했습니다. 다른 접근을 시도하세요."
# 행동 기록
self.action_history.append(action_key)
# 실행
return super().execute_action(tool_name, tool_input)11.2 한계 2: 도구 선택 오류
문제: 잘못된 도구 선택
해결책: 도구 선택 검증
11.3 한계 3: 긴 실행 시간
문제: 많은 단계로 인한 지연
해결책: 조기 종료 및 최적화
class OptimizedReAct(ReActAgent):
"""
최적화된 ReAct
"""
def run(
self,
question: str,
max_steps: int = 10,
timeout: int = 60 # 초
) -> str:
"""
타임아웃 포함 실행
"""
import time
start_time = time.time()
for step in range(1, max_steps + 1):
# 타임아웃 체크
if time.time() - start_time > timeout:
print(f"⏰ 타임아웃 ({timeout}초)")
return self._emergency_answer(question)
# 정상 실행
# ...
def _emergency_answer(self, question: str) -> str:
"""
타임아웃 시 긴급 답변
지금까지 수집한 정보로 최선의 답변
"""
context = self._format_history()
prompt = f"""시간 제약으로 인해 조사를 완료하지 못했습니다.
지금까지 수집한 정보로 최선의 답변을 제공하세요.
질문: {question}
수집한 정보:
{context}
답변 (정보가 불완전함을 명시):"""
# LLM 호출
# ...
return answer11.4 한계 4: 비용
문제: 많은 LLM 호출로 인한 높은 비용
해결책: 비용 최적화 전략
class CostOptimizedReAct:
"""
비용 최적화 ReAct
"""
def __init__(
self,
api_key: str,
budget_per_query: float = 0.10 # 쿼리당 $0.10
):
self.agent = ReActAgent(api_key)
self.budget = budget_per_query
self.cost_per_call = 0.01 # 예상 비용
def run_with_budget(self, question: str) -> str:
"""
예산 제약 하 실행
"""
max_steps = int(self.budget / self.cost_per_call)
print(f"💰 예산: ${self.budget}")
print(f" 최대 단계: {max_steps}\n")
# 실행
answer = self.agent.run(
question,
max_steps=max_steps,
verbose=True
)
# 실제 비용 계산
actual_cost = len(self.agent.history) * self.cost_per_call
print(f"\n💵 실제 비용: ${actual_cost:.3f}")
return answer12 베스트 프랙티스
12.1 1. 명확한 도구 설명
12.2 2. 도구 결과 포맷팅
12.3 3. 오류 처리
12.4 4. 진행 상황 로깅
class LoggingReAct(ReActAgent):
"""
상세한 로깅이 있는 ReAct
"""
def __init__(self, api_key: str, log_file: str = "react.log"):
super().__init__(api_key)
import logging
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(__name__)
def execute_action(self, tool_name: str, tool_input: str) -> str:
"""로깅 포함 행동 실행"""
self.logger.info(f"도구 호출: {tool_name}[{tool_input}]")
try:
result = super().execute_action(tool_name, tool_input)
self.logger.info(f"도구 결과: {result[:100]}...")
return result
except Exception as e:
self.logger.error(f"도구 실행 오류: {e}")
raise12.5 5. 테스트
def test_react_agent():
"""
ReAct Agent 테스트
"""
agent = ReActAgent(api_key="test-key")
# 테스트 케이스
test_cases = [
{
'question': "2 + 2는?",
'expected_tool': 'calculate',
'expected_answer': '4'
},
{
'question': "2024년 노벨상 수상자는?",
'expected_tool': 'search',
'expected_answer_contains': '노벨'
}
]
for i, test in enumerate(test_cases, 1):
print(f"\n테스트 {i}: {test['question']}")
answer = agent.run(test['question'], max_steps=5, verbose=False)
# 검증
if 'expected_answer' in test:
assert answer == test['expected_answer'], f"기대: {test['expected_answer']}, 실제: {answer}"
if 'expected_answer_contains' in test:
assert test['expected_answer_contains'] in answer, f"'{test['expected_answer_contains']}'가 답변에 없음"
# 도구 사용 확인
used_tools = [step['action'].split('[')[0] for step in agent.history]
assert test['expected_tool'] in used_tools, f"기대 도구 {test['expected_tool']} 사용 안 됨"
print("✅ 통과")13 다른 Agent 프레임워크 비교
13.1 ReAct vs LangChain Agents
| 특성 | ReAct | LangChain |
|---|---|---|
| 구조 | Thought-Action-Observation | Tool → Output |
| 추론 가시성 | 높음 (명시적 Thought) | 낮음 (암묵적) |
| 구현 복잡도 | 중간 | 낮음 (추상화) |
| 커스터마이징 | 쉬움 | 제한적 |
| 디버깅 | 쉬움 | 어려움 |
13.2 ReAct vs AutoGPT
| 특성 | ReAct | AutoGPT |
|---|---|---|
| 자율성 | 중간 (사람이 목표 설정) | 높음 (자체 목표) |
| 안정성 | 높음 | 낮음 |
| 비용 | 예측 가능 | 높음 (무제한 실행) |
| 적용 범위 | 특정 작업 | 범용 |
14 정리 및 시리즈 종합
14.1 ReAct 핵심 요약
ReAct (Reasoning + Acting): - Thought-Action-Observation 사이클 - 외부 도구 통합으로 LLM 능력 확장 - HotpotQA에서 CoT 대비 +19% 개선 (Self-Consistency 결합 시) - 오류 회복 능력 67% (CoT: 12%)
주요 구성 요소: 1. Thought: 다음 단계에 대한 추론 2. Action: 도구 호출 3. Observation: 결과 관찰 4. Tools: 검색, 계산, API 호출 등
언제 사용할 것인가: - ✅ 외부 정보 필요 (검색, DB) - ✅ 복잡한 다단계 문제 - ✅ 동적 환경 (상황에 따라 전략 변경) - ✅ 도구 통합 필요
언제 사용하지 말 것인가: - ❌ 단순 질문 (CoT로 충분) - ❌ 비용 매우 민감 - ❌ 실시간 응답 필수 (지연 시간)
14.2 고급 프롬프트 엔지니어링 시리즈 전체 정리
Part 1-10 핵심 기법 요약:
| 기법 | 핵심 아이디어 | 주요 개선 | 권장 사용 |
|---|---|---|---|
| Generate Knowledge | 배경 지식 먼저 생성 | +8% | 상식 추론 |
| Prompt Chaining | 작업 분해 및 연결 | 복잡도 관리 | 다단계 작업 |
| Tree of Thoughts | 트리 탐색 + 백트래킹 | +70% (Game24) | 최적해 필요 |
| RAG | 외부 지식 검색 | 할루시네이션 감소 | 지식 기반 작업 |
| APE | 프롬프트 자동 생성 | +4.5% | 대량 최적화 |
| OPRO | LLM이 LLM 최적화 | +8.4% | 프롬프트 발견 |
| Active-Prompt | 불확실한 예시 선별 | 비용 50% 절감 | Few-shot 효율 |
| Directional Stimulus | 힌트로 유도 | +14% | 도메인 특화 |
| ReAct | 추론 + 행동 결합 | +19% | 도구 사용 |
기법 선택 가이드:
질문 유형에 따른 선택:
단순 사실 질문
→ Zero-shot 또는 Few-shot
복잡한 추론
→ CoT 또는 Tree of Thoughts
최신 정보 필요
→ RAG 또는 ReAct
도메인 특화
→ Directional Stimulus 또는 Active-Prompt
프롬프트 최적화
→ APE 또는 OPRO
다단계 작업
→ Prompt Chaining 또는 ReAct
14.3 실무 적용 로드맵
Week 1-2: 기초 - CoT, Few-shot 마스터 - 기본 프롬프트 엔지니어링 원칙
Week 3-4: 중급 - Prompt Chaining 구현 - RAG 시스템 구축
Week 5-6: 고급 - ReAct Agent 개발 - 도메인 특화 최적화
Week 7-8: 프로덕션 - 성능 최적화 - 모니터링 및 A/B 테스트
14.4 미래 방향
현재 트렌드: 1. Agentic AI: ReAct 기반 자율 Agent 2. Multi-modal: 텍스트 + 이미지 + 비디오 3. Long-context: 100K+ 토큰 윈도우 4. Tool Learning: 도구 자동 발견 및 학습
기대되는 발전: - 더 스마트한 도구 선택 - 자가 개선 Agent - 멀티 Agent 협업 프레임워크 - 저비용 고효율 추론
15 참고문헌
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. (2022). ReAct: Synergizing reasoning and acting in language models. arXiv preprint arXiv:2210.03629.
Schick, T., et al. (2023). Toolformer: Language models can teach themselves to use tools. arXiv preprint arXiv:2302.04761.
Shinn, N., et al. (2023). Reflexion: Language agents with verbal reinforcement learning. arXiv preprint arXiv:2303.11366.
16 추가적 기법
- Prompt Compression
- LLMLingua, AutoCompressor
- 긴 프롬프트를 압축
- In-Context Learning 변형
- Meta-Prompting: 프롬프트가 프롬프트를 생성
- Instruction Induction: 예시에서 instruction 추론
- Rephrase and Respond (RaR): 질문 재구성 후 답변
- Multimodal Prompting
- Vision-Language Prompting: 이미지+텍스트
- Audio Prompting: 음성 입력
- Video Understanding: 비디오 분석