- Published on
LLM Agent 시스템 구축: Tool Use, Planning, Memory 완전 분석
- Authors
- Name
- 1. LLM Agent란? - ReAct 논문 분석
- 2. Tool/Function Calling 메커니즘
- 3. OpenAI Function Calling vs Anthropic Tool Use 비교
- 4. Planning 전략
- 5. Memory 시스템
- 6. LangGraph 공식 문서 기반 분석
- 7. Human-in-the-Loop 패턴
- 8. Multi-Agent 시스템 구축
- 9. MCP (Model Context Protocol) 개요
- 10. Agent 평가 방법론
- 11. 실전 예제: 데이터 분석 Agent
- 12. References
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 동작 흐름
- 요청 단계: 클라이언트가 tool 정의와 함께 메시지를 API에 전송한다.
- 판단 단계: Claude가 사용 가능한 tool 중 적절한 것을 선택하고,
tool_usecontent block을 반환한다. 이때stop_reason은"tool_use"가 된다. - 실행 단계: 클라이언트가 실제로 해당 tool을 실행한다. (Claude가 직접 실행하지 않는다.)
- 결과 전달: tool 실행 결과를
tool_resultcontent block으로 다시 Claude에게 전달한다. - 최종 응답: 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 Calling | Anthropic 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 Calling | parallel_tool_calls 파라미터로 제어 | 지원 (여러 tool_use 블록 반환) |
| Strict Mode | strict: true (Structured Outputs) | strict: true (2025년 추가) |
| Server-side Tools | Web search, Code Interpreter 등 | Web search, Code execution 등 |
| 결과 전달 | tool role message | tool_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의 핵심 구성 요소는 다음과 같다.
- Thought Decomposition: 문제를 중간 단계의 "생각(thought)" 단위로 분해한다.
- Thought Generation: 각 단계에서 여러 후보 생각을 생성한다 (sampling 또는 proposal).
- State Evaluation: LLM 자체를 활용하여 각 상태의 유망성을 평가한다.
- 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
Annotated와 add_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으로 지원한다. 핵심은 Checkpointer와 Interrupt 함수다.
기본 원리
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는 세 가지 유형의 기능을 노출할 수 있다.
- Tools: Agent가 호출할 수 있는 함수 (예: 파일 읽기, API 호출)
- Resources: 컨텍스트로 사용할 수 있는 데이터 (예: 문서, 설정 파일)
- 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
- Yao, S. et al. (2022). "ReAct: Synergizing Reasoning and Acting in Language Models." ICLR 2023. https://arxiv.org/abs/2210.03629
- Yao, S. et al. (2023). "Tree of Thoughts: Deliberate Problem Solving with Large Language Models." NeurIPS 2023. https://arxiv.org/abs/2305.10601
- Anthropic. "Tool use with Claude." Claude API Documentation. https://docs.anthropic.com/en/docs/build-with-claude/tool-use
- Anthropic. "How to implement tool use." Claude API Documentation. https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use
- Anthropic. "Introducing advanced tool use on the Claude Developer Console." https://www.anthropic.com/engineering/advanced-tool-use
- OpenAI. "Function calling." OpenAI API Documentation. https://platform.openai.com/docs/guides/function-calling
- LangChain. "LangGraph Documentation." https://docs.langchain.com/oss/python/langgraph/quickstart
- LangChain. "LangGraph Graph API Overview." https://docs.langchain.com/oss/python/langgraph/graph-api
- LangChain. "Human-in-the-loop." https://docs.langchain.com/oss/python/langchain/human-in-the-loop
- LangChain. "Plan-and-Execute Agents." https://blog.langchain.com/planning-agents/
- LangChain. "LangGraph Supervisor." https://github.com/langchain-ai/langgraph-supervisor-py
- Anthropic. "Introducing the Model Context Protocol." https://www.anthropic.com/news/model-context-protocol
- Model Context Protocol Specification. https://modelcontextprotocol.io/specification/2025-11-25
- Google Research. "ReAct: Synergizing Reasoning and Acting in Language Models." https://research.google/blog/react-synergizing-reasoning-and-acting-in-language-models/