Skip to main content
你编写了一个技能,在一个提示词上进行了尝试,它似乎有效。但它是否可靠地工作——在各种提示词下、在边界情况中、比根本没有技能更好?运行结构化评估(evals)可以回答这些问题,并为你提供一个系统改进技能的反馈循环。

设计测试用例

一个测试用例包含三个部分:
  • 提示词:一个真实的用户消息——某人实际会输入的内容。
  • 预期输出:关于成功样子的可读描述。
  • 输入文件(可选):技能需要处理的文件。
将测试用例存储在你的技能目录内的 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"]
    }
  ]
}
编写良好测试提示词的技巧:
  • 从 2-3 个测试用例开始。 在看到第一轮结果之前不要过度投入。你可以稍后扩展集合。
  • 变化提示词。 使用不同的措辞、详细程度和正式程度。有些提示词应该随意(“嘿,你能清理一下这个 csv 吗”),有些应该精确(“解析 data/input.csv 处的 CSV,丢弃列 B 为 null 的行,并将结果写入 data/output.csv”)。
  • 覆盖边界情况。 包含至少一个测试边界条件的提示词——格式错误的输入、不寻常的请求,或技能指令可能模糊的情况。
  • 使用真实的上下文。 真实用户会提到文件路径、列名和个人上下文。像“处理这个数据”这样的提示词太模糊,无法测试任何有用的东西。
暂时不用担心定义具体的通过/失败检查——只需提示词和预期输出。在你看到第一次运行产生什么之后,你将添加详细的检查(称为断言)。

运行评估

核心模式是将每个测试用例运行两次:一次使用技能,一次不使用它(或使用旧版本)。这为你提供了一个比较的基线。

工作区结构

在你的技能目录旁边的工作区目录中组织评估结果。每次通过完整的评估循环都会有自己的 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。其他 JSON 文件(grading.jsontiming.jsonbenchmark.json)是在评估过程中产生的——由智能体、脚本或你生成。

启动运行

每次评估运行都应该从干净的上下文开始——没有来自先前运行或技能开发过程的遗留状态。这确保智能体只遵循 SKILL.md 告诉它的内容。在支持子智能体的环境中(例如 Claude Code),这种隔离是自然的:每个子任务都是从头开始。如果没有子智能体,为每次运行使用单独的会话。 对于每次运行,提供:
  • 技能路径(基线则无技能)
  • 测试提示词
  • 任何输入文件
  • 输出目录
以下是你为单次使用技能运行提供给智能体的指令示例:
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/
对于基线,使用相同的提示词但不带技能路径,保存到 without_skill/outputs/ 当改进现有技能时,使用旧版本作为基线。在编辑之前对其进行快照(cp -r <skill-path> <workspace>/skill-snapshot/),将基线运行指向快照,并保存到 old_skill/outputs/ 而不是 without_skill/

捕获计时数据

计时数据让你可以比较技能相对于基线消耗了多少时间和 token——一个显著改善输出质量但使 token 使用量翻三倍的技能,与一个既更好又更便宜的技能是不同的权衡。当每次运行完成时,记录 token 数量和持续时间:
timing.json
{
  "total_tokens": 84852,
  "duration_ms": 23332
}
在 Claude Code 中,当子智能体任务完成时,任务完成通知 包含 total_tokensduration_ms。立即保存这些值——它们不会持久化到其他地方。

编写断言

断言是关于输出应该包含或实现内容的可验证陈述。在你看到第一轮输出后添加它们——你通常直到技能运行后才知道“好”是什么样子的。 良好的断言:
  • "输出文件是有效的 JSON" — 可通过程序验证。
  • "条形图有标记的轴" — 具体且可观察。
  • "报告包含至少 3 个建议" — 可计数。
薄弱的断言:
  • "输出很好" — 太模糊,无法评分。
  • "输出完全使用短语 '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"
      ]
    }
  ]
}

评分输出

评分意味着针对实际输出评估每个断言,并记录 通过失败 以及具体证据。证据应该引用或参考输出,而不仅仅是陈述意见。 最简单的方法是将输出和断言提供给 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
  }
}

评分原则

  • 要求通过的具体证据。 不要给予怀疑的利益。如果断言说“包含摘要”而输出有一个标题为”Summary”的部分,只有一句模糊的句子,那是失败——标签在那里但实质不在。
  • 审查断言本身,而不仅仅是结果。 在评分时,注意断言是否太容易(无论技能质量如何总是通过)、太难(即使输出良好也总是失败)或无法验证(仅从输出无法检查)。为下一次迭代修复这些问题。
对于比较两个技能版本,尝试 盲测:将两个输出都呈现给 LLM 评判者,而不透露哪个来自哪个版本。评判者根据其自己的评分标准对整体品质——组织、格式、可用性、润色——进行评分,不受关于哪个版本“应该”更好的偏见影响。这补充了断言评分:两个输出可能都通过所有断言,但在整体质量上显著不同。

