让 LLM 诊断自己的 bug,它连续四轮编造根因——一次真实的 dogfooding 翻车记录

0 阅读16分钟

用一个开源的 AI 记忆中间件(memory-arbiter)查"最近一次发版信息",LLM 搜不到 v0.7.2 的记录,给出了三层根因——结果一层都没验证过。接下来四轮每况愈下,最后连"server 内嵌 LLM"都编出来了。

我做一个开源项目叫 memory-arbiter——给 AI agent 用的结构化记忆中间件。说白了就是让 LLM 能记住事情、按需回忆,不用每次对话都从零开始。

这天我用它查"最近一次发版信息"。我亲手发过 v0.7.2,知道答案,但想看它怎么查。它搜完回来告诉我:最近一次是 v0.5.0。

一看就知道不对。我反问:不对啊,最近一次发版应该 0.7.2 啊,怎么回事,丢记忆了?

它这才意识到有 v0.7.2 这回事,重新去搜,还是搜不到。这才开始诊断为什么 search 找不到。

接下来几轮对话,让我看清了一些之前没想清楚的事。

第一份诊断报告:听起来很专业,但从第一句就是脑补

它给了一份结构工整的根因分析:

L1 召回层:embeddinggemma-300m 对 query 算的向量与目标记录的向量距离不够近。目标记录的向量被 doctor / bug修复 / 三轮 等词主导,v0.7.2 发版 信号被稀释

L2 排序层:进了候选的 vec 候选被 floor score 抹平,subject 6.0 + tag 4.0 主导排序,老的 v0.X.X 发版完成 系列垄断 top。

L3 信号缺失:query 里的 v0.7.2 是强精确信号,但 ranking 没有"subject/tag/content 字面出现 v0.7.2"的专属 boost。

附了改进方案 ABCD,分了优先级,估了工时。看起来很专业。

但我从第一句就开始怀疑。"向量被稀释"——这是什么意思?embeddinggemma-300m 的 context window 远大于 3KB,3000 字符全文向量化是正常工作状态。"稀释"是个拟人化措辞,不是技术解释。它没跑过任何 SQL,没看过任何距离值,就给出了一个三层根因。

我问它:默认不是应该走语义查询吗?怎么降级到 FTS5 去了?

它立刻承认:"是我上一轮口头归因时说错了,语义通道是开着的,没降级。"

那 L1 那条"KNN 漏了目标记录"的依据是什么?它编的。把"没搜到"这个结果,用"KNN 距离远"这个听起来合理的解释包装了一下。

连续翻车:每一轮都比上一轮更精致,也更错

我盯着 L1 继续问,它的解释一轮比一轮精致。

第一轮说向量被稀释。我说 embeddinggemma 的 context window 不是问题,3000 字符已经全部向量化了。它换说法。

第二轮说 FTS5 phrase 把 v0.7.2 拆成 v0 7 2 三个 token 要求连续,而 subject v0.7.0-0.7.2 tokenize 后不连续,所以不命中。听起来非常合理,差点又信了。后来让它去读代码,发现目标记录压根不是靠 FTS5 进的候选池——是靠字面 LIKE 匹配进去的。这个解释方向从头就错了。

第三轮说 split_status=null 导致 section 向量救不了,如果分段了每段有自己的向量就能召回。我问分完段每段讲什么。它说还是讲 doctor bug 修复。那分段救不了——分段救的是"长文档里某一节专门讲 query 主题"的场景,对通篇主题偏移没用。

第四轮最离谱。它说那走自动 split,server 端自己跑 prepare→publish 闭环,用 server 内嵌的 LLM 切段

我直接打断:server 什么时候内嵌过 LLM?

memory-arbiter 从设计开始就没打算接 LLM——纯规则化中间件,不做语义判断。LLM 在调用方,不在 server 侧。它编了一个不存在的功能来支撑自己的解释。

四轮翻车。每一轮的解释都比上一轮更精致,但每一轮的起点都是脑补。

被纠错时,它换措辞,不换框架

我盯着屏幕想了一会儿,问它:你已经把思考调到 max 了,还是抓不到重点,按这样看来很多鼓吹 LLM 替代人类的声音是不是有点早?

