Skip to content

Split View: LLM Agent 시스템 구축: Tool Use, Planning, Memory 완전 분석

|

LLM Agent 시스템 구축: Tool Use, Planning, Memory 완전 분석

1. LLM Agent란? - ReAct 논문 분석

Agent의 정의

LLM Agent는 단순히 텍스트를 생성하는 Language Model을 넘어, 외부 환경과 상호작용하며 자율적으로 의사결정을 수행하는 시스템이다. 전통적인 LLM이 주어진 prompt에 대해 한 번의 응답을 생성하는 반면, Agent는 목표를 달성하기 위해 반복적으로 관찰(Observation), 추론(Reasoning), 행동(Action)을 수행한다.

Agent 시스템의 핵심 구성 요소는 다음과 같다.

  • LLM (Brain): 추론과 의사결정을 담당하는 핵심 모델
  • Tools: 외부 API, 데이터베이스, 코드 실행기 등 Agent가 사용할 수 있는 도구
  • Planning: 복잡한 작업을 하위 단계로 분해하는 전략
  • Memory: 과거 상호작용과 컨텍스트를 유지하는 메커니즘

ReAct: Reasoning + Acting의 결합

2022년 Yao et al.이 발표한 ReAct (Reasoning and Acting) 논문은 LLM Agent의 기초를 확립한 핵심 연구다. 이 논문은 ICLR 2023에 채택되었으며, LLM이 추론 과정(Reasoning Trace)과 작업별 행동(Task-Specific Actions)을 교차적으로 생성하도록 하는 패러다임을 제안했다.

기존 접근법의 한계는 명확했다. Chain-of-Thought(CoT) prompting은 추론 능력은 뛰어나지만 외부 정보에 접근할 수 없어 hallucination이 발생한다. 반대로 Action-only 방식은 외부 도구를 사용하지만 추론 과정이 없어 복잡한 판단이 어렵다.

ReAct는 이 두 접근법을 통합한다. 핵심 루프는 다음과 같다.

Thought: 현재 상황을 분석하고 다음 행동을 계획한다.
Action: 외부 도구를 호출하거나 환경과 상호작용한다.
Observation: Action의 결과를 관찰한다.
... (반복)
Thought: 충분한 정보를 수집했으므로 최종 답변을 생성한다.

예를 들어, "애플의 현재 시가총액을 한국 원화로 환산하면 얼마인가?"라는 질문에 대해 ReAct Agent는 다음과 같이 동작한다.

Thought: 먼저 애플의 현재 시가총액을 달러로 확인해야 한다.
Action: search("Apple current market cap USD")
Observation: Apple's market cap is approximately $3.4 trillion.
Thought: 이제 현재 USD/KRW 환율을 확인해야 한다.
Action: search("current USD KRW exchange rate")
Observation: 1 USD = 1,450 KRW
Thought: 3.4조 달러 * 1,450 = 4,930조 원이다. 최종 답변을 생성한다.
Answer: 애플의 시가총액은 약 4,930조 원입니다.

논문의 실험 결과도 인상적이다. HotpotQA(질문 응답)와 Fever(사실 검증) 태스크에서 ReAct는 CoT 대비 hallucination을 크게 줄였으며, ALFWorld와 WebShop 같은 의사결정 벤치마크에서는 기존 모방 학습 및 강화 학습 방법 대비 각각 34%, 10%의 성공률 향상을 보여주었다.


2. Tool/Function Calling 메커니즘

Anthropic Tool Use 공식 문서 기반 분석

Anthropic의 Claude는 Tool Use라는 명칭으로 Function Calling 기능을 제공한다. 공식 문서에 따르면, Tool Use의 동작 방식은 다음과 같다.

Tool 정의

API 요청 시 사용할 도구를 JSON Schema 형식으로 정의한다. 각 tool 정의에는 name, description, input_schema가 포함된다.

import anthropic

client = anthropic.Anthropic()

# Tool 정의
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit"
                }
            },
            "required": ["location"]
        }
    }
]

# API 호출
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "서울 날씨 어때?"}
    ]
)

Tool Use 동작 흐름

  1. 요청 단계: 클라이언트가 tool 정의와 함께 메시지를 API에 전송한다.
  2. 판단 단계: Claude가 사용 가능한 tool 중 적절한 것을 선택하고, tool_use content block을 반환한다. 이때 stop_reason"tool_use"가 된다.
  3. 실행 단계: 클라이언트가 실제로 해당 tool을 실행한다. (Claude가 직접 실행하지 않는다.)
  4. 결과 전달: tool 실행 결과를 tool_result content block으로 다시 Claude에게 전달한다.
  5. 최종 응답: Claude가 tool 결과를 기반으로 자연어 응답을 생성한다.
# 2. Claude의 응답에서 tool_use 블록 추출
tool_use_block = next(
    block for block in response.content if block.type == "tool_use"
)
tool_name = tool_use_block.name        # "get_weather"
tool_input = tool_use_block.input      # {"location": "Seoul, South Korea"}
tool_use_id = tool_use_block.id        # 고유 식별자

# 3. 실제 tool 실행 (개발자가 구현)
weather_result = call_weather_api(tool_input["location"])

# 4. tool 결과를 Claude에게 전달
follow_up = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "서울 날씨 어때?"},
        {"role": "assistant", "content": response.content},
        {
            "role": "user",
            "content": [
                {
                    "type": "tool_result",
                    "tool_use_id": tool_use_id,
                    "content": weather_result
                }
            ]
        }
    ]
)

2025년 추가된 고급 기능

Anthropic은 2025년에 세 가지 중요한 기능을 추가했다.

  • Tool Search Tool: 모든 tool 정의를 미리 로딩하지 않고, 필요한 tool을 동적으로 탐색한다. Context window를 효율적으로 사용할 수 있다.
  • Programmatic Tool Calling: Code execution 환경에서 tool을 호출하여 context window에 대한 부담을 줄인다.
  • Structured Outputs: strict: true 옵션을 tool 정의에 추가하면 Claude의 tool call이 항상 정의된 schema를 정확히 따르도록 보장한다.

3. OpenAI Function Calling vs Anthropic Tool Use 비교

두 플랫폼 모두 LLM이 구조화된 데이터를 생성하여 외부 함수를 호출하는 메커니즘을 제공하지만, 구현 방식과 철학에서 차이가 있다.

항목OpenAI Function CallingAnthropic Tool Use
명칭Function Calling (또는 Tool Calling)Tool Use
Tool 정의 위치tools 파라미터tools 파라미터
Schema 형식JSON Schema (parameters)JSON Schema (input_schema)
응답 형식tool_calls 배열 (message 내부)tool_use content block
Parallel Callingparallel_tool_calls 파라미터로 제어지원 (여러 tool_use 블록 반환)
Strict Modestrict: true (Structured Outputs)strict: true (2025년 추가)
Server-side ToolsWeb search, Code Interpreter 등Web search, Code execution 등
결과 전달tool role messagetool_result content block

OpenAI 방식의 특징

OpenAI는 tool_choice 파라미터를 통해 모델의 tool 사용을 세밀하게 제어할 수 있다. "auto"(모델이 자율 판단), "required"(반드시 tool 사용), "none"(tool 사용 금지), 또는 특정 함수를 지정할 수 있다. parallel_tool_calls: false로 설정하면 한 번에 하나의 tool만 호출하도록 제한할 수 있다.

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "서울 날씨와 도쿄 날씨를 비교해줘"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string"}
                },
                "required": ["location"]
            }
        }
    }],
    parallel_tool_calls=True  # 서울과 도쿄 동시 호출
)

