ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG系统答错别急着调模型:从检索到生成的排查链路全解析

RAG系统答错别急着调模型:从检索到生成的排查链路全解析

我调试 RAG 系统时,常被同一种问题卡住:检索结果明明相似度很高,答案却是错的。这个问题几乎每个做过 RAG 项目的人都会遇到,也让不少人开始怀疑“向量检索是不是不行”。其实大多数情况下,问题根本不只出在检索这一环,而是切块、排序、生成,甚至评估方式出了问题。这篇文章我会把几个项目里踩过的坑、排查过的链路、最终验证可行的做法梳理一遍,希望能帮你少走弯路。

1. 先搞明白“检索到了相似内容”里的相似,到底是什么相似

1.1 词汇重合 ≠ 语义等价

我在做某电商客服知识库项目时,遇到过一个非常典型的例子:用户问“七天无理由退货的时间怎么计算”,系统检索出一段高相似度文本——“消费者有权在收货后七天内提出无理由退货申请”。从关键词看,“七天”“无理由退货”全都对得上,向量相似度也极高。但用户真正要问的是“时间怎么计算”,而检索到的段落只说了“有权利退货”,根本没提计算起点和截止规则。

这就是第一种隐秘的错误根源:检索命中的是“词汇层面的相似”,而不是“信息层面的完整匹配”。很多基于 embedding 的检索器,对高频共现词非常敏感,一段文字只要跟问题共享了足够多的关键词,得分就会很高。但高得分只代表“你和我聊的是同一件事”,不代表“你想要的信息就在我这里”。

做 RAG 的人最容易产生一种错觉:检索得分高,答案就应该对。这个错觉的直接后果是,你把后续所有问题都归结为“模型没答好”,然后一味调提示词,结果改善微乎其微。实际上,证据就已经错了。

1.2 检索得分高不代表“证据能回答问题”

要理解这个问题,需要先明确两个概念:相关性(Relevance)和可回答性(Answerability)。

相关性衡量的是“这段文档和问句讨论的主题是否一致”,比如用户问“手机保修政策”,文档在讲“保修期是十二个月”,两者主题一致,相关性很高。可回答性衡量的是“单靠这段文档,能否完整回答这个问句”,显然“保修期是十二个月”并不能回答“换机流程是什么”。

向量检索天然优化的是“相关性”,而 RAG 真正需要的是“可回答性”。中间这道鸿沟,单靠检索器本身填不平。很多时候你需要用问题去匹配段落,而不是用段落去匹配问题。

我后来在做检索验证时,一定会加一道“答案溯源检查”:把检索到的段落单独丢给一个不带任何知识库的模型,问它“仅凭以上材料,能否回答:XXXX?如果不行,缺什么信息?”这一步能过滤掉大量“看着相关但根本答不了题”的检索结果。

1.3 从一句话判断检索结果是不是“假阳性”

入门以后,我养成了一个“假阳性自检习惯”:拿到检索片段,先不看相似度分数,快速问自己三个问题:

  • 这段内容里有没有直接对应问题关键限定词的成分?比如问题里出现“2021年之前”,片段里有没有对应时间范围。
  • 这段内容能不能独立回答完整问题,还是需要用多个段落拼图?
  • 如果我把这段删掉只看下一段,回答效果会不会反而更好?

前两个问题很好理解,第三个问题有点反直觉,但非常实用。我曾在日志查询类项目里发现,有时检索第一名是“高亮片段 + 核心结论”,第二名反而是“完整推导过程”。单独送第一名进去,模型能答;两个都送进去,模型反而被第一名的简短结论带偏,答得更差。

提示:不要只看“检索分数最高的是不是正确答案”,要看“把检索结果送进生成器后,能不能产出正确答案”。最终让读者满意的锚点应该是生成结果,而不是检索得分。


2. 切块是检索链路里最容易被低估的“第一块多米诺骨牌”

