Agent Skill 设计与实践
创建 Skill

评估 Skill 的产出质量

采用评估驱动迭代,淬炼 Skill 的产出质量

你写了一个 Skill,拿几个提示词试了试,好像能用。但问题在于:它在不同类型的输入下是否都稳定?遇到边界情况会不会出错?相比不用 Skill,真的更好吗?

通过做一套结构化的评估方案,可以把这些问题说清楚。同时,它还能形成一个持续反馈的机制,让你有依据地、一步步把这个 Skill 打磨得更可靠。

设计测试用例

一个测试用例通常包含三部分:

  • 输入:一段真实用户可能会说的话,而不是理想化的指令
  • 期望输出:用人能看懂的方式描述“什么算成功”
  • 输入文件(可选):Skill 运行时需要用到的文件

你可以把这些测试用例放在 Skill 目录下的 evals/evals.json 文件中,例如:

evals.json
{
  "skill_name": "csv-analyzer",
  "evals": [
    {
      "id": 1,
      "prompt": "我有一份每月销售数据的 CSV 文件,路径是 data/sales_2025.csv。你能帮我找出收入最高的前三个月,并画一张柱状图吗?",
      "expected_output": "生成一张柱状图,展示收入最高的前三个月,并标注清晰的坐标轴和数值。",
      "files": ["evals/files/sales_2025.csv"]
    },
    {
      "id": 2,
      "prompt": "我下载文件夹里有个 customers.csv,有些行缺少邮箱,你能帮我清理一下,并告诉我缺了多少吗?",
      "expected_output": "输出一个清理后的 CSV(处理好缺失邮箱),并给出缺失邮箱的数量统计。",
      "files": ["evals/files/customers.csv"]
    }
  ]
}

写好测试用例的一些建议

  • 先从 2~3 个用例开始:不要一开始就写很多。先跑一轮看看效果,再逐步补充。
  • 让输入多样化:用不同的表达方式、不同的详细程度、不同语气。
    • 比如既有随意的:“帮我把这个 csv 清理一下”,
    • 也有严格的:“解析 data/input.csv,删除 B 列为空的行,并输出到 data/output.csv”。
  • 覆盖边界情况:至少要有一个测试是“刁钻”的:比如输入格式不规范、需求不常见,或者指令本身有歧义。
  • 贴近真实使用场景:真实用户会提到文件路径、字段名、上下文信息。

在设计测试简短先不用纠结怎么定义通过/失败的判断标准,只要把输入和期望结果写清楚就够了。

运行评估

核心思路很简单:每个测试用例都跑两次

  • 一次带上 Skill
  • 一次不带 Skill(或者用旧版本)

这样你就有了一个对照基线,可以清楚地看到 Skill 到底有没有带来提升

目录结构建议

评估结果建议单独放在一个 workspace 目录中,与 Skill 目录并列。

每完整跑一轮评估,就新建一个 iteration-N/ 目录;在这一轮里,每个测试用例都有自己的子目录,并且分成 with_skill/without_skill/ 两部分:

csv-analyzer/
├── SKILL.md
└── evals/
    └── evals.json

csv-analyzer-workspace/
└── iteration-1/
    ├── eval-top-months-chart/
    │   ├── with_skill/
    │   │   ├── outputs/       # 运行生成的文件结果
    │   │   ├── timing.json    # token 消耗和耗时
    │   │   └── grading.json   # 评估结果(断言是否通过)
    │   └── 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         # 汇总统计结果

哪些是你需要写的?

这个阶段你是在定义“测什么”,系统负责帮你记录“测出来什么”。 你真正需要手动维护的,其实只有一个文件: evals/evals.json(测试用例定义)

其他这些文件:

  • grading.json
  • timing.json
  • benchmark.json

都是在评估过程中自动生成的

如何执行每一轮评估

每一次评估运行,都应该在一个干净的上下文里开始——不能带着之前运行残留的状态,也不能带着你在开发 Skill 过程中的“记忆”。

这样可以确保:Agent 完全按照 SKILL.md 的定义来执行,而不是被历史上下文干扰。

  • 如果环境支持子代理(subagent,比如 Claude Code),这种隔离是天然存在的——每个子任务都是全新开始
  • 如果不支持,就需要手动为每次运行创建一个独立会话

每次运行需要提供什么

一次标准的运行,需要明确给 Agent 这些信息:

  • Skill 路径(对照组就不提供)
  • 测试用的输入
  • 输入文件(如果有)
  • 输出保存目录

