你写好了一个 Skill,用一个提示词试了试,似乎能用。但它是否在各种不同提示下、在边界情况里、以及相对于「不用 Skill」时都同样可靠?运行结构化评测(evals)可以回答这些问题,并为你提供一个系统改进 Skill 的反馈闭环。
设计测试用例
一个测试用例由三部分组成:
- Prompt(提示):一个真实感的用户消息——类似真实用户会输入的话。
- 期望输出:用自然语言描述成功应该是什么样子。
- 输入文件(可选):Skill 运行时需要用到的文件。
将测试用例保存在 Skill 目录下的 evals/evals.json 中:
1{2 "skill_name": "csv-analyzer",3 "evals": [4 {5 "id": 1,6 "prompt": "I have a CSV of monthly sales data in data/sales_2025.csv. Can you find the top 3 months by revenue and make a bar chart?",7 "expected_output": "A bar chart image showing the top 3 months by revenue, with labeled axes and values.",8 "files": ["evals/files/sales_2025.csv"]9 },10 {11 "id": 2,12 "prompt": "there's a csv in my downloads called customers.csv, some rows have missing emails — can you clean it up and tell me how many were missing?",13 "expected_output": "A cleaned CSV with missing emails handled, plus a count of how many were missing.",14 "files": ["evals/files/customers.csv"]15 }16 ]17}
编写高质量测试提示的建议:
- 先从 2–3 个测试用例开始。 在你看到第一轮结果之前,不要过度投入。之后可以再扩展集合。
- 让提示多样化。 使用不同的措辞、细节程度和正式程度。有些提示可以很随意(“嘿,你能清理一下这个 CSV 文件吗?”),有些则要非常精确(“解析 data/input.csv 中的 CSV 文件,删除 B 列为空的行,并将结果写入 data/output.csv。”)。
- 覆盖边界情况。 至少包含一个测试边界条件的提示——如格式错误的输入、不寻常的请求,或 Skill 指令存在歧义的情形。
- 使用真实语境。 真实用户会提到文件路径、列名和个人背景。像 “process this data” 这样太模糊的提示无法有效测试任何东西。
先不用担心定义具体的通过 / 失败检查——只需要先写提示和期望输出。等你看到第一轮运行结果后,再补充更详细的检查(称为 assertions,断言)。
运行 evals
核心模式是对每个测试用例运行两次:一次启用 Skill,一次不启用 Skill(或者使用之前的版本)。这样你就有了可以对比的基线。
工作区结构
在 Skill 目录旁边创建一个 workspace(工作区)目录,用来组织评测结果。每完整跑一轮 eval,就建一个 iteration-N/ 目录。在其中,每个测试用例都有一个 eval 目录,下面再分 with_skill/ 和 without_skill/ 子目录:
1csv-analyzer/2├── SKILL.md3└── evals/4 └── evals.json5csv-analyzer-workspace/6└── iteration-1/7 ├── eval-top-months-chart/8 │ ├── with_skill/9 │ │ ├── outputs/ # 本次运行产出的文件10 │ │ ├── timing.json # Token 数与耗时11 │ │ └── grading.json # 断言评测结果12 │ └── without_skill/13 │ ├── outputs/14 │ ├── timing.json15 │ └── grading.json16 ├── eval-clean-missing-emails/17 │ ├── with_skill/18 │ │ ├── outputs/19 │ │ ├── timing.json20 │ │ └── grading.json21 │ └── without_skill/22 │ ├── outputs/23 │ ├── timing.json24 │ └── grading.json25 └── benchmark.json # 汇总统计数据
你手动维护的主要文件是 evals/evals.json。其他 JSON 文件(grading.json、timing.json、benchmark.json)会在评测过程中由 Agent、脚本或你自己生成。
运行评测
每次 eval 运行都应从一个「干净」的上下文开始——没有之前运行或 Skill 开发过程留下的状态。这能保证 Agent 只遵循 SKILL.md 中的内容。
在支持子 Agent 的环境(例如 Claude Code)中,这种隔离是天然存在的:每个子任务都会全新启动。如果没有子 Agent,就为每次运行开一个独立会话。
对每次运行,需要提供:
- Skill 路径(或者没有 Skill,用于基线)
- 测试提示(test prompt)
- 任意输入文件
- 输出目录
下面是一个启用 Skill 的单次运行的示例指令:
1执行此任务:2- 技能路径:/path/to/csv-analyzer3- 任务:我有一个包含月度销售数据的 CSV 文件 data/sales_2025.csv。请找出销售额排名前 3 的月份,并制作一个柱状图。4- 输入文件:evals/files/sales_2025.csv5- 将输出保存到:csv-analyzer-workspace/iteration-1/eval-top-months-chart/with_skill/outputs/
对于基线运行,使用同样的提示,只是不提供 Skill 路径,输出保存到 without_skill/outputs/。
如果是在改进一个已有 Skill,就用旧版本作为基线。在修改前做一份快照(cp -r <skill-path> <workspace>/skill-snapshot/),基线运行指向这个快照,并将输出保存到 old_skill/outputs/ 而不是 without_skill/。
记录时间数据
时间数据让你比较启用 Skill 相对基线的耗时和 Token 成本——一个显著提升输出质量、但 Token 使用量增加 3 倍的 Skill,与一个既更好又更省的 Skill,是完全不同的权衡。
每次运行完成后,记录 Token 数和时长:timing.json
1{2 "total_tokens": 84852,3 "duration_ms": 233324}
有些 Agent 工具,如Claude Code ,当一个子 Agent 任务结束时,任务完成通知会包含 total_tokens 和 duration_ms。要立即保存这些值——系统不会在其他地方持久化它们。
编写断言
断言是关于输出应该包含或实现什么的、可验证的陈述。通常在你看到第一轮输出之后再加断言——在 Skill 实际跑之前,你往往并不知道「好结果」具体长什么样。
好的断言示例:
"输出是一个合法的 JSON"——可以用程序验证。"条形图的坐标轴已标注"——具体且可观察。"该报告至少包含三项建议"——可计数。
较弱的断言示例:
"输出结果良好"——过于模糊,无法打分。"输出结果正是“总收入:$X”"——太脆弱;即使输出是正确的,只要措辞稍有不同就会失败。
无需对所有东西都写断言。有些质量指标——比如写作风格、视觉设计、或者输出是否「感觉对」——很难拆成通过 / 失败的检查。这类问题更适合在人工审阅中发现。把断言保留给那些可以客观检查的点。
在 evals/evals.json 中为每个测试用例添加断言:
1{2 "skill_name": "csv-analyzer",3 "evals": [4 {5 "id": 1,6 "prompt": "我有一个包含每月销售数据的 CSV 文件,文件名为 data/sales_2025.csv。请问您能否找出销售额排名前 3 的月份,并制作一个柱状图?",7 "expected_output": "一张柱状图,显示了收入排名前 3 个月的月份,并标注了坐标轴和数值。",8 "files": ["evals/files/sales_2025.csv"],9 "assertions": [10 "输出结果包含一个柱状图图像文件",11 "图表显示正好三个月",12 "两个坐标轴都已标注",13 "图表标题或说明文字提到了收入"14 ]15 }16 ]17}
为输出打分
打分是指根据实际输出来评估每条断言,并记录 PASS(通过)或 FAIL(失败),同时提供具体证据。证据应引用或指向实际输出,而不仅仅是主观判断。
最简单的方法是把输出和断言一起交给一个 LLM,让它评估每一条。对于可以通过代码检查的断言(例如 JSON 是否有效、行数是否正确、某文件是否以预期尺寸存在),使用验证脚本——脚本在机械检查上比 LLM 更可靠,也更容易在多轮迭代间复用。
1{2 "assertion_results": [3 {4 "text": "输出结果包含一个柱状图图像文件",5 "passed": true,6 "evidence": "在 outputs 目录中找到 chart.png (45KB)"7 },8 {9 "text": "图表显示正好三个月",10 "passed": true,11 "evidence": "图表显示了三月、七月和十一月的柱状图。"12 },13 {14 "text": "两个坐标轴都已标注",15 "passed": false,16 "evidence": "Y轴标签为“收入($)”,但X轴没有标签。"17 },18 {19 "text": "图表标题或说明文字提到了收入",20 "passed": true,21 "evidence": "图表标题为“收入排名前 3 个月”"22 }23 ],24 "summary": {25 "passed": 3,26 "failed": 1,27 "total": 4,28 "pass_rate": 0.7529 }30}
打分原则
- 要求明确证据才能判定通过。 不要「宁可错杀也不放过」。如果断言写的是 “includes a summary(包含摘要)”,而输出只有一个叫 “Summary” 的小节、且只有一句非常含糊的话,那就应该判为失败——标签有了,但内容不达标。
- 不仅要看打分结果,还要回头审视断言本身。 在打分过程中,留意那些总是通过(无论是否启用 Skill 都能通过)的断言,这类断言太容易了,对 Skill 价值几乎没有区分度;也留意那些总是失败的断言,可能是断言本身写错了(要求模型做不到的事情)、测试用例太难,或断言检查的点不对。把这些问题留到下一轮迭代中修正。
在比较两个 Skill 版本时,可以尝试盲评(blind comparison):把两份输出都给一个 LLM 评委,但不告诉它哪份来自哪个版本。评委根据自己的评分标准,从整体上打分(结构是否清晰、格式是否良好、可用性、打磨程度等),不受对「哪个版本应该更好」的先入为主影响。这样可以补充断言打分:两份输出可能都通过了所有断言,但整体质量依然有明显差异。
汇总结果
当本轮迭代中所有运行都打分完成后,按配置(with_skill / without_skill 等)汇总统计,并将结果保存到本轮的 benchmark.json 中(如 csv-analyzer-workspace/iteration-1/benchmark.json):
1{2 "run_summary": {3 "with_skill": {4 "pass_rate": { "mean": 0.83, "stddev": 0.06 },5 "time_seconds": { "mean": 45.0, "stddev": 12.0 },6 "tokens": { "mean": 3800, "stddev": 400 }7 },8 "without_skill": {9 "pass_rate": { "mean": 0.33, "stddev": 0.10 },10 "time_seconds": { "mean": 32.0, "stddev": 8.0 },11 "tokens": { "mean": 2100, "stddev": 300 }12 },13 "delta": {14 "pass_rate": 0.50,15 "time_seconds": 13.0,16 "tokens": 170017 }18 }19}
delta 告诉你这个 Skill 的代价(更多的时间、更多的 Token)以及它带来的收益(更高的通过率)。如果一个 Skill 让运行多花了 13 秒,但使通过率提升了 50 个百分点,那通常是很值得的;而如果一个 Skill 让 Token 使用量翻倍,却只提升了 2 个百分点的通过率,那可能就不太划算。
标准差(stddev)只有在每个 eval 跑多次时才有意义。在早期阶段,你也许只有 2–3 个测试用例、且每个只跑一次,这时可以先关注原始通过数量和 delta——随着测试集扩展、每个 eval 运行多次之后,统计指标才会真正有用。
分析模式
汇总统计有时会掩盖重要模式。在计算完 benchmark 后:
- 移除或替换那些在所有配置下都能通过的断言。 这些断言说明模型在不依赖 Skill 的情况下就能轻松完成,对 Skill 价值判断帮助不大。它们只会虚高启用 Skill 时的通过率,却不能反映真实增益。
- 排查那些在所有配置下都失败的断言。 要么是断言本身有问题(要求的内容模型无法做到),要么是测试用例太难,或者断言检查的点根本不对。在下一轮迭代前先把这些修好。
- 重点关注那些启用 Skill 通过、未启用 Skill 失败的断言。 这里就是 Skill 明确创造价值的地方。要弄清楚为什么——是哪些指令或脚本带来了差异?
- 当同一个 eval 在多次运行中结果不稳定时,收紧指令。 如果同一测试有时通过、有时失败(在 benchmark 中表现为较高的
stddev),可能是评测本身不稳定(对模型随机性的敏感性高),也可能是 Skill 指令存在歧义,导致模型每次理解不一。通过增加示例或更具体的指导来减少歧义。 - 排查时间和 Token 的异常值。 如果某个 eval 比其他用例慢了 3 倍,查看它的执行日志(完整的模型执行记录),找出瓶颈所在。
人工复审结果
断言打分和模式分析能发现很多问题,但它们只能检查「你事先想到要写断言的那些点」。人工审阅则能带来新的视角——发现你没预料到的问题,识别那些技术上正确但实际上不太有用的输出,或者发现难以用通过 / 失败表达的问题。
对每个测试用例,结合输出和打分结果进行人工审阅。把对每个测试用例的具体反馈记录下来,保存在 workspace 中(例如在各 eval 目录旁放一个 feedback.json):
1{2 "eval-top-months-chart": "图表缺少坐标轴标签,月份是按字母顺序排列的,而不是按时间顺序排列的。",3 "eval-clean-missing-emails": ""4}
“图表缺少坐标轴标签,月份按字母顺序而不是时间顺序排列” 这样的反馈是可执行的;“看起来不太好” 则不是。空字符串表示人工审阅时没有问题——该测试用例通过了你的主观检查。在迭代改进步骤中,将精力集中在那些你有具体抱怨的测试用例上。
迭代完善 Skill
在打分和审阅之后,你会获得三类信号:
- 失败的断言 指出具体缺口——缺了一步操作、某条指令不清晰,或者 Skill 没有覆盖某些情形。
- 人工反馈 反映更宏观的质量问题——思路不对、输出结构不好,或者虽然技术上没错,但生成结果并不实用。
- 执行日志(transcripts) 揭示问题产生的原因。如果 Agent 忽略了某条指令,可能是指令有歧义;如果 Agent 花了很多时间在无效步骤上,这些指令可能需要被简化或删除。
将这些信号转化为 Skill 改进的最高效方式,是把三类信息连同当前的 SKILL.md 一起交给一个 LLM,让它提出修改建议。LLM 可以综合失败断言、审阅意见和运行行为之间的模式,而这些关联如果手动梳理会非常费劲。
在给 LLM 写提示时,可包含以下指导原则:
- 从反馈中抽象出通用规律。 Skill 会在许多不同提示下被使用,而不仅仅是测试用例。修改应当针对底层问题,而不是为特定例子打补丁。
- 保持 Skill 精简。 更少但更高质量的指令,通常比一大堆规则更有效。如果运行日志中出现很多无用工作(不必要的校验、多余的中间结果),就删掉对应指令。如果在不停加规则后通过率不再提升,说明 Skill 可能过度约束了——可以尝试移除一些指令,看看结果是否依然良好甚至更好。
- 解释「为什么」。 基于原因的指令(“做 X,因为 Y 往往会导致 Z”)通常比简单的强制指令(“永远做 X,绝不要做 Y”)效果更好。当模型理解指令的目的时,会更可靠地遵循它们。
- 把重复性工作打包。 如果每次测试运行中,模型都会重复写相似的辅助脚本(比如画图工具、数据解析器),那就是应该把这些脚本打包进 Skill 的
scripts/目录的信号。
迭代循环
- 将评测信号和当前的
SKILL.md提供给 LLM,请它给出改进方案。 - 审阅并应用这些修改。
- 在新的
iteration-<N+1>/目录中对所有测试用例重新跑一轮。 - 对新结果进行打分和汇总。
- 进行人工复审。重复以上流程。
当你对结果满意、反馈持续为空,或者每轮迭代都没有明显改进时,就可以停止这个循环。
skill-creator 这个 Skill 可以自动化上述工作流中的大部分步骤——包括运行 evals、打断言分数、汇总 benchmark,以及整理结果供人工审阅。



