<!-- title: 用Evals闭环打磨你的首个Skill -->

## 概述

本教程面向已经跑通过 Claude Code、想把自己的重复工作流沉淀成可复用能力的开发者。你将从零创建一个真实的「单元测试生成」Skill，并采用 Anthropic 官方 skill-creator 推荐的 **Evals 闭环**：先写草稿 → 跑测试用例 → 量化打分 → 根据反馈迭代，直到 Skill 在真实场景下稳定可用。学完你能独立完成"草稿—评测—打磨—发布"全流程，并理解为什么"会写 Skill"不等于"Skill 写得好"。

## 前置条件

- 已安装 Claude Code（`claude --version` 可正常运行），并能完成一次普通会话。
- 安装了官方 **skill-creator** 插件：`claude plugin install skill-creator@anthropics` （对应仓库 `anthropics/claude-plugins-official`）。
- 本机有 Node.js（运行 Jest）或 Python（运行 pytest），用于真实执行 Skill 生成的测试。
- 建议先读过 `SKILL.md` 的两段式结构（YAML frontmatter + Markdown 正文），有基本概念再动手。

## 核心概念：Skill 为什么需要 Evals

一个 Skill 本质是一个目录，至少含一个 `SKILL.md`。但"能触发"和"触发后输出靠谱"是两回事。官方 skill-creator 把创建过程明确建模为一个循环：

1. 确定 Skill 要做什么、大致怎么做；
2. 写草稿；
3. 写几个真实测试 prompt，让"装了 Skill 的 Claude"和"没装 Skill 的 Claude"分别跑一遍；
4. 同时起草量化断言（assertions），等运行结束后打分、聚合、出基准报告；
5. 根据人工反馈和量化指标改写 Skill，回到第 3 步，直到满意。

关键点：**永远用"装了 Skill vs 没装 Skill"的对照**来判断 Skill 是否真的在创造增量价值——否则你可能在优化一个本就靠 Claude 默认能力就能完成的任务。

## 第一步：设计 Skill 的意图与触发

Skill 的触发几乎完全由 frontmatter 里的 `description` 决定，正文里写得再详细也不会参与"是否触发"的判断。因此设计阶段先回答四个问题：

1. 这个 Skill 让 Claude 能做什么？（生成结构化单元测试）
2. 什么时候触发？（用户说"写测试/补单测/提高覆盖率"，或贴了一段函数期望被测）
3. 期望输出格式？（可直接执行的 Jest/pytest 文件）
4. 输出是否可客观验证？（可——测试能跑、有边界用例、有用例名）。可客观验证 → 值得配 Evals；纯主观输出（文风、设计）则不必强加断言。

**常见问题：Skill 不触发或乱触发。** 根因几乎都在 `description` 写得太泛或太窄。官方给出的对策是"适度 pushy"——不要只写功能，要写清触发场景，并主动覆盖那些"用户没明说但意图明显"的情况。

## 第二步：编写 SKILL.md 草稿

目录结构遵循三层渐进式披露（progressive disclosure）：

```text
unit-test-gen/
├── SKILL.md            # 必须：frontmatter + 正文（<500 行）
├── scripts/
│   └── scaffold.py     # 可执行脚本：确定性、重复性任务
└── references/
    └── naming.md       # 按需加载的参考资料
```

`SKILL.md` 草稿：

```markdown
---
name: unit-test-gen
description: 为函数或模块生成结构化单元测试，覆盖正常路径、边界条件和异常分支。当用户提到"写测试""补单测""生成测试用例""提高覆盖率""给这个函数加测试"，或直接粘贴一段函数/类期望被验证时，都应使用本 Skill；即使用户没明说"单元测试"，只要意图是验证代码行为，也请使用。
---

# 单元测试生成

为目标函数生成可直接执行的测试文件。核心目标不是"凑用例数"，而是
用最少的用例覆盖最值得验证的行为。

## 工作流程
1. 读懂被测函数的契约：入参类型、返回值、副作用、可能的异常。
2. 把用例分成三类：正常路径、边界条件、异常/错误路径。
3. 用"行为描述"命名用例（`it('returns 0 when both inputs are 0')`），
   而不是 `test1`、`test2`——名字本身就是文档。
4. 优先调用 scripts/scaffold.py 生成测试文件骨架，再填充用例。

## 为什么强调命名
未来有人改这条用例时，名字会告诉他这条用例在保护什么行为。
命名模糊的用例在被改坏时不会立刻被发现。
```

