ARCH · REACT

ReAct 심화

🔄 TAO 루프 내부 구조 ⚙️ LangGraph 구현 🛡️ 무한 루프 방지

ReAct(Reasoning + Acting)의 Thought-Action-Observation 루프를 내부에서 분해하고, LangGraph로 커스터마이징 가능한 ReAct를 구현합니다.

ReAct 루프 내부 구조

ReAct는 LLM이 추론(Thought)행동(Action)관찰(Observation)을 반복하며 문제를 해결하는 패러다임입니다. 단순해 보이지만, 각 단계에서 정밀한 프롬프트 엔지니어링과 상태 관리가 필요합니다.

❓ 사용자 질문 💭 THOUGHT LLM 내부 추론 — 다음 행동 결정 완료? YES ✅ Final Answer NO ⚡ ACTION 도구 호출 — 함수명 + 파라미터 구조화 🔧 도구 실행 (외부 API, DB, 계산기 등) OBSERVATION 그림 1. ReAct Thought-Action-Observation 루프 — 완료 조건 판단이 핵심

LangGraph로 커스텀 ReAct 구현

create_react_agent는 편리하지만 커스터마이징이 제한됩니다. LangGraph StateGraph로 직접 구현하면 각 노드에 로깅, 검증, 캐싱을 추가할 수 있습니다.

pythoncustom_react.py — 커스텀 ReAct 에이전트
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from typing import TypedDict, Annotated
from operator import add
import json

MAX_ITERATIONS = 10  # 무한 루프 방지 상한

class AgentState(TypedDict):
    messages: Annotated[list, add]
    iteration: int
    tool_calls_made: list[str]  # 중복 도구 호출 추적

llm = ChatAnthropic(model="claude-opus-4-7")

# ─── THOUGHT 노드 ────────────────────────────────────
async def reasoning_node(state: AgentState) -> dict:
    """LLM이 다음 행동을 추론"""
    iteration = state["iteration"] + 1

    # 시스템 프롬프트에 현재 반복 횟수 주입
    system = f"""당신은 도구를 사용하여 질문에 답하는 AI 에이전트입니다.
현재 반복: {iteration}/{MAX_ITERATIONS}

규칙:
1. 확실한 정보가 있으면 바로 Final Answer 작성
2. 동일한 도구를 동일한 인수로 두 번 호출하지 마세요
3. 불필요한 도구 호출은 피하세요"""

    llm_with_tools = llm.bind_tools(tools)
    response = await llm_with_tools.ainvoke(
        state["messages"],
        system=system
    )
    return {"messages": [response], "iteration": iteration}

# ─── ACTION 노드 ────────────────────────────────────
async def action_node(state: AgentState) -> dict:
    """도구 실행 + 중복 호출 감지"""
    last_message = state["messages"][-1]
    tool_messages = []
    calls_made = state["tool_calls_made"][:]

    for call in last_message.tool_calls:
        call_signature = f"{call['name']}:{json.dumps(call['args'], sort_keys=True)}"

        if call_signature in calls_made:
            # 중복 호출 — 캐시된 결과 반환
            result = "[캐시] 이전과 동일한 결과"
        else:
            calls_made.append(call_signature)
            result = await execute_tool(call["name"], call["args"])

        tool_messages.append(ToolMessage(
            tool_call_id=call["id"],
            content=str(result)
        ))

    return {"messages": tool_messages, "tool_calls_made": calls_made}

# ─── 라우팅 ────────────────────────────────────────
def should_continue(state: AgentState) -> str:
    last = state["messages"][-1]

    # 강제 종료: 반복 초과
    if state["iteration"] >= MAX_ITERATIONS:
        return "end"

    # 도구 호출이 있으면 action으로
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "action"

    return "end"

# ─── 그래프 구성 ─────────────────────────────────────
builder = StateGraph(AgentState)
builder.add_node("reasoning", reasoning_node)
builder.add_node("action",    action_node)
builder.set_entry_point("reasoning")
builder.add_conditional_edges("reasoning", should_continue, {
    "action": "action",
    "end":    END
})
builder.add_edge("action", "reasoning")

agent = builder.compile()

ReAct 프롬프트 엔지니어링

ReAct의 성능은 시스템 프롬프트 품질에 크게 의존합니다. 특히 도구 사용 가이드라인최종 답변 형식을 명확히 지정해야 합니다.

pythonreact_prompt.py — 고품질 ReAct 시스템 프롬프트
REACT_SYSTEM_PROMPT = """당신은 정확하고 효율적인 AI 에이전트입니다.

## 사고 과정 (Thought)
각 단계에서 다음을 명확히 합니다:
- 현재까지 파악한 정보
- 아직 확인이 필요한 정보
- 다음 행동의 이유

## 도구 사용 원칙
1. **최소 호출**: 한 번의 도구 호출로 필요한 모든 정보를 얻으세요
2. **병렬 가능 도구**: 독립적인 정보는 Parallel Tool Calling 활용
3. **오류 처리**: 도구 실패 시 대체 방법을 시도하거나 현재 정보로 답변

## 최종 답변 기준
다음 조건 중 하나라도 충족하면 즉시 최종 답변을 작성하세요:
- 질문에 답하기에 충분한 정보를 수집한 경우
- 더 이상 새로운 정보를 얻을 수 없는 경우
- 5회 이상 도구를 사용한 경우

## 금지 사항
- 동일한 도구를 같은 인수로 두 번 호출
- 이미 알고 있는 정보를 재확인하기 위한 도구 호출
- 과도한 자기 의심으로 인한 루프"""

# 도구별 예시 포함 프롬프트 생성
def build_system_prompt(tools: list, domain: str = "") -> str:
    tool_descriptions = "\n".join(
        f"- {t.name}: {t.description}" for t in tools
    )
    domain_context = f"\n## 도메인: {domain}" if domain else ""

    return f"""{REACT_SYSTEM_PROMPT}

## 사용 가능한 도구
{tool_descriptions}{domain_context}"""

무한 루프 방지 전략

전략 1
반복 횟수 상한
MAX_ITERATIONS(보통 10~15)를 초과하면 강제 종료 후 현재까지 수집된 정보로 최선의 답변 생성. 프로덕션에서는 반드시 설정.
전략 2
도구 호출 시그니처 추적
함수명 + 직렬화된 인수를 해시로 저장. 중복 호출 감지 시 캐시 반환 또는 루프 경고 메시지를 LLM에 전달.
전략 3
진행도 검증
N회 반복마다 LLM에게 "현재까지 얼마나 진행됐나?"를 묻는 메타 추론 노드 삽입. 루프 감지 시 전략 전환.
전략 4
총 토큰 예산
전체 대화 토큰 합계가 임계치에 도달하면 요약 후 종료. 비용 통제와 루프 방지를 동시에 달성.

ReAct vs 대안 아키텍처

항목ReActPlan-and-ExecuteLATS
계획 방식즉각적(온라인)사전 계획 후 실행트리 탐색
적응성높음 — 즉시 재계획중간 — 오류 시 재계획높음 — 백트래킹
LLM 호출 수적음중간많음
복잡 문제중간좋음최고
비용낮음중간높음
구현 복잡도낮음중간높음
💡 언제 ReAct를 선택하나
  • 사용 — 단일 단계 질문-답변, 3~5개 도구, 빠른 응답 필요, 예산 제한
  • 대체 고려 — 10단계 이상의 복잡한 계획, 여러 하위 목표를 가진 프로젝트형 작업
  • 대부분 프로덕션 Agent는 ReAct를 기본으로 사용하고, 복잡한 작업에만 Planning을 추가합니다