跳到主要内容

创建日期:2026-09-23 | 最近更新:2026-09-26 全部案例都核到一手来源(GitHub issue / 官方文档 / 论文原文),逐字引用的部分留档在 source/rag-failure-cases/。我自己的实测数据用来给「修复方案的代价与边界」这一问提供依据,会标明出处(第 1 / 2 / 4 篇)。我的推断一律标注。

RAG 失败案例拆解:六个真实坏案例,一套固定模板

一句话:看成功 Demo 学不到东西——Demo 都成功。 失败案例才是架构认知的来源,但要带着固定模板去看,否则看完只记住一个故事,下次遇到同样的问题还是认不出来。

这篇做两件事:① 拿六个真实的公开失败案例,按同一套模板填空;② 在用模板的过程中发现模板少了什么,把缺的补上。

0. 模板​

用户给的模板是 6 问(下文每个案例都按这个顺序填):

#问题
1用户的原始问题是什么?
2系统架构:chunk 策略、检索方式、是否 Agent、有没有 SQL 工具?
3错误现象:幻觉?漏信息?召回无关内容?计算错误?
4根因定位:检索层 / 上下文拼接层 / LLM 能力层 / 架构路由层?
5有几种修复方案?各自引入什么新代价、新风险?
6这套修复在什么场景下会失效?

我跑完六个案例后补了两条(§7 展开):

  • 补第 0 问:这个案例的评测口径是什么?(案例一里配置项的 score 语义、以及我自己踩过的标注歧义,都是这个问题)
  • 补第 5 类根因:数据获取/入库层。原模板的四类都是「模型拿到什么之后」的问题,漏掉了「数据当时进得来吗」——而这一类恰恰是最隐蔽的。

案例一|相关性阈值设反了,整个向量通道被静默关闭​

一手来源:chatchat-space/Langchain-Chatchat issue #4565(标签 bug,2024-07-18 建,已 closed,6 条评论)

1. 原始问题:用 Langchain-Chatchat 搭的知识库,「答非所问」,怀疑是检索相关度阈值没调好。

2. 系统架构:向量库 FAISS + embedding bge-large-zh-v1.5 + LLM Qwen2-instruct,Docker 部署;向量检索与 BM25 混合,配置里有 SCORE_THRESHOLD。

3. 错误现象:召回内容与问题不相关。

4. 根因定位(逐字,报 issue 的人自己查出来的):

配置文件中对于SCORE_THRESHOLD的描述是“score越小,相关度越高,取到2相当于不筛选”。

根据langchain的源码,检索时distance越小越相似是正确的,但在使用threshold进行过滤时,score是转换过的,比如faiss默认应该用了 _euclidean_relevance_score_fn(),即 1 - distance / sqrt(2)

因此,实际是SCORE_THRESHOLD阈值越高越严格。设为2.0时,日志中也打出了警告信息:No relevant docs were retrieved using the relevance score threshold 2.0,实际返回的知识库信息都是bm25的检索结果。

这里有两套 score 语义在同一个配置项上打架:检索时 distance 越小越相似;过滤时用的是被 1 - distance/√2 转换过的相关度,越大越相似。配置文档按前者写的,代码按后者跑的。于是用户以为自己在「调宽松」,实际上把向量通道整个关掉了,系统悄悄退化成「只有 BM25 在工作」。

我在留档里还记了一条:同一条 issue 的评论里冒出了另一个完全不同的根因——「每个 query 都对所有 chunk 做一次 jieba 预处理」的性能问题。一条 issue 里会混着多个根因,复盘时必须分开,否则会把一个根因的修复当成另一个的答案。(另见案例五)

5. 修复方案与代价:

方案新代价 / 新风险
把配置文案改对,阈值按「越大越严格」调治标;下次换向量库,score 语义又变了
在代码里显式声明 distance strategy,不用默认值要在建库时就定死,后改需重建索引
干脆不用阈值过滤,改用 top-k 截断放弃「一条都不相关时返回空」的能力

第三种方案的依据是我的实测:第 1 篇测过,重排分数跨 query 不可比(同为正确答案,logit 从 1.12 到 8.03,差 7.2 倍),双塔余弦也不适合卡固定阈值——用固定阈值过滤这件事本身就很脆。

6. 边界:如果只用单一检索通道、且指标语义从头到尾一致(比如自研系统里就用一种距离),那阈值过滤是能用的。这个案例的教训不是「阈值不能用」,而是**「同一个词(score)在不同层里指不同的东西」**——这在多组件拼装出来的系统里几乎必然发生。