**预期结果：** 保存后，在 Claude Code 里说"帮我给下面这个函数写测试"，该 Skill 应被自动加载。

**常见问题：** 若正文超过 500 行，应拆出 `references/` 子文件并在正文中用明确指引告诉模型"何时去读哪个文件"——避免把所有细节都塞进常驻上下文。

## 第三步：写测试用例（先不写断言）

在与 Skill 目录平级的位置建 `<skill-name>-workspace/`。先只写 prompt，保存到 `evals/evals.json`：

```json
{
  "skill_name": "unit-test-gen",
  "evals": [
    {
      "id": 1,
      "prompt": "给这个函数写测试：function clamp(v, lo, hi) { return Math.min(hi, Math.max(lo, v)); }",
      "expected_output": "针对 clamp 的测试，含 v 在区间内、低于 lo、高于 hi、lo==hi 等边界",
      "files": []
    },
    {
      "id": 2,
      "prompt": "帮我补单测，要求覆盖异常路径：function divide(a, b) { if (b === 0) throw new RangeError('div0'); return a / b; }",
      "expected_output": "包含 b=0 抛 RangeError 的断言，以及正常除法",
      "files": []
    },
    {
      "id": 3,
      "prompt": "提高这个模块的覆盖率：贴入一段含多个分支的计税函数",
      "expected_output": "覆盖每个税率区间的边界值",
      "files": []
    }
  ]
}
```

写完先给用户过一眼："这几个测试用例看着对吗？要加吗？"——用例本身的质量决定整个闭环的上限。

## 第四步：同一轮里并发跑 with_skill 和 baseline

官方反复强调：**别先跑装了 Skill 的、回头再补基线**。每条用例都在同一轮里同时起两个子任务——一个带 Skill、一个不带（改进已有 Skill 时基线换成旧版本快照）。这样两组几乎同时完成，省墙钟时间。

工作区按迭代组织：

```text
unit-test-gen-workspace/
└── iteration-1/
    ├── clamp-bounds/
    │   ├── with_skill/outputs/clamp.test.js
    │   ├── without_skill/outputs/clamp.test.js
    │   └── timing.json
    ├── divide-errors/
    │   └── ...
    └── benchmark.json
```

每个用例目录用**描述性名字**（`clamp-bounds`），不要叫 `eval-0`——这样在结果看板里一眼能看懂。每个用例写一份 `eval_metadata.json`，先留空断言：

```json
{ "eval_id": 1, "eval_name": "clamp-bounds", "prompt": "...", "assertions": [] }
```

`timing.json` 在子任务完成通知到达时立刻落盘（这是捕获 token/耗时的唯一时机，通知里带 `total_tokens` 和 `duration_ms`，别想批量后补）：

```json
{ "total_tokens": 84852, "duration_ms": 23332, "total_duration_seconds": 23.3 }
```

## 第五步：趁运行间隙写断言

不要干等。趁子任务跑，为每条用例起草可客观验证的断言，并解释每条在查什么。好断言有两个特征：**可客观判定**、**名字读得懂**。对本 Skill 合适的断言如：

- `test-file-parses`：生成的测试文件能被 `jest --listTests` 或 `pytest --collect-only` 静态解析通过。
- `has-boundary-case`：至少一个用例落在边界值（0、负数、上/下界、等值边界）。
- `has-error-path`：至少一个用例验证异常或错误返回。
- `descriptive-names`：用例名描述了被测行为，而非 `test1`/`case2`。

能脚本判定的就写脚本判定（快、稳、可复用），别靠肉眼。断言写进 `eval_metadata.json` 与 `evals/evals.json`。

## 第六步：打分、聚合并启动可视化看板

全部跑完后：

