我看到现在很多团队调了很久RAG,最经常遇到的问题就是:向量库里明明捞出了一堆相似片段,甚至Top 1 结果看起来就是跟问题高度相关,最后生成出来的答案却是错的,有些还错得挺离谱。这个现象非常典型,它是 RAG 系统链路中多个环节共同作用的结果,两台字幕一叠加,问题就暴露了。今天我就专门把这个坑拆开来讲,每一步对应什么潜在问题、怎么排查、怎么解决,希望帮你少走不少弯路。
可以先打个比方:你让一个助理去书架上找“红烧肉怎么做”,他给你抱来一本讲五花肉挑选的书。相关吗?相关。但你的问题是“步骤”,他给你的是“食材标准”。RAG 的朴素逻辑本质上就是这样——它检索的是语义相关,不直接保证信息完备。所谓“检索到了相似内容”,有时候一点都不代表“检索到了能回答问题的内容”。很多团队在这里误判了方向,一直在调检索,甚至去换向量模型,结果发现水面之下还压着一整套问题。
1. 先看清 RAG 的完整链路:别把“检索结果”当成“答案正确的前提”
1.1 链路拆解:每一个环节都会埋雷
一个标准的 RAG 流程,大致是:文档解析 → 分块(chunking)→ 向量化(embedding)→ 向量召回(retrieval)→ 重排(rerank)→ 上下文组装(prompt building)→ 大模型生成(generation)。
很多人理解的 RAG 就是“召回 + 生成”两步,但实际上从文档解析开始,每个环节都会往结果里注入误差。检索相关度高只是其中一个中间指标,它不承担“最终答案正确”的全部责任。你得把整条链路当成一条河流来看,任何一个环节污染了,下游再努力也白费。
我在实际调试中见过很多案例,检索端指标刷得很漂亮,Hit Rate 90% 以上,但端到端人工评测一看,错误率还是高。原因就藏在两个地方:检索到的“相似”片段是否真的包含答案,以及大模型拿到相似片段后是否真的会按片段回答。这两件事都不过关,那表面上游刃有余的 RAG 只是在自嗨。
1.2 “相似”与“可用”的差异:向量空间近不等于信息完备
向量检索的核心是计算语义向量之间的相似度。生活化一点理解:你在高维空间里找“邻居”,找到的几个邻居在谈论同一个话题,但谈论的侧面、详略、颗粒度可能完全不同。比如你问“如何重置密码”,检索到的内容讲“密码策略的复杂度要求”——两个片段都围绕“密码”,但一个讲操作流程,一个讲安全规范。向量距离近,是因为话题相近,不是因为没有答。
这里要认真区分两个概念:主题相似(topical similarity)和答案完备性(answer sufficiency)。前者是检索系统的事儿,后者是资料组织方式的事儿。判断一份检索结果能不能用,标准不是“相不相似”,而是“把这段内容交给一个不知道答案的人,他能否直接从里面解锁答案”。这个标准很多团队压根没建立过,所以问题一出现就变成玄学。
1.3 一个很常见的误导性“相似”:关键词重叠但信息层级错位
还有一种很隐蔽的情况:用户的问题里包含某些关键词,这些关键词刚好在某一页文档里高频出现,于是被召回。但文档讲的是这个概念的定义、背景、发展历史,不是用户要的那个即时操作说明。比如问“如何排查服务启动失败”,召回的内容全是“服务启动失败的常见原因分类”——分类看懂了,但还是不知道怎么查具体日志。这时候用户的感受就是:系统确实明白我在问什么,但它就是没有正面回答。
这个错位不是“相似度算错了”,而是文档的组织结构没有和用户的信息需求对应起来。处理思路也不是单纯换 embedding 能解决的,而是要回到文档处理层,把面向“问题”的内容密度做出来。
2. 分块策略:检索到了“块”,但答案不在块里
2.1 切分粒度两难:块太小没上下文,块太大全是噪声
分块策略是 RAG 里最容易被低估的环节。块太小,比如 128 token 以下,语义可能过于局部,一个完整的事件或步骤被打散到两三个块里。结果就是:你要检索的问题是“A 和 B 的关系”,但文档里相关的描述横跨了两块,每一块单独检索都没法完整覆盖答案。
块太大呢,比如 1024 token 以上,一个块里可能容纳了三四个子话题,向量化出来的表征会被“平均掉”。检索时看着相关度还行,但真正对答案有用的句子被淹没在大量周边信息里。大模型在长上下文里“大海捞针”的准确率远没有想象中那么高——上下文越长,注意力稀释越严重。
从我自己的实践经验来看,分块没有万能最优值,但起步可以用这几个经验参数:通用文本 512 token、有明确结构的文档 384 token、代码类 256 token 左右。关键是不要闭眼硬切,要做基于结构的切分。
2.2 结构切分与“父子块”策略:让答案稳准狠地落在一个块内
更好的做法是“结构感知切分”,先按 Markdown 标题、HTML 标签、PDF 章节等文本骨架把文档切成大段,再在大段内部按长度切小块,同时保留大段和小块的关联关系。这样能做两件事:
- 检索可以基于小块(精准度高,语义聚焦)
- 但送入模型时,把小块所属的整段父级内容一起带上(上下文完整)
这就是经典的parent-child chunking(父子分块)策略。我用下来之后,发现它对那种“答案在一段话里,但精准句子分散”的文档特别管用。比如一份操作手册,用户问的是“重启后配置是否保留”,如果只检索到一句“配置会保留在持久化目录”,模型可能不敢确定,但如果你把所在的整节“配置持久化机制”全喂进去,模型就有足够的上下文给出肯定答案。
另外,重叠窗口(overlap)也得设,建议重叠 10%~20% 的 token 量,避免谓语动词或者关键术语正好卡在切分边界上。这种“边界截断”造成的检索失败特别不显眼,因为大部分时候只有个别 case 会踩中,但它会让你找不到规律。
2.3 表格、列表、代码块的反切分处理
像表格、列表、代码块这类强结构内容,用普通文本切分方式极其容易损坏。Markdown 表格如果按行切开,向量化后表格语义基本就废了;代码块切开后语法都断了,检索到也看不懂。
我的个人做法是:识别出这类块,整体作为一个不可分割的 chunk 单元;如果整体太长,就用语义完整的位置作为切分点(比如表格按行组分,代码按函数切),而不是粗暴按字符数切。另外,对表格这类内容,还可以做一步“表格转文本”的旁路副本,让向量化模块更容易获取表格内的语义。
3. 上下文组装:模型“看到了”正确内容,却把它当成干扰信息
3.1 指令边界不清:参考材料被模型当成了“闲聊素材”
这是 RAG 调试里一个极其隐蔽、但极其关键的点。有时候检索没问题,分块也没问题,错误出在prompt 怎么把检索结果和问题组织在一起。如果你只是简单拼接“下面是一些文档片段,请回答用户问题”,模型确实会参考,但它不一定会严格依赖这些片段。它可能会混合自己的内部知识,甚至把检索片段里不相关的边角料也纳入推理。
我习惯在 prompt 里非常明确地声明三层结构:
- “以下是可能包含答案的参考文档片段”
- “请仅基于这些片段回答,如果片段中没有足够信息,请直接说‘资料中未找到相关信息’”
- “用户问题:……”
你别觉得这太啰嗦,实测下来,这种指令边界的明确性能显著提升忠实性(faithfulness)。模型不是不会读资料,而是它默认拥有“自由发挥”的权限。你把权限收紧了,它才会乖乖按资料作答。
3.2 上下文噪声和“位置偏差”:正确答案被相似内容淹没
另一个组装层面的坑是:你把 Top 5 甚至 Top 10 的结果全部塞进去,但这些结果里可能只有两个是有用的,其他都是“差不多的干扰项”。大模型面对这种场景时,容易被多数噪声带偏,尤其是正确答案不在上下文开头或结尾时,中间的片段容易被弱化。位置偏差(lost in the middle)不是传闻,是确实存在的行为特征。
解决办法有这么几个方向:
- 顶部放最相关的 1~2 个片段,把握“多少人看开头”的注意力红利
- 片段之间用明确的 XML 标签或清晰分隔符隔开,甚至加上编号
- 在 prompt 里写明:“上下文中有多个片段,请优先参考编号靠前的片段”
- 最硬核的方案:上下文压缩,把每个片段先用 LLM 抽取出与问题最相关的部分,再拼装发给最终生成模型
3.3 上下文窗口超限导致的“静默截断”
还有一种非常现实的情况:你的检索结果拼起来太长了,而模型的上下文窗口有限,系统会在组装时做截断。截断默认是从尾部开始砍掉,也就是说,最晚拼接进来的片段可能直接被丢掉了。如果你把检索结果按相关度由高到低排列,那么被砍掉的通常是最不相关的,这还好;但如果你把问题放在最后面,有些实现会从中间抽掉内容,或者截断发生在更早的位置,那就没法保证正确答案一定还在上下文里。
我实际踩过一次:拼接了 6 个片段加一个很大的 system prompt,总 token 数超了窗口,结果其中恰好包含正确答案的那个片段被截断,模型给出的答案就变成了胡扯。排查方式也简单,把最终发给模型的 prompt 完整打出来,自己看一眼,到底哪些内容真的被送进去了。很多“AI 答错”的 case,其实问题出在“AI 根本没见过正确答案”。
4. 重排环节:检索 TopK 不等于“值得给模型看的 TopK”
4.1 召回是广撒网,重排才是精挑细选
向量召回的核心是用 embedding 算相似度,它本质上是个“粗筛”。因为 embedding 压缩了大量语义信息,它擅长判断“领域相近”,但不擅长细粒度判断“这个片段是否真的直接支撑了问题答案”。
重排(rerank)就填补了这个空白。它通常在召回之后,把 Top 20~50 的候选片段交给一个更精细的模型做二轮打分,选出最值得进入上下文的 Top 3~5。这套组合拳非常值得加进系统里,因为它能直接过滤掉“主题相似但不可用”的片段。要是跳过这步,那你的大模型就不得不忍受很多低质量上下文,答案出错概率自然上升。
4.2 常见误区:用同一个 Embedding 模型做重排等于白做
有些团队嫌麻烦,会用召回阶段同一个向量模型再算一次相似度,然后把分数排个序,就认为完成了重排。这个做法基本无效——同一个模型学到的表征是一致的,它自己摸不准的细微差别,你让它再打一次分也不可能翻案。
真正有效的重排模型,是专门训练的 cross-encoder(交叉编码器),它会把“问题 + 候选段落”拼在一起做深度交互,判断粒度远比双塔式 embedding 细得多。这类模型在小参数量下也能提供可感知的效果提升,值得在系统中当成独立组件引入。
4.3 轻量级但有效的重排落地思路
如果你暂时没法上很重的外部模型,也可以做一个“规则 + 小模型”的混合重排:
- 规则层:过滤掉与问题实体重叠过少的候选;给包含问题关键词精确匹配的片段加分;给与问题类型匹配的片段加分(问步骤加分给 How-to 类结构、问原因加分给带“因为/所以”的段落)
- 模型层:接入一个小型的交叉编码器模型做打分排序
- 阈值层:低于设定相关度阈值的片段直接丢弃,别硬塞给生成模型
这一层加完之后,端到端准确率通常会有明显提升,尤其是“看着相关但没用”的片段被挡在上下文之外之后,模型答非所问的概率会下降一大截。
5. 不要忽略 LLM 本身的推理与忠实性问题
5.1 上下文有答案,但模型选择“相信自己的记忆”
你能想象一个场景吗?检索命中精准,上下文里也确实有答案,但模型给出的却是另一个说法。为什么?因为大模型的自身参数里也存有相关知识。当上下文和它的内部知识冲突时,模型不一定会选择上下文,尤其当上下文表述不够明确、不够“权威感”时,它可能倾向于依赖自己更“自信”的记忆。
这与模型的忠实性(faithfulness)水平有关。解决思路不只是改 prompt,还可以:
- 上下文片段中把核心答案的表述写得直白一些,降低模型二次解读的负担
- 在 prompt 中用“以下资料是唯一可信来源,禁止使用预训练记忆”这类强约束
- 更严格一点:生成前增加一个可控的“按资料改写”步骤,让模型先抽取要点,再组织语言,减少自由发挥空间
5.2 多跳问题与隐性推理缺口
另一种答错场景非常有意思:检索到的每个片段单独看都对,但问题需要融合两段信息才能解。比如用户问“项目 A 的负责人是否也在项目 B 中担任职务?”第一段资料里有项目 A 的负责人名单,第二段资料里有项目 B 的人员名单,但两段内容没有明确说“同一人”。模型可能需要做一次跨越片段的实体消解与推理,此时如果上下文太长,它容易“忘了”关联,直接答出一个错误结论。
这类多跳问题对 RAG 来说是硬骨头。实操手段有两个:一是拆解问题为子问题,做多轮检索再合并答案;二是把多段相关上下文以结构化小结的形式压缩成一个更精炼的证据链,再让模型做最终推理。想要一次拼接就得到好结果,非常依赖模型自身推理能力,本质上不是单靠 RAG 就能解决的。
5.3 学会“拒绝回答”比硬答更值钱
在很多业务场景里,资料库本来就没有答案,但模型还是会“编一个合理但错误的回答”。这其实是最伤系统信誉的。我在调系统时总是建议团队:把“无法回答”当成一个合法且重要的输出。给模型明确的权限和话语模式,比如“如果资料中无直接信息,请回答‘根据现有资料无法确认’”,同时不要诱导模型强行给结论。
很多错误答案并不是系统不够智能,而是它太好强了。承认自己没找到答案,往往比硬答更能保住系统整体的可信度。
6. 系统性排查方法:五分钟定位“检索对而答案错”的根因
6.1 四步排查法,快速定位错因
如果你也遇到“检索相似但答案错误”,建议别瞎猜,按下面顺序做排查:
- 拆出检索结果单独看:把检索返回的 Top 5 片段直接打出来,人工判断其中是否真的有能直接回答问题的内容。如果没有——问题出在检索/分块层,别怪大模型。
- 丢开检索,直接做人工构造测试:把已知正确答案对应的原文片段,手动放进 prompt 里,不经过检索,直接让模型回答。如果这个时候模型答对了,说明生成层没问题,锅在检索或重排;如果答错了,锅在组装或模型面对上下文的处理方式。
- 把最终完整 prompt 打出来看:确认正确答案是否真的进了上下文,有没有被截断、被放在奇怪的位置、被噪声淹没。
- 逐步消融实验:只留正确答案片段、加一个干扰片段、加全部候选片段,一步步观察模型回答如何变化,定位到底是“噪声干扰”还是“指令不明确”。
6.2 错误类型归类:一次标注胜过一次猜测
我建议团队内部做一版简单的错误分类标注,每次人工看 bad case 都归一下类:检索缺失、上下文截断、组装指令糊涂、模型推理不足、资料本身陈旧。做完一百条 bad case 之后,你会看到明显的分布,而不是一直停留在“模型好笨”这种模糊结论上。
我见过一个客服问答场景,团队天天在换 embedding 模型,效果一直反复。后来我们做了 bad case 归类,发现 60% 的错误其实是答案分布在多个段落,而分块直接把关联信息切散了。后来把分块策略换成父子分块 + 增大重叠窗口,端到端指标直接提升了十多个百分点。这就是归类分析的价值。
6.3 建立端到端评测集:别只盯检索指标
很多团队会跟踪召回率(Recall@K)和平均排名倒数(MRR),但这都是中间指标。真正要想让系统变好,必须建一个端到端评测集:每个样本包含一个用户问题、一份标准答案、一个评测标准(比如用规则核对关键词,或靠人工打分,或用更强的模型当裁判)。
有了端到端评测集,你才能知道“某个改动到底是帮倒忙还是真提升”。比如重排模型加了之后召回率可能变化不大,但端到端准确率上涨,那就是有效改动。别让中间指标绑架你的判断。
7. 常见问题速查表:症状、原因、对策一表清
| 症状 | 可能环节 | 核心原因 | 解决建议 |
|---|---|---|---|
| 检索出来的内容相关,但没有直接答案 | 分块 / 文档组织 | 答案被切散或落在不同段落 | 使用父子分块、结构感知切分 |
| 检索结果瞬间,但模型答非所问 | 上下文组装 | 指令边界不清、噪声片段过多 | 明确 prompt 指令、限制片段数量、加 separators |
| 正确答案在上下文中,但刚好被截断 | 上下文长度管理 | 超窗口后静默截断 | 打印最终 prompt 检查,压缩或选用更大窗口模型 |
| 相关片段排在后面,Top 片段全是水货 | 重排缺失 | 没有 rerank 或用了同模型打分 | 引入交叉编码器模型,设置相关性阈值 |
| 上下文正确,但输出与上下文冲突 | 生成模型忠实性 | 模型过于依赖内部知识 | 强约束“只允许基于资料回答”,必要时加抽取步骤 |
| 多段资料都对,但合一起答错 | 推理链路 | 多跳推理能力不足 | 拆分子问题、做多轮检索、上下文压缩 |
| 资料里没有答案,模型胡编 | 生成模型拒答机制 | 缺乏拒答约束 | 显式声明“资料不足时直接说明无法确认” |
| 检索 TopK 里全是相似干扰项 | 向量检索粒度 | embedding 只擅长粗筛 | 降低直接送入模型的片段数,引入 rerank 精排 |
| 每块都有相关内容,但不够充分 | 分块粒度不足 | 块太小或上下文缺失 | 调整 chunk 大小到 512 左右,增加重叠窗口 |
| 表格/列表内容答得差 | 分块处理方式 | 强结构内容被切断 | 按结构整体切块,表格转文本旁路处理 |
这表基本覆盖了常见的“检索对但答案错”场景。你可以拿它对着自己的 bad case 分类,能省掉很多自行摸索的时间。
8. 个人实操经验:一个让我彻底改变认知的案例
最后分享一个印象很深的项目经历。某知识问答系统当时的问题是:从向量库里检索到的结果“看起来不错”,但人工评估答案正确率一直只有 60% 多,团队一度认为是大模型不够聪明,准备换更大的底座模型。
我们做了全链路的逐层排查之后,发现真相是复合型的:一部分答案分布在两个相邻段落里,被切分切断了;另一部分是因为上下文拼接时超出窗口,正确答案被静默截掉;还有一部分是 prompt 里没有明确“信息不足就直说”的指令,模型在资料不充分时强行脑补。
当时我们把分块策略改成结构感知 + 父子分块,又把 prompt 指令重写了一遍,加入“仅基于资料回答、无法确认时明说”的约束,再把 Top 结果从 8 个砍到 4 个,同时做了个简单重排过滤。整套组合拳下来,同样的大模型没换,答案正确率从 60% 出头拉到了 85% 左右。这个项目之后,我再也不说“这个模型不行”这种话了——大部分时候,不是模型不行,是 RAG 的链路没拉通。
如果你也在调 RAG,我的建议是:先别执着于换模型、换向量库,把分块策略、上下文组装、重排、忠实性约束这四件事逐一做扎实,再回头看那些“奇怪”的错误,你会发现原来答案就藏在自己眼皮底下。