你可以这样给 Agent 下指令,让它加载 Skill,执行任务,并把结果保存到指定目录:

执行以下任务:

* Skill 路径:/path/to/csv-analyzer
* 任务:我有一份包含每月销售数据的 CSV 文件,路径为 data/sales_2025.csv。
       请找出收入最高的 3 个月,并生成一张柱状图。
* 输入文件:evals/files/sales_2025.csv
* 输出保存至:csv-analyzer-workspace/iteration-1/eval-top-months-chart/with_skill/outputs/

对照组怎么跑

对照组的做法很简单:

  • 用完全相同的输入
  • 但不提供 Skill 路径
  • 输出保存到 without_skill/outputs/

这样你就能直接对比:有 Skill 和没 Skill,到底差在哪。

如果是在迭代已有 Skill

如果你是在优化一个已经存在的 Skill,那么对照组就不是“没有 Skill”,而是“旧版本 Skill”。

做法是:

  1. 在修改前先做一个快照
cp -r <skill-path> <workspace>/skill-snapshot/
  1. 对照组运行时,使用这个旧版本路径

  2. 输出目录改为:old_skill/outputs/

这样你比较的就是:“新版本 Skill” vs “旧版本 Skill”,更能准确评估你的改动到底有没有提升。

评估“值不值”

除了评估输出的正确性,还建议记录每次运行的耗时和 token 消耗等数据,这些都是评估 Skill 性能的重要指标。

通过记录每次运行的耗时和 token 消耗,你可以回答一个关键问题:提升效果,值不值这个代价?

例如:

  • 有的 Skill 明显提升了质量,但 token 用量翻了三倍
  • 有的 Skill 不仅效果更好,成本反而更低 这两种情况,决策是完全不同的。

每次运行需要记录的数据

每次结束结束后,记录两个指标:

{
  "total_tokens": 84852,
  "duration_ms": 23332
}
  • total_tokens:本次运行消耗的总 token
  • duration_ms:总耗时(毫秒)

编写断言

断言,就是一条条“验收标准”或“检查清单”。当你让 AI 做一件事时,你心里其实会有一些判断标准,比如:

  • 结果对不对
  • 有没有漏掉关键内容
  • 格式是不是符合要求 断言,就是把这些“心里的标准”,明确写出来,变成可以检查的规则。

举个例子,你点了一杯咖啡,你的“断言”可能是:

  • 是热的
  • 是拿铁(不是美式)
  • 有奶泡 这些就是你的验收标准。

只要有一条不满足,你就会觉得“不对”。

通常建议不要依赖就写“断言“,而是先跑一轮结果,再写断言。因为在没看到实际输出前,你可能并不清楚“什么才算好”。

什么是好的断言

好的断言有一个共同点:可以客观判断,对或错很明确

例如:

  • “输出文件是合法的 JSON” → 可以程序校验
  • “柱状图包含坐标轴标签” → 可以直接观察
  • “报告至少包含 3 条建议” → 可以计数

什么是无效(或低质量)的断言

问题通常出在两个方向:太模糊,或太死板

例如:

  • “输出很好” → 没法判断好在哪里
  • “必须包含 ‘合计: ¥X’ 这句话” → 太脆弱,换个表达就误判失败

不是什么都要写断言

有些维度本来就不适合做“对/错判断”,比如:

  • 写作风格
  • 视觉设计
  • 整体是否“感觉对”

这些更适合留给人工评审,而不是强行写成规则。只有那些具有客观标准的部分,才适合写断言。

如何写进测试用例

evals/evals.json 中,为每个测试用例加上 assertions 字段,例如:

evals.json
{
  "skill_name": "csv-analyzer",
  "evals": [
    {
      "id": 1,
      "prompt": "我有一个位于 data/sales_2025.csv 的月度销售数据 CSV 文件。你能找出按收入排序的前 3 个月,并生成一个柱状图吗?",
      "expected_output": "一个柱状图图片,展示按收入排名的前 3 个月,并带有标注清晰的坐标轴和数值。",
      "files": ["evals/files/sales_2025.csv"],
      "assertions": [
        "输出中包含一个柱状图文件",
        "图表中恰好展示 3 个月的数据",
        "两个坐标轴都有标签",
        "图表标题或说明中提到了收入"
      ]
    }
  ]
}

打分

