Ops & SystemsEN

AI Agent 첫걸음 — ReAct 패턴으로 LLM이 행동하게 만들기

Agent의 기초 ReAct 패턴을 이해합니다. 챗봇과 Agent의 차이, Thought-Action-Observation 루프, 그리고 프로덕션에서 ReAct가 부족한 이유.

AI Agent 첫걸음 — ReAct 패턴으로 LLM이 행동하게 만들기

AI Agent 첫걸음 — ReAct 패턴으로 LLM이 행동하게 만들기

ChatGPT에게 "오늘 서울 날씨 알려줘"라고 하면? "저는 실시간 정보에 접근할 수 없습니다." 하지만 Agent라면 날씨 API를 호출하고, 결과를 해석해서, 자연어로 답을 줍니다. 이 차이가 챗봇과 에이전트를 가르는 핵심입니다.

이 글에서는 Agent의 가장 기본이 되는 ReAct 패턴을 이해하고, 순수 Python으로 직접 구현한 뒤, 왜 Tool Calling으로 진화했는지까지 다룹니다.

시리즈: Part 1 (이 글) | Part 2: LangGraph + Reflection | Part 3: MCP + Multi-Agent | Part 4: 프로덕션 배포

챗봇 vs Agent: 무엇이 다른가?

대부분의 LLM 애플리케이션은 "챗봇"입니다. 사용자가 질문하면 모델이 학습된 지식으로 답변합니다. Input → Output, 끝.

Agent는 다릅니다. LLM이 도구(Tools)를 사용해서 외부 세계와 상호작용합니다. 검색하고, 계산하고, API를 호출하고, 데이터베이스를 조회합니다. 그리고 그 결과를 바탕으로 다음 행동을 스스로 결정합니다.

구분챗봇Agent
동작 방식Input → LLM → OutputInput → LLM → Tool → Observation → ... → Output
지식 범위학습 데이터에 한정실시간 정보 접근 가능
외부 연동불가능API, DB, 파일 시스템 등 연동
의사결정단일 응답다단계 추론 및 행동
오류 대응없음재시도, 대안 도구 선택 가능
핵심 인사이트: Agent = LLM + Tools + Loop. LLM이 "무엇을 할지" 판단하고, Tool이 "실제 행동"을 수행하고, Loop가 "완료될 때까지" 반복합니다.

ReAct: Reasoning + Acting

2022년 Yao et al.이 발표한 ReAct(Reasoning and Acting) 논문은 LLM Agent의 기초를 놓았습니다. 핵심 아이디어는 단순합니다: LLM이 생각(Thought)하고 행동(Action)하는 과정을 번갈아 반복하게 만드는 것입니다.

Thought → Action → Observation 루프

ReAct 에이전트는 세 단계를 반복합니다:

  1. Thought — "지금 상황에서 무엇을 해야 하는가?" 추론합니다
  2. Action — 적절한 도구를 선택하고 실행합니다
  3. Observation — 도구의 결과를 받아 다음 판단에 활용합니다
사용자: "테슬라 현재 주가와 시가총액을 알려줘"

Thought: 테슬라의 현재 주가를 검색해야 합니다.
Action: search["Tesla current stock price"]
Observation: Tesla (TSLA) is currently trading at $248.50...

Thought: 주가를 찾았습니다. 이제 시가총액을 계산해야 합니다.
Action: search["Tesla market cap 2024"]
Observation: Tesla's market capitalization is $792 billion...

Thought: 두 정보를 모두 찾았으므로 답변할 수 있습니다.
Final Answer: 테슬라의 현재 주가는 $248.50이며, 시가총액은 약 7,920억 달러입니다.

이 패턴이 강력한 이유는 추론 과정이 투명하다는 것입니다. 모델이 왜 그런 행동을 했는지, 중간 결과가 무엇이었는지 전부 볼 수 있습니다.

직접 만드는 ReAct Agent

이론은 충분합니다. 순수 Python과 OpenAI API만으로 ReAct Agent를 구현해봅시다.

1단계: 도구 정의

먼저 Agent가 사용할 도구를 만듭니다.

python
import math
import requests

def search(query: str) -> str:
    """웹 검색을 시뮬레이션합니다."""
    # 실제로는 Tavily, SerpAPI 등을 사용합니다
    responses = {
        "서울 날씨": "서울 현재 기온 22°C, 맑음, 습도 45%",
        "파이썬 최신 버전": "Python 3.13.0 (2024년 10월 릴리스)",
    }
    return responses.get(query, f"'{query}'에 대한 검색 결과가 없습니다.")

def calculate(expression: str) -> str:
    """수학 표현식을 계산합니다."""
    try:
        result = eval(expression, {"__builtins__": {}}, {"math": math})
        return str(result)
    except Exception as e:
        return f"계산 오류: {e}"

# 도구 레지스트리
TOOLS = {
    "search": search,
    "calculate": calculate,
}

2단계: 시스템 프롬프트

ReAct 패턴의 핵심은 시스템 프롬프트입니다. LLM에게 "어떤 형식으로 생각하고 행동할지" 명확히 알려줘야 합니다.

python
SYSTEM_PROMPT = """You are a helpful assistant that can use tools to answer questions.

Available tools:
- search[query]: Search the web for information
- calculate[expression]: Evaluate a mathematical expression

You MUST follow this exact format for each step:

Thought: <your reasoning about what to do next>
Action: <tool_name>[<argument>]

After receiving an Observation, continue with another Thought/Action,
or provide your final response:

Thought: <I now have enough information>
Final Answer: <your complete answer to the user>

Always think step by step. Use tools when you need external information."""