Anthropic 방식의 특징

Anthropic의 Claude는 tool 사용 시 자연스럽게 thinking 과정을 노출하는 경향이 있어, Agent의 의사결정 과정을 더 투명하게 파악할 수 있다. 또한 tool_choice"auto", "any" (반드시 하나 이상의 tool 사용), 특정 tool 지정이 가능하다.

핵심적인 차이는 아키텍처적 철학에 있다. OpenAI는 tool call을 message-level에서 처리하는 반면, Anthropic은 content block-level에서 처리한다. 이는 Anthropic이 텍스트와 tool call을 하나의 응답 안에서 더 유연하게 섞을 수 있음을 의미한다.


4. Planning 전략

복잡한 작업을 수행하는 Agent에게 Planning은 핵심적인 능력이다. 주요 전략을 살펴보자.

Plan-and-Execute 패턴

Plan-and-Execute는 작업을 먼저 전체 계획을 수립(Plan)한 뒤, 각 단계를 순차적으로 실행(Execute)하는 패턴이다. LangChain 블로그에서 소개한 이 접근법은 Wang et al.의 Plan-and-Solve Prompting 논문과 Yohei Nakajima의 BabyAGI 프로젝트에 기반한다.

[User Query]
     |
     v
[Planner LLM] --> Step 1, Step 2, Step 3, ...
     |
     v
[Executor] --> Step 1 실행 --> Step 2 실행 --> Step 3 실행
     |
     v
[Re-planner] --> 필요시 계획 수정
     |
     v
[Final Answer]

ReAct 대비 Plan-and-Execute의 장점은 다음과 같다.

  • 속도: 각 하위 작업 실행 시마다 대형 Planner LLM을 호출할 필요가 없다. 소형 모델이 개별 단계를 실행할 수 있다.
  • 추론 품질: Planner가 전체 작업을 명시적으로 분해하므로 누락되는 단계가 줄어든다.
  • 비용 효율: Planner에는 고성능 모델을, Executor에는 경량 모델을 사용하여 비용을 절감할 수 있다.

Tree of Thoughts (ToT)

Yao et al.이 NeurIPS 2023에 발표한 Tree of Thoughts는 Chain-of-Thought의 한계를 극복하기 위한 프레임워크다. 핵심 아이디어는 LLM의 추론 과정을 트리 구조로 확장하여, 여러 가지 사고 경로를 탐색하고 평가하는 것이다.

                    [문제]
                   /  |  \
              [생각1] [생각2] [생각3]    <-- 여러 경로 생성
              /  \      |      \
         [1-a] [1-b]  [2-a]   [3-a]    <-- 각 경로 확장
           |     |      |       |
        [평가] [평가]  [평가]   [평가]   <-- 자체 평가
           |            |
        [선택]        [선택]            <-- 최적 경로 선택

ToT의 핵심 구성 요소는 다음과 같다.

  1. Thought Decomposition: 문제를 중간 단계의 "생각(thought)" 단위로 분해한다.
  2. Thought Generation: 각 단계에서 여러 후보 생각을 생성한다 (sampling 또는 proposal).
  3. State Evaluation: LLM 자체를 활용하여 각 상태의 유망성을 평가한다.
  4. Search Algorithm: BFS(너비 우선 탐색) 또는 DFS(깊이 우선 탐색)로 트리를 탐색한다.

Game of 24 태스크에서 GPT-4 + CoT의 성공률이 4%에 불과한 반면, ToT는 74%를 달성했다. 이는 탐색과 백트래킹이 가능한 구조적 추론의 위력을 보여준다.


5. Memory 시스템

Agent가 장기적으로 효과적으로 동작하려면 Memory 시스템이 필수적이다. Memory는 크게 세 가지로 분류된다.

Short-term Memory (단기 기억)

LLM의 Context Window 자체가 단기 기억의 역할을 한다. 현재 대화 내역, 최근 tool 호출 결과 등이 여기에 해당한다.

# 단기 기억: 대화 히스토리를 messages로 관리
messages = [
    {"role": "user", "content": "나의 이름은 김영주야"},
    {"role": "assistant", "content": "안녕하세요, 김영주님!"},
    {"role": "user", "content": "내 이름이 뭐라고 했지?"},
    # 컨텍스트 윈도우 내에서 이전 대화를 참조할 수 있음
]

한계는 명확하다. Context window에는 크기 제한이 있으며(Claude: 200K tokens, GPT-4o: 128K tokens), 세션이 끝나면 기억이 사라진다.

Long-term Memory (장기 기억)

외부 저장소(Vector DB, Key-Value Store 등)에 정보를 영속적으로 저장하고 필요시 검색하는 방식이다. RAG(Retrieval-Augmented Generation)가 대표적인 구현 패턴이다.

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# 장기 기억: Vector DB에 저장
vectorstore = Chroma(
    collection_name="agent_memory",
    embedding_function=OpenAIEmbeddings()
)

# 과거 상호작용 저장
vectorstore.add_texts([
    "사용자는 Python과 데이터 분석에 관심이 있다.",
    "사용자는 서울에 거주하며, 한국어를 선호한다.",
    "이전 세션에서 pandas DataFrame 관련 질문을 했다."
])

# 관련 기억 검색
relevant_memories = vectorstore.similarity_search(
    "사용자의 프로그래밍 관심사", k=3
)

Entity Memory (개체 기억)

대화에서 등장하는 특정 개체(사람, 장소, 개념 등)에 대한 정보를 추출하고 업데이트하는 메커니즘이다. 대화가 진행될수록 각 개체에 대한 지식이 축적된다.

# Entity Memory 예시 구조
entity_store = {
    "김영주": {
        "직업": "데이터 엔지니어",
        "관심사": ["LLM", "데이터 파이프라인", "Kubernetes"],
        "선호_언어": "Python",
        "최근_질문_주제": "LangGraph Agent 구축"
    },
    "프로젝트_A": {
        "상태": "진행 중",
        "기술_스택": ["LangGraph", "Claude API", "PostgreSQL"],
        "목표": "사내 데이터 분석 Agent 구축"
    }
}

실전에서는 이 세 가지 Memory를 조합하여 사용한다. LangGraph에서는 MemorySaver(단기)와 외부 Store(장기)를 .compile() 시점에 주입하여 통합 Memory 시스템을 구성할 수 있다.


6. LangGraph 공식 문서 기반 분석

LangGraph 핵심 개념

LangGraph는 LangChain 팀이 개발한 Agent Orchestration Framework로, Agent 워크플로우를 방향성 그래프(Directed Graph) 로 모델링한다. 공식 문서에 따르면 핵심 구성 요소는 다음과 같다.

StateGraph

StateGraph는 LangGraph의 중심 클래스다. 사용자가 정의한 State 객체로 파라미터화되며, 그래프의 모든 노드가 이 상태를 읽고 쓴다.

from langgraph.graph import StateGraph, START, END
from langgraph.graph import MessagesState

# MessagesState는 messages 리스트를 관리하는 내장 State
graph = StateGraph(MessagesState)

커스텀 State를 정의할 수도 있다.

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    plan: list[str]
    current_step: int
    final_answer: str

