Skip to main content
你写好了一个 skill,在某个 prompt 上试了试,看起来能用。但它是否真的可靠?面对不同的 prompt、边界场景,以及和“不使用 skill”相比时,它是否依然更好?运行结构化评测(eval)可以回答这些问题,并为你提供一个持续改进 skill 的反馈闭环。

设计测试用例

一个测试用例包含三个部分:
  • Prompt:真实的用户消息,也就是用户真的可能会输入的那种内容。
  • 期望输出:用人类可读的方式描述“成功长什么样”。
  • 输入文件(可选):skill 完成任务时需要用到的文件。
把测试用例存放在 skill 目录内的 evals/evals.json 中:
evals/evals.json
{
  "skill_name": "csv-analyzer",
  "evals": [
    {
      "id": 1,
      "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?",
      "expected_output": "A bar chart image showing the top 3 months by revenue, with labeled axes and values.",
      "files": ["evals/files/sales_2025.csv"]
    },
    {
      "id": 2,
      "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?",
      "expected_output": "A cleaned CSV with missing emails handled, plus a count of how many were missing.",
      "files": ["evals/files/customers.csv"]
    }
  ]
}
编写优质测试 prompt 的建议:
  • 先从 2 到 3 个测试用例开始。 在看到第一轮结果之前,不要一开始就投入过多。后面再扩充也不迟。
  • 让 prompt 有变化。 使用不同措辞、不同细节程度和不同正式程度。有些 prompt 可以更口语化(“hey can you clean up this csv”),有些则更精确(“Parse the CSV at data/input.csv, drop rows where column B is null, and write the result to data/output.csv”)。
  • 覆盖边界场景。 至少加入一个用于测试边界条件的 prompt,比如格式错误的输入、非常规请求,或者 skill 指令可能产生歧义的情况。
  • 使用真实上下文。 真实用户会提到文件路径、列名和个人上下文。像 “process this data” 这样的 prompt 太模糊,测不出什么有效信息。
先不用着急定义具体的通过/失败检查项,只需要先准备 prompt 和期望输出。等你看到第一轮运行结果后,再补更细的检查项,也就是断言。

运行 eval

核心模式是:每个测试用例都运行两次,一次 使用 skill,一次 不使用 skill(或者使用 skill 的旧版本)。这样你就有了一个可以对比的 baseline。

工作区结构

把 eval 结果组织在 skill 目录旁边的一个工作区目录里。完整跑完一轮 eval 循环就对应一个 iteration-N/ 目录。在该目录下,每个测试用例都有自己的 eval 目录,目录中再分成 with_skill/without_skill/ 两个子目录:
csv-analyzer/
├── SKILL.md
└── evals/
    └── evals.json
csv-analyzer-workspace/
└── iteration-1/
    ├── eval-top-months-chart/
    │   ├── with_skill/
    │   │   ├── outputs/       # Files produced by the run
    │   │   ├── timing.json    # Tokens and duration
    │   │   └── grading.json   # Assertion results
    │   └── 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         # Aggregated statistics
你手工编写的主要文件是 evals/evals.json。其他 JSON 文件(grading.jsontiming.jsonbenchmark.json)则是在 eval 过程中生成的,可以由 agent、脚本,或者你自己来产出。

发起运行

每次 eval 运行都应该从干净的上下文开始,不能带着上一次运行残留的状态,也不要带着开发 skill 时留下的上下文。这样才能确保 agent 只依据 SKILL.md 中的内容执行。在支持 subagent 的环境里(例如 Claude Code),这种隔离是天然成立的:每个子任务都会从新的上下文启动。如果没有 subagent,就为每次运行单独开一个会话。 每次运行都要提供以下信息:
  • skill 路径(baseline 则不提供 skill)
  • 测试 prompt
  • 任何输入文件
  • 输出目录
下面是一次“带 skill 运行”的指令示例:
Execute this task:
- Skill path: /path/to/csv-analyzer
- Task: 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?
- Input files: evals/files/sales_2025.csv
- Save outputs to: csv-analyzer-workspace/iteration-1/eval-top-months-chart/with_skill/outputs/
baseline 运行使用同样的 prompt,只是不提供 skill 路径,并把结果保存到 without_skill/outputs/ 如果你是在改进一个已经存在的 skill,可以把旧版本当作 baseline。编辑前先做一个快照(cp -r <skill-path> <workspace>/skill-snapshot/),让 baseline 运行指向这个快照,并把结果保存到 old_skill/outputs/,而不是 without_skill/

记录耗时数据

耗时数据可以帮助你比较:skill 相比 baseline 多花了多少时间、消耗了多少 token。一个大幅提升输出质量但让 token 使用量翻三倍的 skill,和一个既更好又更便宜的 skill,是完全不同的取舍。每次运行完成后,记录 token 数和持续时间:
timing.json
{
  "total_tokens": 84852,
  "duration_ms": 23332
}
在 Claude Code 中,当一个 subagent 任务完成时,任务完成通知 会包含 total_tokensduration_ms。请立刻把这些值保存下来,因为它们不会被持久化到别处。

