AI 에이전트: 필수 3가지 패턴 마스터하기 (ReAct). 파트 2/3
Source: Dev.to
번역을 진행하려면 번역하고자 하는 전체 텍스트(본문)를 제공해 주시겠어요?
본문을 주시면 원본 서식과 코드 블록을 그대로 유지하면서 한국어로 번역해 드리겠습니다.
기사 – 파트 1
이 패턴들의 코드는 GitHub에 있습니다. → [Repo]
패턴 “Tool Using” (레벨 1)으로 AI에게 외부 세계를 “만질” 수 있는 “손”을 줬다면, ReAct로는 “기능적인 뇌”를 설치하는 것입니다.
레벨 1을 기억하자
모든 것이 매우 선형적이었습니다:
Estímulo → Respuesta
당신이 데이터를 요청하면, 에이전트가 도구를 실행하고 끝났습니다. 의심도 계획도 없었습니다.
하지만 현실 세계는 무질서합니다.
- 질문이 한 번의 클릭으로 해결되지 않을 때는 어떻게 될까요?
- 데이터를 조사하고, 비교하고, 그 후 계산을 해야 할 때는 어떻게 할까요?
그러한 경우 선형 모델은 실패하고 ReAct가 등장합니다 (2022년 Princeton과 Google 연구진이 제안). 오늘날 이것은 사실상 업계 표준입니다.
ReAct = Reasoning + Acting (Razonar + Actuar)
ReAct를 다르게 만드는 것은 무엇인가?
도구를 사용한다는 점만으로는 충분하지 않습니다 (그것은 이미 가지고 있었습니다).
LLM에게 내부 독백—즉 “Thought Trace”—을 유지하도록 강제합니다.
무작위로 답변을 내뱉는 대신, 에이전트는 인지 루프에 들어갑니다:
Pensar → Actuar → Observar → Repetir
에이전트는 로그에서 문자 그대로 “스스로와 대화”합니다.
방금 전 발견한 내용을 기반으로 다음 단계를 계획하고, 예상대로 진행되지 않을 경우 즉시 방향을 수정할 수 있습니다.
단계별 예시
사용자 질문:
“프랑스 현 대통령의 나이의 제곱근은 무엇인가요?”
| 반복 | 생각 | 행동 | 관찰 |
|---|---|---|---|
| 1 | “음, 대통령의 나이를 전혀 몰라요. 사실 현재 대통령이 누군지도 모릅니다. 먼저 그걸 찾아보죠.” | search_tool("Presidente de Francia actual") | “Emmanuel Macron.” |
| 2 | “좋아요, 이름은 알겠어요: 마크론. 이제 그의 나이가 필요합니다.” | search_tool("Emmanuel Macron edad") | “47세.” |
| 3 | “47을 얻었어요. 사용자는 이 값의 제곱근을 원했습니다.” | calculator_tool("sqrt(47)") | “6.8556…” |
최종 답변:
“에마뉘엘 마크론(47세)의 나이의 제곱근은 약 6.85입니다.”
마법이 보이시나요? 3단계는 2단계를 먼저 발견하지 않으면 불가능했습니다. 에이전트가 ‘실마리를 연결’하고 있었습니다.
Agno를 사용한 ReAct 패턴 구현
다음은 생각 → 행동 → 관찰 → 반복 논리를 따르는 에이전트를 만드는 전체 Python 코드입니다.
import os
import sys
import logging
import traceback
from typing import List, Optional
from dotenv import load_dotenv, find_dotenv
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.tavily import TavilyTools
# ----------------------------------------------------------------------
# 1. Configuración global de logging y manejo de excepciones
# ----------------------------------------------------------------------
LOG_DIR = os.path.join(os.path.dirname(__file__), "log")
LOG_FILE = os.path.join(LOG_DIR, "logs.txt")
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_FILE, encoding="utf-8"),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
def global_exception_handler(exctype, value, tb):
"""Captura excepciones no manejadas y las registra."""
error_msg = "".join(traceback.format_exception(exctype, value, tb))
logger.error(f"Unhandled exception:\n{error_msg}")
sys.__excepthook__(exctype, value, tb)
sys.excepthook = global_exception_handler
# ----------------------------------------------------------------------
# 2. Carga de variables de entorno
# ----------------------------------------------------------------------
env_path = find_dotenv()
if env_path:
load_dotenv(env_path)
logger.info(f".env file loaded from: {env_path}")
else:
logger.warning(".env file not found")
# ----------------------------------------------------------------------
# 3. Definición de herramientas
# ----------------------------------------------------------------------
def calculate(expression: str) -> str:
"""
Resuelve una expresión matemática simple (suma, resta, multiplicación, división).
Útil para cálculos de años o diferencias de fechas.
Args:
expression (str): Expresión a evaluar, p.ej. "2024 - 1789".
Returns:
str: Resultado o mensaje de error.
"""
try:
# Permitir solo caracteres seguros
allowed_chars = "0123456789+-*/(). "
if all(c in allowed_chars for c in expression):
result = eval(expression) # noqa: S307 (uso controlado)
return f"Result: {result}"
else:
return "Error: Disallowed characters in expression."
except Exception as e:
return f"Error while calculating: {str(e)}"
# ----------------------------------------------------------------------
# 4. Configuración del agente Agno (Patrón ReAct)
# ----------------------------------------------------------------------
model_id = os.getenv("BASE_MODEL", "gpt-4o")
agent = Agent(
model=OpenAIChat(id=model_id),
tools=[TavilyTools(), calculate],
instructions=[
"You are a researcher using the ReAct (Reason + Act) method.",
"1. Think step‑by‑step about what information you need to answer the user's question.",
"2. Use the search tool (Tavily) to find specific dates, facts, or data.",
"3. Use the calculator ('calculate') for any mathematical operation or time calculation.",
"4. Do not guess historical information. If you don't have a piece of data, look it up.",
"5. Show your reasoning clearly: 'Thought:', 'Action:', 'Observation:'.",
"6. Continue investigating until you have a complete and verified answer."
],
)
# ----------------------------------------------------------------------
# 5. Interfaz de usuario
# ----------------------------------------------------------------------
def main():
logger.info("Starting Historical Detective Agent (ReAct)...")
print("--- Historical Detective - ReAct Pattern ---")
print("Type 'exit' to quit.\n")
while True:
try:
user_input = input("Researcher, what is your question?: ")
if user_input.lower() == "exit":
logger.info("The user has ended the session.")
break
if not user_input.strip():
continue
logger.info(f"User query: {user_input}")
print("\nInvestigating...\n")
# Ejecutar el agente con el prompt del usuario
response = agent.run(user_input)
print("\n--- Answer ---")
print(response)
print("\n----------------\n")
except KeyboardInterrupt:
logger.info("Session interrupted by user (Ctrl+C).")
break
except Exception as e:
logger.error(f"Error during processing: {e}")
if __name__ == "__main__":
main()
이 스크립트가 하는 일
- Logging → 모든 활동을
log/logs.txt에 저장하고 콘솔에도 표시합니다. - 예외 처리 → 예기치 않은 오류를 포착하고 기록합니다.
- 도구 →
- 웹 검색을 위한
TavilyTools. - 간단한 산술 연산을 위한
calculate.
- 웹 검색을 위한
- ReAct 에이전트 → 모델에게 다음을 강제하는 지침을 따릅니다:
- 단계별로 생각하기.
- 행동하기 (검색하거나 계산하기).
- 결과를 관찰하기.
- 완전하고 검증된 답변을 얻을 때까지 반복하기.
- 인터페이스 → 사용자가 질문을 입력하고 에이전트가 답변을 반환하는 콘솔 루프이며, 내부 프로세스는 로그에 표시됩니다.
주의:
BASE_MODEL변수(예:gpt-4o)가 포함된.env파일을 반드시 준비하세요.- 스크립트를 실행하기 전에 필요한 종속성(
agno,python-dotenv등)을 설치하세요.
이제 ReAct 패턴을 실험해 보고, 에이전트가 복잡한 질문을 해결하기 위해 “스스로 대화”하는 모습을 관찰할 수 있습니다.
nt.print_response(user_input, stream=True, show_tool_calls=True)
print("\n")
except KeyboardInterrupt:
logger.info("Keyboard interrupt detected.")
break
except Exception as e:
logger.error(f"Error in main loop: {str(e)}")
print(f"\nAn error occurred: {e}")
if __name__ == "__main__":
main()
코드에서 ReAct는 어디에 있나요?
생각 (Think): 에이전트의 instructions에 정의됩니다.
행동 (Act): 에이전트는 tools에 정의된 외부 기능을 가지고 있습니다.
관찰 (Observe): 프레임워크(Agno)가 도구를 실행하고 결과를 에이전트에게 반환합니다.
반복 (Repeat): 에이전트가 충분한 정보를 가질 때까지 루프가 계속됩니다.
왜 우리는 이 패턴을 이렇게 좋아할까? (Pros)
-
“다중 홉”(Multi‑hop) 문제를 해결한다
A가 B로, B가 C로 이어지는 질문에 답할 수 있는 능력이다. -
“자동 치유”(Self‑Healing) 능력
이것은 매우 중요하다. 예를 들어 “Macron의 나이”를 검색했는데 Google이 실패했다고 해보자. 일반 스크립트는 바로 중단될 것이다. ReAct 에이전트는 이렇게 생각한다: “아, 검색이 실패했네. 그럼 그의 생년월일을 찾아서 직접 나이를 계산하겠다”. -
블랙 박스와 작별
개발자는 Thoughts를 읽을 수 있다. 에이전트가 계산기를 사용하기로 결정한 이유를 정확히 알 수 있다. -
거짓말 감소 (환각)
각 단계를 실제 관찰(Observation)에 기반하도록 강제하면 데이터 조작이 훨씬 어려워진다. -
느리다 (지연)
ReAct는 순차적이다. 3번 생각한다는 것은 LLM을 3번 호출하고 도구를 실행한다는 의미다. 10~30초 정도 대기해야 한다. -
API 청구서 (비용)
그 “내부 독백”은 토큰을 무한히 소모한다. 컨텍스트 히스토리가 금방 가득 찬다. -
집착 위험
가끔 루프에 빠진다: “X를 찾음 → 없음 → 다시 X를 찾아야 한다고 생각 → X를 찾음…”. -
섬세한 프롬프트
모델이 언제 생각을 멈추고 답을 줄지 알 수 있도록 시스템 Prompt를 아주 정교하게 조정해야 한다.
프로덕션을 위한 모범 사례 (Agno, LangChain 등)
-
제한을 두세요 (Max Iterations)
항상 반복 횟수 제한을 설정하세요 (예: 10 단계). 10 단계 안에 문제를 해결하지 못한다면 100 단계에서도 해결되지 않을 것이며, 단지 비용만 낭비하게 됩니다. -
침묵을 가르치세요 (Stop Sequences)
기술적으로 LLM은 Action을 실행한 직후 작성을 멈춰야 합니다. 그 지점에서 멈추지 않으면 스스로 Observation을 만들어내기 시작합니다 (도구의 결과를 상상함). 최신 프레임워크는 보통 이를 처리하지만, 직접 확인하세요. -
쓰레기를 정리하세요 (컨텍스트 관리)
긴 대화에서는 오래된 사고 흔적을 삭제하고 최종 답변만 남겨두세요. 그렇지 않으면 에이전트가 곧 메모리 “RAM”(컨텍스트 윈도우)를 잃게 됩니다.
ReAct 워크플로우 예시
-
프로그래밍 에이전트 (Devin 또는 Copilot 유형)
“코드를 작성 → 실행 → 오류 확인 → 해결 방법 생각 → 다시 작성”. -
레벨 2 기술 지원
“티켓을 읽음 → 서버 상태 확인 → 사용자 로그 확인 → 데이터 교차 → 응답”. -
재무 분석가
“현재 가격 검색 → 최신 뉴스 검색 → 과거 데이터와 비교 → 추천 생성”.
행복한 코딩! 🤖