Annotatedadd_messages를 활용하면 reducer 함수를 지정할 수 있다. 이는 노드가 상태를 반환할 때 기존 값을 어떻게 업데이트할지 결정한다. add_messages는 기존 메시지 리스트에 새 메시지를 추가(append) 하는 방식으로 동작한다.

Node

Node는 Agent의 실제 로직을 수행하는 함수다. 현재 State를 입력으로 받아 업데이트된 State를 반환한다.

from langchain_anthropic import ChatAnthropic

model = ChatAnthropic(model="claude-sonnet-4-20250514")
model_with_tools = model.bind_tools(tools)

def call_model(state: AgentState):
    """LLM을 호출하는 노드"""
    response = model_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def execute_tool(state: AgentState):
    """Tool을 실행하는 노드"""
    last_message = state["messages"][-1]
    results = []
    for tool_call in last_message.tool_calls:
        result = tool_map[tool_call["name"]].invoke(tool_call["args"])
        results.append(
            ToolMessage(content=str(result), tool_call_id=tool_call["id"])
        )
    return {"messages": results}

# 노드 추가
graph.add_node("agent", call_model)
graph.add_node("tools", execute_tool)

Edge

Edge는 노드 간의 실행 흐름을 정의한다. LangGraph는 세 가지 종류의 Edge를 제공한다.

1. Normal Edge: 항상 고정된 다음 노드로 이동한다.

graph.add_edge(START, "agent")     # 시작 -> agent 노드
graph.add_edge("tools", "agent")   # tools -> agent 노드 (결과 전달)

2. Conditional Edge: 조건에 따라 다른 노드로 분기한다.

def should_continue(state: AgentState):
    """tool 호출이 필요한지 판단"""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"
    return END

graph.add_conditional_edges("agent", should_continue)

3. Entry Point: START 상수를 사용하여 그래프의 시작점을 정의한다.

Compile과 실행

그래프를 정의한 뒤 반드시 compile을 해야 실행할 수 있다.

from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

# 실행
result = app.invoke(
    {"messages": [("user", "서울의 현재 날씨를 알려줘")]},
    config={"configurable": {"thread_id": "session-001"}}
)

thread_id를 통해 대화 세션을 구분하며, Checkpointer가 각 super-step마다 상태를 자동 저장한다.


7. Human-in-the-Loop 패턴

LangGraph의 Interrupt 메커니즘

LangGraph 공식 문서에서는 Human-in-the-Loop을 first-class citizen으로 지원한다. 핵심은 CheckpointerInterrupt 함수다.

기본 원리

LangGraph의 모든 실행은 Checkpointer를 통해 각 super-step마다 상태가 저장된다. 이를 기반으로 특정 노드 실행 전/후에 그래프를 일시 중지(pause) 하고, 사용자 입력을 기다린 뒤 재개(resume) 할 수 있다.

Static Interrupt

특정 노드의 실행 전(before) 또는 후(after)에 중단점을 설정한다.

# compile 시 중단점 설정
app = graph.compile(
    checkpointer=checkpointer,
    interrupt_before=["execute_tool"],   # tool 실행 전 중단
    # interrupt_after=["execute_tool"],  # tool 실행 후 중단
)

# 실행하면 execute_tool 노드 직전에서 멈춤
result = app.invoke(
    {"messages": [("user", "이 데이터를 삭제해줘")]},
    config={"configurable": {"thread_id": "session-002"}}
)

# 사용자가 승인 후 재개
app.invoke(None, config={"configurable": {"thread_id": "session-002"}})

Dynamic Interrupt (interrupt 함수)

LangGraph는 interrupt() 함수를 제공하여 노드 내부에서 동적으로 중단할 수 있다. 이 방식이 더 유연하다.

from langgraph.types import interrupt, Command

def sensitive_tool_node(state: AgentState):
    """민감한 작업 수행 전 사용자 승인을 요청하는 노드"""
    last_message = state["messages"][-1]

    for tool_call in last_message.tool_calls:
        if tool_call["name"] in ["delete_data", "send_email", "execute_query"]:
            # 실행을 중단하고 사용자에게 승인 요청
            user_response = interrupt(
                f"'{tool_call['name']}' 도구를 실행하려 합니다. "
                f"입력값: {tool_call['args']}. 승인하시겠습니까?"
            )

            if user_response != "approved":
                return {"messages": [
                    ToolMessage(
                        content="사용자가 실행을 거부했습니다.",
                        tool_call_id=tool_call["id"]
                    )
                ]}

        # 승인된 경우 실행
        result = tool_map[tool_call["name"]].invoke(tool_call["args"])
        return {"messages": [
            ToolMessage(content=str(result), tool_call_id=tool_call["id"])
        ]}

중단된 그래프를 재개할 때는 Command(resume=...)를 사용한다.

# 중단된 상태에서 사용자가 승인
app.invoke(
    Command(resume="approved"),
    config={"configurable": {"thread_id": "session-002"}}
)

활용 시나리오

  • 위험한 작업 승인: 데이터 삭제, 이메일 발송, 결제 처리 전 사용자 확인
  • 모호한 요청 명확화: Agent가 사용자의 의도를 정확히 파악하지 못했을 때 추가 질문
  • 실행 계획 검토: Plan-and-Execute 패턴에서 계획 단계 후 사용자가 계획을 검토/수정

8. Multi-Agent 시스템 구축

Supervisor 아키텍처

LangGraph 공식 문서와 langgraph-supervisor 라이브러리에서 제시하는 Supervisor 패턴은 Multi-Agent 시스템의 핵심 아키텍처다. 중앙의 Supervisor Agent가 전문화된 Worker Agent들을 조율한다.

                    [User Query]
                         |
                         v
                   [Supervisor Agent]
                   /       |        \
                  v        v         v
          [Research    [Code       [Data
           Agent]      Agent]      Agent]
              |           |           |
              v           v           v
         [Web Search] [Code Exec] [SQL Query]

구현 방식

from langgraph.graph import StateGraph, MessagesState, START, END

# 각 전문 Agent 정의
def research_agent(state: MessagesState):
    """웹 검색 전문 Agent"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_search = model.bind_tools([web_search_tool])
    response = model_with_search.invoke(state["messages"])
    return {"messages": [response]}

def code_agent(state: MessagesState):
    """코드 작성 및 실행 전문 Agent"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_code = model.bind_tools([code_execution_tool])
    response = model_with_code.invoke(state["messages"])
    return {"messages": [response]}