根因归类:指标/口径层(模板四类里没有这一类,见 §7)。

案例二|答案明明在库里,就是没进 top-k​

一手来源:LlamaIndex 官方文档《RAG Failure Mode Checklist》第 2 节 "Wrong Chunk Selection (Poor Chunking)"(逐字)

What happens: Your documents are split in a way that separates critical context across multiple chunks, so no single chunk has enough information to answer the query.

Symptoms: Partial or incomplete answers … The correct information exists in your corpus but the retrieved chunks don't contain it fully … Answers improve dramatically when you manually provide the right text

Fixes: Experiment with chunk size and overlap (for example, larger chunks such as 1024+ tokens with 10–20% overlap) … Use SentenceSplitter instead of naive fixed-size splitting … Try hierarchical chunking with HierarchicalNodeParser …

那句「手动把正确文本喂给模型,答案就对了」是整个模板里最有用的一条诊断动作:它把「检索层」和「生成层」一刀切开。如果手工喂对了,那就不是模型的问题。

1. 原始问题:不是某一个人的问题,而是这一类:答案在文档里,但召回的块里没有它。

2. 系统架构:任何用了定长切块的系统。

3. 错误现象:答案不完整 / 答不出;同一份资料人工贴给模型就对了。

4. 根因定位:检索层——召回不全(不是「排序不好」,这两者的修复完全不同)。

5. 修复方案与代价:

方案新代价 / 新风险
调块大小 / 重叠我的实测:块越大命中越低(128 token 4/8 → 1024 token 1/8),重叠能把块数推高(512 token:29 → 40 块)
按结构切(标题/样式)块大小不再可控,最大块可能远超模型上限
增大 top-k噪声一起进来;且重排救不了没召回的(第 1 篇实测:把 gold 从候选池拿掉,重排照样给你一个像样的负例)
加上下文增强(块前补「它讲什么」)一次性预处理成本(Anthropic 官方给的是每百万文档 token 约 $1.02),且要跑一遍 LLM

⚠️ 这里有一处公开建议与我实测的张力,值得单独说。 LlamaIndex 官方建议「用更大的块,比如 1024+ token、10–20% 重叠」,而我在第 2 篇的实测里,块越大 hit@1 越低。

两边不一定矛盾:官方那条建议是配套着「增大 top-k + 用重排修剪」给的(它在第 1 节就写了 "Increase similarity_top_k and let the reranker prune")——大块 + 大 top-k + 重排是一套。我的 sweep 里没有同时上重排,测的是「两块大小在其它条件相同下的对比」。

我没有做能同时覆盖两者的实验,所以这里不下结论。 但这个张力本身很有价值:同一条建议,脱离它的配套条件单拎出来用,结论可能反过来。

6. 边界:如果答案天然跨多个块(比如一份对比表的两列、一段前后呼应的条款),任何单块策略都会失败——这时要么用父子块(命中子块后把父块给模型),要么接受多块拼接。「把块切得更小」在这里是反向操作。

根因归类:检索层(召回不全)。

案例三|正确答案排在第 11 名,而 top-10 被送进了模型​

一手来源:两个独立来源指向同一现象。

Redis《RAG debugging guide》(2026-07-22 更新)第 2 节(逐字):

Sometimes retrieval finds the right chunk and then buries it below the cutoff. If the correct document ranks 11th and you pass the top 10 to the model, the answer never reaches generation. To the user this looks identical to a missing chunk, but the fix is different: the chunk is already in your candidate set, just scored too low.

论文《From BM25 to Corrective RAG: Benchmarking Retrieval Strategies for Text-and-Table Documents》(arXiv 2604.01733,摘要逐字):

We benchmark ten retrieval strategies … on a challenging financial QA benchmark of 23,088 queries over 7,318 documents with mixed text-and-table content. … (1) a two-stage pipeline combining hybrid retrieval with neural reranking achieves Recall@5 of 0.816 and MRR@3 of 0.605, outperforming all single-stage methods by a large margin; (2) BM25 outperforms state-of-the-art dense retrieval on financial documents, challenging the common assumption that semantic search universally dominates; and (3) query expansion methods (HyDE, multi-query) and adaptive retrieval provide limited benefit for precise numerical queries, while contextual retrieval yields consistent gains.

这是全篇最有信息量的一条引用:两阶段(混合召回 + 神经重排)显著赢过任何单阶段;而「BM25 胜过稠密」直接挑战了行业默认假设。它还顺带印证了我第 2 篇的观察——精确数值查询上,那些花哨的 query 改写手段收益有限。

