评估输出质量#

如何使用评测驱动(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.jsontiming.jsonbenchmark.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_tokensduration_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/ 目录的信号。参见使用脚本了解具体操作。

循环步骤#

  1. 将评估信号和当前 SKILL.md 交给 LLM,请求改进建议。
  2. 评审并应用修改。
  3. 在新的 iteration-<N+1>/ 目录中重新运行所有测试用例。
  4. 对新结果进行评分和汇总。
  5. 进行人工复核。重复上述过程。

当你对结果满意、人工反馈始终为空,或者在迭代之间不再看到有意义的提升时,即可停止迭代。

skill-creator Skill 能够自动化此工作流的大部分内容——包括运行评估、断言评分、汇总基准测试,并向你呈现人工复核所需的结果。