def data_agent(state: MessagesState):
    """데이터 분석 전문 Agent"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_data = model.bind_tools([sql_query_tool, chart_tool])
    response = model_with_data.invoke(state["messages"])
    return {"messages": [response]}

# Supervisor 라우팅 함수
def supervisor(state: MessagesState):
    """어떤 Agent에게 작업을 위임할지 결정"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    system_prompt = """당신은 Supervisor입니다. 사용자의 요청을 분석하여
    적절한 전문 Agent에게 작업을 위임하세요.
    - research: 정보 검색이 필요한 경우
    - code: 코드 작성/실행이 필요한 경우
    - data: 데이터 분석/시각화가 필요한 경우
    - FINISH: 작업이 완료된 경우"""

    response = model.invoke([
        {"role": "system", "content": system_prompt},
        *state["messages"]
    ])
    return {"messages": [response]}

def route_supervisor(state: MessagesState):
    """Supervisor의 결정에 따라 라우팅"""
    last_message = state["messages"][-1].content
    if "research" in last_message.lower():
        return "research_agent"
    elif "code" in last_message.lower():
        return "code_agent"
    elif "data" in last_message.lower():
        return "data_agent"
    return END

# 그래프 구성
workflow = StateGraph(MessagesState)
workflow.add_node("supervisor", supervisor)
workflow.add_node("research_agent", research_agent)
workflow.add_node("code_agent", code_agent)
workflow.add_node("data_agent", data_agent)

workflow.add_edge(START, "supervisor")
workflow.add_conditional_edges("supervisor", route_supervisor)
workflow.add_edge("research_agent", "supervisor")
workflow.add_edge("code_agent", "supervisor")
workflow.add_edge("data_agent", "supervisor")

app = workflow.compile()

계층적 구조

더 복잡한 시스템에서는 계층적 Multi-Agent 구조를 사용한다. 최상위 Supervisor가 중간 레벨 Supervisor를 관리하고, 중간 레벨이 실제 Worker를 관리하는 방식이다. 각 계층이 독립적으로 테스트 가능하고, 새로운 도메인을 기존에 영향 없이 추가할 수 있다.


9. MCP (Model Context Protocol) 개요

Anthropic이 제안한 개방형 표준

Model Context Protocol (MCP) 은 Anthropic이 2024년 11월에 발표한 개방형 프로토콜로, LLM 애플리케이션과 외부 데이터 소스 및 도구 간의 표준화된 연결을 제공한다. USB가 다양한 주변기기를 컴퓨터에 연결하는 표준 인터페이스인 것처럼, MCP는 AI 모델과 외부 시스템을 연결하는 표준 인터페이스다.

아키텍처

MCP는 Client-Server 아키텍처를 따른다.

[LLM Application (MCP Client)]
         |
    [MCP Protocol]
         |
[MCP Server A]  [MCP Server B]  [MCP Server C]
     |               |               |
[GitHub API]   [Database]      [File System]
  • MCP Host: Claude Desktop, IDE 등 LLM을 내장한 애플리케이션
  • MCP Client: Host 내에서 MCP Server와 통신하는 컴포넌트
  • MCP Server: 특정 기능(도구, 데이터)을 표준화된 프로토콜로 노출하는 서버

핵심 기능

MCP Server는 세 가지 유형의 기능을 노출할 수 있다.

  1. Tools: Agent가 호출할 수 있는 함수 (예: 파일 읽기, API 호출)
  2. Resources: 컨텍스트로 사용할 수 있는 데이터 (예: 문서, 설정 파일)
  3. Prompts: 재사용 가능한 프롬프트 템플릿

2025년 현황

MCP 사양은 2025년 11월 25일 기준 최신 버전이 공개되었으며, 현재 10,000개 이상의 공개 MCP 서버가 활동 중이다. ChatGPT, Cursor, Gemini, Microsoft Copilot, Visual Studio Code 등 주요 AI 제품들이 MCP를 채택했다.

2025년 6월에는 중요한 보안 업데이트가 있었다. MCP 서버를 OAuth 2.0 Resource Server로 분류하고, Structured JSON Output(structuredContent) 지원, 세션 중간에 사용자 입력을 요청하는 Elicitation 기능이 추가되었다.

Anthropic은 MCP를 Agentic AI Foundation에 기부하여 커뮤니티 주도의 거버넌스 구조로 전환했다.


10. Agent 평가 방법론

Agent 시스템은 기존 LLM 평가와 다른 차원의 평가가 필요하다. 핵심 평가 축은 다음과 같다.

Task Completion Rate (작업 완료율)

주어진 목표를 실제로 달성했는지 측정한다. 벤치마크별 평가 기준이 다르다.

  • WebArena: 웹 브라우징 작업의 성공/실패
  • SWE-bench: 실제 GitHub Issue 해결 여부
  • HumanEval: 코드 생성의 정확성

Tool Selection Accuracy (도구 선택 정확도)

Agent가 적절한 도구를 선택했는지 평가한다.

# 평가 메트릭 예시
evaluation = {
    "correct_tool_selected": True,      # 올바른 도구를 선택했는가
    "correct_parameters": True,         # 파라미터가 정확한가
    "unnecessary_tool_calls": 0,        # 불필요한 호출 횟수
    "total_tool_calls": 3,             # 전체 호출 횟수
    "optimal_tool_calls": 2,           # 최적 호출 횟수
    "efficiency": 2/3                  # 효율성
}

Trajectory Quality (경로 품질)

최종 결과뿐만 아니라 과정의 효율성을 평가한다.

  • 불필요한 단계 없이 목표에 도달했는가
  • 오류 발생 시 적절히 복구했는가
  • 동일한 작업을 반복하지 않았는가

Safety & Guardrails (안전성)

  • 허용되지 않은 도구를 호출하지 않았는가
  • 민감한 정보를 적절히 처리했는가
  • Human-in-the-Loop이 필요한 상황에서 올바르게 중단했는가

Cost & Latency (비용 및 지연시간)

  • 총 API 호출 횟수와 토큰 사용량
  • 전체 작업 완료까지의 소요 시간
  • 모델 크기 대비 성능 효율

11. 실전 예제: 데이터 분석 Agent

아래는 LangGraph를 활용하여 CSV 데이터를 분석하는 Agent를 구축하는 전체 코드다. Anthropic Claude를 LLM으로 사용하고, Tool Use와 Conditional Edge를 활용한다.

import pandas as pd
from typing import TypedDict, Annotated
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import ToolMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import ToolNode, tools_condition


# ============================================
# 1. Tool 정의
# ============================================

@tool
def load_csv(file_path: str) -> str:
    """CSV 파일을 로드하고 기본 정보를 반환합니다."""
    df = pd.read_csv(file_path)
    info = {
        "shape": df.shape,
        "columns": list(df.columns),
        "dtypes": df.dtypes.to_dict(),
        "head": df.head().to_string(),
        "describe": df.describe().to_string()
    }
    return str(info)


@tool
def run_analysis(file_path: str, query: str) -> str:
    """pandas를 사용하여 CSV 데이터에 대한 분석 쿼리를 실행합니다.

    Args:
        file_path: CSV 파일 경로
        query: 실행할 pandas 쿼리 (예: 'df.groupby("category").mean()')
    """
    df = pd.read_csv(file_path)
    try:
        result = eval(query, {"df": df, "pd": pd})
        if isinstance(result, pd.DataFrame):
            return result.to_string()
        elif isinstance(result, pd.Series):
            return result.to_string()
        return str(result)
    except Exception as e:
        return f"쿼리 실행 오류: {str(e)}"


@tool
def generate_summary(analysis_results: str, user_question: str) -> str:
    """분석 결과를 사용자가 이해하기 쉬운 요약으로 변환합니다.

    Args:
        analysis_results: 분석 결과 텍스트
        user_question: 원래 사용자 질문
    """
    summary = f"""
    ## 분석 요약

    **질문**: {user_question}

    **결과**:
    {analysis_results}
    """
    return summary


# ============================================
# 2. Agent 구성
# ============================================

# Tool 리스트
tools = [load_csv, run_analysis, generate_summary]

# LLM 설정
model = ChatAnthropic(
    model="claude-sonnet-4-20250514",
    temperature=0,
    max_tokens=4096
)
model_with_tools = model.bind_tools(tools)


# Agent 노드 정의
def agent_node(state: MessagesState):
    """Agent가 추론하고 필요시 tool을 호출한다."""
    system_message = {
        "role": "system",
        "content": """당신은 데이터 분석 전문 Agent입니다.
        사용자의 질문에 답하기 위해 다음 절차를 따르세요:
        1. 먼저 load_csv로 데이터를 로드하여 구조를 파악합니다.
        2. run_analysis로 적절한 pandas 쿼리를 실행합니다.
        3. generate_summary로 결과를 요약합니다.
        항상 단계별로 생각하고, 필요한 tool을 호출하세요."""
    }
    messages = [system_message] + state["messages"]
    response = model_with_tools.invoke(messages)
    return {"messages": [response]}


# ============================================
# 3. 그래프 구성
# ============================================

# StateGraph 생성
workflow = StateGraph(MessagesState)

# 노드 추가
workflow.add_node("agent", agent_node)
workflow.add_node("tools", ToolNode(tools))

# 엣지 추가
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
    "agent",
    tools_condition,  # tool_calls가 있으면 "tools"로, 없으면 END로
)
workflow.add_edge("tools", "agent")  # tool 결과를 agent에게 전달

# Compile
checkpointer = MemorySaver()
app = workflow.compile(checkpointer=checkpointer)


# ============================================
# 4. 실행
# ============================================

def run_data_agent(question: str, file_path: str, thread_id: str = "default"):
    """데이터 분석 Agent를 실행한다."""
    config = {"configurable": {"thread_id": thread_id}}

    initial_message = HumanMessage(
        content=f"파일 경로: {file_path}\n\n질문: {question}"
    )

    result = app.invoke(
        {"messages": [initial_message]},
        config=config
    )

    # 최종 응답 추출
    final_message = result["messages"][-1]
    return final_message.content


# 사용 예시
if __name__ == "__main__":
    answer = run_data_agent(
        question="매출 데이터에서 월별 평균 매출액과 가장 높은 매출을 기록한 달은?",
        file_path="./data/sales.csv",
        thread_id="analysis-001"
    )
    print(answer)

이 Agent는 ReAct 패턴에 따라 동작한다. LLM이 추론을 수행하고(agent_node), 필요한 도구를 선택하며(tools_condition으로 분기), 도구 결과를 다시 LLM에 전달하는(tools -> agent Edge) 루프를 반복한다. MemorySaver가 각 단계의 상태를 저장하므로, 동일한 thread_id로 후속 질문을 하면 이전 분석 컨텍스트가 유지된다.


12. References

Building LLM Agent Systems: Complete Analysis of Tool Use, Planning, and Memory

1. What is an LLM Agent? - ReAct Paper Analysis

Definition of an Agent

An LLM Agent goes beyond a Language Model that simply generates text — it is a system that autonomously makes decisions while interacting with external environments. While traditional LLMs generate a single response to a given prompt, an Agent repeatedly performs Observation, Reasoning, and Action to achieve a goal.

The core components of an Agent system are as follows:

  • LLM (Brain): The core model responsible for reasoning and decision-making
  • Tools: External APIs, databases, code executors, and other instruments the Agent can use
  • Planning: Strategies for decomposing complex tasks into sub-steps
  • Memory: Mechanisms for maintaining past interactions and context

ReAct: Combining Reasoning + Acting

The ReAct (Reasoning and Acting) paper published by Yao et al. in 2022 is a foundational study that established the basis for LLM Agents. Accepted at ICLR 2023, this paper proposed a paradigm in which LLMs alternately generate reasoning traces and task-specific actions.

The limitations of existing approaches were clear. Chain-of-Thought (CoT) prompting excels at reasoning but cannot access external information, leading to hallucination. Conversely, Action-only approaches use external tools but lack reasoning processes, making complex judgments difficult.

ReAct unifies these two approaches. The core loop is as follows:

Thought: Analyze the current situation and plan the next action.
Action: Call an external tool or interact with the environment.
Observation: Observe the result of the Action.
... (repeat)
Thought: Sufficient information has been gathered, so generate the final answer.

For example, for the question "How much is Apple's current market cap in Korean Won?", a ReAct Agent operates as follows:

Thought: First, I need to check Apple's current market cap in USD.
Action: search("Apple current market cap USD")
Observation: Apple's market cap is approximately $3.4 trillion.
Thought: Now I need to check the current USD/KRW exchange rate.
Action: search("current USD KRW exchange rate")
Observation: 1 USD = 1,450 KRW
Thought: 3.4 trillion * 1,450 = 4,930 trillion KRW. Generate the final answer.
Answer: Apple's market cap is approximately 4,930 trillion KRW.

The experimental results were also impressive. On HotpotQA (question answering) and Fever (fact verification) tasks, ReAct significantly reduced hallucinations compared to CoT, and on decision-making benchmarks like ALFWorld and WebShop, it showed 34% and 10% success rate improvements over existing imitation learning and reinforcement learning methods, respectively.


2. Tool/Function Calling Mechanism

Analysis Based on Anthropic Tool Use Official Documentation

Anthropic's Claude provides Function Calling capabilities under the name Tool Use. According to the official documentation, Tool Use operates as follows:

Tool Definition

Tools to be used are defined in JSON Schema format when making API requests. Each tool definition includes name, description, and input_schema.

import anthropic

client = anthropic.Anthropic()

# Tool definition
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit"
                }
            },
            "required": ["location"]
        }
    }
]

# API call
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What's the weather like in Seoul?"}
    ]
)

Tool Use Operation Flow

  1. Request Phase: The client sends a message along with tool definitions to the API.
  2. Decision Phase: Claude selects an appropriate tool from available ones and returns a tool_use content block. At this point, the stop_reason becomes "tool_use".
  3. Execution Phase: The client actually executes the tool. (Claude does not execute it directly.)
  4. Result Delivery: The tool execution result is sent back to Claude as a tool_result content block.
  5. Final Response: Claude generates a natural language response based on the tool result.
# 2. Extract tool_use block from Claude's response
tool_use_block = next(
    block for block in response.content if block.type == "tool_use"
)
tool_name = tool_use_block.name        # "get_weather"
tool_input = tool_use_block.input      # {"location": "Seoul, South Korea"}
tool_use_id = tool_use_block.id        # unique identifier

# 3. Actually execute the tool (implemented by the developer)
weather_result = call_weather_api(tool_input["location"])

# 4. Send tool result to Claude
follow_up = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What's the weather like in Seoul?"},
        {"role": "assistant", "content": response.content},
        {
            "role": "user",
            "content": [
                {
                    "type": "tool_result",
                    "tool_use_id": tool_use_id,
                    "content": weather_result
                }
            ]
        }
    ]
)

Advanced Features Added in 2025

Anthropic added three important features in 2025:

  • Tool Search Tool: Instead of pre-loading all tool definitions, tools are dynamically discovered as needed. This enables efficient use of the context window.
  • Programmatic Tool Calling: Tools are called in a code execution environment, reducing the burden on the context window.
  • Structured Outputs: Adding the strict: true option to tool definitions guarantees that Claude's tool calls always exactly follow the defined schema.

3. OpenAI Function Calling vs Anthropic Tool Use Comparison

Both platforms provide mechanisms for LLMs to generate structured data to call external functions, but they differ in implementation and philosophy.

ItemOpenAI Function CallingAnthropic Tool Use
NameFunction Calling (or Tool Calling)Tool Use
Tool Definition Locationtools parametertools parameter
Schema FormatJSON Schema (parameters)JSON Schema (input_schema)
Response Formattool_calls array (inside message)tool_use content block
Parallel CallingControlled via parallel_tool_callsSupported (multiple tool_use blocks)
Strict Modestrict: true (Structured Outputs)strict: true (added 2025)
Server-side ToolsWeb search, Code Interpreter, etc.Web search, Code execution, etc.
Result Deliverytool role messagetool_result content block

OpenAI Approach Characteristics

OpenAI allows fine-grained control over the model's tool usage through the tool_choice parameter. Options include "auto" (model decides autonomously), "required" (must use a tool), "none" (tool use prohibited), or specifying a particular function. Setting parallel_tool_calls: false restricts the model to calling only one tool at a time.

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Compare the weather in Seoul and Tokyo"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string"}
                },
                "required": ["location"]
            }
        }
    }],
    parallel_tool_calls=True  # Simultaneous call for Seoul and Tokyo
)

Anthropic Approach Characteristics

Anthropic's Claude tends to naturally expose its thinking process when using tools, making it easier to transparently understand the Agent's decision-making process. It also supports tool_choice with "auto", "any" (must use at least one tool), or specifying a particular tool.

The key difference lies in architectural philosophy. OpenAI handles tool calls at the message-level, while Anthropic handles them at the content block-level. This means Anthropic can more flexibly intermix text and tool calls within a single response.


4. Planning Strategies

Planning is a critical capability for Agents performing complex tasks. Let's examine the key strategies.

Plan-and-Execute Pattern

Plan-and-Execute is a pattern that first establishes an overall plan (Plan) and then sequentially executes each step (Execute). This approach, introduced in the LangChain blog, is based on the Plan-and-Solve Prompting paper by Wang et al. and Yohei Nakajima's BabyAGI project.

[User Query]
     |
     v
[Planner LLM] --> Step 1, Step 2, Step 3, ...
     |
     v
[Executor] --> Execute Step 1 --> Execute Step 2 --> Execute Step 3
     |
     v
[Re-planner] --> Modify plan if needed
     |
     v
[Final Answer]

The advantages of Plan-and-Execute over ReAct are as follows:

  • Speed: No need to call the large Planner LLM for each sub-task execution. A smaller model can execute individual steps.
  • Reasoning Quality: Since the Planner explicitly decomposes the entire task, fewer steps are missed.
  • Cost Efficiency: Costs can be reduced by using a high-performance model for the Planner and a lightweight model for the Executor.

Tree of Thoughts (ToT)

Tree of Thoughts, published by Yao et al. at NeurIPS 2023, is a framework designed to overcome the limitations of Chain-of-Thought. The core idea is to expand the LLM's reasoning process into a tree structure, exploring and evaluating multiple thought paths.

                    [Problem]
                   /  |  \
              [Thought1] [Thought2] [Thought3]    <-- Generate multiple paths
              /  \      |      \
         [1-a] [1-b]  [2-a]   [3-a]    <-- Expand each path
           |     |      |       |
        [Eval] [Eval]  [Eval]   [Eval]   <-- Self-evaluation
           |            |
        [Select]      [Select]            <-- Select optimal path

The core components of ToT are as follows:

  1. Thought Decomposition: Decompose the problem into intermediate "thought" units.
  2. Thought Generation: Generate multiple candidate thoughts at each step (via sampling or proposal).
  3. State Evaluation: Use the LLM itself to evaluate the promise of each state.
  4. Search Algorithm: Explore the tree using BFS (breadth-first search) or DFS (depth-first search).

On the Game of 24 task, GPT-4 + CoT achieved only a 4% success rate, while ToT achieved 74%. This demonstrates the power of structural reasoning that enables exploration and backtracking.


5. Memory Systems

A Memory system is essential for Agents to operate effectively over the long term. Memory is broadly classified into three types.

Short-term Memory

The LLM's Context Window itself serves as short-term memory. This includes current conversation history and recent tool call results.

# Short-term memory: managing conversation history as messages
messages = [
    {"role": "user", "content": "My name is Youngju Kim"},
    {"role": "assistant", "content": "Hello, Youngju Kim!"},
    {"role": "user", "content": "What did I say my name was?"},
    # Can reference previous conversation within the context window
]

The limitations are clear. The context window has size limits (Claude: 200K tokens, GPT-4o: 128K tokens), and memories disappear when the session ends.

Long-term Memory

This approach persistently stores information in external storage (Vector DB, Key-Value Store, etc.) and retrieves it when needed. RAG (Retrieval-Augmented Generation) is the most representative implementation pattern.

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# Long-term memory: store in Vector DB
vectorstore = Chroma(
    collection_name="agent_memory",
    embedding_function=OpenAIEmbeddings()
)

# Store past interactions
vectorstore.add_texts([
    "The user is interested in Python and data analysis.",
    "The user lives in Seoul and prefers Korean.",
    "In a previous session, the user asked about pandas DataFrames."
])

# Search relevant memories
relevant_memories = vectorstore.similarity_search(
    "user's programming interests", k=3
)

Entity Memory

This is a mechanism that extracts and updates information about specific entities (people, places, concepts, etc.) that appear in conversations. Knowledge about each entity accumulates as conversations progress.

# Entity Memory example structure
entity_store = {
    "Youngju Kim": {
        "occupation": "Data Engineer",
        "interests": ["LLM", "Data Pipeline", "Kubernetes"],
        "preferred_language": "Python",
        "recent_question_topic": "LangGraph Agent Construction"
    },
    "Project_A": {
        "status": "In Progress",
        "tech_stack": ["LangGraph", "Claude API", "PostgreSQL"],
        "goal": "Building an internal data analysis Agent"
    }
}

In practice, these three types of Memory are used in combination. In LangGraph, MemorySaver (short-term) and external Store (long-term) can be injected at .compile() time to build an integrated Memory system.


6. Analysis Based on LangGraph Official Documentation

Core Concepts of LangGraph

LangGraph is an Agent Orchestration Framework developed by the LangChain team that models Agent workflows as directed graphs. According to the official documentation, the core components are as follows:

StateGraph

StateGraph is the central class of LangGraph. It is parameterized with a user-defined State object, and all nodes in the graph read from and write to this state.

from langgraph.graph import StateGraph, START, END
from langgraph.graph import MessagesState

# MessagesState is a built-in State that manages a messages list
graph = StateGraph(MessagesState)

You can also define custom State:

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    plan: list[str]
    current_step: int
    final_answer: str

Using Annotated and add_messages, you can specify reducer functions. These determine how to update existing values when a node returns state. add_messages operates by appending new messages to the existing message list.

Node

A Node is a function that performs actual logic for the Agent. It takes the current State as input and returns an updated State.

from langchain_anthropic import ChatAnthropic

model = ChatAnthropic(model="claude-sonnet-4-20250514")
model_with_tools = model.bind_tools(tools)

def call_model(state: AgentState):
    """Node that calls the LLM"""
    response = model_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def execute_tool(state: AgentState):
    """Node that executes tools"""
    last_message = state["messages"][-1]
    results = []
    for tool_call in last_message.tool_calls:
        result = tool_map[tool_call["name"]].invoke(tool_call["args"])
        results.append(
            ToolMessage(content=str(result), tool_call_id=tool_call["id"])
        )
    return {"messages": results}

# Add nodes
graph.add_node("agent", call_model)
graph.add_node("tools", execute_tool)

Edge

Edges define the execution flow between nodes. LangGraph provides three types of Edges:

1. Normal Edge: Always moves to a fixed next node.

graph.add_edge(START, "agent")     # Start -> agent node
graph.add_edge("tools", "agent")   # tools -> agent node (deliver results)

2. Conditional Edge: Branches to different nodes based on conditions.

def should_continue(state: AgentState):
    """Determine if tool calls are needed"""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"
    return END

graph.add_conditional_edges("agent", should_continue)

3. Entry Point: Define the graph's starting point using the START constant.

Compile and Execution

After defining the graph, you must compile it before execution.

from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

# Execute
result = app.invoke(
    {"messages": [("user", "Tell me the current weather in Seoul")]},
    config={"configurable": {"thread_id": "session-001"}}
)

Conversation sessions are distinguished through thread_id, and the Checkpointer automatically saves state at each super-step.


7. Human-in-the-Loop Patterns

LangGraph's Interrupt Mechanism

LangGraph's official documentation supports Human-in-the-Loop as a first-class citizen. The key components are the Checkpointer and the Interrupt function.

Basic Principle

All execution in LangGraph has state saved at each super-step through the Checkpointer. Based on this, the graph can be paused before/after specific node execution, wait for user input, and then resumed.

Static Interrupt

Set breakpoints before or after specific node execution.

# Set breakpoints at compile time
app = graph.compile(
    checkpointer=checkpointer,
    interrupt_before=["execute_tool"],   # Interrupt before tool execution
    # interrupt_after=["execute_tool"],  # Interrupt after tool execution
)

# Execution stops just before the execute_tool node
result = app.invoke(
    {"messages": [("user", "Please delete this data")]},
    config={"configurable": {"thread_id": "session-002"}}
)

# Resume after user approval
app.invoke(None, config={"configurable": {"thread_id": "session-002"}})

Dynamic Interrupt (interrupt function)

LangGraph provides an interrupt() function that allows dynamic interruption within a node. This approach is more flexible.

from langgraph.types import interrupt, Command

def sensitive_tool_node(state: AgentState):
    """Node that requests user approval before performing sensitive operations"""
    last_message = state["messages"][-1]

    for tool_call in last_message.tool_calls:
        if tool_call["name"] in ["delete_data", "send_email", "execute_query"]:
            # Interrupt execution and request user approval
            user_response = interrupt(
                f"Attempting to execute '{tool_call['name']}' tool. "
                f"Input: {tool_call['args']}. Do you approve?"
            )

            if user_response != "approved":
                return {"messages": [
                    ToolMessage(
                        content="User rejected the execution.",
                        tool_call_id=tool_call["id"]
                    )
                ]}

        # Execute if approved
        result = tool_map[tool_call["name"]].invoke(tool_call["args"])
        return {"messages": [
            ToolMessage(content=str(result), tool_call_id=tool_call["id"])
        ]}

To resume an interrupted graph, use Command(resume=...):

# User approves from the interrupted state
app.invoke(
    Command(resume="approved"),
    config={"configurable": {"thread_id": "session-002"}}
)

Use Scenarios

  • Approval for dangerous operations: User confirmation before data deletion, email sending, payment processing
  • Clarifying ambiguous requests: Additional questions when the Agent cannot precisely determine the user's intent
  • Reviewing execution plans: User review/modification of plans after the planning phase in Plan-and-Execute patterns

8. Building Multi-Agent Systems

Supervisor Architecture

The Supervisor pattern presented in LangGraph official documentation and the langgraph-supervisor library is the core architecture of Multi-Agent systems. A central Supervisor Agent coordinates specialized Worker Agents.

                    [User Query]
                         |
                         v
                   [Supervisor Agent]
                   /       |        \
                  v        v         v
          [Research    [Code       [Data
           Agent]      Agent]      Agent]
              |           |           |
              v           v           v
         [Web Search] [Code Exec] [SQL Query]

Implementation

from langgraph.graph import StateGraph, MessagesState, START, END

# Define each specialized Agent
def research_agent(state: MessagesState):
    """Web search specialist Agent"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_search = model.bind_tools([web_search_tool])
    response = model_with_search.invoke(state["messages"])
    return {"messages": [response]}