打分的过程,其实就是:拿你的“检查清单(断言)”,逐条去对照实际输出,看是通过还是不通过。

每一条断言都需要记录:

  • 通过 / 不通过
  • 证据:说明你为什么这么判断

注意:证据要引用实际输出内容,而不是主观评价。

怎么做打分

最简单的方式是:把“输出结果 + 断言”一起交给一个 LLM,让它逐条判断

但要注意,为避免“先入为主“的偏见,最好采用盲测对比的方式:

  • 把两个输出一起给一个 LLM
  • 不告诉它哪个是新版本,哪个是旧版本
  • 让它从整体质量去评分(结构、可读性、完成度等)

除了让 LLM 打分,有一些情况更适合用代码来判断,比如:

  • JSON 是否合法
  • 行数是否正确
  • 文件是否存在、大小是否符合预期

这些“机械性检查”,用脚本更稳定,而且可以复用

下面是一个打分结果的示例格式:

{
  "assertion_results": [
    {
      "text": "输出中包含一个柱状图文件",
      "passed": true,
      "evidence": "在 outputs 目录中找到 chart.png(45KB)"
    },
    {
      "text": "图表中恰好展示 3 个月的数据",
      "passed": true,
      "evidence": "图中包含 March、July、November 三个柱子"
    },
    {
      "text": "两个坐标轴都有标签",
      "passed": false,
      "evidence": "Y 轴标注为 'Revenue ($)',但 X 轴没有标签"
    },
    {
      "text": "标题或说明中提到了收入",
      "passed": true,
      "evidence": "标题为 'Top 3 Months by Revenue'"
    }
  ],
  "summary": {
    "passed": 3,
    "failed": 1,
    "total": 4,
    "pass_rate": 0.75
  }
}

打分时的几个原则

1. “通过“必须有“硬证据”

不要“差不多就算通过”。

例如:

  • 断言是“包含总结”
  • 结果里确实有个标题叫 Summary,但只有一句空话

种应该判“失败“,因为形式有了,但内容不达标

2. 顺便检查“断言本身”

打分的时候,不只是看结果,也要反过来看断言是否合理:

  • 太容易:不管 Skill 好坏都能通过
  • 太苛刻:结果已经不错了还是通不过
  • 无法验证:从输出里根本看不出来

汇总结果

当一轮所有测试用例都完成打分后,需要做一件事:把零散结果汇总成整体结论。

具体就是:按不同配置(比如“有 Skill / 没 Skill”)计算一些关键指标,并保存到 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
    }
  }
}

怎么理解这些数据?

可以分三层来看:

1. 各自表现

每一组都会统计:

  • 通过率(pass_rate)
  • 耗时(time_seconds)
  • token 消耗(tokens)

并且给出:

  • 平均值(mean)
  • 波动范围(stddev,标准差)

2. 最关键的是差值(delta)

delta 本质是在回答一个问题: 这个 Skill,多花了多少成本,换来了多少收益?

比如上面的例子:

  • 通过率 +50%(明显提升)
  • 多花 13 秒
  • 多用 1700 tokens

3. 如何做判断?

可以用一个很实际的标准:

  • 小成本换大提升 → 值得
  • 大成本换小提升 → 可能不值

例如:

  • +13 秒换 +50% 通过率 → 很可能值得
  • token 翻倍只提升 2% → 基本不划算

关于标准差

标准差衡量的大家离“平均水平”有多散,判断的是结果“稳不稳”,波动有多大。但如果你只是跑了 2~3 个测试用例,每个只跑一次,那标准差基本没什么意义

这时候更应该关注:

  • 实际通过了多少条
  • 差值(delta)是多少

当你:

  • 测试用例变多
  • 每个用例多跑几次后

这个时候才需要引入标准差。

分析问题模式

数据不是唯一需要关注的,如果我们只看整体统计(比如平均通过率)往往会掩盖很多关键问题。 所以在看完 benchmark 之后,需要进一步“拆开看”。

可以重点关注这几类情况:

1. 总是通过的断言

如果某条断言:

  • 有 Skill 通过
  • 没 Skill 也通过

说明它没有区分度,本身没什么价值

这种断言会“虚高”通过率,但并不能说明 Skill 真有提升。 建议直接删除或替换。

2. 总是失败的断言

如果某条断言:

  • 有 Skill 也失败
  • 没 Skill 也失败

