读完这篇文章,你会知道应该如何测试上一篇文章中创建的 Skill。
没错,Skill 也是可以测试的!
同时,我们还会得到一个真正可以运行的脚本。以后每次修改 Skill,都可以用它重新检查一遍,确认原来的能力没有被破坏。
前面的文章,我们已经创建了一个可以工作的 Skill:meeting-notes-formatter。它可以把杂乱的会议记录,整理成一份结构清晰、方便团队分享的会议纪要。
不过,创建一个可以工作的 Skill,和真正信任它、使用它,并推荐给你的同事,是两回事。
一个 Skill 只成功运行一次,可能只是这次提示词刚好合适。只有它在不同输入和多次运行中,仍然能够满足同一套标准,才算真正成为了一套可靠的基础设施。
我们使用 Skill,本来就是为了提高一致性。而测试,就是我们获得这种一致性的方式。
所以,还是准备一杯咖啡吧。☕
接下来看看,怎样让这个 Skill 成为“可以真正信任、使用,并愿意推荐给同事”的 Skill。
为什么测试总是最容易被跳过
这种情况其实很常见:
我们写完 Skill,粘贴一段杂乱的会议记录,它很快生成了一份漂亮的摘要。看起来没有什么问题,于是就会觉得:这个 Skill 完成了。
但当你推荐给你的同事,一周之后,同事换了一种写法,Skill 可能根本没有触发;或者虽然触发了,却凭空添加了一个从来没有人确认过的行动项。甚至同样的输入,周一和周五得到的结果也完全不一样(稍微不一样是可以接受的,毕竟这就是 AI)。
这并不一定说明 Skill 已经坏了。更准确地说,它从一开始就没有经过测试。
测试一个 Skill,需要回答三个问题,而且三个都不能少:
- 该触发的时候,它能否正确触发? 不该触发时,又能否保持安静?
- 它的输出是否正确? 结构是否符合要求,有没有虚构信息?
- 它是否足够一致? 多次运行时,结果是否保持相同的结构?
少了其中任何一项,这个 Skill 都仍然带有很强的偶然性。
不过,在开始测试之前,我们还要先确定:这个 Skill 究竟需要测试到什么程度。
三种测试强度
并不是所有 Skill 都需要一套完整的测试系统。测试投入应该和它的重要程度相匹配。
- 手动测试:直接向 Agent 输入请求,观察它是否触发,以及最终输出是否正确。速度快,不需要额外准备,很适合第一次检查。
- 脚本测试:把测试用例自动化。以后每次修改 Skill,都可以重新运行相同的测试。这也是本文后面会用到的方式。
- 程序化评估:针对固定测试集系统运行完整的 Eval,通过具体宿主或模型平台提供的 API、运行记录和评分器,对结果进行衡量。
自己使用的 Skill,测试要求显然不需要和面向几千名用户发布的 Skill 一样。
可以先记住一个简单的判断方法:
个人 Skill,手动测试通常已经够用;团队 Skill,最好加入脚本;产品级 Skill,则需要程序化评估。
我们先从手动测试开始,再把后面那些重复工作交给脚本。
不过,在正式开始之前,还有一个很实用的经验,可以节省不少时间。
先解决一个真正困难的样本
真正擅长创建 Skill 的人,通常不会一开始就准备二十个测试用例。
他们会先拿出一个真正难处理的输入,例如能找到的最混乱的一份会议记录,然后不断修改 Skill,直到 Agent 可以正确处理它。接下来,再把这次有效的经验整理出来,扩展到更多类型的测试用例。
这种方式得到反馈的速度更快。解决一个真正困难的失败案例,通常比观察十个简单用例全部通过更有价值。
所以,可以先让一个很糟糕的样本稳定工作,然后再扩大测试覆盖。
下面继续使用 meeting-notes-formatter,依次完成三种测试。
测试一:它会在正确的时机出现吗
前两篇提到过,Agent 在决定是否加载一个 Skill 时,首先看到的是 Frontmatter 中的 name 和 description。其中,自动匹配主要依赖 description。
因此,触发测试本质上是在检查我们的 description:它是否覆盖了真实用户可能使用的表达,同时又没有把范围写得过于宽泛。
首先,需要准备两个列表。
应该触发: 真实用户可能输入的内容。
- “整理一下这些会议记录”
- “把电话会议的笔记清理一下”
- “根据下面的内容编写会议纪要”
- “总结一下我们刚才的会议”
- 直接粘贴一段杂乱的会议记录,不附带任何命令
不应该触发: 看起来有些接近,但实际没有关系的请求。
- “旧金山今天的天气怎么样?”
- “帮我写一个 Python 函数”
- “创建一份支出统计表”
第二个问题列表和第一个问题列表同样重要。一个几乎遇到什么请求都会触发的 Skill,和一个永远不会触发的 Skill 一样有问题。
这里可以先做一个快速检查,直接问 Agent:
“你会在什么情况下使用
meeting-notes-formatterSkill?”
Agent 的回答通常会重新解释一遍 description。如果它的回答根本没有覆盖“应该触发”列表中的表达,那么问题多半出在描述上,而不是 Agent 本身。
不过,这种方式只能检查 Agent 如何理解描述,不能代替真正的触发测试。正式测试时,仍然要把前面的正向和负向请求逐条输入,并观察 Skill 是否真的被调用。
能够正确触发只是第一步。接下来还要看看,它真正生成的内容是否正确。
测试二:输出正确吗
这时,我们在上一篇文章中定义的成功标准就派上用场了。
我们要求这个 Skill:
- 始终生成四个章节:参会者、已做出的决策、行动项、待解决问题;
- 不虚构原始记录中不存在的信息;
- 为每个行动项提供负责人,无法确认时标记为“未分配”;
- 最终结果比原始会议记录更短。
所谓功能测试,就是运行一个已知输入,然后按照这些规则检查输出。
例如,可以使用下面这段杂乱的会议记录:
“和 Sarah、Tom 讨论了产品发布。我们决定推迟到 3 月 15 日。Sarah 会在下周五之前完成新闻稿。Tom 需要和法务沟通,但还不知道什么时候完成。我们仍在讨论是否把移动端功能放进 v1。”
正确的输出应该包含全部四个章节,将 Sarah 和 Tom 列为参会者,记录 3 月 15 日这个决定,并把新闻稿的负责人分配给 Sarah。
还有一个非常关键的要求:不能凭空补充会议日期,因为原始记录中根本没有提供。
最后这一项很容易被忽略。
Skill 最常见的问题,往往不是少了一个章节,而是非常肯定地补出一条原文里不存在的信息。
豆包胡编乱造的情况还是很多的。我问过它一些关于 Compose 的问题,它甚至会捏造一些函数来糊弄我。
所以,测试集中至少应该加入一个“正确答案就是未说明”的用例。
前两项测试通过之后,还剩最后一项。它决定了我们得到的究竟是一个演示,还是一个可以真正使用的工具。
测试三:它每次都能工作吗
使用同一份输入,连续运行五次。
然后看看,五次输出是否都采用相同的结构。
如果第一次运行时生成了全部四个章节,第三次却丢掉了“待解决问题”,那么即使每次输出单独看起来都没有明显错误,这个 Skill 仍然不能算可靠。
这里检查的并不是完全相同的文字。具体措辞可以变化,我们关心的是相同的结果形状:章节相同、格式规则相同、缺失信息的处理方式相同。
如果结构在多次运行中发生漂移,通常说明指令还不够严格。可以把关键规则移动到更靠前的位置,并更明确地写出什么才算真正完成。
这也可以看作 Agent Harness 的一部分。当然,这不是 Agent Harness 的全部,但在 Agent 开发中,我们确实会花很多时间编写成功标准、反复运行样本并检查回归。
一致性本来就是 Skill 存在的意义。如果只能保留一组测试,就优先保留包含明确成功标准的重复回归测试,避免只验证“每次都一样”,却没有验证“每次都正确”。
不过,手动重复这些操作很快就会让人厌烦。下面把它们自动化。
使用一个脚本完成三种测试
手动测试很适合最开始的检查。
但 Skill 后面还会继续修改,我们也希望每次改动之后,可以在几秒钟内重新检查全部内容,而不是再次输入十几条请求。
因此,可以在 Skill 目录中加入一个小型但真正能够运行的测试 Harness。它会读取实际使用的 SKILL.md,而不是单独复制一份,然后完成前面的三种测试。
meeting-notes-formatter/
├── SKILL.md
├── references/
│ └── formatting-rules.md
├── assets/
│ └── meeting-template.md
└── tests/
└── test_skill.py ← 测试 Harness
完整脚本可以在这里获取:test_skill.py
这份脚本只使用 Python 标准库。设置 API Key 后即可运行:
export ANTHROPIC_API_KEY="sk-ant-..."
python tests/test_skill.py --mode all
这里使用的是基于 Anthropic API 编写的测试脚本。如果使用 Codex、OpenAI API 或其他 Agent 宿主,需要替换对应的模型调用与运行观测方式,但测试思路本身没有变化。
运行之后,会得到类似下面的结果:
=== 1. TRIGGERING ===
[PASS] (expected: trigger ) Format these meeting notes for me
[PASS] (expected: stay quiet ) What's the weather in San Francisco?
...
-> 9/9 trigger cases correct
=== 2. FUNCTIONAL ===
[PASS] happy_path_with_owners: required sections present
[PASS] happy_path_with_owners: no invented date
...
-> 6/6 functional checks passed
=== 3. CONSISTENCY (5 runs) ===
run 1: sections = ['Action Items', 'Attendees', 'Decisions', 'Open Questions']
...
-> PASS: all runs produced the same structure
==================================================
TOTAL: 16/16 checks passed
关于它的实现方式,注意几个地方:
- 触发测试 只把
description作为路由信息发送给模型,然后询问“这个 Skill 是否应该触发”。因此,它直接测试了自动匹配中最重要的那段文字; - 功能测试和一致性测试 会把
SKILL.md的正文作为指令,再使用普通代码,根据部分成功标准检查输出,例如章节是否完整、内容是否过长,以及在未提供日期时是否出现了 ISO 8601 格式的日期; - 脚本读取的是真实
SKILL.md,因此不会另外维护一份容易与实际 Skill 脱节的指令副本。
它并不是一套完整的生产级 Skill Eval,更不是所有平台通用的 Skills API。它只是一个可以在每次修改之后快速运行的测试 Harness。例如,它对“虚构日期”的检查主要依赖 YYYY-MM-DD 格式与少量“未说明”类关键词,仍可能漏掉“3 月 10 日”这类自然语言中的虚构日期;如果这是关键风险,还需要补充更严格的检查或人工复核。
同时,它的触发测试是在模拟路由逻辑,不能完全替代真实 Agent 宿主中的端到端测试。不同宿主对 Skill 的发现、上下文注入、工具调用和权限处理可能存在差异。
不过,它已经足够适合快速回归。测试用例只是脚本顶部的一些列表,后续扩展也很简单:增加一条表达,补充一个检查,然后重新运行。
测试失败时,它也会告诉我们应该调整什么地方。
怎样理解不同的失败结果
测试不是为了让我们看到一排绿色的 PASS,而是为了指出问题所在。
应该触发,却没有触发,也就是漏触发(undertriggering)。 通常应该先检查 description 是否太模糊,或者缺少真实用户会使用的表达。可以补充更具体的触发用语;对于技术类 Skill,还可以加入准确的关键词和文件类型。
不相关的请求也会触发,也就是误触发(overtriggering)。 这时可以先检查 description 的范围是否太宽。需要进一步明确任务范围,并说明哪些相邻场景不应该使用它。
能够正确触发,但输出错误或者不一致。 这时问题通常不在 description,而在具体指令。需要收紧操作步骤,把关键规则移动到更靠前的位置,并明确说明正确结果应该是什么样。
测试并不是为了第一次运行就全部通过。真正重要的是:一旦某个地方发生退化,我们可以在几秒钟内发现,并且知道应该从哪里开始检查。
Skill 本来就是一份会持续变化的文档。发布之后,真实输入会带来我们没有预料到的情况,我们也会重新回来完善指令。测试套件可以让这个循环变得更快,也更安全。
未完待续
现在,我们已经创建了一个 Skill,也初步验证了它可以正确触发、生成符合要求的结果,并在多次运行中保持结构一致。
接下来只剩最后一步:怎样把它从自己的目录中拿出来,交给其他人使用。
接下来我们会继续讨论:
- 怎样正确分享 Skill;
- 如何进行版本管理,避免更新破坏其他人的工作流;
- 怎样在团队中部署;
- 如何避开那些会在长期使用中悄悄降低 Skill 可靠性的问题。
实际上这个短系列最开始的问题,是我们总要在每次对话中重复相同的要求。现在,我们已经可以创建一套工作流,证明它足够可靠,并且有信心把它交给其他人。
这就是我们最终想做的事情。