1. **打分**：对每条断言判 pass/fail，结果存 `grading.json`，字段必须严格是 `text`、`passed`、`evidence`（看板依赖这三个确切字段名，写成 `name/met/details` 会渲染失败）：

   ```json
   { "expectations": [
     { "text": "has-boundary-case", "passed": true, "evidence": "it('clamps to lo when v < lo')" }
   ] }
   ```

2. **聚合基准**：在 skill-creator 目录跑

   ```bash
   python -m scripts.aggregate_benchmark unit-test-gen-workspace/iteration-1 --skill-name unit-test-gen
   ```

   产出 `benchmark.json` 与 `benchmark.md`，含每个配置（with_skill / without_skill）的 `pass_rate`、耗时、token，给出均值±标准差与差值。展示时把 with_skill 放在对应 baseline 前面。

3. **分析师视角**：读一遍数据，挑出聚合统计会掩盖的问题——比如某条断言"不管装不装 Skill 都过"（无区分度，该删或该换）；某条用例方差极大（可能 flaky）；或耗/token 的取舍是否值得。

4. **启动看板**：

   ```bash
   nohup python <skill-creator-path>/eval-viewer/generate_review.py \
     unit-test-gen-workspace/iteration-1 \
     --skill-name unit-test-gen \
     --benchmark unit-test-gen-workspace/iteration-1/benchmark.json \
     > /dev/null 2>&1 &
   ```

   无显示环境（容器/CI）改用 `--static out.html` 生成单文件。看板两个页签：**Outputs** 逐条看产物并留反馈，**Benchmark** 看量化对比。用户点 "Submit All Reviews" 后，反馈落成 `feedback.json`。

## 第七步：读反馈并迭代（闭环的核心）

读取 `feedback.json`，聚焦用户有具体意见的用例（空反馈=满意，不必动）。官方给了四条改进心法，比"改文案"重要得多：

1. **从反馈里抽象出通用规律。** 你只在三五个例子上反复迭代是为了快，但目标是"被用上百万次"。别加针对个例的过拟合补丁，也别堆 MUST/NEVER；遇到顽固问题，换隐喻、换工作模式再试。
2. **保持提示精简。** 删掉不起作用的部分。读**完整 transcript** 而非只看最终输出——若发现 Skill 让模型在无意义环节空转，就把诱因删掉。
3. **讲清 why。** 尽量解释"为什么这么要求"，而不是堆全大写的命令。今天的模型有不错的 theory of mind，讲清理由比硬约束更有效。一旦你想写 `ALWAYS`/`NEVER`，就是黄灯——尝试改写成带原因的陈述。
4. **发现重复劳动就下沉成脚本。** 若三个用例的子任务都各自手写了一个"生成测试骨架"的小脚本，这就是强信号：把它写一次放进 `scripts/`，让 Skill 调用它，省掉未来每次调用的重复造轮子。

### iteration-1 假想结果与改进

| 用例 | 断言 | without_skill | with_skill(iter-1) |
|---|---|---|---|
| clamp-bounds | has-boundary-case | ✅ | ✅ |
| divide-errors | has-error-path | ⚠️ 命名随意 | ✅ |
| tax-brackets | descriptive-names | ❌ test1/test2 | ❌ 同样含糊 |

反馈显示：with_skill 在边界/异常上更稳，但**命名依然含糊**——因为草稿正文只说了"要行为描述命名"，没说为什么、也没给范式。改进措施：在正文补一段"为什么强调命名"的解释，并在 `references/naming.md` 放范式对照表；同时把骨架生成下沉为 `scripts/scaffold.py`。

改完进入 iteration-2：重跑全部用例（含基线），看板用 `--previous-workspace iteration-1` 并排对比，直到反馈全空或你满意为止。基线选择上——新建 Skill 时基线恒为"无 Skill"；改进已有 Skill 时，基线用你最初拿到的版本或上一迭代，按判断决定。

## 实战案例：从草稿到稳定的两次迭代

完整可跑的最小项目（Node 环境）：

```text
unit-test-gen/
├── SKILL.md
├── scripts/scaffold.py        # 生成 .test.js 骨架
├── references/naming.md
└── evals/evals.json
```

