RAG 检索命中后为何仍会答错
正确文档进入上下文并不保证回答忠实,重复内容、旧版本和无关片段仍可能干扰模型。文章建议检查失败请求的完整提示词,统计有效片段比例,再通过扩大召回、重排和精简上下文提高证据质量。
完整中文译文
你查看日志,发现正确的文档已经检索到了。它就在 prompt 里,5 条结果中排第 2。可模型还是信心满满地给出了错误答案。
这是最让人抓狂的 RAG 问题,因为所有监控面板都显示“检索成功”。常见的建议,比如改进 embedding、调整分块策略,都帮不上忙,因为出问题的根本不是检索。
一个让人不太舒服的事实是:检索只是把证据放进上下文,并不能强制模型忠实地使用它。从“检索到文档”到“回答正确”,至少有五个环节可能出错。
1. “检索到了”不等于“检索结果干净”
正确的文档确实在 top-k 里,但一起进来的还有大量噪声:近乎重复的文档、旧版本,以及主题相关却无法回答问题的段落。
模型不擅长忽略干扰信息。如果正确的 chunk 写着退款期限是 30 天,而过时的 chunk 写着 14 天,模型可能把两者混在一起,也可能选择那个听起来更自然的说法。
检查方法:针对回答出错的查询,打印完整的 prompt。数一数,到底有多少个 chunk 真正有助于回答问题。如果 8 个里面只有 1 个有用,那你遇到的是检索精确率问题,而不是幻觉问题。
解决方法:先扩大检索范围,再做 rerank,筛选出更少的 chunk。数量少但质量高的 chunk,胜过数量多却噪声大的 chunk。
2. 位置很重要:中间的信息容易被忽略
面对长 prompt,LLM 对不同位置的关注程度并不相同。上下文塞得满满当当时,埋在中间的证据,比放在开头或结尾的证据更容易被忽略。
检查方法:把正确的 chunk 移到第一位,重新运行那个回答出错的查询。如果答案变正确了,你就找到了问题所在。
解决方法:把排名最靠前的 chunk 放在上下文的开头或结尾,同时尽量精简上下文总量。
3. chunk 是对的,但不完整
你检索到了包含答案的段落,但分块时,例外情况、表头、脚注,或者带有“除非”条件的从句被切到了下一个 chunk。
模型只看到半条规则,就欣然用自己的先验知识补全了剩下的部分。
Chunk A: "Customers can return items within 30 days..."
Chunk B: "...except for final-sale items and gift cards." <- never retrieved
解决方法:让相邻 chunk 保留重叠内容,采用结构感知分块,也就是按标题而不是 token 数量切分;还可以使用父文档检索:用较小的 chunk 做匹配,但传给模型的是它所在的整个章节。
4. 上下文与记忆冲突:模型更相信自己
当检索到的文本与模型“已经知道”的内容不一致时,模型就得选边站。有时,它会忽略你提供的上下文,转而根据训练数据作答。这种情况最常见于那些广为人知的话题,尤其是你的内部文档与公开知识不一致时,比如你产品的 API 与“标准”API 存在差异。
反过来也会发生:一段错误的检索内容,也可能说服模型放弃原本正确的认知。
解决方法:在 system prompt 里明确说明。要求模型只能根据提供的上下文回答问题,并且即使上下文与通用知识矛盾,也要以上下文为准。
5. 综合推理出错:事实正确,结论错误
有时,上下文中的每一条事实都正确,答案却依然是错的。模型可能算错了数、错误地组合了两个条件、误读了日期范围,或者过度概括,比如把“部分套餐包含 X”变成“所有套餐都包含 X”。
再怎么改进检索,也解决不了这种问题。这是推理能力和忠实度出了问题。
解决方法:对于需要大量综合推理的查询,使用能力更强的模型。要求模型先逐步提取信息,再给出最终答案。把工作拆成两步:先“提取相关事实”,再“根据这些事实回答”。
一个最简的基于证据作答的 prompt
SYSTEM = """You answer using ONLY the numbered sources below.
Rules:
1. Every claim must cite a source like [2].
2. If the sources don't contain the answer, say "I don't have enough information."
3. If sources conflict, say so and prefer the most recent one.
4. Never use outside knowledge."""
def build_prompt(question, chunks):
sources = "\n\n".join(
f"[{i+1}] ({c.title}, updated {c.updated_at})\n{c.text}"
for i, c in enumerate(chunks)
)
return f"{SYSTEM}\n\nSOURCES:\n{sources}\n\nQUESTION: {question}"
这段 prompt 能发挥作用,靠的是两点。强制引用来源,让模型更难编造没有依据的说法;明确允许拒答,则给了模型一条安全的退路。如果没有这条退路,即使上下文提供的信息不足,模型也会被推着给出答案。
当 RAG 回答错误,但正确的文档已经检索到时,别靠猜。按下面的清单逐项排查:
- 正确的 chunk 内容真的完整吗?(分块)
- 它是否淹没在无关或相互冲突的 chunk 中?(精确率、版本)
- 把它移到最前面,答案会变正确吗?(位置)
- 答案是否与模型的先验知识冲突?(记忆冲突)
- 是否事实都对,但结论错了?(推理)
衡量忠实度,而不只是检索效果
大多数团队只跟踪检索指标,比如 precision@k、recall、MRR,然后就到此为止。这些指标只能告诉你证据是否送到了,无法告诉你模型有没有正确使用它。还应该增加:
- 忠实度:答案中的每一项说法,是否都有检索上下文作为依据?
- 答案正确性:用一组经过标注的真实问题,评估答案是否正确。
- 上下文精确率:检索到的内容中,到底有多少真正有用?
RAGAS、DeepEval 这类工具可以为这些指标打分。每次修改分块策略、prompt 或模型时,都运行一次评估,就像运行单元测试一样。
RAG 并没有消除幻觉,而是改变了幻觉发生的位置。模型可能败在噪声过多的上下文、被埋没的证据、被拆开的 chunk、与自身记忆的冲突,或者单纯的推理错误上。
无论你构建或采用哪种 RAG 方案,都应该根据忠实度和答案正确性来评判,而不只是看有没有检索到正确文档。按照上面的顺序修复这些故障模式,大多数“文档明明就在那儿!”的问题都会消失。
如果还想采取进一步措施,可以考虑屏蔽此人和/或举报滥用行为。