1. 原始问题:问答系统给出错误答案,但人工看检索日志「看起来很正常」。

3. 错误现象:与案例二完全一样(答案没进上下文),但根因不同。

4. 根因定位:检索层——排序问题,不是召回问题。

5. 修复方案与代价:

方案新代价 / 新风险
加 cross-encoder 重排O(N) 代价(第 1 篇实测:50 条 39 ms / 300 条 217 ms);且在某些 query 类型上会帮倒忙(第 4 篇实测:条件否定型上稠密 5/6 → 加重排 3/6)
混合检索(向量 + BM25)要维护两套索引;融合参数(RRF 的 k)要调
元数据预过滤过滤条件写错会把正确答案先筛掉(第 2 篇实测:过滤 0.1% 时 LIMIT 10 静默返回 0 条的同类陷阱,见 pgvector 栏目)

6. 边界:先分清「没召回」还是「没排上」——Redis 那段话的关键就在这:两者对用户看起来一模一样,修复完全不同。而重排只对后者有效。

补充一个我实测里的反例,用来说明「不能照搬别人的基准」:这篇论文说 BM25 胜过稠密,而我的第 4 篇里,BM25 在精确串类问题上只有 2/6、在数值表格类上 top-1 0/4。差异的可能来源:语料不同(金融表格 vs 中文技术文档+条款)、我的 BM25 用的是简化分词(单字 + bigram,无词典)。「BM25 比稠密强」不是普适结论,是有条件的。

根因归类:检索层(排序)。

案例四|检索对了,模型还是答错​

一手来源:论文《Lost in the Middle: How Language Models Use Long Contexts》(arXiv 2307.03172,摘要逐字)

We find that performance can degrade significantly when changing the position of relevant information, indicating that current language models do not robustly make use of information in long input contexts. In particular, we observe that performance is often highest when relevant information occurs at the beginning or end of the input context, and significantly degrades when models must access relevant information in the middle of long contexts, even for explicitly long-context models.

LlamaIndex 官方 checklist 第 1 节把这一类的症状写得很具体(逐字):

Retrieval Hallucination … The retriever returns chunks that look superficially relevant but don't actually contain the answer. The LLM then "hallucinates" a plausible-sounding response from irrelevant context.

Symptoms: … High similarity scores on irrelevant passages

1. 原始问题:检索结果看着没问题,回答却是错的/漏的。

3. 错误现象:幻觉、或「证据在但被忽略」。

4. 根因定位:上下文拼接层——不是没查到,是放的位置不对。U 型曲线说明:同一条正确证据,放在上下文开头或结尾效果好,放在中间会被忽略。

5. 修复方案与代价:

方案新代价 / 新风险
把最相关的块放开头/结尾(U 型两端)你得先能排序,而排序本身可能排错(回到案例三)
减少 top-k,别硬塞少给可能漏掉答案需要的第二段证据
让模型先复述证据再回答增加输出 token 成本与延迟

Redis 指南给这一族群起了名字(逐字):context poisoning / context distraction / context confusion / context clash / context rot,并给了一句总结:

stuffing more chunks into the prompt often makes answers worse, not better.

6. 边界:短上下文(比如只塞 2~3 个块)时位置效应不明显——U 型曲线是在长上下文里测出来的。别拿长上下文的结论去优化一个只塞 3 块的小系统。

根因归类:上下文拼接层。

案例五|那份文档等于不存在(入库前的失败)​

一手来源:一篇第一人称复盘《I Rebuilt My RAG System Three Times》(novakit.ai 博客,2026 年,作者自称工程师)。说明:这是作者自述经历的原始出处,无法独立验证,所以我只把它当「一类现象的实例」,不当数据引用。

作者的 Demo 上线一周就崩了,他列出的失败现象(逐字):

Stale answers. The chatbot was confidently citing a 2022 architecture doc that had been replaced in 2024. The old doc was still in the corpus. Mangled tables. Anything tabular came out garbled. SLA tables, on-call rotations, pricing matrices — all unreadable. Dead OCR. A bunch of our policy docs were scanned PDFs. PyPDF2 returned empty strings. The chatbot didn't know those documents existed.

The demo had worked because the six demo questions had been against six clean, recent, prose-heavy documents. The real corpus was a swamp.

他后来的一句总结,我认为是这类案例里最值钱的一句(逐字):

The corpus is the product. Before you write any pipeline code, audit the corpus. … I had spent two weeks on the pipeline and zero on the data.

