前三篇把离线链路搭完了:切分、向量化、入库。库建好之后,很多人会松一口气,然后发现效果并不好——资料明明在库里,答案也明明在资料里,模型却答不对。 这一篇讲的就是从"检索到了"到"答对了"之间那段距离,以及一件比具体技术更重要的事:先判断问题出在哪一层。
一、"检索到了"和"答对了"之间,有三层问题
这是本文最重要的一节。因为在动手优化之前,你必须先知道自己在优化什么——不然很容易在错误的环节上使劲,越优化越困惑。
RAG 的失败可以拆成三层,症状相似但原因完全不同:
第一层:答案所在的块,压根没进召回列表。
检索阶段就漏了。后面的重排、拼装再精巧也没用——没进列表的东西,后面所有环节都看不见它。 这一层的原因是检索能力问题:查询和文档的表达不匹配、切分把答案切散了、向量模型对这类内容不敏感。
第二层:块进来了,但排在很后面。
如果一次只取前 5 个,而正确答案排在第 12 位,它就会被截掉。这一层是排序问题,不是检索问题——召回是成功的,只是顺序不对。重排正是为这一层设计的。
第三层:块排得挺靠前,模型还是答错了。
这一层已经和检索无关了。可能的原因:拼装的上下文里有矛盾信息、块太多把关键信息淹没了、块的顺序让模型忽略了中间部分、或者提示词没要求它"基于给定资料回答"。
三层的解法完全不同:第一层去查改写和切分,第二层上重排,第三层改拼装和提示词。
所以下面这几节,应该对照你的实际症状来挑——不是全都做一遍。一个实用的判断方法是:把某个问题的召回结果原样打印出来读一遍。 如果正确答案根本不在里面,那是第一层;如果在里面但位置靠后,那是第二层;如果它就在前几名而模型仍然答错,那是第三层。这个问题第十三篇也提过,因为它是定位问题最快的方法。
二、查询改写:收益最大,却最常被跳过
这一节放在最前面,是因为它被跳过的比例最高。
原因很自然:用户输入什么问题,就拿什么问题去检索——这个逻辑太顺了,顺到没人会去想它有没有问题。但实际情况是,用户的问题和文档的表述常常不是一套语言。
看几个典型的落差:
- 口语化:用户问"这个怎么退",文档标题写的是"退款流程说明"。字面几乎不重合,但语义是一回事。
- 指代:多轮对话里用户说"那它呢"、"这个呢"。这三个字单独拿出来,没有任何检索价值。
- 缺关键词:用户描述的是现象("打开就闪退"),文档记的是原因("初始化超时导致进程退出")。
- 问题太宽:用户一次问了三个不相关的问题,当成一个查询去检索,向量会被平均掉,哪个都不像。
针对这几种,改写有几种做法:
基础改写:让模型把口语化的问句改成更接近文档表述的检索语句。
结合历史消解指代:把"那它呢"补全成"XX 功能的退款时效是多少"。这一步必须用到对话历史——单独看当前这句话是无解的。这也是为什么多轮场景下的检索比单轮难。
多子问题拆解:把复合问题拆成几个独立查询,各自检索、各自召回,最后合并去重。代价是检索次数变多。
HyDE(假设性文档生成):先让模型"编"一段可能的答案,再用这段答案去检索。
这个方法一开始听着反直觉——答案都是编的,怎么能用来检索?但它的道理是:编出来的答案,在表达方式上更接近文档,而不是更接近问题。 比如用户问"退款要几天",模型编的答案是"退款一般在 X 个工作日内到账",这句话的措辞和文档里的句子高度相似,检索自然更容易命中。
代价是每次查询多一次模型调用,延迟和成本都会上去。所以它适合那些查询量不大、但准确性要求高的场景。
要不要做改写,有个务实的判断:先统计一下线上查询里,"字面上和任何文档都不相近"的比例有多高。如果这个比例不低,改写的收益就明显;如果你的用户本来就习惯用专业术语提问,改写的收益可能有限。
三、混合检索:向量不擅长的那些东西
向量检索擅长语义相似,但有几类内容它天然不敏感:
- 专有名词:产品名、人名、地名,尤其是生僻的;
- 编号与单号:订单号、工单号、型号;
- 错误码:
ERR_4032、TimeoutException; - 缩写:团队内部约定俗成的短写。
这几类内容的共同点是:它们的语义信息很弱,识别价值全在字面上。 而向量是把语义压缩成数值,字面细节恰恰在压缩中被抹掉了。所以你问"ERR_4032 怎么解决",向量检索很可能召回的是一堆"错误处理"相关的泛泛内容,而不是那条写着 4032 的文档。
关键词检索(倒排索引)正好补上这一块——它只看字面匹配,编号、错误码命中得非常准。
混合检索就是把两者结合:一路走向量,一路走关键词,然后融合结果。
融合方式常见的有两种:
- 加权求和:给两路结果各配一个权重,按分数加总。简单,但两路的分数尺度不同,权重要调。
- RRF(倒数排名融合):不看分数,只看排名——每一路里排第 1 的加 1/(k+1) 分,排第 2 的加 1/(k+2) 分,以此类推,最后加总。
RRF 的好处是不需要校准两路的分数量纲,实现简单、效果稳定,所以在混合检索里被广泛采用。它的直觉是:一个结果如果在两路里都排在前面,那它很可能是真的好。
如果你的资料里有大量编号、错误码、专有名词,混合检索的收益通常比换一个更好的向量模型还明显——因为它补的是能力缺口,不是精度提升。
四、重排:为第二层问题设计的
前面说过,重排解决的是"召回了但排后面"。它值得做,因为向量检索的排序精度天生有限。
原因在于向量检索用的是双编码器:文档和查询各自独立编码成向量,然后比距离。它的优点是快——文档向量可以提前算好,查询只要算一次就能和百万个向量比。但缺点是文档和查询之间没有任何交互,两边的语义是各自压缩的,细节丢失了。
重排模型用的是交叉编码器:把查询和文档放在一起输入模型,让它们直接"对话"。这样能捕捉到细粒度的相关性,精度明显更高。代价是慢得多——每对(查询, 文档)都要过一次模型,没法预处理。
所以两者的分工是明确的:
- 向量检索负责从海量里粗筛,比如从 10 万块里取 50 个;
- 重排负责把这 50 个精细排序,取前 5 个送去拼装。
关键在于只对 top-K 做重排。 如果对所有召回结果都重排,成本会失控。K 取多少是个权衡——取太小,真正相关的可能没进来;取太大,重排的耗时上去。实践中从几十这个量级起步比较常见,具体要按你的延迟预算调。
重排模型怎么选,看两点:一是中文效果(和第十四篇选 embedding 一样,别看英文榜单,用自己的数据测);二是延迟(重排是在线路径上的同步操作,慢一点用户就能感觉到)。如果一个重排模型把延迟从 200ms 拉到 2s,那它带来的精度提升可能不值得。
还要注意一个取舍:重排是"锦上添花",不是"雪中送炭"。 如果正确答案根本没被召回(第一层问题),重排救不了——它只能在给定的候选里重新排序。所以顺序必须是:先保证召回得了,再考虑排得准。
五、最后一公里:上下文怎么拼
这一节最容易被忽略,但它属于第三层问题,而且改起来最便宜——不用重跑任何离线任务,改几行代码就能验证。
几个要点:
块数不是越多越好。 一个常见的直觉是"多给点资料总没坏处",但实际相反:拼进去的块越多,噪音越多,模型越容易被无关内容误导。而且输入 token 增加意味着成本和延迟同时上升(第九篇算过这笔账)。
有一个已知的普遍现象值得注意:当上下文很长时,模型对中间部分的关注度明显弱于开头和结尾。 这意味着——如果你非要放 10 个块,最相关的那几个应该尽量放在开头和结尾,而不是中间。 这个调整不花任何成本,但效果差异明显。
必须去重。 第十三篇讲过切分时的重叠会导致相邻块内容重复。如果两个高度相似的块同时被召回,它们会占掉两倍位置却只提供一份信息。拼装前做一次相似度去重,很便宜。
把出处带上。 每个块拼进去时标上来源(文档名、章节),并明确要求模型在回答里引用。第十二篇讲过理由:这是用户发现"检索错了"的唯一机制。 如果模型答得通顺但引用了一份不相关的文档,用户至少能看出来。
提示词里要明确约束。 告诉模型"只根据给定资料回答,资料里没有就说没有"。不写这句,模型遇到资料不全的情况会用自己的先验补齐——这正是第十二篇说的"RAG 降低但不消除幻觉"的成因之一。
块之间要有清晰的分隔。 别把几个块首尾相接成一大段,那样模型分不清哪句来自哪份文档,引用也就无从谈起。
六、落地顺序:别一次全上
这几件事的收益和成本不一样,建议按这个顺序推进:
- 先修拼装和提示词。 零成本、可即时验证。很多人做完这一步就明显改善了,也顺便排除了第三层问题。
- 再加查询改写。 一次模型调用,解决指代和表达鸿沟。这是收益最大的一步,尤其多轮场景。
- 然后上重排。 需要引入模型和延迟预算,但能把"排后面"的问题压掉。
- 最后评估混合检索。 如果你的资料里有大量编号、错误码、专有名词,这一步的收益会非常突出;如果没有,可以缓一缓。
- HyDE、多查询扩展这类更重的手段,放在最后,只在你已经确认前面几步都用尽、且准确性要求很高时才考虑。
这个顺序的理由是:每一步都要能被单独验证。 如果一次全上,效果好了你也不知道是哪一步的功劳,效果不好更不知道是哪一步的问题。
七、小结与检查清单
- 先分层再优化:召回没进来(第一层)→ 查改写和切分;进来了排后面(第二层)→ 上重排;排前面还答错(第三层)→ 改拼装和提示词
- 最快的定位手段:把召回结果原样打印读一遍,一眼就能看出属于哪一层
- 查询改写:收益最大也最常被跳过;重点是口语化落差、指代消解、多子问题拆解;HyDE 有效但多一次调用
- 混合检索:补的是"向量对字面不敏感"这个能力缺口,编号、错误码、专有名词场景收益明显;RRF 融合不需要校准量纲
- 重排:交叉编码器 vs 双编码器,只对 top-K 做;它是锦上添花,救不了召回不到的情况
- 拼装:块数不是越多越好、最相关的放首尾、去重、标出处、提示词里明确"只依据给定资料"
- 推进顺序:拼装与提示词 → 查询改写 → 重排 → 混合检索 → 更重的手段;每一步都要能单独验证
一道判断标准:问你"现在这套 RAG 的问题主要出在哪一层",如果答不出来,那你做的所有优化都只是在碰运气——而运气这件事,在没有指标的地方是看不出来的。
下一篇
优化做完了,下一个问题很自然:怎么知道它到底变好了? 凭感觉是不行的——同一批问题,改一版觉得好一点,再改一版又觉得差一点,来回摆动几天没有结论。下一篇讲 RAG 的效果评测:评测集怎么建、召回率和命中率分别测什么、答案质量这种"没法算对错"的东西怎么评、以及怎么让评测结果真的能指导决策。
本文是「用 Java 接入大模型」系列第十六篇。上一篇讲了向量数据库选型与 Java 落地,建议按顺序看。文中参数与比例(如重排 top-K)为实践起点,真实取值请按你的资料规模与延迟预算实测确定。