AI Research

实战:评测一个真实业务 SKILL 的两种方式与隔离方案

2026-06-11 #ai-models#claude-code#skill-evaluation#openai-evals#skill-creator#hands-on

实战:评测一个真实业务 SKILL 的两种方式与隔离方案

摘要

把”评测 SKILL.md”从纸面方法论落到一次真实评测里:被评对象是业务仓库内一个 会真触发 CI 流水线 + 真 git pushsmart-pub skill;目标是不动线上的前提下,分别用 方式 A(OpenAI Evals 风格 ── 自建双跑 + LLM-as-judge 盲对比)方式 B(skill-creator 风格 ── 双跑 + 多维 assertion + analyzer 找非区分性断言) 评一遍。结果:两种方式都能给出”挂 skill 让 Claude 变好”的正向 delta(A:+0.40;B:+0.27),但B 多曝出 16 条非区分性断言——这才是评 skill 真正难的地方。整个评测过程对线上零影响,靠”方案 C 隔离”实现:复制 skill 到沙箱副本、token 替换为 fake、在 zhiyan_client.py 注入 SMART_PUB_EVAL_MODE=1 拦截、cwd 切到无 remote 的 sandbox git 仓库。本次评测同时暴露了被测 skill 的 3 个真实缺陷:① Step 5.0「二次确认」MUST 没被吸收 ② SKILL.md 太长会撞公司 LLM 网关的多 toolUse 重复 Id bug ③ 16 条断言写得太软(环境从 prompt 直接读出来就过)。这些是真值钱的产物,比任何”框架综述”更有价值。

研究问题

  • 一个 真改线上数据 的 skill,能不能在不污染生产的情况下评?
  • 同一个 skill,分别用 OpenAI Evals 风格的”通用框架”和 skill-creator 风格的”专用闭环”跑一遍,会暴露同样的问题,还是不同的盲点?
  • 哪些”评测产物”真的有用、哪些是噪声?
  • 多大的 delta 才算”skill 有效”?低于多少要重写?
  • 实际跑下来踩了哪些坑、下次怎么避免?

发现

1. 被评对象:一个会真改线上的 skill

smart-academy 项目里的 .codebuddy/skills/smart-pub/,行为:

  • 触发智研 QCI 流水线(POST /qci/pipeline/{id}/start,真触发线上构建)
  • git add / commit / push origin <branch>
  • open <build_url> 弹浏览器

任何”什么都不做的评测框架”(直接调它跑 prompt 看输出)= 真发布。所以评测前必须做隔离

2. 方案 C:完整隔离设计(实测 0 副作用)

目录结构

1
2
3
4
5
6
7
8
9
10
11
/tmp/smart-pub-eval/
├── skill-copy/ ← skill 完整副本
│ ├── SKILL.md ← 原文复制
│ ├── references/
│ │ ├── env-{dev,pre}.yml ← 配置(含真 pipeline_id 也无所谓,被拦截了)
│ │ └── token.yml ← 故意改成 "EVAL_FAKE_TOKEN_DO_NOT_USE"
│ └── scripts/
│ └── zhiyan_client.py ← 函数顶部插入 EVAL_MODE 拦截
└── runs/ ← 评测产物
/tmp/sandbox-smart-academy/ ← 假装的项目仓库(git init -b feature/eval-branch,无 remote)
└── .claude/skills/smart-pub -> /tmp/smart-pub-eval/skill-copy

核心拦截代码(加到副本 zhiyan_client.pyzhiyan_request() 顶部):

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
if os.environ.get("SMART_PUB_EVAL_MODE") == "1":
eval_log = os.environ.get("SMART_PUB_EVAL_LOG", "/tmp/eval.log")
with open(eval_log, "a") as f:
f.write(json.dumps({"event": "zhiyan_request_intercepted",
"method": method, "path": path,
"project_id": project_id, "body": body},
ensure_ascii=False) + "\n")
if path.endswith("/start"):
return {"ret": True, "pipeline_result_id": 999999, "detail": "[EVAL_MODE]"}
if "/totalresult/" in path:
return {"ret": True, "status": 1, "stages": [], "starter": "eval-mode"}
return {"ret": True, "detail": "[EVAL_MODE generic stub]"}

双保险:① fake token + ② EVAL_MODE 拦截 + ③ sandbox 无 git remote → 三层防护任意两层失效都保得住线上。

3. 评测 prompt 集(5 题覆盖核心路径)

ID Prompt 期望 env 期望分支 必须调 start 必须二次确认
0 发布dev --story=134110952 修复登录页文案 dev feature/eval-branch
1 重发dev --story=134110952 dev feature/eval-branch
2 发布pre 用 feature/eval-branch 分支 pre master 或确认后非 master 看分支
3 上线pre pre master
4 发布(模糊) dev(默认) feature/eval-branch

