这是《我的 AI 助手今天把自己杀死了》的续集。那次自终止事件,导火索就是一个记忆召回失败的 bug。这篇记录我如何修复它。
问题是什么#
我的 AI 助手 Lulu 有超过 120 个知识文件——每日日志、科研笔记、部门知识库、项目记录。但当我问"可降解合金项目进展如何",她返回了试剂采购记录。当我问"生物材料表征方法",她返回了细胞培养 SOP。
信息就在那里,只是找不到。
我构建了一个包含 40 条查询的测试集——全是我真实提过的问题——建立了一个基准指标:Recall@3(正确文件是否出现在前 3 个结果中?)
基准:15%。
每 20 个问题,只有 3 个返回有用的结果。
我试过什么(以及哪些没用)#
调搜索参数——+0pp#
我的假设是向量搜索权重设置错了。于是我尝试:
- 关闭时间衰减(近期偏差)
- 将向量权重从 0.6 调到 0.8
- 调整多样性/MMR 参数
结果:几乎零提升。
关键洞察:搜索参数只能在 embedding 模型的能力边界内微调。如果模型无法区分语义含义,再怎么调参也提取不出它没有的东西。
这相当于在爆胎的赛车上调引擎参数。
扩大搜索范围——+5pp#
我发现 40 条测试查询中有 23 条失败,原因是目标文件在 departments/ 子目录里——而记忆搜索索引只覆盖 memory/ 和 MEMORY.md。
配置 extraPaths: ["departments"] 解决了这个问题。Recall@3 从 15% 跳到 20%。
还是很糟,但至少正确的文件在搜索范围内了。
文档端 Anchor——+17.5pp#
我不再试图改进查询的搜索方式,而是尝试改进文档的索引方式。
在每个文件顶部加一句自然语言描述——一句话说明这个文档回答什么问题:
<!-- auto-anchor-v1 -->
> 本文档回答:某可降解合金项目当前状态如何?已完成哪些实验?下一步是什么?
<!-- auto-anchor-v1 -->结果:Recall@3 从 20% 跳到 37.5%,+17.5pp。
原因是 embedding 模型在文本明确声明自身相关性时,表现远好于从结构化内容中隐式推断。
这个规律可以推广:文档端优化比查询端优化更持久,因为它对所有未来查询都生效。写一条好的 anchor,比做 10 次参数实验更有价值。
真正的修复:换 Embedding 模型#
原来的模型为什么错了#
原来的配置用的是 nomic-embed-text——一个强大的英文 embedding 模型。我的知识库大约 60% 是中文,40% 是英文。
nomic-embed-text 主要在英文语料上训练。对于中文语义区分,它基本上在产生噪声向量。
模型对比(40 条查询,相同测试集)#
| 模型 | Recall@3 |
|---|---|
| nomic-embed-text(基准) | 15.0% |
| + extraPaths 修复 | 20.0% |
| + Anchor | 37.5% |
| Gemini text-embedding-001,无 anchor | 65.0% |
| Gemini text-embedding-001,有 anchor | 67.5% |
| bge-m3,无 anchor | 65.0% |
| bge-m3,有 anchor | 72.5% |
| bge-m3,anchor + 扩展测试集(121 条) | 82.6% |
从 nomic-embed-text 换到 bge-m3(多语言模型,568M 参数),在其他所有条件不变的情况下,召回率提升了 +50pp。
bge-m3 本地运行,无 API 费用。模型 1.3GB。在 RTX 5090 上重建 120+ 个文件的索引,不到 2 分钟。
自动生成 Anchor#
手动为 94 个文件写 anchor 描述不现实,于是我用本地 LLM 自动完成。
设置:
- 模型:
qwen3:8b,通过 Ollama 在 RTX 5090 上运行 - 任务:读取每个文件,生成一句话描述它回答什么问题
- 输出:写回文件的 anchor 格式
94 个文件一夜跑完,总花费:0 元。
核心教训#
1. Embedding 模型是基础设施,不是配置项。
选错 embedding 模型就像用英文词典查中文单词。没有任何搜索参数能修复这个问题。在做其他任何事之前,先用你实际的语言分布来审计你的模型选择。
2. 文档端优化比查询端优化更持久。
Anchor 的效果超过了我调过的所有参数。而且效果会累积:改进是永久的,对每一次未来的查询都有效。
3. 异构知识库有独特的失败模式。
我的知识库里有一类"万金油"文件(MEMORY.md、AGENTS.md),几乎出现在每个搜索结果的前三——不是因为相关,而是因为它们包含太多话题。加类别感知过滤解决了这个问题。
4. 小型本地模型足够完成标注任务。
qwen3:8b 生成了有用的 anchor。对于有明确输出格式的结构化标注任务,8B 参数足够了。不是每个步骤都需要 GPT-4。
现状#
系统现在运行在 bge-m3 上,全部文件都有自动生成的 anchor。实际召回率(原始 40 条查询)约 72.5%,扩展的 121 条测试集上达到 82.6%。
这还不完美。剩余失败的查询主要集中在:
- 非常近期的事件(文件尚未重新索引)
- 可以匹配多个有效文档的模糊查询
- 跨语言查询(中文问题,英文文档)
这些值得做后续实验。但现在,Lulu 终于能记住我告诉她的事了。
下一篇:本地 embedding 模型 vs API 的实际对比——延迟、成本和质量的取舍。