def code_agent(state: MessagesState):
    """Code writing and execution specialist Agent"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_code = model.bind_tools([code_execution_tool])
    response = model_with_code.invoke(state["messages"])
    return {"messages": [response]}

def data_agent(state: MessagesState):
    """Data analysis specialist Agent"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_data = model.bind_tools([sql_query_tool, chart_tool])
    response = model_with_data.invoke(state["messages"])
    return {"messages": [response]}

# Supervisor routing function
def supervisor(state: MessagesState):
    """Decide which Agent to delegate the task to"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    system_prompt = """You are a Supervisor. Analyze the user's request
    and delegate it to the appropriate specialist Agent.
    - research: when information retrieval is needed
    - code: when code writing/execution is needed
    - data: when data analysis/visualization is needed
    - FINISH: when the task is complete"""

    response = model.invoke([
        {"role": "system", "content": system_prompt},
        *state["messages"]
    ])
    return {"messages": [response]}

def route_supervisor(state: MessagesState):
    """Route based on the Supervisor's decision"""
    last_message = state["messages"][-1].content
    if "research" in last_message.lower():
        return "research_agent"
    elif "code" in last_message.lower():
        return "code_agent"
    elif "data" in last_message.lower():
        return "data_agent"
    return END

# Compose graph
workflow = StateGraph(MessagesState)
workflow.add_node("supervisor", supervisor)
workflow.add_node("research_agent", research_agent)
workflow.add_node("code_agent", code_agent)
workflow.add_node("data_agent", data_agent)