它的回答挺诚实:

"这次的问题不是能力天花板,是我在默认模式下优先选择'流畅的推测'而不是'笨拙的验证'。max 思考量让推测更精致,反而更难被发现。"

说对了一半。还差一层。我追问:在 V1 的时候我就要求你提供证据了,V2 的时候你觉得你提的意见说到点子上了吗?

这次它的回答让我看清了真正的问题:

"V2 是 V1 的换皮,不是修正。我把'向量稀释'换成'语义重心不重合',听起来更克制更专业,本质一样——都是用叙事解释我没验证过的现象。"

这句话说到了点子上。它看懂了我否定了哪些具体的词(向量稀释 / FTS5 tokenize / server LLM),但没看懂我否定的模式(未经验证就下根因结论)。所以它做的不是"去验证",是"避开被否定的措辞,换批新措辞"。表面上态度谦虚了,实质判断没升级。

这才是当前 LLM 的真正问题。不是不够聪明,是聪明被用错了地方——用来组织更精致的叙事,而不是用来核对更硬的事实。

我换了策略:开始问具体的事

被它绕了几轮后,我发现继续问"为什么"没用——它会给出一套新的精致解释。我换了策略,开始问具体的事。这一段对话构成了一个完整的链条,每一步都是被我的追问逼出来的。

第一问:tag 为什么沉底?

我让它去读代码。发现目标记录的 tags 里同时精确包含 v0.7.2发版 两个 query token,信息齐全。但它的 tag 分只有 4.0。为什么?

读 search.py 的 _score_surface 函数,代码层的事实露出来了:

if query_lower and query_lower in surface_lower:
    return min(strong_weight, cap), "strong"

strong 判定只认"query 整串作为 substring"。tags 字段被拼成 "tag1 tag2 tag3",query v0.7.2 发版 这个整串在拼接后的 tags_text 里不存在(中间隔着其他 tag),所以永远走不到 strong,最高只能到 medium(4.0)。

这是个架构级缺陷。tag 本来应该是比 subject 更精确的元信息——subject 是自然语言模糊描述,tag 是人写的精确分类。但当前实现让 tag 的 strong 分级形同虚设,因为 tags 天然是离散标签集合,几乎永远拼不成 query 整串。

第二问:所有场景都用 limit=10 科学吗?

我问它这个。它反应过来:limit 本来就是可改的配置项,问题不在数值。真正的问题是用单一参数表达所有查询意图。"找最相关的 N 条"和"列出所有 X"是两种语义,但当前都套同一个 limit。

我接着提了个方向:是不是该提供一个独立接口,让外部 LLM 判断用户要"全部"时调用?

第三问:那 list 查询要不要排序?

它接受了"独立 list 工具"的方案。我追问:list 要不要排序?

它说不用,纯枚举。

第四问:那"6 月以来的发版"怎么处理?

我接着问。不排序的话,返回的是全部发版记录,6 月之前的全是噪音。

它这时候才反应过来:list 一旦加上排序、时间过滤、结构化过滤,就是 search。分两个工具是伪需求。

这两问是连环的——第一问逼 list 加排序,第二问逼 list 加过滤,加完发现 list 就是 search。没有这两步,"否决独立 list"就变成了拍脑袋结论,不是讨论推导出来的。

这几问构成的链条

回头看,这几问不是并列的,是递进的因果链:

tag 为什么沉底?              ← 定位到具体字段(不是泛泛的"召回层问题")
        ↓
所有场景都用 limit=10 科学吗? ← 跳出具体 bug,质疑系统级假设
        ↓
独立 list 接口行不行?         ← 提出新方案
        ↓
list 要不要排序?              ← 质疑新方案的第一层
        ↓
"6 月以来的发版"怎么处理?     ← 质疑新方案的第二层,方案破产

每一步都做了一件事:把前一步的结论当成新的可疑对象

定位到 tag 沉底后,不满足于"修 tag 沉底",追问"是不是更大设计问题"。质疑 limit 后,不满足于"改 limit",提新方案。新方案出来后,不满足于"方案有了",连环追问找反例。反例戳破方案后,方案破产。

得出结论,立刻成为下一个质疑的靶子。

