你最后一次验证 Skill 是怎么做的?
写完一个 Skill,手动触发了几次,输出看起来还不错——然后就上线了。
这大概是大多数人验证 Skill 的完整流程。说出来有点惭愧,但确实如此。我们在写普通代码时会写单元测试、跑 CI,但到了 Skill 这里,突然回到了"凭感觉"的时代。
问题不是懒。问题是我们不知道 Skill 会以哪些方式悄悄失效,也没有一套语言来描述"什么叫做好"。
这篇文章就是要解决这两件事:先把 Skill 的失效路径说清楚,再用这套失效地图反推出验证体系。
Skill 会怎么失效?
在聊怎么测之前,先想清楚有什么可能出错。Skill 的失效通常沿着四条路径发生,而且往往是静悄悄的——没有报错,只有"结果不对劲"。
路径一:根本没触发
这是最隐蔽的失效。用户明明说了"帮我格式化一下代码",但 Agent 没有调用你的代码格式化 Skill,而是直接用模型自己的知识瞎改了一通。
原因通常在 SKILL.md 的 description 字段写得太模糊——太宽泛会和其他 Skill 冲突,太窄又会漏掉很多合理的触发场景。更棘手的是,这种失效在手动测试时很难发现,因为你测试的时候往往用的是最标准的触发语句,而用户在真实场景里的表达千变万化。
路径二:触发了,但任务没完成
Skill 被调用了,工具也跑了,但最终该做的事没做到。比如:应该创建三个文件,结果只创建了两个;应该执行一段迁移脚本,结果中途退出了。
这类失效叫做 Outcome 失效,是最直接、影响最大的一类。用户最终看到的就是结果——结果不对,Skill 就等于没用。
路径三:结果对了,但过程走歪了
这一类更隐蔽。最终输出看起来没问题,但 Agent 的执行路径是错的:调用了不该调的工具、步骤顺序乱了、绕了一大圈弯路才完成任务。
举个例子:一个数据库迁移 Skill,正确的顺序应该是"备份 → 迁移 → 验证"。如果 Agent 先迁移再备份,输出文件可能是对的,但下次迁移失败时你就没有可用的备份了。这是 Process 失效,在纯结果导向的验证里完全看不出来。
路径四:完成了,但质量不达标
任务完成了,过程也对了,但:生成的代码风格和项目规范不一致;Commit message 格式不符合团队约定;用了 500 个 token 完成了本可以用 100 个搞定的任务。
这是 Style 和 Efficiency 失效,不会直接报错,但会在团队协作和成本控制上慢慢积累成大问题。
定义"成功":四个维度的验证目标
这四条失效路径,正好对应四类成功标准。确认过这四个维度,才算真正定义清楚了"这个 Skill 应该做到什么"。
| 维度 | 对应失效 | 核心问题 |
|---|---|---|
| Outcome(结果) | 任务没完成 | 该做的事做了吗? |
| Process(过程) | 执行路径错误 | 用对工具了吗?顺序对吗? |
| Style(风格) | 质量不达标 | 输出符合规范吗? |
| Efficiency(效率) | 资源浪费 | 有没有绕弯路?token 用量合理吗? |
接下来,针对每个维度,介绍对应的验证方法。
验证 Outcome:确定性检查
Outcome 是最容易量化的维度,适合用确定性检查(Deterministic Grader)——即通过解析运行日志或检查文件系统状态,来判断任务是否完成。
先建一个小测试集
不需要几百条用例,10~20 条就够了。但要覆盖三种场景:
显式触发:"/use code-formatter 帮我格式化这个文件"
隐式触发:"这段代码格式有点乱,能帮我整理一下吗?"
负样本: "帮我写一个排序算法"(不应该触发格式化 Skill)
负样本尤其重要,它检验的是 Skill 有没有被错误触发——过度触发和没触发一样是问题。
用 JSON 输出做确定性断言
codex exec --json 会输出结构化的运行日志(JSONL 格式),包含每次工具调用的详情。针对 Outcome 验证,可以这样检查:
import json, sys
# 加载运行日志
with open("run_output.jsonl") as f:
events = [json.loads(line) for line in f]
# 检查目标文件是否被创建
created_files = [
e["path"] for e in events
if e.get("type") == "file_write"
]
expected = ["src/index.ts", "src/types.ts", "README.md"]
missing = [f for f in expected if f not in created_files]
if missing:
print(f"❌ FAIL: 以下文件未被创建: {missing}")
sys.exit(1)
else:
print("✅ PASS: 所有预期文件均已创建")
这类检查有一个好处:它是确定性的,不依赖模型判断,运行结果稳定可重复。Outcome 层的 Eval 应该尽量用这种方式,把主观判断留给后面的 Rubric 评分。
验证 Process:工具调用序列检查
Outcome 检查告诉你"事做了没有",Process 检查告诉你"事是怎么做的"。
对于有明确执行顺序要求的 Skill,需要验证工具调用的序列是否符合预期。还是用 JSONL 日志:
# 提取所有工具调用,按顺序
tool_calls = [
e["tool"] for e in events
if e.get("type") == "tool_use"
]
# 定义期望的调用序列
expected_sequence = ["db_backup", "db_migrate", "db_verify"]
# 检查是否是子序列(允许中间有其他工具调用)
def is_subsequence(expected, actual):
it = iter(actual)
return all(step in it for step in expected)
if not is_subsequence(expected_sequence, tool_calls):
print(f"❌ FAIL: 工具调用顺序不符合预期")
print(f" 期望包含: {expected_sequence}")
print(f" 实际调用: {tool_calls}")
sys.exit(1)
else:
print("✅ PASS: 工具调用序列正确")
Process 验证还有一个更进阶的用途:检测"命令抖动"(command thrashing)——即 Agent 在同一个操作上反复尝试、来回横跳。这通常意味着 Skill 的指令不够清晰,Agent 在不确定的情况下乱试。可以通过统计同一工具的连续调用次数来检测:
# 检测连续重复调用(超过 3 次视为抖动)
from itertools import groupby
for tool, group in groupby(tool_calls):
count = sum(1 for _ in group)
if count > 3:
print(f"⚠️ WARNING: '{tool}' 连续调用 {count} 次,疑似命令抖动")
验证 Style 和 Efficiency:Rubric 模型评分
Outcome 和 Process 都是"事实性"检查,对错非黑即白。但 Style 和 Efficiency 是定性判断——代码风格对不对、Commit message 规不规范、有没有绕弯路——这些没有唯一标准答案。
这时候需要换一个工具:让另一个模型来打分,但要给它一张清晰的评分表(Rubric),并用 --output-schema 强制返回结构化 JSON,保证分数可比较。
定义 Rubric
# rubric.yaml
criteria:
- name: code_style
description: "生成的代码是否符合项目的 ESLint 规范?"
scale: [1, 5]
anchor_1: "完全不符合,大量违规"
anchor_5: "完全符合,零违规"
- name: commit_format
description: "Commit message 是否遵循 Conventional Commits 规范?"
scale: [1, 5]
anchor_1: "格式完全错误"
anchor_5: "格式完全正确,类型、scope、描述均规范"
- name: efficiency
description: "Agent 是否存在明显的冗余步骤或不必要的工具调用?"
scale: [1, 5]
anchor_1: "大量冗余,步骤混乱"
anchor_5: "执行路径简洁高效"
用 --output-schema 强制结构化输出
import subprocess, json
# 定义输出 schema
output_schema = {
"type": "object",
"properties": {
"code_style": {"type": "integer", "minimum": 1, "maximum": 5},
"commit_format": {"type": "integer", "minimum": 1, "maximum": 5},
"efficiency": {"type": "integer", "minimum": 1, "maximum": 5},
"reasoning": {"type": "string"}
},
"required": ["code_style", "commit_format", "efficiency", "reasoning"]
}
result = subprocess.run(
["codex", "exec", "--output-schema", json.dumps(output_schema),
"--", "评估以下运行结果是否符合代码规范..."],
capture_output=True, text=True
)
scores = json.loads(result.stdout)
print(f"代码风格: {scores['code_style']}/5")
print(f"提交规范: {scores['commit_format']}/5")
print(f"执行效率: {scores['efficiency']}/5")
print(f"评分理由: {scores['reasoning']}")
使用结构化输出的核心价值:跨版本可比较。你改了 Skill 的某个指令,重新跑一遍 Eval,Style 分从 3.2 变成 4.1——这才是可以信赖的改进信号,而不是"感觉更好了"。
渐进式叠加:让 Eval 随 Skill 一起成长
上面四个维度的检查,不需要一次全部建好。Skill 验证应该和 Skill 本身一起迭代。
第一阶段(Skill 刚写完):先跑手动测试,确认基本 Outcome 没问题。
第二阶段(准备推广):加入 Outcome 的确定性检查,建 10 条测试用例。
第三阶段(团队在用):补 Process 的序列验证,加 Style 的 Rubric 评分。
第四阶段(生产关键路径):加入命令抖动检测、token 用量监控、构建验证、运行时冒烟测试。
这套渐进式思路有一个好处:在 Skill 最简单的时候就养成写 Eval 的习惯,不会因为"要建完整体系"而拖延。一个有 2 条 Outcome 检查的 Skill,也比一个零检查的 Skill 安全得多。
随着 Eval 套件成熟,把它集成进 CI 管道,每次修改 Skill 都自动跑一遍——这时候你改 Skill 才是真的有底气,而不是每次都在赌"这次应该没问题吧"。
总结
回到最开始的问题:你的 Skill 真的好用吗?
现在我们有了一套可以回答这个问题的框架:
| 你想知道… | 用这个方法 |
|---|---|
| 任务完成了吗? | 确定性检查(解析 JSONL,验证文件/状态) |
| 步骤走对了吗? | 工具调用序列验证 + 命令抖动检测 |
| 质量达标了吗? | Rubric 模型评分(结构化 JSON 输出) |
| 有没有在浪费? | token 用量追踪 + 冗余步骤检测 |
好的 Eval 有两个作用:让回归清晰——你知道改了什么导致分数下降;让失败可解释——不是"感觉不对",而是"第 3 步工具调用顺序错了"。
这才是让你敢持续迭代 Skill 的底气。
参考来源:本文核心方法论来自 OpenAI 官方技术博客 Testing Agent Skills Systematically with Evals。