大模型应用实战

你的 Skill 真的好用吗?用 Eval 系统化验证 Agent 技能

从四条 Skill 失效路径出发,介绍如何用 Eval 体系针对 Outcome、Process、Style、Efficiency 四个维度系统化验证 Agent Skill,让"感觉不错"变成"数据说话"。

·10 分钟阅读·AI工程

你最后一次验证 Skill 是怎么做的?

写完一个 Skill,手动触发了几次,输出看起来还不错——然后就上线了。

这大概是大多数人验证 Skill 的完整流程。说出来有点惭愧,但确实如此。我们在写普通代码时会写单元测试、跑 CI,但到了 Skill 这里,突然回到了"凭感觉"的时代。

问题不是懒。问题是我们不知道 Skill 会以哪些方式悄悄失效,也没有一套语言来描述"什么叫做好"。

这篇文章就是要解决这两件事:先把 Skill 的失效路径说清楚,再用这套失效地图反推出验证体系。


Skill 会怎么失效?

在聊怎么测之前,先想清楚有什么可能出错。Skill 的失效通常沿着四条路径发生,而且往往是静悄悄的——没有报错,只有"结果不对劲"。

路径一:根本没触发

这是最隐蔽的失效。用户明明说了"帮我格式化一下代码",但 Agent 没有调用你的代码格式化 Skill,而是直接用模型自己的知识瞎改了一通。

原因通常在 SKILL.mddescription 字段写得太模糊——太宽泛会和其他 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


🎉 感谢关注,让我们一起享受技术带来的精彩!

我做了一个个人主页,能找到所有我提供给你的资源个人主页