2.1 固定长度切块,为什么会拆碎答案结构

我最早做 RAG 原型时,用最偷懒的方式切块:按 500 个字符随便切,切完直接建索引。结果线上准确率惨不忍睹。

举个我后来复盘时发现的例子:某保险条款文档里有一段二百五十字的“免责情形说明”,开头是“在以下情形下,保险人不承担赔偿责任”,中间列举了五种情况,最后一句是“以上情形不包括被保险人本人故意行为”。固定长度切块时,第一部分被切到了块 A,最后那句限定条件被甩到了块 B 的末尾。

用户问“哪些情况不属于免责范围”,块 A 检索得分极高,因为里面全是“免责情形”“不承担赔偿责任”,正好命中关键词。块 B 虽然包含关键否定信息,但因为开头是“经营性用车发生事故”,跟用户问题的关键词重合度低,排名非常靠后。

于是模型拿到的证据只有块 A,自然给出“这些情形都属于免责”的错误结论。其实它的输出在逻辑上完全自洽,错的不是推理,而是它手里根本没有拿到那块关键限定信息。

2.2 不解决“信息断点”,调再多的相似模型都是白搭

很多团队在切块策略上跳过了思考,直接追求“让每块富含语义”,然后花大量精力调 embedding 模型。但 embedding 模型再强,也无法重建已经被切碎的逻辑链路。它只能在你喂给它的那段碎片里找关联,碎片本身不完整,神仙模型也救不回来。

理想的切块目标是:每一块在语义上自包含,且只包含一个可以独立回答问题的信息单元。

这听上去像废话,实操起来却不简单。我的经验是把切块策略分成两类:

  • 结构化数据(表格、清单、条款):按逻辑单元切,比如一个完整条目、一节完整列表、一份条款的完整编号块。宁可块大,也不能散。
  • 长文本叙述(方案说明、技术文档):优先按标题层级切,标题能给出该块内容的主题框定;如果层级不明显,则利用段落边界 + 对关键词密度做二次合并,把相邻且主题相同的段落粘在一起。

实际项目中我验证过一个比较有效的策略:先按段落或标题切“粗块”,再用滑动窗口做“细块重切”,但索引时保留父文档 ID。检索命中“细块”后,可以直接回溯到“粗块”整段作为上下文。相当于既保留了精细检索的命中率,又避免信息断点。

2.3 不同文档类型的最佳切块策略参考

我整理过一张自己常用切块决策表,分享出来供你参考:

文档类型推荐切块方式注意事项
表格类数据按行切,保留列名信息不要只切单元格,要带上表头,否则“字段含义”丢失
代码库按函数/类切,保留注释跨函数引用问题最难处理,可用“文件级+函数级”双层索引
条款合同按条款编号切严格保留条款之间的引用关系,否则“例外”与“主体”会脱节
长方案文档按标题+段落边界切记录父文档 ID 和上下文索引,以便回溯扩展窗口
FAQ/客服知识按“问题+答案”整体切不要只切答案,否则丢失问句侧的信息引导

我见过不少团队在处理表格类文档时,直接把表格转成 markdown 再切字符,结果“产品名称”和“价格”被切到不同块里,检索命中“产品名称”那块,模型压根不知道价格是多少。这类问题跟模型能力无关,就是切块方式埋的雷。

2.4 检索命中后要不要“加料”?上下文窗口扩展的取舍

切块问题还有一个隐藏分支:即使检索命中了一个语义完整的段落,它前后的内容可能才是回答问题的关键。

比如用户问“该设备的续航为什么衰减这么快”,检索到的块可能只描述了“电池衰减率曲线”,但真正解释原因的是紧随其后的“温度影响”段落。这种情况下,我建议做上下文窗口扩展(context expansion):命中某个块之后,把它的前一两个兄弟块也一起送进生成器,让模型有更完整的推导空间。

