skill 评估
如何使用基于评估的迭代来测试你的技能是否能产生良好的输出。
你编写了一个技能,在一个提示上尝试了一下,似乎有效。但它在各种不同的提示下、在边缘情况下,是否都能可靠地工作?是否比没有技能时更好?运行结构化评估(evals)可以回答这些问题,并为你提供一个系统性地改进技能的反馈循环。
1. 设计测试用例
一个测试用例包含三个部分:
- 提示:一条真实的用户消息——类似用户实际会输入的内容。
- 预期输出:对成功结果的人工可读描述。
- 输入文件(可选):技能需要处理的文件。
将测试用例存储在技能目录内的 evals/evals.json 中:
{ "skill_name": "csv-analyzer", "evals": [ { "id": 1, "prompt": "我有一份月度销售数据的 CSV 文件,在 data/sales_2025.csv。你能找出收入最高的 3 个月并生成一个柱状图吗?", "expected_output": "一张柱状图图片,显示收入最高的 3 个月,带有标注的坐标轴和数值。", "files": ["evals/files/sales_2025.csv"] }, { "id": 2, "prompt": "我的下载文件夹里有个叫 customers.csv 的 CSV 文件,有些行缺少邮箱——你能清理一下并告诉我有多少行缺失吗?", "expected_output": "一份处理了缺失邮箱的清理后 CSV,以及缺失数量的计数。", "files": ["evals/files/customers.csv"] } ]}编写良好测试提示的技巧:
- 从 2-3 个测试用例开始。 在看到第一轮结果之前不要过度投入。后续可以扩充测试集。
- 变化提示风格。 使用不同的措辞、详细程度和正式程度。一些提示应该随意(“嘿,能清理一下这个 csv 吗”),另一些则应精确(“解析 data/input.csv 中的 CSV,删除 B 列为空的行,并将结果写入 data/output.csv”)。
- 覆盖边缘情况。 至少包含一个测试边界条件的提示——格式错误的输入、不寻常的请求,或技能指令可能存在歧义的情况。
- 使用真实的上下文。 真实用户会提及文件路径、列名和个人背景。像 “处理这些数据” 这样的提示过于模糊,无法测试任何有用的东西。
暂时不要担心定义具体的通过/失败检查——只需提供提示和预期输出。你将在看到第一轮运行产生的结果后,再添加详细的检查(称为 assertions)。
2. 运行评估
核心模式是每个测试用例运行两次:一次携带技能,一次不带技能(或使用先前版本)。这样你就有了一个可对比的基准。
2.1 工作区结构
在技能目录旁边的工作区目录中组织评估结果。每完成一轮完整的评估循环,都获得自己的 iteration-N/ 目录。在该目录内,每个测试用例都有一个评估目录,内含 with_skill/ 和 without_skill/ 子目录:
csv-analyzer/├── SKILL.md└── evals/ └── evals.jsoncsv-analyzer-workspace/└── iteration-1/ ├── eval-top-months-chart/ │ ├── with_skill/ │ │ ├── outputs/ # 运行产生的文件 │ │ ├── timing.json # Token 数和耗时 │ │ └── grading.json # 断言结果 │ └── without_skill/ │ ├── outputs/ │ ├── timing.json │ └── grading.json ├── eval-clean-missing-emails/ │ ├── with_skill/ │ │ ├── outputs/ │ │ ├── timing.json │ │ └── grading.json │ └── without_skill/ │ ├── outputs/ │ ├── timing.json │ └── grading.json └── benchmark.json # 聚合统计你手工编写的主要文件是 evals/evals.json。其他 JSON 文件(grading.json、timing.json、benchmark.json)是在评估过程中生成的——由代理、脚本或你本人生成。
2.2 启动运行
每次评估运行都应从干净上下文开始——不保留之前运行或技能开发过程中的任何残留状态。这能确保代理只遵循 SKILL.md 告诉它的内容。在支持子代理的环境(例如 Claude Code)中,这种隔离是天然的:每个子任务都从全新状态开始。如果没有子代理,可以为每次运行使用单独的会话。
对于每次运行,提供:
- 技能路径(基线运行则不提供)
- 测试提示
- 任何输入文件
- 输出目录
以下是针对单次带技能运行,你需要提供给代理的指令示例:
执行此任务:- 技能路径:/path/to/csv-analyzer- 任务:我有一份月度销售数据的 CSV 文件,在 data/sales_2025.csv。 你能找出收入最高的 3 个月并生成一个柱状图吗?- 输入文件:evals/files/sales_2025.csv- 将输出保存到:csv-analyzer-workspace/iteration-1/eval-top-months-chart/with_skill/outputs/对于基线,使用相同的提示但不提供技能路径,并保存到 without_skill/outputs/。
当改进现有技能时,使用先前版本作为基线。在编辑之前对其进行快照(cp -r <skill-path> <workspace>/skill-snapshot/),将基线运行指向快照,并保存到 old_skill/outputs/ 而不是 without_skill/。
2.3 捕获计时数据
计时数据让你能对比技能相对于基线消耗的时间和 token 数——一个能显著提升输出质量但使 token 使用量增加两倍的技能,与一个既更好又更便宜的技能,是不同性质的取舍。每次运行完成后,记录 token 数和耗时:
{ "total_tokens": 84852, "duration_ms": 23332}在 Claude Code 中,当子代理任务完成时,任务完成通知会包含 total_tokens 和 duration_ms。请立即保存这些值——它们不会在其他地方持久化。
3. 编写断言
断言是关于输出应包含或达成哪些内容的可验证陈述。在你看完第一轮输出后再添加它们——你通常需要在技能运行之后才知道“好”是什么样子。
好的断言:
"输出文件是有效的 JSON"—— 可通过编程验证。"柱状图有标注的坐标轴"—— 具体且可观察。"报告包含至少 3 条建议"—— 可计数。
弱的断言:
"输出很好"—— 过于模糊,无法评分。"输出精确使用短语 'Total Revenue: $X'"—— 过于脆弱;措辞不同但正确的输出会失败。
并非所有东西都需要断言。某些质量属性——写作风格、视觉设计、输出是否“感觉对了”——难以分解为通过/失败检查。这些更适合在人工评审中发现。请将断言保留给可以客观检查的内容。
将断言添加到 evals/evals.json 的每个测试用例中:
{ "skill_name": "csv-analyzer", "evals": [ { "id": 1, "prompt": "我有一份月度销售数据的 CSV 文件,在 data/sales_2025.csv。你能找出收入最高的 3 个月并生成一个柱状图吗?", "expected_output": "一张柱状图图片,显示收入最高的 3 个月,带有标注的坐标轴和数值。", "files": ["evals/files/sales_2025.csv"], "assertions": [ "输出包含一张柱状图图片文件", "图表正好显示 3 个月", "两个坐标轴都有标签", "图表标题或说明提到了收入" ] } ]}4. 对输出进行评分
评分是指根据实际输出评估每个断言,并记录 通过(PASS) 或 失败(FAIL),附上具体证据。证据应引用或参考输出,而不仅仅是陈述观点。
最简单的方法是将输出和断言交给 LLM,让其逐条评估。对于可通过代码检查的断言(有效 JSON、正确的行数、存在符合预期尺寸的文件),应使用验证脚本——对于机械性检查,脚本比 LLM 判断更可靠,并且可跨迭代复用。
{ "assertion_results": [ { "text": "输出包含一张柱状图图片文件", "passed": true, "evidence": "在 outputs 目录中找到 chart.png(45KB)" }, { "text": "图表正好显示 3 个月", "passed": true, "evidence": "图表显示三月、七月和十一月的柱状条" }, { "text": "两个坐标轴都有标签", "passed": false, "evidence": "Y 轴标注了 'Revenue ($)',但 X 轴没有标签" }, { "text": "图表标题或说明提到了收入", "passed": true, "evidence": "图表标题为 'Top 3 Months by Revenue'" } ], "summary": { "passed": 3, "failed": 1, "total": 4, "pass_rate": 0.75 }}4.1 评分原则
- 通过必须有具体证据。 不要给予怀疑的好处。如果一个断言说“包含摘要”,而输出中有一个标题为“摘要”的部分但只有一句含糊的话,那就是 失败——标签存在,但实质内容不足。
- 不仅检查结果,也要检查断言本身。 在评分过程中,注意断言是否过于容易(无论技能质量如何总是通过)、过于困难(即使输出很好也总是失败)或无法验证(无法仅从输出中检查)。在下一轮迭代中修正这些问题。
对于比较两个技能版本,可以尝试盲测对比:将两份输出交给 LLM 评判者,但不透露哪个来自哪个版本。评判者根据自己设定的标准对整体质量——组织、格式、可用性、润色程度——打分,从而避免对哪个版本“应该”更好的偏见。这可以作为断言评分的补充:两份输出可能都通过了所有断言,但整体质量却存在显著差异。
5. 聚合结果
在迭代中的每次运行都完成评分后,计算每个配置的汇总统计,并将结果保存到 benchmark.json 中,放在评估目录旁边(例如 csv-analyzer-workspace/iteration-1/benchmark.json):
{ "run_summary": { "with_skill": { "pass_rate": { "mean": 0.83, "stddev": 0.06 }, "time_seconds": { "mean": 45.0, "stddev": 12.0 }, "tokens": { "mean": 3800, "stddev": 400 } }, "without_skill": { "pass_rate": { "mean": 0.33, "stddev": 0.10 }, "time_seconds": { "mean": 32.0, "stddev": 8.0 }, "tokens": { "mean": 2100, "stddev": 300 } }, "delta": { "pass_rate": 0.50, "time_seconds": 13.0, "tokens": 1700 } }}delta 告诉你技能的成本(更多时间、更多 token)和收益(更高的通过率)。一个增加 13 秒但将通过率提升 50 个百分点的技能,很可能是值得的。一个 token 使用量翻倍但仅带来 2 个百分点提升的技能,可能就不值得了。
标准差(stddev)只有在每个评估有多次运行时才有意义。在早期迭代中,如果只有 2-3 个测试用例且各运行一次,应关注原始通过计数和 delta——当你扩展测试集并对每个评估进行多次运行后,统计指标才变得有用。
6. 分析模式
聚合统计可能会掩盖重要的模式。在计算基准后:
- 删除或替换在两个配置中总是通过的断言。 这些断言不会告诉你任何有用信息——模型在没有技能的情况下也能很好地处理它们。它们抬高了带技能的通过率,却没有反映实际技能价值。
- 调查在两个配置中总是失败的断言。 要么断言本身有问题(要求模型无法做到的事情),要么测试用例太难,要么断言检查了错误的内容。在下一轮迭代前修复这些问题。
- 研究在带技能时通过、不带技能时失败的断言。 这些是技能明显增加价值的地方。理解为什么——哪些指令或脚本起到了作用?
- 当结果在不同运行之间不一致时,收紧指令。 如果同一个评估有时通过有时失败(在基准中体现为高
stddev),可能评估本身不稳定(对模型的随机性敏感),或者技能的指令可能过于模糊,导致模型每次理解不同。通过增加示例或更具体的指导来减少歧义。 - 检查时间和 token 异常值。 如果某个评估耗时是其他评估的 3 倍,阅读其执行记录(运行期间模型所做操作的完整日志)以找到瓶颈。
7. 人工评审结果
断言评分和模式分析能发现很多问题,但它们只能检查你想到要写断言的那些方面。人工评审者带来新的视角——发现你未预料到的问题,注意到输出在技术上正确但未切中要点,或发现难以用通过/失败检查表达的问题。对于每个测试用例,请评审实际输出及其评分。
为每个测试用例记录具体反馈,并保存到工作区中(例如,作为与评估目录并列的 feedback.json):
{ "eval-top-months-chart": "图表缺少坐标轴标签,且月份按字母顺序排列而非时间顺序。", "eval-clean-missing-emails": ""}“图表缺少坐标轴标签”是可操作的;“看起来不好”则不是。空反馈意味着输出看起来没问题——该测试用例通过了你的评审。在迭代步骤中,将改进重点放在你有具体意见的测试用例上。
8. 迭代技能
在完成评分和评审后,你拥有三个信号来源:
- 失败的断言指向具体的缺口——遗漏的步骤、不清晰的指令,或技能未处理的情况。
- 人工反馈指向更广泛的质量问题——方法错误、输出结构不佳,或技能产生了技术上正确但无用的结果。
- 执行记录揭示了为什么出错。如果代理忽略了一条指令,该指令可能模棱两可。如果代理在无效步骤上浪费时间,这些指令可能需要简化或删除。
将这些信号转化为技能改进的最有效方式,是同时将这三个来源以及当前的 SKILL.md 交给 LLM,并要求它提出修改建议。LLM 可以综合失败断言、评审意见和记录行为中的模式,而这些模式手动连接会很繁琐。在提示 LLM 时,请包含以下指导原则:
- 从反馈中泛化。 技能将在许多不同的提示下使用,而不仅仅是测试用例。修复应广泛解决根本问题,而不是为特定示例添加窄补丁。
- 保持技能精简。 更少、更好的指令往往胜过详尽的规则。如果记录显示有浪费的工作(不必要的验证、不需要的中间输出),请删除这些指令。如果尽管添加了更多规则,通过率仍停滞不前,可能技能约束过多了——尝试删除一些指令,看看结果是否保持不变或有所改善。
- 解释原因。 基于推理的指令(“做 X,因为 Y 往往导致 Z”)比刚性指令(“总是做 X,绝不做 Y”)效果更好。当模型理解目的时,它们会更可靠地遵循指令。
- 捆绑重复工作。 如果每次测试运行都独立编写了类似的辅助脚本(图表构建器、数据解析器),这说明应将脚本打包到技能的
scripts/目录中。参见 skill 脚本 了解具体做法。
8.1 循环
- 将评估信号和当前
SKILL.md交给 LLM,要求其提出改进建议。 - 评审并应用修改。
- 在新的
iteration-<N+1>/目录中重新运行所有测试用例。 - 对结果进行评分和聚合。
- 进行人工评审。重复。
当结果令你满意、反馈始终为空,或你不再看到迭代之间有意义的改进时,即可停止。
skill-creator 技能可以自动化该工作流的大部分——运行评估、评分断言、聚合基准,并呈现结果供人工评审。具体流程参见 skill-creator.
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!