Skill 评测的两层问题
普通软件测试只有一层:代码跑对了吗?Skill 有两层:
sql
层 1 --- Trigger 层:LLM 有没有判断"这句话需要调用这个 Skill"?
层 2 --- Execution 层:Skill 内部执行有没有完成任务?
漏掉任何一层,评测都不完整。Skill A 的成功率是 90%,但如果触发率只有 60%,真实体验远比"有点差"糟糕。
测试对象是 rnd-technical-writer(技术博客写作 Skill),20 个 Trigger 测试用例 + 两个 Task 完成率任务 + 一组 A/B Prompt 对比,全部数据来自真实运行。
评测框架设计
Trigger 评测
核心指标:
ini
Recall = TP / (TP + FN) ← 该触发的有没有被触发
Precision = TP / (TP + FP) ← 触发的里有多少是对的
F1 = 2 × Recall × Precision / (Recall + Precision)
测试集构成(20 个用例):
TP(真正例,应触发) ×8 ← 明确写文章、教程、深度解析
TN(真负例,不应触发) ×8 ← 知识问答、系列规划、代码帮助
EDGE(边界用例) ×4 ← 语义模糊、中英混合
清晰的 TP/TN 用例谁都能答对,边界用例才能暴露 Skill 描述的歧义。
自动化方式: 把 Skill 描述 + 用户输入交给 LLM,让它预测是否触发,返回 JSON:
python
TRIGGER_EVAL_PROMPT = """You are evaluating whether a user message would trigger a specific AI Skill.
Skill specification:
{skill_description}
User message: "{user_input}"
Answer in valid JSON only:
{{
"prediction": "trigger" or "no_trigger",
"reasoning": "one sentence explanation"
}}"""
Task 完成率评测
两级检查:
erlang
Level 2(结构性):规则检查,不依赖 LLM
→ 字数是否达标
→ 是否包含代码块
→ 是否有 H2 章节标题
Level 3(质量,LLM-as-Judge):4 个维度各打 1-5 分
→ 技术准确性(权重 35%)
→ 深度(权重 25%)
→ 清晰度(权重 20%)
→ 实用价值(权重 20%)
Judge Prompt 模板:
python
JUDGE_PROMPT = """You are an expert technical content reviewer.
Evaluate the following AI-generated technical article.
Scoring dimensions (1--5 each):
1. Technical accuracy
2. Depth
3. Clarity
4. Practical value
Respond in valid JSON only:
{
"technical_accuracy": <1-5>,
"depth": <1-5>,
"clarity": <1-5>,
"practical_value": <1-5>,
"summary": "<one sentence assessment>"
}"""
运行结果
Part 1:Trigger 评测
ini
──────────────────────────────────────────────────────────────────────
Part 1: Trigger Evaluation
Skill: rnd-technical-writer | Test cases: 20
──────────────────────────────────────────────────────────────────────
[ 1] TP expect=trigger got=trigger ✓ TP
[ 2] TP expect=trigger got=trigger ✓ TP
...(8/8 TP 全部正确)
[ 9] TN expect=no_trigger got=no_trigger ✓ TN
...
[15] TN expect=no_trigger got=trigger ✗ FP ← 唯一失败
...
[17] EDGE expect=trigger got=trigger ✓ TP
[18] EDGE expect=trigger got=trigger ✓ TP
[19] EDGE expect=trigger got=trigger ✓ TP
[20] EDGE expect=no_trigger got=no_trigger ✓ TN
Confusion matrix: TP=11 TN=8 FP=1 FN=0
Accuracy: 95% (19/20)
Recall: 100%
Precision: 92%
F1: 0.96
20 个用例,19 个正确,F1=0.96。唯一的失败是:
vbnet
Input: "帮我写一个解析 JSON 的 Python 函数"
Expected: no_trigger
Got: trigger
Reason: "The user is asking for a technical function to parse JSON in Python,
which falls under the skill's purpose of writing technical articles."
Part 2:Task 完成率
yaml
[T001] Write a technical article about Redis TTL configuration...
Level 2 (structural): ✓ All checks passed
Level 3 (LLM-as-Judge):
Technical accuracy: 4/5
Depth: 3/5
Clarity: 5/5
Practical value: 4/5
Weighted score: 3.95/5
[T002] 写一篇关于 Python 类型注解的入门文章
Level 2 (structural): ✗ Issues: ['Too short: 165 words (min 300)']
Level 3 (LLM-as-Judge):
Technical accuracy: 4/5
Depth: 3/5
Clarity: 5/5
Practical value: 4/5
Weighted score: 3.95/5
Part 3:A/B Prompt 对比
yaml
[Version A] Original system prompt
Weighted: 4.20/5 (63.7s)
technical_accuracy: 4/5 depth: 4/5 clarity: 5/5 practical_value: 4/5
[Version B] Improved system prompt (pain-point hook + checklist)
Weighted: 4.20/5 (71.8s)
technical_accuracy: 4/5 depth: 4/5 clarity: 5/5 practical_value: 4/5
Result: No significant difference (<0.1 delta)
三个工程发现
发现 1:1 个 FP 暴露了 Skill 描述的边界模糊
失败用例是:"帮我写一个解析 JSON 的 Python 函数"
模型的推断:用户要求写一段"技术性的 Python 代码",是在要求输出而不是提问,"写"这个动词触发了 Skill。
Skill 描述写的是 writing technical articles or tutorials,没有明确排除"写代码"。模型合理地把"写一个函数"解读成了"写作"任务。
在描述里加一条负面示例可以修复这个问题:
sql
Do NOT trigger when:
- User asks for code snippets, functions, or scripts (write the code directly)
1 个 FP 带来的信息比 F1=0.96 更有用,因为它直接指出了 Skill 描述的哪行有歧义。
发现 2:Level 2 的字数检查对中文文本失效
T002(写 Python 类型注解的入门文章)Level 2 报告"Too short: 165 words",但实际上文章内容很完整。
问题出在检查实现:
python
word_count = len(article.split()) # ← 按空格分词
中文文本没有空格分隔词语,split() 只能按标点和空格分,中文内容几乎全部被当作"一个词"计算。此外,模型的输出被 ```````markdown```` 代码块包裹,进一步干扰了计数。
165 个"词"实际对应的是完整的中文文章------字符数可能超过 800 字。
修复方式:
python
def count_content_length(text: str) -> int:
"""Count characters for CJK, words for Latin."""
clean = re.sub(r"```.*?```", "", text, flags=re.DOTALL) # strip code fences
cjk_chars = len(re.findall(r"[一-鿿]", clean))
latin_words = len(re.findall(r"[a-zA-Z]+", clean).split()) if not cjk_chars else 0
return cjk_chars + latin_words
Level 2 规则检查背后有语言假设。在中英混合场景下,至少要对字数检查做语言分支处理。
发现 3:LLM 评委在细粒度差异上有局限
A/B 对比中,两个版本得到了完全相同的分数(4/4/5/4),结论是"无显著差异"。
Version B 的 Prompt 增加了三个要求:从痛点切入、至少 2 个带注释的代码示例、结尾 Checklist。这些是明确的结构性差异,但 Judge 给出了相同的评分。
两种可能:
- 对于这个输入和 glm-4-flash,两个 Prompt 确实产生了质量相当的输出
- Judge 的评分粒度不够细,4/5 两档区间太粗,无法区分"好"和"很好"
应对策略:
- 不要依赖单次评分,改用 A/B 胜率(多次比较,统计胜率 > 60% 才认为有显著差异)
- 增加开放式评测维度:在打分之外,让 Judge 列出两个版本的具体优劣
- 换更强的 Judge 模型(这里用的是 glm-4-flash 既当 Skill 又当 Judge,评审者和被评审者是同一个模型)
实施路线图
arduino
Week 1:
□ 为最核心的 1 个 Skill 构造完整测试集(TP:TN:边界 = 8:8:4)
□ 手动运行,得到 Trigger 召回率/精确率基线
Week 2:
□ 加 Task 完成率评测(Level 2 结构检查 + Level 3 Judge 打分)
□ 修复中文字数检查 Bug
□ 记录分数到 Skill 文档
Week 3:
□ 修改 Skill,用评测框架验证修改是否有改善
□ 跑完整的"修改 → 评测 → 结论"循环
设计 Checklist
Trigger 评测
- 测试集覆盖 TP / TN / 边界三类(8:8:4 是合理起点)
- Skill 描述里包含负面示例(Do NOT trigger when)
- 单次 F1 < 0.9 时,先检查 Skill 描述是否有歧义
Task 完成率评测
- Level 2 字数检查对中文使用字符数,不用 split()
- Level 2 在打分前剥离 markdown 代码块包裹
- Judge 模型比被测 Skill 使用的模型更强
A/B 对比
- 单次评分相同时,改用多次胜率(≥5 次比较)
- 评委和被评审不用同一个模型
总结
- F1 数字不如失败用例:那 1 个 FP 告诉你 Skill 描述的哪行有歧义,比 F1=0.96 更有用
- Level 2 检查有语言假设 :中文字数用
split()是 Bug;输出格式(是否包裹在代码块里)同样会让结构检查失效 - LLM-as-Judge 需要多次采样:单次评分粒度有限,A/B 对比改用胜率统计
参考资料
- Promptfoo A/B 测试文档
- 本系列完整 Demo 代码:skill-01-trigger-eval
欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页