评估 Skill 的产出质量
采用评估驱动迭代,淬炼 Skill 的产出质量
你写了一个 Skill,拿几个提示词试了试,好像能用。但问题在于:它在不同类型的输入下是否都稳定?遇到边界情况会不会出错?相比不用 Skill,真的更好吗?
通过做一套结构化的评估方案,可以把这些问题说清楚。同时,它还能形成一个持续反馈的机制,让你有依据地、一步步把这个 Skill 打磨得更可靠。
设计测试用例
一个测试用例通常包含三部分:
- 输入:一段真实用户可能会说的话,而不是理想化的指令
- 期望输出:用人能看懂的方式描述“什么算成功”
- 输入文件(可选):Skill 运行时需要用到的文件
你可以把这些测试用例放在 Skill 目录下的 evals/evals.json 文件中,例如:
{
"skill_name": "csv-analyzer",
"evals": [
{
"id": 1,
"prompt": "我有一份每月销售数据的 CSV 文件,路径是 data/sales_2025.csv。你能帮我找出收入最高的前三个月,并画一张柱状图吗?",
"expected_output": "生成一张柱状图,展示收入最高的前三个月,并标注清晰的坐标轴和数值。",
"files": ["evals/files/sales_2025.csv"]
},
{
"id": 2,
"prompt": "我下载文件夹里有个 customers.csv,有些行缺少邮箱,你能帮我清理一下,并告诉我缺了多少吗?",
"expected_output": "输出一个清理后的 CSV(处理好缺失邮箱),并给出缺失邮箱的数量统计。",
"files": ["evals/files/customers.csv"]
}
]
}写好测试用例的一些建议
- 先从 2~3 个用例开始:不要一开始就写很多。先跑一轮看看效果,再逐步补充。
- 让输入多样化:用不同的表达方式、不同的详细程度、不同语气。
- 比如既有随意的:“帮我把这个 csv 清理一下”,
- 也有严格的:“解析 data/input.csv,删除 B 列为空的行,并输出到 data/output.csv”。
- 覆盖边界情况:至少要有一个测试是“刁钻”的:比如输入格式不规范、需求不常见,或者指令本身有歧义。
- 贴近真实使用场景:真实用户会提到文件路径、字段名、上下文信息。
在设计测试简短先不用纠结怎么定义通过/失败的判断标准,只要把输入和期望结果写清楚就够了。
运行评估
核心思路很简单:每个测试用例都跑两次
- 一次带上 Skill
- 一次不带 Skill(或者用旧版本)
这样你就有了一个对照基线,可以清楚地看到 Skill 到底有没有带来提升
目录结构建议
评估结果建议单独放在一个 workspace 目录中,与 Skill 目录并列。
每完整跑一轮评估,就新建一个 iteration-N/ 目录;在这一轮里,每个测试用例都有自己的子目录,并且分成 with_skill/ 和 without_skill/ 两部分:
csv-analyzer/
├── SKILL.md
└── evals/
└── evals.json
csv-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(测试用例定义)
其他这些文件:
- grading.json
- timing.json
- benchmark.json
都是在评估过程中自动生成的
如何执行每一轮评估
每一次评估运行,都应该在一个干净的上下文里开始——不能带着之前运行残留的状态,也不能带着你在开发 Skill 过程中的“记忆”。
这样可以确保:Agent 完全按照 SKILL.md 的定义来执行,而不是被历史上下文干扰。
- 如果环境支持子代理(subagent,比如 Claude Code),这种隔离是天然存在的——每个子任务都是全新开始
- 如果不支持,就需要手动为每次运行创建一个独立会话
每次运行需要提供什么
一次标准的运行,需要明确给 Agent 这些信息:
- Skill 路径(对照组就不提供)
- 测试用的输入
- 输入文件(如果有)
- 输出保存目录
你可以这样给 Agent 下指令,让它加载 Skill,执行任务,并把结果保存到指定目录:
执行以下任务:
* Skill 路径:/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/对照组怎么跑
对照组的做法很简单:
- 用完全相同的输入
- 但不提供 Skill 路径
- 输出保存到
without_skill/outputs/
这样你就能直接对比:有 Skill 和没 Skill,到底差在哪。
如果是在迭代已有 Skill
如果你是在优化一个已经存在的 Skill,那么对照组就不是“没有 Skill”,而是“旧版本 Skill”。
做法是:
- 在修改前先做一个快照
cp -r <skill-path> <workspace>/skill-snapshot/-
对照组运行时,使用这个旧版本路径
-
输出目录改为:
old_skill/outputs/
这样你比较的就是:“新版本 Skill” vs “旧版本 Skill”,更能准确评估你的改动到底有没有提升。
评估“值不值”
除了评估输出的正确性,还建议记录每次运行的耗时和 token 消耗等数据,这些都是评估 Skill 性能的重要指标。
通过记录每次运行的耗时和 token 消耗,你可以回答一个关键问题:提升效果,值不值这个代价?
例如:
- 有的 Skill 明显提升了质量,但 token 用量翻了三倍
- 有的 Skill 不仅效果更好,成本反而更低 这两种情况,决策是完全不同的。
每次运行需要记录的数据
每次结束结束后,记录两个指标:
{
"total_tokens": 84852,
"duration_ms": 23332
}- total_tokens:本次运行消耗的总 token
- duration_ms:总耗时(毫秒)
编写断言
断言,就是一条条“验收标准”或“检查清单”。当你让 AI 做一件事时,你心里其实会有一些判断标准,比如:
- 结果对不对
- 有没有漏掉关键内容
- 格式是不是符合要求 断言,就是把这些“心里的标准”,明确写出来,变成可以检查的规则。
举个例子,你点了一杯咖啡,你的“断言”可能是:
- 是热的
- 是拿铁(不是美式)
- 有奶泡 这些就是你的验收标准。
只要有一条不满足,你就会觉得“不对”。
通常建议不要依赖就写“断言“,而是先跑一轮结果,再写断言。因为在没看到实际输出前,你可能并不清楚“什么才算好”。
什么是好的断言
好的断言有一个共同点:可以客观判断,对或错很明确
例如:
- “输出文件是合法的 JSON” → 可以程序校验
- “柱状图包含坐标轴标签” → 可以直接观察
- “报告至少包含 3 条建议” → 可以计数
什么是无效(或低质量)的断言
问题通常出在两个方向:太模糊,或太死板
例如:
- “输出很好” → 没法判断好在哪里
- “必须包含 ‘合计: ¥X’ 这句话” → 太脆弱,换个表达就误判失败
不是什么都要写断言
有些维度本来就不适合做“对/错判断”,比如:
- 写作风格
- 视觉设计
- 整体是否“感觉对”
这些更适合留给人工评审,而不是强行写成规则。只有那些具有客观标准的部分,才适合写断言。
如何写进测试用例
在 evals/evals.json 中,为每个测试用例加上 assertions 字段,例如:
{
"skill_name": "csv-analyzer",
"evals": [
{
"id": 1,
"prompt": "我有一个位于 data/sales_2025.csv 的月度销售数据 CSV 文件。你能找出按收入排序的前 3 个月,并生成一个柱状图吗?",
"expected_output": "一个柱状图图片,展示按收入排名的前 3 个月,并带有标注清晰的坐标轴和数值。",
"files": ["evals/files/sales_2025.csv"],
"assertions": [
"输出中包含一个柱状图文件",
"图表中恰好展示 3 个月的数据",
"两个坐标轴都有标签",
"图表标题或说明中提到了收入"
]
}
]
}打分
打分的过程,其实就是:拿你的“检查清单(断言)”,逐条去对照实际输出,看是通过还是不通过。
每一条断言都需要记录:
- 通过 / 不通过
- 证据:说明你为什么这么判断
注意:证据要引用实际输出内容,而不是主观评价。
怎么做打分
最简单的方式是:把“输出结果 + 断言”一起交给一个 LLM,让它逐条判断
但要注意,为避免“先入为主“的偏见,最好采用盲测对比的方式:
- 把两个输出一起给一个 LLM
- 不告诉它哪个是新版本,哪个是旧版本
- 让它从整体质量去评分(结构、可读性、完成度等)
除了让 LLM 打分,有一些情况更适合用代码来判断,比如:
- JSON 是否合法
- 行数是否正确
- 文件是否存在、大小是否符合预期
这些“机械性检查”,用脚本更稳定,而且可以复用
下面是一个打分结果的示例格式:
{
"assertion_results": [
{
"text": "输出中包含一个柱状图文件",
"passed": true,
"evidence": "在 outputs 目录中找到 chart.png(45KB)"
},
{
"text": "图表中恰好展示 3 个月的数据",
"passed": true,
"evidence": "图中包含 March、July、November 三个柱子"
},
{
"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
}
}打分时的几个原则
1. “通过“必须有“硬证据”
不要“差不多就算通过”。
例如:
- 断言是“包含总结”
- 结果里确实有个标题叫 Summary,但只有一句空话
种应该判“失败“,因为形式有了,但内容不达标
2. 顺便检查“断言本身”
打分的时候,不只是看结果,也要反过来看断言是否合理:
- 太容易:不管 Skill 好坏都能通过
- 太苛刻:结果已经不错了还是通不过
- 无法验证:从输出里根本看不出来
汇总结果
当一轮所有测试用例都完成打分后,需要做一件事:把零散结果汇总成整体结论。
具体就是:按不同配置(比如“有 Skill / 没 Skill”)计算一些关键指标,并保存到 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
}
}
}怎么理解这些数据?
可以分三层来看:
1. 各自表现
每一组都会统计:
- 通过率(pass_rate)
- 耗时(time_seconds)
- token 消耗(tokens)
并且给出:
- 平均值(mean)
- 波动范围(stddev,标准差)
2. 最关键的是差值(delta)
delta 本质是在回答一个问题: 这个 Skill,多花了多少成本,换来了多少收益?
比如上面的例子:
- 通过率 +50%(明显提升)
- 多花 13 秒
- 多用 1700 tokens
3. 如何做判断?
可以用一个很实际的标准:
- 小成本换大提升 → 值得
- 大成本换小提升 → 可能不值
例如:
- +13 秒换 +50% 通过率 → 很可能值得
- token 翻倍只提升 2% → 基本不划算
关于标准差
标准差衡量的大家离“平均水平”有多散,判断的是结果“稳不稳”,波动有多大。但如果你只是跑了 2~3 个测试用例,每个只跑一次,那标准差基本没什么意义
这时候更应该关注:
- 实际通过了多少条
- 差值(delta)是多少
当你:
- 测试用例变多
- 每个用例多跑几次后
这个时候才需要引入标准差。
分析问题模式
数据不是唯一需要关注的,如果我们只看整体统计(比如平均通过率)往往会掩盖很多关键问题。 所以在看完 benchmark 之后,需要进一步“拆开看”。
可以重点关注这几类情况:
1. 总是通过的断言
如果某条断言:
- 有 Skill 通过
- 没 Skill 也通过
说明它没有区分度,本身没什么价值
这种断言会“虚高”通过率,但并不能说明 Skill 真有提升。 建议直接删除或替换。
2. 总是失败的断言
如果某条断言:
- 有 Skill 也失败
- 没 Skill 也失败
通常有三种可能:
- 断言本身不合理(要求模型做不到的事)
- 测试用例太难
- 检查点写错了
这类断言需要修掉,否则后面的评估没有意义。
3. 有 Skill 才能通过的断言
这是最有价值的一类:
- 有 Skill → 通过
- 没 Skill → 失败
说明 Skill 确实带来了提升
关键是要进一步理解:
- 是哪一段指令起作用?
- 是哪个脚本/结构在发挥作用?
这部分是你优化 Skill 的核心依据。
4. 同一个测试结果不稳定
如果一个测试:
- 有时通过,有时失败
- benchmark 里表现为标准差(stddev)较高
可能的原因通常有两个:
- 测试本身“太随机”(对模型波动敏感)
- Skill 写得不够清晰,模型每次理解不一样
解决方式:
- 补充示例
- 把指令写得更具体,减少歧义
5. 异常的耗时或 token
如果某个测试:比其他测试慢很多(比如 3 倍),就需要重点排查
做法是:
- 查看完整执行日志
- 找出卡在哪里(比如重复推理、无效步骤等)
人工复核
无论是断言和统计,都有一个继局限:它们只能检查“你提前想到的东西”,而人可以发现:
- 没被覆盖的问题
- “看起来对,但其实不对”的结果
- 很难写成规则的问题(比如表达是否合理)
怎么做人工复核
对每个测试用例:
- 看实际输出
- 对照打分结果
- 记录具体反馈
可以存成一个 feedback.json,例如:
{
"eval-top-months-chart": "图表缺少坐标轴标签,而且月份是按字母排序的,不是按时间顺序。",
"eval-clean-missing-emails": ""
}什么是“好的反馈”
好的反馈应该是:
- 具体的
- 可以改的
例如: “缺少坐标轴标签” ,而“看起来不太好”就不够具体,无法指导改进。
如果这个测试结果通过了你的人工复核,就直接记录一个空字符串,表示没有问题。
迭代优化
在完成打分和复核之后,你手里其实有三类很有价值的信息:
1. 断言失败
它告诉你:哪里具体有问题
比如:
- 少了某一步
- 指令不清晰
- 某类情况没覆盖到
这是最“具体、可定位”的问题来源
2. 人工反馈
它更多反映的是:整体质量问题
比如:
- 方法不对
- 结构混乱
- 虽然 technically 正确,但没什么用
这些问题通常比较“抽象”,但很关键
3. 执行日志
它告诉你:为什么会出问题
比如:
- Agent 忽略了某条指令 → 说明指令可能有歧义
- 花了很多时间做无用步骤 → 说明流程设计有问题
这是理解“过程”的唯一依据
这时候,你可以把这三类信息 + 当前的 SKILL.md 一起交给 LLM,让它提出修改建议。
原因是:
- LLM 更容易发现跨多个测试用例的模式
- 人手动总结这些关联,会比较费力
给 LLM 提示时的几个原则
1. 不要只修个例,要解决“底层问题”
Skill 是要应对各种输入的,不只是当前这些测试用例。所以 优化要“泛化”,而不是针对某个用例打补丁
2. 保持精简(Lean)
规则不是越多越好,如果执行日志里出现:
- 多余的校验
- 无意义的中间步骤
就应该删掉
如果规则越加越多,但效果没提升:很可能已经“过度约束”了,反而限制了模型
3. 解释“为什么”,而不是死规定
相比: ❌ “必须这样做,绝不能那样做”
更有效的是: ✅ “这样做是因为那样会导致某种问题”
模型理解“原因”,执行会更稳定
4. 把重复工作沉淀下来
如果你发现:每次运行都在重复写类似的脚本(比如画图、解析数据)
说明这些应该:抽出来放进 scripts/ 目录,变成 Skill 的一部分
标准迭代流程
整个过程可以抽象成一个循环:
- 把评估结果 + 当前 Skill 交给 LLM,让它给出优化建议
- 人来审核并修改 Skill
- 在新的 iteration-N+1/ 目录中重新跑所有测试
- 重新打分并汇总
- 再做人工复核
- 重复这个过程
什么时候可以停
可以考虑停止迭代的几个信号:
- 结果已经达到你的预期
- 人工反馈基本为空(没有明显问题)
- 多轮迭代后,提升已经不明显