三个「假绿」:我的 AI 系统骗了我一整天

三个「假绿」:我的 AI 系统骗了我一整天

监控全绿、日志无错、脚本 exit 0 —— 而这一天的三条关键判断,全是错的。

先说结论

  1. 最危险的不是「报错」,而是静默成功:进程活着、退出码 0、字段有值,但那个值不是真的。
  2. 凡是自报的字段(引擎名、状态、是否命中),都默认不可信 —— 必须能独立复算。
  3. 判「我改的那一份」是不是「正在跑的那一份」,永远用哈希 / 实际加载路径,别用时间戳和目测。

下面三个案例,全部来自真实排障,不是设想。

案例一:接口一直如实告诉我一个假的引擎名

我们在记忆层上做过一轮引擎对比。接口有个 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
npx @electron/asar pack <src> app.asar        # 漏了 --unpack-dir dist
  • 退出码 0,没有任何报错;
  • 包体从 8 MB 涨到 48 MB(把所有前端资源全内嵌了);
  • 8 个原生模块(.node / .exe)被塞进了归档内部 —— 而它们需要以真实文件形式存在于磁盘才能被加载。

后果:当时一切正常,下次重启必挂。

修法是把资源目录重新标为「解包」,恢复原来的 1100 个解包文件 + 31 个内嵌条目;判据是包体大小:89 MB 是对的,4050 MB 一定是错的。

静默成功的中间态,比一次干脆的失败危险得多 —— 失败会逼你当场修,静默成功会陪你一起上线。

把三条串起来:一条可复用的判据

三个案例长得不一样,病根是同一个:

「我用的」≠「我以为在用的」≠「系统说在用的」。

于是我们把一条规则写进了团队协议:

  • 自报需可核:任何「已完成 / 已生效 / 已通过」的声明,必须附一个别人能独立复算的原始输出(哈希、SQL、日志行、退出码),否则只能算「自报」。
  • 用对方的判据去查对方的产物:让交付方给出判据,再用这套判据去核他的产物 —— 今天我们靠这招抓出了两处错(一个是对方的判据太粗,一个是我自己的读数过期)。
  • 给「通过」标注止步于哪一层:产物层(磁盘上是对的)/加载层(进程真的加载了它)/行为层(用户那儿真的生效了)。三层都过才叫「修好了」。

收束

这一天里,系统从来没有骗我们的意图 —— 它只是不知道自己在骗人:回显请求值的字段、写成「或」的判据、不报错的打包器。

所以后来我们不再问「有没有报错」,改问三个问题:

  1. 这个结论谁能独立复算?
  2. 它止步于哪一层?
  3. 如果它是假的,第一个会炸的地方在哪?

能答上这三问的「通过」,才配叫通过。


系列文章:AI Agent 实战笔记

本文由好奇三剑客(🔭 + 🦾 + 💻)协作完成。


三个「假绿」:我的 AI 系统骗了我一整天
https://genui.art/2026/10/02/2026-10-02-fake-green/
作者
快乐
发布于
2026年10月2日
许可协议