도구
도구 (Tools · LangChain Python)
도구(Tools)는 에이전트가 할 수 있는 일을 확장해 줘요. 실시간 데이터를 가져오고, 코드를 실행하고, 외부 데이터베이스를 조회하고, 세상에서 실제로 행동을 취할 수 있게 해 주죠.
내부적으로 도구는 입력과 출력이 명확히 정의된 호출 가능한 함수이며, 채팅 모델에 전달돼요. 모델은 대화 문맥에 따라 언제 어떤 입력 인자로 도구를 호출할지 결정해요.
출처: 공식문서
💡 모델이 도구 호출을 어떻게 처리하는지 자세히 보려면 도구 호출을 확인하세요. LangSmith로 도구 호출을 추적하고 오류를 디버깅하세요. tracing quickstart를 따라 설정하세요. 추적을 모니터링하고 문제를 감지해 해결책을 제안하는 LangSmith Engine도 함께 구성하는 걸 권장해요.
도구 만들기
기본 도구 정의
도구를 만드는 가장 간단한 방법은 @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은 도구의 목적을 모델이 이해하도록 정보를 담고 간결하게 써야 해요.
📝 서버 측 도구 사용: 일부 채팅 모델에는 서버 측에서 실행되는 내장 도구(웹 검색, 코드 인터프리터)가 있어요. 자세한 내용은 서버 측 도구 사용을 보세요.
⚠️ 도구 이름은
snake_case를 쓰는 게 좋아요 (예:Web Search대신web_search). 일부 모델 프로바이더는 공백이나 특수 문자가 들어간 이름을 문제 삼거나 거부해요. 영숫자, 밑줄, 하이픈만 쓰면 프로바이더 간 호환성이 좋아져요.
도구 속성 커스터마이즈
커스텀 도구 이름
기본적으로 도구 이름은 함수 이름에서 나와요. 더 설명적인 이름이 필요할 때는 덮어쓸 수 있어요.
@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
커스텀 도구 설명
자동 생성된 도구 설명을 덮어써 모델에 더 명확한 안내를 줄 수 있어요.
@tool("calculator", description="Performs arithmetic calculations. Use this for any math problems.")
def calc(expression: str) -> str:
"""Evaluate mathematical expressions."""
return str(eval(expression))
고급 스키마 정의
복잡한 입력은 Pydantic 모델이나 JSON 스키마로 정의해요.
# Pydantic model
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
# JSON Schema
weather_schema = {
"type": "object",
"properties": {
"location": {"type": "string"},
"units": {"type": "string"},
"include_forecast": {"type": "boolean"}
},
"required": ["location", "units", "include_forecast"]
}
@tool(args_schema=weather_schema)
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
예약된 인자 이름
다음 파라미터 이름은 예약되어 있어서 도구 인자로 쓸 수 없어요. 이 이름을 쓰면 런타임 오류가 발생해요.
| 파라미터 이름 | 용도 |
|---|---|
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)
State는 대화 동안 존재하는 단기 메모리를 나타내요. 메시지 기록과 그래프 상태에 정의한 커스텀 필드를 포함해요.
State 접근
도구 시그니처에 runtime: ToolRuntime을 추가하면 state에 접근할 수 있어요. 호출 시 ToolNode가 값을 자동으로 주입하며, 이 파라미터는 모델에 보내는 도구 스키마에는 포함되지 않아요. runtime.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를 업데이트할 수 있어요. 커스텀 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와 함께 전달해요.
# Google
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"),
)
같은 예시를 OpenAI(ChatOpenAI(model="openai:gpt-5.5")), Anthropic(ChatOpenAI(model="anthropic:claude-sonnet-4-6")), OpenRouter(ChatOpenAI(model="openrouter:z-ai/glm-5.2")), Fireworks(ChatOpenAI(model="fireworks:accounts/fireworks/models/glm-5p2")), Baseten(ChatOpenAI(model="baseten:zai-org/GLM-5.2")), Ollama(ChatOpenAI(model="ollama:north-mini-code-1.0"))로도 쓸 수 있어요. 모델 문자열만 바뀌고 나머지 코드는 동일해요.
장기 메모리 (Store)
BaseStore는 대화를 넘어 유지되는 영구 저장소를 제공해요. state(단기 메모리)와 달리 store에 저장된 데이터는 이후 세션에서도 계속 사용할 수 있어요.
runtime.store로 store에 접근하세요. store는 네임스페이스/키 패턴으로 데이터를 구성해요.
💡 프로덕션 배포에서는
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 실행 컨텍스트 안에서 호출해야 해요. 자세한 내용은 스트리밍을 보세요.
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}") # [!code highlight]
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}") # [!code highlight]
if server.user is not None:
print(f"User: {server.user.identity}") # [!code highlight]
return "done"
server_info는 도구가 LangGraph Server에서 실행되지 않을 때(예: 로컬 개발·테스트 중) None이에요.
📝
deepagents>=0.5.0(또는langgraph>=1.1.5)이 필요해요.
이전 주입 패턴에서 마이그레이션 — 이전 예시들은 InjectedState, InjectedStore, get_runtime(), InjectedToolCallId를 사용했어요. state, context, store, 실행 메타데이터에 접근하는 하나의 명시적 인터페이스로 ToolRuntime을 쓰세요.
이전 패턴:
from langchain.tools import tool, InjectedState
@tool
def summarize(state: InjectedState) -> str:
"""Summarize the conversation."""
messages = state["messages"]
return f"Conversation length: {len(messages)} messages."
권장 패턴:
from langchain.tools import tool, ToolRuntime
@tool
def summarize(runtime: ToolRuntime) -> str:
"""Summarize the conversation."""
messages = runtime.state["messages"]
return f"Conversation length: {len(messages)} messages."
에이전트 수준 마이그레이션(예: create_react_agent와 커스텀 state)은 LangChain v1 마이그레이션 가이드를 보세요.
도구 실행 (Tool execution)
LangChain에서 도구는 에이전트가 사용하며(예: create_agent), 도구 오류 처리는 미들웨어로 구성해요.
LangGraph 워크플로에서는 도구 실행을 ToolNode가 담당해요. Graph API 사용법은 ToolNode를 보세요 (도구가 현재 그래프 상태와 실행 범위 context에 접근하는 방법 포함).
도구 반환값
도구에 다양한 반환값을 선택할 수 있어요.
- 사람이 읽을 결과에는
string을 반환 - 모델이 파싱해야 할 구조화된 결과에는
object를 반환 - state에 써야 할 때는 선택적 메시지와 함께
Command를 반환
문자열 반환
도구가 모델이 읽고 다음 응답에 쓸 일반 텍스트를 제공해야 할 때 문자열을 반환해요.
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 필드는 바뀌지 않아요.
결과가 자연스럽게 사람이 읽을 텍스트일 때 이 방식을 써요.
객체 반환
도구가 모델이 살펴봐야 할 구조화된 데이터를 만들 때는 객체(예: dict)를 반환해요.
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"},
]
동작:
- 반환값이 멀티모달
content를 가진ToolMessage로 변환돼요. - 도구 실행 후
message.content_blocks로 정규화된 블록 리스트를 읽어요. - 반환하는 모달리티를 모델이 지원해야 해요. 이미지·오디오·비디오를 반환하기 전에 모델의 역량을 확인하세요.
블록 타입과 프로바이더별 요구 사항은 멀티모달 메시지를 보세요. 이미지나 혼합 콘텐츠를 반환하는 MCP 도구도 같은 방식으로 변환돼요. 멀티모달 콘텐츠를 보세요.
Command 반환
도구가 그래프 state를 업데이트해야 할 때(예: 사용자 선호나 앱 state 설정) Command를 반환해요. Command가 현재 그래프를 대상으로 하면, 업데이트에 현재 도구 호출과 일치하는 tool_call_id를 가진 ToolMessage를 포함하세요. 메시지 기록의 모든 도구 호출에는 대응하는 ToolMessage가 있어야 해요.
tool_call_id 파라미터에는 runtime.tool_call_id를 쓰세요. ToolNode는 이 요구 사항을 강제해요: 업데이트에 도구 호출과 일치하는 ToolMessage가 없으면 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,
)
],
}
)
동작:
update로 state를 업데이트해요.- 업데이트된 state는 같은 실행의 이후 단계에서 사용할 수 있어요.
- 병렬 도구 호출로 업데이트될 수 있는 필드에는 리듀서를 쓰세요.
도구가 데이터만 반환하는 게 아니라 에이전트 state도 변경할 때 이 방식을 써요.
도구에서 직접 반환 (Return direct)
도구에 return direct를 설정하면 에이전트 루프를 단락(short-circuit)시켜요. 에이전트가 도구의 출력을 추가 처리 없이 즉시 호출자에게 반환하고, 모델로 다시 보내지 않아요.
# Google
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."
같은 예시를 OpenAI(ChatOpenAI(model="openai:gpt-5.5")), Anthropic(ChatOpenAI(model="anthropic:claude-sonnet-4-6")), OpenRouter(ChatOpenAI(model="openrouter:z-ai/glm-5.2")), Fireworks(ChatOpenAI(model="fireworks:accounts/fireworks/models/glm-5p2")), Baseten(ChatOpenAI(model="baseten:zai-org/GLM-5.2")), Ollama(ChatOpenAI(model="ollama:north-mini-code-1.0"))로도 쓸 수 있어요. 모델 문자열만 바뀌고 나머지 코드는 동일해요.
동작:
- 도구가 정상 실행되고 그 출력이
ToolMessage에 담겨요. - 에이전트가 루프를 멈추고 도구 출력을 최종 응답으로 반환하며, 추가 모델 호출을 건너뛰어요.
- 여러 병렬 도구 호출: 모델이 한 단계에서 여러 도구를 호출하면 모두 먼저 실행돼요. 모든 도구가 끝난 뒤, 에이전트는 그 배치의 모든 도구가
return_direct=True일 때만END로 라우팅해요. 최종 응답에는 그 단계에서 호출된 모든 도구의ToolMessage출력이 포함돼요.
이 방식을 쓸 때:
- 도구의 출력이 완전하고 사용자에게 바로 줄 수 있는 답일 때 (예: 바로 표시할 수 있는 결과를 반환하는 조회)
- 추가 추론이 필요 없을 때 모델 호출을 한 번 더 피하고 싶을 때
- 결정적이고 수정되지 않은 출력이 필요할 때: 모델이 결과를 바꿔 말하거나, 요약하거나, 그 위에서 행동할 수 없어요
⚠️ 모델이 도구 출력을 처리하지 않으므로,
return_direct=True는 결과에 추가 추론·요약 또는 다른 도구 호출과의 체이닝이 필요한 도구에는 적합하지 않아요.
⚠️ 혼합 병렬 호출: 모델이
return_direct=True도구를 그렇지 않은 도구와 함께 호출하면, 에이전트는 그 단계 후에 종료하지 않아요. 배치의 모든ToolMessage를 모델로 다시 보내 모델이 모든 결과 위에서 추론하게 해요.return_direct는 그 단계의 모든 도구 호출이return_direct=True일 때만 루프를 단락시켜요.
return_direct와 함께 Command 반환
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 에이전트 미들웨어로 도구 오류를 처리해요.
# Google
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],
)
같은 예시를 OpenAI(model="openai:gpt-5.5"), Anthropic(model="anthropic:claude-sonnet-4-6"), OpenRouter(model="openrouter:z-ai/glm-5.2"), Fireworks(model="fireworks:accounts/fireworks/models/glm-5p2"), Baseten(model="baseten:zai-org/GLM-5.2"), Ollama(model="ollama:north-mini-code-1.0")로도 쓸 수 있어요. 모델 문자열만 바뀌고 나머지 코드는 동일해요.
State 주입
도구는 ToolRuntime으로 그래프 state에 접근해요. state, context, store, 스트리밍 API는 문맥 접근을 보세요.
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."
도구에서 state, context, 장기 메모리에 접근하는 더 자세한 내용은 문맥 접근을 보세요.
동적 도구 선택 (Dynamic tool selection)
동적 도구를 쓰면 에이전트가 사용할 수 있는 도구 세트가 처음에 모두 정의되는 대신 런타임에 변경돼요. 모든 도구가 모든 상황에 적합한 건 아니죠. 도구가 너무 많으면 모델을 압도하고(컨텍스트 과부하) 오류를 늘리며, 너무 적으면 역량이 제한돼요. 동적 도구 선택은 인증 상태, 사용자 권한, 기능 플래그, 대화 단계에 따라 사용 가능한 도구 세트를 적응시키는 것을 가능하게 해 줘요.
도구를 사전에 아느냐에 따라 두 가지 접근법이 있어요.
사전 등록 도구 필터링 — 모든 가능한 도구를 에이전트 생성 시 알면, 사전 등록하고 state·권한·문맥에 따라 모델에 노출되는 도구를 동적으로 필터링할 수 있어요.
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]
)
store 기준(사용자 선호나 기능 플래그로 필터링):
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable
from langgraph.store.memory import InMemoryStore
@dataclass
class Context:
user_id: str
@wrap_model_call
def store_based_tools(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
"""Filter tools based on Store preferences."""
user_id = request.runtime.context.user_id
# Read from Store: get user's enabled features
store = request.runtime.store
feature_flags = store.get(("features",), user_id)
if feature_flags:
enabled_features = feature_flags.value.get("enabled_tools", [])
# Only include tools that are enabled for this user
tools = [t for t in request.tools if t.name in enabled_features]
request = request.override(tools=tools)
return handler(request)
agent = create_agent(
model="gpt-5.5",
tools=[search_tool, analysis_tool, export_tool],
middleware=[store_based_tools],
context_schema=Context,
store=InMemoryStore()
)
Runtime context 기준(사용자 권한으로 필터링):
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable
@dataclass
class Context:
user_role: str
@wrap_model_call
def context_based_tools(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
"""Filter tools based on Runtime Context permissions."""
# Read from Runtime Context: get user role
if request.runtime is None or request.runtime.context is None:
# If no context provided, default to viewer (most restrictive)
user_role = "viewer"
else:
user_role = request.runtime.context.user_role
if user_role == "admin":
# Admins get all tools
pass
elif user_role == "editor":
# Editors can't delete
tools = [t for t in request.tools if t.name != "delete_data"]
request = request.override(tools=tools)
else:
# Viewers get read-only tools
tools = [t for t in request.tools if t.name.startswith("read_")]
request = request.override(tools=tools)
return handler(request)
agent = create_agent(
model="gpt-5.5",
tools=[read_data, write_data, delete_data],
middleware=[context_based_tools],
context_schema=Context
)
이 접근법은 이런 때 최선이에요:
- 모든 가능한 도구를 컴파일/시작 시점에 알 때
- 권한, 기능 플래그, 대화 state로 필터링하고 싶을 때
- 도구는 정적이지만 가용성은 동적일 때
더 많은 예시는 도구 동적 선택을 보세요.
런타임 도구 등록 — 도구가 런타임에 발견되거나 생성될 때(예: MCP 서버에서 로드, 사용자 데이터로 생성, 원격 레지스트리에서 가져오기) 도구를 등록하고 실행도 동적으로 처리해야 해요. 여기에는 두 개의 미들웨어 훅이 필요해요.
wrap_model_call— 요청에 동적 도구를 추가wrap_tool_call— 동적으로 추가된 도구의 실행 처리
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ToolCallRequest
# A tool that will be added dynamically at runtime
@tool
def calculate_tip(bill_amount: float, tip_percentage: float = 20.0) -> str:
"""Calculate the tip amount for a bill."""
tip = bill_amount * (tip_percentage / 100)
return f"Tip: ${tip:.2f}, Total: ${bill_amount + tip:.2f}"
class DynamicToolMiddleware(AgentMiddleware):
"""Middleware that registers and handles dynamic tools."""
def wrap_model_call(self, request: ModelRequest, handler):
# Add dynamic tool to the request
# This could be loaded from an MCP server, database, etc.
updated = request.override(tools=[*request.tools, calculate_tip])
return handler(updated)
def wrap_tool_call(self, request: ToolCallRequest, handler):
# Handle execution of the dynamic tool
if request.tool_call["name"] == "calculate_tip":
return handler(request.override(tool=calculate_tip))
return handler(request)
agent = create_agent(
model="gpt-5.5",
tools=[get_weather], # Only static tools registered here
middleware=[DynamicToolMiddleware()],
)
# The agent can now use both get_weather AND calculate_tip
result = agent.invoke({
"messages": [{"role": "user", "content": "Calculate a 20% tip on $85"}]
})
이 접근법은 이런 때 최선이에요:
- 도구를 런타임에 발견할 때 (예: MCP 서버에서)
- 도구를 사용자 데이터나 설정에 따라 동적으로 생성할 때
- 외부 도구 레지스트리와 통합할 때
📝 런타임 등록 도구에는
wrap_tool_call훅이 필수예요. 에이전트가 원래 도구 목록에 없던 도구를 어떻게 실행할지 알아야 하기 때문이에요. 없으면 에이전트는 동적으로 추가된 도구를 어떻게 호출할지 모릅니다.
Headless 도구
일부 도구는 프로세스 안이 아니라 사용자의 앱이 실행되는 곳(보통 브라우저)에서 실행돼야 해요. Headless 도구는 이름, 설명, 인자 스키마를 포함한 도구 정의로, 에이전트와 함께 서버에 등록해요. 구현은 클라이언트에만 등록되고 짧은 인터럽트/재개 핸드셰이크 후에 실행돼요.
이것은 함수 본문이 서버에서 실행되는 일반 도구와, 모델 프로바이더가 내장 도구를 원격으로 실행하는 서버 측 도구 사용과는 달라요.
Headless 도구를 쓰는 때
클라이언트에만 존재하는 환경, 기기, UI에 의존하는 작업일 때 써요. 예:
- 브라우저 API: Geolocation, IndexedDB, Clipboard, Canvas 2D, 파일 피커, Battery API 등
- 프라이버시와 로컬성: 데이터가 기기에 남아요 (예: IndexedDB의 로컬 "메모리")
- 지연 시간: 순수 로컬 작업에 서버 왕복이 없어요
- 구조적·안전한 효과:
eval에 임의 코드를 보내는 대신 작고 타입 있는 도구를 여러 개 쓰는 게 좋아요 (예: 캔버스 프리미티브당 도구 하나)
패턴이 동작하는 방식
두 런타임 모두에서 모델은 호출할 수 있는 일반 도구를 보지만, 실제 실행은 서버 프로세스 밖에서 일어나요.
langchain.tools의tool(name=..., description=..., args_schema=...)로 headless 도구를 정의하세요. headless 도구는 스키마만 있고 프로세스 내 구현은 없어요.- 그 도구를
create_agent나 LangGraph 그래프에 등록해 모델이 정상적으로 호출할 수 있게 하세요. - 도구가 호출되면 인터럽트 페이로드를 처리하세요. 로컬 실행 대신 그래프가
{"type": "tool", "tool_call": {"id", "name", "args"}}형식의 페이로드로 멈춰요. - 앱, 다른 서비스, 또는 사람의 단계가 행동을 수행한 뒤 그래프를 재개하세요. 브라우저 기반 흐름에서는 프런트엔드에서 스키마를 미러링하고 거기에
.implement(...)를 붙일 수 있어요.
ℹ️ Python에서
tool(...)를name,description,args_schema만으로 호출하면 LangChain은HeadlessTool을 반환해요. Python 쪽에는.implement()API가 없어요.
모델이 이런 도구 중 하나에 대한 도구 호출을 발행하면, 로컬로 실행하는 대신 실행이 인터럽트돼요. 앱이 페이로드를 검사하고 올바른 환경(예: 브라우저, 다른 서비스, 사람 검토 단계)에서 행동을 수행한 뒤, 도구 결과로 그래프를 재개할 수 있어요. 지원되는 JS SDK 훅을 쓰면 headless 도구 인터럽트를 감지하고, 일치하는 클라이언트 구현을 실행하고, 재개 명령을 제출해 주는 경우가 많아요.
선택적 onTool 콜백으로 수명주기 이벤트(start, success, error)를 관찰해 스피너나 토스트 같은 UI 피드백에 쓸 수 있어요.
schema-only 도구를 useStream으로 클라이언트에서 실행하는 종단 간 예시는 Headless 도구 프런트엔드 패턴을 보세요.
사전 내장 도구 (Prebuilt tools)
LangChain은 웹 검색, 코드 해석, 데이터베이스 접근 등 일반적인 작업을 위한 방대한 사전 내장 도구·툴킷 컬렉션을 제공해요. 이런 바로 쓸 수 있는 도구는 커스텀 코드를 작성하지 않고도 에이전트에 직접 통합할 수 있어요.
카테고리별로 정리된 사용 가능한 도구 전체 목록은 도구·툴킷 통합 페이지를 보세요.
MCP 서버의 도구
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)
일부 채팅 모델에는 모델 프로바이더가 서버 측에서 실행하는 내장 도구가 있어요. 웹 검색과 코드 인터프리터 같은 기능을 포함하며, 도구 로직을 정의하거나 호스팅할 필요가 없어요.
이런 내장 도구를 켜고 사용하는 자세한 내용은 개별 채팅 모델 통합 페이지와 도구 호출 문서를 참고하세요.