首页 / 资讯中心 / 文章详情

智能体工具系统落地指南:从工具定义到调度与错误处理

智能体工具系统落地指南:从工具定义到调度与错误处理 ★ FEATURED ARTICLE
前阵子我把公司内部一个散落着大量“伪工具”的智能体项目彻底重构了一遍把所有能力收拢成一套真正可被调度的工具系统。这个过程中踩了不少坑很多问题平时根本不会注意到但在工具真正被模型高频调用时会集中爆发。所以我想开一个系列围绕 Hermes agent 的 tools 体系把落地应用这件事拆开讲清楚——从工具怎么定义、怎么注册到模型怎么选工具、怎么执行、怎么处理错误、怎么做观测每个环节都有值得深挖的细节。这篇算系列的开篇先把整体方法论和几个最容易出问题的环节讲透。如果你正在做智能体开发或者你已经被各种 Agent 框架的“工具调用”功能折磨过这篇文章应该能帮你省下不少试错时间。我不会讲太多概念层面的东西重点放在“真实项目中这些模块到底该怎么设计和实现”以及那些文档里不会写的坑。1. 为什么智能体离不开工具系统而不是单纯靠大模型硬答1.1 语言模型的能力边界恰好是工具的起点先把最基础的事情说透。大语言模型本质上是一个基于概率的文本生成系统你给它一段输入它输出一段最符合规律的文字。这意味着它在“知识回忆”“文本重写”“逻辑推理”这类任务上很强但有一个天然的致命短板它对自己回答的内容没有事实校验能力也碰不到任何外部系统。你问它“帮我查一下订单号 12345 的物流状态”它编一个答案出来的概率很高。不是它故意骗你而是它的训练数据里根本没有你的数据库。它唯一能做的是根据见过的类似文本猜一个看起来合理的回复。这就是为什么把智能体直接接给业务系统用会出事——你既不敢让它瞎操作又没法信任它带回来的数据。工具系统解决的就是这个问题。工具是外部能力的“接口”模型并不直接执行动作它只负责做一件事根据对话上下文决定“现在应该调用哪个工具并生成该工具的输入参数”。真正的查库、写文件、发请求都是在工具执行阶段完成的执行结果再作为新的上下文喂回给模型。于是模型从一个“什么都敢说”的生成器变成了一个“只做决策”的调度器。我自己在项目重构过程中的感受特别明显。重构前的系统各种系统能力是通过很长的系统提示词“塞进”模型脑子里的表面上看起来模型什么都会但一旦涉及真实数据就频繁幻觉而且上下文被塞得越来越长推理越来越慢。重构后模型只需要面对一个精简的工具清单决策链路反而清晰了很多。1.2 一个反面案例把所有能力写进 Prompt 的苦日子我原来负责的某内部客服工单系统一开始做智能体的时候就是因为嫌工具调用太麻烦直接把系统操作全部描述写进了系统提示词。看起来挺聪明的做法——团队一共维护了十几个“能力说明”每个说明都写得特别详细包括“查询工单时请使用 GET 请求访问 /api/ticket/{id}返回的 JSON 里包含 status 字段请根据 status 字段判断工单状态”。这套方案在 demo 阶段跑得还行但真正放上线就崩了。主要有几个问题。一是上下文膨胀。十几个能力说明加起来几千字再加上用户问题的历史消息、临时夹带的业务数据模型每轮要处理的 token 量巨大响应延迟从 2 秒涨到了 8 秒。二是参数解析不稳定。模型经常把请求路径拼接错或者把查询参数写到 body 里这种问题在真实调用中相当普遍。更糟糕的是模型失败后还会“自作聪明”地根据错误信息瞎猜比如试着把 URL 里的 id 从字符串改成数字再试一次结果把不相关的订单查出来。三是完全没法做权限控制。因为只要模型掌握了接口地址和参数规范它在一次长对话里可以随意调用任何接口。你也许想过在提示词里写“禁止调用删除类接口”但模型对这种指令的遵守程度在实际压力场景下并不稳定。后来我把这套系统彻底改成工具调用体系所有能力收敛为标准化工具模型不再直接接触接口实现细节而是从工具名和描述中做选择。效果很直接工单查询类任务的准确率从原先的 78% 左右提升到了 96% 左右上下文 token 占用也降到了原来的三分之一。1.3 Hermes agent 工具系统的整体形态很多初学者对“工具系统”的理解比较模糊以为就是在代码里写几个函数然后传给模型就行。实际落地时一个完整的工具系统至少包含下面几层。工具注册中心负责收集、校验、存储所有可用工具并提供查询接口供 Agent 执行引擎在每轮决策前拉取工具清单。工具执行器收到模型的工具调用指令后解析参数、调用对应工具函数、处理返回值。它屏蔽了模型与底层实现之间的差异也负责执行超时、重试这些策略。结果处理器工具执行产生的原始返回数据不能直接一股脑喂回给模型。要先做截断、脱敏、结构化整理让模型拿到的是高质量输入。观测与追踪模块记录每一轮的工具选择、参数生成、执行结果和错误信息方便事后调试和回归。另外还有一个很容易被忽略的模块——权限控制层。不同场景下模型应该拥有不同级别的工具调用权限。比如只读查询的工具可以放心调用但写操作工具可能需要二次确认。这块我后面会展开讲。Hermes agent 的工具体系从一开始就按这个框架设计这给后续的功能迭代省了非常多事。很多东西只有在真实流量下才会暴露问题框架搭对了后面填坑就只是时间问题。2. 工具定义与注册这一步偷懒后面全是灾难2.1 Tool Schema 设计的关键字段工具注册中心的第一件事是把“工具长什么样”用一套标准协议描述清楚。模型能不能正确使用工具很大程度上取决于工具定义的描述质量。这不是玄学是信息工程。一个正规的工具 Schema 至少应该包含这些字段工具名称必须是见名知意的短标识例如 query_order_status不要用那种内部内部代号。工具描述说明工具能做什么、不能做什么、适合什么场景。描述里可以放一些模型容易踩坑的注意事项。参数定义每个参数的名称、类型、是否必填、取值范围、说明。返回值定义返回的数据结构以及可能出现的错误码说明。执行属性超时时间、是否允许并发、是否需要用户确认、访问权限级别。我在项目里见过一个很典型的反例工具描述写得太简略比如“查询订单”模型根本不知道该传什么参数。然后模型就会选择发挥它的想象力把用户说的任何数字都当成订单 ID 传进来结果自然是大量无效查询。于是我给团队定了一条规矩每个工具的描述必须包含“能力边界”。比如“查询订单状态根据订单 ID 查询订单当前状态仅支持查询近 90 天内的订单不支持订单创建、修改操作订单 ID 为系统生成的 13 位数字”。这样模型就不会去用这个工具做它不该做的事。参数定义方面除了常规的 JSON Schema 类型描述外我还强烈建议给每个参数写一个“说明”字段里面尽量写清楚这个参数从哪里获取。比如“用户说‘我的订单’时请先调用 get_current_user 获取用户 ID再传入本工具”。这能让模型在参数生成时显著少出错。2.2 静态注册与动态注册怎么选工具注册有两种常见模式静态注册和动态注册。静态注册就是系统启动时把所有工具一次性注册好之后不再变化模型每轮看到的工具清单都是固定的。它的好处是简单、稳定、容易调试工具清单在启动时就能做完整校验。坏处是不灵活如果工具数量非常多——几百上千个——每轮决策把所有工具描述都塞给模型token 开销会变得非常大模型的选择准确率也会下降。动态注册是根据当前对话上下文动态决定哪些工具进入模型的可见清单。比如用户聊的是“退款”主题就从工具库里检索出与退款相关的工具其他不相关工具就不出现在模型面前。这种方式的优点是省钱、准缺点是复杂度高需要维护索引和召回策略而且召回失败会导致模型“看不到”该用的工具最后给出一个很蠢的回答。从我的实践经验来看起步阶段建议先用静态注册把所有工具都暴露给模型把正确率做上来。当工具数超过 50 个之后再考虑做动态注册或者说工具路由。工具路由本身也可以做成一个工具——让模型先选“工具包”再在工具包内选具体工具这种层次化设计在很多场景下都更好用。提示工具注册中心一定要做 Schema 校验。上线前写一个自动化测试遍历所有工具定义检查描述里是否有过长的字段、参数是否有重复、默认值是否合法。这些问题在运行时爆发排查成本远高于启动时拦截。2.3 从函数到可调用工具的完整流程用一个实际例子说明工具注册的代码实现。假设我们有一个查询库存的工具在 Hermes agent 体系里大致长这样。tool.register( namequery_inventory, description查询指定商品SKU的当前库存数量。仅用于查询不支持库存修改。适合用户询问还有货吗库存多少等场景。, parameters{ sku_id: { type: string, required: True, description: 商品的SKU编码格式为字母数字的组合例如 SKU123456, }, warehouse: { type: string, required: False, enum: [华东仓, 华南仓, 华北仓], description: 仓库名称不传则默认查询所有仓库, } }, timeout_seconds10, require_confirmFalse, access_levelread, ) def query_inventory(sku_id: str, warehouse: str all): # 真实逻辑查询库存服务 result inventory_service.query(sku_id, warehouse) return { sku_id: sku_id, warehouse: warehouse, total: result.total, available: result.available, reserved: result.reserved, }注意几个细节。tool 描述里写了“不支持库存修改”这相当关键因为模型在对话里如果发现用户有修改需求就知道不能用这个工具会去寻找其他工具而不是拿只读工具硬凑。参数描述里给了 SKU 格式样例模型生成参数时就有了参考锚点实测参数错误率能下降一个量级。注册完成后每次 Agent 循环开始前执行引擎会从注册中心拉取工具清单经过必要的过滤比如权限过滤、上下文路由过滤后把工具描述转成模型要求的协议格式发送给模型。模型返回结构化的工具调用指令执行器再根据指令里的工具名找到对应函数调用并收集结果。这一步整体来看不算难难的是把工具定义写得足够好。我见过太多团队在工具描述上敷衍了事然后花好几周时间调试模型为什么“不听话”实际上根因就是工具描述太差。3. Agent 循环里的工具调度模型到底怎么决定调用什么3.1 工具选择背后的原理以及 Tool Choice 策略当模型收到用户消息和工具清单时它内部实际上是在做一个“下一段文本生成”的任务只不过这段文本的格式被约束成了工具调用的结构。模型会综合理解对话历史、用户当前诉求、工具的名称与描述然后输出一个 JSON包含工具名和参数。这里有一个很重要的概念叫 Tool Choice 策略它控制模型是在“必须调用工具”“可以调用也可以不调用”“禁止调用”之间如何选择。不同的策略适合不同的场景。auto 模式模型自己判断要不要调用工具不调用就直接回答用户。required 模式模型必须调用某一个工具。none 模式禁止调用工具直接回答。我在实际使用中最常用的是 auto 模式但有一个坑在 auto 模式下模型偶尔会自作主张直接回答用户问题而实际上这个问题必须调用工具才能回答。比如用户问“我的订单到哪了”模型如果没抓住“订单”这个关键词可能会凭常识说“请提供订单号”。这种“假聪明”情况在工具设计不合理时特别常见。我的建议是对特定场景可以强制 required。比如在客服工单场景里只要用户表达出查单意图就要求模型必须调用查单工具即使工具执行后返回错误也要把错误信息交给模型二次判断而不是让模型直接猜。实操中可以用意图分类器先判断用户意图再根据意图预置 Tool Choice 策略会稳定很多。3.2 标准 Agent Loop 的实现方式工具调度的核心是 Agent 循环模型决策→执行工具→观察结果→再次决策→直到得出最终答案。def agent_loop(user_message: str, max_steps: int 5): messages [{role: user, content: user_message}] for step in range(max_steps): response llm.chat( messagesmessages, toolstool_registry.get_visible_tools(messages), tool_choiceauto, ) assistant_msg response.message messages.append(assistant_msg) if not assistant_msg.tool_calls: # 模型不再调用工具输出最终回复 return assistant_msg.content for tool_call in assistant_msg.tool_calls: # 执行工具并把结果作为 tool 消息加入上下文 result tool_executor.execute(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) return 已达最大步骤限制请稍后再试。这个循环本身不复杂复杂的是步骤数限制、上下文长度管理和信息质量。我设置的 max_steps 往往是 5 到 8太大会导致模型陷入“反复调用同一个失败工具”的死循环太小则复杂任务根本完不成。真实项目里还有一种情况是模型连续调用多个工具后就忘了最开始的任务目标所以有经验的团队会在系统提示词里明确要求模型“每一步都要回顾当前任务主线不要偏离”。关于并行工具调用现在很多模型支持在一次响应中返回多个 tool_call。这是个好东西比如用户同时问“天气”和“机票价格”模型可以并行调用两个工具。但要注意同一批并行工具调用里最好不要有依赖关系。比如先要拿到用户 ID 才能查订单这两个工具就不能并行必须等第一个结果出来再发起第二个调用。工程上可以在工具描述里写明依赖关系或者在代码层面跟踪依赖图实测下来前者对模型更直观。3.3 超时、并发与重试这些参数不是拍脑袋定的工具调用的工程质量往往体现在这些“小参数”上它们直接决定了用户体验。超时设置方面我先说结论普通查询类工具超时 5 秒内涉及外部第三方接口的10 秒内因为第三方接口经常有偶发慢请求。超时设置太短工具频繁中断模型拿不到结果就会开始胡编太长等待时间拖垮体验。我一般用“P95 延迟 × 2”作为基准然后人工再取一个整数。重试方面并非所有工具都该重试。查询类工具、幂等操作可以重试创建订单、转账这类有副作用的操作绝不能盲目重试否则可能产生重复订单或重复扣款。重试次数我一般控制在 2 次以内且采用指数退避第一次等 500ms第二次等 1.5s。超过重试次数就直接报错不要把错误无限吞掉。并发方面模型如果发起了多个并行工具调用执行器要控制并发上限。我见过一次事故某个工具的接口原本只有 50 QPS 的容量结果模型一次并行调了 20 个工具直接把上游服务打挂了。之后我强制要求每个执行队列设置并发信号量比如Semaphore(5)配合连接池复用情况就稳多了。经验工具执行器里一定要设计“熔断”机制。当某个工具连续失败超过阈值比如 5 次自动开启熔断不再接收新的调用并直接返回“工具暂不可用”的错误信息。不然模型会在循环里反复调一个已经挂掉的工具浪费 token 不说还把错误上下文越堆越长。4. 工具执行的可靠性与错误处理脏数据绝不能回灌给模型4.1 返回值不是数据库里有啥就回传啥工具执行器最容易被忽视的一个环节就是怎么处理工具返回的原始数据。很多初学者的做法是把查询结果直接转成 JSON 塞进消息里然后让模型去读结果模型满脑子都是这堆格式化数据反而忘了用户原本问的是什么。这里有三道处理流程可以做。第一字段裁剪。业务系统返回的数据通常有许多内部字段这些字段对模型回答问题没有帮助只会增加 token 消耗和干扰。执行器应该根据工具返回值定义只保留对外可见的必要字段。第二超长截断。如果某个字段非常长比如订单备注有几大段文字全部回传给模型既费 token 又容易影响模型注意力。应该在返回值定义阶段就设定上限例如“备注最多返回前 200 字多余部分以省略号代替”。这个细节对长文档类工具尤其重要。第三脱敏处理。身份证号、手机号这类敏感信息在回传给模型前必须打码。不是说你信不过模型而是这些数据会被完整记录在你的会话日志里一旦日志泄露就是事故。所以最佳实践是在执行器出口统一做脱敏拦截而不是在每个工具里各自处理。4.2 错误信息要“结构化、面向模型”而不是甩一个堆栈工具执行报错时错误信息会作为 tool 消息返回给模型模型会基于这段文字尝试下一步决策。如果错误信息写得很差比如直接抛一个 Python 异常堆栈模型大概率会懵掉接下来就是各种乱试。给模型看的错误信息应该做到说明发生了什么、为什么发生、以及模型可以采取什么行动。举个例子一个库存查询工具抛错时好的错误信息格式是{ error: { code: SKU_NOT_FOUND, message: 没有找到 SKU123456 对应的商品请检查SKU编码是否正确或者询问用户是否提供了正确的商品编号。, suggestion: 你可以尝试让用户核对商品编号也可以引导用户通过商品名称搜索相关商品。 } }模型看到这段内容后大概率会顺着 suggestion 的路径走而不是自己瞎想一个接口去调。所以工具出错处理不仅是一个工程问题还是一个“给模型铺路”的问题。原则上错误信息要告诉模型下一步做什么而不是单纯告诉它做错了。4.3 权限、沙箱与二次确认让 Agent 不能乱来工具系统接入真实业务后权限控制就是一条不可触碰的红线。我给工具分了四个权限级别只读级别查询订单、查询库存、查询用户信息。这类工具模型可以自主调用。操作级别修改备注、更新状态、发送通知。这类工具需要用户在对话中明确表达出操作意图并且系统会进行简单的参数校验。高影响级别创建订单、退款、删除数据。除了用户意图确认外执行前必须弹出确认卡片让用户点击确认才能执行。危险级别批量删除、导出全量数据。这类工具默认不允许运行需要管理员手动审批。这里有朋友可能会问模型真的会“越权调用”吗我的回答是模型当然不会故意越权但它会“误解”。比如用户说“帮我把订单 123 取消掉”模型可能直接调用一个具备写权限的取消订单工具。如果你设计的工具权限里没有拦截机制这单就真的取消了。所以权限校验必须在执行器层面强制检查一定不能指望模型自己守规矩。执行器收到模型的工具调用指令后会先对比该工具所需权限和当前会话的授权等级不满足直接返回“权限不足需要用户确认”。这个过程要在代码里做硬校验不能是提示词层面的软约束。此外涉及运行外部命令、读取任意文件这类高风险工具尽量放到沙箱容器里执行确保即使工具本身有 bug 也不会连累主服务。5. 观测、调试与回归工具落地不可跳过的三件套5.1 全链路日志与调用追踪工具系统一旦上线调试是每个开发者的噩梦。模型调的哪个工具传了什么参数工具返回了什么模型看到结果后怎么反应的如果中间缺掉任何一环出了问题根本定位不了。所以从第一天起就要做好全链路日志每个 Agent 循环都记录一条结构化日志大致包括会话 ID、步骤号、本次工具清单、模型原始响应、提取出的 tool_call 列表、执行器命中策略、工具执行返回、最终回复。这些日志千万别只写在本地文件里要接入集中式日志系统方便按会话 ID 过滤。我遇到过一个问题某用户反馈智能体答错了但本地日志早被轮转覆盖了查无可查。后来全部接入统一日志平台配合 Trace ID 串联整个请求链路复盘效率提升非常多。5.2 离线回放让模型犯错可以被复现工具系统有一个很奇特的调试困境模型是概率性的同一段输入可能产生不同输出所以“复现 bug”不像传统软件那样容易。这也是为什么我会特别强调离线回放。做法不难把线上真实请求的输入和工具调用记录全部保存下来形成一个样本库。模型系统更新后先在样本库上批量跑一遍看看结果有没有变好或变坏。这个过程有点类似传统机器学习里的测试集评估。回放时要注意工具调用需要做 mock不能让测试又真的去查询真实订单、真实库存。要把线上记录的工具执行结果缓存起来回放时直接使用缓存结果保证只测模型决策链路不碰真实外部依赖。这个技巧在给模型切换版本、调整 prompt、修改工具描述时特别有用能帮你快速判断改动到底有没有效果。5.3 工具压测与并发稳定性最后说压测工具系统的瓶颈往往不是模型本身而是下游外部服务。模型推理可以靠队列削峰但如果工具系统疯狂调用外部 API外部服务一挂所有智能体功能都会跟着瘫痪。我吃过一个具体的苦头某次促销活动期间智能体工具调用量激增某个下游搜索服务响应越来越慢最终拖垮了整个智能体链路。后来加了连接池限制和熔断器给该工具单独设了下游调用上限并且把“下游不可用”的错误信息做了兜底回复让模型告诉用户“当前查询人数较多请稍后再试”而不是保持无响应。压测方面建议做两类一是单工具压测摸清每个工具自己的容量上限二是全链路压测模拟真实对话过程里模型反复调用工具的场景。全链路压测最容易发现的问题包括工具描述太长导致推理变慢、并行工具调用过多导致下游被打爆、上下文越来越长导致超时暴增。经验每次上线新工具先灰度放量控制工具在总流量里的曝光比例。比如先让 10% 的会话能看到新工具确认没有异常再逐步放开。很多人直接全量接入结果模型对某个工具描述的理解和预期不符满屏都是乱调用。6. 这个系列接下来要拆解的具体方向开篇写了这么多其实主要是把 Hermes agent 工具系统的整体框架和最容易踩坑的环节梳理了一遍。这个系列我在规划时打算从这几个方向继续深入。单个工具从痛点场景到稳定落地的全过程。比如拿“查订单状态”这个工具举例从需求分析、Schema 编写、参数设计、错误处理到上线观测完整走一遍。多工具协作的编排问题。当一次用户请求需要连续调用三四个工具才能完成时怎么设计工具之间的依赖关系怎么让模型在不同工具之间平滑切换怎么避免模型在长链路中迷失主线。工具自动生成的实践。很多工具是从已有 API 和函数定义自动生成的怎么让模型或代码生成器根据接口文档自动产生工具描述以及自动生成后的描述质量问题怎么检测。工具系统的性能优化。包括工具并行调度的并发模型、上下文压缩策略、大数量工具的召回与路由、以及成本控制方面的实操经验。这些问题我都在具体项目中逐个踩过后续系列会结合真实案例逐一展开。如果你正在做 Agent 或接口工具化改造可以按这个系列的结构对照你的系统很多设计可以拿过去直接改改就用。最后说点个人体会工具系统做得好不好核心还是有耐心去抠细节。描述里多一句话可能模型调用准确率就高一大截错误信息多写一个建议可能模型就少一次无效乱试。这些东西看似琐碎实际才是工具落地成败的关键。后续我会在这个系列里持续分享有具体问题也欢迎随时交流。
阅读完成 · 觉得有帮助?
咨询建站