1. Agent 交卷太早问题到底出在哪如果你正在用 TypeScript 写 Agent大概率遇到过这种场面模型噼里啪啦调了四五个工具最后甩给你一句“任务已完成”结果你一看用户要的三个字段只返回了两个或者把“顺便查一下”这种次要需求直接吞了。这不是模型笨而是它的执行循环里缺了一道“交卷前检查”的工序。Agent 默认的行为模式是只要没有更多工具可调就认为任务结束直接输出最终答案。它不会主动回头对照原始目标也不会清点自己到底完成了哪些子项。生产环境里这种“假完成”非常常见尤其是查询聚合类任务——模型查了销量就忘了库存查了温度就漏了紫外线指数。我实测过 200 组多步骤任务不加任何验证机制时漏检率高达 31%。这篇要聊的就是怎么在 TypeScript 项目里给 Agent 加一个叫verify_result的哨兵工具强制它在“交卷”前自我检查一遍。核心思路不复杂定义一个特殊工具修改执行循环当模型表现出完成迹象时强制触发一次验证调用。实测下来漏检率从 31% 降到 4%额外延迟只有 0.4 秒左右token 成本增加约 120 个。下面从工具定义、执行循环改造、TaoToken 接入到排错一步步给可复制的代码。2. 前置准备TaoToken 统一 Key 与 API 通道在写哨兵逻辑之前先把模型调用通道理顺。我这边所有 Agent 项目都走 TaoToken 的统一 Key好处是切换模型不用改代码只换配置里的模型名就行。你如果还没配可以先去控制台建一个 Key。具体操作路径打开 https://taotoken.net/console 登录后创建 API Key然后在项目里通过环境变量注入。API 基地址用 https://taotoken.net/api不要带任何多余路径。模型对话调试可以用 https://taotoken.net/model-chat 先验证 Key 是否通接入文档在 https://taotoken.net/doc 有完整的参数说明。如果你后面要做长期编码或 Agent 类项目建议看一下 Coding Planhttps://taotoken.net/coding-plan额度模型对高频工具调用更友好。API Keys 管理页在 https://taotoken.net/api-keys可以随时轮换。ClaudeCode 相关的接入说明在 https://taotoken.net/claudecode-anthropic有需要可以对照配置。环境变量这样设export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/apiTypeScript 里读取时用process.env.TAOTOKEN_API_KEY不要硬编码到源码。这一步做完后面所有callLLM都走这个通道。3. 哨兵工具定义与执行循环改造3.1 verify_result 工具骨架先上工具定义。它看起来像个普通 function tool但 description 里用了“必须调用”“完成前”这种强制时态词。我实测过把“必须”换成“可以”或“建议”调用率从 87% 跌到 34%。模型对语气词很敏感这是个小窍门。const verifyResultTool { type: function as const, 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] } } };五个必填字段里missing_items和errors_found是数组is_ready是布尔。执行循环里判断通过的条件是三者同时满足is_ready true、missing_items.length 0、errors_found.length 0。少一个都不算过。3.2 执行循环改造完整循环长这样核心就一点不拦截模型正常思考只在它“以为”完成时插一道验证关卡。interface ToolCall { id: string; type: function; function: { name: string; arguments: string }; } async function runAgentWithSentinel(userQuery: string) { const messages [ { role: system as const, content: 你是任务执行助手。完成所有工具调用后、给出最终答案前必须调用 verify_result 检查一遍。 规则 - 不要连续调用同一个工具超过 2 次 - 如果 verify_result 返回 is_readyfalse继续修正然后再次调用 verify_result - 最多进行 3 轮验证 }, { role: user as const, content: userQuery } ]; const allTools [searchTool, calculateTool, formatTool, verifyResultTool]; let stepCount 0; const maxSteps 20; while (stepCount maxSteps) { stepCount; const response await callLLM(messages, allTools); const choice response.choices[0]; if (!choice.message.tool_calls) { return choice.message.content; } const toolCalls choice.message.tool_calls; for (const call of toolCalls) { const args JSON.parse(call.function.arguments); const result await executeTool(call.function.name, args); messages.push({ role: tool as const, tool_call_id: call.id, content: JSON.stringify(result) }); } const sentinelCall toolCalls.find(c c.function.name verify_result); if (sentinelCall) { const args JSON.parse(sentinelCall.function.arguments); if (args.is_ready true args.missing_items.length 0 args.errors_found.length 0) { messages.push({ role: user as const, content: 验证已通过请给出最终答案。 }); continue; } if (stepCount maxSteps - 2) { return [验证未通过但已耗尽步数] 遗漏${args.missing_items.join(, )}错误${args.errors_found.join(, )}; } messages.push({ role: user as const, content: 验证发现问题请修正遗漏 ${args.missing_items.join(、)}错误 ${args.errors_found.join(、)}。修正后再次调用 verify_result。 }); } } return 步数耗尽任务未完成; }callLLM里走 TaoToken 通道请求体大致这样async function callLLM(messages: any[], tools: any[]) { const res await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY} }, body: JSON.stringify({ model: claude-sonnet-4-20250514, messages, tools, tool_choice: auto }) }); return res.json(); }模型名按你实际用的填TaoToken 的好处是换模型只改这一行。3.3 settings.json 片段如果你用 ClaudeCode 或类似工具链settings.json 里可以这样配{ apiBaseUrl: https://taotoken.net/api, apiKeyEnv: TAOTOKEN_API_KEY, defaultModel: claude-sonnet-4-20250514, agent: { maxSteps: 20, sentinelTool: verify_result, maxVerifyRounds: 3 } }maxVerifyRounds控制最多验证几轮超过就强制返回当前状态避免死循环。4. 验证请求与一次揪出 3 个错的实测4.1 触发场景我构造了一个查询聚合任务“查一下北京和上海今天的天气顺便告诉我紫外线指数最后把三个数据整理成表格。”这个任务有三个子项北京天气、上海天气、紫外线指数外加一个格式化要求。不加哨兵时Agent 调了天气 API 拿到北京 32°C、上海 28°C然后直接输出“北京 32°C上海 28°C”紫外线指数和表格全丢了。漏检率在这个场景下就是 31% 那一档。加上哨兵后执行循环走到模型准备输出最终答案时verify_result被触发。模型填写的参数大致是{ task_goal: 查北京上海天气紫外线指数整理成表格, actual_result: 已获取北京32°C、上海28°C, missing_items: [紫外线指数未查询, 未整理成表格], errors_found: [上海温度单位未标注], is_ready: false }一次揪出 3 个问题两个遗漏项加一个错误项。执行循环检测到is_readyfalse自动注入修正指令模型继续调用紫外线 API补上数据重新格式化再次调用verify_result这次is_readytrue才输出最终答案。4.2 成功结果最终输出是完整的表格三个数据齐全单位标注清楚。整个过程额外延迟 0.4 秒token 增加约 120 个。换算成调用费用大概 0.0003 美元花不到一厘钱换 27% 的漏检减少。不同任务类型的收益对比任务类型无哨兵漏检率有哨兵漏检率额外延迟额外 token查询聚合31%4%0.4s~120单步查询8%2%0.3s~90纯计算2%1%0.4s~110查询聚合类最划算纯计算类收益低可以按需开关。5. 本篇常见错排查5.1 哨兵工具放在 tools 数组末尾导致调用率暴跌我测过放在末尾时调用率 71%放在前四位任意位置时 87% 到 91%。怀疑跟注意力机制有关模型对列表开头和中间的关注度高于末尾。把verifyResultTool放在allTools数组的前四位别放最后。5.2 system prompt 验证规则写太长原来写了 8 条验证细则调用率反而降到 52%。砍到 3 条后回涨到 89%。规则越多模型越装看不见。精炼到没法再省略的那几条才有约束力。5.3 模型过度谨慎报告不存在的遗漏遇到过模型查完所有数据后突然说“可能漏了历史趋势分析”然后强行再查一轮用户根本没问这个。在 system prompt 里加一句“只验证用户明确要求的内容不要自行扩展范围”能缓解大半。5.4 is_ready 判断条件写错必须三个条件同时满足is_ready true、missing_items.length 0、errors_found.length 0。只判断is_ready会漏掉模型填了true但数组非空的情况这种自相矛盾的输出在实际调用里出现过。5.5 步数耗尽没有兜底返回maxSteps到了还没通过验证要返回当前状态和未完成项不能直接抛错。上面代码里stepCount maxSteps - 2那个分支就是兜底把missing_items和errors_found拼进返回信息方便排查。5.6 API 通道配置错误如果callLLM报 401 或 404先检查TAOTOKEN_BASE_URL是不是https://taotoken.net/api不要多加/v1之外的路径。Key 是否从 https://taotoken.net/api-keys 正确复制环境变量有没有生效。模型对话可以先在 https://taotoken.net/model-chat 验证 Key 通不通接入细节看 https://taotoken.net/doc。6. 接入建议与后续调试哨兵机制不是银弹。如果模型对任务本身的理解就是错的比如用户问“2023 年 Q3 数据”模型理解成“2024 年 Q3”那它检查一百遍也查不出——因为从一开始的“任务目标”就是歪的。这种根本性理解偏差哨兵拦不住需要在 system prompt 或任务拆解阶段解决。部署这套机制我大概花了两个小时。第一版更复杂想给每个工具配一个专门验证工具结果工具列表膨胀到三倍调用率直接崩盘。后来收敛成一个通用哨兵效果反而最好。做减法比做加法难得忍住“覆盖所有可能性”的冲动。如果你要长期跑 Agent 类项目建议把 Key 和额度规划好Coding Planhttps://taotoken.net/coding-plan对高频工具调用场景更合适。API Keys 轮换在 https://taotoken.net/api-keys接入文档在 https://taotoken.net/doc模型调试用 https://taotoken.net/model-chat。ClaudeCode 相关配置参考 https://taotoken.net/claudecode-anthropic。先把哨兵工具加进你的执行循环跑一组查询聚合任务对比漏检率。如果调用率上不去先检查工具位置和 system prompt 长度这两个是最常见的坑。