评估 Skill 输出质量
编写 Skill 只是第一步,持续评估和改进是确保 Skill 有效性的关键。本章介绍如何系统地评估 Skill 的输出质量,并通过迭代优化提升效果。
为什么需要评估?
Skill 的初稿通常需要优化。你可能面临以下问题:
- 触发问题:Skill 在不该触发时触发,或应该触发时不触发
- 输出不稳定:同样的输入产生不同质量的输出
- 指令模糊:Agent 理解指令的方式与预期不同
- 遗漏场景:某些边界情况未被覆盖
系统化的评估可以帮助你发现这些问题并持续改进。
评估框架概述
评估 Skill 的核心模式是:对每个测试用例运行两次——一次使用 Skill,一次不使用(或使用旧版本)。这提供了一个可比较的基准线。
评估流程
设计测试用例
一个测试用例包含三个部分:
| 组成部分 | 说明 |
|---|---|
| 提示词 | 真实的用户消息——人们实际会输入的内容 |
| 预期输出 | 用人类可读的语言描述成功是什么样的 |
| 输入文件(可选) | Skill 需要处理的文件 |
测试用例结构
将测试用例存储在 Skill 目录下的 evals/evals.json 中:
[
{
"id": "basic-review",
"prompt": "请审查这段 Python 代码:def add(a,b): return a+b",
"expected_output": "识别缺少类型注解、文档字符串、空格问题",
"input_files": []
},
{
"id": "complex-review",
"prompt": "审查 src/services/user_service.py,重点关注错误处理",
"expected_output": "检查异常处理、None 检查、资源管理",
"input_files": ["src/services/user_service.py"]
}
]
编写好的测试提示词
从 2-3 个测试用例开始。在看到第一轮结果之前不要过度投入。稍后可以扩展测试集。
变化提示词的表达方式。使用不同的措辞、详细程度和正式程度:
随意风格: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
覆盖边界情况。至少包含一个测试边界条件的提示——格式错误的输入、不寻常的请求,或 Skill 指令可能模糊的情况。
使用真实的上下文。真实用户会提及文件路径、列名和个人上下文。像"处理这个数据"这样的提示太模糊,无法测试任何有用的内容。
工作区结构
组织评估结果在工作区目录中,与 Skill 目录并列。每次完整的评估循环有自己的 iteration-N/ 目录:
csv-analyzer/
├── SKILL.md
├── evals/
│ └── evals.json # 测试用例定义
└── ...
csv-analyzer-workspace/
├── iteration-1/
│ ├── eval-basic-clean/ # 测试用例 1
│ │ ├── with_skill/
│ │ │ └── outputs/
│ │ └── without_skill/
│ │ └── outputs/
│ ├── eval-complex-parse/ # 测试用例 2
│ │ ├── with_skill/
│ │ └── without_skill/
│ ├── benchmark.json # 汇总统计
│ └── timing.json # 时间和 token 数据
└── iteration-2/
└── ...
运行评估
生成运行
每次评估运行应该从干净的上下文开始——没有前一次运行或 Skill 开发过程中的残留状态。这确保 Agent 只遵循 SKILL.md 告诉它的内容。
使用子代理:在支持子代理的环境中(如 Claude Code),这种隔离是自然的:每个子任务从干净状态开始。
使用独立会话:如果没有子代理,每次运行使用单独的会话。
对于每次运行,提供:
- Skill 路径(或不使用 Skill 的基准线)
- 测试提示词
- 任何输入文件
- 输出目录
捕获时间和 Token 数据
时间数据让你比较 Skill 相对于基准线花费了多少时间和 token:
{
"eval_id": "basic-clean",
"with_skill": {
"tokens": 4500,
"duration_seconds": 12
},
"without_skill": {
"tokens": 2800,
"duration_seconds": 8
}
}
一个显著提高输出质量但使 token 使用量增加三倍的 Skill,与一个既更好又更便宜的 Skill,是不同的权衡。
编写断言
断言是关于输出应该包含或实现的可验证陈述。在看到第一轮输出后添加它们——在 Skill 运行之前,你通常不知道"好"是什么样子的。
好的断言
| 断言 | 说明 |
|---|---|
"输出文件是有效的 JSON" | 可编程验证 |
"条形图有标签化的坐标轴" | 具体且可观察 |
"报告包含至少 3 条建议" | 可计数 |
不好的断言
| 断言 | 问题 |
|---|---|
"输出是好的" | 太模糊,无法评分 |
"输出使用确切短语 'Total Revenue: $X'" | 太脆弱,正确但不同措辞的输出会失败 |
添加断言到测试用例
{
"id": "basic-review",
"prompt": "请审查这段 Python 代码",
"expected_output": "识别代码质量问题",
"assertions": [
"输出包含问题列表",
"每个问题都有行号和修改建议",
"输出以总体评价结束",
"问题按严重程度排序"
]
}
评分输出
评分意味着针对实际输出评估每个断言,并记录 PASS 或 FAIL 及具体证据。证据应该引用或参考输出,而不仅仅是陈述观点。
评分原则
要求 PASS 的具体证据。不要给予好处。如果断言说"包含摘要"而输出有一个标题为"摘要"的部分但只有一句模糊的句子,那是 FAIL——标签存在但实质不存在。
审查断言本身。在评分时,注意断言是否:
- 太容易(无论 Skill 质量如何总是通过)
- 太难(即使输出很好也总是失败)
- 不可验证(无法从输出中检查)
评分结果示例
{
"eval_id": "basic-review",
"configuration": "with_skill",
"results": [
{
"assertion": "输出包含问题列表",
"verdict": "PASS",
"evidence": "输出有'### 问题列表'部分,包含 5 个问题"
},
{
"assertion": "每个问题都有行号和修改建议",
"verdict": "FAIL",
"evidence": "问题 3 和 5 缺少行号,只有问题描述"
}
]
}
使用脚本验证
对于可以通过代码检查的断言(有效 JSON、正确的行数、文件存在且尺寸符合预期),使用验证脚本——脚本对于机械检查比 LLM 判断更可靠,并且可跨迭代重用。
汇总结果
一旦迭代中的每次运行都被评分,计算每个配置的汇总统计:
{
"iteration": 1,
"configuration": "with_skill",
"summary": {
"total_assertions": 12,
"passed": 10,
"failed": 2,
"pass_rate": 0.83
},
"delta": {
"pass_rate": 0.25,
"tokens": 1700,
"duration_seconds": 4
}
}
delta 告诉你 Skill 的成本(更多时间、更多 token)和收益(更高的通过率)。
一个增加 13 秒但将通过率提高 50 个百分点的 Skill 可能是值得的。一个将 token 使用量翻倍但只提高 2 个百分点的 Skill 可能不值得。
分析模式
汇总统计可能隐藏重要的模式。在计算基准后:
清理无信息的断言
移除或替换在两种配置中总是通过的断言。这些没有告诉你任何有用的东西——模型在没有 Skill 的情况下也能很好地处理它们。它们夸大了使用 Skill 的通过率,而没有反映实际的 Skill 价值。
调查总是失败的断言
调查在两种配置中总是失败的断言。可能是:
- 断言有问题(要求模型做不到的事情)
- 测试用例太难
- 断言在检查错误的东西
在下次迭代前修复这些。
研究有价值的断言
研究使用 Skill 通过但不使用 Skill 失败的断言。这是 Skill 明显增加价值的地方。理解为什么——哪些指令或脚本产生了差异?
解决不一致性
当结果在运行间不一致时收紧指令。如果同一个评估有时通过有时失败(在基准中反映为高 stddev),可能是:
- 评估不稳定(对模型随机性敏感)
- Skill 的指令足够模糊,模型每次解释不同
添加示例或更具体的指导以减少歧义。
检查异常值
检查时间和 token 异常值。如果一个评估花费的时间是其他的三倍,阅读其执行转录(模型在运行期间所做的一切的完整日志)以找到瓶颈。
人工审查
断言评分和模式分析捕捉了很多,但它们只检查你想到写断言的内容。人工审查者带来全新的视角——捕捉你没有预料到的问题,注意到输出在技术上正确但偏离了要点,或发现难以表达为通过/失败检查的问题。
对于每个测试用例,审查实际输出以及评分。记录每个测试用例的具体反馈:
{
"eval_id": "basic-review",
"feedback": "输出正确识别了问题,但建议太笼统。应该提供具体的代码示例来修复每个问题。",
"rating": 6,
"improvement_suggestions": [
"为每个问题添加代码示例",
"按严重程度分组问题"
]
}
迭代改进
在评分和审查后,你有三个信号来源:
| 信号来源 | 指示的内容 |
|---|---|
| 失败的断言 | 特定的差距——缺失的步骤、不清晰的指令、Skill 未处理的情况 |
| 人工反馈 | 更广泛的质量问题——方法错误、输出结构差、技术上正确但无帮助 |
| 执行转录 | 为什么出错了——Agent 忽略了指令(指令可能模糊),Agent 在无用的步骤上浪费时间(指令可能需要简化或移除) |
使用 LLM 提出改进建议
将评估信号——评分结果、反馈和当前的 SKILL.md——提供给 LLM,要求它提出修改建议。LLM 可以综合跨失败断言、审查者投诉和转录行为的模式,这些手动连接会很繁琐。
迭代指南
从反馈中泛化。Skill 将用于许多不同的提示词,不仅仅是测试用例。修复应该广泛解决潜在问题,而不是为特定示例添加狭窄的补丁。
保持 Skill 精简。更少、更好的指令通常优于详尽的规则。如果转录显示浪费的工作(不必要的验证、不需要的中间输出),移除这些指令。如果通过率在添加更多规则后趋于平稳,Skill 可能被过度约束——尝试移除指令,看看结果是否保持或改善。
解释原因。基于推理的指令("做 X 因为 Y 往往会导致 Z")比僵化的指令("总是做 X,永远不做 Y")效果更好。当模型理解目的时,更可靠地遵循指令。
捆绑重复工作。如果每次测试运行都独立编写类似的辅助脚本(图表构建器、数据解析器),那就是将脚本捆绑到 Skill 的 scripts/ 目录的信号。
评估循环
- 将评估信号和当前
SKILL.md提供给 LLM,要求提出改进建议 - 审查并应用更改
- 在新的
iteration-<N+1>/目录中重新运行所有测试用例 - 评分并汇总新结果
- 与人工审查。重复。
最佳实践总结
| 实践 | 说明 |
|---|---|
| 从小开始 | 2-3 个测试用例,在看到结果前不要过度投入 |
| 变化提示词 | 使用不同措辞、详细程度、正式程度 |
| 覆盖边界 | 测试格式错误的输入、不寻常的请求 |
| 使用真实上下文 | 包含文件路径、列名等真实信息 |
| 要求具体证据 | PASS 需要具体证据,不要给予好处 |
| 审查断言质量 | 移除总是通过的,修复总是失败的 |
| 保持精简 | 更少更好的指令 > 详尽的规则 |
| 解释原因 | 基于推理的指令效果更好 |