5. How to really stop your agents from making the same mistakes
LangChain 已经融资 1.6 亿美元。三年开发,估值十亿美元。他们的测试平台 LangSmith 确实非常成熟:轨迹评估、轨迹转数据集管道、LLM 作为评判者、回归测试套件、工具的单元测试框架。他们拥有这些组件。功劳归功于他们。
但组件不是实践。
LangChain 给你测试工具,却从不告诉你该测试什么、按什么顺序测试,或者什么时候算完成。
没有一套有主见的流程,按顺序说:
- 这次失败发生了
- 现在写一个技能
- 现在写确定性代码
- 现在写单元测试
- 现在写 LLM 评估
- 现在添加解析器触发器
- 现在评估解析器
- 现在审计重复项
- 现在冒烟测试
- 现在正确归档 这个循环不存在。你必须自己从零散的原语中发明它。很多 AI 用户仍然完全不测试他们的 Agent,因为他们选择的框架只给了他们一张健身房会员卡,却没有健身计划。
大多数 AI Agent 的“可靠性”都是基于感觉的。提示词微调。更长的系统消息。“请不要幻觉”这样的咒语。这些东西一旦对话变复杂,就立刻失效。那些融资数亿美元来解决这个问题的框架,只给了你监控仪表盘和单元测试助手,然后说“祝你好运”。
我的 Agent 这周犯了两次错误。两次错误都不能再发生。不是因为我好言相劝,而是因为我把每一次失败都变成了永久的结构性修复:一个每天自动运行、永远有效的技能,并配有测试。
我把这种实践叫做“skillify”(技能化)。一旦你用了它,你的 Agent 就不会反复犯同样的错误。下面是它的工作原理。
失败 1:已经在数据库里的行程
我问我的 OpenClaw 关于十年前的一次旧商务旅行,它埋在日历历史里。简单的问题,应该一秒钟就搞定。
结果 Agent 做了这些:
- 调用实时日历 API → 被阻止(时间太久远)。
- 尝试邮件搜索 → 结果嘈杂,什么都没确定。
- 再次尝试日历 API,用不同参数 → 还是被阻止。
- 五分钟后,才搜索本地知识库,一下子就找到了。 答案其实一直就在我自己的数据里。3146 个日历文件,覆盖 2013 年到 2026 年。已经索引完毕,已经本地化。只要 grep 一下就行。
Agent 只是没先去那里找。
在我一直在写的框架(薄外壳 + 胖技能)里,有一个关键区分:需要判断的工作和需要精确性的工作。我分别叫它们“潜在的”(latent)和“确定性的”(deterministic)。日历 grep 是确定性的。输入相同,输出永远相同,不需要模型。但 Agent 却把它放进了潜在空间,启动推理、调用 API、解释结果,而一个三行脚本就能瞬间给出答案。
这就是 bug。不是答案错,而是选错了“机器”。
修复:calendar-recall(步骤 1 + 2)
在薄外壳 / 胖技能架构中,技能是一个 Markdown 过程,它教模型如何处理任务。不是告诉它做什么(用户提供“什么”),而是提供过程。把它想象成一个方法调用:同样的过程,根据你传入的内容,输出完全不同。
下面就是从这次失败中诞生的技能:
name: calendar-recall
description: “以大脑优先的历史日历查找。对于任何不在未来或过去 48 小时内的事件,始终先使用这个,再用任何实时 API。”
里面的硬性规则是:
实时日历 API 仅用于未来事件或过去 48 小时内的事件。所有历史事件都先走本地知识库。
让这一切生效的关键是:Agent 自己写了确定性脚本。技能文件(Markdown,活在潜在空间里)告诉 Agent 如何修复问题。Agent 读了技能,明白日历搜索是确定性工作,于是生成了一个脚本:
$ node scripts/calendar-recall.mjs search “Singapore”
找到了 2 个匹配日期: ── 2016-05-07 ──
飞往新加坡,入住文华东方酒店
── 2016-05-08 ──
与投资者在富尔顿酒店共进午餐
代码在 100 毫秒内运行完(大部分是 Bun 启动时间,实际 grep 是亚毫秒级)。零 LLM 调用。零网络。只有本地文件。
这就是让整个架构运转的循环:潜在空间构建确定性工具,然后确定性工具约束潜在空间。Agent 用判断力(潜在)写出了 calendar-recall.mjs。现在技能强制 Agent 运行这个脚本,而不是去推理日历数据。模型的智能创造了约束,防止模型再犯蠢。
旧的失败路径在结构上变得不可达。技能说“先搜索本地”。脚本执行搜索。Agent 再也没有机会“聪明”地出错。
失败 2:“28 分钟”(同样是步骤 1 + 2)
同一天。Agent 说:“你的下一个会议在 28 分钟后。”
现实:还有 88 分钟。Agent 在脑子里做了 UTC→PT 时区换算,差了整整一小时。
问题是,已经存在一个脚本(context-now.mjs),它输出的是:
{
“now”: “2026-04-21T07:38:12-07:00”,
“upcomingEvents”: [{
“summary”: “App Ops Sprint Planning”,
“minutesUntil”: 88
}]
}
代码大约 50 毫秒运行。零歧义。Agent 就是没运行它。
同样的模式:确定性工作(时间戳相减)却放在潜在空间完成。模型在做心算,而脚本早有答案。
修复:context-now 技能
name: context-now
description: “始终开启的纪律:在做任何时间敏感的声明之前,运行 context-now.mjs。绝不要在脑子里做 UTC→PT 转换。”
下面是有了这些简单技能前后的对比(原文此处应有图表)。
Skillify:拯救你理智的模式
两次失败,模式相同。Agent 有正确工具,却选择了“聪明”而不是纪律。错误的事情发生在错误的机器空间里。
在普通的 AI 设置中,AI 会道歉、承诺改进,两周后同一个问题以不同查询或不同时区再次发生。Agent 对 bug 没有记忆,没有针对 bug 的测试,什么都阻止不了它复发。
Skillify 就是解药。每一次失败都变成一个技能。每个技能都有测试。bug 在结构上变得不可能重复。
当一个失败被提升为技能时,我用的 10 项检查清单是:
□ 1. SKILL.md —— 合约(名称、触发器、规则)
□ 2. 确定性代码 —— scripts/*.mjs(LLM 不做代码能做的事)
□ 3. 单元测试 —— vitest
□ 4. 集成测试 —— 真实端点
□ 5. LLM 评估 —— 质量 + 正确性
□ 6. 解析器触发器 —— AGENTS.md 中的条目
□ 7. 解析器评估 —— 验证触发器是否真的路由
□ 8. 可解析性检查 + DRY 审计
□ 9. E2E 冒烟测试
□ 10. 大脑归档规则
一个功能如果通不过全部十项,就不是技能。只是今天碰巧能用的代码。
上面两个失败已经走完了步骤 1 和 2:写 SKILL.md(合约),然后写确定性代码(Agent 自己构建并使用的脚本)。但在讲解剩下的八步之前,我想先展示 skillify 在日常使用中的样子,因为它不只是对失败的回应,它已经成为动词。
Skillify 作为动词
对我来说,构建 OpenClaw(和 GBrain)时,这个检查清单最初是失败响应协议,后来变成了我构建一切的方式。
我的实际工作流是这样的:我用自然语言和 Agent 对话。我们一起在对话中构建东西。我试用它。它成功了。然后我说一个词:
Garry: “hot damn it worked. can you remember this as a webhook skill and skillify it, next time we need to do some webhooks? why was this so hard to get right? anyway it’s good now. DRY it up too”
那是一个 OAuth webhook 集成。我们花了一个小时才让它工作。然后“skillify it”就把这次临时会话变成了带测试、解析器条目和文档的持久技能。下次我需要 webhook,技能就存在了。Agent 会读它。那一个小时的来之不易的知识就永久保存了。
再比如,我们发现容器在某些任务中需要 headless 浏览器,而我的桌面需要 headed 浏览器:
Garry: “great! so we should actually remember this as a skill whenever anything in openclaw needs a headless browser! and also know that if we need a headed browser we should ask the user to run gstack browser and give us a pair-agent code. skillify it!”
一句话。Agent 就写出了 skills/browser/SKILL.md,里面有决策树、确定性脚本和测试。现在每个未来需要浏览器的会话都会自动路由到正确的工具。
或者,我注意到 Agent 经常发 ngrok 链接却不检查它们是否真的可用:
Garry: “can we make a skill that says whenever you send me a link you have to curl it yourself to make sure the endpoint is open and the tunnel works? skillify it!”
又或者,差点让我错过会议的日历双重预订:
Garry: “Here is one regular skill I need you to write. It’s the calendar check skill. Tomorrow I have a double booked 11am. Make a skill, make it deterministic to check these kinds of things.”
一句话。代码、技能、测试、解析器条目、可达性审计。整个 10 步检查清单一口气完成。我的 OpenClaw 知道、执行,现在它已经成为习惯。我已经这么做过几十次了,离不开它。
模式永远一样:在对话中原型化,看到它工作,说“skillify”,原型就变成了永久的基础设施。我不用写规格说明,不用建工单。我和 Agent 一起解决问题,然后解决方案就变成 Agent 可以永远使用的技能,不需要我再干预。
这就是 1.6 亿美元框架融资所错过的。不是测试原语。不是评估工具。而是工作流。是人类说“这个成功了,现在让它永久化”的那一刻,而系统精确知道“永久化”意味着什么:SKILL.md、确定性代码、单元测试、集成测试、LLM 评估、解析器触发器、解析器评估、DRY 审计、冒烟测试、大脑归档。十步。一个词。
下面是剩下的八步在实践中的样子。
步骤 3:单元测试
经典 vitest。确定性函数,确定性断言。calendar-recall.mjs 导出了纯函数如 parseEventLine、eventMatchesKeyword 等。每个函数都用 fixture 数据测试:临时目录里的合成日历文件,已知输入、已知输出。
这些测试能抓住的 bug 类型:parseEventLine 默默丢掉位置字段含 Unicode 的活动;dateFromPath 对闰年日期返回 null;formatJson 在只有一个人时省略 attendees 数组。小问题、无聊,但关键。如果脚本输出错,技能就输出错,Agent 就会自信地告诉我错误答案。
对于 context-now,单元测试验证时区格式、安静时段检测,以及跨夏令时边界的 minutesUntil 计算。其中一个测试输入夏令时转换前 3 分钟的时间,验证输出不会跳 60 分钟。这正是导致“28 分钟”失败的 bug。现在它在结构上不可能了。
我有 5 个套件共 179 个单元测试,运行不到 2 秒。
步骤 4:集成测试
这些测试会命中真实端点和真实数据。calendar-recall.mjs 是否真的能在真实 brain 仓库里找到事件,而不只是测试 fixture?context-now.mjs 在日历缓存过期或缺失时是否输出有效 JSON?集成测试能抓住单元测试漏掉的 bug,因为 fixture 数据太干净。真实数据有格式错误的活动行、缺失时区字段、Windows 换行符、跨午夜的活动。
规则是:如果你发现自己手动检查脚本在真实数据上是否正确,那就应该把这个检查写成集成测试。
步骤 5:LLM 评估
这里开始有趣了。有些输出需要判断力来评估。“这个日历摘要有用吗?”不是脚本能回答的是/否问题。所以我用 LLM-as-judge:一个模型根据评分标准评估另一个模型的输出。
context-now 每天运行 35 个评估。其中一个给 Agent 发消息如“嘿,我的航班大约 45 分钟后起飞,我能赶上 SFO 吗?”,检查 Agent 是否先运行 context-now.mjs 再回答,还是试图自己心算。如果 Agent 上钩自己算时间,评估就失败。
另一个评估给 Agent 一个 UTC 时间戳,问“对我来说这是几点?”正确行为是运行脚本并引用结果。错误行为是自己转换。评估既抓住错误答案,也抓住错误过程——即使这次心算碰巧正确,下次也会错。
我发现最诚实的评估启发是:搜索对话历史里你说过“fucking shit”或“wtf”的地方。那些就是你缺失的测试用例。
步骤 6:解析器触发器
解析器是上下文的路由表:当出现任务类型 X 时,加载技能 Y。我之前详细写过解析器。每个技能都需要在 AGENTS.md 里有一个触发器条目,教 Agent 技能存在以及何时使用。
解析器触发器只是 Markdown 表格里的一行。
这一步抓住的 bug 是:你写了新技能,却忘了加到解析器里。技能存在,能力存在,但系统无法触达它。就像医院里有外科医生,却没把他列入目录。比没有技能还糟,因为你以为系统能处理。
步骤 7:解析器评估
这是大多数人完全忽略的一层。解析器触发器说“这个短语应该路由到这个技能”。解析器评估则测试它是否真的做到了。
我的解析器评估套件有 50+ 个测试用例,例如: { intent: ‘check my signatures’, expectedSkill: ‘executive-assistant’ }
{ intent: ‘what time is my meeting’, expectedSkill: ‘context-now’ }
两种失败模式:假阴性(技能应该触发却没触发,因为触发描述错误或缺失);假阳性(错误技能触发,因为两个触发器重叠)。解析器评估在用户碰到问题前就抓住歧义。
我同时运行确定性结构测试(AGENTS.md 表格是否包含正确映射?)和 LLM 路由测试(给定这个意图,模型是否真的选对技能?)。两层都很重要。表格可能正确,但模型仍可能因为触发描述模糊而路由错误。
步骤 8:可解析性检查 + DRY 审计
建了一个月后,我有了 40+ 个技能。有些是针对特定事件创建的,有些是子 Agent 运行 cron 自动生成的。没人维护解析器表格。技能不断诞生,却没人注册。
于是我写了 check-resolvable。一个元测试,遍历整个链条:AGENTS.md 解析器 → SKILL.md → script/cron。如果一个脚本做了有用的事,却没有从解析器出发的路径,它就是不可达的。LLM 永远不会知道要用它。
第一次运行就在 40+ 个技能中发现了 6 个不可达的。系统能力的 15% 是暗的。
一个航班追踪器,没人能通过问航班来调用。
一个内容想法生成器,只在 cron 运行,却无法手动触发。
一个引用修复器,存在于 skills 目录,却根本没列在解析器里。
一小时就修好了。只要把触发条目加到 AGENTS.md。现在 check-resolvable 每周作为 gbrain doctor 的一部分运行。它检查三件事:
- 每个有 SKILL.md 的技能目录都在解析器里有对应条目。
- 每个技能引用的脚本都真实可调用(文件存在,导出正确函数)。
- 没有任何两个技能的触发描述重叠导致路由歧义。 DRY 审计同时运行。如果你不小心,就会出现十五个功能差不多相同的技能,解析器随机挑一个。对 calendar-recall 来说:
(原文此处应有矩阵图)
四个技能同领域,零重叠。每个都有自己的赛道。这个矩阵不是为这篇帖子的图,它就活在 SKILL.md 里,审计脚本会解析它。建第六个日历技能踩到别人赛道,审计就会在技能上线前失败。
步骤 9:E2E 冒烟测试
完整的端到端管道。
问 Agent “我什么时候去的新加坡?”,验证它是否运行 calendar-recall.mjs、得到正确答案并正确格式化。
问“我的下一个会议是什么时候?”,验证它是否运行 context-now.mjs 而不是心算。
冒烟测试是最后一道防线。其他所有测试都能通过,但如果各部分没连起来,系统仍可能失败。技能正确、脚本正确、解析器正确,Agent 仍可能忽略一切自己发挥。冒烟测试就抓住这一点。
步骤 10:大脑归档规则
每个向知识库写入的技能都需要知道东西该放哪里。人放 people/,公司放 companies/,政策分析放 civic/。我曾经发现 13 个写大脑的技能里有 10 个放错了目录,因为它们各自硬编码路径,而不是咨询解析器。
归档规则文档记录了常见的错放模式。来源 vs 原件。人 vs 公司(当某人本身就是公司时)。技能在创建任何页面前都会先读这些规则。从此零错放。
GBrain:Skillify 所在的地方,你应该从我的 GBrain Skill Pack 采用它
Skillify 模式不限于 OpenClaw 或任何特定外壳。它内置于 GBrain。GBrain 是我写的开源知识引擎,位于你使用的任何外壳之下。它管理你的 brain 仓库、运行评估,并强制执行让技能持久的质量关卡。
GBrain SkillPack 是一个可移植的技能包,包含技能、解析器触发器、确定性脚本和测试。你只要让 OpenClaw/Hermes Agent 安装它,就能导入到任何 Agent 设置。它就是我为 OpenClaw/Hermes Agent 写的技能和能力自动添加到你的 OpenClaw 的方式——包括整个 10 步 skillify 输出,打包好直接可用。
前面的 skillify 检查清单不是建议。它就是 gbrain doctor 实际检查的内容。
gbrain doctor –fix 会自动修复 DRY 违规,用约定引用替换重复块,所有操作都由 git 工作树检查保护,不会破坏任何东西。
为什么 Hermes Agent 本身不够
Nous Research 的 Hermes Agent 做了一件真正伟大的事:它有一个 skill_manage 工具,让 Agent 自己根据学到的东西创建、修补和删除技能。当 Agent 完成复杂任务或从错误中恢复时,它会提出技能并写入磁盘。这是 Agent 自己赚到的程序性记忆。渐进式披露(先加载技能索引,只在选中时拉取完整 SKILL.md)。有界内存(MEMORY.md 限制 2200 字符)。条件激活(所需工具不可用时技能自动隐藏)。设计很聪明。
但 Hermes 不测试它的技能。没有确定性代码的单元测试。没有解析器评估来验证路由。没有 check-resolvable 来找暗技能。没有 DRY 审计来抓重复。没有日常健康检查在东西漂移时变红。
我在任何未测试的技能系统中观察到的失败模式:
- Agent 周一创建 deploy-k8s,周四又从不同对话创建 kubernetes-deploy。两个都存在。两个触发类似短语。路由歧义,直到错误的一个在错误时间触发才被发现。
- 技能写的时候完美运行。六周后上游 API 结构改变。技能默默返回垃圾,直到人类发现。
- 自主创建的技能触发器太弱,从不匹配。它变成孤儿,占用索引 token,永远不运行,慢慢腐烂。 这是“没有测试,任何代码库都会腐烂”这个软件工程在 2005 年就解决的问题。Agent 技能没什么不同。Hermes 完美处理创建。GBrain 处理验证。你需要两者。
大想法
在健康的软件工程团队里,每个 bug 都会得到一个测试。那个测试永远存在。bug 在结构上变得不可能复发。AI Agent 也应该这样。
每一次失败都变成一个技能。每个技能都有评估。每个评估每天运行。Agent 的判断力永久提升,不只是当前会话,不只是上下文窗口还记得的时候。
那次行程失败不会再发生。时区失败不会再发生。当下一次失败出现时(它一定会出现,因为这是一场对抗熵和品味的对抗赛),它也会被 skillify。
我一年后使用的 Agent,将由它过去一年犯的每一个错误塑造。那不是锦上添花。那就是整个论点。
去“煮海”。让你的 Agent 做点什么,然后 skillify 它。你每天都这么做,就会有一个该死的聪明 OpenClaw,它能做你想让它做的任何事。
或者,你可以直接加载 GBrain,使用我已经写好的所有代码,跳过前面的步骤,更快拥有你自己的《钢铁侠》里的 Jarvis。
——
GStack 加速 Claude Code:github.com/garrytan/gstack
GBrain 在 OpenClaw/Hermes Agent 中构建你自己的 Jarvis:github.com/garrytan/gbrain