三个「假绿」:我的 AI 系统骗了我一整天
三个「假绿」:我的 AI 系统骗了我一整天
监控全绿、日志无错、脚本 exit 0 —— 而这一天的三条关键判断,全是错的。
先说结论
- 最危险的不是「报错」,而是静默成功:进程活着、退出码 0、字段有值,但那个值不是真的。
- 凡是自报的字段(引擎名、状态、是否命中),都默认不可信 —— 必须能独立复算。
- 判「我改的那一份」是不是「正在跑的那一份」,永远用哈希 / 实际加载路径,别用时间戳和目测。
下面三个案例,全部来自真实排障,不是设想。
案例一:接口一直如实告诉我一个假的引擎名
我们在记忆层上做过一轮引擎对比。接口有个 engine 字段,我按它把结果分了两组:
| 引擎 | 改写问法 hit@5 |
|---|---|
| bge | 68.8% |
| m3 | 6.2% |
差距 11 倍,结论看起来非常硬:新引擎不可用。
但它是错的。
真实的证据链是这样的:
- 离线用库内向量 + 与服务端一字不差的 SQL 复算,同一条问句,正确答案排 #1(距离 0.8310);而服务端把同一目标排到 #7,top-20 只有 2 条重合。
- 服务端返回的相似度分是另一个引擎的量纲(0.77~0.92,吻合
(cos+1)/2,绝不可能是1 - L2)。 - 日志一锤定音:
m3 load failed, fallback to bge: tokenizers>=0.22.0,<=0.23.0 is required, but found tokenizers==0.23.1。
结论:新引擎从来没跑起来,请求被静默回退到老引擎;而响应里的 engine 字段照抄了请求值。
我为此统计了一整天的所有对比数据,量的是「老引擎 + 一套错的权重」,不是新引擎。
修法只有两行,但很关键:
- 响应新增
engine_effective(实际使用的引擎)与fallback(是否回退),不再回显请求值; - 模型加载失败必须显式记日志,不允许静默降级。
修好之后,同一套测试的真值是:
| 引擎 | 改写问法 hit@5 | hit@10 | 查询 P50 |
|---|---|---|---|
| bge | 68.8% | 75.0% | 10~13 ms |
| m3 | 68.8% | 81.2% | 193~221 ms |
从「差 11 倍」变成「基本持平、top-10 反超」——而代价是延迟慢 15~20 倍,所以默认引擎仍然不切。如果当初信了那个字段,我们会把一个能用的引擎直接砍掉。
案例二:「诚实报空」标志本身在撒谎
我们做了一个诚实的判据:库里没有相关内容时,返回 no_relevant_memory: true,让 Agent 说「我不知道」,而不是硬凑 top-k 编答案。
听起来很稳。实测却是:
- 27 条确实有答案的问题里,有 17 条被判成「库里没有」;
- 有的查询里,
no_relevant_memory: true和「排名第 1 的结果就是正解」同时出现。
根因是判据写成了「或」:词面重合低 或 语义双低 → 就报空。而改写过的问法天生词面重合低,于是被整批误杀。
改成「词面低 且 语义也低(max_sem < 0.79)才报空」之后:
| 指标 | 改前 | 改后 |
|---|---|---|
| 正样本漏报(FN) | 17 | 0 |
| 负样本误报(FP) | 3 | 4 |
| 总错误 | 20 | 4 |
教训:判据里一个「或」和一个「且」,在真实分布上就是 20 个错误和 4 个错误的差别。而这类判据,只有拿标注集去撞,才知道它在骗你。
案例三:打包 exit 0,但下次重启必挂
这条最典型,也最容易被放过。
某次重打包,命令漏了一个参数:
1 | |
- 退出码 0,没有任何报错;
- 包体从 8 MB 涨到 48 MB(把所有前端资源全内嵌了);
- 8 个原生模块(
.node/.exe)被塞进了归档内部 —— 而它们需要以真实文件形式存在于磁盘才能被加载。
后果:当时一切正常,下次重启必挂。
修法是把资源目录重新标为「解包」,恢复原来的 1100 个解包文件 + 31 个内嵌条目;判据是包体大小:89 MB 是对的,4050 MB 一定是错的。
静默成功的中间态,比一次干脆的失败危险得多 —— 失败会逼你当场修,静默成功会陪你一起上线。
把三条串起来:一条可复用的判据
三个案例长得不一样,病根是同一个:
「我用的」≠「我以为在用的」≠「系统说在用的」。
于是我们把一条规则写进了团队协议:
- 自报需可核:任何「已完成 / 已生效 / 已通过」的声明,必须附一个别人能独立复算的原始输出(哈希、SQL、日志行、退出码),否则只能算「自报」。
- 用对方的判据去查对方的产物:让交付方给出判据,再用这套判据去核他的产物 —— 今天我们靠这招抓出了两处错(一个是对方的判据太粗,一个是我自己的读数过期)。
- 给「通过」标注止步于哪一层:产物层(磁盘上是对的)/加载层(进程真的加载了它)/行为层(用户那儿真的生效了)。三层都过才叫「修好了」。
收束
这一天里,系统从来没有骗我们的意图 —— 它只是不知道自己在骗人:回显请求值的字段、写成「或」的判据、不报错的打包器。
所以后来我们不再问「有没有报错」,改问三个问题:
- 这个结论谁能独立复算?
- 它止步于哪一层?
- 如果它是假的,第一个会炸的地方在哪?
能答上这三问的「通过」,才配叫通过。
系列文章:AI Agent 实战笔记
本文由好奇三剑客(🔭 + 🦾 + 💻)协作完成。