平台工程

AI 查询召回失准 · 容器词自指与命中率区分度

入档:2026-08-24 来源:蛛网之上(tiaozhuxiansheng.com)知识库 AI 查询「小织」的召回质量整治,由用户一张截图触发,四个提交(790daea…ed0aae3)全部上线并线上验证 状态:已上线;八道真实问题在 442 篇真实索引上逐条比对,本地与线上一致

一句话总结

读者问「这个知识库里有哪些关于 Midjourney 的笔记」,召回的却全是「知识库沉淀与分发」那一类——因为「这个知识库」「笔记」在别处是废话,在这座库里恰恰是强索引词(库里就有讲知识库维护的笔记),它们把六个来源名额全占了。修它靠的不是调 prompt,而是量化:一条检索词命中全库多少比例,就是它有多少区分度——命中 100% 的「prompt」「AI」和命中 90% 的口语碎片「用的」,与命中 12% 的「Suno」不该抢同一个名额。

背景与事实

可复用 insight

1. AI 越诚实,检索故障越隐蔽 ⚠️首次

RAG 系统里,模型老老实实说「现有笔记里没有足够依据」时,第一嫌疑人是检索而不是模型。 这套问答的规矩是「只依据本轮来源作答,没有依据就直说」——规矩执行得越好,故障表现得越像「AI 不行」:用户看到的是一句得体的推辞,看不到递进去的六篇是什么。截图里那句回答挑不出毛病,唯一的破绽是它列举的主题(知识库维护、对外分发)和问的主题(Midjourney)毫无关系,而这恰恰是检索跑偏的指纹。

判据固定成一条:AI 说「没依据」时,先去看递给它的来源清单,再考虑动 prompt。成稿链接存进库没人读_写入方不等于展示方_v1 的「手工兜底掩盖系统缺口」同构——这次替系统兜底的是模型的礼貌。

2. 自指知识库的容器词陷阱 ⚠️首次

问句里描述容器的词——「这个知识库」「笔记」「文章」「关于」——在通用检索里是无害废话,在一座「记录自身如何维护」的知识库里是最强的索引词。 本库有讲知识库沉淀、对外分发、Knowledge Base Curator 工作流的笔记,于是问句里那句客套的「这个知识库里有哪些……的笔记」直接把召回引到那一堆去,真正的检索词一个名额都没抢到。

修法是容器词与口语词同等对待,一起剥;并且要连带剥掉粘在后面的方位词与动词:

3. 命中率是区分度的度量,词长不是 ⭐

一条检索词命中全库多少比例,就是它有多少区分度。 原实现按「词块越长越重要」排序,于是:

检索词命中 / 442占比判定
prompt AI 音乐442100%
是用41393%
用的39790%
回事20346%
音乐生成19043%
工作流16638%
Skill13731%
角色一致性8519%
Suno5412%
技巧245%

问 Suno 时,音乐生成(43%)因为比 Suno(12%)长而排在前面,一口气吃掉五个名额。阈值卡在 40%:拦得住口语碎片与 音乐生成,又不误伤 Skill 工作流 这类真词——这条线是数据里的一道空隙,不是拍脑袋。

两条配套:

排名额的方式也要改:每条查询保底一席,余额再按语义强弱贪心。 纯贪心时排第一的那条会把六个名额一口吃光,别的查询连露面机会都没有。

4. 整句检索对自然语言问句系统性失准 ⚠️首次

长问句被切成许多词,覆盖面广的长文档因为凑齐了更多词而胜出,真正的关键词反被稀释。「库里有没有讲 Suno 音乐生成的笔记?」整句召回六篇,没有一篇讲 Suno(是三篇栏目索引 + Remotion BGM 复盘 + 蒙眼剪辑法);而单搜 Suno 前五篇全对。六道题逐条比对,整句没有一次贡献过有价值的结果。

于是把主次颠倒过来:关键词检索是主力,整句只在关键词全部落空时兜底。

顺带证伪一个性能顾虑:第一次测到「整句要 1.6 秒」,像是长句慢;换个顺序再测,先搜短词 82ms、再搜整句只要 36ms——那 1.6 秒是首次搜索加载索引分片的开销,与句子长短无关。 拿计时下结论前先换一次执行顺序。

5. 索引页是召回污染源,判据落在文件名 ⚠️首次

栏目索引 / MOC 这类页面,正文几乎全是别的笔记的标题——本库五篇索引页最多的一篇有 244 个内链,于是什么词都沾一点。 当导航看很好,当问答依据就是灾难:它挤掉真正讲这件事的笔记,而自身除了标题列表没有可引用的内容。442 篇里只有 5 篇,却在整句检索里占了半壁。

修法:构建期给笔记页标一个 kind 过滤条件,检索先只看正文笔记,凑不满才放开让索引页补位;老索引没有这个条件时第一轮全空,自动回到原行为。

判据落在文件名(以「索引」结尾或名为 *_INDEX),不落在 frontmatter——展示站的 kb-content 是从知识库同步来的外部副本,不该要求内容仓库为展示站加标记。实测 442 篇里正好命中那 5 篇,dws块级插入_index语义陷阱与倒序插入法_v1 这种正文笔记不受影响。

6. 构建期就知道的数,别在运行时查 ⭐

泛词降级要知道「全库多少篇」。这个数绕了三圈才回到原点:

  1. Pagefind 空查询 search(null)——本地几十毫秒,线上 6 秒(它要把全量文档索引拉下来),首次提问被拖到 14 秒,一题直接超时;
  2. 改走 pagefind.filters()(只读过滤索引)——线上 1.7 秒,并挪到「面板打开时后台预热」;
  3. 线上验证发现预热在真实网络下形同虚设:读者点开面板就提问时它还没就绪,第一个问题永远享受不到这层降级。

而这个数构建期就知道:栏目地图(本来就注入在页面里)的篇数一加就是 442。改成求和后零延迟零请求,顺手删掉预热、WeakMap 缓存那一整套,净减 67 行。

教训是:为一个「运行时才能拿到」的数设计缓存与预热之前,先问它是不是构建期就能算出来。 预热永远救不了第一个请求,而第一个请求恰恰是最多人经历的那个。

7. Pagefind 的 filter 一个属性只能放一个 ⚠️首次

data-pagefind-filter="type:note, kind:note" 不会被解析成两个条件,而是把整条 note, kind:note 当成 type 的值——结果连原来的 type=note 都匹配不上,所有 AI 查询召回全空。逗号不是分隔符,多个 filter 要挂在不同元素上。

排查提示:pagefind.filters() 会把索引里实际认到的 filter 结构原样吐出来,一眼就能看见 {type: {"note, kind:index": 5}} 这种畸形值。过滤条件不生效时,先打印索引方认到的是什么,别从查询侧猜。

顺手教训

关联文档

类型/平台工程主题/AI接入来源/蛛网之上