3단계: Agent 루프 구조

나머지 구현은 두 가지입니다: LLM 응답에서 Action을 파싱하는 것, 그리고 이 전체를 루프로 엮는 것입니다. 구조를 의사코드로 보면:

def react_agent(question):
    messages = [system_prompt, user_question]

    반복 (최대 5회):
        response = LLM(messages)

        if "Final Answer" in response:
            return 최종 답변

        if "Action: tool[arg]" in response:
            result = 도구 실행(tool, arg)
            messages에 Observation 추가

    return "최대 반복 초과"

핵심은 LLM의 텍스트 출력에서 정규표현식으로 Action을 추출한다는 것입니다. 이 접근법은 작동하지만, 프로덕션에서 심각한 문제를 일으킵니다 (뒤에서 다룹니다).

이 Agent에 "서울 날씨가 어때? 화씨로 변환해줘"라고 물으면 이런 결과가 나옵니다:

--- Step 1 ---
Thought: 서울의 현재 날씨를 검색해야 합니다.
Action: search[서울 날씨]
Observation: 서울 현재 기온 22°C, 맑음, 습도 45%

--- Step 2 ---
Thought: 서울 기온은 22°C입니다. 화씨로 변환하겠습니다. 공식: F = C * 9/5 + 32
Action: calculate[22 * 9/5 + 32]
Observation: 71.6

--- Step 3 ---
Thought: 모든 정보를 얻었습니다.
Final Answer: 서울의 현재 날씨는 맑음이며, 기온은 22°C (71.6°F), 습도는 45%입니다.

Thought → Action → Observation이 자연스럽게 이어지면서 최종 답에 도달합니다.

ReAct의 한계 — 왜 이대로는 안 되는가

위 Agent는 데모에서 잘 동작합니다. 하지만 프로덕션에 올리려면 치명적인 문제들이 있습니다.

문제ReAct (텍스트 파싱)해결책
형식 보장LLM이 형식을 어길 수 있음?
타입 안전성모든 인자가 문자열?
다중 인자파싱 규칙이 복잡해짐?
에러 처리수동 구현 필요?
병렬 호출불가능?

예를 들어, LLM이 Action: search[ 서울 날씨 ] (공백 추가)나 action: search[서울 날씨] (소문자)로 형식을 살짝 바꾸면 정규표현식 파싱이 깨집니다. Action: calculate[len([1,2,3])] 같은 중첩 괄호도 파싱 불가능합니다.

이 모든 문제를 해결하는 것이 Tool Calling — OpenAI의 Function Calling API와 Pydantic을 결합한 구조화된 접근법입니다. Part 2에서 이 진화를 다룹니다.

언제 Agent를 써야 하는가?

Agent는 강력하지만 만능이 아닙니다. 불필요한 곳에 Agent를 도입하면 복잡성만 높아집니다.

Agent가 필요한 경우

  • 실시간 정보가 필요할 때: 날씨, 주가, 뉴스 등 실시간 데이터 조회
  • 다단계 추론이 필요할 때: "A를 조회하고, 그 결과로 B를 계산해서, C와 비교해줘"
  • 외부 시스템과 연동할 때: DB 조회, API 호출, 파일 읽기/쓰기
  • 사용자 의도가 다양할 때: 하나의 인터페이스로 여러 기능을 제공해야 하는 경우

Agent가 불필요한 경우

  • 단순 Q&A: 학습 데이터로 충분히 답변 가능한 질문
  • 고정된 파이프라인: 입력 → 처리 → 출력이 항상 동일한 워크플로우
  • 지연 시간이 중요할 때: 도구 호출은 추가 latency를 발생시킵니다
  • 비용이 민감할 때: 루프를 돌 때마다 API 호출 비용이 발생합니다
질문이 들어왔을 때 의사결정 플로우:

외부 정보가 필요한가? ─── No ──→ 일반 LLM 호출
        │
       Yes
        ↓
단일 API 호출로 충분한가? ─── Yes ──→ 단순 함수 호출
        │
       No
        ↓
다단계 추론이 필요한가? ─── No ──→ RAG 파이프라인
        │
       Yes
        ↓
    Agent 도입

다음 편: Tool Calling + LangGraph + Reflection

Part 1에서는 Agent의 기초 — ReAct 패턴의 개념과 구조를 다뤘습니다. Part 2부터 본격적인 구현입니다:

  • Tool Calling 구현 — ReAct의 정규표현식 파싱을 Pydantic + Function Calling으로 교체
  • LangGraph — 에이전트 워크플로우를 그래프로 구조화
  • Self-Critique (Reflection) — 에이전트가 자기 출력을 스스로 검증하고 개선
  • Planning Agent — 복잡한 태스크를 단계별로 분해하고 실행

Part 3에서는 MCP + Multi-Agent, Part 4에서는 Guardrails + Docker 배포까지 다룹니다.

참고 자료

Part 1 / 4 완료

나머지 3편이 기다리고 있습니다

이론에서 프로덕션 배포까지 — 구독하면 전체 시리즈와 모든 프리미엄 콘텐츠를 잠금 해제합니다.

요금제 비교

더 많은 콘텐츠를 받아보세요

SNS에서 새로운 글과 튜토리얼 소식을 가장 먼저 받아보세요

이메일로 받아보기

관련 포스트