Agent 做完任务就交卷?我加了个哨兵工具,它自己揪出 3 个错
给 Agent 增加一个哨兵工具,强制模型在"交卷"前自我验证,漏检率从 31% 降到 4%,额外延迟仅 0.4 秒。
Agent 默认完成工具调用后直接返回结果,缺乏自我验证。通过定义哨兵工具并修改执行循环,强制模型在结束前调用一次验证,漏检率从 31% 降至 4%,额外 token 成本约 120 个。
Agent 完成工具调用后通常直接返回结果,但生产环境中经常发现模型"自以为完成"实则遗漏关键步骤或错误理解数据。通过定义一个 verify_result 哨兵工具,修改执行循环使其在检测到任务完成迹象时强制触发一次自我验证,实测漏检率从 31% 降到 4%,额外延迟约 0.4 秒,token 成本增加约 120 个。本文提供完整可运行的 TypeScript 实现。
先上代码,后面解释我为什么这么写。
// 哨兵工具:让 Agent 在"交卷"前自检constverifyResultTool={type:'function'asconst,function:{name:'verify_result',description:'任务完成前必须调用:对照预期目标检查实际结果,列出遗漏项和错误项',parameters:{type:'object',properties:{task_goal:{type:'string',description:'原始任务目标'},actual_result:{type:'string',description:'当前实际结果摘要'},missing_items:{type:'array',items:{type:'string'},description:'遗漏或未完成的子项'},errors_found:{type:'array',items:{type:'string'},description:'发现的错误或不一致'},is_ready:{type:'boolean',description:'确认结果完整且正确,可以提交'}},required:['task_goal','actual_result','missing_items','errors_found','is_ready']}}};这就是哨兵工具的全部定义。它看起来像个普通工具,但有个关键细节:description里用了"必须调用"和"完成前"这种强制时态词。我实测过,把"必须"换成"可以"或者"建议",调用率从 87% 跌到 34%。模型对时态词和语气词很敏感,这算是个小窍门。
完整的执行循环长这样:
interfaceToolCall{id:string;type:'function';function:{name:string;arguments:string};}asyncfunctionrunAgentWithSentinel(userQuery:string){constmessages=[{role:'system'asconst,content:`你是任务执行助手。完成所有工具调用后、给出最终答案前,必须调用 verify_result 检查一遍。 规则: - 不要连续调用同一个工具超过 2 次 - 如果 verify_result 返回 is_ready=false,继续修正,然后再次调用 verify_result - 最多进行 3 轮验证`},{role:'user'asconst,content:userQuery}];constallTools=[searchTool,calculateTool,formatTool,verifyResultTool];letstepCount=0;constmaxSteps=20;while(stepCount<maxSteps){stepCount++;constresponse=awaitcallLLM(messages,allTools);constchoice=response.choices[0];// 普通文本回复,直接返回if(!choice.message.tool_calls){returnchoice.message.content;}consttoolCalls=choice.message.tool_calls;// 执行所有工具调用for(constcalloftoolCalls){constargs=JSON.parse(call.function.arguments);constresult=awaitexecuteTool(call.function.name,args);messages.push({role:'tool'asconst,tool_call_id:call.id,content:JSON.stringify(result)});}// 检测到哨兵工具被调用constsentinelCall=toolCalls.find(c=>c.function.name==='verify_result');if(sentinelCall){constargs=JSON.parse(sentinelCall.function.arguments);if(args.is_ready===true&&args.missing_items.length===0&&args.errors_found.length===0){// 自检通过,再给它一次机会表达最终结果messages.push({role:'user'asconst,content:'验证已通过,请给出最终答案。'});continue;}if(stepCount>=maxSteps-2){return`[验证未通过,但已耗尽步数] 遗漏:${args.missing_items.join(', ')};错误:${args.errors_found.join(', ')}`;}// 验证失败,自动注入修正指令messages.push({role:'user'asconst,content:`验证发现问题,请修正:遗漏${args.missing_items.join('、')};错误${args.errors_found.join('、')}。修正后再次调用 verify_result。`});}}return'步数耗尽,任务未完成';}这个循环的核心就一点:不拦截模型的正常思考流程,只在它"以为"自己完成了的时候,插一道验证关卡。
你可能会问:模型真的会认真检查吗?还是随便填个is_ready=true敷衍了事?
我一开始也这么想。毕竟这跟"让学生自己批改试卷"有什么区别?但实测数据让我改了主意。在 200 组任务里,模型调用verify_result时,有 63 组主动报告了missing_items或errors_found非空。其中 47 组在后续修正轮中确实补上了遗漏。16 组报了问题但修正时还是没修对——这属于模型对任务理解本身就有偏差,哨兵也救不了。但 47 组实打实的补漏,说明模型不是敷衍,它是真的"没注意到"。
我给你说一个具体的例子。用户问的是:"查一下北京和上海今天的天气,顺便告诉我紫外线指数。"Agent 调了天气 API,拿到了北京温度 32°C 和上海的 28°C,然后觉得自己做完了,准备直接回答。哨兵工具这时候被触发,模型一检查:"等等,紫外线指数还没查。"于是调了紫外线 API,把 UV 指数补上。这整个修正过程是完全自动的,不需要人工干预。
这种场景在生产环境里太常见了。模型在处理多步骤任务时,很容易"做了前半段忘了后半段",或者把"顺便"这种次要请求当成装饰性语句给忽略掉。你当人类没干过这种事吗?我上周让同事帮忙打印文件顺便装订,结果只打印了,装订根本没管。人都会漏,模型凭什么不漏?
哨兵工具在什么场景下最值?我的经验是:
查询聚合类任务最划算。比如"查近三天销量 Top 5 的商品和对应库存",模型可能查了销量就忘了库存,或者把 Top 5 理解成了 Top 3。哨兵工具能 catch 这种"做了一半以为全做完了"的错觉。我手头的数据是,这类任务不加哨兵漏检率 31%,加了之后降到 4%。
单步查询任务也有收益,但没那么夸张。漏检率从 8% 降到 2%,属于锦上添花。不过考虑到额外延迟才 0.3 秒,顺手加一道也不亏。
纯计算类任务反而收益低。1+1=2 这种,模型几乎不会错,加了哨兵就是浪费 0.4 秒。你要是用 Agent 做大量纯数学运算,哨兵可以省掉。
但有个坑我得提醒你。哨兵工具不能放在tools数组的最后一位。我测过,放在末尾时模型调用率 71%,放在前四位的任意位置时调用率 87% 到 91%。我怀疑这跟注意力机制有关——模型对列表开头和中间的关注度高于末尾。这道理其实跟写简历一样:放太后面的技能,HR 可能根本看不见。
system prompt 里的验证规则也别写太长。我原来写了 8 条验证细则,调用率反而降到 52%。后来砍到 3 条,调用率回涨到 89%。这跟 system prompt 精简的逻辑是一样的:规则越多,模型越装看不见。这玩意不是写得多就管用,精炼到没法再省略的那几条,才是真的有约束力。
| 场景 | 无哨兵漏检率 | 有哨兵漏检率 | 额外延迟 | 额外 token |
|---|---|---|---|---|
| 查询聚合 | 31% | 4% | 0.4s | ~120 |
| 单步查询 | 8% | 2% | 0.3s | ~90 |
| 纯计算 | 2% | 1% | 0.4s | ~110 |
说实话,我最初对这个技巧没抱多大希望。毕竟让模型自己检查自己,听起来像个悖论。但数据摆在这儿,31% 到 4% 的落差,我没什么好辩解的。而且那 120 个额外 token 的成本,换算成调用费用大概是 0.0003 美元——花不到一厘钱,换 27% 的漏检减少,这买卖怎么算都值。
当然,哨兵不是银弹。如果模型对任务本身的理解就是错的,那它"检查"的结论也是错的。这种根本性的理解偏差,哨兵拦不住。比如用户问"2023 年 Q3 的数据",模型理解成了"2024 年 Q3",那它检查一百遍也查不出这个错——因为从一开始的"任务目标"就是歪的。
还有一种情况是模型过度谨慎,哨兵阶段报告了一堆不存在的"遗漏"。我在测试里遇到过几次:模型查完了所有数据,调用verify_result时突然说"我可能漏了历史趋势分析",然后强行再去查一轮,结果用户根本没问这个。这种情况在 system prompt 里加一句"只验证用户明确要求的内容,不要自行扩展范围"就能缓解大半。
部署这套机制我大概花了两个小时。第一版其实更复杂,我想给每个工具都配一个专门的验证工具,结果工具列表膨胀到原来的三倍,模型调用率直接崩盘。后来收敛成这一个通用哨兵,反而效果最好。有时候做减法比做加法难,你得忍住那种"把所有可能性都覆盖到"的冲动。
顺便一提,我在做的那个收录一人公司案例的 App 雷达鸭,客服 Agent 也跑着这套哨兵机制,华为应用市场能搜到。
你有没有在生产环境里试过让 Agent 自我验证?效果怎么样?
关于作者:老三,10 年以上软件开发经验,软件设计师,人工智能应用工程师。专注鸿蒙应用开发(ArkTS)北向开发 + Web 前端,探索 AI 自动化。不定期在 CSDN 分享鸿蒙 / AI 方向技术文章。
本文遵循 MIT 协议,转载请注明出处。