但这个策略不是没有代价。窗口扩展必然增加 token 消耗,也增加噪声干扰。我在某问答项目中尝试统一“左右各扩一块”,结果答案正确率提升了约 12 个百分点,但响应延迟增加了近 30%,成本也上升不少。后来改成“只在检索分数超过阈值时才扩展”,整体性价比好了很多。有些场景甚至需要做更多轮扩展,那就需要引入“递归检索”,先定位小节、再回溯到父级文档,这一步留到后面讲多跳问题时再展开。


3. Top-K 的取值悖论:召回越全,噪声越响;召回越少,答案越空

3.1 搜索到一个“看似正确”但残缺的答案,比没搜到更危险

Top-K 是几乎所有人都会设置的参数,但它对错误答案的影响经常被低估。

K 值太小,关键信息可能根本没进上下文。比如一个多段文档里,用户问“第一方法的优缺点”,而“优点”在第一段,“缺点”在第三段,如果你只取 Top-3,而第二段是“另一种方案的说明”,那“缺点”信息大概率被淘汰出局。

K 值太大,又会出现另一种问题:检索结果里塞满了高分但无关紧要的内容,模型需要在噪声中找信号。模型本身没这个能力时,它会“被噪声带偏”,尤其当噪声段落里包含看似权威或具体的数字时。

我在一次日志分析项目里遇到一个经典翻车:用户问“服务 X 的错误率是多少”,正确数据是 1.2%。检索结果里有一份前一天的报告写着“错误率 3.8%”,因为“错误率”三个字段高度匹配,排序直接冲到前二。模型两个段落都看,最终选择了更显眼的 3.8%。这个错误跟检索“相关性”——它确实相关——但答案完全错误。这本质上是“相关”与“正确”的错位被 Top-K 放大。

3.2 动态 Top-K:按查询复杂度决定上下文口径

实践发现,静态 Top-K 很难兼顾所有查询。常识性问题可能 1 段就够,复杂对比问题可能需要 5 段以上。我的应对方案是动态 Top-K:

  • 先用关键词和问题类型做一次分类:事实型 / 步骤型 / 对比型 / 因果型。
  • 事实型:K=3;步骤型和对比型:K=5;因果型:K=6~8。
  • 同时对每个召回的段落做一次“信息量粗筛”:如果段落占比主要是停用词、转折词和虚词,就算分数高也先剔除。

这套方案在模拟项目 X 里跑出过不错的效果:整体准确率从 68% 提升到 81%,单次查询的最大 token 消耗反而下降了,因为我剔除了大量“高噪但低价值”的段落。

3.3 混合检索:让关键词和语义互为补充

另一个显著改善 Top-K 利用率的手段,是把稀疏检索(BM25)和稠密检索(向量)结果融合。在长尾问题上,BM25 往往能抓住精确的实体名,向量检索则能理解语义近义表达。两者互补效果远好于单用任何一种。

实际操作时,我常用一个很简单的融合策略:对候选池加和排序后取综合分 Top-K。具体公式并没有多玄乎,就是把两个分数各自归一化后按权重相加。我试过 0.5:0.5、0.4:0.6 的配比,效果差异不大,但千万别忘了归一化。不归一化就直接相加,分数量级大的检索器会完全主导结果,相当于混合了个寂寞。

3.4 用元数据过滤先缩小范围,别把重担全压在排序上

在很多业务场景里,排序兜不住的问题,用“过滤”反而很好解决。

比如用户问“门店 A 的营业时间”,如果你对整个城市的门店列表做向量检索,Top-K 很可能把“门店 B 的营业时间”挤进来,因为它们的语义几乎完全一致。此时最有效的办法不是调 K、调模型,而是在检索之前就通过结构化元数据(门店 ID、城市属性、日期范围)把候选集缩小到门店 A 自己。

我建议所有 RAG 项目在建索引初期,就为每个文档块预留元数据字段,比如来源库、时间戳、区域、产品线、文档类型等。过滤条件越早介入,排序压力越小,错误答案率自然也就降下来了。