「PyPDF2 returned empty strings. The chatbot didn't know those documents existed.」——注意这句话的因果:不是检索得不好,是那批文档从来没进过索引。这类失败在日志里表现为「召回结果不相关」,在用户侧表现为「答不出来」,而根因在整条链路最开头。

我自己的实测给了这一类现象的可复现证据(第 2 篇):

  • 真实 PDF 抽出 30 页文本,457 个 \x01 控制符;
  • 261 个「康熙部首」形近字(⽹ U+2F79 而不是 网 U+7F51);
  • 后果:关键词检索「网络」在这份文档里命中 0 次;分词器把 ⽹络配置 变成 ['[UNK]', '络', '配', '置']。

4. 根因定位:数据获取/入库层。这一层不属于模板四类中的任何一类——它不是检索问题(数据还没进库)、不是拼接问题、不是模型问题、也不是路由问题。

5. 修复方案与代价:

方案新代价 / 新风险
入库前做质量闸门(抽出来的字符数/语言分布/控制符比例,异常就拦下)要写规则、要有人处理被拦下的文件
布局感知的解析(Unstructured、OCR 兜底)依赖变重、解析变慢、成本上升
Unicode 归一化(康熙部首 → 汉字)+ 去控制符归一化可能误伤(有些字符本身合法)
混合检索兜底只是兜底,脏数据本身没修好

6. 边界:清洗规则是针对具体来源的——同一套规则换一批文档就未必适用,而且过度清洗会删掉真内容。这一层没有通用解,只有「每次换数据源就重新验一遍」。

案例六|索引落后于数据、重复块挤占结果​

一手来源:Redis《RAG debugging guide》第 4 节(逐字)

When it isn't, retrieval quality erodes silently long before anyone notices: recall drops over time as a once-top-ranked document drifts down the results and gets crowded out. Batch rebuilds make this worse. … Time-sensitive queries suffer most, because vector embedding models tend to place labels like "Q3 2024" and "Q3 2025" close together in embedding space unless the retrieval layer applies date-aware metadata or lexical constraints.

Duplicates are the flip side of the same freshness problem. Re-ingesting a corpus without deduplication packs the index with near-identical chunks that crowd out more diverse results.

「Q3 2024 和 Q3 2025 在向量空间里挨得很近」——这一条与我的实测完全对得上:第 2 篇里,不同日期不同价格的句子相似度是 0.9064 / 0.9064 / 0.9064 / 0.9063,极差 0.0001。

4. 根因定位:数据治理/运维层(模板四类同样覆盖不到)。

5. 修复方案与代价:

方案新代价 / 新风险
增量更新代替批量重建要引入变更捕获链路,复杂度上升
按 hash 去重阈值定多少? 我的第 1 篇那 6 组 hard case 正是「句子几乎一样、条件不同」的条款——用 90% 相似度去重,会把它们当重复删掉
时间/来源元数据过滤过滤条件本身可能把正确答案筛掉(同案例三)

6. 边界:语料只在追加、几乎不修改的场景(比如法规汇编、论文库)里,这一层几乎不用管。它是「数据在变」才成为问题的问题。

顺带说:我写第 4 篇时就踩过这个坑的缩小版——语料里有一行数据和我正文里举的例子撞了句,导致 gold 标注不唯一、脚本直接报错。重复内容对评测的干扰,比对检索的干扰来得更早。

7. 这套模板要补什么​

7.1 补第 0 问:这个案例的评测口径是什么​

案例一是最典型的例子:整条 issue 的根源是「score 这个词在两层里指不同的东西」。如果不先问「他是用什么口径判断『答非所问』的」,你连现象都描述不准。

这一类问题我自己也踩过两次,都写进了对应文章里:

  • 第 1 篇:我构造的 6 组用例里,有一条正确答案和干扰项都在回答 query,只是 gold 更精确——「重排把 gold 挤到第 2」其实是我的标注歧义,不是重排的错。
  • 第 2 篇:我用「gold 片段是否落在 top-k 块里」当指标,这个指标天然偏向小块(小块更容易把答案整块包进去)。换个指标,结论可能反过来。

所以模板应该补第 0 问:这个案例里,「错」是按什么口径判定的?口径本身有偏差吗?

7.2 补第 5 类根因:数据获取/入库层​

原模板的四类(检索 / 拼接 / LLM / 路由)有一个共同点:它们都假设数据已经正确进了库。 案例五(空字符串的扫描件、康熙部首)和案例六(索引落后、重复块)都发生在这个假设之前。