编写断言

断言是关于输出“应该包含什么”或“应该达成什么”的可验证陈述。通常在你看过第一轮输出后再补断言会更合适,因为在 skill 真正跑起来之前,你往往并不知道“好的结果”具体是什么样。 好的断言示例:
  • "The output file is valid JSON":可以通过程序验证。
  • "The bar chart has labeled axes":具体且可观察。
  • "The report includes at least 3 recommendations":可以计数。
不好的断言示例:
  • "The output is good":太模糊,无法评分。
  • "The output uses exactly the phrase 'Total Revenue: $X'":太脆弱了,哪怕输出是正确的,只是换了种表达也会失败。
不是所有东西都适合写成断言。比如写作风格、视觉设计、或者输出是否“感觉对劲”这类特质,很难拆解成通过/失败检查。它们更适合放到后面的人工复核环节里处理。把断言保留给那些可以客观检查的内容。 把断言加入 evals/evals.json 中的每个测试用例:
evals/evals.json
{
  "skill_name": "csv-analyzer",
  "evals": [
    {
      "id": 1,
      "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?",
      "expected_output": "A bar chart image showing the top 3 months by revenue, with labeled axes and values.",
      "files": ["evals/files/sales_2025.csv"],
      "assertions": [
        "The output includes a bar chart image file",
        "The chart shows exactly 3 months",
        "Both axes are labeled",
        "The chart title or caption mentions revenue"
      ]
    }
  ]
}

评分输出

评分意味着:针对每条断言,对照实际输出判断其 PASS 还是 FAIL,并记录具体证据。证据应该引用或指向输出内容,而不是只写一个主观意见。 最简单的做法是把输出结果和断言一起交给 LLM,让它逐条评估。对于那些可以靠代码检查的断言(例如 JSON 是否合法、行数是否正确、文件是否存在且尺寸符合预期),则更适合用验证脚本,因为脚本在这种机械性检查上比 LLM 判断更可靠,也更容易在多轮迭代中复用。
grading.json
{
  "assertion_results": [
    {
      "text": "The output includes a bar chart image file",
      "passed": true,
      "evidence": "Found chart.png (45KB) in outputs directory"
    },
    {
      "text": "The chart shows exactly 3 months",
      "passed": true,
      "evidence": "Chart displays bars for March, July, and November"
    },
    {
      "text": "Both axes are labeled",
      "passed": false,
      "evidence": "Y-axis is labeled 'Revenue ($)' but X-axis has no label"
    },
    {
      "text": "The chart title or caption mentions revenue",
      "passed": true,
      "evidence": "Chart title reads 'Top 3 Months by Revenue'"
    }
  ],
  "summary": {
    "passed": 3,
    "failed": 1,
    "total": 4,
    "pass_rate": 0.75
  }
}

评分原则

  • PASS 必须有具体证据。 不要“差不多就算过”。如果断言写的是“includes a summary”,而输出里只有一个标题叫 “Summary”、内容却只有一句空泛的话,那也应该算 FAIL。标签有了,不代表实质满足了。
  • 评分时也要顺便审视断言本身。 在评分过程中,注意哪些断言太容易(不管 skill 质量如何都会通过)、太难(即使输出已经不错也总失败)、或者根本不可验证(无法仅凭输出本身检查)。这些问题都应该在下一轮迭代里修正。
如果你是在比较两个 skill 版本,可以尝试 盲评(blind comparison):把两个输出都交给 LLM 评审,但不要告诉它哪个来自哪个版本。让评审基于自己的 rubric 去评价整体质量,比如组织结构、格式、可用性和精致度,避免“理论上新版本应该更好”的偏见。它和断言评分是互补的:两个输出可能都通过了全部断言,但整体质量仍然差异很大。

汇总结果

当这一轮中的每次运行都完成评分后,按配置计算汇总统计,并把结果保存到 eval 目录旁边的 benchmark.json 中(例如 csv-analyzer-workspace/iteration-1/benchmark.json):
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 反映的是这个 skill 的“成本”和“收益”:它多花了多少时间、多消耗了多少 token,又换来了多少通过率提升。一个多花 13 秒、但通过率提升 50 个百分点的 skill,大概率是值得的;一个 token 消耗翻倍、却只提升 2 个百分点的 skill,可能就不值。
标准差(stddev)只有在同一个 eval 进行多次运行时才有实际意义。在早期迭代里,如果你只有 2 到 3 个测试用例、每个用例只跑一次,就先关注原始通过数量和 delta 即可。等测试集扩大、每个 eval 也开始重复多次时,这些统计指标才会真正变得有用。

分析模式