4. 重排不是摆设:很多人把它当成摆设,用错了还怪它没用

4.1 向量相似度排序的“高分迷信”困境

很多团队用向量检索之后,直接按照原始得分作为最终顺序使用。这种做法的弊端在于:向量相似度的得分分布是不均匀的,且不同查询之间的绝对分缺乏可比性。有时候第一名和第二名只差 0.01,实际上语义天差地别;有时候第一名和第二名相差 0.1,但内容高度雷同。

所以我把排序这件事拆成两个层次来理解:第一层是召回(recall),用宽条件把“可能相关的候选”都捞上来;第二层是精排(precision),对候选做更严格的相关性判断。原生的向量得分只适合做第一层,不适合独自承担第二层。

4.2 两种常用重排方案的对比

方案原理优点缺点
基于规则的组合分向量分 + BM25 分 + 元数据权重加权实现简单、可解释、速度快需要手动调权,跨场景泛化一般
交叉编码器重排把问题与每个候选段落拼成一句,交给模型判断相关性精度高,能捕捉细粒度语义成本高,候选越多越慢

交叉编码器重排(cross-encoder reranking)是我现在的主力方案。它本质上是在做“问题-段落”的深度相关性判断,而不是像向量检索那样把两者各自压缩成向量再做相似度比较。后者在这个过程中丢失了大量交互信息,前者保留得更完整。实际项目中,我用交叉编码器重排把 Top-20 候选精排为 Top-5,答案正确率提升非常明显。

4.3 使用重排的三个容易踩的坑

第一,候选集太大或者太小都没用。候选集太小(比如只有 5 个),重排器没有足够的挑选空间;候选集太大(比如 50 个),重排器本身会引入延迟,而且很多无效候选混进来,反而干扰判断。我的经验是先做召回 Set 10~20 个,再做精排 Top-4~6。

第二,重排器与检索器如果不是同一个训练范式,分数不能直接互相比较。正确的做法是重排器只负责“把候选重新排队”,而不是“给出绝对分数”。不要试图把重排分数和原始相似度分数做线性拼接,那通常会把两个分布搅在一起,结果更糟。

第三,重排虽然叫“排”,其实作用更像“筛”。我发现真正让它发挥最大价值的方法,不是按重排得分从高到低取前几个,而是设定一个“及格线”,低于及格线的候选一律不进生成器。比如交叉编码器打分的范围是 0 到 1,我通常会把 0.4 以下的内容全部丢掉。这比单纯取 Top-4 更能防止噪声段落混入。

4.4 重排之后仍然出错?检查“段落内信息缺失”而非“排序错误”

重排调通之后,如果还出错,我的排查重点会立刻转向“有没有哪条真正重要的信息,压根没进入候选集”。很多情况下,问题不是排序排错了,而是“真相不在牌桌上”。

比如用户问“备份失败后怎么恢复”,文档里关于“恢复步骤”的详细介绍在第五百页,距离“备份失败”这个关键词非常远,检索器压根没召回它。这时候你再怎么调重排器也没用,反而应该去调召回策略,比如扩大候选集、增加同义词扩展、或引入父文档检索机制,把长程依赖关系补进来。

所以请记住一句话:重排解决的是“候选人选错了”的问题,不解决“候选人里根本没有正确答案”的问题。


5. 生成端同样会翻车:上下文太长、冲突信息、模型本身的推理局限

5.1 上下文越长,模型越容易“迷失在中间地带”

有研究表明,LLM 在长上下文中对“中间部分”的注意力往往弱于开头和结尾。RAG 系统把一堆检索段落拼在一起喂给生成器时,如果核心答案恰好被埋在中间,模型很可能忽略它,转而参考开头或结尾那些相关度没那么高但更显眼的内容。