`scaffold.py` 核心逻辑（确定性、可复用）：

```python
# Generate a Jest test scaffold with behavior-descriptive placeholder names
import sys, pathlib
target = pathlib.Path(sys.argv[1])
out = target.with_suffix(".test" + target.suffix)
out.write_text(
    "import { describe, it, expect } from '@jest/globals';\n"
    f"import {{ /* exports from {target.name} */ }} from './{target.name}';\n\n"
    "describe('<unit under test>', () => {\n"
    "  it('returns <expected> when <condition>', () => {\n"
    "    // arrange / act / assert\n"
    "  });\n"
    "});\n"
)
```

验收：iteration-2 后 `descriptive-names` 对三条用例全部 pass，`has-boundary-case` 与 `has-error-path` 的 with_skill 通过率稳定高于 without_skill 一个标准差以上，且耗时/token 差异可接受。此时 Skill 才算"上线就绪"。

## 整体工作流

```mermaid
flowchart TD
    A[明确意图与触发] --> B[写 SKILL.md 草稿]
    B --> C[写 evals.json 测试用例<br/>先不写断言]
    C --> D[同一轮并发:<br/>with_skill + baseline]
    D --> E[运行间隙起草<br/>可客观验证断言]
    E --> F[打分 grading.json<br/>聚合 benchmark.json]
    F --> G[启动看板<br/>人工留反馈 feedback.json]
    G --> H{反馈全空<br/>或满意?}
    H -- 否 --> I[读 transcript 改写 Skill<br/>通用化/精简/讲why/下沉脚本]
    I --> D
    H -- 是 --> J[运行 description 优化器<br/>微调触发]
    J --> K[发布 Skill]
```

## 延伸阅读

- 官方 Skills 文档：`code.claude.com/docs/en/skills`——权限、共享、组织级 Skill 管理。
- 官方 skill-creator SKILL.md 源文件：`github.com/anthropics/claude-plugins-official`（本教程 Evals 闭环、断言/看板/反馈流程的权威出处）。
- 官方博客《Improving skill-creator: Test, measure, and refine Agent Skills》——evals 设计与 A/B 基准的理念。

## 训练营适用性评估

- **适合类型**：面向已具备基础 Claude Code 经验的进阶训练营（工程师/技术 Lead/平台工具团队），尤其"Agent 工程化""内部工具沉淀"方向。不适合零基础营——学员需先理解代理循环与会话上下文。
- **预计时长**：讲解 + 动手约 3.5–4 小时。其中 Skill 草稿 45 分钟、Evals 用例与断言设计 60 分钟、两轮迭代跑通 90 分钟、看板解读与改进讨论 45 分钟。
- **交付物**：每个学员/小组产出 1 个经过至少两轮 Evals 验证的 Skill，附带 `benchmark.md` 证据。
- **评分维度建议**：以"with_skill 相对 baseline 的增量 pass_rate"和"断言无区分度排查是否到位"作为客观考核点，而非"Skill 是否能触发"。

## 参考来源

1. [skill-creator SKILL.md（anthropics/claude-plugins-official）](https://github.com/anthropics/claude-plugins-official/blob/main/plugins/skill-creator/skills/skill-creator/SKILL.md) — 官方 Skill 创建与 Evals 迭代循环的权威流程、断言/看板/反馈规范出处
2. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — Skill 创建、管理与共享的官方文档
3. [Improving skill-creator: Test, measure, and refine Agent Skills](https://claude.com/blog/improving-skill-creator-test-measure-and-refine-agent-skills) — 官方关于用 evals 测试与改进 Skill 的方法论述
4. [SKILL.md frontmatter reference — ClaudSkills](https://claudskills.com/learn/skill-md-frontmatter-reference/) — name/description/model/allowed-tools 等 frontmatter 字段逐项说明
5. [The Complete Guide to Testing Claude Code Skills With the Skill Creator](https://tarobobaai.com/blog/skill-creator-complete-guide) — 25 个 Skill 审计与 7 个 Skill 实测的实践复盘，含断言设计与基准解读经验

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
