上个月我收到一个挺头疼的反馈:给某个内部项目配的 Agent 又在生产环境里乱来。任务很简单,让它在几十个文件里找出需要修改的位置,它翻了半天之后忽然开始自由发挥,把完全无关的文件也动了一遍。这种事我见得太多。说白了,Agent 不可靠,问题往往不在模型本身,而在于我们没有给它一套能查、能试、能验证的基础设施。所以我去做了个给 Agent 用的网站,把能力拆成一个个带文档、带校验、带回放验证的 Skill 包。这篇文章就把我踩过的坑、总结的思路和最终落地的结构完整梳理一遍。如果你也在做 Agent 落地,或者正被"模型老是乱来"搞得焦头烂额,应该能从中找到几条可以直接抄的方案。
1. Agent 不可靠,问题通常不在模型本身
1.1 我复现过的最典型的失败链路
先说说我观察到的现象。很多人以为 Agent 不够可靠,是模型太笨、推理能力不行。但我把失败任务一条一条拉出来看之后发现,绝大多数翻车根本轮不到推理那一层,而是在更早的地方就出问题了。
最常见的链路是这样的:模型拿到一个任务 → 它决定调用某个工具 → 工具的参数没填对 → 工具返回了一个错误 → 模型看了一眼错误,开始自行"脑补"修复方案 → 越补越歪。比如它需要调用一个文件搜索接口,却把搜索路径写成了文档里根本不存在的目录;或者它需要按特定格式传一个时间戳,却自作主张传了个自然语言日期。这些错误不是模型不会推理,而是它根本不知道正确的约定长什么样。
更隐蔽的是"静默失败"。工具其实跑成功了,但返回的数据量特别大,模型在上下文窗口里翻来翻去找不到自己需要的那一小段,于是干脆按自己的理解给用户编了一个答案。这种失败最难受,因为从日志上看一切都是通的,只有最终结果对不上。你能感受到那种无力感:每一步都执行了,最后交付的东西却不该是这样。
1.2 用三个指标定义"可靠"
既然要和可靠性作斗争,第一步就得先把"可靠"这件事量化,不然改来改去都说不清有没有变好。我给项目定了三个硬指标,后面所有设计都围绕它们展开。
第一个是任务成功率。指一个端到端任务里,Agent 能在不产生不可恢复错误的前提下把核心目标完成,最终产物通过自动校验的比例。第二个是单任务平均工具调用次数。这个参数很有意思,它衡量的是 Agent 的"摸索成本"。成功率差不多的两个版本,调用次数越少,说明它越清楚该怎么做,也就越不容易在中途失控。第三个是可回溯性。我给它的定义是:任何一个失败任务,能否在一分钟内定位到具体是哪一步调用出了问题、传错了什么参数。
这三个指标定下来之后,很多争论就变成了一件可以量化的事。比如有人说"它好像在瞎试",我不跟他吵,直接拉调用次数出来看;有人说"结果还行吧,偶尔不对也正常",我也不跟他辩,直接把成功率曲线拍在桌上。做 Agent 工程最怕感觉好但指标差,所以先有尺子,再谈优化。
2. 这个网站长什么样:给 Agent 看的不是给用户看的
2.1 为什么是网站而不是工具集合
确定了目标之后,下一个问题是:把能力沉淀成什么形态?一开始我确实犹豫过,是不是做一个普通的功能包或者 API 聚合网关就够了。但后来想明白一件事——Agent 需要的不只是"能调用的函数",它更需要"能查阅的说明书"。
一个函数签名只能告诉模型"这个工具有什么参数",而不能告诉它"这个工具在什么场景下该用、用了之后会有什么副作用、遇到什么报错应该怎么处理"。而这些恰恰是 Agent 做决策时真正需要的信息。我需要的不是一个藏着函数定义的黑盒子,而是一个 Agent 可以自己浏览、自己查阅、自己确认的"操作手册"。
所以最后做成了网站的形式。这个网站没有花哨的界面,不需要注册,不需要登录,它更像一个极简的文档服务加校验服务综合体。Agent 在开始干活前会先去站点上找到对应的 Skill 页面,读完操作说明,再照着说明里的步骤实际调用。相当于给 Agent 配了一套可以随取随用的"标准作业流程书",而不是让它蒙着猜。
2.2 三类组件:Skill 注册中心、运行时校验、回放日志
整个网站拆成三个功能块,各管一摊,后面维护起来很有条理。
第一块是 Skill 注册中心。它维护一份技能包清单,包含每个技能的名字、功能简介、适用场景、依赖关系。Agent 面对一个任务时,可以通过访问一个索引页来快速判断"这件事我应该用哪个技能包"。这个索引页不能写得太复杂,要让模型一眼扫过去就能匹配上下文。
第二块是运行时校验服务。Agent 调用具体操作时,请求会先经过一个统一的校验层。参数有没有缺失、类型对不对、枚举值是否合法,都在这里被挡住。挡住之后返回的错误信息也是结构化的 JSON,而不是一行让人看了更懵的日志。这个设计直接干掉了前面说的大部分"静默失败"。
第三块是回放日志。站点会记录每一次 Agent 访问了哪个 Skill、调用了哪些操作、每一步花了多久、返回了什么结果。这些日志平时看起来没什么用,但一旦线上出了个诡异任务,它就是最好的侦探工具。我可以按任务 ID 把整个调用链拉出来,像看录像一样看它到底是怎么一步步走到翻车那一步的。
2.3 Skill 目录协议:文件夹即技能包
Skill 的组织方式我也改了不知多少版,最后定下来的结构非常朴素:每一个技能就是网站上的一个文件夹,文件夹里用固定协议装几类东西。
默认入口叫 SKILL.md,是整个技能的说明书。它告诉 Agent 这个技能是干什么的、什么时候该用、操作步骤是什么、有什么限制。SKILL.md 旁边会有一个 schemas 目录,存放所有操作请求和响应的 JSON Schema 定义。再往下是 actions 目录,放真正的可执行操作描述,包括每个动作的路径、方法、必要的 header 或 body。最后还有一个 examples 目录,放几组典型的调用样例,每个样例都标注了"输入 + 期望输出 + 常见错误"。
听起来很像文档站加 API 网关的合体,对吧?对,但关键的一点是,这一切都是按照模型最容易理解的方式来组织的。比如文件夹命名、描述文字、样例选择,都要站在"给一个只能读文本的东西看"的角度来思考。这跟给人写文档很不一样,后面我会专门展开说这块的差异。
3. 让 Skill 真正"照着做":文档、Schema 与幂等控制
3.1 SKILL.md 的写法:给出正例、反例和终止条件
很多人写技能文档时旁边都是给程序员看的思维,恨不得把每个角落都解释清楚。但我踩过坑之后想明白了:给 Agent 看的文档不是越全越好,而是要刻意控制信息量和信息结构。
我在 SKILL.md 里只保留四块内容。第一块是触发条件,用两三句话说明这个技能适合处理什么问题,不适合处理什么问题。第二块是执行步骤,按顺序编号,每一条尽量简单直接,不要在一条里塞太多复合动作。第三块是正例和反例。举一个完全正确的用法,再举一个看起来很合理但实际会出错的用法,两者都配上一句话解释为什么。第四块是终止条件,也就是"做到什么程度算完成,什么情况下应该主动停手或者上报"。
这里面的反例和终止条件是最容易被人忽略的,但恰恰是最有价值的。模型在面对模糊边界时,如果没有任何指引,它倾向于自己脑补一个"差不多"的答案。你提前把那个糟糕的"差不多"的样子摆出来,再加上"如果遇到这个情况就停下来"的硬性约定,它跑偏的概率会小非常多。我见过很多团队把文档写得很长很漂亮,但忘了给终止条件,结果 Agent 在一个错误路径上越走越远,最后把整个项目目录都改烂了。
3.2 输出强制走 JSON Schema,禁止自由发挥
文档是软约束,Schema 是硬约束。在我的站点里,所有操作请求都要过一层强制校验,这不是靠提示词请求模型"请规范输出",而是直接在网关层拦截。
每个操作在 schemas 目录里都有一份 JSON Schema,里面定义了参数名、参数类型、是否必填、取值范围、嵌套结构。Agent 发过来的请求如果不符合这个 Schema,会收到一个 400 响应,响应体里明确写出"哪个字段缺失、期望的类型是什么、你实际传了什么"。我在实测中发现,模型在收到这种结构化错误提示之后,第二次修正成功的概率非常高。因为不是它不会改,而是它之前可能并不清楚自己错在哪。
响应体同样要过 Schema。我以前吃过不少这方面的亏:工具明明正常执行了,但返回的 JSON 里某个字段嵌套层级不对,模型读完之后完全理解歪了。现在所有响应在发回给模型之前都会做一次标准化重排,确保字段名、类型、层级完全符合约定。这样模型每次拿到手的都是同一种格式,它对结果的解读就稳定多了。
3.3 幂等设计与可恢复检查点
Agent 和人的一个很大区别是,它会重复调用同一个操作,而且自己不一定知道之前调过。如果某个操作带有副作用,重复执行就会产生灾难。所以我在所有写操作上都加了幂等控制。
谁都能做的一个方案是给每个任务生成一个全局唯一的 requestId,携带在请求头上。站点收到带相同 requestId 的重复请求时,直接返回第一次请求的结果,不再重复执行。这个思路实现起来不难,但收益很直接,它把"模型重复点了两次提交"这种看起来很蠢但真实存在的问题彻底消灭了。
另一个配套设计是可恢复检查点。对多步骤任务,我会把中间阶段的产出物缓存到一个按任务 ID 隔离的临时空间里。如果 Agent 在第三步挂了,恢复执行时不需要从头跑一遍,直接读取临时空间里的检查点继续即可。这个机制极大地缩短了任务收敛时间,也让 Agent 愿意去做更长链条的事——反正失败了能从检查点接着来,而不是担心自己白干了一场。
4. 可靠性不是靠提示词,是靠"验证闭环"
4.1 每个 Skill 配一组黄金样例
网站上线之初我就在做一件事:给每个 Skill 配黄金样例。所谓黄金样例,就是一组已经被人工确认过"完全正确"的输入输出对。它们不只是用来给模型做提示参考,更重要的用途是作为回归测试的基准。
每个 Skill 上线前,我会整理大约 5 到 10 组这样的样例。覆盖的场景要有区分度:一组是很常规的用法,一组是边界参数,一组是故意容易写错的参数组合,还有一组模拟模型常见的失败模式。比如一个文件操作 Skill,我会考虑"文件不存在时应该返回什么""路径带特殊字符时应该怎么处理""目标文件是只读时接口给什么提示"等等。
这些黄金样例的价值在平时不显眼,但当你改了一版 Skill 逻辑、换了一个底层模型版本、或者调整了某个 Schema 之后,它的作用就会凸显出来——你不需要拍脑袋判断"这次有没有变好",直接跑一遍样例对比就行了。我甚至会把样例回放做成了站点上的一个专门页面,每次改动后都会跑,跑完显示通过率。通过率低于阈值的改动不允许合并,这是硬性规定。
4.2 沙箱回放与自动回归
有黄金样例还不够,还得有能自动执行它的环境。我的做法是在一个隔离的测试环境里搭建了一套沙箱回放服务:它拿真实的历史调用记录或者黄金样例作为输入,再把当前版本的 Skill 逻辑整个跑一遍,对比输出结果和期望结果。
这个环节里我比较看重的是"调用链对比"而不只是"最终结果对比"。因为一个任务哪怕最终输出是对的,中间可能绕了弯路、产生了大量无效调用。这些弯路的危害在单个任务上不明显,但当任务量大了之后,Act 的调用次数、token 消耗、响应延迟都会恶化。所以回放对比会很严格地检查每一步的调用是否合理。
还有一层是自动回归。它在每次网站内容更新、Schema 修改、新增技能时都会被触发。跑完之后的报告会自动发到项目群里,里面只留两个信息:通过案例数量和失败案例清单。失败案例不是甩一堆日志,而是把"期望值、实际值、可能原因"三行贴出来。这样一来,任何一次修改的影响范围都变得非常清晰,而不是等线上出事了才去翻记录。
4.3 质量门禁:发布前必须跑过的五项检查
后来我干脆把这套流程固化成了发布前的质量门禁,一共五项检查,缺一不可。
第一项是 Schema 完备性检查,确认所有操作都有对应的请求与响应 Schema,没有漏网之鱼。第二项是黄金样例回放,全部通过才准进下一步。第三项是新技能烟雾测试,用一个最小化的任务去跑一遍,确认它能完整走通,且没有异常报错。第四项是模型兼容性抽查,用项目里实际在用的模型版本跑几个典型任务,观察输出质量有没有明显退化。第五项是延迟与成本预估,对比改动前后的平均调用次数和 token 开销,防止某种"看起来很聪明但实际很昂贵"的实现混进来。
这五项检查看似繁琐,但实际跑一遍的时间不长。它们保证的不是"这个功能特别惊艳",而是"这个功能稳定可靠、可预期、可回溯"。对我来说,Agent 工程的可靠性从来不是靠某次灵光一闪的提示词优化,而是靠这种反复强制校验的笨功夫堆出来的。
5. 一个真实任务从翻车到稳定复现的过程
5.1 上线之前:成功率惨不忍睹
理论说再多,不如看一个真实任务的前后对比。这个任务我挑了最典型的一个:让 Agent 自动把一批散落在多个目录里的素材文件统一整理为结构化清单,并为每个文件生成一段摘要。听起来很简单,对吧?但在没有这套网站的时期,它简直是"翻车重灾区"。
当时的实现方式是:给 Agent 一道比较长的 Prompt,把整理规则、文件名格式、摘要字数、输出样式全部写进去,然后让它自由调用文件读取工具。结果我统计了一下十多次试运行的表现:有 4 次输出的 JSON 结构是坏的,数组嵌套错位、字符串多了多余引号之类的问题层出不穷;有 2 次漏掉了文件,它读完一个目录就以为全部搞定了;还有 2 次是在固定路径上反复试错,明明看一眼目录结构就能确定的事情,它非要挨个尝试所有可能的路径。
最后成功的次数大概只有 4 成左右。而且更让人头疼的是,每一次失败的原因都各不相同,根本没有规律可循。当时的我只能对着结果叹气,完全找不到一个统一的修法。这也让我下决心:不能再靠提示词这种"软办法",必须上硬约束。
5.2 接入网站与 Skill 之后的变化
后来我把这个任务重新封装成了一个 Skill:文件整理与摘要生成。网站上给它建了完整的资料页,SKILL.md 里写清楚步骤和终止条件,schemas 里定义了清单的字段结构,examples 里放了两组完整样例。
Agent 接到任务时,第一步不是直接动手,而是访问 Skill 索引页,找到"文件整理与摘要生成",再打开 SKILL.md 读一遍。然后它按照文档里的步骤:先列出目录结构,再逐个目录读取文件元数据,接着按规则生成摘要,最后把结果通过一个统一的校验接口提交。提交之后,站点会校验清单结构是否合法,如果不合法就返回具体的字段错误。
这个版本我连着测了一个月,结果非常直观:成功率从原来的 4 成左右提到了 95% 以上。剩下那 5% 的失败也不是乱来,而是集中在某些极端情况,比如文件编码损坏、目录权限异常。这些失败在回溯日志里都有清晰的调用链记录,我能在一分钟之内定位到根因。
5.3 两周的数据对比
我把上线前后的两周数据拉了一张表,整理完之后自己都挺意外,变化是全方位的。
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 任务成功率 | 约 40% | 95% 以上 |
| 单任务平均工具调用次数 | 18 次 | 6 次 |
| 失败任务平均定位耗时 | 约 30 分钟 | 1 到 3 分钟 |
| 输出结构坏掉的比例 | 25% 左右 | 0%(被 Schema 拦截) |
| 重复操作发生率 | 高,经常重复写文件 | 极低(幂等拦截) |
调用次数从 18 次降到 6 次,这个数据我特别看重。它说明 Agent 在动手之前真的先读了操作手册,而不是像以前那样对着一个工具反复试探。token 成本也随之降了一大截,这在长期运行的项目里是可观的数字。更重要的是,整个系统从"黑箱"变成了"白箱":每个任务为什么成功、为什么失败,都可以从日志里还原出完整过程。
6. 踩过的坑和改了很多版的经验
6.1 文档写得越长,Agent 就越容易抓不住重点
第一个坑是我自己挖的。做第一版 Skill 的时候,我特别想把它写全面,背景、原理、边界情况、历史变更全塞进去,SKILL.md 写了几千字。结果发现 Agent 的表现并没有变好,反而变差了。它像是在一篇长文里迷了路,抓不住核心步骤,经常被文档里某个冷门注意事项带跑偏。
后来我做了个很极端的压缩:每篇 SKILL.md 正文保持在几百字以内,只保留步骤、正反例、终止条件三件事。其他背景信息一律挪到站点上的独立页面,并且明确标注"不是必读内容"。实测下来,这个改动直接让任务成功率涨了一截。给 Agent 的信息设计,本质上是"宁可少给,给到点上"。
6.2 让模型自己猜参数,是一切奇葩报错的根源
这个坑潜伏了很久。有一版 Skill 的某个参数需要传一个特定的枚举值,但我只在文档里用了一句自然语言描述"请传天数对应的英文缩写",没有把它定义成枚举类型。结果 Agent 每跑一次就换一种写法,今天传 MONDAY,明天传 Mon,后天传 Monday。解析方没做严格校验,于是各种诡异报错都冒出来了。
解决方式也很粗暴:把参数改成强枚举类型,Schema 里限定死合法值,非法值直接拒绝并给出可用选项列表。这一招以后,相关报错笔数直接清零。我后来形成的原则是:所有参数都要尽量往下拉明确程度,能用枚举就不用自由字符串,能用结构体就不要让模型自己拼字符串。模型自由发挥的空间越小,系统的可靠性就越高。
6.3 版本号、缓存和变更日志必须一起做
网站上线稳定了之后,新的烦恼出现了:Agent 访问 Skill 页面会走各类缓存,文档更新之后,有时候 Agent 用的还是旧缓存里的版本,导致行为和新 Schema 对不上。
这个问题的解决方案其实很常规:给每个 Skill 包加版本号,并在页面上显示"最后更新日期"。每次内容变更时同步更新版本号,同时把站点上的缓存策略调整为按版本号做哈希隔离。更关键的是我在每个 Skill 页面的顶部加了"变更提示"区:如果这个版本和上一个版本在操作步骤或参数上有所变化,会明确写出来。这样 Agent 读到新版本时也能意识到"这个行为规则变了"。
6.4 Skill 多了之后,要做上下文排名
最后还有一个属于"规模变大后才遇到"的坑:当网站上的 Skill 超过几十个之后,Agent 面对索引页时开始选择困难。它经常翻完整个列表之后还是选错了技能,或者选了一个名字相似但场景不对的,然后在错误的调用链里浪费大量 token。
我后来给索引页加了一道轻量排序,不涉及什么玄学算法,就是把关键词匹配结果、使用频率统计、最近更新时间这几个因素做一个简单的加权。最常用的 Skill 排在最前,更新过的排在次前,匹配度低的往后排。就这样一个小改动,技能选错的概率明显下降。可见给 Agent 做的任何界面,都要以"减少它的决策负担"为最高优先级。
回头看看,这个"给 Agent 用的网站"本质上做的两件事:一是把知识外置,让 Agent 不用靠记忆和猜测去干活;二是把约束前置,在动手之前就堵住大部分出错的可能。我自己最大的体会是,所谓让 AI 更可靠,其实不是去训练模型,而是去重新设计它身边的环境。你有多少精力投入到文档、校验、回放这些看起来很朴素的环节上,就有多少收获反映在任务成功率里。如果后面有机会,我还会往多智能体协作方向扩展——让不同角色通过这个站点共享技能包和校验规则,那应该是另一个更有意思的战场。