我在排查时,遇到过这类情况:正确答案在第三段,而第五段有个相似度不太高但非常“抢眼”的数字,模型最终采用了第五段的数字。处理方法是把检索结果按“问题匹配度”重新排序后送进上下文,并把最重要的一段放在最前面,次重要的放最后,形成“两头夹住”的结构。这个方法在不同模型上的效果稳定性,比模型本身能力差异影响还大。

5.2 冲突信息会导致模型“和稀泥”式回答

当上下文里既有“A 方案有效”也有“A 方案无效”的段落时,很多模型会试图“照顾”两边的证据,输出一些模棱两可的结论,比如“这取决于具体场景”“两种说法都有一定道理”。对用户来说,这基本等于没有回答。

我在医疗科普类项目里见过最典型的翻车:用户问“高血压患者能不能吃某类补剂”,知识库中有针对普通人群的“可以”说法,也有针对特定并发人群的“不建议”,两个段落都被搜进来了。模型最终给了“一般可以,但部分情况下不建议”的模糊答案,看似稳妥,实际等于没说。

处理这类问题的关键,不是让模型“更聪明”,而是在提示词里给模型立下明确规则:当证据存在冲突时,必须判断冲突的类型。如果是信息补全型冲突(一个段落补充另一个段落),就完整呈现;如果是结论对立型冲突,要以时间戳较新、权威层级较高的证据为准,并明确告诉用户“当前存在不同观点”。让模型在原来“生成”的职能基础上,多承担一步“证据仲裁”的工作,效果比单纯叠提示词好很多。

5.3 提示词的隐性束缚:模型“以为”自己必须用检索内容

另一类错误答案来源于提示词的隐性压制。很多 RAG 提示词都会写“请基于以下资料回答”,这暗示模型必须在资料里找答案。当资料里压根没有正确答案时,模型不会坦然说“不知道”,而是会强行编一段貌似合理的说法,甚至把资料里的不相关内容牵强附会成“答案”。

我后来采用的提示词写法是:

请根据给定资料回答问题。如果资料中不包含足够信息,请直接回复“资料中未找到相关信息”,不要猜测。只有在资料明确支持的情况下,才给出答案,并在答案后附上依据片段编号。

这里的关键是给模型一条“合法退路”。很多看似是模型幻觉的 RAG 错误,其实源于提示词把模型逼进了死胡同,让它只能“硬答”。

5.4 生成端评估别只看“字面正确”,还要看“是否忠于原材料”

最后,评估答案时不要只看 BLEU、ROUGE 这类字面重叠指标。一个真正合格的 RAG 答案,应该同时具备两个特性:信息正确性(factual correctness)和忠实性(faithfulness)。信息正确性考验的是“答的是不是对的事实”,忠实性考验的是“答的是不是来自给定资料”。如果答案是忠实的但不符合事实,说明检索证据本身有问题;如果答案是事实正确的但完全不忠实,说明模型在“抛开材料自说自话”,这同样是 RAG 的大忌。

要分别评估这两个维度,我建议至少保留一个小规模的人工标注集,每类新增错误案例都记录为四种类型:检索不到、检索到了但没用、检索到了但证据本身错误、生成时曲解了证据。这一步是后面排查链路的基础。


6. 一套务实的 RAG 错误排查链路:一步步锁定“错误答案”的根因

6.1 复现错误,先别急着改代码

遇到错误答案时,最怕的是急着调参。我的建议是先完整记录错误案例,包括问题原文、召回的前十条内容、重排后的顺序、喂给生成器的上下文、模型输出原文。没有这些原始数据,后面所有优化都是盲目的。

6.2 分阶段设置验证点,让根因自动“浮出水面”

