> ## Documentation Index
> Fetch the complete documentation index at: https://skills.developerdoc.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# 评估 skill 输出质量

> 如何用由 eval 驱动的迭代，测试你的 skill 是否能稳定地产生高质量输出。

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

## 设计测试用例

一个测试用例包含三个部分：

* **Prompt**：真实的用户消息，也就是用户真的可能会输入的那种内容。
* **期望输出**：用人类可读的方式描述“成功长什么样”。
* **输入文件**（可选）：skill 完成任务时需要用到的文件。

把测试用例存放在 skill 目录内的 `evals/evals.json` 中：

```json evals/evals.json theme={null}
{
  "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.json`、`timing.json`、`benchmark.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 数和持续时间：

```json timing.json theme={null}
{
  "total_tokens": 84852,
  "duration_ms": 23332
}
```

<Tip>
  在 Claude Code 中，当一个 subagent 任务完成时，[任务完成通知](https://platform.claude.com/docs/en/agent-sdk/typescript#sdk-task-notification-message) 会包含 `total_tokens` 和 `duration_ms`。请立刻把这些值保存下来，因为它们不会被持久化到别处。
</Tip>

## 编写断言

断言是关于输出“应该包含什么”或“应该达成什么”的可验证陈述。通常在你看过第一轮输出后再补断言会更合适，因为在 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` 中的每个测试用例：

```json evals/evals.json highlight={9-14} theme={null}
{
  "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 判断更可靠，也更容易在多轮迭代中复用。

```json grading.json theme={null}
{
  "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 质量如何都会通过）、太难（即使输出已经不错也总失败）、或者根本不可验证（无法仅凭输出本身检查）。这些问题都应该在下一轮迭代里修正。

<Tip>
  如果你是在比较两个 skill 版本，可以尝试 **盲评（blind comparison）**：把两个输出都交给 LLM 评审，但不要告诉它哪个来自哪个版本。让评审基于自己的 rubric 去评价整体质量，比如组织结构、格式、可用性和精致度，避免“理论上新版本应该更好”的偏见。它和断言评分是互补的：两个输出可能都通过了全部断言，但整体质量仍然差异很大。
</Tip>

## 汇总结果

当这一轮中的每次运行都完成评分后，按配置计算汇总统计，并把结果保存到 eval 目录旁边的 `benchmark.json` 中（例如 `csv-analyzer-workspace/iteration-1/benchmark.json`）：

```json benchmark.json theme={null}
{
  "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，可能就不值。

<Note>
  标准差（`stddev`）只有在同一个 eval 进行多次运行时才有实际意义。在早期迭代里，如果你只有 2 到 3 个测试用例、每个用例只跑一次，就先关注原始通过数量和 delta 即可。等测试集扩大、每个 eval 也开始重复多次时，这些统计指标才会真正变得有用。
</Note>

## 分析模式

汇总统计可能会掩盖一些重要模式。完成 benchmark 之后，重点看这些点：

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

## 由人工复核结果

断言评分和模式分析已经能发现很多问题，但它们只能检查“你事先想到要写成断言的内容”。人工评审能带来一层新的视角，比如发现你没有预料到的问题、注意到输出虽然技术上正确却没有真正解决需求，或者识别那些很难写成通过/失败检查的问题。对于每个测试用例，都要把实际输出和评分结果一起看一遍。

把每个测试用例的具体反馈记录下来，并保存到工作区中（例如作为 eval 目录旁边的 `feedback.json`）：

```json feedback.json theme={null}
{
  "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](/skill-creation/using-scripts)。

### 迭代循环

1. 把 eval 信号和当前的 `SKILL.md` 一起交给 LLM，让它提出改进建议。
2. 审查并应用这些改动。
3. 在新的 `iteration-<N+1>/` 目录里重新运行全部测试用例。
4. 对新结果评分并做汇总。
5. 再做一次人工复核。重复这个过程。

当你对结果已经满意、反馈长期为空，或者多轮迭代之间已经看不到明显改进时，就可以停下来。

<Tip>
  [`skill-creator`](https://github.com/anthropics/skills/tree/main/skills/skill-creator) 这个 Skill 可以自动化这套流程中的很多环节，包括运行 eval、给断言评分、汇总 benchmark，以及把结果整理给人工复核。
</Tip>