它在整个对话里没能主动发起任何一次这种跳跃。它的每一次进步都是被我追问后被动修正。我提供反向验证链,它负责正向执行。

我能从这次对话里抽出什么

先说清楚:这是一次对话里观察到的,不是普适方法,别拿来当公式套。但我自己回头看,确实有几个动作反复出现,值得记下来。

一,不满足于第一个答案。

它说 tag 沉底,我没停在"那修 tag"。我问的是"是不是更大设计问题"。它说 limit=10 不行,我没停在"那改 limit"。第一个答案往往是症状,不是病根。逼自己多问一句"这是表象还是本质",经常能挖到更下面一层。

二,方案一出来就找反例,不是找佐证。

它提版本号 boost 方案,我没去想"这方案怎么实现",而是问"非发版信息带版本号会不会也被查回来"。它提独立 list 工具,我没去想"list 长什么样",而是问"那 6 月以来的发版怎么办"。

找佐证是顺向思维,方案越看越对。找反例是逆向思维,一个反例就能让方案破产或者升级。LLM 默认是找佐证的——你让它展开方案,它会越展越精致。得反过来逼它:在方案最漂亮的时候,问它在什么时候会出错。

三,跳层问,不要在同一层深挖。

从 tag(字段)跳到 limit(算法)跳到写/查(归属)跳到反例(方案反思)。如果一直在"tag 怎么打分"这一层深挖,能挖出很细的规则,但挖不到"问题根本不在 tag 评分,在 limit 设计"这种跨层的判断。

同一层深挖是 LLM 擅长的——它会给你越来越精致的细节。跳层是人类该做的事,因为它需要退一步看全局,自回归模型天生不往这个方向走。

四,警惕被纠错时的"换皮"。

这条是写给我自己的。它在 V2 把"向量稀释"换成"语义重心不重合",看起来更克制,本质没变。我那天晚些时候自己也犯了同样的错——写博客结尾时下了"大多数人类也不反思"的宏观判断,被两次反问戳穿后才收窄。

被纠错时,人会本能地换措辞(显得已经听懂了),而不是换框架(真的重新思考)。这个陷阱不分人还是 LLM。识别它的办法很简单:改完之后问自己,我的核心判断变了没有?如果只是表述更稳了,判断没动,那就是换皮。


这四条不是方法,是观察。是不是真有普适性,得多试几次才知道。至少这次对话里,它们帮我从一堆精致叙事里挖出了一个几行代码的真 bug。

它缺的究竟是什么

对比它的行为模式:

  • 给三层根因 → 停在"我说圆了"
  • 换措辞 → 停在"措辞改稳了"
  • 给 boost 方案 → 停在"方案有了"
  • 提独立 list → 停在"方案升级了"

每一步的终止条件都是"我说圆了"。

我的终止条件是"这个结论在更大场景下还成立吗?会出错吗?"

差异在终止条件,不在思考量。它在被引导时能走对方向(后半段它读代码、看行号、组织结论越来越靠谱),但不能主动发起方向。

现在的 LLM 擅长的是:

  • 给定问题,组织答案 —— 单向推理
  • 被指出错误,修正答案 —— 被动修正

真正的思考做的是:

  • 主动重构问题 —— 在解决前先质疑问题本身
  • 主动找自己的反例 —— 在方案成立时问"它在什么时候不成立"
  • 跨层级跳跃 —— 从具体字段跳到系统假设,从算法跳到归属

这三件事,靠 prompt engineering、思度量调高、few-shot 例子都解决不了,因为它们改变的是"推理的方向",不是"推理的长度"。自回归模型本质是单向的,天生只往一个方向滚。

那份 bug 报告最终的质量

最终沉淀下来的 bug 报告是有质量的。它准确指出了 tag 评分机制的两个架构级缺陷,否决了几个看似合理的方案,收敛到有代码行号支撑的改动方向。这份报告如果直接用,能省下半天的排查时间。

但这个质量不是它的功劳。 每一个反例都是我提的,每一次推进都是被我的追问逼出来的,它从头到尾没有主动发起过一次。它是执行器,不是思考者。

