首页 › tutorials › 用Evals闭环打磨你的首个Skill

概述

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

前置条件

核心概念: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):

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

SKILL.md 草稿:

---
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:

{
  "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 时基线换成旧版本快照)。这样两组几乎同时完成,省墙钟时间。

工作区按迭代组织:

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,先留空断言:

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

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

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

第五步:趁运行间隙写断言

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

能脚本判定的就写脚本判定(快、稳、可复用),别靠肉眼。断言写进 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')" } ] }

  1. 聚合基准:在 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 前面。

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

  2. 启动看板:

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 环境):

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

scaffold.py 核心逻辑(确定性、可复用):

# 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 才算"上线就绪"。

整体工作流

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]

延伸阅读

训练营适用性评估

参考来源

  1. skill-creator SKILL.md(anthropics/claude-plugins-official) — 官方 Skill 创建与 Evals 迭代循环的权威流程、断言/看板/反馈规范出处
  2. Extend Claude with skills — Claude Code Docs — Skill 创建、管理与共享的官方文档
  3. Improving skill-creator: Test, measure, and refine Agent Skills — 官方关于用 evals 测试与改进 Skill 的方法论述
  4. SKILL.md frontmatter reference — ClaudSkills — name/description/model/allowed-tools 等 frontmatter 字段逐项说明
  5. The Complete Guide to Testing Claude Code Skills With the Skill Creator — 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学通信知识星球海报-扫码加入