
如果你要做一个可上线的 AI 助手,LangChain 的 Tools 到底应该怎么设计?
很多人第一次看 LangChain 的 Tools,会把它理解成“给大模型多挂几个函数”。
但如果你真的要把 Agent 放进生产环境,Tools 绝不只是“函数调用封装”这么简单。它实际上是 Agent 和外部系统之间的协议层:负责定义输入输出、控制状态读写、连接长期记忆、处理错误,甚至参与整个 LangGraph 工作流的路由。官方文档明确把 Tools 定义为扩展 Agent 能力的机制,让模型能够获取实时数据、执行代码、访问数据库并执行现实世界中的动作;其底层本质是“具有明确输入输出的可调用函数”,这些函数会被传给聊天模型,由模型决定何时调用以及传入什么参数。(LangChain Docs)
一、先别急着写工具:你要先理解 Tool 在工程里的位置
官方文档里最重要的一句话,不是 @tool 怎么写,而是 Tools 在 Agent 体系中的角色:模型负责决策,Tool 负责执行。Tool 本身不是智能体,它只是一个受控、可描述、可验证的执行单元。(LangChain Docs)
这意味着在工程里,Tool 设计的重点不是“能不能跑”,而是下面这几个问题:
- 模型能不能正确理解什么时候该调用它
- 模型能不能准确传参
- Tool 返回的数据,能不能让后续推理继续进行
- Tool 出错时,系统会不会崩
- Tool 是否会影响对话状态、用户上下文和长期记忆
所以,Tools 设计得好不好,直接决定了 Agent 是否“可控”。
二、最基础的 @tool,真正关键的不是装饰器,而是 schema
官方给出的最简单写法是用 @tool 装饰函数;函数的 docstring 默认会变成工具描述,而类型注解会定义工具输入 schema。文档还特别强调:type hints 是必需的,因为它们决定了工具的输入结构。(LangChain Docs)
最小示例可以写成这样:
from langchain.tools import tool@tooldef search_orders(order_id: str) -> str:"""Query order information by order ID."""return f"Order {order_id} is in transit."
很多人会觉得这很简单,但工程上最容易踩坑的恰恰是这里。
工程实践建议 1:把 Tool 当成“面向模型的 API”
你写 Tool,不是在给人类同事写函数,而是在给 LLM 暴露 API。
所以这三样东西比实现本身更重要:
- 函数名:模型怎么识别它
- 参数名 + 类型:模型怎么构造调用参数
- docstring / description:模型什么时候选择它
官方文档建议工具名优先使用 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."""...
工程实践建议 2:description 要回答“什么时候用”,不是“它是什么”
很多团队写 description 时,只写“查询退款”。这对人类够了,对模型不够。
你更应该告诉模型:
- 适用场景:什么时候调用
- 不适用场景:什么时候不要调用
- 参数语义:参数到底是什么
- 输出边界:返回的是摘要还是原始数据
因为模型的错误,不少时候不是“调不动工具”,而是“调错工具”。
四、真实业务里,简单参数远远不够:要上 Pydantic Schema
官方文档支持用 Pydantic 模型或 JSON Schema 定义复杂输入,其中示例重点展示了 args_schema=WeatherInput 的方式。这样你可以为字段增加类型约束、枚举值和描述。(LangChain Docs)
这在工程里非常关键,因为真实 Tool 往往不是一个字符串参数,而是一组结构化输入。
比如一个客服工单查询工具:
from typing import Literalfrom pydantic import BaseModel, Fieldfrom langchain.tools import toolclass 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)
也就是说,下面这种写法是危险的:
@tooldef query_user(runtime: str) -> str:...
如果你真要访问运行时信息,应该显式接收 ToolRuntime。(LangChain Docs)
六、真正让 Tool 从“函数”升级成“系统组件”的,是 ToolRuntime
官方文档里最有工程价值的部分,其实是 ToolRuntime。
它让 Tool 不只是一个无状态函数,而是能访问运行时环境,包括:
- State:当前会话里的短期状态
- Context:调用时传入的不可变上下文
- Store:跨会话持久化存储
- Stream Writer:执行过程中的实时流式输出
- Config:执行配置
- Tool Call ID:当前工具调用的唯一标识
这些能力都是通过 runtime: ToolRuntime 暴露给 Tool 的。(LangChain Docs)
这套设计很重要,因为它把 Tool 从“函数工具箱”变成了“有上下文的业务执行点”。
七、短期状态:适合放“本轮对话里会变”的信息
文档把 State 定义为短期记忆,覆盖当前会话期间存在的数据,包括消息历史和你定义的自定义字段。Tool 可以通过 runtime.state 访问这些信息。(LangChain Docs)
典型场景:客服会话里的多轮澄清
比如用户说:
帮我查一下订单状态
系统先问:
请提供订单号
用户接着说:
A20260318001
这时候后续 Tool 就可以从消息历史或自定义状态里取信息,而不是每次都重新解析整段上下文。
示例:
from langchain.tools import tool, ToolRuntimefrom langchain.messages import HumanMessage@tooldef 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.contentreturn "No user message found"
这类能力在“多轮收集参数”的业务流程里很好用。官方文档也明确说明:runtime 参数会被自动注入,并且不会出现在模型可见的工具 schema 里。也就是说,模型只会看到真正需要它填写的业务参数。(LangChain Docs)
八、状态更新:不是 return 字符串,而是 return Command
如果 Tool 只是查数据,返回字符串或对象就够了。
但如果 Tool 需要改状态,例如“记录用户语言偏好”“保存用户姓名”“设置业务流程阶段”,官方文档建议返回 Command(update=...) 来更新状态。(LangChain Docs)
例如:
from langgraph.types import Commandfrom langchain.tools import tool@tooldef set_user_name(new_name: str) -> Command:"""Set the user's name in state."""return Command(update={"user_name": new_name})
这在工程里意味着什么
这意味着 Tool 不只是“做事”,还可以“改系统状态”。
比如你做一个企业知识助手,用户第一次说:
以后请默认用中文回答
你不应该只返回一句“好的”。
更好的做法是:
- Tool 更新
preferred_language = zh-CN - 后续流程自动读取这个状态
- 之后所有回答默认用中文
官方文档还提醒了一个非常工程化的问题:如果多个 Tool 可能并发更新同一个状态字段,要考虑 reducer,否则并发写入时可能发生冲突。(LangChain Docs)
这说明 LangChain 官方其实已经把“并发状态一致性”这个生产问题摆到桌面上了。
九、Context:适合放用户身份、租户信息、会话配置,而不是临时变量
文档把 Context 定义为调用时传入的不可变配置数据,例如 user ID、session 信息或应用配置,Tool 可以通过 runtime.context 访问。官方示例用 dataclass 定义了 UserContext,再通过 context_schema 传给 agent。(LangChain Docs)
这在多租户系统里非常重要。
一个常见反模式
不少项目会把 user_id 暴露成 Tool 参数,让模型自己填。
这其实不安全,因为 user_id 属于系统侧可信上下文,不应该交给模型生成。
更合理的方式是:
user_id放进context- Tool 从
runtime.context.user_id读取 - 模型只负责填写业务参数,比如订单号、时间范围、查询类型
示例:
from dataclasses import dataclassfrom langchain.tools import tool, ToolRuntime@dataclassclass AppContext:user_id: strtenant_id: str@tooldef get_my_profile(runtime: ToolRuntime[AppContext]) -> str:"""Get current user's profile."""user_id = runtime.context.user_idtenant_id = runtime.context.tenant_idreturn f"user={user_id}, tenant={tenant_id}"
工程价值
这样做能明显降低两个风险:
- 模型伪造身份参数
- 不同租户数据串读
十、长期记忆 Store:让 Tool 真正具备“跨会话连续性”
官方文档把 BaseStore 定义为可跨会话持久化的数据存储,并通过 runtime.store 暴露给 Tool;同时明确建议生产环境使用持久化存储实现,比如 PostgresStore,而不是 InMemoryStore。(LangChain Docs)
这部分非常适合做“用户偏好”和“长期画像”。
例如:
from typing import Anyfrom langchain.tools import tool, ToolRuntime@tooldef 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."@tooldef 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"
什么时候该用 State,什么时候该用 Store?
结合官方定义,可以简单理解为:
- State:只对当前会话有效
- Store:跨会话长期保留 (LangChain Docs)
真实工程建议
适合放进 Store 的有:
- 用户常用语言
- 默认展示时区
- 常查项目 / 常用筛选条件
- 企业助手的个性化偏好
不适合放进去的有:
- 当前轮临时槽位
- 本轮上下文里的中间推理结果
- 一次性 API 返回值缓存
十一、长耗时工具不要“闷头跑”,要流式汇报进度
官方文档提供了 runtime.stream_writer,允许 Tool 在执行过程中发出实时更新,适合长时间运行的任务。文档同时说明:如果在 Tool 中使用 runtime.stream_writer,这个 Tool 必须运行在 LangGraph 执行上下文里。(LangChain Docs)
这个能力在生产里非常重要,尤其是以下场景:
- 调多个外部 API
- 执行复杂 ETL
- 跑检索 + 重排 + 汇总
- 批量处理文件
示例:
from langchain.tools import tool, ToolRuntimeimport time@tooldef generate_monthly_report(month: str, runtime: ToolRuntime) -> str:"""Generate monthly business report."""writer = runtime.stream_writerwriter(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."
工程收益
这不是“体验优化”那么简单。它能减少用户误以为系统卡死,也能让前端更容易做可观测性展示。
十二、ToolNode:真正做复杂工作流时,不要只会 create_agent
文档明确说,ToolNode 是 LangGraph 里的预构建节点,负责执行工具,并自动处理并行执行、错误处理和状态注入;如果你要做定制工作流、需要对工具执行模式有更细粒度控制,应该使用 ToolNode,而不是只依赖 create_agent。(LangChain Docs)
这其实是在告诉你:
- 简单 Agent:
create_agent - 复杂工作流:
ToolNode + StateGraph
为什么工程里常常要上 ToolNode
因为真实系统往往不是“模型说一句,工具调一次”这么简单,而是:
- 模型先判断是否需要工具
- 如果需要,进入工具节点
- 工具执行后再回到模型
- 某些分支失败时要进入兜底流程
- 某些情况下直接结束,不再回到模型
官方文档里的 tools_condition 就是为这种路由提供支持:它能根据 LLM 是否发起了工具调用,把流程分流到 tools 或 END。(LangChain Docs)
一个常见图可以抽象成这样:
from langgraph.prebuilt import ToolNode, tools_conditionfrom langgraph.graph import StateGraph, MessagesState, START, ENDbuilder = 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()
适合 ToolNode 的业务
- 企业问答 + 多工具编排
- 客服自动化流程
- 带状态推进的业务助理
- 有明确“调用工具 / 不调用工具”分支的系统
十三、Tool 返回什么,不是编码习惯问题,而是推理接口设计问题
官方文档把 Tool 返回值分成三类:
- 返回
string - 返回
object - 返回
Command(LangChain Docs)
这三类在工程上分别对应三种完全不同的用途。
1)返回 string:给模型“读”
适合人类可读结果,例如:
@tooldef get_weather(city: str) -> str:"""Get weather for a city."""return f"It is currently sunny in {city}."
官方文档说明,这类返回值会被转换成 ToolMessage,再交给模型继续处理。(LangChain Docs)
适合场景:
- 搜索摘要
- 简单查询结果
- 简单执行回执
2)返回 object:给模型“解析”
适合结构化数据,例如:
@tooldef get_weather_data(city: str) -> dict:"""Get structured weather data."""return {"city": city,"temperature_c": 22,"conditions": "sunny",}
官方文档说明,object 会被序列化后作为工具输出,让模型读取具体字段。(LangChain Docs)
适合场景:
- 查询订单详情
- 检索命中列表
- 指标聚合结果
- 需要多字段推理的中间数据
3)返回 Command:给系统“改状态”
适合那些不仅要返回结果,还要更新系统状态的 Tool。官方文档还特别提到:如果模型需要看到“工具执行成功”的反馈,可以在 Command 里附带 ToolMessage,并使用 runtime.tool_call_id 作为 tool_call_id。(LangChain Docs)
这在“写状态 + 给用户确认”的流程里很实用。
十四、错误处理别留给异常栈,应该设计成产品能力
官方文档里,ToolNode 支持多种错误处理方式:
- 默认行为
handle_tool_errors=True:捕获错误并把错误信息返回给 LLM- 自定义固定错误消息
- 自定义错误处理函数
- 只捕获指定异常类型 (LangChain Docs)
这意味着在生产环境里,Tool 的异常处理可以是系统化设计,而不是简单 try/except。
例如:
from langgraph.prebuilt import ToolNodedef handle_error(e: ValueError) -> str:return f"Invalid input: {e}"tool_node = ToolNode(tools, handle_tool_errors=handle_error)
工程建议
按错误来源分层处理:
- 参数错误:返回明确可纠正提示
- 权限错误:提示无权访问,不暴露内部细节
- 外部服务错误:给用户兜底话术,并记录日志
- 系统异常:不要原样抛给用户
官方提供的接口,已经足够把这些策略做成统一层。(LangChain Docs)
十五、一个更接近真实项目的 Tool 设计方案
下面我给你一个“企业内部助手”的 Tool 设计模板,思路完全基于这页文档能力组合而来。
目标
做一个“员工自助助手”,支持:
- 查询个人资料
- 查询报销状态
- 设置默认语言
- 保存个人偏好
- 长耗时任务显示进度
工具分层建议
1. 查询类 Tool:返回 object
@tooldef 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"}
2. 用户身份相关:从 context 拿,不让模型传
from dataclasses import dataclassfrom langchain.tools import ToolRuntime@dataclassclass EmployeeContext:user_id: strdepartment: str
3. 偏好设置:返回 Command 更新 state
from langgraph.types import Commandfrom langchain.messages import ToolMessage@tooldef 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,)]})
4. 长期偏好:写入 store
@tooldef save_dashboard_pref(pref: dict, runtime: ToolRuntime) -> str:"""Save dashboard preferences."""user_id = runtime.context.user_idruntime.store.put(("preferences",), user_id, pref)return "Dashboard preference saved."
5. 报表生成:用 stream_writer
@tooldef 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."
这个方案为什么靠谱
因为它把官方文档里的四类运行时能力分工得很清楚:
- 会话内临时信息 →
state - 调用侧身份配置 →
context - 跨会话持久化 →
store - 长任务反馈 →
stream_writer(LangChain Docs)
这比“把一切都塞进一个万能 Tool 里”要稳定得多。
十六、什么时候该自己写 Tool,什么时候该用预构建 Tool
官方文档提到,LangChain 提供了大量预构建工具和 toolkits,可用于网页搜索、代码解释、数据库访问等常见场景;同时也提到,一些聊天模型本身支持服务端工具能力,比如 web search 和 code interpreter,这类能力由模型提供方在服务端执行,不需要你自己定义或托管工具逻辑。(LangChain Docs)
对工程团队来说,可以这样决策:
优先用预构建 / 服务端工具的场景
- 通用能力,没明显业务壁垒
- 快速验证 PoC
- 不想自己维护执行环境
优先自定义 Tool 的场景
- 涉及业务系统权限
- 需要严格控制输入输出
- 需要读写状态 / 上下文 / 长期记忆
- 需要做异常治理和审计日志
换句话说:
“会不会写 Tool”不重要,重要的是你是否知道哪些能力应该掌握在自己系统里。
十七、我对这页文档的一个总结:它讲的不是工具调用,而是 Agent 的工程边界
如果只看表面,这页文档像是在讲:
- 如何写
@tool - 如何自定义 name / description
- 如何定义 schema
- 如何用 ToolNode
但如果从工程角度读,它其实在回答一个更关键的问题:
Agent 到底怎样才能安全地接入真实系统?
官方给出的答案,基本都在这页里了:
- 用类型和 schema 约束输入 (LangChain Docs)
- 用 description 约束调用意图 (LangChain Docs)
- 用 runtime 读写状态、上下文和记忆 (LangChain Docs)
- 用 ToolNode 承接复杂工作流 (LangChain Docs)
- 用错误处理和条件路由把执行过程产品化 (LangChain Docs)
所以,真正成熟的 Tool 设计思路应该是:
Tool = 面向模型的受控 API + 面向系统的状态接口 + 面向生产的执行单元
十八、落地时最值得记住的 8 条经验
最后我把这页文档提炼成 8 条工程经验,方便你直接放进博客结尾:
- Tool 名称和描述不是装饰,是模型决策接口。 官方建议使用
snake_case,避免特殊字符,提高兼容性。(LangChain Docs) - 复杂参数一定要 schema 化。 Pydantic/JSON Schema 能显著降低模型乱传参。(LangChain Docs)
runtime和config不能当普通参数名。 这是官方保留字段。(LangChain Docs)- 短期会话数据放 state,跨会话数据放 store。 两者职责要分清。(LangChain Docs)
- 用户身份这类可信信息放 context,不要让模型生成。 官方支持通过
context_schema注入不可变上下文。(LangChain Docs) - 需要改系统状态时,用
Command,不要硬塞文本返回。 (LangChain Docs) - 复杂流程优先用 ToolNode 和条件路由。 这比单纯
create_agent更适合生产工作流。(LangChain Docs) - 错误处理要产品化。
ToolNode已经给出了统一异常治理入口。(LangChain Docs)