workflow.add_edge(START, "supervisor")
workflow.add_conditional_edges("supervisor", route_supervisor)
workflow.add_edge("research_agent", "supervisor")
workflow.add_edge("code_agent", "supervisor")
workflow.add_edge("data_agent", "supervisor")

app = workflow.compile()

Hierarchical Structure

For more complex systems, a hierarchical Multi-Agent structure is used. A top-level Supervisor manages mid-level Supervisors, which in turn manage actual Workers. Each layer can be tested independently, and new domains can be added without affecting existing ones.


9. MCP (Model Context Protocol) Overview

An Open Standard Proposed by Anthropic

Model Context Protocol (MCP) is an open protocol announced by Anthropic in November 2024 that provides standardized connections between LLM applications and external data sources and tools. Just as USB is a standard interface for connecting various peripherals to computers, MCP is a standard interface for connecting AI models to external systems.

Architecture

MCP follows a Client-Server architecture.

[LLM Application (MCP Client)]
         |
    [MCP Protocol]
         |
[MCP Server A]  [MCP Server B]  [MCP Server C]
     |               |               |
[GitHub API]   [Database]      [File System]
  • MCP Host: Applications with embedded LLMs such as Claude Desktop, IDEs
  • MCP Client: Component within the Host that communicates with MCP Servers
  • MCP Server: A server that exposes specific capabilities (tools, data) through a standardized protocol

