Skill 打分器
给一个 Skill(即 SKILL.md 文件)做质量打分。 用一份提前写好的测试题集,跑一遍这个 Skill,看它该触发时有没有触发、输出对不对、会不会乱触发、多次跑结果稳不稳定,并且能和上一版结果做对比。 最后输出一份打分报告(人看的 Markdown + 机器读的 JSON),让 Skill 的好坏"看得见、比得了、追得回"。
⚠️ Hard Rules
- 必须用测试题打分 — 必须读取
testset.json,不允许凭感觉说"还行"、"不太行"。没有测试题就拒绝跑分 - 该触发的、不该触发的都要测 — 测试题里既要有"应该触发的输入",也要有"不该触发的输入",只测一边的结果不可信
- 每条失败都要列证据 — 报告里每个失败用例必须写出:输入是什么、期望什么、实际是什么。不允许只写"失败了"
- 不准改被测 Skill — 打分器只读,不写。发现问题只在报告里提建议,让用户自己决定改不改
- 想做版本对比,必须给上一版的报告 — 要看"是变好还是变差",就必须把旧版报告也提供出来,否则只输出当前结果
- 指标算法统一 — 准确率/召回率/综合分都用业界通用算法,不允许自己发明"满意度"之类的指标
- 调优集和验收集分开 — 测试题分两批:一批用来给改进建议(调优集),另一批专门用来打分(验收集)。禁止用验收集去反推怎么改 Skill,否则分数会"虚高"
- 必须测稳定性 — 同一道题至少跑 3 次,结果波动超过 10% 就算不稳,单次高分不算数
- AI 打分员要防偏见 — 让 AI 给 Skill 打分时,要打乱顺序(防"先看到的得分高")、隐藏 Skill 作者(防"AI 偏向自己写的")
触发条件
当用户说以下内容时触发本 Skill:
- "给这个 skill 打分一下" / "测一下 skill 准不准"
- "skill 改完了,跑一下看有没有变差" / "新旧版本对比一下分数"
- "看看 skill 会不会乱触发" / "查一下命中率和误触率"
- "skill 跑得快不快、花费多不多"
不触发条件
以下情况用别的 Skill:
- 用户想新建一个 Skill → 用
SKILL.template.md自己写 - 用户想测试游戏关卡能不能通关 → 用
ai-game-playtester - 用户想检查素材文件格式 → 用
ai-game-asset-pipeline - 用户只是想了解 Skill 怎么写 → 直接看模板文档
输入参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
skill_path |
字符串 | ✓ | - | 要打分的那个 Skill 的 SKILL.md 文件路径 |
testset_path |
字符串 | ✓ | - | 测试题集 JSON 文件路径(参考 testset.example.json) |
baseline_report |
字符串 | - | 上一版的报告路径,给了就会做新旧对比 | |
model |
字符串 | default |
跑测试用的模型名字,仅记录在报告里 | |
output_dir |
字符串 | eval-reports/ |
报告输出目录 | |
pass_threshold |
数字 | 0.85 |
综合分及格线,低于此值整体判为不及格 | |
holdout_ratio |
数字 | 0.4 |
"验收集"占比(推荐 4 成验收 + 6 成调优) | |
stability_runs |
数字 | 3 |
同一题重复跑几次,用来看稳不稳定 | |
judge_model |
字符串 | - | 当 AI 打分员的模型名字,留空就不打主观分 |
测试题集格式
测试题集是一个 JSON 数组,每道题包含:
{
"id": "case_001",
"type": "应触发 | 不应触发",
"split": "调优集 | 验收集",
"input": "用户的输入原话",
"expected_trigger": true,
"expected_output_contains": ["关键字1", "关键字2"],
"expected_params": { "字段名": "字段值" },
"expected_tool_calls": ["read_file", "validate_schema", "write_file"],
"rubric": { "清晰度": 4, "完整度": 5 },
"tag": "可选标签,用于分组统计"
}
应触发— 这种输入应该触发本 Skill,并且输出要包含指定关键字不应触发— 这种输入不该触发本 Skill(用来测会不会乱触发)split—调优集(用来给改进建议)/验收集(用来打最终分,不能用它来反向调 Skill)expected_output_contains— 输出里必须包含的关键字(数组里全部命中才算通过)expected_params— 输出里必须有的字段(按字段名严格匹配)expected_tool_calls— 期望调用了哪些工具,且顺序也对rubric— AI 打分员的打分项(1-5 分),只有配置了judge_model才用
执行流程
步骤 0:前置检查
- 检查
skill_path文件存在,且文件开头有头部信息(即---包起来的那段) - 检查
testset_path是合法的 JSON 数组(题目 ≥ 10 条,应触发和不应触发各 ≥ 3 条) - 缺一项就报告缺哪个 → 停止执行
步骤 1:读懂被测 Skill
读取 Skill 文件,提取:
| 信息 | 来源 | 用途 |
|---|---|---|
| Skill 名字 | 头部信息 | 报告里显示用 |
| description 描述 | 头部信息 | 提取触发词 |
| 触发词列表 | description 里的"触发词:"段 | 算命中率用 |
| 强制规则数 | 正文 | 看 Skill 是否健壮 |
| 步骤数 | 正文中的"### 步骤 X" | 看 Skill 复杂度 |
步骤 2:跑测试题
每道题按下面流程跑一遍:
| 环节 | 动作 | 记录什么 |
|---|---|---|
| 喂入题目 | 把 input 当作用户输入 |
- |
| 看是否触发 | 判断 Skill 是否被激活 | 是否触发 |
| 收集输出 | 拿到 Skill 输出的文字和字段 | 实际输出、实际字段 |
| 看工具顺序 | 记录都调用了哪些工具、顺序如何 | 实际工具调用 |
| 重复跑 N 次 | 同题跑 stability_runs 次看波动 |
多次结果、波动幅度 |
| 看会不会瞎编 | 输出里的字段必须能在输入里找到依据 | 没依据的字段 |
| AI 打分员(可选) | 让另一个模型按打分表给分 | 各项分数、打分理由 |
| 对答案 | 跟 expected_* 字段一项一项比 |
是否通过、哪些断言不过 |
| 算性能 | 记录耗时和字数消耗 | 耗时毫秒、字数 |
步骤 3:算分
打分维度(写大白话,但保留业界算法):
| 指标 | 算法 | 通俗解释 |
|---|---|---|
| 准确率(Precision) | 触发对的次数 ÷ 全部触发次数 | 触发的里面,有多少是真的该触发的(防乱触发) |
| 召回率(Recall) | 触发对的次数 ÷ 全部应触发题数 | 该触发的里面,触发到了多少(防漏触发) |
| 综合分(F1) | 准确率和召回率的加权平均 | 把上面两个综合起来的总分 |
| 输出通过率 | 输出断言通过数 ÷ 应触发题总数 | 输出对不对 |
| 误触率 | 不该触发却触发的次数 ÷ 不应触发题总数 | 越低越好 |
| 工具顺序得分 | 工具序列对得上的题数 ÷ 总数 | 是不是按预期的顺序调工具 |
| 稳定性 | 1 - 多次跑结果的波动幅度 | 同一道题反复跑,结果稳不稳定 |
| 编造率 | 没依据的字段数 ÷ 总字段数 | 输出里有多少是 AI 自己瞎编的(越低越好) |
| AI 打分 | AI 打分员各项均分(1-5 分) | 主观质量分,仅配置了 judge_model 时有 |
| 响应时间(P95) | 95% 的题在多少秒内能跑完 | 看快不快 |
| 平均字数消耗 | 每题平均花了多少字数 | 看花费贵不贵 |
⚠️ 打分用"验收集",建议用"调优集" —— 严守强制规则 7。报告里这两批的得分要分开列出来。
步骤 4:根据结果给改进建议(综合分不达标时)
当综合分 < pass_threshold 或误触率 > 0.1 时,进入分析循环(最多 3 轮):
- 把失败题归类 — 按原因分组:
- 触发词漏掉了 → 提示:description 里要补哪些词
- 字段没解析对 → 提示:哪个字段的解析规则要改
- 误触集中在某类输入 → 提示:"不触发条件"要补哪些边界
- 生成改进建议日志:
🔄 改进建议日志(第 N/3 轮)
━━━━━━━━━━━━━━━━━━━━
❌ 失败原因归类: 触发词覆盖不全(4 条不该触发的输入被触发了)
样例输入: ["帮我看看代码", "查询一下文件"]
分析: description 里"查"字这个触发词太宽泛
建议: 在"不触发条件"里明确排除"查询/查看"这类只是看不动的意图
- 建议只放在报告里 — 不会自动改 Skill(强制规则 4 不允许),由用户自己决定改不改
- 3 轮没有新建议 → 停止循环,进入下一步
步骤 5:和上一版做对比(给了上一版报告时)
读取 baseline_report 文件,把每项指标和当前对比:
| 指标 | 上一版 | 这一版 | 变化 | 判定 |
|---|---|---|---|---|
| 综合分 | 0.82 | 0.91 | +0.09 | ✅ 变好了 |
| 误触率 | 0.15 | 0.08 | -0.07 | ✅ 变好了 |
| 响应时间 P95 | 1200ms | 1500ms | +300ms | ⚠️ 变慢了 |
判定规则:
- 任意一项指标比上一版差 5% 以上 → 标记为"倒退"
- 全部持平或变好 → 标记为"持平"或"改善"
步骤 6:先预览,确认后再写报告
生成两种文件:
1) Markdown 报告(给人看):
📊 Skill 打分报告: ai-game-level-generator
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
打分时间: 2025-11-08 14:30 | 模型: default | 题目总数: 24
✅ 整体结果: 通过 (综合分=0.91, 及格线=0.85)
核心指标:
准确率: 0.93 (15 次触发里 14 次正确)
召回率: 0.88 (16 道应触发题里触发了 14 道)
综合分: 0.91
输出通过率: 12/14 (85.7%)
误触率: 0.08 (12 道不应触发题里 1 次误触)
响应时间 P95: 1.4 秒
❌ 失败题目(3 道):
- case_017 [应触发]: "做个最终关" 没有触发(触发词里缺"最终关")
- case_022 [输出]: 输出里缺少 "level_id" 字段
- case_009 [不应触发]: "查看现有关卡" 被误触发了
🔄 改进建议:
1. description 里补充触发词: "最终关"、"final stage"
2. 在"不触发条件"里明确排除: "查看/浏览"这类只看不动的意图
📈 对比上一版 v1.0: 综合分 +0.09(变好了)
2) JSON 数据(给机器读,作为下次的对比基准):
{
"skill_name": "ai-game-level-generator",
"timestamp": "2025-11-08T14:30:00Z",
"model": "default",
"metrics": { "precision": 0.93, "recall": 0.88, "f1": 0.91 },
"verdict": "PASS",
"failed_cases": [...]
}
先把 Markdown 摘要展示给用户 → 等用户确认 → 再写到 output_dir 目录。
完成标志
报告写完后,输出:
<promise>DONE</promise>
错误处理
| 出错场景 | 处理方式 |
|---|---|
| Skill 文件没头部信息 | 报告"不是合法的 SKILL.md",停止 |
| 测试题不够 10 条,或应触发/不应触发不齐 | 拒绝执行,提示用户补题 |
| 单道题跑超 30 秒 | 标记为"超时",计入失败统计,但不中断整体 |
| 上一版报告格式不对 | 跳过对比,只输出当前结果 |
| 输出目录没写入权限 | 报告具体路径并提示用户授权 |
| 稳定性波动 > 10% | 标"不稳定",不中断流程但提示用户关注 |
| AI 打分员两次打分差超过 1 分 | 多跑几次取平均;还是波动就标"结论可信度低" |
| 没有验收集(用户全标了"调优集") | 警告并强制按 6:4 自动切分 |
与其他 Skill 的协作
Skill 写好了
↓
ai-skill-evaluator(本 Skill)
├─ 读取 SKILL.md + testset.json
├─ 跑测试 + 算分
├─ 给改进建议
└─ 输出报告(Markdown + JSON)
↓
用户根据报告手动改 SKILL.md
↓
再跑一次(带上一版报告)→ 看变好还是变差