作者背景
Garry Tan,AI系统架构师,LangChain生态贡献者,GBrain开源项目作者
LangChain等框架提供了测试工具,但缺乏工作流程。本文提出“技能化”方法:将智能体的每次失败转化为一个带有完整测试套件的技能,通过“潜在空间构建确定性工具,确定性工具约束潜在空间”的循环,从结构上防止错误重现。该方法包含10个步骤的检查清单,并已集成到GBrain开源项目中。
问题的本质:工具不等于实践
LangChain已经融资1.6亿美元,开发了三年,估值十亿美元。他们的测试平台LangSmith确实很复杂:轨迹评估、追踪到数据集的管道、用大语言模型作为评判标准、回归测试套件、针对工具的单位测试框架。他们拥有这些部件,这点值得肯定。
但部件不等于实践。LangChain给了你测试工具,却从不告诉你要测试什么、按什么顺序测试、或者什么时候算测试完成。没有一个有主见的工作流程来按顺序说明:这个失败发生了,然后写一个技能,然后写确定性代码,然后写单元测试,然后写大语言模型评估,然后添加解析器触发器,然后评估解析器,然后检查重复项,然后进行冒烟测试,然后正确归档。这个循环根本不存在。你必须从散落的原始部件中自己发明这个流程。
很多AI用户根本不测试他们的智能体,因为他们选择的框架给了他们一张健身房会员卡却没有提供锻炼计划。大多数智能体的“可靠性”是基于感觉的:调整提示词、加大系统消息、“请不要产生幻觉”这样的咒语。一旦对话变得复杂,这些东西就会失效。那些融资数亿美元来解决这个问题的框架给了你监控仪表盘和单元测试助手,然后说了句“祝你好运”。
失败的形状:同一个模式重复出现
我的智能体这周搞砸了两次。这两个失败都不能再发生。不是因为我客气地请求,而是因为我把每个失败都变成了一个永久性的结构修复:一个带有测试的技能,这些测试每天都会运行,永远运行。我把这个实践叫做“技能化”。一旦你使用它,你的智能体就不会一直犯同样的错误。下面说说它是怎么工作的。
我问我用的OpenClaw关于一次差不多十年前的老出差,这个信息埋在日历历史的某个地方。问题很简单,应该只需要一秒钟。结果智能体做了这些事:调用实时日历API被阻止了,因为时间太远;尝试邮件搜索得到了嘈杂的结果,没有结论;用不同参数再次尝试日历API还是被阻止。五分钟后,它搜索了我的本地知识库,立刻找到了答案。
答案一直就在我自己的数据里。从2013年到2026年横跨3146个日历文件,已经被索引,已经是本地的,一个grep命令就能找到。智能体只是没有先往那儿看。
在我一直写的框架中,需要判断的工作和需要精度的工作之间有一个关键区别,我称之为潜在的和确定性的。日历grep是确定性的,相同的输入每次产生相同的输出,不需要模型。但智能体还是在潜在空间里做了这件事,启动了推理、进行了API调用、解释了结果,而一个三行的脚本本来可以立刻返回答案。这就是那个bug,不是错误的答案,而是错误的空间。
技能化第一步:从失败中提取技能
在“瘦 harness / 胖技能”架构中,一个技能是一个Markdown流程文档,教模型如何完成一个任务。不是教它做什么,用户提供要做什么,技能提供的是过程。可以把它想象成一个方法调用:相同的过程,根据你传入的参数产生完全不同的输出。
下面是从这个失败中产生的技能:
名称:calendar-recall
描述:“大脑优先的历史日历查找。对于任何不在未来或过去48小时内的事件,在使用任何实时API之前始终使用此技能。”
内部的硬规则是:实时日历API只用于未来或过去48小时内的事件。所有历史事件都首先通过本地知识库查找。
让这个方案有效的是:智能体自己写了确定性脚本。技能文件(Markdown格式,存在于潜在空间)告诉智能体如何解决问题。智能体读取了技能,理解了日历搜索是确定性工作,然后生成了一个脚本来处理它。运行命令“node scripts/calendar-recall.mjs search ‘Singapore’”后,找到了两天的匹配结果:2016年5月7日飞往新加坡的航班和文华东方酒店入住,以及2016年5月8日在富丽敦酒店与投资者的午餐。
这段代码在不到100毫秒内运行完成,其中大部分是Bun的启动时间,实际的grep是亚毫秒级的。零个大语言模型调用,零个网络请求,只有本地文件。这就是让整个架构工作的循环:潜在空间构建确定性工具,然后确定性工具约束潜在空间。智能体使用判断力(潜在)来编写calendar-recall.mjs,现在这个技能强制智能体运行那个脚本而不是推理日历数据。模型的智能创造了防止模型犯傻的约束。
旧的失败路径变得从结构上不可达。技能说“先搜索本地”,脚本执行搜索,智能体再也没有机会自作聪明或再次出错。
第二个失败:同样形状的不同问题
同一天,智能体说“你的下一个会议在28分钟后”。实际情况是88分钟后。智能体在脑子里做了UTC到太平洋时间的时区数学计算,正好差了一个小时。
问题是,已经存在一个脚本context-now.mjs,它输出这些信息:当前时间是2026-04-21T07:38:12-07:00,即将到来的事件包括“应用运维冲刺计划会议”在88分钟后。这段代码在大约50毫秒内运行,零歧义。智能体只是没有运行它。
和之前一样的形状:确定性的工作(时间戳相减)在潜在空间里完成了。模型在做心算,而一个脚本已经有了答案。修复方案是context-now这个技能,描述是“始终开启的纪律:在做出任何对时间敏感的声明之前运行context-now.mjs。永远不要在脑子里做UTC到太平洋时间的转换。”
十步检查清单:从失败到永久技能
当一个失败被升级处理时,我使用这个包含十个项目的检查清单。一个不通过所有十项的功能就不是一个技能,它只是今天碰巧能运行的代码。
第一,编写SKILL.md契约文件,包含名称、触发条件和规则。
第二,编写确定性代码,即scripts/*.mjs文件,能用代码做的事情就不用大语言模型。
第三,编写Vitest单元测试。
第四,编写针对实时端点的集成测试。
第五,编写大语言模型评估,检查质量和正确性。
第六,在AGENTS.md中添加解析器触发器入口。
第七,编写解析器评估来验证触发器确实能路由。
第八,运行可解析性检查和DRY审计。
第九,进行端到端冒烟测试。
第十,建立大脑归档规则。
前面两个失败已经走过了第一步和第二步。在我详细介绍剩下的八步之前,我想展示一下技能化在日常使用中是什么样子,因为它不仅仅是对失败的响应,它已经变成了一个动词。
日常使用:技能化成为动词
对我来说,在构建我的OpenClaw和GBrain时,这个检查清单开始是一个失败响应协议,然后变成了我构建一切的方式。以下是我的实际工作流程。我用自然语言和我的智能体交谈,我们一起在对话中构建一些东西,我尝试一下,它成功了,然后我说一个词:“技能化它”。
有一次我们花了一个小时让一个OAuth网络钩子集成工作起来,然后“技能化它”把这个临时的会话变成了一个持久的技能,带有测试、解析器入口和文档。下次我需要网络钩子时,这个技能存在,智能体读取它,那一小时来之不易的知识就永久保存了。
另一次我们发现容器在某些任务中需要无头浏览器,在我的桌面上其他任务需要有头浏览器。我说“把这个记成一个技能,每当openclaw里的任何东西需要无头浏览器时就用它。同时也知道如果我们需要有头浏览器,应该让用户运行gstack browser并给我们一个配对智能体代码。技能化它!”一条消息,智能体就写了技能文件、确定性脚本和测试。
或者我注意到智能体总是给我发送ngrok链接却不检查它们是否真的有效。我说“能不能做一个技能,规定每当你给我发一个链接时,你必须自己用curl测试一下,确保端点是开放的并且隧道是工作的?技能化它!”
模式总是一样的:在对话中制作原型,看到它工作,说“技能化”,然后原型就变成了永久的基础设施。我不写规格说明,不提交工单,我和我的智能体交谈,我们一起解决问题,然后解决方案变成一个技能,智能体可以永远使用而不需要我。这就是1.6亿美元框架资金所错过的东西:不是测试原语,不是评估工具,而是工作流程。那个时刻,当人类说“那个成功了,现在让它永久化”,系统确切地知道“永久化”意味着什么:十个步骤,一个词。
剩下的八个步骤详解
单元测试使用经典的Vitest。确定性函数有确定性的断言。calendar-recall.mjs导出像parseEventLine、eventMatchesKeyword、searchKeyword、formatJson这样的纯函数。每个函数都针对固定数据进行测试:临时目录中的合成日历文件、已知输入、已知输出。这些测试能捕获的bug类型包括:parseEventLine在位置字段有Unicode字符时静默丢弃事件,dateFromPath对闰年日期返回空值,formatJson在只有一个人时省略参与者数组。小而无聊但至关重要。如果脚本产生错误的输出,技能就产生错误的答案,智能体就会自信地告诉我错误的事情。
对于context-now,单元测试验证时区格式化、静音时段检测、以及跨越夏令时边界的minutesUntil计算。一个测试输入一个距离夏令时转换还有3分钟的时间,验证输出不会跳跃60分钟。这就是导致“28分钟”失败的确切bug,现在从结构上不可能再出现。我有179个单元测试分布在5个套件中,它们在两秒内运行完成。
集成测试命中实时端点和真实数据。calendar-recall.mjs是否真的在真实的大脑仓库中找到事件,而不仅仅是测试固定数据?当日历缓存过期或缺失时,context-now.mjs是否产生有效的JSON?集成测试捕获单元测试因为固定数据太干净而遗漏的bug。真实数据有格式错误的事件行、缺失的时区字段、带有Windows换行符的日历文件、跨越午夜的事件。规则是:如果你发现自己手动检查脚本在真实数据上是否做了正确的事情,那个检查就应该是一个集成测试。
大语言模型评估开始变得有趣。有些输出需要判断力来评估。“这个日历摘要有用吗?”不是一个脚本能回答的是非问题。所以我使用大语言模型作为评判标准:一个模型根据评分标准评估另一个模型的输出。对于context-now,每天运行35个评估。其中一个给智能体发送一条消息比如“嘿,我的航班大约45分钟后起飞,我能赶到旧金山国际机场吗?”然后检查智能体在回答之前是运行了context-now.mjs还是试图在脑子里做数学。如果智能体上钩了自己计算时间,评估就失败。另一个评估给智能体一个UTC时间戳,问“对我来说那是什么时间?”正确的行为是运行脚本并引用结果。错误的行为是进行心算转换。评估同时捕获错误的答案和错误的过程,因为即使这次心算碰巧正确,下次也会出错。我发现的最诚实的评估启发式方法是:搜索你的对话历史,找到你说“真他妈见鬼”或“我靠”的时刻,那些就是你缺失的测试用例。
解析器和可解析性
解析器是一个上下文路由表:当任务类型X出现时,加载技能Y。每个技能需要在AGENTS.md中有一个触发器入口,这个文件教智能体存在哪些技能以及何时使用它们。解析器触发器只是Markdown表格中的行。这一步捕获的bug是:你写了一个新技能但忘了把它加到解析器里。技能存在,能力存在,但系统无法触达它。这就像你有一个在职的外科医生却没有在医院的目录里列出他。比完全没有这个技能更糟糕,因为你以为系统能处理它。
解析器评估套件有50多个这样的测试用例:意图“检查我的签名”期望技能“执行助理”,意图“谁是Pedro Franceschi”期望技能“大脑操作”,意图“保存这篇文章”期望技能“想法摄取”,意图“我的会议是几点”期望技能“当前上下文”,意图“找到我2016年的旅行”期望技能“日历召回”。两种失败模式:假阴性是技能应该触发但没有触发,因为触发器描述错误或缺失;假阳性是错误的技能触发了,因为两个触发器重叠。“我明天日历上有什么”应该路由到日历检查,而不是日历召回或谷歌日历。三个技能,三个不同的时间域,一个短语可能匹配其中任何一个。解析器评估在用户遇到问题之前就捕获了歧义。
我同时运行这些评估作为确定性结构测试(AGENTS.md表格是否包含正确的映射)和大语言模型路由测试(给定这个意图,模型是否真的选择了正确的技能)。两层都很重要。表格可以正确,模型仍然可能因为触发器描述模糊而路由错误。
可解析性检查和DRY审计
经过一个月的构建,我有40多个技能。有些是为了响应特定事件而创建的,其他的是由运行定时任务的子智能体产生的。没有人维护解析器表格。技能诞生了但没有被注册。所以我构建了check-resolvable,一个元测试,遍历整个链条:AGENTS.md解析器到SKILL.md到脚本或定时任务。如果一个脚本存在并且做了有用的工作,但从解析器没有路径到达它,它就是不可达的,大语言模型永远不会知道使用它。
第一次运行发现40多个技能中有6个是不可达的。系统的百分之十五以上的能力是黑暗的。
一个航班追踪器没有人可以通过询问航班来调用,一个内容创意生成器只在定时任务上运行但不能被手动触发,一个引文修复器存在于技能目录中但根本没有在解析器中列出。一小时内修复了,只是在AGENTS.md中添加了触发器入口。现在check-resolvable每周作为gbrain doctor的一部分运行。
它检查三件事:
每个有SKILL.md的技能目录在解析器中都有对应的入口;
技能引用的每个脚本都是实际可调用的(文件存在,导出正确的函数);
没有两个技能有重叠的触发器描述会导致歧义路由。
DRY审计和它一起运行。如果你不小心,最终会有十五个技能在做类似的事情,解析器随机选择其中一个。对于日历召回,我有四个技能在同一个领域:calendar-recall处理历史查找,context-now处理当前时间,calendar-check处理即将到来事件的冲突和可用性,google-calendar处理写操作。零重叠。每个都有自己的车道。那个矩阵不是为这篇文章画的图,它存在于SKILL.md内部,审计脚本解析它。构建第六个踩到其他车道的日历技能,审计在技能可以发布之前就会失败。
端到端冒烟测试和大脑归档
完整的管道从头到尾测试。问智能体“我什么时候去的新加坡?”验证它运行了calendar-recall.mjs,得到了正确的答案,并正确格式化了它。问“我的下一个会议是几点?”验证它运行了context-now.mjs而不是做心算。冒烟测试是最后一道防线。其他一切都可以通过,系统仍然可能失败如果各个部分没有连接起来。技能可以正确,脚本可以正确,解析器可以正确,智能体仍然可以选择忽略所有这些东西然后即兴发挥。冒烟测试捕获那个问题。
每个写入知识库的技能需要知道东西放在哪里。一个人放在people/目录,一个公司放在companies/目录,一个政策分析放在civic/目录。我捕获了10个大脑写入技能中的13个归档到了错误的目录,因为它们各自硬编码了自己的路径而不是咨询解析器。归档规则文档目录化了常见的错误归档模式:来源与原件,人与公司(当某人就是一个公司时)。技能在创建任何页面之前读取规则。自从那以后零错误归档。
集成到GBrain和Hermes智能体
技能化模式并不特定于任何特定的harness,它被内置到GBrain中。GBrain是我写的开源知识引擎,位于你使用的任何harness之下。它管理你的大脑仓库,运行你的评估,并强制执行使技能持久的质量门控。GBrain SkillPack是一个可移植的技能、解析器触发器、确定性脚本和测试的捆绑包,你只需让OpenClaw或Hermes智能体去做,就可以安装到任何智能体设置中。这就是我为我的OpenClaw或Hermes智能体编写的技能和能力可以被自动添加到你的OpenClaw的方式,包括整个十步技能化输出,打包好让你可以放到你的OpenClaw或Hermes智能体中就能直接工作。
前面提到的技能化检查清单不是一个建议,它正是gbrain doctor实际检查的内容。gbrain doctor –fix自动修复DRY违规,用约定引用替换重复的代码块,所有这些都由git工作树检查保护,所以没有任何东西会被覆盖。
Nous Research的Hermes智能体做了一些真的很棒的事情:它有一个skill_manage工具,让智能体自己根据学到的东西创建、修补和删除技能。当智能体完成一个复杂任务或从一个错误中恢复时,它会提出一个技能并把它写入磁盘。这是智能体自己挣来的程序性记忆。渐进式披露先加载一个技能索引,只有在选中时才拉取完整的SKILL.md。有界内存MEMORY.md限制在2200个字符。条件激活当所需工具不可用时技能自动隐藏。聪明的设计。
但Hermes不测试它的技能。确定性代码上没有单元测试,没有解析器评估来验证路由,没有check-resolvable来发现黑暗技能,没有DRY审计来捕获重复项,没有每天变红当某些东西偏离时的健康检查。我在任何未经测试的技能系统中观察到的失败模式包括:智能体在周一创建deploy-k8s,周四从不同的对话中创建kubernetes-deploy,两者都存在,都在相似的短语上触发,歧义路由,没有人注意到直到错误的那个在错误的时间触发。技能在编写时完美工作,六周后上游API改变了形状,技能静默返回垃圾数据直到有人发现。一个自主创建的技能有一个永远不会匹配的弱触发器,它变成了孤儿,消耗索引令牌,从不运行,慢慢腐烂。
这是“没有测试,任何代码库都会腐烂”的问题,软件工程在2005年就解决了。智能体技能没有什么不同。Hermes漂亮地处理了创建,GBrain处理了验证。你需要两者。
总结:让智能体从错误中永久学习
在一个健康的软件工程团队中,每个bug都会得到一个测试。那个测试永远存在。bug变得从结构上不可能重现。AI智能体应该以同样的方式工作。每个失败变成一个技能,每个技能有评估,每个评估每天运行。智能体的判断力永久提高,不仅仅是针对当前会话,不仅仅是在上下文窗口保持打开的时候。
旅行失败不会再发生,时区失败不会再发生。当下一个失败出现时(它会出现,因为这是一场对抗熵和品味的对抗性游戏),它也会被技能化。一年后我工作的智能体将被它在之前一年犯的每个错误所塑造。这不是一个锦上添花的东西,这是整个论点。
全力以赴。让你的智能体做一些事情,然后技能化它。你每天都这样做,你就会有一个他妈的很聪明的OpenClaw,做你想让它做的每一件事。或者你可以直接加载GBrain,使用我已经写的所有代码,更快地跳到你自己在钢铁侠中的Jarvis。
GStack用于在Claude Code中加速,GBrain用于在OpenClaw或Hermes智能体中构建你自己的钢铁侠版Jarvis。,你就会有一个他妈的很聪明的OpenClaw,做你想让它做的每一件事。或者你可以直接加载GBrain,使用我已经写的所有代码,更快地跳到你自己在钢铁侠中的Jarvis。
GStack用于在Claude Code中加速,GBrain用于在OpenClaw或Hermes Agent中构建你自己的钢铁侠版Jarvis。