如何真正防止你的 Agents 重复犯错

@garrytan
英语3个月前 · 2026年4月22日
931K
1.7K
160
94
4.9K

TL;DR

Garry Tan 介绍了 Skillify,这是一个包含 10 个步骤的框架,旨在通过将失败转化为经过测试的确定性“技能”,确保 AI Agents 不再重复犯错。

LangChain 已融资 1.6 亿美元。历经三年开发,估值数十亿。他们的测试平台 LangSmith 确实很先进:轨迹评估、跟踪转数据集管道、LLM 评判、回归测试套件、工具单元测试框架。该有的他们都有,功劳该算还是得算。

但零散的组件不等于系统性的实践。

LangChain 给了你测试 工具。但它从没告诉你要测什么、按什么顺序测,或者何时才算完成。

没有一个明确的流程告诉你,按顺序来:

  • 发生了这个故障
  • 现在写一个技能
  • 现在写确定性代码
  • 现在写单元测试
  • 现在写 LLM 评估
  • 现在添加一个 Resolver 触发器
  • 现在评估 Resolver
  • 现在检查重复
  • 现在做冒烟测试
  • 现在正确归档

这个循环根本不存在。 你只能从零散的原始模块里自己拼凑出来。绝大多数 AI 用户仍然完全不测试他们的 Agent,这是因为他们选择的框架可能只给了他们一张健身卡,却没有附带训练计划。

大多数 AI Agent 的“可靠性”都靠感觉。调试提示词。加长系统消息。“请勿产生幻觉”之类的咒语。一旦对话变得复杂,这些东西就会失效。那些融资数亿美元想解决这个问题的框架,最终只给了你监控仪表盘和单元测试助手,然后说了句“祝你好运”。

我的 Agent 这周搞砸了两次。这两个错误都不能再发生。不是因为我很客气地请求它。而是因为我把每个错误都变成了永久性的结构性修复:一个带有测试的技能,这些测试每天、永远都会运行。

我把这个实践称为“Skillify”。 一旦你开始使用它,你的 Agent 就不会再犯同样的错误。具体方法如下。

故障 1:早已存在于数据库中的行程

我问我的 OpenClaw 关于一次以前的商务旅行,差不多十年前的事了,埋在日历历史中的某个角落。简单的问题。应该一秒钟就能回答。

结果 Agent 做了以下这些:

  1. 调用实时日历 API → 被阻止(时间太早了)。
  2. 尝试电子邮件搜索 → 结果很杂乱,没有决定性信息。
  3. 用不同的参数再次尝试日历 API → 仍然被阻止。
  4. 五分钟后,搜索我的本地知识库,立刻找到了答案。

答案一直就躺在我的数据里。 3146 个日历文件,覆盖 2013 年到 2026 年。 已经索引过。已经存在于本地。一次 grep 就能找到的事。

Agent 只是没有先在那里查找。

在我一直撰写的框架(瘦框架,胖技能)中,需要判断的工作和需要精确性的工作之间存在关键区别。我把它们称为 潜在(latent)确定性(deterministic)。日历 grep 是确定性的。相同的输入,相同的输出,每次都一样。不需要模型。但 Agent 仍然在潜在空间里做了这件事,启动推理、调用 API、解读结果,而一个三行的脚本本来能立刻返回答案。

这就是 bug。不是答案错了,而是处理问题的方式错了。

修复:calendar-recall(步骤 1 + 2)

在瘦框架 / 胖技能模式中,一个 技能 是一个 Markdown 流程,它教会模型如何处理任务。不是教它做什么(“做什么”由用户提供)。技能提供的是流程。可以把它想象成方法调用:同样的流程,根据你传入的参数不同,输出结果截然不同。

以下是从这次故障中产生的技能:

名称:calendar-recall 描述:“大脑优先的历史日历查询。对于任何不是未来 48 小时内的事件,务必在使用任何实时 API 之前使用此技能。”

以及内部的硬性规则:

实时日历 API 仅用于 FUTURE(未来) 或 LAST 48 HOURS(最近 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": "应用运维冲刺规划", "minutesUntil": 88 }] }

这段代码运行时间大约 50 毫秒。绝对精确。Agent 只是没有运行它。

和之前一样的模式:在潜在空间里做确定性工作(减去时间戳)。模型在脑子里计算,而脚本已经有答案了。

修复:context-now 技能:

名称:context-now 描述:“始终开启的纪律:在做出任何与时间相关的断言之前,务必运行 context-now.mjs。绝不要在心里做 UTC→PT 转换。”

下面是一个简单的使用和不使用这些简单技能的对比:

Garry Tan - inline image

Skillify:能让你省心的模式

两个故障。相同的模式。Agent 拥有正确的工具,却选择了耍小聪明而非遵循纪律。 错误的事情发生在错误的机器空间里。

在普通的 AI 设置下,AI 会道歉,保证下次做得更好,然后两周后,同样的错误会因为一个不同的查询或另一个时区再次发生。Agent 没有关于这个 bug 的记忆,没有针对这个 bug 的测试,没有任何东西能阻止它重复发生。

Skillify 就是解决方案。每个故障都变成一个技能。每个技能都有测试。这个 bug 在结构上变得不可能重复发生。

以下是我在将一个故障升级为永久技能时使用的 10 项检查清单:

□ 1. SKILL.md — 契约(名称、触发器、规则) □ 2. 确定性代码 — scripts/*.mjs(代码能做的事就不需要 LLM) □ 3. 单元测试 — vitest □ 4. 集成测试 — 实时端点 □ 5. LLM 评估 — 质量 + 正确性 □ 6. Resolver 触发器 — 在 AGENTS.md 中的条目 □ 7. Resolver 评估 — 验证触发器是否实际路由 □ 8. 检查可解析性 + DRY 审计 □ 9. 端到端冒烟测试 □ 10. 大脑归档规则

一个不能通过全部十项的功能不是一个技能。它只是恰好今天能运行的代码而已。

上面的两个故障已经演示了步骤 1 和 2:编写 SKILL.md(契约),然后编写确定性代码(Agent 构建并使用的脚本)。但在讲解剩下的八个步骤之前,我想向你展示 skillify 在日常使用中是什么样子,因为它不仅仅是应对失败的措施。它已经变成了一个动词。

Skillify 作为一个动词

对我来说,在构建我的 OpenClaw(和 GBrain)时,这个清单最初是一个故障响应协议。然后它变成了我构建一切的方式。

以下是我实际工作流程的样子。我用自然语言和我的 Agent 交谈。我们在对话中一起构建一些东西。我试一下。它工作了。然后我说一个词:

Garry:

哇靠居然成功了。你能不能把这个记为一个 webhook 技能并 skillify 它,下次我们需要做 webhooks 时直接能用?为什么这个之前搞得这么难?总之现在好了。也帮我 DRY 一下。

那是一个 OAuth webhook 集成。我们花了一个小时才让它工作起来。然后“skillify it”把那个即兴的会话变成了一个包含测试、resolver 条目和文档的持久化技能。下次我需要 webhook 时,技能已经存在了。Agent 会读取它。那一个小时中艰难获得的知识就永久保存了。

另一个例子。我们发现我们的容器在某些任务中需要一个无头浏览器,在桌面上的其他任务中需要一个有头浏览器:

Garry:

太好了!所以我们应该把这个记为一个技能,只要 openclaw 里需要无头浏览器的时候就用它!并且也要知道如果我们需要有头浏览器,应该让用户运行 gstack browser 并给我们一个 pair-agent 代码。skillify 它!

一条消息。Agent 就编写了 skills/browser/SKILL.md,包含决策树、确定性脚本和测试。现在,每个未来需要浏览器的会话都会自动路由到正确的工具。

或者这个。我注意到 Agent 总是给我发 ngrok 链接,却从不检查它们是否真的能用:

Garry:

我们能不能做一个技能,规定每当你给我发一个链接时,你必须自己先 curl 一下,确保端点是开放的并且隧道是通的?skillify 它!

或者那个差点让我错过会议的日历冲突:

Garry:

这是我需要你写的一个常规技能。这是日历检查技能。明天我上午 11 点被重复预约了。创建一个技能,让它能确定性检查这类问题。

一句话。代码、技能、测试、resolver 条目、可达性审计。完整的 10 步清单一口气完成。我的 OpenClaw 知道怎么做,并且去做了,现在这已经成了一个熟悉的流程。我已经做了几十次了。没有它我简直活不下去。

模式始终如一:在对话中构建原型,看到它起作用,说“skillify”,然后原型就变成了永久性的基础设施。我不写规格说明。我不提交工单。我和我的 Agent 交谈,我们一起解决问题,然后解决方案就变成一个 Agent 可以永久使用、无需我操心的技能。

这就是那些拿到了 1.6 亿美元投资的框架所遗漏的东西。不是测试原语。不是评估工具。而是工作流程。是那个人类说“做对了,现在让它永久化”的时刻,而系统确切知道“永久化”意味着什么:SKILL.md、确定性代码、单元测试、集成测试、LLM 评估、resolver 触发器、resolver 评估、DRY 审计、冒烟测试、大脑归档。十个步骤。一个词搞定。

以下是剩下的八个步骤在实际中是什么样的。

步骤 3:单元测试

经典的 vitest。确定性函数,确定性断言。calendar-recall.mjs 导出纯函数,如 parseEventLineeventMatchesKeywordsearchKeywordformatJson。每一个都使用固定数据进行测试:临时目录中的模拟日历文件、已知输入、已知输出。

这类 bug 能捕获的问题:parseEventLine 静默丢弃位置字段包含 Unicode 字符的事件。dateFromPath 对闰年日期返回 nullformatJson 在只有一个人的时候省略了 attendees 数组。小而无聊,但至关重要。如果脚本产生错误输出,技能就会产生错误答案,然后 Agent 就会自信地告诉我错误的信息。

对于 context-now,单元测试验证时区格式化、静默时间检测以及跨 DST 边界的 minutesUntil 计算。一个测试输入一个夏令时转换前 3 分钟的时间,并验证输出不会跃变 60 分钟。这正是导致“28 分钟”故障的那个 bug。现在它在结构上已经不可能了。

我在 5 个测试套件中有 179 个单元测试。它们运行时间不到 2 秒。

步骤 4:集成测试

这些测试会访问实时端点和真实数据。calendar-recall.mjs 是否真的能在真实的大脑仓库中找到事件,而不仅仅是测试夹具?当日历缓存过期或缺失时,context-now.mjs 是否还能产生有效的 JSON?集成测试能捕获单元测试遗漏的 bug,因为测试夹具数据太干净了。真实数据包含格式错误的事件行、缺失的时区字段、带有 Windows 换行符的日历文件、跨午夜的事件。

规则:如果你发现自己手动检查脚本是否在真实数据上正确运行,那么这个检查就应该是一个集成测试。

步骤 5:LLM 评估

这里开始变得有趣了。某些输出需要判断力来评估。“这个日历摘要是否有用?”不是脚本能用“是/否”回答的问题。所以我使用 LLM 作为评判者:一个模型根据评分标准评估另一个模型的输出。

对于 context-now,每天运行 35 个评估。其中一个评估会向 Agent 输入一条消息,比如“嘿,我的航班大约 45 分钟后起飞,我能赶上飞往 SFO 的航班吗?”,并检查 Agent 是在回答之前运行了 context-now.mjs,还是试图在心里计算时间。如果 Agent 上钩并自己计算了时间,评估失败。

另一个评估给 Agent 一个 UTC 时间戳,并问“对我来说那是什么时间?”正确的行为是运行脚本并引用结果。不正确的行为是在心里做转换。评估能捕获错误的答案和错误的流程,因为即使这次心算碰巧对了,下次也会错。

我发现最诚实的评估启发法:在你对话历史中搜索你说“操蛋”或“什么鬼”的时刻。那些就是你缺少的测试用例。

步骤 6:Resolver 触发器

一个 Resolver 是一个用于上下文的“路由表”:当任务类型 X 出现时,加载技能 Y。我 在这里 详细写过 Resolver。每个技能都需要在 AGENTS.md(教会 Agent 存在哪些技能以及何时使用它们的文件)中有一个触发器条目。

Resolver 触发器只是 Markdown 表格中的行:

Garry Tan - inline image

这个步骤能捕获的 bug:你写了一个新技能,但忘记将其添加到 resolver 中。技能存在。能力存在。但系统无法调用它。这就像医院里有外科医生,但没有把他列在医院通讯录里。比根本没有这个技能更糟糕,因为你以为系统能处理这件事。

步骤 7:Resolver 评估

这是大多数人完全错过的一层。一个 Resolver 触发器说“这个短语应该路由到那个技能”。Resolver 评估 测试 它是否真的做到了。

我的 Resolver 评估套件有超过 50 个这样的测试用例:

{ intent: '检查我的签名', expectedSkill: 'executive-assistant' }, { intent: '谁是 Pedro Franceschi', expectedSkill: 'brain-ops' }, { intent: '保存这篇文章', expectedSkill: 'idea-ingest' }, { intent: '我的会议是什么时候', expectedSkill: 'context-now' }, { intent: '找到我 2016 年的旅行', expectedSkill: 'calendar-recall' },

两种失败模式。漏报(假阴性):技能应该触发但实际上没触发,因为触发器描述错误或缺失。误报(假阳性):错误的技能被触发,因为两个触发器重叠了。“我明天的日历上有什么”应该路由到 calendar-check,而不是 calendar-recallgoogle-calendar。三个技能,三个不同的时间域,一个可能匹配任何一个的短语。Resolver 评估在用户遇到问题之前就捕获了歧义。

我既作为确定性结构测试(检查 AGENTS.md 表是否包含正确的映射关系)运行这些评估,也作为 LLM 路由测试(给定此意图,模型是否真的选择了正确的技能)运行。两个层面都很重要。表本身可能是正确的,但如果触发器描述模糊,模型仍然可能路由错误。

步骤 8:检查可解析性 + DRY 审计

构建一个月后,我有了 40 多个技能。有些是为了响应特定事件而创建的,其他的是由运行 crons 的子 Agent 生成的。没有人维护 Resolver 表。技能在不断产生,但没有被注册。

所以我构建了 check-resolvable。一个元测试,遍历整个链条:AGENTS.md resolver → SKILL.md → 脚本/cron。如果一个脚本存在并能做有用的工作,但从 resolver 没有路径指向它,它就是不可达的。LLM 永远不会知道使用它。

第一次运行发现 40 多个技能中有 6 个不可达。系统百分之十五的能力是隐藏的。

  • 一个航班追踪器,没人能通过询问航班来调用它。
  • 一个内容创意生成器,只在 cron 上运行,无法手动触发。
  • 一个引用修复器,存在于技能目录中,但根本不在 Resolver 里列出。

一个小时就修好了。只需在 AGENTS.md 中添加触发器条目。现在 check-resolvable 作为 gbrain doctor 的一部分每周运行一次。它检查三件事:

  1. 每个包含 SKILL.md 的技能目录在 Resolver 中都有对应的条目。
  2. 每个技能引用的脚本实际上都是可调用的(文件存在,导出正确的函数)。
  3. 没有两个技能具有可能导致路由歧义的重叠触发器描述。

DRY 审计同时运行。如果不小心,你最终会有十五个功能类似的技能,而 Resolver 会根据某种随机选择来调用其中一个。以 calendar-recall 为例:

Garry Tan - inline image

同一领域有四个技能。零重叠。每个都有自己的职能范围。那个矩阵不是为此文而绘制。它存在于 SKILL.md 内部,审计脚本会解析它。如果你构建了第六个日历技能,侵犯了另一个的职能范围,审计会在该技能部署之前就失败。

步骤 9:端到端冒烟测试

完整的流程,从头到尾。

  • 询问 Agent“我什么时候去过新加坡?”并验证它运行了 `calendar-recall.mjs`,得到了正确的答案,并且格式正确。
  • 询问“我的下一个会议是什么时候?”并验证它运行了 `context-now.mjs`,而不是在心里计算时间。

冒烟测试是最后一道防线。其他所有测试都能通过,但如果各个组件没有正确连接,系统仍然可能失败。技能可以正确,脚本可以正确,Resolver 可以正确,但 Agent 仍然可以选择忽略所有这一切,自己随意发挥。冒烟测试就能抓住这种情况。

步骤 10:大脑归档规则

每个向知识库写入内容的技能都需要知道数据该去哪里。人员信息去 people/。公司信息去 companies/。政策分析去 civic/。我发现 13 个写入大脑的技能中有 10 个归档到了错误的目录,因为它们各自硬编码了自己的路径,而没有咨询 Resolver。

归档规则文档列出了常见的错误归档模式。来源 vs. 原件。人员 vs. 公司(当某个人本身就是一个公司时)。技能在创建任何页面之前会先读取这些规则。自那以后零次归档错误。

GBrain:Skillify 的所在地,你应该从我的 GBrain 技能包中采用它

Skillify 模式并非 OpenClaw 或任何特定框架所独有。它内置于 GBrain 中。GBrain 是我编写的开源知识引擎,它位于你使用的任何框架之下。它管理你的大脑仓库,运行你的评估,并执行使技能持久化的质量门禁。

一个 GBrain 技能包(SkillPack)是一套可移植的技能、Resolver 触发器、确定性脚本和测试的集合。你只需让 OpenClaw/Hermes Agent 执行安装,就可以将它集成到任何 Agent 设置中。这就是我为我的 OpenClaw/Hermes Agent 编写的技能和能力如何被自动添加到你的 OpenClaw 的方式——包括完整的 10 步 skillify 产出物,打包好以后,你可以直接放入你的 OpenClaw/Hermes Agent 中,它就能正常工作。

之前提到的 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 不测试它的技能。确定性代码没有单元测试。没有 Resolver 评估来验证路由。没有 check-resolvable 来发现隐藏技能。没有 DRY 审计来捕获重复项。没有每日健康检查在出现偏差时发出红色警报。

我观察到在任何未经测试的技能系统中会累积的失败模式:

  • Agent 在周一创建了 deploy-k8s。周四,它从另一次对话中创建了 kubernetes-deploy。两者都存在。两者都在相似的短语上触发。路由模糊不清,而且没有人注意到,直到错误的任务在错误的时间触发了。
  • 技能在创建时完美运行。六周后,上游 API 发生变化。技能静默地返回垃圾数据,直到有人发现。
  • 一个自动创建的技能有一个微弱的触发器,从未被匹配。它变成了孤儿,消耗索引令牌,从不运行,慢慢腐烂。

这就是“没有测试,任何代码库都会腐烂”的问题,软件工程界在 2005 年就已经解决了这个问题。Agent 技能也不例外。Hermes 擅长处理创建。GBrain 擅长处理验证。你需要两者兼顾。

核心思想

在一个健康的软件工程团队中,每个 bug 都会有一个对应的测试。这个测试会永久存在。这个 bug 在结构上变得不可能复发。AI Agent 也应该以同样的方式工作。

每个失败都变成一个技能。每个技能都有评估。每个评估每天运行。Agent 的判断力是永久性提升的,而不仅仅是在当前会话中,也不仅仅是在上下文窗口还能容纳的时候。

行程故障不会再发生。时区故障不会再发生。当下一个故障出现时(它一定会出现,因为这是一场对抗混乱和品味的抗争),它也会被 skillify。

我一年后一起工作的 Agent,将由它在过去一年里犯下的每一个错误塑造而成。这不是锦上添花。这就是核心论点。

把池子搅热。让你的 Agent 做点什么,然后 skillify 它。你每天都这样做,你就会有一个该死的、聪明的 OpenClaw,它能做你想让它做的任何事情。

或者你可以直接加载 GBrain,使用我已经写好的所有代码,然后更快地迈向属于你自己的《钢铁侠》中的贾维斯。

--

GStack 用于在 Claude Code 中加速 github.com/garrytan/gstack

GBrain 用于在 OpenClaw/Hermes Agent 中构建你自己的《钢铁侠》贾维斯 github.com/garrytan/gbrain

https://x.com/garrytan/status/2042925773300908103

https://x.com/garrytan/status/2044479509874020852

一键保存

使用 YouMind AI 深度阅读爆款文章

保存原文、追问细节、总结观点,并在一个 AI 工作空间里把爆款文章沉淀成可复用笔记。

了解 YouMind
写给创作者

把你的 Markdown 变成干净的 𝕏 文章

图片上传、表格、代码块,往 𝕏 上手动重排太痛苦。YouMind 把整篇 Markdown 一键转成干净、可直接发布的 𝕏 文章草稿。

试试 Markdown 转 𝕏

更多可拆解样本

近期爆款文章

探索更多爆款文章