도구 다루기
도구 다루기 (Tools)
에이전트가 실제로 세상과 상호작용하게 만드는 게 바로 도구(tools)예요. 도구는 에이전트의 능력을 확장해서 실시간 데이터를 가져오고, 코드를 실행하고, 외부 데이터베이스에 질의하고, 세상에서 실제 행동을 취할 수 있게 해줍니다. 내부적으로 도구는 잘 정의된 입력과 출력을 가진 호출 가능한 함수(callable function)로, 채팅 모델에 전달돼요. 모델은 대화 맥락에 따라 언제 도구를 호출할지, 어떤 입력 인자를 줄지를 결정합니다.
모델이 도구 호출을 다루는 방법은 Tool calling 문서를 참고하세요. 도구 호출을 추적하고 오류를 디버깅하려면 LangSmith를 쓰면 돼요. 추적 퀵스타트를 따라 설정하고, 트레이스를 모니터링하고 문제를 감지·해결 방안을 제안하는 LangSmith Engine도 함께 설정하는 걸 권장합니다.
도구 만들기 (Create tools)
기본 도구 정의 (Basic tool definition)
가장 간단한 방법은 @tool 데코레이터를 쓰는 거예요. 기본적으로 함수의 docstring이 도구의 설명이 되어, 모델이 언제 사용해야 할지 이해하도록 도와줍니다.
from langchain.tools import tool
@tool
def search_database(query: str, limit: int = 10) -> str:
"""Search the customer database for records matching the query.
Args:
query: Search terms to look for
limit: Maximum number of results to return
"""
return f"Found {limit} results for '{query}'"
타입 힌트는 필수인데, 도구의 입력 스키마를 정의하기 때문이에요. docstring은 모델이 도구의 목적을 이해하도록 정보가 담기고 간결해야 합니다.
참고로 일부 채팅 모델은 웹 검색, 코드 인터프리터 같은 내장 도구를 서버 측에서 실행하기도 해요. 이건 Server-side tool use 문서에서 다룹니다.
도구 이름은 snake_case를 권장해요 (예: Web Search 대신 web_search). 일부 모델 프로바이더는 이름에 공백이나 특수 문자가 들어가면 오류를 내거나 거부하기 때문이죠. 영숫자, 밑줄, 하이픈만 쓰면 프로바이더 간 호환성이 좋아집니다.
도구 속성 커스터마이즈 (Customize tool properties)
커스텀 도구 이름 (Custom tool name)
기본적으로 도구 이름은 함수 이름에서 나와요. 더 설명적인 이름이 필요할 때는 override하면 됩니다.
@tool("web_search") # Custom name
def search(query: str) -> str:
"""Search the web for information."""
return f"Results for: {query}"
print(search.name) # web_search
커스텀 도구 설명 (Custom tool description)
모델에게 더 분명한 안내를 주려면 자동 생성된 도구 설명을 override할 수 있어요.
@tool("calculator", description="Performs arithmetic calculations. Use this for any math problems.")
def calc(expression: str) -> str:
"""Evaluate mathematical expressions."""
return str(eval(expression))
고급 스키마 정의 (Advanced schema definition)
복잡한 입력은 Pydantic 모델이나 JSON 스키마로 정의할 수 있습니다.
from pydantic import BaseModel, Field
from typing import Literal
class WeatherInput(BaseModel):
"""Input for weather queries."""
location: str = Field(description="City name or coordinates")
units: Literal["celsius", "fahrenheit"] = Field(
default="celsius",
description="Temperature unit preference"
)
include_forecast: bool = Field(
default=False,
description="Include 5-day forecast"
)
@tool(args_schema=WeatherInput)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
"""Get current weather and optional forecast."""
temp = 22 if units == "celsius" else 72
result = f"Current weather in {location}: {temp} degrees {units[0].upper()}"
if include_forecast:
result += "\nNext 5 days: Sunny"
return result
예약된 인자 이름 (Reserved argument names)
다음 파라미터 이름은 예약되어 있어서 도구 인자로 쓸 수 없어요. 이 이름을 쓰면 런타임 오류가 발생합니다.
| 파라미터 이름 | 용도 |
|---|---|
config |
도구 내부적으로 RunnableConfig를 전달하기 위해 예약 |
runtime |
ToolRuntime 파라미터용으로 예약 (state, context, store 접근) |
런타임 정보에 접근하려면 자신의 인자를 config나 runtime으로 이름 짓는 대신 ToolRuntime 파라미터를 사용하세요. InjectedState, InjectedStore, get_runtime(), InjectedToolCallId를 쓴다면 이전 주입 패턴에서 마이그레이션하는 방법을 참고하세요.
맥락에 접근하기 (Access context)
도구는 대화 기록, 사용자 데이터, 영구 메모리 같은 런타임 정보에 접근할 수 있을 때 가장 강력해져요. 이 섹션에서는 도구 안에서 이런 정보에 접근하고 갱신하는 방법을 다룹니다. ToolRuntime 파라미터를 통해 접근할 수 있어요.
| 컴포넌트 | 설명 | 사용 사례 |
|---|---|---|
| State | 단기 메모리 - 현재 대화 동안 존재하는 변경 가능한 데이터 (메시지, 카운터, 커스텀 필드) | 대화 기록 접근, 도구 호출 횟수 추적 |
| Context | 호출 시 전달되는 불변 설정 (사용자 ID, 세션 정보) | 사용자 신원 기반 응답 개인화 |
| Store | 장기 메모리 - 대화를 넘어 유지되는 영구 데이터 | 사용자 선호 저장, 지식 베이스 유지 |
| Stream Writer | 도구 실행 중 실시간 업데이트 방출 | 오래 걸리는 작업의 진행 상황 표시 |
| Execution Info | 현재 실행의 신원과 재시도 정보 (thread ID, run ID, 시도 횟수) | thread/run ID 접근, 재시도 상태에 따른 동작 조정 |
| Server Info | LangGraph Server에서 실행 시 서버별 메타데이터 (assistant ID, graph ID, 인증된 사용자) | assistant ID, graph ID, 인증 사용자 정보 접근 |
| Config | 실행을 위한 RunnableConfig |
콜백, 태그, 메타데이터 접근 |
| Tool Call ID | 현재 도구 호출의 고유 식별자 | 로그와 모델 호출을 위한 도구 호출 상관관계 |
단기 메모리 – State (Short-term memory)
State는 대화가 지속되는 동안 존재하는 단기 메모리를 나타내요. 메시지 기록과 그래프 state에 정의한 커스텀 필드를 포함합니다.
State에 접근하려면 도구 시그니처에 runtime: ToolRuntime을 추가하세요. 호출 시점에 ToolNode가 값을 자동으로 주입하며, 이 파라미터는 모델에 보내는 도구 스키마에는 포함되지 않습니다. runtime.state로 현재 대화 state를 읽을 수 있어요.
from langchain.tools import tool, ToolRuntime
from langchain.messages import HumanMessage
@tool
def get_last_user_message(runtime: ToolRuntime) -> str:
"""Get the most recent message from the user."""
messages = runtime.state["messages"]
# Find the last human message
for message in reversed(messages):
if isinstance(message, HumanMessage):
return message.content
return "No user messages found"
# Access custom state fields
@tool
def get_user_preference(
pref_name: str,
runtime: ToolRuntime
) -> str:
"""Get a user preference value."""
preferences = runtime.state.get("user_preferences", {})
return preferences.get(pref_name, "Not set")
runtime 파라미터는 모델에게 숨겨져요. 위 예시에서 모델은 도구 스키마에서 pref_name만 보게 됩니다.
State를 갱신할 때는 Command를 사용합니다. 커스텀 state 필드를 갱신해야 하는 도구에 유용해요. 모델이 도구 호출 결과를 볼 수 있도록 갱신 항목에 ToolMessage를 포함하세요.
from langchain.agents import AgentState
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command
class CustomState(AgentState):
user_name: str
@tool
def set_user_name(new_name: str, runtime: ToolRuntime[None, CustomState]) -> Command:
"""Set the user's name in the conversation state."""
return Command(
update={
"user_name": new_name,
"messages": [
ToolMessage(
content=f"User name set to {new_name}.",
tool_call_id=runtime.tool_call_id,
)
],
}
)
도구가 state 변수를 갱신한다면 그 필드에 리듀서(reducer)를 정의하는 걸 고려하세요. LLM은 여러 도구를 병렬로 호출할 수 있으므로, 같은 state 필드가 동시에 갱신될 때 충돌을 어떻게 해결할지 리듀서가 정해줍니다.
문맥 – Context
Context는 호출 시점에 전달되는 불변 설정 데이터예요. 사용자 ID, 세션 정보, 대화 중 바뀌지 않아야 하는 앱별 설정에 사용합니다.
thread_id ( config={"configurable": {"thread_id": ...}}로 전달)가 대화의 메시지 기록과 체크포인트 범위를 지정한다면, context는 도구와 미들웨어가 호출 시점에 읽는 실행별 데이터를 담아요. 프로덕션에서는 보통 둘을 함께 넘깁니다: 대화마다 안정적인 thread_id, 그리고 매 invoke마다 context 객체를요.
runtime.context로 context에 접근하고, 대화가 턴마다 영속되도록 thread_id와 함께 전달하세요.
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime
from langchain_core.utils.uuid import uuid7
from langchain_openai import ChatOpenAI
USER_DATABASE = {
"user123": {
"name": "Alice Johnson",
"account_type": "Premium",
"balance": 5000,
"email": "[email protected]",
},
"user456": {
"name": "Bob Smith",
"account_type": "Standard",
"balance": 1200,
"email": "[email protected]",
},
}
@dataclass
class UserContext:
user_id: str
@tool
def get_account_info(runtime: ToolRuntime[UserContext]) -> str:
"""Get the current user's account information."""
user_id = runtime.context.user_id
if user_id in USER_DATABASE:
user = USER_DATABASE[user_id]
return (
f"Account holder: {user['name']}\n"
f"Type: {user['account_type']}\n"
f"Balance: ${user['balance']}"
)
return "User not found"
model = ChatOpenAI(model="google_genai:gemini-3.6-flash")
agent = create_agent(
model,
tools=[get_account_info],
context_schema=UserContext,
system_prompt="You are a financial assistant.",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What's my current balance?"}]},
config={"configurable": {"thread_id": str(uuid7())}},
context=UserContext(user_id="user123"),
)
장기 메모리 – Store (Long-term memory)
BaseStore는 대화를 넘어 유지되는 영구 저장소를 제공해요. state(단기 메모리)와 달리 store에 저장된 데이터는 이후 세션에서도 계속 사용할 수 있습니다. runtime.store로 접근하며, namespace/key 패턴으로 데이터를 구성합니다.
프로덕션 배포에서는 InMemoryStore 대신 PostgresStore, MongoDBStore, RedisStore 같은 영구 store 구현을 사용하세요. 자세한 설정은 메모리 문서를 참고합니다.
from langgraph.store.memory import InMemoryStore
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime
from langchain_openai import ChatOpenAI
# Access memory
@tool
def get_user_info(user_id: str, runtime: ToolRuntime) -> str:
"""Look up user info."""
store = runtime.store
user_info = store.get(("users",), user_id)
return str(user_info.value) if user_info else "Unknown user"
# Update memory
@tool
def save_user_info(user_id: str, name: str, age: int, email: str, runtime: ToolRuntime) -> str:
"""Save user info."""
store = runtime.store
store.put(("users",), user_id, {"name": name, "age": age, "email": email})
return "Successfully saved user info."
model = ChatOpenAI(model="gpt-5.5")
store = InMemoryStore()
agent = create_agent(
model,
tools=[get_user_info, save_user_info],
store=store
)
# First session: save user info
agent.invoke({
"messages": [{"role": "user", "content": "Save the following user: userid: abc123, name: Foo, age: 25, email: [email protected]"}]
})
# Second session: get user info
agent.invoke({
"messages": [{"role": "user", "content": "Get user info for user with id 'abc123'"}]
})
# Here is the user info for user with ID "abc123":
# - Name: Foo
# - Age: 25
# - Email: [email protected]
스트림 라이터 (Stream writer)
오래 걸리는 작업 동안 사용자에게 진행 상황을 알려주려면 도구가 실시간 업데이트를 스트리밍하게 할 수 있어요. runtime.stream_writer로 커스텀 업데이트를 방출합니다.
from langchain.tools import tool, ToolRuntime
@tool
def get_weather(city: str, runtime: ToolRuntime) -> str:
"""Get weather for a given city."""
writer = runtime.stream_writer
# Stream custom updates as the tool executes
writer(f"Looking up data for city: {city}")
writer(f"Acquired data for city: {city}")
return f"It's always sunny in {city}!"
도구 안에서 runtime.stream_writer를 쓰려면 그 도구가 LangGraph 실행 컨텍스트 안에서 호출되어야 해요. 자세한 내용은 Streaming 문서를 참고하세요.
실행 정보 (Execution info)
도구 안에서 runtime.execution_info로 thread ID, run ID, 재시도 상태에 접근할 수 있어요.
from langchain.tools import tool, ToolRuntime
@tool
def log_execution_context(runtime: ToolRuntime) -> str:
"""Log execution identity information."""
info = runtime.execution_info
print(f"Thread: {info.thread_id}, Run: {info.run_id}")
print(f"Attempt: {info.node_attempt}")
return "done"
deepagents>=0.5.0(또는 langgraph>=1.1.5)이 필요합니다.
서버 정보 (Server info)
도구가 LangGraph Server에서 실행될 때 runtime.server_info로 assistant ID, graph ID, 인증된 사용자에 접근할 수 있어요.
from langchain.tools import tool, ToolRuntime
@tool
def get_assistant_scoped_data(runtime: ToolRuntime) -> str:
"""Fetch data scoped to the current assistant."""
server = runtime.server_info
if server is not None:
print(f"Assistant: {server.assistant_id}, Graph: {server.graph_id}")
if server.user is not None:
print(f"User: {server.user.identity}")
return "done"
server_info는 도구가 LangGraph Server에서 실행되지 않을 때(예: 로컬 개발이나 테스트 중) None이에요. deepagents>=0.5.0(또는 langgraph>=1.1.5)이 필요합니다.
도구 실행 (Tool execution)
LangChain에서 도구는 에이전트(예: create_agent)가 사용하며, 도구 오류 처리는 미들웨어를 통해 구성합니다. LangGraph 워크플로에서는 도구 실행을 ToolNode가 처리해요. Graph API 사용법과 도구가 현재 그래프 state와 run-scoped context에 접근하는 방법은 ToolNode 문서를 참고하세요.
도구 반환 값 (Tool return values)
도구의 반환 값은 상황에 따라 다르게 고를 수 있어요.
- 문자열(string) – 사람이 읽을 수 있는 결과용.
- 객체(object) – 모델이 파싱해야 할 구조화된 결과용.
- Command (+ 선택적 메시지) – state에 기록해야 할 때.
문자열 반환: 도구가 모델이 읽고 다음 응답에 쓸 평문을 제공해야 할 때 씁니다.
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get weather for a city."""
return f"It is currently sunny in {city}."
동작: 반환 값이 ToolMessage로 변환되고, 모델이 그 텍스트를 보고 다음에 무엇을 할지 결정합니다. 모델이나 다른 도구가 나중에 바꾸지 않는 한 에이전트 state 필드는 바뀌지 않아요. 결과가 자연스럽게 사람이 읽을 텍스트일 때 사용하세요.
객체 반환: 도구가 모델이 살펴봐야 할 구조화된 데이터를 만들 때 씁니다.
from langchain.tools import tool
@tool
def get_weather_data(city: str) -> dict:
"""Get structured weather data for a city."""
return {
"city": city,
"temperature_c": 22,
"conditions": "sunny",
}
동작: 객체가 직렬화되어 도구 출력으로 돌아갑니다. 모델은 특정 필드를 읽고 그 위에서 추론할 수 있어요. 문자열 반환처럼 그래프 state를 직접 갱신하지는 않습니다. 자유 형식 텍스트보다 명시적 필드가 다운스트림 추론에 도움이 될 때 사용하세요.
멀티모달 콘텐츠 반환: 도구는 평문으로 제한되지 않아요. 모델이 멀티모달 도구 결과를 지원하면, 표준 콘텐츠 블록을 반환해서 텍스트, 이미지, 그 밖의 미디어를 한 도구 결과 안에 담아 모델에 전달할 수 있습니다.
from langchain.tools import tool
@tool
def capture_screenshot() -> list[dict]:
"""Capture a screenshot of the current page."""
return [
{"type": "text", "text": "Screenshot of the current page:"},
{"type": "image", "url": "https://example.com/page.png"},
]
동작: 반환 값이 멀티모달 콘텐츠가 담긴 ToolMessage로 변환됩니다. 도구 실행 후 message.content_blocks로 정규화된 블록 목록을 읽을 수 있어요. 반환하는 모달리티를 모델이 지원해야 하므로, 이미지·오디오·비디오를 반환하기 전에 모델의 능력을 확인하세요. 블록 타입과 프로바이더별 요구사항은 Multimodal messages 문서를 참고합니다. MCP 도구가 이미지나 혼합 콘텐츠를 반환해도 같은 방식으로 변환돼요 (Multimodal content 참고).
Command 반환: 도구가 그래프 state를 갱신해야 할 때(예: 사용자 선호나 앱 state 설정) Command를 반환합니다. Command가 현재 그래프를 대상으로 하면, 해당 도구 호출 ID와 일치하는 ToolMessage를 갱신 항목에 포함해야 해요. 메시지 기록의 모든 도구 호출에는 대응하는 ToolMessage가 있어야 합니다. tool_call_id 파라미터에는 runtime.tool_call_id를 사용하세요. 갱신에 도구 호출과 일치하는 ToolMessage가 없으면 ToolNode가 ValueError를 발생시켜요.
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command
@tool
def set_language(language: str, runtime: ToolRuntime) -> Command:
"""Set the preferred response language."""
return Command(
update={
"preferred_language": language,
"messages": [
ToolMessage(
content=f"Language set to {language}.",
tool_call_id=runtime.tool_call_id,
)
],
}
)
동작: Command가 update로 state를 갱신하고, 갱신된 state는 같은 실행의 이후 단계에서 사용할 수 있어요. 병렬 도구 호출로 갱신될 수 있는 필드에는 리듀서를 쓰세요. 데이터를 반환할 뿐만 아니라 에이전트 state까지 변경하는 도구에 사용합니다.
도구에서 바로 반환하기 (Return directly from a tool)
도구에 return_direct를 설정하면 에이전트 루프를 단락(short-circuit)시켜요. 에이전트가 도구의 출력을 추가 처리 없이 즉시 호출자에게 반환하고, 더 이상의 모델 호출을 거치지 않습니다.
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
@tool(return_direct=True)
def fetch_order_status(order_id: str) -> str:
"""Fetch the current status of a customer order."""
# In production, query your order management system here
return f"Order {order_id} is shipped and will arrive in 2 days."
agent = create_agent(
ChatOpenAI(model="google_genai:gemini-3.6-flash"),
tools=[fetch_order_status],
)
result = agent.invoke({
"messages": [{"role": "user", "content": "What is the status of order #12345?"}]
})
# The agent returns the tool output directly without another LLM call:
# "Order 12345 is shipped and will arrive in 2 days."
동작: 도구는 정상 실행되고 출력이 ToolMessage로 감싸집니다. 에이전트는 루프를 멈추고 도구의 출력을 최종 응답으로 반환하여 추가 모델 호출을 건너뜁니다. 여러 병렬 도구 호출이 있으면: 모델이 한 단계에서 여러 도구를 호출했다면 모두 먼저 실행되고, 그 배치의 모든 도구가 return_direct=True일 때만 에이전트가 END로 라우팅합니다. 최종 응답에는 그 단계에서 호출된 모든 도구의 ToolMessage 출력이 포함됩니다.
이 경우에 사용하세요: 도구의 출력이 완전하고 사용자에게 바로 보여줄 수 있는 답일 때(예: 바로 표시할 수 있는 결과를 반환하는 조회), 추가 추론이 필요 없어 추가 모델 호출을 피하고 싶을 때, 모델이 도구 결과를 다시 표현하거나 요약하거나 조작할 수 없는 결정적·수정되지 않은 출력이 필요할 때. 모델이 도구 출력을 처리하지 않으므로, return_direct=True는 결과에 추가 추론·요약·다른 도구 호출과의 체이닝이 필요한 도구에는 적합하지 않아요. 혼합 병렬 호출: 모델이 return_direct=True 도구와 그렇지 않은 도구를 함께 호출하면 에이전트는 그 단계에서 종료하지 않고, 배치의 모든 ToolMessage와 함께 모델로 다시 라우팅해서 모든 결과를 추론하게 합니다. return_direct는 단계의 모든 도구 호출이 return_direct=True일 때만 루프를 단락시켜요.
return_direct=True 도구는 에이전트가 종료하기 전에 그래프 state를 갱신하기 위해 Command를 반환할 수도 있어요. 일반 반환 값과 달리 Command는 자동으로 ToolMessage로 변환되지 않습니다. Command가 현재 그래프를 대상으로 하면(graph가 설정되지 않았거나 None), Command.update에 도구 호출의 tool_call_id와 일치하는 ToolMessage를 포함하세요. 빼먹으면 ToolNode가 ValueError를 발생시켜요. 메시지 기록의 모든 AIMessage 도구 호출에는 대응하는 ToolMessage가 있어야 하기 때문이죠.
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command
@tool(return_direct=True)
def fetch_and_store_order(order_id: str, runtime: ToolRuntime) -> Command:
"""Fetch order status and store it in state."""
status = f"Order {order_id} is shipped and will arrive in 2 days."
return Command(
update={
"last_order_status": status,
# Must include a ToolMessage so the message history stays valid
"messages": [
ToolMessage(
content=status,
tool_call_id=runtime.tool_call_id,
)
],
}
)
부모 그래프에 쓰려면 graph=Command.PARENT로 설정하세요. 그 경우 실행이 현재 그래프를 완전히 떠나므로 ToolMessage 요구사항이 완화됩니다.
오류 처리 (Error handling)
도구 오류는 LangChain 에이전트 미들웨어로 처리해서 실패한 도구 호출을 재시도하거나 커스텀 오류 메시지를 반환할 수 있어요.
from collections.abc import Callable
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
from langchain.tools.tool_node import ToolCallRequest
@wrap_tool_call
def handle_tool_errors(
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage],
) -> ToolMessage:
"""Convert tool exceptions into ToolMessages the model can handle."""
try:
return handler(request)
except Exception as e:
return ToolMessage(
content=f"Tool error: Please check your input and try again. ({e})",
tool_call_id=request.tool_call["id"],
)
agent = create_agent(
model="google_genai:gemini-3.6-flash",
tools=[],
middleware=[handle_tool_errors],
)
상태 주입 (State injection)
도구는 ToolRuntime을 통해 그래프 state에 접근합니다. state, context, store, 스트리밍 API에 대한 자세한 내용은 Access context 섹션을 참고하세요.
from langchain.tools import tool, ToolRuntime
@tool
def get_message_count(runtime: ToolRuntime) -> str:
"""Get the number of messages in the conversation."""
messages = runtime.state["messages"]
return f"There are {len(messages)} messages."
동적 도구 선택 (Dynamic tool selection)
동적 도구(dynamic tools)를 사용하면 에이전트가 쓸 수 있는 도구 집합을 처음에 모두 정의하는 대신 런타임에 수정할 수 있어요. 모든 도구가 모든 상황에 적합한 건 아니죠. 도구가 너무 많으면 모델을 압도해서(컨텍스트 과부하) 오류가 늘어나고, 너무 적으면 능력이 제한됩니다. 동적 도구 선택은 인증 상태, 사용자 권한, 기능 플래그, 대화 단계에 따라 사용 가능한 도구 집합을 조정하게 해줍니다. 도구를 미리 아는지에 따라 두 가지 접근법이 있어요: 사전 등록 도구 필터링, 런타임 도구 등록.
모든 가능한 도구가 에이전트 생성 시점에 알려져 있다면, 사전 등록하고 state·권한·맥락에 따라 모델에 노출할 도구를 동적으로 필터링할 수 있어요.
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable
@wrap_model_call
def state_based_tools(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
"""Filter tools based on conversation State."""
# Read from State: check if user has authenticated
state = request.state
is_authenticated = state.get("authenticated", False)
message_count = len(state["messages"])
# Only enable sensitive tools after authentication
if not is_authenticated:
tools = [t for t in request.tools if t.name.startswith("public_")]
request = request.override(tools=tools)
elif message_count < 5:
# Limit tools early in conversation
tools = [t for t in request.tools if t.name != "advanced_search"]
request = request.override(tools=tools)
return handler(request)
agent = create_agent(
model="gpt-5.5",
tools=[public_search, private_search, advanced_search],
middleware=[state_based_tools]
)
이 접근법은 다음 경우에 좋아요: 모든 가능한 도구를 컴파일/시작 시점에 알고 있을 때, 권한·기능 플래그·대화 state에 따라 필터링하고 싶을 때, 도구는 정적이지만 그 가용성은 동적일 때. 더 많은 예시는 Dynamically selecting tools 문서를 참고하세요.
헤드리스 도구 (Headless tools)
일부 도구는 서버 프로세스 내부가 아니라 사용자의 앱(보통 브라우저)이 실행되는 곳에서 실행돼야 해요. 헤드리스 도구(headless tools)는 이름·설명·인자 스키마를 포함하는 도구 정의로, 서버에 에이전트와 함께 등록합니다. 실제 구현은 클라이언트에만 등록되고 짧은 interrupt/resume 핸드셰이크 후 클라이언트에서 실행됩니다. 함수 본문이 서버에서 실행되는 일반 도구나, 모델 프로바이더가 내장 도구를 원격으로 실행하는 server-side tool use와는 달라요.
언제 쓸까 (When to use)
작업이 클라이언트에만 존재하는 환경·기기·UI에 의존할 때 사용하세요. 예: 브라우저 API(Geolocation, IndexedDB, Clipboard, Canvas 2D, file pickers, Battery API 등), 프라이버시와 로컬리티(데이터가 기기에 남을 때, 예: IndexedDB의 로컬 'memory'), 지연 시간(순수 로컬 작업에 서버 왕복이 필요 없음), 구조화된 안전한 효과(임의 코드를 eval에 보내는 대신 작고 타입이 있는 도구를 많이 쓰기, 예: 캔버스 프리미티브마다 도구 하나).
패턴이 어떻게 동작하나 (How the pattern works)
두 런타임 모두에서 모델은 호출할 수 있는 일반 도구를 보지만, 실제 실행은 서버 프로세스 밖에서 일어나요.
langchain.tools에서tool(name=..., description=..., args_schema=...)로 헤드리스 도구를 정의합니다. 헤드리스 도구는 스키마만 있고 프로세스 내 구현은 없어요.- 그 도구를
create_agent나 LangGraph 그래프에 등록해서 모델이 정상 호출하게 합니다. - 도구가 호출되면 interrupt 페이로드를 처리합니다. 로컬에서 실행하는 대신 그래프가
{"type": "tool", "tool_call": {"id", "name", "args"}}형태의 페이로드로 멈춥니다. - 앱·다른 서비스·사람 단계가 그 행동을 수행한 뒤 그래프를 재개(resume)합니다. 브라우저 기반 흐름에서는 프론트엔드에 스키마를 미러링하고 거기에
.implement(...)를 붙이면 됩니다.
Python에서 tool(...)을 name, description, args_schema만 주고 호출하면 LangChain은 HeadlessTool을 반환해요. Python 쪽에는 .implement() API가 없습니다. 모델이 이 도구 중 하나에 대해 도구 호출을 발행하면 실행을 로컬에서 하는 대신 실행이 interrupt되고, 앱이 페이로드를 살펴보고 올바른 환경(브라우저, 다른 서비스, 사람 검토 단계)에서 작업을 수행한 뒤 도구 결과로 그래프를 재개할 수 있어요. 지원되는 JS SDK 훅을 쓰면 헤드리스 도구 interrupt를 감지하고, 일치하는 클라이언트 구현을 실행하며, resume 명령을 자동으로 제출합니다. 선택적 onTool 콜백으로 시작·성공·오류 같은 생애주기 이벤트를 관찰해서 스피너나 토스트 같은 UI 피드백에 쓸 수 있어요.
사전 구축 도구 (Prebuilt tools)
LangChain은 웹 검색, 코드 해석, 데이터베이스 접근 등 흔한 작업을 위한 많은 사전 구축 도구와 도구 키트를 제공합니다. 이런 즉시 사용 가능한 도구는 커스텀 코드 없이 에이전트에 바로 통합할 수 있어요. 카테고리별 전체 도구 목록은 tools and toolkits 통합 페이지를 참고하세요.
MCP 서버의 도구 (Tools from MCP servers)
Model Context Protocol(MCP)은 애플리케이션이 언어 모델에 도구를 노출하는 방식을 표준화하는 오픈 프로토콜이에요. 도구를 직접 작성하는 대신 MCP 서버에 연결해서 서버가 광고하는 도구를 LangChain 도구로 변환하고, 에이전트에 다른 도구처럼 전달합니다. MCPAdapter가 서버의 도구를 발견해서 LangChain 도구로 변환해요. 어댑터를 열고 list_tools()를 호출하고 그 결과를 create_agent에 넘기면 됩니다.
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter
async with MCPAdapter("https://example.com/mcp") as adapter:
tools = await adapter.list_tools()
agent = create_agent("claude-sonnet-4-6", tools)
langchain.mcp 네임스페이스는 langchain[mcp]>=1.4.0이 필요하며 베타 상태예요. API가 바뀔 수 있습니다. 전송, 인증, 여러 서버, 도구 결과 처리에 대해서는 Model Context Protocol(MCP) 문서를 참고하세요.
서버 측 도구 사용 (Server-side tool use)
일부 채팅 모델은 모델 프로바이더가 서버 측에서 실행하는 내장 도구를 갖고 있어요. 여기에는 도구 로직을 정의하거나 호스팅할 필요가 없는 웹 검색, 코드 인터프리터 같은 기능이 포함됩니다. 이런 내장 도구를 활성화하고 사용하는 방법은 개별 채팅 모델 통합 페이지와 tool calling 문서를 참고하세요.