我自己的排查顺序是这样的:

  1. 先查召回。把用户问题直接拿去做检索,人工看 Top-K 里有没有“足以回答这个问题的信息片段”。如果没有,问题出在召回层(切块、检索策略、元数据过滤),停止往下查。
  2. 再查精排。如果 Top-K 里有正确答案但排名不够靠前,比如排在第六名,而生成器只取前四,那就是排序/截断问题。调 Top-K 或引入重排。
  3. 再查上下文构造。如果正确答案在候选里,也排进了生成上下文,但被一堆高噪声段落包围,那么要调整上下文结构、去掉低价值段落,或者改成“证据块 + 明确引文”的形式。
  4. 最后查生成层。如果上下文里只有正确答案,模型还是答错,那才去调提示词、换模型或增加“证据仲裁”规则。

这个顺序的本质是一种“逐层隔离”策略:每一层都先证明自己没问题,再放行到下一层。实际使用中,它能避免大量的“越级调参”。

6.3 一个基于模拟场景的排查过程

有一次我处理“某跨平台系统”的问答模块,用户问“导入失败时,系统是否会自动重试”。日志显示检索出了三份内容:一份是“导入失败的处理流程”、一份是“传输层重试机制”、一份是“其它模块的导入规范”。第三份跟问题几乎无关,但因为关键词重叠也被送进上下文。

生成器最终回答“系统不会自动重试”。但真正的规则文档里写的是“导入失败后默认自动重试三次,可通过配置关闭”。正确答案在检索结果里排第二名,完全进入了上下文,模型却抓错了重点。

我没有去调 Top-K,也没有调提示词,而是把第三段无关内容从候选里滤掉,然后把包含“自动重试三次”的段落放在上下文第一位,问题立刻解决。这个案例告诉我:很多“答案错误”的真相是上下文噪声压制了答案信号,而不是模型能力不足。

6.4 建立错误案例“小样本库”,每次优化都要回归验证

做 RAG 优化很容易“按下葫芦浮起瓢”:调好了 A 类问题,B 类问题反而变差了。要避免这种情况,最好建立一个小而稳的回归集,专门覆盖之前记录过的二十到五十个典型错误案例。每次调整完检索参数、切块策略或提示词后,先跑一遍回归集,看看之前修好的问题有没有复发。

我吃过不少亏,之前有次为了提升一个业务场景的答案准确率,把元数据过滤条件调严了,结果另一个场景的跨部门查询大量丢失命中。回归集帮我在上线前就发现了这个问题,否则生产环境又是一波事故。

6.5 不要为了“刷精度”陷入过度工程

演进的完整链路往往是:一开始答案错,你怀疑检索;调了检索,发现排序不行;调了排序,发现上下文不行;调了上下文,发现模型不行;换了大模型,发现成本翻倍,但准确率没再涨多少。

这个循环走到某个临界点后,边际收益会变得很低。我的体会是,RAG 系统需要优先保证的是“稳定的下限”(不让弱相关和无关内容干扰模型),而不是盲目追求“上限”(找最完美的模型和参数)。下限够稳之后,大部分可用性问题已经解决了。


说实话,做 RAG 这么长时间,我最深刻的体会是:问题大多时候不是“单一环节坏了”,而是“几个环节的错误叠加在一起”。检索返回了一个相关片段,排序给了它一个名次,上下文把它和噪声放在一起,模型最终又做了错误取舍。任何一环稍微修正,答案可能就对了。

如果你现在正被同样的问题折磨,我建议先别动参数,直接把你线上一个错误案例按上面 6.1 到 6.3 的路径推演一遍。大多数情况下,你会很快意识到:原来答案不是被“生成”错的,而是在更早的阶段——检索、切块,或者上下文构造那里就已经注定了。

最后再分享一个小技巧:每次修完 bug,我会在错误案例旁边记一行“如果这个问题再出现,我会先检查哪一步”,相当于给自己的排查链路留了一张寻路地图。过一个月回来看,这张地图能帮你快速定位新问题,效果比反复调试模型明显得多。这也是我从这些项目里沉淀下来,最值得保留的实操习惯。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进