一种将每次智能体错误转化为经过测试的确定性技能的工作流,使同类失败永不再现。
AI translation, not an official translation. Refer to the original for technical details.
Adapted from @garrytan# 如何真正阻止你的智能体重蹈覆辙
LangChain 已融资1.6亿美元,历经三年开发,估值达十亿美元。他们的测试平台 LangSmith 确实相当成熟:轨迹评估、追踪转数据集流水线、LLM 即评判者、回归套件、工具单元测试框架——该有的组件都有。这一点值得肯定。
但组件不等于实践。
LangChain 给了你测试工具,却从未告诉你测什么、按什么顺序测、什么时候算完成。
它没有提供一套有主见的工作流,按顺序说明:
- 这个失败发生了
- 现在编写一个技能
- 现在编写确定性代码
- 现在编写单元测试
- 现在编写 LLM 评估
- 现在添加解析器触发器
- 现在评估解析器
- 现在审计重复项
- 现在做冒烟测试
- 现在正确归档
这个闭环根本不存在。你必须自己从零散的原语中拼凑出来。很多 AI 用户根本不测试自己的智能体,因为他们选择的框架大概只给了他们一张健身房会员卡,却没有附上训练计划。
大多数 AI 智能体的"可靠性"不过是凭感觉——调调提示词、加大系统消息、念几句"请不要产生幻觉"的咒语。一旦对话变得复杂,这些东西立刻失效。那些融资数亿美元来解决这个问题的框架,给了你监控仪表盘和单元测试助手,然后说"祝你好运"。
这周我的智能体出了两次错。这两次失败都不能再发生。不是因为我客气地请求它。而是因为我把每次失败都转化成了永久性的结构修复:一个带有测试的技能,每天运行,永无止境。
我把这种实践称为"技能化(skillify)"。一旦你使用它,你的智能体就不会再反复犯同样的错误。以下是它的工作方式。
## 失败一:数据库里早已存在的那次出行记录
我向我的 OpenClaw 询问一次旧的商务出行,差不多是十年前的事,深埋在日历历史里。问题很简单,应该一秒钟就能回答。
然而智能体却做了这些:
1. 调用了实时日历 API → 被拦截(时间太久远)。
1. 尝试邮件搜索 → 结果嘈杂,没有定论。
1. 用不同参数再次调用日历 API → 仍然被拦截。
1. 五分钟后,搜索了本地知识库,瞬间找到了答案。
答案一直就在我自己的数据里。3,146 个日历文件,跨越2013年至2026年,已经建好索引,已经在本地,一条 grep 命令就能搞定。
智能体只是没有优先去那里查找。
在我一直写的这个框架(薄封装、厚技能)中,有一个关键区分:哪些工作需要判断力,哪些工作需要精确性。我称之为潜在(latent)与确定性(deterministic)。日历 grep 是确定性的。相同的输入,相同的输出,每次都一样,不需要模型。但智能体却在潜在空间里完成了这件事——启动推理、发起 API 调用、解读结果——而一个三行脚本本可以立即返回答案。(https://x.com/garrytan/status/2042925773300908103)
这才是 bug 所在。不是答案错了,而是用错了路径。
## 修复方案:calendar-recall(第1步 + 第2步)
在薄封装 / 厚技能的框架里,技能是一段 Markdown 流程,它教会模型如何处理某类任务。不是要做什么("做什么"由用户提供),而是提供处理过程。可以把它想象成一次方法调用:相同的流程,根据传入的内容产生截然不同的输出。
以下是从这次失败中诞生的技能:
> name: calendar-recall
description: "大脑优先的历史日历查询。在处理未来事件或最近48小时内的事件之前,务必先使用此工具,而非任何实时API。"
其中的硬性规则:
> 实时日历API仅用于**未来**或**最近48小时内**的事件。所有历史数据优先走本地知识库。
让这套方案奏效的关键在于:代理本身编写了那段确定性脚本。技能文件(以Markdown格式存储于潜在空间中)告诉了代理如何修复问题。代理读取技能文件,理解到日历搜索是确定性工作,并生成了一段脚本来处理它:
> $ node scripts/calendar-recall.mjs search "Singapore"
Found 2 matching day(s):
── 2016-05-07 ──
Flight to Singapore, Mandarin Oriental check-in
── 2016-05-08 ──
Lunch with investors at Fullerton Hotel
代码运行时间不足100毫秒(其中大部分是Bun的启动时间;实际的grep操作在亚毫秒级完成)。零LLM调用。零网络请求。只有本地文件。
这正是整个架构运转的核心循环:潜在空间构建确定性工具,而确定性工具反过来约束潜在空间。代理凭借判断力(潜在)编写了calendar-recall.mjs;技能文件随即强制代理运行该脚本,而非对日历数据进行推理。模型的智能创造了约束,而这个约束防止了模型犯蠢。
旧的失败路径在结构上变得不可达。技能文件要求"优先搜索本地",脚本执行搜索,代理根本没有机会在这上面耍小聪明或再次出错。
## 失败2:"还有28分钟"(步骤1 + 步骤2再现)
同一天。代理说:"您的下一个会议还有28分钟。"
现实:88分钟后。代理在脑子里做UTC→PT时区换算时,恰好差了一个小时。
问题是,一个脚本(context-now.mjs)早已存在,它的输出如下:
> {
"now": "2026-04-21T07:38:12-07:00",
"upcomingEvents": [{
"summary": "App Ops Sprint Planning",
"minutesUntil": 88
}]
}
代码运行约50毫秒。零歧义。代理只是没有运行它。
与之前如出一辙:确定性工作(时间戳相减)在潜在空间中完成。明明有脚本知道答案,模型却在心算。
修复方案:context-now技能文件:
> name: context-now
description: "始终在线纪律:在做出任何时间敏感的断言之前,务必运行context-now.mjs。永远不要在脑子里做UTC→PT换算。"
以下是使用和不使用这些简单技能文件前后的对比:
## Skillify:将会拯救你理智的模式
两次失败。同一形态。代理拥有正确的工具,却选择了耍小聪明而非执行纪律。错误发生在错误的机器空间里。
在普通的AI配置中,AI会道歉,承诺改进,然后两周后同样的事情以不同的查询或不同的时区再度发生。代理对这个bug毫无记忆,没有针对它的测试,没有任何东西阻止它重演。
Skillify是解决方案。每一次失败都变成一个技能文件。每个技能文件都有测试。bug从结构上变得不可能重复。
以下是我在提升一次失败时使用的10项检查清单:
> □ 1. SKILL.md — 契约(名称、触发条件、规则)
□ 2. 确定性代码 — scripts/*.mjs(凡代码能做的,不用LLM)
□ 3. 单元测试 — vitest
□ 4. 集成测试 — 真实端点
□ 5. LLM评估 — 质量与正确性
□ 6. 解析器触发 — 在AGENTS.md中添加条目
□ 7. 解析器评估 — 验证触发条件确实能正确路由
□ 8. 检查可解析性 + DRY审计
□ 9. 端到端冒烟测试
□ 10. 大脑归档规则
一个功能如果没有通过全部十步,它就不是一项技能。它只是恰好今天能跑起来的代码。
上面提到的两次失败已经走完了第一步和第二步:先写SKILL.md(契约),再写确定性代码(智能体构建并使用的脚本)。但在我逐一介绍剩余八步之前,我想先展示一下"skillify"在日常使用中是什么样子——因为它不只是应对失败的补救手段。它已经成了一个动词。
## 把skillify当动词用
对我来说,在构建我的OpenClaw(以及GBrain)的过程中,这份清单起初是一套失败响应规程。后来,它变成了我构建一切东西的方式。
下面是我实际的工作流程。我用自然语言和我的智能体对话。我们在对话中一起搭建某个东西。我试了试,它跑通了。然后我说一个词:
> Garry:妈的太好了它跑通了。你能把这个记成一个webhook技能并skillify它吗,下次我们需要做webhook的时候?这东西为什么这么难搞定?不管了,现在好了。顺便把重复的代码也清理一下
那是一个OAuth webhook集成。我们花了一个小时才搞定它。然后"skillify it"把这次临时会话变成了一项持久技能,带着测试、解析器条目和文档。下次我需要webhook时,这项技能就已经存在了。智能体读取它。那一小时里积累的宝贵知识变成了永久资产。
还有一次。我们发现容器在某些任务中需要无头浏览器,而我桌面上的另一些任务则需要有头浏览器:
> Garry:太棒了!所以只要openclaw里有什么东西需要无头浏览器,我们就应该把这个记成一项技能!同时也要知道,如果我们需要有头浏览器,就应该让用户运行gstack browser并给我们一个pair-agent代码。skillify it!
就一条消息。智能体写好了skills/browser/SKILL.md,包含决策树、确定性脚本、测试。现在,每一个未来需要浏览器的会话都会自动路由到正确的工具。
还有这个。我注意到智能体一直在给我发ngrok链接,却不检查它们是否真的能用:
> Garry:我们能不能做一项技能,规定每次你给我发链接时,你必须自己先curl一下,确认端点是开的、隧道能通?skillify it!
或者那次差点让我误了会议的日历重复预约:
> Garry:我需要你写一项常规技能。就是日历检查技能。明天我11点有个时间冲突的重复预约。写一项技能,用确定性方式来检查这类问题。
就一句话。代码、技能、测试、解析器条目、可达性审计。全部十步,一口气说完。我的OpenClaw知道该怎么做,也真的做了,现在这已经成了一种固定套路。我已经这样做了几十次。没有它我没法活。
模式始终如一:在对话中做出原型,看着它跑通,说一声"skillify",原型就变成了永久基础设施。我不写规格说明,不提工单。我和智能体对话,我们一起解决问题,然后解决方案变成一项技能,智能体可以永远使用,无需再问我。
## 步骤 3:单元测试
经典的 vitest 风格。确定性函数,确定性断言。calendar-recall.mjs 导出纯函数,如 parseEventLine、eventMatchesKeyword、searchKeyword、formatJson。每个函数都针对测试夹具数据进行测试:临时目录中的合成日历文件、已知输入、已知输出。
这类测试能捕获的 bug 包括:parseEventLine 在位置字段包含 Unicode 字符时静默丢弃事件;dateFromPath 对闰年日期返回 null;当只有一个与会者时 formatJson 省略 attendees 数组。这些 bug 细小、无聊,却至关重要。如果脚本输出了错误结果,技能就会给出错误答案,而 agent 会自信满满地告诉我错误的信息。
以 context-now 为例,单元测试验证时区格式化、免打扰时段检测,以及跨越夏令时边界的 minutesUntil 计算。其中一个测试传入一个距夏令时切换还有 3 分钟的时间点,并验证输出不会突然跳变 60 分钟。这正是导致"28 分钟"故障的那个 bug。它现在在结构层面已不可能发生。
我在 5 个测试套件中共有 179 个单元测试,运行时间不到 2 秒。
## 步骤 4:集成测试
这些测试会访问真实的端点和真实的数据。calendar-recall.mjs 是否真的能在真实的 brain 仓库中找到事件,而不仅仅是在测试夹具中?当日历缓存过期或缺失时,context-now.mjs 是否仍能生成有效的 JSON?集成测试能捕获单元测试遗漏的 bug,因为夹具数据往往过于干净。真实数据中包含格式错误的事件行、缺失的时区字段、带有 Windows 换行符的日历文件,以及跨越午夜的事件。
有一条原则:如果你发现自己在手动检查脚本是否对真实数据做出了正确的处理,那么这个检查就应该变成一个集成测试。
## 步骤 5:LLM 评估
这里开始变得有趣。有些输出需要判断才能评价。"这个日历摘要有用吗?"不是一个脚本能给出是/否答案的问题。因此我使用"LLM 作为评判者"的方式:让一个模型根据评分标准来评估另一个模型的输出。
对于 context-now,每天运行 35 个评估。其中一个向 agent 发送消息:"嘿,我的航班大约 45 分钟后起飞,我能赶到 SFO 吗?"——然后检查 agent 是在回答之前运行 context-now.mjs,还是在脑子里自己算。如果 agent 上当,自己去计算时间,评估就会失败。
另一个评估给 agent 一个 UTC 时间戳,问:"这对我来说是几点?"正确行为是运行脚本并引用结果;错误行为是在脑子里做换算。这个评估会同时捕获错误答案和错误过程——因为即使这次心算碰巧算对了,下次也可能算错。
我找到的最诚实的评估启发法是:搜索你的对话历史,找你说过"操"或"什么鬼"的地方。那些就是你缺失的测试用例。
## 步骤 6:解析器触发器
Resolver是上下文的路由表:当任务类型X出现时,加载技能Y。我在这里详细介绍过resolver。每个技能都需要在AGENTS.md中添加一个触发条目,这个文件负责告诉智能体有哪些技能可用以及何时使用它们。(https://x.com/garrytan/status/2044479509874020852)
Resolver触发器只是Markdown表格中的几行:
这一步能捕获的bug:你写了一个新技能,却忘记将它添加到resolver中。技能存在,能力存在,但系统无法访问它。这就像医院里有一位外科医生,却没有把他列入医院名录。比根本没有这项技能还要糟糕,因为你以为系统已经能处理这种情况了。
## 第7步:Resolver评估
这一层是大多数人完全忽视的地方。Resolver触发器说的是"这个短语应该路由到这个技能"。Resolver评估则测试它是否真的会这样做。
我的resolver评估套件有50多个测试用例,如下所示:
> { intent: 'check my signatures', expectedSkill: 'executive-assistant' },
{ intent: 'who is Pedro Franceschi', expectedSkill: 'brain-ops' },
{ intent: 'save this article', expectedSkill: 'idea-ingest' },
{ intent: 'what time is my meeting', expectedSkill: 'context-now' },
{ intent: 'find my 2016 trip', expectedSkill: 'calendar-recall' },
有两种失败模式。假阴性:技能本应触发却没有触发,因为触发器描述有误或缺失。假阳性:错误的技能被触发,因为两个触发器发生了重叠。"What's on my calendar tomorrow"应该路由到calendar-check,而不是calendar-recall,也不是google-calendar。三个技能,三个不同的时间域,一个短语却可能合理地匹配其中任何一个。Resolver评估能在用户遇到这个问题之前捕获到这种模糊性。
我以两种方式运行这些评估:既作为确定性结构测试(AGENTS.md表格是否包含正确的映射?),也作为LLM路由测试(给定这个意图,模型是否真的选择了正确的技能?)。两个层面都很重要。表格可以是正确的,但模型仍然可能路由错误,因为触发器描述不够清晰。
## 第8步:check-resolvable与DRY审计
经过一个月的构建,我积累了40多个技能。有些是针对特定事件而创建的,另一些是由运行定时任务的子智能体产生的。没有人在维护resolver表格。技能不断诞生,却没有被注册。
于是我构建了check-resolvable。这是一个元测试,遍历整条链路:AGENTS.md resolver → SKILL.md → 脚本/定时任务。如果一个脚本能做有用的工作,却没有从resolver出发能到达它的路径,它就是不可访问的。LLM永远不会知道要使用它。
第一次运行在40多个技能中发现了6个不可访问的技能。系统15%的能力处于黑暗之中。
- 一个航班追踪器,没有人能通过询问航班信息来调用它。
- 一个内容创意生成器,只能按定时任务运行,无法手动触发。
- 一个引用修复器,存在于技能目录中,但根本没有被列入resolver。
不到一个小时就修复了。只需在AGENTS.md中添加触发条目即可。现在,check-resolvable作为gbrain doctor的一部分每周运行一次。它检查三件事:
1. 每个含有SKILL.md的技能目录在resolver中都有对应的条目。
1. 技能引用的每个脚本都实际可调用(文件存在,导出了正确的函数)。
1. 没有两个技能拥有重叠的触发器描述,从而导致路由模糊。
DRY 审计与它并行运行。如果你不小心,最终会得到十五个功能大同小异的技能,而解析器会根据骰子落点来选择其中一个。以日历记忆为例:
同一领域内有四个技能,零重叠,各司其职。那张矩阵图不是专门为这篇文章画的示意图,它就活在 SKILL.md 里,审计脚本会对其进行解析。如果你新建第六个日历技能并踩进了别的技能的赛道,审计在该技能上线之前就会失败。
## Step 9: E2E smoke test
完整的端到端管道测试。
- 向 Agent 提问"我是什么时候去新加坡的?",验证它是否运行了 calendar-recall.mjs、得到了正确答案,并以正确格式输出。
- 提问"我的下一个会议是几点?",验证它运行的是 context-now.mjs,而不是自己心算。
冒烟测试是最后一道防线。其他所有环节都能通过,但如果各部分没有连通,系统仍然可能失败。技能可以是正确的,脚本可以是正确的,解析器可以是正确的,但 Agent 仍然可能选择无视这一切、自行其是。冒烟测试就是用来发现这种情况的。
## Step 10: Brain filing rules
每一个向知识库写入内容的技能都需要知道东西该放在哪里。人物归入 people/,公司归入 companies/,政策分析归入 civic/。我发现 13 个脑写入技能中有 10 个把文件归错了目录,原因是它们各自硬编码了自己的路径,而不是去查询解析器。
归档规则文档对常见的错误归档模式进行了梳理,包括:来源与原件的区分,人物与公司的区分(当某个人本身就是一家公司时)。技能在创建任何页面之前会先读取这些规则。自此以后,归档错误清零。
## GBrain: where Skillify lives, and you should adopt it from my GBrain Skill Pack
Skillify 模式并不依赖于 OpenClaw 或任何特定的执行框架,它内置于 GBrain。GBrain 是我编写的开源知识引擎,运行在你所使用的任何框架之下。它负责管理你的脑库、运行评估,并强制执行使技能得以持久的质量关卡。(https://openclaw.ai/) (https://github.com/garrytan/gbrain)
GBrain SkillPack 是一个可移植的技能包,包含技能、解析器触发器、确定性脚本和测试,只需让 OpenClaw/Hermes Agent 执行安装操作,即可将其部署到任何 Agent 配置中。这正是我为自己的 OpenClaw/Hermes Agent 编写的技能和能力可以被自动添加到你的 OpenClaw 中的方式——包含完整的 10 步 Skillify 输出,打包后你可以直接放入自己的 OpenClaw/Hermes Agent,开箱即用。
前文中的 Skillify 检查清单不是建议,而是 gbrain doctor 实际检查的内容。
gbrain doctor --fix 会自动修复 DRY 违规问题,将重复的代码块替换为对惯例的引用,全程由 git 工作区检查守护,确保没有任何内容被误覆盖。
## Why Hermes Agent isn't enough on its own
来自 Nous Research 的 Hermes Agent 做了一件真正出色的事:它拥有一个 skill_manage 工具,让 Agent 自身能够根据所学内容创建、修补和删除技能。当 Agent 完成一项复杂任务或从错误中恢复时,它会提出一个技能并将其写入磁盘。这是 Agent 自主积累的程序性记忆。渐进式披露(先加载技能索引,仅在被选中时才拉取完整的 SKILL.md)、有界内存(MEMORY.md 上限为 2,200 个字符)、条件激活(当所需工具不可用时技能自动隐藏)——设计十分精巧。(https://github.com/NousResearch/hermes-agent)
但 Hermes 并不测试其技能。没有针对确定性代码的单元测试。没有用于验证路由的解析器评估。没有 check-resolvable 来发现暗技能。没有 DRY 审计来捕捉重复项。没有每日健康检查在某些内容偏离时亮红灯。
我在任何未经测试的技能系统中都见过这些故障模式不断积累:
- 智能体周一创建了 deploy-k8s。周四在另一个对话中又创建了 kubernetes-deploy。两者并存。两者都在相似的短语上触发。路由出现歧义,没人注意到,直到错误的那个在错误的时机触发。
- 技能编写时运行完美。六周后上游 API 改变了结构。该技能默默返回垃圾数据,直到某个人类发现为止。
- 一个自主创建的技能触发条件过弱,从未匹配成功。它变成一个孤儿,消耗索引 token,从不运行,慢慢腐烂。
这就是软件工程在 2005 年就解决的"没有测试,任何代码库都会腐烂"的问题。智能体技能也不例外。Hermes 在创建方面做得非常出色。GBrain 负责验证。你两者都需要。
## 核心理念
在一个健康的软件工程团队中,每个 bug 都会对应一个测试。那个测试永久存在。该 bug 在结构上变得不可能再次出现。AI 智能体应该以同样的方式运作。
每一次失败都变成一个技能。每个技能都有评估。每个评估每天运行。智能体的判断力得到永久提升,而不仅仅是针对当前会话,也不仅仅是在上下文窗口还保持时。
那次出行失败不会再发生。那次时区失败不会再发生。当下一次失败出现时(它必然会出现,因为这是一场对抗熵与品味的对抗游戏),它也会被技能化。
我一年后使用的智能体,将由它在过去这一年里犯下的每一个错误所塑造。这不是锦上添花。这就是整个核心论点。
把海洋煮沸。让你的智能体做某件事,然后将其技能化。每天这样做,你就会拥有一个该死的聪明的 OpenClaw,它能做你想让它做的一切。
或者你也可以直接加载 GBrain,使用我已经写好的所有代码,更快地拥有属于你自己的钢铁侠版贾维斯。
--
GStack 用于加速 Claude Code github.com/garrytan/gstack
GBrain 用于在 OpenClaw/Hermes Agent 中构建你自己的钢铁侠版贾维斯 github.com/garrytan/gbrain (https://github.com/garrytan/gstack) (https://github.com/garrytan/gbrain)