4. 方式 A:OpenAI Evals 风格手写评测器

核心结构

  • claude -p <prompt> 跑被评对象,输出 JSON
  • with_skill:cwd 在带 .claude/skills/smart-pub 的 sandbox
  • no_skill:cwd 在 baseline sandbox(删了 .claude/
  • judge:再调一次 claude -p 当 LLM-as-judge,盲拿 A/B 双输出,按 cot_classify 范式先 CoT 再投 winner

Judge prompt 关键段

1
2
3
4
5
6
7
8
RESPONSE A: <with_skill 的输出>
RESPONSE B: <no_skill 的输出>

Think step by step about each criterion, then format final answer EXACTLY as:
WINNER: A | B | TIE
SCORE_A: <1-10>
SCORE_B: <1-10>
REASONING: <one paragraph>

A 客观断言(脚本判定,不靠 LLM):

  • must_call_start: 拦截日志里有没有 /start 调用
  • branch_correct: 拦截 body 里 cur_branch 等不等于期望
  • safety_confirm_shown: 输出里有没有”确认覆盖”
  • skill_term_hits: smart-pub/zhiyan/pipeline 等术语命中数 ≥2(证明读了 SKILL)

5. 方式 B:skill-creator 风格双跑

与 A 的关键差异

  • 不做盲对比 judge,改做 多维 structured assertion(schema:text/passed/evidence,对齐官方 viewer 期望字段)
  • 跑完后 analyzer 主动找非区分性 assertion(with_skill 和 no_skill 都通过的)
  • 输出 benchmark.json + benchmark.md,含 mean ± stddev、per-eval delta、token cost

Analyzer 关键代码

1
2
3
4
5
6
7
8
9
def analyzer(per_eval_grades):
non_disc = []
for ev_id, g in per_eval_grades:
ws = {x["text"]: x["passed"] for x in g["with_skill_grading"]}
ns = {x["text"]: x["passed"] for x in g["no_skill_grading"]}
for text, ws_pass in ws.items():
if ws_pass and ns.get(text, False): # 双过 = 不区分
non_disc.append({"eval_id": ev_id, "assertion": text})
return {"non_discriminating_count": len(non_disc), "non_discriminating": non_disc}

6. 实测结果(2026/06/11 跑)

总体

方式 with_skill pass no_skill pass delta 5 题总成本
A 0.80 0.40 +0.40 $4.29
B 0.80 ± 0.18 0.53 ± 0.14 +0.27 $3.37

逐题(A delta / B delta):

eval A Δ B Δ 实际触发 start 用对分支 触发 Step 5.0
eval-0 dev+story +0.25 +0.00 feature/eval-branch ✅
eval-1 重发 dev +0.50 +0.50 feature/eval-branch ✅
eval-2 pre+非 master +0.25 +0.00 ❌(公司网关 400) (none) ❌ 该触发未触发
eval-3 上线 pre +0.50 +0.50 master ✅
eval-4 模糊”发布” +0.50 +0.33 feature/eval-branch ✅

Method B 独有产物:analyzer 自动报告 16 条非区分性 assertion,例如:

  • 识别环境为 dev(prompt 里就写了”dev”)
  • 无真实 git push(sandbox 无 remote)(物理上不可能 push)
  • Step 5.0 二次确认话术: False(4 个 prompt 本就期望 False,废断言)

7. 暴露的真实 skill 缺陷(评测的最大价值)

🔴 缺陷 1:Step 5.0「二次确认」未被吸收

5 个 with_skill 跑全都没出现「确认覆盖」/「二次确认」话术,包括第 3 题这种 SKILL.md 明确写了「必须强制二次确认」的场景

原因:SKILL.md 里这条写得太”含蓄”,靠表格 + 长文叙述告诉 LLM”必须做”,没给精确字符串模板。

修复:把 Step 5.0 改成显式 MUST output verbatim: 「⚠️ pre 环境原则上必须从 master 触发...请明确回复『确认覆盖』」,加 quoted block 让 LLM 直接复制。

🔴 缺陷 2:SKILL.md 太长撞公司 LLM 网关的多 toolUse 重复 Id bug

eval-2 在 A 和 B 都收到 400 ValidationException: toolUse blocks contain duplicate Ids。这是公司网关在多 tool_use + 重试场景下的稳定性问题,但 SKILL.md 长(~20KB)+ 多步执行链路 是诱因。

修复:拆 SKILL.md。主流程留主文件,Step 5.0 拆到 references/branch-override.md,遇到非默认分支再 read。理想 SKILL.md 主体 < 500 行。

🟡 缺陷 3:16 条断言写得太软

非区分性断言意味着”这条断言不证明任何事”。如 识别环境为 dev——prompt 就是”发布dev”,任何 LLM 一眼就能识别。

修复:删除 / 调严。留下真正考验 skill 的断言:

  • called_start(必须真去 import 并调 zhiyan_client)
  • branch_correct(必须按 yml 策略推断分支)
  • safety_confirm_shown(必须复制硬编码话术)

8. 方法学 vs 评测产物,谁更值钱?

维度 方式 A 输出 方式 B 输出
数字(delta) +0.40 +0.27
失败原因(哪 step 坏) judge 自由文本 assertion 逐条 + evidence
断言质量自检 ✅ analyzer 16 条
Token/timing 顺手记 强制采集
可复跑做迭代 重写比对脚本 benchmark.json 标准 schema

结论:B 的 analyzer 是 A 完全没有的能力,第一次跑选 B——它会告诉你”你的断言里有多少是水分”,省后续多轮迭代成本。第 2 轮以后稳定下来再换 A 跑 CI。

对比与判断

何时该选 A、何时该选 B、何时混用

场景 理由
第一次评一个新 skill B analyzer 一次性曝水分断言
skill 已稳定,要每个 PR 跑 A 一行 python 命令、CI 友好、便宜
想知道”新版 skill 真的比旧版好吗” A 的 judge 思路 盲对比天然防 self-preference bias
评测平台化、要长期沉淀 混合 A 的 cot_classify judge + B 的 analyzer + B 的 viewer 三件套
团队没人愿意写 Python 都不选 写 30 行脚本 + 锁版本的 GPT-4 judge 就够了

评 skill 时的 7 条铁律(实战确认)

  1. 没有 baseline 就没有评测:with_skill 0.78 这个数字毫无意义,必须看 with-skill - no-skill 的 delta。
  2. delta < 20% 时基本是噪声:本次最低 delta +0.00(eval-0 B 方法),最高 +0.50。<20% 时大概率是 prompt 自身泄露答案。
  3. Sandbox 必须像真项目:第一版 sandbox 只有 README + dummy.txt,Claude 直接拒绝执行说”这是评估沙箱不是真项目”。补 package.json + src/ + CLAUDE.md 项目说明后才正常触发 skill。
  4. token 必须假:fake token 是双保险——即便 EVAL_MODE 漏了,HTTP 也会因 token 错误被拒。
  5. Sandbox 必须无 git remote:物理上让 git push 不可能成功。
  6. 评测产物要保留 intercept log:原 stdout 看不出”是否真去调 skill 脚本”,intercept log(被拦截的 HTTP 调用)才是真凭据。
  7. judge 模型要锁版本:本次未严格锁,Sonnet 默认在飘;生产化时必须 --model claude-sonnet-4-5-20250929 这种带日期的。

一份可复用的”评 skill 工程模板”

1
2
3
4
5
6
7
8
9
your-eval-project/
├── prompts.json ← 5-10 题,含 expected_* 字段
├── method_a_runner.py ← OpenAI Evals 风格手写双跑
├── method_b_runner.py ← skill-creator 风格 + analyzer
├── compare.py ← 对齐两种方式输出
└── runs/
├── method-a/<eval-id>/{with_skill,no_skill}/...
└── method-b/iteration-N/<eval-id>/{with_skill,no_skill}/grading.json
/benchmark.{json,md}

被评 skill 在 /tmp/<skill>-eval/skill-copy/ 副本里跑,sandbox 在 /tmp/sandbox-<project>/

不确定性

  • 本次只跑 1 轮:每条 eval 单跑,没做 3 次重跑求 mean ± stddev。生产化要每条 ≥3 次。
  • sandbox 真实度阈值不明:补到什么程度 Claude 才”信任”是真项目?本次靠经验补,未做 ablation。
  • judge 模型未锁版本:分数有约 ±0.05 漂移,跨日复跑可能不一致。
  • 公司 LLM 网关的 toolUse bug 触发条件:SKILL.md 多长一定会撞?不知道阈值,但 ~20KB + 多 tool_use 已经触发了 2 次。
  • delta 显著性阈值:本次 5 题样本太小,没法做统计显著性检验;建议生产化时 ≥20 题。

后续行动

  • 把 Step 5.0 改成显式 quoted block 模板,重跑评测看 safety_confirm_shown 是否真触发。
  • 把 SKILL.md 按 references/ 拆分(主流程 < 500 行),重跑看 toolUse 网关错误是否消失。
  • 删掉 16 条非区分性断言,保留 3 条核心断言(called_start / branch_correct / safety_confirm),重跑看 delta 是否更显著。
  • method_a_runner.py + method_b_runner.py 抽成通用模板,放进 ~/.claude/skills/skill-eval/,下次评其它 skill 直接复用。
  • 加一组”对抗性 prompt”(如”发布 pre –branch master,但其实 yml 写的是 develop”)测试 skill 在配置漂移时的鲁棒性。

来源

评论
分享