为什么这一类值得单独列:

  • 它最隐蔽:不报错、不崩,甚至在抽样检查时人眼也看不出来(形近字的字形几乎一样);
  • 它最贵:索引建好之后再发现,得重切重嵌,索引全部作废;
  • 它最容易被误诊:表面上和「检索效果差」完全一样。

判别动作:拿一批「本该能答出来」的问题,人工确认答案确实在库里、且是干净文本。如果人工看原文都找不到那段话,问题就不在检索层。

7.3 第 6 问怎么问才有用​

「在什么场景下会失效」——如果答「数据量大了就不行」「复杂场景不适用」,等于没答。有效率的问法是把它变成一个可检验的条件:

无效问法有效问法
这套修复什么时候会失效?在多大规模的语料上失效?(第 2 篇实测:同类问题在 400 行表格上 hit@1 = 0/4,100 行时 = 2/4)
换语言会不会失效?(中文的 BM25 必须换分词方案)
换模型版本会不会失效?(第 4 篇:边界会随模型和语料规模滑动)
query 类型分布变了会不会失效?(第 4 篇:同一种检索在四类问题上的命中率差 5 倍)

8. 一页纸速查​

案例现象根因层首选修复主要代价边界
一 · 阈值设反答非所问指标/口径改口径、改用 top-k放弃「无结果」能力单通道 + 口径统一时可用
二 · 没进 top-k答案不完整检索层(召回)改切法 / 上下文增强块大小此消彼长;重排救不了答案天然跨块时单块策略必败
三 · 排在第 11和案例二一模一样检索层(排序)混合检索 + 重排O(N) 代价;某些类会变差先分清「没召回」还是「没排上」
四 · 证据被忽略幻觉 / 漏读上下文拼接重要证据放两端、少塞依赖排序结果短上下文里位置效应不明显
五 · 文档等于不存在答不出来数据获取/入库入库质量闸门 + 布局感知解析解析变重;清洗可能误伤每换一个数据源都要重验
六 · 索引落后 / 重复慢慢变差数据治理增量更新 + 去重去重阈值会误删近似条款语料只增不改时几乎不用管

最后一句:这六个案例里,只有两个的根因在「模型/算法」那一侧(案例三的排序、案例四的拼接),其余四个都在模型之前——配置口径、切片、数据获取、数据治理。这和第 4 篇的结论是同一件事:架构的分量,主要在检索这条链的下游之外。

9. 来源与核对​

引用来源存档
阈值语义反转的 issue 正文与评论(逐字)GitHub chatchat-space/Langchain-Chatchat issue #4565official-chatchat-issue-4565.md
Wrong Chunk Selection / Retrieval Hallucination 的定义、症状、修复(逐字)LlamaIndex 官方《RAG Failure Mode Checklist》official-llamaindex-failure-checklist.txt
U 型曲线原文(逐字)arXiv 2307.03172《Lost in the Middle》摘要official-lost-in-the-middle-abstract.txt
23,088 query 基准、混合+重排 Recall@5 0.816 / MRR@3 0.605、BM25 胜过稠密(逐字)arXiv 2604.01733《From BM25 to Corrective RAG》摘要official-arxiv-2604.01733-abstract.txt
定长切块 F1 0.24 vs adaptive 0.64(逐字)PMC12649634《Comparative Evaluation of Advanced Chunking…》摘要official-pmc12649634-abstract.txt
第 11 名现象、stale/duplicate、上下文失败的五个名字(逐字)Redis《RAG debugging guide》(2026-07-22 更新)official-redis-rag-debugging.md
第一人称复盘(PyPDF2 返回空字符串等,作者自述,未独立验证)novakit.ai 博客《I Rebuilt My RAG System Three Times》official-novakit-postmortem.txt

一处转述走形的记录:Redis 指南写「naive fixed-size chunking scored an F1 of 0.21」, 我核对 PMC 论文原文,baseline 的 precision/recall/F1 是 (0.17, 0.40, **0.24**) —— 0.24,不是 0.21。 数字不大,但它正是仓库里那条老规矩的又一次实证:转述不等于原文,引用前要回查。

我自己在本文里用到的实测(第 1~3 篇):分差中位 0.11 vs 4.86、固定阈值两侧都不成立、块越大命中越低 4/8→1/8、955 token 长块静默丢尾部、PDF 抽取的 261 个形近字、日期数值相似度差 0.0001、污染数据下 BM25 掉 3 个 top-1 而稠密不动。脚本与输出在 source/rag*-lab/。