通常有三种可能:

  • 断言本身不合理(要求模型做不到的事)
  • 测试用例太难
  • 检查点写错了

这类断言需要修掉,否则后面的评估没有意义。

3. 有 Skill 才能通过的断言

这是最有价值的一类:

  • 有 Skill → 通过
  • 没 Skill → 失败

说明 Skill 确实带来了提升

关键是要进一步理解:

  • 是哪一段指令起作用?
  • 是哪个脚本/结构在发挥作用?

这部分是你优化 Skill 的核心依据。

4. 同一个测试结果不稳定

如果一个测试:

  • 有时通过,有时失败
  • benchmark 里表现为标准差(stddev)较高

可能的原因通常有两个:

  • 测试本身“太随机”(对模型波动敏感)
  • Skill 写得不够清晰,模型每次理解不一样

解决方式:

  • 补充示例
  • 把指令写得更具体,减少歧义

5. 异常的耗时或 token

如果某个测试:比其他测试慢很多(比如 3 倍),就需要重点排查

做法是:

  • 查看完整执行日志
  • 找出卡在哪里(比如重复推理、无效步骤等)

人工复核

无论是断言和统计,都有一个继局限:它们只能检查“你提前想到的东西”,而人可以发现:

  • 没被覆盖的问题
  • “看起来对,但其实不对”的结果
  • 很难写成规则的问题(比如表达是否合理)

怎么做人工复核

对每个测试用例:

  • 看实际输出
  • 对照打分结果
  • 记录具体反馈

可以存成一个 feedback.json,例如:

feedback.json
{
  "eval-top-months-chart": "图表缺少坐标轴标签,而且月份是按字母排序的,不是按时间顺序。",
  "eval-clean-missing-emails": ""
}

什么是“好的反馈”

好的反馈应该是:

  • 具体的
  • 可以改的

例如: “缺少坐标轴标签” ,而“看起来不太好”就不够具体,无法指导改进。

如果这个测试结果通过了你的人工复核,就直接记录一个空字符串,表示没有问题。

迭代优化

在完成打分和复核之后,你手里其实有三类很有价值的信息:

1. 断言失败

它告诉你:哪里具体有问题

比如:

  • 少了某一步
  • 指令不清晰
  • 某类情况没覆盖到

这是最“具体、可定位”的问题来源

2. 人工反馈

它更多反映的是:整体质量问题

比如:

  • 方法不对
  • 结构混乱
  • 虽然 technically 正确,但没什么用

这些问题通常比较“抽象”,但很关键

3. 执行日志

它告诉你:为什么会出问题

比如:

  • Agent 忽略了某条指令 → 说明指令可能有歧义
  • 花了很多时间做无用步骤 → 说明流程设计有问题

这是理解“过程”的唯一依据

这时候,你可以把这三类信息 + 当前的 SKILL.md 一起交给 LLM,让它提出修改建议。

原因是:

  • LLM 更容易发现跨多个测试用例的模式
  • 人手动总结这些关联,会比较费力

给 LLM 提示时的几个原则

1. 不要只修个例,要解决“底层问题”

Skill 是要应对各种输入的,不只是当前这些测试用例。所以 优化要“泛化”,而不是针对某个用例打补丁

2. 保持精简(Lean)

规则不是越多越好,如果执行日志里出现:

  • 多余的校验
  • 无意义的中间步骤

就应该删掉

如果规则越加越多,但效果没提升:很可能已经“过度约束”了,反而限制了模型

3. 解释“为什么”,而不是死规定

相比: ❌ “必须这样做,绝不能那样做”

更有效的是: ✅ “这样做是因为那样会导致某种问题”

模型理解“原因”,执行会更稳定

4. 把重复工作沉淀下来

如果你发现:每次运行都在重复写类似的脚本(比如画图、解析数据)

说明这些应该:抽出来放进 scripts/ 目录,变成 Skill 的一部分

标准迭代流程

整个过程可以抽象成一个循环:

  1. 把评估结果 + 当前 Skill 交给 LLM,让它给出优化建议
  2. 人来审核并修改 Skill
  3. 在新的 iteration-N+1/ 目录中重新跑所有测试
  4. 重新打分并汇总
  5. 再做人工复核
  6. 重复这个过程

什么时候可以停

可以考虑停止迭代的几个信号:

  • 结果已经达到你的预期
  • 人工反馈基本为空(没有明显问题)
  • 多轮迭代后,提升已经不明显

On this page