
如果你要做一个可上线的 AI 助手,LangChain 的 Tools 到底应该怎么设计?
SERIES · Langchain Agent 应用开发
2026-03-19 · 30 min read · by GUMP

如果你要做一个可上线的 AI 助手,LangChain 的 Tools 到底应该怎么设计?
很多人第一次看 LangChain 的 Tools,会把它理解成“给大模型多挂几个函数”。
但如果你真的要把 Agent 放进生产环境,Tools 绝不只是“函数调用封装”这么简单。它实际上是 Agent 和外部系统之间的协议层:负责定义输入输出、控制状态读写、连接长期记忆、处理错误,甚至参与整个 LangGraph 工作流的路由。官方文档明确把 Tools 定义为扩展 Agent 能力的机制,让模型能够获取实时数据、执行代码、访问数据库并执行现实世界中的动作;其底层本质是“具有明确输入输出的可调用函数”,这些函数会被传给聊天模型,由模型决定何时调用以及传入什么参数。(LangChain Docs)
官方文档里最重要的一句话,不是 @tool 怎么写,而是 Tools 在 Agent 体系中的角色:模型负责决策,Tool 负责执行。Tool 本身不是智能体,它只是一个受控、可描述、可验证的执行单元。(LangChain Docs)
这意味着在工程里,Tool 设计的重点不是“能不能跑”,而是下面这几个问题:
所以,Tools 设计得好不好,直接决定了 Agent 是否“可控”。
@tool,真正关键的不是装饰器,而是 schema官方给出的最简单写法是用 @tool 装饰函数;函数的 docstring 默认会变成工具描述,而类型注解会定义工具输入 schema。文档还特别强调:type hints 是必需的,因为它们决定了工具的输入结构。(LangChain Docs)
最小示例可以写成这样:
from langchain.tools import tool
@tool
def search_orders(order_id: str) -> str:
"""Query order information by order ID."""
return f"Order {order_id} is in transit."很多人会觉得这很简单,但工程上最容易踩坑的恰恰是这里。
你写 Tool,不是在给人类同事写函数,而是在给 LLM 暴露 API。
所以这三样东西比实现本身更重要:
官方文档建议工具名优先使用 snake_case,避免空格和特殊字符,因为有些模型提供方会拒绝包含这些字符的工具名。(LangChain Docs)
所以,别写这种:
@tool("Get Order Info!!!")
def x(id: str) -> str:
...更稳妥的是:
@tool("get_order_info")
def get_order_info(order_id: str) -> str:
"""Get order status by order ID."""
...这不是“代码风格问题”,而是跨模型兼容性问题。(LangChain Docs)
官方文档支持自定义工具名和 description。默认情况下,docstring 会变成工具描述;如果你需要更明确地告诉模型何时调用,可以手动写 description。(LangChain Docs)
例如:
from langchain.tools import tool
@tool(
"refund_lookup",
description="Look up refund status for an existing paid order. "
"Use this only when the user asks about refund progress or refund eligibility."
)
def refund_lookup(order_id: str) -> str:
"""Query refund record by order ID."""
...很多团队写 description 时,只写“查询退款”。这对人类够了,对模型不够。
你更应该告诉模型:
因为模型的错误,不少时候不是“调不动工具”,而是“调错工具”。
官方文档支持用 Pydantic 模型或 JSON Schema 定义复杂输入,其中示例重点展示了 args_schema=WeatherInput 的方式。这样你可以为字段增加类型约束、枚举值和描述。(LangChain Docs)
这在工程里非常关键,因为真实 Tool 往往不是一个字符串参数,而是一组结构化输入。
比如一个客服工单查询工具:
from typing import Literal
from pydantic import BaseModel, Field
from langchain.tools import tool
class TicketQuery(BaseModel):
user_id: str = Field(description="Unique user identifier in CRM")
ticket_type: Literal["refund", "shipment", "invoice"] = Field(
description="Type of support ticket"
)
include_history: bool = Field(
default=False,
description="Whether to include previous related tickets"
)
@tool(args_schema=TicketQuery)
def query_ticket(user_id: str, ticket_type: str, include_history: bool = False) -> dict:
"""Query customer support ticket data."""
return {
"user_id": user_id,
"ticket_type": ticket_type,
"latest_status": "processing",
"history_included": include_history,
}因为它能把模型调用行为约束在你允许的空间里:
ticket_type 只能是固定枚举官方文档给出的结论也很明确:复杂输入可以用 Pydantic 或 JSON Schema 定义。(LangChain Docs)
config 和 runtime 不能拿来当普通参数名这是一个很容易在项目里踩的坑。官方文档明确列出了保留参数名:config 和 runtime。这两个名字不能作为普通工具参数使用,否则会导致运行时错误。原因是它们被 LangChain 内部用于传递 RunnableConfig 和 ToolRuntime。(LangChain Docs)
也就是说,下面这种写法是危险的:
@tool
def query_user(runtime: str) -> str:
...如果你真要访问运行时信息,应该显式接收 ToolRuntime。(LangChain Docs)
ToolRuntime官方文档里最有工程价值的部分,其实是 ToolRuntime。
它让 Tool 不只是一个无状态函数,而是能访问运行时环境,包括:
这些能力都是通过 runtime: ToolRuntime 暴露给 Tool 的。(LangChain Docs)
这套设计很重要,因为它把 Tool 从“函数工具箱”变成了“有上下文的业务执行点”。
文档把 State 定义为短期记忆,覆盖当前会话期间存在的数据,包括消息历史和你定义的自定义字段。Tool 可以通过 runtime.state 访问这些信息。(LangChain Docs)
比如用户说:
帮我查一下订单状态
系统先问:
请提供订单号
用户接着说:
A20260318001
这时候后续 Tool 就可以从消息历史或自定义状态里取信息,而不是每次都重新解析整段上下文。
示例:
from langchain.tools import tool, ToolRuntime
from langchain.messages import HumanMessage
@tool
def get_last_user_message(runtime: ToolRuntime) -> str:
"""Get the most recent user message."""
for msg in reversed(runtime.state["messages"]):
if isinstance(msg, HumanMessage):
return msg.content
return "No user message found"这类能力在“多轮收集参数”的业务流程里很好用。官方文档也明确说明:runtime 参数会被自动注入,并且不会出现在模型可见的工具 schema 里。也就是说,模型只会看到真正需要它填写的业务参数。(LangChain Docs)
Command如果 Tool 只是查数据,返回字符串或对象就够了。
但如果 Tool 需要改状态,例如“记录用户语言偏好”“保存用户姓名”“设置业务流程阶段”,官方文档建议返回 Command(update=...) 来更新状态。(LangChain Docs)
例如:
from langgraph.types import Command
from langchain.tools import tool
@tool
def set_user_name(new_name: str) -> Command:
"""Set the user's name in state."""
return Command(update={"user_name": new_name})这意味着 Tool 不只是“做事”,还可以“改系统状态”。
比如你做一个企业知识助手,用户第一次说:
以后请默认用中文回答
你不应该只返回一句“好的”。
更好的做法是:
preferred_language = zh-CN官方文档还提醒了一个非常工程化的问题:如果多个 Tool 可能并发更新同一个状态字段,要考虑 reducer,否则并发写入时可能发生冲突。(LangChain Docs)
这说明 LangChain 官方其实已经把“并发状态一致性”这个生产问题摆到桌面上了。
文档把 Context 定义为调用时传入的不可变配置数据,例如 user ID、session 信息或应用配置,Tool 可以通过 runtime.context 访问。官方示例用 dataclass 定义了 UserContext,再通过 context_schema 传给 agent。(LangChain Docs)
这在多租户系统里非常重要。
不少项目会把 user_id 暴露成 Tool 参数,让模型自己填。
这其实不安全,因为 user_id 属于系统侧可信上下文,不应该交给模型生成。
更合理的方式是:
user_id 放进 contextruntime.context.user_id 读取示例:
from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime
@dataclass
class AppContext:
user_id: str
tenant_id: str
@tool
def get_my_profile(runtime: ToolRuntime[AppContext]) -> str:
"""Get current user's profile."""
user_id = runtime.context.user_id
tenant_id = runtime.context.tenant_id
return f"user={user_id}, tenant={tenant_id}"这样做能明显降低两个风险:
官方文档把 BaseStore 定义为可跨会话持久化的数据存储,并通过 runtime.store 暴露给 Tool;同时明确建议生产环境使用持久化存储实现,比如 PostgresStore,而不是 InMemoryStore。(LangChain Docs)
这部分非常适合做“用户偏好”和“长期画像”。
例如:
from typing import Any
from langchain.tools import tool, ToolRuntime
@tool
def save_user_preference(user_id: str, preference: dict[str, Any], runtime: ToolRuntime) -> str:
"""Save user preference."""
runtime.store.put(("users", "preferences"), user_id, preference)
return "Preference saved."
@tool
def get_user_preference(user_id: str, runtime: ToolRuntime) -> str:
"""Get user preference."""
item = runtime.store.get(("users", "preferences"), user_id)
return str(item.value) if item else "No preference found"结合官方定义,可以简单理解为:
适合放进 Store 的有:
不适合放进去的有:
官方文档提供了 runtime.stream_writer,允许 Tool 在执行过程中发出实时更新,适合长时间运行的任务。文档同时说明:如果在 Tool 中使用 runtime.stream_writer,这个 Tool 必须运行在 LangGraph 执行上下文里。(LangChain Docs)
这个能力在生产里非常重要,尤其是以下场景:
示例:
from langchain.tools import tool, ToolRuntime
import time
@tool
def generate_monthly_report(month: str, runtime: ToolRuntime) -> str:
"""Generate monthly business report."""
writer = runtime.stream_writer
writer(f"Start generating report for {month}")
time.sleep(1)
writer("Loading sales data")
time.sleep(1)
writer("Aggregating KPI metrics")
time.sleep(1)
writer("Finalizing report")
return f"Monthly report for {month} is ready."这不是“体验优化”那么简单。它能减少用户误以为系统卡死,也能让前端更容易做可观测性展示。
create_agent文档明确说,ToolNode 是 LangGraph 里的预构建节点,负责执行工具,并自动处理并行执行、错误处理和状态注入;如果你要做定制工作流、需要对工具执行模式有更细粒度控制,应该使用 ToolNode,而不是只依赖 create_agent。(LangChain Docs)
这其实是在告诉你:
create_agentToolNode + StateGraph因为真实系统往往不是“模型说一句,工具调一次”这么简单,而是:
官方文档里的 tools_condition 就是为这种路由提供支持:它能根据 LLM 是否发起了工具调用,把流程分流到 tools 或 END。(LangChain Docs)
一个常见图可以抽象成这样:
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.graph import StateGraph, MessagesState, START, END
builder = StateGraph(MessagesState)
builder.add_node("llm", call_llm)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "llm")
builder.add_conditional_edges("llm", tools_condition)
builder.add_edge("tools", "llm")
graph = builder.compile()官方文档把 Tool 返回值分成三类:
stringobjectCommand (LangChain Docs)这三类在工程上分别对应三种完全不同的用途。
适合人类可读结果,例如:
@tool
def get_weather(city: str) -> str:
"""Get weather for a city."""
return f"It is currently sunny in {city}."官方文档说明,这类返回值会被转换成 ToolMessage,再交给模型继续处理。(LangChain Docs)
适合场景:
适合结构化数据,例如:
@tool
def get_weather_data(city: str) -> dict:
"""Get structured weather data."""
return {
"city": city,
"temperature_c": 22,
"conditions": "sunny",
}官方文档说明,object 会被序列化后作为工具输出,让模型读取具体字段。(LangChain Docs)
适合场景:
适合那些不仅要返回结果,还要更新系统状态的 Tool。官方文档还特别提到:如果模型需要看到“工具执行成功”的反馈,可以在 Command 里附带 ToolMessage,并使用 runtime.tool_call_id 作为 tool_call_id。(LangChain Docs)
这在“写状态 + 给用户确认”的流程里很实用。
官方文档里,ToolNode 支持多种错误处理方式:
handle_tool_errors=True:捕获错误并把错误信息返回给 LLM这意味着在生产环境里,Tool 的异常处理可以是系统化设计,而不是简单 try/except。
例如:
from langgraph.prebuilt import ToolNode
def handle_error(e: ValueError) -> str:
return f"Invalid input: {e}"
tool_node = ToolNode(tools, handle_tool_errors=handle_error)按错误来源分层处理:
官方提供的接口,已经足够把这些策略做成统一层。(LangChain Docs)
下面我给你一个“企业内部助手”的 Tool 设计模板,思路完全基于这页文档能力组合而来。
做一个“员工自助助手”,支持:
@tool
def get_expense_status(expense_id: str) -> dict:
"""Get expense reimbursement status by expense ID."""
return {
"expense_id": expense_id,
"status": "under_review",
"amount": 1280.0,
"currency": "CNY"
}from dataclasses import dataclass
from langchain.tools import ToolRuntime
@dataclass
class EmployeeContext:
user_id: str
department: strfrom langgraph.types import Command
from langchain.messages import ToolMessage
@tool
def set_language(language: str, runtime: ToolRuntime) -> Command:
"""Set preferred reply language."""
return Command(
update={
"preferred_language": language,
"messages": [
ToolMessage(
content=f"Preferred language updated to {language}.",
tool_call_id=runtime.tool_call_id,
)
]
}
)@tool
def save_dashboard_pref(pref: dict, runtime: ToolRuntime) -> str:
"""Save dashboard preferences."""
user_id = runtime.context.user_id
runtime.store.put(("preferences",), user_id, pref)
return "Dashboard preference saved."@tool
def build_department_report(runtime: ToolRuntime) -> str:
"""Build department report."""
runtime.stream_writer("Loading department data")
runtime.stream_writer("Computing monthly metrics")
runtime.stream_writer("Packaging final report")
return "Department report generated."因为它把官方文档里的四类运行时能力分工得很清楚:
statecontextstorestream_writer (LangChain Docs)这比“把一切都塞进一个万能 Tool 里”要稳定得多。
官方文档提到,LangChain 提供了大量预构建工具和 toolkits,可用于网页搜索、代码解释、数据库访问等常见场景;同时也提到,一些聊天模型本身支持服务端工具能力,比如 web search 和 code interpreter,这类能力由模型提供方在服务端执行,不需要你自己定义或托管工具逻辑。(LangChain Docs)
对工程团队来说,可以这样决策:
换句话说:
“会不会写 Tool”不重要,重要的是你是否知道哪些能力应该掌握在自己系统里。
如果只看表面,这页文档像是在讲:
@tool但如果从工程角度读,它其实在回答一个更关键的问题:
Agent 到底怎样才能安全地接入真实系统?
官方给出的答案,基本都在这页里了:
所以,真正成熟的 Tool 设计思路应该是:
Tool = 面向模型的受控 API + 面向系统的状态接口 + 面向生产的执行单元
最后我把这页文档提炼成 8 条工程经验,方便你直接放进博客结尾:
snake_case,避免特殊字符,提高兼容性。(LangChain Docs)runtime 和 config 不能当普通参数名。 这是官方保留字段。(LangChain Docs)context_schema 注入不可变上下文。(LangChain Docs)Command,不要硬塞文本返回。 (LangChain Docs)create_agent 更适合生产工作流。(LangChain Docs)ToolNode 已经给出了统一异常治理入口。(LangChain Docs)