汇总结果

一旦迭代中的每次运行都完成评分,计算每个配置的汇总统计信息,并将它们保存到 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 告诉你技能的代价(更多时间、更多 token)以及它带来的收益(更高的通过率)。一个增加 13 秒但将通过率提高 50 个百分点的技能可能是值得的。一个使 token 用量加倍但仅提高 2 个百分点的技能可能不值得。
标准差 (stddev) 仅在每次评估有多次运行时才有意义。在只有 2-3 个测试用例和单次运行的早期迭代中,关注原始通过计数和 delta —— 随着你扩展测试集并多次运行每个评估,统计措施会变得有用。

分析模式

汇总统计可能会掩盖重要的模式。计算基准测试后:
  • 移除或替换在两种配置中始终通过的断言。 这些不能告诉你任何有用的信息 —— 模型在没有技能的情况下也能很好地处理它们。它们虚高了带技能的通过率,而没有反映实际技能价值。
  • 调查在两种配置中始终失败的断言。 要么是断言坏了(要求模型做不到的事情),要么是测试用例太难,要么是断言检查了错误的内容。在下次迭代之前修复这些问题。
  • 研究带技能通过但不带技能失败的断言。 这是技能明显增加价值的地方。理解为什么 —— 哪些指令或脚本起了作用?
  • 当运行结果不一致时收紧指令。 如果同一个评估有时通过有时失败(在基准测试中反映为高 stddev),评估可能不稳定(对模型随机性敏感),或者技能的指令可能足够模糊,导致模型每次解释不同。添加示例或更具体的指导以减少模糊性。
  • 检查时间和 token 异常值。 如果一个评估花费的时间是其他的 3 倍,阅读其执行记录(模型在运行期间所做的事情的完整日志)以找到瓶颈。

与人工一起审查结果

断言评分和模式分析能捕捉到很多问题,但它们只检查你想到要编写断言的内容。人工审查员带来全新的视角 —— 捕捉你没预料到的问题,注意到输出技术上正确但未切中要点时,或发现难以用通过/失败检查来表达的问题。对于每个测试用例,结合评分审查实际输出。 记录每个测试用例的具体反馈,并将其保存在工作区中(例如,作为 feedback.json 与评估目录放在一起):
feedback.json
{
  "eval-top-months-chart": "图表缺少轴标签,且月份按字母顺序排列而不是时间顺序。",
  "eval-clean-missing-emails": ""
}
“图表缺少轴标签” 是可操作的;“看起来不好” 则不是。空反馈意味着输出看起来没问题 —— 该测试用例通过了你的审查。在 迭代步骤 期间,将改进重点放在你有具体投诉的测试用例上。

迭代技能

评分和审查后,你有三个信号来源:
  • 失败的断言 指向具体的差距 —— missing 步骤、不清楚的指令,或技能未处理的情况。
  • 人工反馈 指向更广泛的质量问题 —— 方法错误、输出结构糟糕,或技能产生了技术上正确但无帮助的结果。
  • 执行记录 揭示事情出错的原因。如果代理忽略了指令,指令可能模糊。如果代理花费时间在没有成效的步骤上,这些指令可能需要简化或移除。
将这些信号转化为技能改进的最有效方法是将所有这三个——连同当前的 SKILL.md——交给 LLM 并要求它提出更改。LLM 可以综合失败断言、审查员投诉和记录行为中的模式,这些模式手动连接会很繁琐。提示 LLM 时,包含以下指南:
  • 从反馈中概括。 技能将用于许多不同的提示,而不仅仅是测试用例。修复应广泛解决根本问题,而不是为特定示例添加狭窄的补丁。
  • 保持技能精简。 更少、更好的指令通常优于详尽的规则。如果记录显示浪费的工作(不必要的验证、不需要的中间输出),移除这些指令。如果尽管添加了更多规则通过率仍停滞不前,技能可能约束过多 —— 尝试移除指令,看看结果是否保持或改善。
  • 解释原因。 基于推理的指令(“做 X,因为 Y 往往会导致 Z”)比僵硬的指令(“总是做 X,绝不做 Y”)效果更好。当模型理解目的时,它们更可靠地遵循指令。
  • 打包重复工作。 如果每次测试运行都独立编写类似的辅助脚本(图表构建器、数据解析器),这是一个将脚本打包到技能的 scripts/ 目录的信号。参见 使用脚本 了解如何执行此操作。

循环

  1. 将评估信号和当前的 SKILL.md 交给 LLM 并要求它提出改进。
  2. 审查并应用更改。
  3. 在新的 iteration-<N+1>/ 目录中重新运行所有测试用例。
  4. 评分并汇总新结果。
  5. 与人工一起审查。重复。
当你对结果满意时停止,反馈持续为空,或者你在迭代之间不再看到有意义的改进。
skill-creator 技能自动化了这个工作流的许多部分 —— 运行评估、评分断言、汇总基准测试,并呈现结果供人工审查。