LLM 에이전트 툴 사용 완전 정복: Function Calling부터 MCP까지
에이전트를 에이전트답게 만드는 것은 **툴 사용(Tool Use)**입니다. LLM이 단순히 텍스트를 생성하는 것을 넘어, 외부 API를 호출하고, 데이터베이스를 조회하고, 코드를 실행하게 만드는 메커니즘입니다.
툴 사용의 동작 원리
LLM은 텍스트만 입력받고 텍스트만 출력합니다. 툴 사용은 이 제약을 아래 흐름으로 우회합니다.
1. 사용자 메시지 + 툴 스펙 → LLM
2. LLM → "이 툴을 이 인자로 호출하라" (JSON 형태 출력)
3. 런타임이 실제 함수 실행
4. 실행 결과 → LLM에게 다시 전달
5. LLM → 최종 자연어 답변 생성핵심은 LLM이 직접 함수를 실행하지 않는다는 점입니다. 런타임이 실행하고, 결과를 LLM에게 넘겨줍니다.
OpenAI Function Calling
from openai import OpenAI
import json
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "특정 종목의 현재 주가를 조회합니다",
"parameters": {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "주식 티커 (예: AAPL, TSLA)"
},
"currency": {
"type": "string",
"enum": ["USD", "KRW"]
}
},
"required": ["symbol"]
}
}
}
]
def get_stock_price(symbol, currency="USD"):
return {"symbol": symbol, "price": 192.5, "currency": currency}
def run_agent(user_message):
messages = [{"role": "user", "content": user_message}]
while True:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
messages.append(msg)
if msg.tool_calls is None:
return msg.content
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = get_stock_price(**args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result)
})Anthropic Tool Use
Claude의 툴 사용은 tool_use 콘텐츠 블록으로 호출하고, tool_result로 반환합니다.
import anthropic, json
client = anthropic.Anthropic()
tools = [{
"name": "search_docs",
"description": "내부 문서 데이터베이스에서 관련 문서를 검색합니다",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"max_results": {"type": "integer", "default": 5}
},
"required": ["query"]
}
}]
def search_docs(query, max_results=5):
return [{"title": "API 인증 가이드", "content": "Bearer 토큰 방식을 사용합니다..."}]
messages = [{"role": "user", "content": "우리 API 인증 방식이 어떻게 돼?"}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages
)
if response.stop_reason == "end_turn":
text_block = next(b for b in response.content if b.type == "text")
print(text_block.text)
break
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = search_docs(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False)
})
messages.append({"role": "user", "content": tool_results})Model Context Protocol (MCP)
MCP는 Anthropic이 제안한 툴 표준화 프로토콜입니다. 개별 LLM SDK에 종속되지 않고, 서버-클라이언트 구조로 툴을 독립적으로 제공합니다.
# MCP 서버 구현
from mcp.server import Server
import mcp.types as types
server = Server("my-tools-server")
@server.list_tools()
async def handle_list_tools():
return [
types.Tool(
name="query_database",
description="SQL 쿼리를 실행하고 결과를 반환합니다",
inputSchema={
"type": "object",
"properties": {
"sql": {"type": "string", "description": "실행할 SELECT 쿼리"}
},
"required": ["sql"]
}
)
]
@server.call_tool()
async def handle_call_tool(name, arguments):
if name == "query_database":
results = execute_query(arguments["sql"])
return [types.TextContent(type="text", text=str(results))]MCP의 장점은 재사용성입니다. 한 번 만든 MCP 서버는 Claude Desktop, Claude Code, 자체 에이전트 등 어디서든 연결해 쓸 수 있습니다.
툴 설계 원칙
좋은 툴과 나쁜 툴의 차이는 description에 있습니다.
| 나쁜 예 | 좋은 예 |
|---|---|
| "데이터를 가져온다" | "주문 ID로 특정 주문의 상태, 금액, 배송 정보를 조회한다. 취소된 주문도 조회 가능하다." |
| 매개변수 설명 없음 | 각 매개변수의 타입, 허용 값, 기본값 명시 |
LLM은 description을 읽고 툴을 선택합니다. 설명이 부정확하면 잘못된 툴을 호출하거나 잘못된 인자를 넣습니다.
병렬 툴 호출
여러 툴을 순차 실행하면 느립니다. GPT-4o와 Claude 모두 병렬 툴 호출을 지원합니다.
import asyncio
async def execute_tools_parallel(tool_calls):
tasks = [dispatch_tool(tc.function.name, json.loads(tc.function.arguments))
for tc in tool_calls]
return await asyncio.gather(*tasks)다음 편에서는 에이전트가 대화 맥락을 유지하고 장기 작업을 수행할 수 있게 하는 메모리 시스템 설계를 다룹니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...