误召回(false trigger)指的是”用户没说要做这件事,模型却把对应 Skill 加载了进来”。它比漏召回更难排查,因为 Skill 一旦被错误加载,它的 instruction layer 与 action layer 都会跟着上线——一个写权限的 deploy 脚本哪怕只被误触发一次,都可能跑出不可逆后果。把误召回率当成 Skill 上线的硬门槛、把可写 Skill 强行收回”必须确认才执行”,是把这条风险挡在生产之外的关键。
一、误召回为什么严重
Skill 的危险性来自”三层叠加”:触发层决定是否加载、指令层告诉模型怎么做、动作层决定它能触到哪些系统。误召回只发生在触发层,但一旦发生,指令层与动作层就被无差别激活。一个描述里写了”清理数据库”的 Skill,误加载后哪怕只跑了一条”统计表行数”也是浪费;要是 action layer 接到的是真删表语句,就是事故。
误召回还有”无声扩散”的特性:模型不会报错、不会拒绝,它只是按”已加载的技能”继续工作。日志里看是一次”正常的工具调用”,但调用的是一份不该出现的指令。这种”看起来都对”的失败比”明显的崩溃”更难被人工巡检发现。
二、误召回与漏召回必须配对评估
评估 Skill 触发质量时,召回(recall)和精度(precision)必须同时算,单独盯任何一项都会失真。
| 指标 | 公式 | 反映的问题 | 数值含义 |
|---|---|---|---|
| 召回率 | 应当触发的次数里实际触发的比例 | 描述是否够具体到能被命中 | 低 = 描述太抽象、漏触发 |
| 精度 | 实际触发里”应该触发”的比例 | 描述是否收得太窄、太具体 | 低 = 描述太宽、误召回 |
| 误召回率 | 1 – 精度 | 不应触发时却被加载的比例 | 0% 起步,上限按场景定 |
误召回率 = 1 – 精度。两者其实是一回事,但语义方向不同:精度高时容易盯漏,误召回率更适合作为告警阈值,因为它直接对应”模型在不该动手时动手了”。
三、误召回的四个常见来源
| 来源 | 描述样例 | 误召回风险 |
|---|---|---|
| 描述过宽 | “用于代码相关工作” | 任何写代码请求都会命中 |
| 关键词堆叠 | “部署、发布、上线、CDN、k8s” 一锅炖 | 提到任一关键词都触发 |
| 语义重叠 | 两个 Skill 都写”改进代码质量” | 模型随机选一个,选错概率高 |
| 用户口语模糊 | “看看生产”被识别成”部署到生产” | 反问句被当命令执行 |
前三类是 Skill 自身问题,第四类是用户表达的不确定性。两个层面都需要修:前者改 description,后者靠”模糊请求先澄清再加载”。
四、构建 trigger 评测集的步骤
误召回率的可信度完全取决于评测集是否够”硬”。一份高质量评测集应当这样构建:
- 收集 30~50 条贴近真实用法的 prompt,必须包含”硬负样本”——看起来像但不该触发、或者该触发别的 Skill;
- 每条 prompt 由人标注 expected_skill 或 “none”,未标注的不能进评测集;
- 用被测模型跑 N 次(推荐 3 次起步),记录每次实际加载的 Skill 名;
- 把 (prompt, expected, actual) 三元组落表,统计 precision / recall / 误召回率;
- 把每条”误触发”具体归类到”触发错了哪个 Skill”,区分是描述过宽还是语义重叠。
硬负样本的价值远高于正样本。一个只含”该触发”prompt 的评测集,能轻易让 Skill 跑出 100% 召回,但完全测不出它有多少误触发。
下面这段脚本把”prompt → 标注 → 跑分”三步串起来,可以直接嵌进 CI:
def score_skill(rows, target_skill):
# rows: [{"prompt": str, "expect": str, "actual": str}]
tp = fn = fp = 0
for r in rows:
if r["expect"] == target_skill and r["actual"] == target_skill:
tp += 1
elif r["expect"] == target_skill and r["actual"] != target_skill:
fn += 1
elif r["expect"] != target_skill and r["actual"] == target_skill:
fp += 1
recall = tp / (tp + fn) if (tp + fn) else 0.0
precision = tp / (tp + fp) if (tp + fp) else 0.0
false_trigger_rate = 1 - precision
return {
"recall": recall,
"precision": precision,
"false_trigger_rate": false_trigger_rate,
"tp": tp, "fn": fn, "fp": fp,
}
效果上,这段函数对几千行 prompt 也能在毫秒级内算完 precision / recall / 误召回率;注意点是”rows 里的 expect 字段必须由人工标注”,不能让被测 Skill 自己当裁判,否则评估就成了”自证循环”。
五、把误召回率压到可接受区间的七条做法
| 做法 | 适用问题 | 落地动作 |
|---|---|---|
| 收紧 description 关键词 | 描述过宽 | 删除宽泛词、留下”只在 X 时使用” |
| 用互斥完备(MECE)原则 | 语义重叠 | 每个 Skill 描述写清楚不接的邻居 |
| 加 “DO NOT trigger for” 反向条款 | 关键词堆叠 | 在 description 里写明拒绝的近邻场景 |
| user-invokable: false 关闭斜杠入口 | 想留 Skill 但不想被命令触发 | frontmatter 标记,禁止裸名唤起 |
| 高风险动作加确认门禁 | 不可逆工具被误触发 | SKILL.md 写 “执行前必须人类确认” |
| dry-run 默认开启 | 写/删/部署类 Skill | 脚本默认打印 diff,参数确认才执行 |
| LLM-as-Judge 抽检 | 上线后回归 | 用另一个模型对 1% 调用做误召回判定 |
5.1 收紧 description 不是写得越短越好
description 太短会让模型只靠”指令层”猜意图,猜错的概率反而上升。原则是”具体到动作 + 拒绝近邻场景”:写清 Skill 做什么、明确写出哪类请求不要用它。短到 8 个字以内一般就开始失真。
5.2 高风险 Skill 的”动作层收口”原则
误召回在触发层无法做到 0%,所以高风险 Skill 必须把”动作层”做小。脚本里所有写、删、部署类操作默认走 dry-run + diff 日志,参数里加 --confirm 才允许真正执行;触发层错了,diff 也只是写到日志里。这是”承认误召回无法消除”后的兜底。
5.3 评估 Skill 与被评 Skill 必须异源
用 LLM-as-Judge 自动判误召回时,评估用的 LLM 必须和被评估 Skill 使用的 LLM 不同源。同一系列模型对”该不该触发”的判断会高度趋同,无法发现同源偏差。
六、误召回率上线门禁建议
把误召回率做成”硬门槛”放进发布门禁,按 Skill 危险等级分档:
| 危险等级 | 典型 Skill | 误召回率硬门槛 | 配套动作 |
|---|---|---|---|
| P0 不可逆 | 部署、删表、发版 | 0% | 必须确认 + dry-run 默认 |
| P1 部分可逆 | 写文件、改配置 | < 2% | 沙箱化 + 路径白名单 |
| P2 只读 / 不可写 | 搜索、读取、汇总 | < 10% | 常规 review 即可 |
P0 类的零容忍是因为误召回一次就可能造成真实业务损失;P2 类的可读 Skill 即使误召回也只是浪费一点 token,门槛可放宽。
七、上线后持续监控
误召回率不是一次性评测就够,还要在生产中持续抽检:
- 灰度阶段按 1% 抽样做人工复核,把”实际触发”与”用户原始意图”对齐,标注误召回;
- 接入用户反馈通道,让用户能直接标记”这个 Skill 不是我想要的”,把反馈落到 trigger 评测集;
- 每两周用最新模型版本重跑历史评测集,看 baseline 漂移;
- 任何一个 P0 Skill 的误召回率突破 0% 即刻回滚,并在 SKILL.md 的 description 与动作层同步加固。
到这里,误召回的成因、量化、压缩手段与上线门禁就形成闭环。把这条链路坚持下去,Skill 库规模再大也不会退化成”加载什么全凭模型心情”。
常见问题(FAQ)
Q1:召回率和误召回率哪个优先?
P0 类不可逆 Skill 优先压误召回率到 0%;只读 Skill 可在精度合格的前提下尽量抬升召回率。
Q2:description 写多长合适?
经验区间在 60~200 汉字之间,过短会过宽、过长会被指令层稀释;必须含”做什么”与”不接什么近邻”。
Q3:上线后误召回率升高怎么办?
先回滚描述到上一个稳定版本,再用最新 prompt 集重跑 trigger eval,找到是哪一类近邻请求被错误命中,针对性收紧 description 的反向条款。