评估输出质量#
如何使用评测驱动(eval-driven)的迭代来测试你的技能是否能产出高质量的输出。
你编写了一个 Skill,在提示词上试了一下,看起来运行良好。但它是否能可靠地工作——在各种不同的提示词下、在边缘情况下,是否真的比没有 Skill 时表现更好?运行结构化的评估(Evals)可以回答这些问题,并为你提供系统性改进 Skill 的反馈闭环。
设计测试用例#
一个测试用例包含三个部分:
- 提示词 (Prompt):真实的普通用户消息——即用户实际会输入的内容。
- 预期输出 (Expected output):对成功结果的人类可读描述。
- 输入文件(可选):Skill 运行时需要配合使用的文件。
将测试用例存储在 Skill 目录下的 evals/evals.json 中:
{
"skill_name": "csv-analyzer",
"evals": [
{
"id": 1,
"prompt": "我有一份 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 指令可能产生歧义的情况。
- 使用真实的上下文。真实用户会提到文件路径、列名和个人背景。像“处理这个数据”这样模糊的提示词对于测试没有任何帮助。
现在不用担心如何定义具体的通过/失败标准——只需关注提示词和预期输出。在看到第一轮运行结果后,你再添加详细的检查项(称为断言)。
运行评估 (Evals)#
核心模式是将每个测试用例运行两次:一次 使用 Skill,一次 不使用 Skill(或使用之前的版本)。这为你提供了一个对比的基准。
工作区结构#
在 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。其他 JSON 文件(grading.json、timing.json、benchmark.json)是在评估过程中产生的——由 Agent、脚本或你自己生成。
启动运行#
每次评估运行都应该从一个干净的上下文开始——不应有之前运行或开发过程中留下的任何状态。这确保了 Agent 仅遵循 SKILL.md 中的说明。在支持子 Agent 的环境中(例如 Claude Code),这种隔离是天然存在的:每个子任务都会重新开始。如果没有子 Agent,请为每次运行使用一个独立的会话。
对于每次运行,你需要提供:
- Skill 路径(基准测试则不提供)
- 测试提示词
- 任何必要的输入文件
- 输出保存目录
以下是给 Agent 执行单个“使用 Skill”任务的指令示例:
执行此任务:
- Skill 路径: /path/to/csv-analyzer
- 任务内容: 我有一份 data/sales_2025.csv 的月度销售数据。你能帮我找出收入排名前三的月份并制作一张柱状图吗?
- 输入文件: evals/files/sales_2025.csv
- 保存输出至: csv-analyzer-workspace/iteration-1/eval-top-months-chart/with_skill/outputs/对于基准测试,使用相同的提示词但不提供 Skill 路径,保存到 without_skill/outputs/。
在优化现有 Skill 时,请将之前的版本作为基准。在编辑前对 Skill 进行快照(cp -r <skill-path> <workspace>/skill-snapshot/),将基准运行指向快照,并保存到 old_skill/outputs/ 而非 without_skill/。
采集时间与 Token 数据#
时间数据可以让你对比 Skill 相对于基准测试的成本——如果一个 Skill 极大地提高了输出质量但使 Token 消耗增加了两倍,这与一个既更好又更便宜的 Skill 是完全不同的权衡。每次运行完成后,记录 Token 数量和持续时间:
在 Claude Code 中,当子 Agent 任务完成时,任务完成通知 会包含
total_tokens和duration_ms。请立即保存这些值,因为它们不会被持久化在其他地方。
{
"total_tokens": 84852,
"duration_ms": 23332
}编写断言 (Assertions)#
断言是关于输出应该包含什么或达到什么效果的、可验证的陈述。在看到第一轮输出后再添加断言——通常在 Skill 运行之前,你可能并不清楚“好”具体是什么样的。
好的断言:
"输出文件是有效的 JSON"—— 可通过程序验证。"柱状图有轴标签"—— 具体且可观察。"报告至少包含 3 条建议"—— 可计数的。
不好的断言:
"输出很好"—— 太模糊,无法评分。"输出必须完全匹配短语 '总收入: $X'"—— 太死板;措辞稍有不同但正确的输出也会失败。
并不是所有内容都需要断言。有些特质——如写作风格、视觉设计、输出是否“感觉对味”——很难分解为通过/失败的检查。这些最好留在人工复核阶段。断言应保留给那些可以客观检查的内容。
在 evals/evals.json 的每个测试用例中添加断言:
{
"skill_name": "csv-analyzer",
"evals": [
{
"id": 1,
"prompt": "我有一份 data/sales_2025.csv 的月度销售数据。你能帮我找出收入排名前三的月份并制作一张柱状图吗?",
"expected_output": "一张显示收入前三月份的柱状图,带有清晰的轴标签和数值说明。",
"files": ["evals/files/sales_2025.csv"],
"assertions": [
"输出包含一个柱状图图片文件",
"图表恰好显示了 3 个月份",
"两条坐标轴都有标签",
"图表标题或说明提到了收入"
]
}
]
}对输出进行评分#
评分意味着针对实际输出评估每个断言,并记录 通过 (PASS) 或 失败 (FAIL),同时附带具体的证据。证据应该引用或参考输出内容,而不仅仅是表达观点。
最简单的方法是将输出和断言交给 LLM,让它评估每一项。对于可以通过代码检查的断言(例如:有效的 JSON、正确的行数、存在符合预期尺寸的文件),请使用验证脚本——对于机械性的检查,脚本比 LLM 的判断更可靠,且可以在迭代中重复使用。
{
"assertion_results": [
{
"text": "输出包含一个柱状图图片文件",
"passed": true,
"evidence": "在输出目录中找到了 chart.png (45KB)"
},
{
"text": "图表恰好显示了 3 个月份",
"passed": true,
"evidence": "图表显示了 3 月、7 月和 11 月的柱状条"
},
{
"text": "两条坐标轴都有标签",
"passed": false,
"evidence": "纵轴有 '收入 ($)' 标签,但横轴没有标签"
},
{
"text": "图表标题或说明提到了收入",
"passed": true,
"evidence": "图表标题为 '按收入计算的前 3 个月份'"
}
],
"summary": {
"passed": 3,
"failed": 1,
"total": 4,
"pass_rate": 0.75
}
}评分原则#
- 只有确凿证据才能给出“通过”。不要想当然。如果一个断言写着“包含摘要”,而输出只有一个标题叫“摘要”但内容模糊的一句话,那也是“失败”——标签在但实质内容缺失。
- 审视断言本身,而不仅仅是结果。在评分时,注意那些太容易(无论 Skill 质量如何都能通过)、太难(即使输出很好也会失败)或无法验证(无法仅凭输出内容检查)的断言。在下一次迭代中修正它们。
为了对比两个版本的 Skill,可以尝试 盲测 (Blind Comparison):将两份输出同时呈现给 LLM 裁判,但不告知哪份来自哪个版本。裁判根据自己的一套准则(组织、格式、可用性、精致程度等)进行综合评分,从而避免“哪个版本应该更好”的先入为主的偏见。这可以作为断言评分的补充:两份输出可能都通过了所有断言,但在整体质量上可能存在显著差异。
汇总结果#
当一轮迭代中的每次运行都评分完毕后,计算每个配置的总结统计数据,并保存到评估目录旁的 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.1 },
"time_seconds": { "mean": 32.0, "stddev": 8.0 },
"tokens": { "mean": 2100, "stddev": 300 }
},
"delta": {
"pass_rate": 0.5,
"time_seconds": 13.0,
"tokens": 1700
}
}
}delta 展示了 Skill 的成本(更多的时间、更多的 Token)和它的收益(更高的通过率)。一个增加了 13 秒但通过率提高了 50 个百分点的 Skill 可能是值得的;但一个为了 2 个百分点的提升而使 Token 消耗翻倍的 Skill 可能并不划算。
标准差 (
stddev) 只有在每次评估进行多次运行测算时才有意义。在仅有 2-3 个测试用例且每个用例只运行一次的早期迭代中,请关注原始通过数和差值 (Delta)——随着测试集的扩展及每个评估的多次运行,统计指标才会变得有用。
分析模式#
汇总统计数据可能会掩盖一些重要的模式。在计算完基准测试后:
- 移除或替换在两种配置下总是能通过的断言。由于 Agent 在没有 Skill 的情况下也能处理好这些内容,它们无法体现 Skill 的核心价值,反而会虚增通过率。
- 调查在两种配置下总是失败的断言。这通常意味着断言本身有问题(要求了模型做不到的事)、测试用例太难,或者断言检查点找错了。在进入下一轮迭代前修复这些问题。
- 研究那些在有 Skill 时通过但在没有时失败的断言。这是 Skill 真正发挥价值的地方。弄清楚 原因 ——是哪些具体的指令或脚本起到了作用?
- 当不同运行结果不一致时,收紧指令。如果同一个评估用例有时通过有时失败(表现为基准测试中的高
stddev),则可能是该评估太脆弱(受模型随机性影响大),或者是 Skill 的指令不够清晰,导致模型每次理解不同。添加示例或更具体的指引以消除模糊性。 - 检查时间或 Token 消耗的离群点。如果某个评估用例耗时是其他的 3 倍,阅读其执行日志(Agent 在运行过程中的完整记录)来找出瓶颈。
人工复核结果#
断言评分和模式分析能捕捉很多问题,但它们只能检查你想到要写的断言。人工复核能带来全新的视角——捕捉你未预料到的问题,注意到输出虽然“技术上正确”但偏离了重点,或者发现难以表达为通过/失败检查项的问题。对于每个测试用例,将实际输出与评分结果放在一起进行复核。
记录每个测试用例的具体反馈,并保存到工作区(例如,在评估目录旁保存一个 feedback.json):
{
"eval-top-months-chart": "图表缺少轴标签,且月份按字母顺序(英文环境下)排列,而非按时间顺序。",
"eval-clean-missing-emails": ""
}“图表缺少轴标签”是具备可操作性的反馈;而“看起来很糟”则不是。反馈为空意味着输出看起来不错——该测试用例通过了你的人工复核。在接下来的迭代步骤中,将优化重点放在你有具体反馈的测试用例上。
迭代 Skill#
在评分和复核之后,你拥有三个信号来源:
- 失败的断言 指向具体的短板——缺少的步骤、不清晰的指令,或者是 Skill 尚未处理的情况。
- 人工反馈 指向更广泛的质量问题——方法不对、输出结构不佳,或者 Skill 产生了一个技术上正确但没用的结果。
- 执行日志 揭示了出错的具体 原因。如果 Agent 漏掉了一条指令,那说明该指令可能有歧义;如果 Agent 在无用的步骤上浪费了时间,那说明这些指令可能需要简化或移除。
将这三种信号连同当前的 SKILL.md 一起交给 LLM,并让它提出修改建议。LLM 可以综合跨断言、复核意见和日志行为的模式,而这些模式如果手动梳理会非常繁琐。在提示 LLM 时,请遵循以下准则:
- 从反馈中提炼通用的改进。Skill 会被用于各种不同的提示词,而不只是这几个测试用例。修复方案应广泛解决底层问题,而不是针对特定用例打补丁。
- 保持 Skill 精简。更少、更好的指令往往优于详尽复杂的规则。如果执行日志显示了无谓的工作(没必要的验证、不需要的中间产物),请移除相关指令。如果即使添加了更多规则通过率也停滞不前,说明 Skill 可能被过度限制了——尝试移除一些指令,看看结果是否保持或有所提升。
- 解释原因。基于推理的指令(“做 X,因为 Y 会导致 Z”)比生硬的指令(“务必做 X,绝不做 Y”)效果更好。当模型理解了意图,执行指令会更可靠。
- 打包重复工作。如果每次测试运行都独立编写了类似的辅助脚本(如图表构建器、数据解析器),这是一个将脚本打包进 Skill 的
scripts/目录的信号。参见使用脚本了解具体操作。
循环步骤#
- 将评估信号和当前
SKILL.md交给 LLM,请求改进建议。 - 评审并应用修改。
- 在新的
iteration-<N+1>/目录中重新运行所有测试用例。 - 对新结果进行评分和汇总。
- 进行人工复核。重复上述过程。
当你对结果满意、人工反馈始终为空,或者在迭代之间不再看到有意义的提升时,即可停止迭代。
skill-creatorSkill 能够自动化此工作流的大部分内容——包括运行评估、断言评分、汇总基准测试,并向你呈现人工复核所需的结果。