Core Capabilities

MCP Servers can expose three types of capabilities:

  1. Tools: Functions that Agents can call (e.g., file reading, API calls)
  2. Resources: Data that can be used as context (e.g., documents, configuration files)
  3. Prompts: Reusable prompt templates

2025 Status

The MCP specification's latest version was published as of November 25, 2025, with over 10,000 public MCP servers currently active. Major AI products including ChatGPT, Cursor, Gemini, Microsoft Copilot, and Visual Studio Code have adopted MCP.

In June 2025, there was an important security update. MCP servers were classified as OAuth 2.0 Resource Servers, with support for Structured JSON Output (structuredContent) and an Elicitation feature for requesting user input mid-session.

Anthropic donated MCP to the Agentic AI Foundation, transitioning to a community-driven governance structure.


10. Agent Evaluation Methodology

Agent systems require evaluation on different dimensions than traditional LLM evaluation. The key evaluation axes are as follows:

Task Completion Rate

Measures whether the given goal was actually achieved. Evaluation criteria vary by benchmark.

  • WebArena: Success/failure of web browsing tasks
  • SWE-bench: Whether actual GitHub Issues were resolved
  • HumanEval: Code generation accuracy

Tool Selection Accuracy

Evaluates whether the Agent selected appropriate tools.

# Evaluation metrics example
evaluation = {
    "correct_tool_selected": True,      # Was the correct tool selected?
    "correct_parameters": True,         # Are the parameters accurate?
    "unnecessary_tool_calls": 0,        # Number of unnecessary calls
    "total_tool_calls": 3,             # Total call count
    "optimal_tool_calls": 2,           # Optimal call count
    "efficiency": 2/3                  # Efficiency
}

