深入学LangChain 官方文档(六)Tools 与 Tool Calling 首讲
深入学 LangChain 官方文档六Tools 与 Tool Calling 首讲本篇对应的官方文档LangChain Tools工具定义、schema、ToolRuntime、返回值、状态更新与错误处理。LangChain Agents工具在 Agent harness 与核心循环中的位置。LangChain MessagesAIMessage.tool_calls、ToolMessage与调用标识的消息合同。本篇讲解范围本篇完整讲清一个 Python 能力怎样变成模型可选择的 Tool模型怎样产生 Tool Calling 请求应用怎样执行真实函数并把结果送回对应调用。Agent loop 已在第 05 篇建立Runtime Context、State 与 Store 的系统边界留给第 07 篇Middleware 的完整钩子体系留给第 10 篇。第 05 篇把 Agent 的核心循环压缩成“模型选择下一步工具接触外部世界结果回到 messages”。其中最容易被一句话带过的正是中间这段 Tool Calling模型为什么知道有哪些工具它生成函数名以后代码真的执行了吗工具返回一个字典和更新 Agent state 是一回事吗接下来我们仍然沿用售后场景。用户要求退款Agent 可能需要三个能力查询订单、查询退款规则、提交退款申请。前两个只读最后一个会改变业务状态。把三个 Python 函数放进列表并不意味着工具设计已经完成模型要看到稳定的名称、描述和参数合同执行层还要做身份、权限、幂等与异常处理。整条协议有五个动作应用定义工具schema 暴露给模型模型生成调用请求harness 执行真实函数结果以ToolMessage或 state update 回到循环。模型参与的是“选择和填参”不是直接持有数据库连接也不是绕过应用权限调用后台。本篇会按这五步往下拆。先从函数如何变成 schema 开始再读取AIMessage.tool_calls随后解释tool_call_id、ToolRuntime、三类返回值和Command最后把并发冲突、错误分层和高风险工具的工程闸门接回完整代码。1. Tool 不是“模型会调用的普通函数”从 Python 角度看工具确实是一段可调用代码从 Agent 角度看它还是一份提供给模型的能力合同。合同至少包含名称、描述、输入 schema 和输出语义。模型依据这些信息决定是否选择工具以及怎样构造参数。普通函数只需要让开发者知道怎么调用。Tool 还要让模型在有限上下文中区分它与其他能力。比如lookup_order与get_order_status都写成“查询订单”模型就难以判断一个返回完整订单一个只返回物流状态。函数可以正常运行Tool Calling 却可能在选择阶段就出错。输出语义同样属于合同。submit_refund返回success看起来简单却没有说明退款申请号、当前状态、是否已重复提交。模型下一轮只能猜测“success”意味着资金已到账还是申请已受理。工具结果需要面向后续判断设计而不是只面向函数作者。Tool 是模型可见的调用协议与应用内部的执行能力的交界面。模型看到的是 schema运行系统持有的才是函数、依赖和权限。守住这个分界才能解释“模型请求了什么”和“系统实际做了什么”是否一致。2.tool怎样把 Python 定义转换成 schemaLangChain 的tool装饰器可以把普通函数转换成 Tool。默认情况下函数名成为工具名docstring 成为描述类型标注用于推导输入 schema。fromlangchain.toolsimporttooltooldeflookup_order(order_id:str)-dict:按订单号查询商品、支付状态、配送状态和可退款金额。return{order_id:order_id,payment_status:paid,delivery_status:delivered,refundable_amount:499.0,}模型不会读取函数体也不知道字典是怎样查出来的。这里要区分的边界是它能够使用的只有工具名、描述和order_id: str这一输入结构函数体里的数据库连接、缓存和重试都留在应用内部。转换前后存在两种视角Python 侧关注实现与返回模型侧关注名称、描述和参数。tool把两者连接起来却不会自动修正模糊命名、缺失业务约束或危险权限schema 质量仍由开发者负责。函数名应表达单一动作。handle_order可能表示查询、修改或取消不如lookup_order明确。描述不仅要说“做什么”还要说明“什么时候用”和关键限制。例如“仅查询当前用户订单不创建退款”能减少模型把只读工具当成写操作入口。参数也要尽量接近业务语言。让模型生成order_id比生成复杂数据库查询对象更可靠让模型传reason和amount再由后端依据当前用户与订单校验比把user_id、权限标志和数据库表名全部暴露在 schema 中更安全。工具选择错误不一定来自模型能力。名称重叠会让候选难分描述缺少使用条件会让调用时机漂移类型过宽会让参数格式不稳定。先修正 schema再考虑添加更长的 system prompt通常更接近问题根因。高级 schema 可以使用 Pydantic 显式描述字段和值域但原则没有变化模型只应填入它能够从对话推断、又确实属于业务请求的参数。运行身份、连接和内部状态不该伪装成模型参数这些信息会由ToolRuntime接管。工具描述还要避免把业务结果写成承诺。submit_refund的描述如果写“立即退回款项”模型可能在用户尚未确认、后端只创建申请的情况下给出错误预期。更准确的描述应说明工具创建的是申请、需要哪些前置条件、成功后返回什么。描述既影响调用也影响模型如何向用户解释结果。版本变化同样需要管理。工具参数新增必填字段、枚举值改变或返回结构重命名都可能让已有 prompt、测试和历史 trace 失效。把 schema 当作内部 API 做版本审查比直接修改函数签名更稳重要变更要配套回归用例确认模型仍能选择正确工具并填写有效参数。3. Tool Calling 是请求不是执行模型收到工具 schema 后可能返回一个包含tool_calls的AIMessage。标准化后的调用通常包含名称、参数、调用标识和类型。它表达的是“我建议调用这个工具并使用这些参数”。ai_message.tool_calls# 示例形态[{name:lookup_order,args:{order_id:A1024},id:call_abc123,type:tool_call,}]此时订单查询还没有发生。模型只生成了结构化请求harness 接下来才查找工具、验证参数、执行函数并处理结果。这个执行边界必须保留把tool_calls当成执行成功会让审计日志提前记录动作也可能让前端向用户展示不存在的业务结果。调用边界把权限牢牢留在应用侧。即使模型请求submit_refund工具执行前仍然可以拒绝越权用户、校验订单状态、检查金额、要求人工确认或命中幂等记录。模型的决定是输入不是授权凭证。同一条AIMessage可能包含多个工具调用。查询不同订单或获取互不依赖的事实时运行层可以并发执行如果后一个动作依赖前一个结果就应让结果先回到模型或预定义 workflow。是否并发取决于数据依赖不取决于模型一次生成了几个调用。手动使用模型的bind_tools时开发者需要自己读取tool_calls、调函数、构造ToolMessage并再次调用模型。create_agent的 tool node 自动完成这段编排但自动化不改变协议本身。理解消息结构才能在 trace 中判断问题发生在请求还是执行。4.tool_call_id把结果送回正确请求工具执行后结果通常以ToolMessage回到消息链。最关键的字段不是自然语言内容而是tool_call_id。它必须与原始调用的 id 对应让模型和运行系统知道这个结果回答了哪个请求。单工具调用时配对错误可能暂时不明显并发查询订单与规则时没有 id 就无法可靠区分两个返回。顺序也不能替代标识因为外部服务完成时间不同第二个请求可能先返回。ToolMessage.content保存给模型消费的结果。它可以是简洁文本也可以是序列化对象或多模态内容。内容应包含模型下一步需要的事实但不必复制全部内部响应。请求追踪 id、敏感字段和调试堆栈可以进入日志或 artifact不应无条件进入模型上下文。如果工具失败也应保留同一调用 id。模型才能知道“订单查询 call_abc123 失败”而不是把错误当成新的用户消息。错误内容还要区分可恢复与不可恢复参数缺失可以让模型修正权限拒绝不应该通过重复调用绕过服务超时则可能按预算重试或降级。第 03 篇讲 Message 时ToolMessage是一种消息类型放到本篇它还是一次能力调用的回执。消息角色解决上下文顺序调用 id 解决请求结果关联两层合同共同保证 Agent loop 不会在并发和失败时串线。5.ToolRuntime隐藏模型不该填写的参数真实工具通常不只需要模型给出的业务参数。查询订单还需要当前 user id 和数据库客户端写操作需要 request id、state、store 或 stream writer。如果把这些都写进函数参数模型 schema 会出现它不应知道、也无法可靠生成的字段。LangChain 使用ToolRuntime提供统一运行期入口。函数签名可以同时拥有业务参数和runtime但runtime会从模型可见 schema 中隐藏。模型只填写order_id应用在执行时注入身份、state、store 等信息。fromdataclassesimportdataclassfromlangchain.toolsimportToolRuntime,tooldataclassclassSupportContext:user_id:strtooldeflookup_order(order_id:str,runtime:ToolRuntime[SupportContext],)-dict:查询当前用户拥有的订单。user_idruntime.context.user_idreturn{order_id:order_id,owner:user_id,status:delivered}这段函数的分层重点在两个入口order_id来自模型参数runtime.context.user_id由运行系统注入最终执行时二者才在工具内部汇合。参数分层不仅让 schema 更短也形成安全边界。用户不能在对话中要求模型把user_id改成别人因为身份由调用方 context 提供数据库连接不进入 prompt也不会出现在模型生成的 JSON 中。ToolRuntime还能访问state、store、stream_writer、execution_info、server_info和当前tool_call_id。这些成员的生命周期不同第 07 篇会系统拆开。本篇只需记住模型负责业务参数runtime 提供执行环境两者在工具函数里汇合却不共享同一可见性。旧示例可能使用InjectedState、InjectedStore或单独注入 tool call id。当前官方文档推荐用ToolRuntime作为显式统一接口。迁移时不要把旧名字和新接口混在一段代码里否则读者很难判断哪个是当前主路径。隐藏并不等于可以忽略验证。runtime.context.user_id由调用方注入比模型生成更可信但调用入口仍要完成身份认证runtime.store提供存取能力却不代表所有工具都应该读写任意命名空间。ToolRuntime解决的是依赖怎样进入函数依赖本身的授权和生命周期仍由应用负责。测试时可以构造不同 context 和 state 调用工具验证同一个模型参数在不同用户下只能访问各自数据。这样安全边界由确定性测试证明而不是只检查 prompt 是否写了“不得访问他人订单”。6. 返回 string、object 还是Command工具结果怎样返回取决于后续步骤需要什么。官方文档给出三条常见路径string、object 和Command。它们不是写法偏好而是不同的数据流。返回 string 时值会转换为ToolMessage模型读取文本并决定下一步。查询规则、返回一句明确状态适合这种形式。文本应稳定、简短避免把面向用户的话术与机器状态混在一起。返回 object例如字典适合模型需要读取多个明确字段。对象会序列化进入工具输出但不会直接更新其他 state 字段。订单查询返回order_id、status、refundable_amount比拼成一段自由文本更便于后续推理和调试。当工具不仅提供信息还要修改 Agent state 时应返回Command。例如用户确认退款语言后把refund_stage从waiting_confirmation改成submitted。这类变化属于运行状态不能靠模型从一条自然语言消息里“记住”。选择规则可以压缩为三个问题模型只需读一句结论吗需要读取多个字段吗还是后续节点必须看到某个 state 字段已改变前两种结果进入消息第三种通过Command.update改变图状态。返回 object 也不等于写入 Store。工具可以在函数内部调用外部 API 或数据库但 Agent state 是否更新要看返回合同长期 Store 是否写入要看工具是否显式执行 store 操作。真实世界副作用、消息结果和图状态是三层不同事实不能用一个“返回成功”代替。7.Command更新 State 时为什么还需要ToolMessageCommand的update可以修改自定义 state 字段。若模型需要知道工具成功还应同时追加一条ToolMessage并使用runtime.tool_call_id完成配对。代码中的refund_stage与messages正好对应这两个消费者阅读时应分别追踪它们的写回路径。fromlangchain.messagesimportToolMessagefromlangchain.toolsimportToolRuntime,toolfromlanggraph.typesimportCommandtooldefmark_refund_submitted(request_id:str,runtime:ToolRuntime,)-Command:记录退款申请已经提交并更新当前处理阶段。returnCommand(update{refund_stage:submitted,messages:[ToolMessage(contentf退款申请{request_id}已提交。,tool_call_idruntime.tool_call_id,)],})返回对象中的两条路径必须同时成立结构化字段推进程序状态带调用标识的消息则把执行结果送回下一次模型调用。refund_stage面向后续程序和节点ToolMessage面向下一次模型调用。只更新字段、不返回消息模型可能不知道当前调用已经完成只返回消息、不更新字段后续逻辑只能从自然语言反推状态。两者各自服务不同消费者。状态写入还要考虑 reducer。两个并行工具若同时更新同一列表或计数器没有合并规则就可能覆盖或冲突。官方文档明确提醒可能被并行工具调用更新的字段应定义 reducer。对于“当前退款阶段”这类排他状态更安全的做法往往是禁止并行写入或由确定性 workflow 仲裁而不是简单选择最后一个结果。并行查询天然适合合并结果并行写操作却可能破坏业务顺序。工具 schema 只描述参数无法替你定义并发语义这部分必须由 state schema、reducer、事务或 workflow 决定。下一节的错误处理也沿用同一原则运行层必须保留足以判断后续动作的业务语义。8. 错误处理要保留业务语义工具错误至少可以分成四层。第一层是 schema 或参数错误例如缺少order_id模型可能根据错误反馈修正。第二层是权限与业务拒绝例如订单不属于当前用户重复调用不应改变结论。第三层是外部服务失败例如超时或限流可以按预算重试。第四层是程序缺陷例如未处理的空值应记录并修复不宜把内部堆栈直接交给模型。LangChain 可以用wrap_tool_call在工具执行周围捕获异常并返回带正确tool_call_id的ToolMessage。这个机制适合把技术异常转换成 Agent 能消费的结果但转换时仍要保留类别。统一写成“工具失败请重试”会让模型对权限拒绝反复尝试也会让运维失去定位信息。生产环境可以为模型、用户和日志准备不同视图。模型看到可采取下一步的短结果用户看到不会泄露内部细节的说明日志保留 error code、trace id 和受控诊断信息。三个消费者的信息需求不同不应该共享同一长字符串。重试也要放在正确层。网络抖动可由工具客户端重试模型参数不合法可回到 Tool Calling 修正整条 Agent run 是否重试则由 harness 决定。多层同时无上限重试会把一次小故障放大成调用风暴。错误码应稳定到足以驱动下一步。例如order_not_found可以引导补问订单号permission_denied应停止当前数据访问service_unavailable可以提示稍后再试或转人工。若工具每次返回不同自然语言模型也许能理解但程序很难统计失败分布回归测试也难以精确断言。对于写工具还要区分“请求未送达”“请求已送达但响应丢失”和“业务明确拒绝”。前两种场景若没有幂等键盲目重试可能重复创建退款第三种场景继续重试则是在绕过业务规则。可靠工具应把不确定执行状态暴露给调用方而不是把所有异常压成布尔值。9. 把三个售后工具注册进 Agent现在把只读查询、规则查询和状态更新放进同一示例。代码强调协议边界模型填写业务参数context 提供身份写工具返回Command而真实退款接口仍应在函数内部执行权限、金额和幂等校验。importosfromdataclassesimportdataclassfromlangchain.agentsimportAgentState,create_agentfromlangchain.messagesimportToolMessagefromlangchain.toolsimportToolRuntime,toolfromlangchain_openaiimportChatOpenAIfromlanggraph.typesimportCommanddataclassclassSupportContext:user_id:strclassSupportState(AgentState):refund_stage:strtooldeflookup_order(order_id:str,runtime:ToolRuntime[SupportContext])-dict:查询当前用户订单的支付、配送和可退款金额。return{order_id:order_id,owner:runtime.context.user_id,status:delivered,refundable_amount:499.0,}tooldeflookup_refund_policy(product_type:str)-str:查询指定商品类型当前适用的退款规则。returnf{product_type}签收后 30 天内可申请需完成订单与商品状态校验。tooldefsubmit_refund_request(order_id:str,reason:str,runtime:ToolRuntime[SupportContext],)-Command:为当前用户提交退款申请调用前必须已获得用户明确确认。request_idfrefund-{order_id}returnCommand(update{refund_stage:submitted,messages:[ToolMessage(contentf退款申请已受理申请号{request_id},tool_call_idruntime.tool_call_id,)],})modelChatOpenAI(modelqwen3.7-plus,api_keyos.environ[DASHSCOPE_API_KEY],base_urlos.environ[DASHSCOPE_BASE_URL],)agentcreate_agent(modelmodel,tools[lookup_order,lookup_refund_policy,submit_refund_request],state_schemaSupportState,context_schemaSupportContext,system_prompt(先查询订单和规则再向用户说明只有用户明确确认后才允许提交退款申请。),)resultagent.invoke({messages:[{role:user,content:订单 A1024 的耳机坏了我想申请退款。}],refund_stage:not_started,},contextSupportContext(user_iduser-42),)第一次模型调用会从三个 schema 中选择。正常路径先产生lookup_order或lookup_refund_policy请求tool node 执行后返回事实。由于用户只表达“想申请”system prompt 要求明确确认模型应先说明规则并询问而不是立即调用写工具。当用户在后续 turn 确认模型才生成submit_refund_request。函数从runtime.context取得受信任 user id内部业务服务还应验证订单归属、可退款金额和幂等键。成功后Command同时把refund_stage更新为submitted并给模型一条包含申请号的ToolMessage。示例中的request_id只是展示结果合同不能作为生产幂等实现。真实系统应由退款服务生成或接受稳定幂等键并在重复调用时返回同一业务结果。Agent 可能因为网络超时、消息重放或模型重试再次请求写工具工具必须把“重复请求”当成正常边界处理。10. 高风险 Tool 上线前的五道闸门工具能够运行只是上线条件的一部分。尤其是退款、删除、发信和修改权限等写操作至少要检查五道闸门。第一道是最小权限工具只能访问完成当前动作所需的数据和 API。不要把后台管理员 token 交给通用 HTTP 工具。第二道是输入校验模型生成的参数必须经过类型、值域和业务规则检查。第三道是幂等与事务重复调用不能重复扣款或创建多条申请。第四道是超时与错误分类失败必须可恢复、可转交或明确终止。第五道是审计与审批记录调用者、参数摘要、结果和人工确认。五道闸门都在模型之外。Prompt 可以提醒模型何时使用工具却不能证明调用者有权限schema 可以限制字段类型却不能证明订单真实存在ToolMessage可以说明动作完成却不能替代事务和审计。工具数量增加时还应做能力治理。合并语义重复的工具拆分读写权限记录每个工具的 owner、SLA 和数据敏感级别定期检查调用分布和失败率。一个长期 Agent 的工具集合更像受控 API 产品而不是不断增长的函数列表。观测指标也应覆盖协议各段。工具未被选择要看 schema 暴露和模型决策参数校验失败要看字段设计执行失败要看外部依赖结果回流后仍然误判要看ToolMessage内容。仅统计工具函数的 HTTP 成功率无法说明整个 Tool Calling 是否可靠。上线前可以用离线用例覆盖三类输入明确应调用、明确不应调用、两个工具容易混淆。再用沙箱执行读工具和写工具确认权限、重复调用和失败回执。工具一旦接触真实世界选择准确率与执行安全性必须同时达标。11. 回到协议模型只提出动作系统决定动作怎样成立现在可以重新解释售后 Agent 的三项能力。tool把函数名、描述和类型转成 schema模型根据上下文生成tool_callsharness 验证并执行tool_call_id把结果送回正确请求ToolRuntime注入模型不该填写的身份和依赖string、object 或Command决定结果进入消息还是改变 state。这条链最重要的边界是Tool Calling 是模型产生的结构化请求Tool execution 是应用受控执行的真实动作。二者之间要经过参数校验、权限、幂等、超时和审计。任何“模型已经调用了数据库”的表述只要没有指出执行层就把关键工程责任隐藏了。设计新工具时可以按固定顺序自查名称和描述是否能与其他工具区分模型参数是否只包含业务输入运行依赖是否通过ToolRuntime注入返回值是否匹配后续消费者并发更新是否有 reducer 或事务失败是否保留类别高风险动作是否要求审批。第 07 篇会接住这里留下的问题ToolRuntime里的 context、state、store、stream writer 和 execution info 分别是什么为什么 user id 适合放 Runtime Context当前退款阶段适合放 State而长期偏好又应进入 Store把数据放进正确容器工具协议才不会在运行久了以后逐渐失控。