汇总统计可能会掩盖一些重要模式。完成 benchmark 之后,重点看这些点:
  • 移除或替换在两种配置下都总是通过的断言。 这类断言不能提供有效信息,说明模型即使不依赖 skill 也能轻松完成。它们会抬高 with-skill 的通过率,却不能反映 skill 的真实价值。
  • 调查在两种配置下都总是失败的断言。 要么断言本身有问题(要求了模型做不到的事情),要么测试用例太难,要么断言检查的方向不对。先把这些修正掉,再进入下一轮。
  • 重点研究“使用 skill 时通过、不使用 skill 时失败”的断言。 这正是 skill 明确带来价值的地方。理解 为什么 会这样,是哪条指令、哪个脚本真正起了作用?
  • 如果结果在多次运行之间不稳定,就收紧指令。 如果同一个 eval 有时通过、有时失败(在 benchmark 中体现为较高的 stddev),要么这个 eval 本身不稳定,容易受模型随机性影响;要么 skill 的指令存在歧义,导致模型每次理解都不一样。可以通过增加示例或补更具体的指导来减少歧义。
  • 检查时间和 token 的异常值。 如果某个 eval 比其他 eval 慢 3 倍,就去读它的执行转录(也就是模型在运行过程中做了什么的完整日志),找到瓶颈在哪里。

由人工复核结果

断言评分和模式分析已经能发现很多问题,但它们只能检查“你事先想到要写成断言的内容”。人工评审能带来一层新的视角,比如发现你没有预料到的问题、注意到输出虽然技术上正确却没有真正解决需求,或者识别那些很难写成通过/失败检查的问题。对于每个测试用例,都要把实际输出和评分结果一起看一遍。 把每个测试用例的具体反馈记录下来,并保存到工作区中(例如作为 eval 目录旁边的 feedback.json):
feedback.json
{
  "eval-top-months-chart": "The chart is missing axis labels and the months are in alphabetical order instead of chronological.",
  "eval-clean-missing-emails": ""
}
像 “The chart is missing axis labels” 这样的反馈是可执行的;“looks bad” 则不是。空字符串表示这份输出看起来没问题,也就是这个测试用例通过了你的人工复核。在后面的 skill 迭代阶段,把精力优先放在那些你有明确抱怨点的测试用例上。

迭代 skill

在评分和复核之后,你会拿到三类信号:
  • 失败的断言 指向具体缺口,比如漏了一步、指令不清楚,或者 skill 没有覆盖某类情况。
  • 人工反馈 指向更宽泛的质量问题,比如方法错了、输出结构差,或者 skill 给出了技术上正确但实际上没帮助的结果。
  • 执行转录 揭示了问题 为什么 会发生。如果 agent 忽略了某条指令,说明那条指令可能有歧义;如果 agent 把时间花在无效步骤上,说明对应指令可能需要简化或删除。
把这些信号真正转化成 skill 改进,最有效的方法通常是:把这三类材料和当前的 SKILL.md 一起交给 LLM,让它提出修改建议。LLM 可以综合失败断言、评审意见和执行行为中的模式,找出那些手工串联起来会很费劲的问题。向 LLM 提示时,可以附带这些指导原则:
  • 从反馈中抽象出通用问题。 skill 最终会面对许多不同 prompt,而不只是当前这几个测试用例。修改应该解决底层问题,而不是只给单个示例打补丁。
  • 让 skill 保持精简。 更少但更高质量的指令,往往比一大堆规则效果更好。如果转录显示存在大量无效工作(没必要的校验、没必要的中间输出),就删掉这些指令。如果通过率在不断加规则后依然停滞不前,说明这个 skill 可能被限制得太死了,可以试着删一些规则,看结果是否保持甚至变好。
  • 解释“为什么”。 基于原因的指令(“做 X,因为 Y 往往会导致 Z”)通常比僵硬的命令式指令(“ALWAYS do X, NEVER do Y”)效果更好。模型在理解目的时,执行会更稳定。
  • 把重复工作打包起来。 如果每次测试运行都会独立写出一个相似的辅助脚本(比如画图脚本、数据解析脚本),那就是一个信号:应该把这个脚本收进 skill 的 scripts/ 目录中。关于这件事的做法,可以继续看 Using scripts

迭代循环

  1. 把 eval 信号和当前的 SKILL.md 一起交给 LLM,让它提出改进建议。
  2. 审查并应用这些改动。
  3. 在新的 iteration-<N+1>/ 目录里重新运行全部测试用例。
  4. 对新结果评分并做汇总。
  5. 再做一次人工复核。重复这个过程。
当你对结果已经满意、反馈长期为空,或者多轮迭代之间已经看不到明显改进时,就可以停下来。
skill-creator 这个 Skill 可以自动化这套流程中的很多环节,包括运行 eval、给断言评分、汇总 benchmark,以及把结果整理给人工复核。