设计测试用例
一个测试用例包含三个部分:- Prompt:真实的用户消息,也就是用户真的可能会输入的那种内容。
- 期望输出:用人类可读的方式描述“成功长什么样”。
- 输入文件(可选):skill 完成任务时需要用到的文件。
evals/evals.json 中:
evals/evals.json
- 先从 2 到 3 个测试用例开始。 在看到第一轮结果之前,不要一开始就投入过多。后面再扩充也不迟。
- 让 prompt 有变化。 使用不同措辞、不同细节程度和不同正式程度。有些 prompt 可以更口语化(“hey can you clean up this csv”),有些则更精确(“Parse the CSV at data/input.csv, drop rows where column B is null, and write the result to data/output.csv”)。
- 覆盖边界场景。 至少加入一个用于测试边界条件的 prompt,比如格式错误的输入、非常规请求,或者 skill 指令可能产生歧义的情况。
- 使用真实上下文。 真实用户会提到文件路径、列名和个人上下文。像 “process this data” 这样的 prompt 太模糊,测不出什么有效信息。
运行 eval
核心模式是:每个测试用例都运行两次,一次 使用 skill,一次 不使用 skill(或者使用 skill 的旧版本)。这样你就有了一个可以对比的 baseline。工作区结构
把 eval 结果组织在 skill 目录旁边的一个工作区目录里。完整跑完一轮 eval 循环就对应一个iteration-N/ 目录。在该目录下,每个测试用例都有自己的 eval 目录,目录中再分成 with_skill/ 和 without_skill/ 两个子目录:
evals/evals.json。其他 JSON 文件(grading.json、timing.json、benchmark.json)则是在 eval 过程中生成的,可以由 agent、脚本,或者你自己来产出。
发起运行
每次 eval 运行都应该从干净的上下文开始,不能带着上一次运行残留的状态,也不要带着开发 skill 时留下的上下文。这样才能确保 agent 只依据SKILL.md 中的内容执行。在支持 subagent 的环境里(例如 Claude Code),这种隔离是天然成立的:每个子任务都会从新的上下文启动。如果没有 subagent,就为每次运行单独开一个会话。
每次运行都要提供以下信息:
- skill 路径(baseline 则不提供 skill)
- 测试 prompt
- 任何输入文件
- 输出目录
without_skill/outputs/。
如果你是在改进一个已经存在的 skill,可以把旧版本当作 baseline。编辑前先做一个快照(cp -r <skill-path> <workspace>/skill-snapshot/),让 baseline 运行指向这个快照,并把结果保存到 old_skill/outputs/,而不是 without_skill/。
记录耗时数据
耗时数据可以帮助你比较:skill 相比 baseline 多花了多少时间、消耗了多少 token。一个大幅提升输出质量但让 token 使用量翻三倍的 skill,和一个既更好又更便宜的 skill,是完全不同的取舍。每次运行完成后,记录 token 数和持续时间:timing.json
编写断言
断言是关于输出“应该包含什么”或“应该达成什么”的可验证陈述。通常在你看过第一轮输出后再补断言会更合适,因为在 skill 真正跑起来之前,你往往并不知道“好的结果”具体是什么样。 好的断言示例:"The output file is valid JSON":可以通过程序验证。"The bar chart has labeled axes":具体且可观察。"The report includes at least 3 recommendations":可以计数。
"The output is good":太模糊,无法评分。"The output uses exactly the phrase 'Total Revenue: $X'":太脆弱了,哪怕输出是正确的,只是换了种表达也会失败。
evals/evals.json 中的每个测试用例:
evals/evals.json
评分输出
评分意味着:针对每条断言,对照实际输出判断其 PASS 还是 FAIL,并记录具体证据。证据应该引用或指向输出内容,而不是只写一个主观意见。 最简单的做法是把输出结果和断言一起交给 LLM,让它逐条评估。对于那些可以靠代码检查的断言(例如 JSON 是否合法、行数是否正确、文件是否存在且尺寸符合预期),则更适合用验证脚本,因为脚本在这种机械性检查上比 LLM 判断更可靠,也更容易在多轮迭代中复用。grading.json
评分原则
- PASS 必须有具体证据。 不要“差不多就算过”。如果断言写的是“includes a summary”,而输出里只有一个标题叫 “Summary”、内容却只有一句空泛的话,那也应该算 FAIL。标签有了,不代表实质满足了。
- 评分时也要顺便审视断言本身。 在评分过程中,注意哪些断言太容易(不管 skill 质量如何都会通过)、太难(即使输出已经不错也总失败)、或者根本不可验证(无法仅凭输出本身检查)。这些问题都应该在下一轮迭代里修正。
汇总结果
当这一轮中的每次运行都完成评分后,按配置计算汇总统计,并把结果保存到 eval 目录旁边的benchmark.json 中(例如 csv-analyzer-workspace/iteration-1/benchmark.json):
benchmark.json
delta 反映的是这个 skill 的“成本”和“收益”:它多花了多少时间、多消耗了多少 token,又换来了多少通过率提升。一个多花 13 秒、但通过率提升 50 个百分点的 skill,大概率是值得的;一个 token 消耗翻倍、却只提升 2 个百分点的 skill,可能就不值。
标准差(
stddev)只有在同一个 eval 进行多次运行时才有实际意义。在早期迭代里,如果你只有 2 到 3 个测试用例、每个用例只跑一次,就先关注原始通过数量和 delta 即可。等测试集扩大、每个 eval 也开始重复多次时,这些统计指标才会真正变得有用。分析模式
汇总统计可能会掩盖一些重要模式。完成 benchmark 之后,重点看这些点:- 移除或替换在两种配置下都总是通过的断言。 这类断言不能提供有效信息,说明模型即使不依赖 skill 也能轻松完成。它们会抬高 with-skill 的通过率,却不能反映 skill 的真实价值。
- 调查在两种配置下都总是失败的断言。 要么断言本身有问题(要求了模型做不到的事情),要么测试用例太难,要么断言检查的方向不对。先把这些修正掉,再进入下一轮。
- 重点研究“使用 skill 时通过、不使用 skill 时失败”的断言。 这正是 skill 明确带来价值的地方。理解 为什么 会这样,是哪条指令、哪个脚本真正起了作用?
- 如果结果在多次运行之间不稳定,就收紧指令。 如果同一个 eval 有时通过、有时失败(在 benchmark 中体现为较高的
stddev),要么这个 eval 本身不稳定,容易受模型随机性影响;要么 skill 的指令存在歧义,导致模型每次理解都不一样。可以通过增加示例或补更具体的指导来减少歧义。 - 检查时间和 token 的异常值。 如果某个 eval 比其他 eval 慢 3 倍,就去读它的执行转录(也就是模型在运行过程中做了什么的完整日志),找到瓶颈在哪里。
由人工复核结果
断言评分和模式分析已经能发现很多问题,但它们只能检查“你事先想到要写成断言的内容”。人工评审能带来一层新的视角,比如发现你没有预料到的问题、注意到输出虽然技术上正确却没有真正解决需求,或者识别那些很难写成通过/失败检查的问题。对于每个测试用例,都要把实际输出和评分结果一起看一遍。 把每个测试用例的具体反馈记录下来,并保存到工作区中(例如作为 eval 目录旁边的feedback.json):
feedback.json
迭代 skill
在评分和复核之后,你会拿到三类信号:- 失败的断言 指向具体缺口,比如漏了一步、指令不清楚,或者 skill 没有覆盖某类情况。
- 人工反馈 指向更宽泛的质量问题,比如方法错了、输出结构差,或者 skill 给出了技术上正确但实际上没帮助的结果。
- 执行转录 揭示了问题 为什么 会发生。如果 agent 忽略了某条指令,说明那条指令可能有歧义;如果 agent 把时间花在无效步骤上,说明对应指令可能需要简化或删除。
SKILL.md 一起交给 LLM,让它提出修改建议。LLM 可以综合失败断言、评审意见和执行行为中的模式,找出那些手工串联起来会很费劲的问题。向 LLM 提示时,可以附带这些指导原则:
- 从反馈中抽象出通用问题。 skill 最终会面对许多不同 prompt,而不只是当前这几个测试用例。修改应该解决底层问题,而不是只给单个示例打补丁。
- 让 skill 保持精简。 更少但更高质量的指令,往往比一大堆规则效果更好。如果转录显示存在大量无效工作(没必要的校验、没必要的中间输出),就删掉这些指令。如果通过率在不断加规则后依然停滞不前,说明这个 skill 可能被限制得太死了,可以试着删一些规则,看结果是否保持甚至变好。
- 解释“为什么”。 基于原因的指令(“做 X,因为 Y 往往会导致 Z”)通常比僵硬的命令式指令(“ALWAYS do X, NEVER do Y”)效果更好。模型在理解目的时,执行会更稳定。
- 把重复工作打包起来。 如果每次测试运行都会独立写出一个相似的辅助脚本(比如画图脚本、数据解析脚本),那就是一个信号:应该把这个脚本收进 skill 的
scripts/目录中。关于这件事的做法,可以继续看 Using scripts。
迭代循环
- 把 eval 信号和当前的
SKILL.md一起交给 LLM,让它提出改进建议。 - 审查并应用这些改动。
- 在新的
iteration-<N+1>/目录里重新运行全部测试用例。 - 对新结果评分并做汇总。
- 再做一次人工复核。重复这个过程。