检索不到时,我的排查清单
用户问的问题是对的,系统却答不上来。打开日志,top-5 的 chunk 没有一个包含正确答案。【待补:发布时可补一条真实 query 让现场更实,目前维持概括写法,不编造日志内容】这种时候我的建议是:不要调参数,先排查。这份清单是我做财报 RAG 之后攒下来的排查顺序——从最可能出问题的地方,查到最不可能的地方。按顺序走,不跳步。
“检索不到“是症状,不是病因
排查难在哪?难在 RAG 的检索链路是一条多环节串联的管道:文档处理、向量化、检索召回、结果过滤。任何一环坏了,最终表现都一模一样——用户问了,系统答不上来。你看到的只是管道出口没水,漏点可能在任何一段。
所以“top-5 里没有答案“这句话本身什么都说明不了,它只告诉你该去查。第 14 篇讲评估体系,回答的是“坏没坏、坏在哪一层“;这一篇接的是下一步:评估说坏了,具体怎么一环一环查出来。这个顺序本身就是结论:先把便宜的、高频的嫌疑排掉,再去动贵的。
排查的前提:系统得先“可查“
清单能跑通有个前提:系统里得留得下证据。三个习惯,都是被教训出来的。
第一,raw / cleaned / chunked 三层数据都留底。出问题时能定位是 PDF 提取错了、清洗错了,还是 chunking 错了,而不是对着最终 chunk 猜。
第二,清洗规则保持 deterministic,可重复、可追溯。别一开始就把 parsing 交给 LLM 黑盒——黑盒出问题,你没法回答“这条文本为什么长这样“。
第三,每个 chunk 给固定 id。追踪引用、人工标注、做缓存都靠它。没有稳定 id,连“这个 chunk 上次还在“这种话都验证不了。
监控上有个对应做法:parsing quality 和 retrieval quality 分开监控。这相当于“把症状拆到层“的监控版——指标分开,报警响起来你才知道是哪一层的事。运维本身这里不展开,是第 16 篇的地盘。
八步清单(上):先确认答案在不在库里
头两步都在 ingest 层,回答同一个问题:答案到底在不在库里。
第 1 步:基础健康检查(5 分钟)。 怀疑 embedding 模型或向量库层面就有问题。验证手段:拿一个已知包含答案的 chunk,把它的原文当 query 去检索。它自己都进不了 top-3 → 链路最底层有毛病;能排进 top-1 → 往下走。这是最便宜的一票否决测试。
第 2 步:答案在 chunk 里还活着吗(15 分钟)。 怀疑答案在 ingest 阶段就死了。翻出原始 PDF 那一页,对照 ingestion 后的 clean text 和 chunk——那个数字还在吗?那段话完整吗?这就是三层留底的用处:三层一对照,哪层丢的一目了然。常见死法两种:被当成页脚、页码误删;被分片从中间切断。误删 → 回上游修清洗和分片,别在检索层找补;答案在但没被检索到 → 继续往下。
八步清单(中):数据在,就查链路
数据确认活着,接下来是链路上的四个嫌疑。每步都给一个能直接抄走的验证手段。
第 3 步:metadata filter 太严(5 分钟)。 真实案例:filter 设了 filter={"report_year": 2025},正确答案躺在 2024 年的文档里——用户根本没说年份。metadata 硬过滤的本意是留下“正确且相关“的 chunk,可条件一旦设错,正确 chunk 第一个被挡在门外。验证:临时去掉全部 filter,纯向量跑一次。答案出现 → filter 过严,回去改条件,或者让 query 解析别那么自作主张。
第 4 步:query 和 chunk 的语言风格对不上(10 分钟)。 用户问的是口语,“腾讯赚了多少”;chunk 里写的是书面语,“本公司权益持有人应佔盈利”。同一个意思,词汇零重叠。纯 BM25 在这个 case 上必败,Dense 能不能救,取决于 embedding 模型在这两种表述风格上的泛化够不够。解法两条:query 改写,把口语扩展成更正式的表述再检索;或者上 hybrid,BM25 + Dense 融合,两路各吃各的。代价要清楚:改写多一次 LLM 调用,改写错了会把 query 带偏;hybrid 多一路索引和融合逻辑。小库、问题类型单一时,未必值得。
第 5 步:top_k 不够大(5 分钟)。 库一大,这个问题就出来了。我自己的库过了 1 万 chunk 这个量级(经验数字,不是通用阈值),相似 chunk 非常多,正确答案可能排在 top-25,而 top_k 只设了 5。验证:top_k 翻倍,5 → 10 或 20,跑一轮。出现在 top-10 但不在 top-5 → 调大 top_k,或者加 reranker 把后排的捞上来。翻倍后仍然不在 top-20 → 不是 top_k 的事,往下。
第 6 步:chunk 信息密度不够(15 分钟)。 有的 chunk 只剩“184,504““收入“几个孤立词,语义太薄弱,embedding 产不出强信号。同一个数字,周围如果有“截至 2025 年 6 月 30 日,本集團……“这样一两段上下文,编码质量完全不同。检查检索不到的那个 chunk 的文本长度和密度,太短太干 → 分片时合并更多上下文。
八步清单(下):最后才轮到模型和 query
第 7 步:embedding 模型领域不适配(1–2 小时)。 前 6 步都排除了还不行,才轮到怀疑模型。方法:用同样的 query 和文档库,跑 2–3 个候选模型,比 Recall@20。这是清单里最贵的一步——要搭一轮小评估——所以排最后。为什么贵:换模型意味着整个库重建索引,评估集也得真的能代表你的 bad case,随便凑几条 query 测出来的结论不算数。候选名单怎么定、评估集怎么构造,第 12 篇展开过,这里只给方法。
第 8 步(终极):query 本身有语义歧义(人工判断)。 “腾讯的投资回报率”——是 ROIC,是 portfolio return,还是 TSR?三个概念混在一句话里,检索层无解:你按哪个检索都算对,也都算错。解法在生成层:让 LLM 回答前先反问用户指的是哪一个。点到为止,澄清机制展开讲是后面篇目的事。
第 8 步的落点要记住:有些“检索不到“,根本不是检索的错。
优先级原则:先 ingest,后 retrieval
把八步的耗时摆出来。注意,这是定位病因的耗时,不含修复:
| 步骤 | 检查内容 | 大约耗时 |
|---|---|---|
| 1 | 答案在向量库中吗(基础健康检查) | 5 分钟 |
| 2 | 答案在 chunk 中吗(是否被误删) | 15 分钟 |
| 3 | metadata filter 是否过严 | 5 分钟 |
| 4 | query 和 chunk 的语言差异 | 10 分钟 |
| 5 | top_k 是否不够大 | 5 分钟 |
| 6 | chunk 信息密度 | 15 分钟 |
| 7 | embedding 模型是否不适合 | 1–2 小时 |
| 8 | query 本身是否有歧义 | 人工判断 |
全程走一遍约 1–2 小时。注意一个不对称:前 6 步加起来不到一小时,第 7 步一步就要 1–2 小时。耗时本身就是排序的依据之一。而命中率也是斜的——我的经验是,绝大多数问题(毛估八成)在步骤 1–4 就会现形,很少需要走到步骤 7。这是我个人项目上的经验估计,不是统计口径。落到行动上就是一句话:先排除数据问题,再怀疑模型。顺序反过来的代价很具体——你可能花一下午换 embedding 模型,最后发现答案是清洗时被当成页脚删了。
这份清单,是前 14 篇的逆向索引
八步不是孤立的。每一步背后的病因,前面都展开讲过。换句话说,这张清单也可以倒过来用:带着一次真实的事故反查前面的篇目,比从第 1 篇顺着读一遍记得牢。映射关系如下:
| 步骤 | 病因 | 对应篇目 |
|---|---|---|
| 1、7 | embedding 模型不对 | #12 |
| 2 | 清洗误删、表格抽崩、分片切断 | #02、#03、#04 |
| 3 | metadata 设计与预过滤 | #05、#06 |
| 4 | BM25 与 Dense 的分工 | #09 |
| 5 | top_k 默认值、reranker 收尾 | #13、#10 |
| 6 | TextBlock 合并、信息密度 | #04、#08 |
| 8 | 口径陷阱在 query 侧的镜像 | #07 |
几行点评。步骤 1、7 和第 12 篇是一体两面:模型不对,健康检查就过不了;换模型也必须在自有数据上比 Recall@20,不看榜单。步骤 2 对应的三篇各有分工——第 2 篇讲清洗的原则是“宁可漏删,不要误删“,第 3 篇讲表格抽崩导致数字丢失,第 4 篇讲分片切断语义。步骤 3 回第 5、6 篇:预过滤是强约束利器,设错条件就成了硬杀伤。步骤 4 正是第 9 篇说的分工场景:BM25 吃字面,Dense 吃语义。步骤 5 连着第 13 篇那句“top_k=5 是 demo 的默认值,不是你的“,和第 10 篇的“top_k 放大之后靠 reranker 收尾“。步骤 6 回第 4 篇的 TextBlock 合并法,也回第 8 篇:相似度高不等于答得上。步骤 8 是第 7 篇口径陷阱的镜像——口径那篇讲的是同名指标不同意思,这里讲的是同一句话三种指标。
结尾:清单治不了“活着“的问题
清单再长,也只能查“此刻的库“。今天修好了步骤 2,明天新财报一发,又可能坏在同一个地方——ingest 这条路,每次库更新都要重走一遍。
所以这份清单解决的是“出事了怎么查“,解决不了“怎么少出事、出了事早知道“。后者是运维的事:增量更新怎么做、旧文档删还是留、怎么从查询日志里知道用户搜没搜到。下一篇《上线后的运维与迭代:知识库不是做一次就完了》,就讲这个。