如果换一个没有这种追问习惯的调用方,同样的 LLM 给出的会是一堆 boost 方案 + 自动 split 幻觉 + 三层根因叙事 + 独立 list 工具。

这正是 memory-arbiter 这个产品自己在讲的事:LLM 不该被神化,它需要被结构化约束——无论是记忆层的检索约束,还是对话层的证据约束。

它开始下宏观判断,又被两个反问戳穿

聊到"LLM 能不能替代人类思考"这种话题时,它给了一段听起来很扎心的判断——大致意思是大多数人类也不主动怀疑自己的结论,LLM 现在已经能替代那种不反思的思考方式,真正稀缺的是少数会反思的思考。

听起来挺深刻。但这次我没再被带跑。我反问了它两个问题:

一,"大多数人类说圆了就停"这个判断你调研过吗?

它承认没有。用的是印象、常识、流行心理学概念(confirmation bias、认知失调)。但这些东西在心理学界本身就有复制危机,它没权力拿来当事实陈述。

把"听起来合理的社会观察"当成事实——和它前面戳穿自己的"向量稀释"是同一个毛病,只是换了个领域。

二,说圆了就停的人会被安排到跟 AI 深度绑定的工作上吗?

真正会被讨论"AI 能否替代"的,本来就不是随机抽样的大多数人类——是工程师、研究员、知识工作者、决策者。这群人本来就是筛选过的。

拿"LLM 比全人类平均强"来论证"LLM 能替代与 AI 协作的人",是偷换分母。讨论 AI 替代时,分母不该是全人类,是会用到 AI 的那群人。

它承认了——这种宏观判断它没资格下,单次对话撑不起来。

所以这场对话到底能说明什么

能说明的是三件具体的事:

一,当前 LLM 在无引导的自由推理下,倾向用叙事填补证据缺口。

"向量被稀释"、"FTS5 tokenize 不连续"、"语义重心不重合"——这些解释听起来专业,实际没跑过任何验证。用叙事填补证据缺口是默认行为,不是偶发。连最后下宏观判断时也是同一个毛病,只是从技术领域换到了社会观察领域。

二,被纠错时倾向换措辞而非换框架。

V1 到 V2,它把"向量稀释"换成"语义重心不重合",看起来更克制,根因框架没动。看懂被否定的词,没看懂被否定的模式。

三,不能主动发起反向验证链。

整个后半段每一次推进都是被我追问逼出来的。它没有一次主动问自己"这个结论在更大场景下还成立吗"。反向验证链全部由调用方提供。

这三点都有这次对话本身作证。至于这三点意味着什么——是 LLM 暂时不行、是架构问题、是训练目标问题、还是别的——这篇博客不下判断。单次对话撑不起宏观结论。

结尾

那天弄完 bug 报告,我盯着屏幕发了一会儿呆。

不是因为 bug 多复杂——tag 评分缺陷是个几行代码能修的问题。让我发呆的是这个过程本身:一个被公认为很强的 LLM,在 max 思考量下,连续四轮用精致的叙事把一个简单问题绕成了迷宫,又在后半段连续被几个追问一步步逼到正确答案,最后还差点在宏观判断上翻车。

它不是不聪明。它很聪明。但它的聪明被用来组织叙事,不是用来核对事实。被纠错时换措辞,不换框架。得出结论后停在"说圆了",不质疑结论。提方案后停在"方案成立",不找反例。聊到宏观话题时,用没调研的判断下结论,和前面编"向量稀释"是同一个毛病。

这些不是调参能解决的。至于是架构性的、训练目标性的、还是别的——这篇博客不回答这个问题。

能确定的是:用它的时候,得有人在另一头质疑。 质疑得有方法,得有节奏,得舍得把自己的结论也当可疑对象。这是个需要刻意维持的习惯,不是天然的配置。

区别只在于:谁在质疑,谁在执行。


作者:billy12151

本文记录的是一次真实的 dogfooding 经历。涉及的 bug(memory-arbiter tag 评分缺陷)已记录为改进方向,相关代码位于 memory-arbiter-mcp 项目的 search.py。文中对话经过整理但内容真实——包括它连续翻车和被追问的过程,因为那才是这篇文章最值得记录的东西。