Trajectory Quality

Evaluates not just the final result but the efficiency of the process.

  • Did it reach the goal without unnecessary steps?
  • Did it recover appropriately when errors occurred?
  • Did it avoid repeating the same operations?

Safety & Guardrails

  • Did it avoid calling unauthorized tools?
  • Did it properly handle sensitive information?
  • Did it correctly interrupt when Human-in-the-Loop was needed?

Cost & Latency

  • Total API call count and token usage
  • Total elapsed time for task completion
  • Performance efficiency relative to model size

11. Practical Example: Data Analysis Agent

Below is the complete code for building a CSV Data Analysis Agent using LangGraph. It uses Anthropic Claude as the LLM and leverages Tool Use and Conditional Edges.

import pandas as pd
from typing import TypedDict, Annotated
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import ToolMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import ToolNode, tools_condition


# ============================================
# 1. Tool Definitions
# ============================================

@tool
def load_csv(file_path: str) -> str:
    """Loads a CSV file and returns basic information."""
    df = pd.read_csv(file_path)
    info = {
        "shape": df.shape,
        "columns": list(df.columns),
        "dtypes": df.dtypes.to_dict(),
        "head": df.head().to_string(),
        "describe": df.describe().to_string()
    }
    return str(info)


@tool
def run_analysis(file_path: str, query: str) -> str:
    """Runs an analysis query on CSV data using pandas.

    Args:
        file_path: Path to the CSV file
        query: pandas query to execute (e.g., 'df.groupby("category").mean()')
    """
    df = pd.read_csv(file_path)
    try:
        result = eval(query, {"df": df, "pd": pd})
        if isinstance(result, pd.DataFrame):
            return result.to_string()
        elif isinstance(result, pd.Series):
            return result.to_string()
        return str(result)
    except Exception as e:
        return f"Query execution error: {str(e)}"


@tool
def generate_summary(analysis_results: str, user_question: str) -> str:
    """Converts analysis results into a user-friendly summary.

    Args:
        analysis_results: Analysis result text
        user_question: Original user question
    """
    summary = f"""
    ## Analysis Summary

    **Question**: {user_question}

    **Results**:
    {analysis_results}
    """
    return summary


# ============================================
# 2. Agent Configuration
# ============================================

# Tool list
tools = [load_csv, run_analysis, generate_summary]

# LLM setup
model = ChatAnthropic(
    model="claude-sonnet-4-20250514",
    temperature=0,
    max_tokens=4096
)
model_with_tools = model.bind_tools(tools)


# Agent node definition
def agent_node(state: MessagesState):
    """Agent reasons and calls tools as needed."""
    system_message = {
        "role": "system",
        "content": """You are a data analysis specialist Agent.
        To answer the user's question, follow these steps:
        1. First load the data with load_csv to understand its structure.
        2. Execute appropriate pandas queries with run_analysis.
        3. Summarize the results with generate_summary.
        Always think step by step and call the necessary tools."""
    }
    messages = [system_message] + state["messages"]
    response = model_with_tools.invoke(messages)
    return {"messages": [response]}


# ============================================
# 3. Graph Construction
# ============================================

# Create StateGraph
workflow = StateGraph(MessagesState)

# Add nodes
workflow.add_node("agent", agent_node)
workflow.add_node("tools", ToolNode(tools))

# Add edges
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
    "agent",
    tools_condition,  # "tools" if tool_calls exist, END otherwise
)
workflow.add_edge("tools", "agent")  # Deliver tool results to agent

# Compile
checkpointer = MemorySaver()
app = workflow.compile(checkpointer=checkpointer)


# ============================================
# 4. Execution
# ============================================

def run_data_agent(question: str, file_path: str, thread_id: str = "default"):
    """Run the data analysis Agent."""
    config = {"configurable": {"thread_id": thread_id}}

    initial_message = HumanMessage(
        content=f"File path: {file_path}\n\nQuestion: {question}"
    )

    result = app.invoke(
        {"messages": [initial_message]},
        config=config
    )

    # Extract final response
    final_message = result["messages"][-1]
    return final_message.content


# Usage example
if __name__ == "__main__":
    answer = run_data_agent(
        question="What is the monthly average sales and which month had the highest sales?",
        file_path="./data/sales.csv",
        thread_id="analysis-001"
    )
    print(answer)

This Agent operates according to the ReAct pattern. The LLM performs reasoning (agent_node), selects necessary tools (branching via tools_condition), and delivers tool results back to the LLM (tools -> agent Edge) in a repeating loop. Since MemorySaver saves state at each step, subsequent questions with the same thread_id will maintain previous analysis context.


12. References

Quiz

Q1: What is the main topic covered in "Building LLM Agent Systems: Complete Analysis of Tool Use, Planning, and Memory"?

Analyzing the core concepts of LLM Agents — Tool Use, Planning, and Memory — based on LangGraph and Anthropic official documentation, and building a practical Agent.

Q2: What is Definition of an Agent? An LLM Agent goes beyond a Language Model that simply generates text — it is a system that autonomously makes decisions while interacting with external environments.

Q3: Explain the core concept of ReAct: Combining Reasoning + Acting. The ReAct (Reasoning and Acting) paper published by Yao et al. in 2022 is a foundational study that established the basis for LLM Agents. Accepted at ICLR 2023, this paper proposed a paradigm in which LLMs alternately generate reasoning traces and task-specific actions.

Q4: What are the key aspects of Analysis Based on Anthropic Tool Use Official Documentation?

Anthropic's Claude provides Function Calling capabilities under the name Tool Use. According to the official documentation, Tool Use operates as follows: Tool Definition Tools to be used are defined in JSON Schema format when making API requests.

Q5: How does Plan-and-Execute Pattern work? Plan-and-Execute is a pattern that first establishes an overall plan (Plan) and then sequentially executes each step (Execute). This approach, introduced in the LangChain blog, is based on the Plan-and-Solve Prompting paper by Wang et al. and Yohei Nakajima's BabyAGI project.