Harness Engineering实战:将Agent工具调用成功率从65%提升到92%
做Agent的人几乎都有过这种体验demo演示时智能体调用工具行云流水一换到真实业务场景成功率立刻掉得让人怀疑人生。我最近在做一个模拟项目X基于大模型实现客服工单自动分类与处理。第一版里系统调用工具的成功率只有65%左右剩下的35%里有参数格式写错的、有选错工具的、还有明明没查到数据却编造一个订单号传进去的。后来我才意识到问题不在模型本身而在于我完全没有为“工具调用”这一动作做工程化设计。这正好对应了标题里的Harness Engineering围绕智能体的工具调用做一套系统性的约束、校验、恢复和观测机制。这篇文章就把我自己的改造过程、框架思路和踩坑记录完整展开适合正在做Agent落地、对口才率头疼的开发者参考。1. 先搞清楚Harness Engineering到底在解决什么问题1.1 一个让我刻骨铭心的联调现场我第一次真正重视工具调用成功率是在某客服工单处理Agent的第一轮联调中。当时的方案并不复杂让大模型根据用户问题判断意图在订单查询、库存查询、退货申请、工单创建这四个工具里选一个来调用把返回结果整理成回复。我准备了20条测试case觉得这还不简单结果一跑既惊喜又意外。能从头到尾走通工具调用链路的只有13条。剩下7条里有2条是返回的JSON里参数名错了一个字母有2条是多轮对话里模型突然开始“自由发挥”压根不调用工具直接自己编了个订单状态出来还有3条更离谱前端对话里明明已经给过用户的订单号模型第二轮却把这个id传进了另一个完全不相关的工具里。这个场景千万别说你遇不到。Demo环境里你给模型的任务通常是“晴天霹雳”式的单点问题上下文干净、干扰少真实业务里每个case都带着上下文、带着历史记录、带着上游接口的各种不确定性。工具调用成功率低于某个阈值Agent就根本没法上线。1.2 成功率难提的三层原因要把一个问题解决掉第一步是把问题拆开。我习惯把工具调用成功率定义成两层第一层是模型成功选对工具、产出合规参数第二层是工具真的执行成功、结果能被正确解析和使用。但在实际排查中你会发现还有一条看不见的鸿沟就是模型本身只活在文本世界里。第一类失败我叫它生成侧失败。具体表现是JSON解析报错、参数缺失、参数名写错、枚举值超出定义范围、干脆选错工具。根本原因是模型在生成结构化内容时没有严格遵守你隐性地要求它的“格式契约和业务契约”。第二类失败我叫它执行侧失败。模型给的参数没问题但上游接口超时了、返回了异常格式、鉴权过期、服务临时不可用。这类失败很多人忽略模型输出对了就觉得万事大吉结果准线上崩的都是这一层。第三类最隐蔽我叫它上下文污染。多轮工具调用会把中间结果整个塞进上下文塞得越多模型越容易被旧结果干扰。上一轮的错误内容、上一轮的中间状态都会在后续决策中产生误导。成功率的提升绕不开这三层问题的同时治理。1.3 为什么说这是工程问题不是模型问题不少人的第一反应是“换一个更强的模型不就行了”。我承认模型能力是上限但工程是下限。哪怕换了更强的模型如果不给工具调用加约束和容错同样会出现参数幻觉和工具误选。反过来把工具描述、参数校验、错误回填、重试降级、观测评估这五件事做扎实用相对普通的模型也能把成功率拉到可用线上。另一个现实因素是模型能力升级是黑盒。今天你用某个大模型API调通了明天它升级了版本你的工具调用成功率可能完全不一样。工程的本质就是把这些不确定性收敛到一个可控范围里。模型不稳定就用校验、重试、降级来兜底。说直白点Harness Engineering的精髓不是“把模型供起来”而是给它套上一套工程“夹具”让它在发挥不稳定的情况下也能稳定产出。2. 五层工程框架把工具调用成功率从碰运气变成可管理2.1 工具定义层Schema是接口契约不是填空题很多人写工具描述的时候特别随意觉得“不就是给模型看一句话说明嘛”。这是大坑。工具的名称、描述、参数说明、枚举值不仅仅是给开发者看的更是给模型看的“接口契约”。我总结了一套比较稳的写法。工具名尽量用动词开头比如query_order、create_ticket不要用orderQuery这种不伦不类的命名。描述里至少要包含三部分这个工具是干什么的、什么时候才该用它、什么情况下绝对不能用它。参数说明要写清楚取值规则比如“订单号来自用户输入严禁编造”就比“订单号”三个字有用得多。再说一个细节很多工具的参数是有依赖关系的比如“查询某订单的物流”必须依赖“先拿到订单号”。这种依赖关系在Schema里没有标准字段可以表达只能在描述里写明白。如果模型不知道这个依赖它就可能跳过前置查询直接编参数。这类边界条件必须写进描述里而不是指望模型自己顿悟。2.2 决策引导层把使用规则写进提示词工具Schema负责“定义有什么工具”决策提示词负责“什么时候用哪个工具”。很多Agent项目把这部分交给模型自由发挥效果很不稳定。我的做法是写一段“工具使用三原则”固定放在系统提示词里。原则一能不调用工具就不调用。不是每个问题都需要翻库闲聊、寒暄、解释性回答直接回复就行。原则二调用前必须确认目标没有拿到足够信息之前不用猜。原则三参数必须来自已知数据。订单号、用户ID、商品编码必须来自用户提供或工具返回不能“创作”。这段规则看起来简单但实际效果非常显著。我加的这三条规则把“强行调用工具”的比例从25%降到了8%左右。模型不是不知道该不该调工具而是没有一个清晰的决策边界容易在上下文里迷失。你给它规则它就有依据可循。2.3 执行校验层把错误拦截在模型之外模型输出的参数直接传给工具是一定会出问题的。无论它多聪明都有概率把字符串写进数字字段把日期格式填错或者给一个超出业务范围的数值。所以在真正执行工具之前必须加一个校验层。我通常做两层校验。第一层是格式校验也即JSON Schema校验检查类型、必填项、枚举值。第二层是业务校验需要开发者自己写规则比如金额必须大于0、日期不能早于今天、订单号必须匹配某种格式。这两层校验任一层不过都不能直接执行工具。关键点来了校验失败之后不要把错误简单吞掉而要把明确的错误信息喂回给模型。举个例子你告诉它“参数amount必须是正数你传了-50”它大概率下一轮就能改对。这个过程叫错误回填是整个Harness Engineering里最值钱的一环。不加这层成功率上不去加了这层模型就有了“自我修正”的能力。2.4 恢复补偿层给智能体一条改错的路智能体不是一次调用定生死而是一个循环过程模型生成工具调用、执行工具、把结果放回上下文、模型看到结果再决定下一步。这个循环本身就是天然的重试机会。我的恢复策略分三层。第一层叫单步修正校验失败后把错误信息回填给模型让它修正参数后重试。第二层叫整体降级如果连续两三次都在同一工具上报错就放弃这个工具改用其他可用工具或直接转人工兜底。第三层叫超时熔断单个工具执行超过设定阈值后立刻终止避免用户长时间等待。这里有个很容易犯的错重试次数设得太多。有人觉得“让模型多发几次总会蒙对”实际情况是重试几次后模型会陷入重复犯错的状态不仅成功不了还会浪费大量token和耗时。我的经验是同一工具的单轮重试上限设2次就够了超过就直接走降级路径。2.5 观测评估层用数据代替感觉工具调用成功率不是一个模糊的“体感”必须量化。我在项目里做了两件事。第一是日志追踪每次工具调用都记录下触发了哪个工具、模型原始输出是什么、校验是否通过、执行结果如何、哪一层失败的。有了这些日志你才能对着数据做分析。第二是回放评估集。把历史上2周到1个月的真实case攒成一个固定集合每次改动提示词、调整Schema或者升级模型之后都对整个集合跑一遍对比前后成功率的变化。没有这个评估集你根本不知道一次改动是变好了还是变坏了只能靠拍脑袋。这两个东西合起来就是给Harness Engineering装上了一只“眼睛”。没有度量就没有优化方向。我见过很多团队花大量时间调提示词却从不回放历史case最后改来改去不知道自己在干什么。3. 实操全过程把成功率从65%拉到92%3.1 第一步重新设计工具Schema这个改造是从工具定义开始的。以订单查询工具为例我最初的描述就一句话“查询订单状态”。改完之后是这个样子{ name: query_order, description: 根据订单号精确查询订单状态。当用户询问配送进度、订单状态、售后进度时使用。订单号必须来自用户输入或历史消息中已出现的信息严禁编造。若订单号缺失直接回复用户请求补充不要调用本工具。, parameters: { type: object, properties: { order_id: { type: string, description: 完整订单号10位字母数字组合 }, include_details: { type: boolean, description: 是否包含完整明细行默认false } }, required: [order_id] } }改动点有三个名称从orderQuery改成了query_order描述从一句话扩展成了一段带条件和边界的话参数说明里明确写了“来自用户输入或历史消息”和“严禁编造”。光这几行改动参数幻觉类错误就明显变少了。当时我不理解为什么要写这么长描述后来发现这是模型唯一能看到“工具语义”的窗口。描述越模糊模型的猜测空间就越大。你给它足够清晰的行为边界它就不需要靠猜。3.2 第二步写入工具使用规则与决策提示词第二步是把决策规则写进系统提示词。我在原有系统提示词后面追加了一段工具使用规则 1. 优先回复用户问题除非问题确实需要查询数据或执行业务操作。 2. 在执行工具前必须确认已经从上下文获得全部必要参数比如订单号、用户ID。 3. 参数值必须来自用户提供或工具返回不得猜测或编造。 4. 调用工具之前先用一句话说明你为什么要调用这个工具。第四条很关键。让模型显式说出来“我要调用query_order因为用户提供了订单号”会极大提高调用正确率。原因是这个“思考外化”让模型在决策时多了一步自检。改造之后无效调用率从25%降到了8%。无效调用指的是本不该调工具却调了或者调了但参数明显无意义。就一段提示词效果立竿见影。3.3 第三步实现参数校验与错误回填闭环代码层面我写了一个简单的执行循环核心就是把校验、执行、错误回填串起来。伪代码如下import json def run_tool_loop(system_prompt, user_message, tools, max_rounds3): messages [ {role: system, content: system_prompt}, {role: user, content: user_message} ] for _ in range(max_rounds): resp llm_chat(messages, toolstools) if not resp.tool_calls: return resp.content for call in resp.tool_calls: try: args json.loads(call.function.arguments) validate_schema(args, call.function.name) # 格式校验 validate_business(args, call.function.name) # 业务校验 result execute_tool(call.function.name, args) result normalize_result(result) except ValidationError as e: result { result: None, error: str(e), hint: 请严格参考错误信息修正参数后重试 } except ToolExecutionError as e: result { result: None, error: f工具执行失败{str(e)}, hint: 若重试仍失败请改用其他方案或说明当前无法处理 } messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) return 【已超过最大处理轮次转人工】这套循环里最关键的是把校验失败的错误信息回填给了模型让它在下一轮自己修正。实测下来很多参数错误不需要单独写纠错逻辑模型看了错误提示就能自己改对。我当时统计过加上这层错误回填之后单轮内修正的case比例大约是38%。也就是说原来注定失败的case里有一大部分被这个机制“捞”了回来。3.4 第四步设定重试、熔断与降级策略重试策略不能只靠脑子记要写成代码逻辑。我给自己定了一个规则单工具校验或执行失败最多回填重试两次第三次依然失败就不再让模型继续试同一个工具而是给它两个选择调用其他等价工具或者直接回复用户“暂时无法处理已转人工”。熔断和降级针对的是真正超时或系统级故障。比如上游订单接口挂了不管模型重试多少次都是失败。这时候就要在工具执行函数里加超时控制超过5秒就抛异常同时标记这个工具为“本次会话不可用”。降级路径还有一个价值防止用户长时间卡在一个不可能完成的任务里。我们做的是客服场景用户体验优先级极高。工具失败后能给出明确反馈并转人工远比让用户对着一个永不出结果的聊天框强得多。3.5 第五步建立回放评估集改造走到这一步光看几个case已经不够了我开始搭回放评估集。做法很简单把过去一个月的真实对话凡是涉及工具调用的case全部抽出来去重后打标签分成“应该调用A工具”“应该调用B工具”“不应该调用任何工具”三类。然后每次改动Schema、提示词、或者换模型版本我都对整个评估集重跑一遍统计几个指标工具选择准确率、参数合法率、链路完整率、无效调用率。一次改动好不好直接看这几个数字。这里分享一个数据。我用这个评估集对比改造前后整体工具调用成功率从65%涨到了92%其中工具选择准确率从78%涨到了96%参数合法率从72%涨到了94%链路完整率从60%涨到了87%。没有这几层工程手段模型再聪明也顶多帮你提升几个点工程手段才是把那二十多个点补上的关键。4. 高频问题与排查技巧实录4.1 七种“当场翻车”场景速查表问题现象根本原因解决办法JSON解析一直失败模型输出被截断调大max_tokens精简工具描述减少单次生成内容量参数名错一个字母模型对参数名理解不准参数名用全称避免缩写描述里给每个字段举例选了错误的工具工具描述混淆重写描述加入“何时使用”“何时不用”的边界说明编造不存在的订单号上下文里没有明确来源加规则“参数必须来自用户输入或已查询结果”前置工具调用多轮调用时复用旧参数上下文污染在工具结果中明确标注“该结果仅本次有效”必要时清理历史tool消息工具返回结果太长模型读不过来上下文爆炸结果归一化只保留关键字段长列表分页同一个工具反复失败不换路缺少降级逻辑设置重试上限超过后强制转人工或换其他工具这个表里的每一行都对应真实踩过的坑。第4行尤其典型模型编参数不是因为它“坏”而是它的生成目标里根本没有“必须来自真实数据”这个约束。你把这一条写清楚问题就缓解了。4.2 定位问题的分层排查法一旦发现某个case成功率不对我的排查顺序是先看日志里的失败层再看模型原始输出最后才动代码和提示词。第一步判断是生成侧还是执行侧。如果模型输出了工具调用但参数校验不过属于生成侧如果参数校验过了但工具执行抛异常属于执行侧。这两类问题的修法完全不一样搞混了会白花时间。第二步看模型原始输出。很多框架帮你把工具调用解析好了但你看不到原始的arguments字符串。我强烈建议在日志里保存原文。因为有不少问题是框架解析层的bug而不是模型的问题。不保留原始输出这些bug根本看不见。第三步复现并缩小范围。把失败的case单独拎出来只保留最少的上下文重跑一遍。如果能复现说明是固定的提示词或Schema问题如果不能复现说明是随机性或上下文干扰问题继续调整上下文管理策略。4.3 我踩过的三个隐蔽坑第一个坑max_tokens设太小。我当时为了省成本把max_tokens设成了512。订单查询工具还好一旦工具多了、参数多了模型输出到一半就被截断返回的JSON永远解析不完。后来我把max_tokens调大并把工具结果精简到只保留关键字段截断问题基本消失。第二个坑工具结果直接全文回填。第一次做多工具调用时我把工具返回的完整JSON全塞回上下文。结果就是模型被一堆无关字段干扰下一轮决策反而更容易出错。我的解决办法是写了一个归一化函数每个工具只返回精简摘要。比如订单查询只保留“状态、预计送达时间、物流公司”其他字段全部丢弃。上下文干净了准确率立刻提升。第三个坑评估集里全是“顺利case”。最初我筛评估集时只挑了那些成功或者半成功的case失败case总觉得没用。后来发现这导致我改提示词后旧的失败模式虽然消失了新的失败模式却完全没有发现。从那以后我要求自己评估集必须包含失败case尤其是哪些曾经连续失败的case。它们是优化工具描述和规则最宝贵的素材。5. 多工具协同场景下的进阶优化5.1 从单工具到多工具的编排策略前面的改造基本是单工具场景。但真实Agent不可能只有一个工具我那个客服系统就同时有订单查询、库存查询、退货申请、工单创建四个工具。工具一多模型的选择难度指数级上升。这时候需要引入“技能路由”的思路。也就是说不要把四个工具都一次性平等地丢给模型。你可以先用一个意图分类器把用户问题粗分成几类每一类只激活对应的两三个工具。意图分类可以用一个小分类模型甚至可以用同一个大模型做一次“意图判断”后再进入工具调用阶段。这样模型面对的工具候选集变小选择准确率自然就上去了。我实测的效果是四工具全量开放时工具选择准确率大约94%改成意图路由后同样的case准确率升到了98%。减少选项数量比反复改提示词更有效。5.2 并行调用与参数依赖多工具场景里还有一个麻烦有些工具可以并行调用有些工具必须串行因为后者依赖前者的输出。比如“查询用户最近一单的物流信息”必须先调用“查询用户最近订单”拿到订单号再根据订单号调用“查询物流”。这种参数依赖关系Schema里表达不清需要在编排逻辑里显式控制。我当时的做法是把工具调用拆成多个阶段。第一阶段调用无依赖工具拿到结果之后再把结果中的关键字段注入第二阶段提示词去调用依赖工具。这个过程本质上是把模型的多步推理拆成了可控制的步骤。并行调用还有一个好处是节省时间。两个互不依赖的工具同时发起请求总耗时从串行的几百毫秒降到了几十毫秒。不过并行时要注意所有参数都必须能在调用前拿到不存在相互依赖。5.3 工具数量膨胀后的收敛策略工具一多描述文字也会变多。把5个工具的描述全塞给模型每个描述300字光这块就占了1500字很容易让模型注意力涣散。我的策略是控制“同时可见的工具数量”。做法是分组。交易相关工具一组物流相关工具一组客服管理工具一组。根据意图路由的结果只把当前相关组里的工具定义传给模型。这样每次调用时可见工具保持在2到3个既省token又提升准确性。如果某个工具长期没被模型选中我会回头检查它的描述是否太模糊、参数是否太复杂、和别的工具是否功能重叠。工具设计也是一个需要持续迭代的事不是写完了就完。随着业务演进旧工具要退役新工具要上线评估集也要跟着更新。另外提一个长期习惯每新增一个工具必须先在回放评估集里挑至少5个相关case测试一下确认不会干扰已有工具的选择。刚开始我嫌麻烦直接上线结果新工具一加旧工具的选择准确率掉了好几个点。从那以后新工具上线前先过评估集成了我的标准流程。工具调用成功率这件事做到92%不难难的是守住它。模型升级、工具接口变化、用户问题分布变化都会让成功率悄悄回落。我自己现在的习惯是每周看一次成功率趋势每次改动都回放评估集每次上线新工具都跑回归。这套流程坚持下来工具调用才真正变成了Agent里最可靠的一环而不是整个项目里最容易翻车的部分。