RAG面试篇8
13. 什么是多路召回具体怎么做多路召回就是同时用多种不同的检索方式去捞候选内容然后合并排序而不是只靠单一的向量检索。我理解核心出发点是向量检索和关键词检索各有盲区向量检索擅长语义相似但对精确词语比如产品型号、缩写、数字效果比较差BM25 关键词检索正好相反精确匹配强但不理解语义。我在项目里常用的组合是向量检索加 BM25 混合检索再加上多 Query 扩展也就是把用户问题改写成多个版本分别检索。多路的结果用 RRF 算法融合最后送进 Rerank 精排。什么是多路召回多路召回顾名思义就是同时用多种不同的检索方式去捞候选内容最后再把各路结果合并、排序交给 LLM 生成答案。和它对应的是「单路召回」也就是只用一种检索方式最典型的就是只用向量检索用户问题转成向量去向量库里找相似度最高的 Top-K 个 chunk然后直接喂给 LLM。这套流程在很多场景下能跑起来但有明显的天花板。你可能会问既然向量检索有语义理解能力为什么还不够用原因很简单没有任何一种检索方式是全能的向量检索有它的盲区BM25 也有它的盲区把它们组合起来互相补盲区总召回质量才会比单路高。常见的组合是三路第一路是向量检索负责语义层面的覆盖处理同义词、近义词、不同表达方式「退货」和「申请售后」在向量空间里是邻居能互相命中。第二路是BM25 关键词检索负责精确词匹配专门处理产品型号、专有名词、数字这类向量检索搞不定的场景。第三路是多 Query 扩展把用户问题改写成多个不同角度的版本覆盖更多表述差异相当于撒了一张更宽的网。三路各自召回一批候选再用 RRF 算法把排名融合成一个统一的结果列表最后送进 Rerank 精排才把最终上下文交给 LLM。为什么单路召回不够用?理解了多路召回是什么之后自然会追问单路到底差在哪为什么非要搞这么复杂单纯依赖向量检索是大多数 RAG 系统早期的做法但它有一个明显的短板对精确词语的召回效果差。比如用户问「M4 Pro 芯片的性能参数」这里的「M4 Pro」是一个专有名词向量模型可能把它和「苹果最新处理器」的向量拉得很近但如果知识库里这个词本来就是「M4 Pro」向量检索不如直接关键词匹配来得准。很多人以为换个更好的 Embedding 模型就能解决其实不行——这是向量检索「只看语义、不看字面」的固有局限不管用哪个模型都存在这个问题。反过来关键词检索BM25对同义词和不同表达方式无能为力。用户问「怎么退货」文档里写的是「申请售后」词没重叠BM25 完全召回不到。两种检索方式的盲区恰好互补这就是「多路召回」的出发点。理解了这个互补关系后面每一路的作用就非常清晰了。第一路向量检索Dense Retrieval向量检索是多路召回的基础一路。核心做法是把文档和用户问题都用 Embedding 模型转成向量然后用余弦相似度在向量库里找最近的 top-K 个 chunk。它擅长语义匹配同义词、不同表达方式都能覆盖但对精确词语产品型号、缩写效果不佳——而这恰好是第二路 BM25 的强项。第二路BM25 关键词检索Sparse Retrieval理解了向量检索搞不定精确词语这个问题BM25 的加入就顺理成章了。BM25 是 TF-IDF 的改进版本根据词频和文档频率给每个词打分找出包含查询词最多、且这些词在整个语料里不太常见也就是区分度高的文档。它的核心逻辑是一个词在这篇文档里出现多词频高但在整个知识库里出现少区分度高说明这个词对这篇文档很具代表性权重就高。本质上是在问「这个词有没有『代表』这篇文档」中文场景下用 BM25 需要先做分词比如用 jieba 切词再对分词后的词列表建索引和检索。实现上就是对每个 chunk 先切词建索引查询时同样切词后计算 BM25 分数排序。纯文本匹配速度极快对精确词语、数字、专有名词的召回效果非常好。前两路解决了「语义匹配」和「精确词匹配」的问题但还有一个场景它们都覆盖不到——用户提问的角度和文档表述的角度压根不一样不是同义词的问题而是整个视角的差异。这就需要第三路出场了。第三路多 Query 扩展召回为什么还需要第三路因为用户提问的角度和知识库里文档的表述角度不一致这不是同义词能解决的。比如用户问「产品多久能送到」文档里写的是「配送时效说明」两句话角度完全不同向量相似度可能不高BM25 也匹配不到关键词。多 Query 扩展的做法是用 LLM 把用户的原始问题改写成 35 个不同角度的版本分别去检索然后把所有结果合并去重。这样只要有一个改写版本和文档的表述对上了就能把正确的内容召回来就像拦截网越宽捕到鱼的概率越高。实现上先调 LLM 生成多个问题变体每个变体分别跑向量检索最后把所有结果汇总并去重同一个 chunk 被多个变体召回时只保留一份。代价是多了几次 LLM 调用但在用户提问风格多变的场景下召回覆盖率能提升 10%20%。结果融合RRF 算法三路召回各自拿回了一批候选接下来的问题就是怎么把三路结果合成一份每一路都有自己的排序结果分数单位不一样向量检索是余弦相似度BM25 是 TF-IDF 分数没法直接加权平均。你可能会想能不能归一化之后再加权理论上可以但实际上各路分数的分布差异很大归一化效果不稳定工程上也更复杂。RRFReciprocal Rank Fusion倒数排名融合是目前最常用的融合方法它只用排名而不用原始分数巧妙地绕开了分数不可比的问题其中 k 是平滑参数通常取 60rank 是文档在某一路结果里的排名。RRF 的直觉很好理解不管各路分数怎么算因为向量相似度和 BM25 分数本来就没有可比性只看排名。一个文档在多路检索里都排名靠前它的 RRF 综合分就高就像多位评委都给高分的选手最后的综合排名就高。这个方法不需要训练、计算量极小工程落地成本很低。值得注意的是RRF 本质上还是粗排适合在 Rerank 之前做候选集合并。如果对最终召回精度要求很高还是要在 RRF 融合之后接一个 Cross-Encoder 结构的精排模型比如bge-reranker-v2-m3做深度打分把真正相关的内容筛到最前。实战建议不是每个场景都需要三路全上按业务特点来选知识库里有大量专有名词、产品型号、数字的场景比如电商、IT 文档向量 BM25 双路是标配收益很明显。用户提问方式多变、和文档表述差异大的场景比如客服问答加上多 Query 扩展召回覆盖率能提升 10%20%。对召回质量要求极高的场景三路全上最后接 Rerank把融合结果精排一遍再给 LLM把质量做到天花板。14. RAG 检索优化策略有哪些我理解 RAG 的检索优化可以从四个层次来看索引层决定知识怎么存查询层决定问题怎么转换召回层决定从哪些路径去找重排序层决定最终哪些内容进入 prompt。每一层都有对应的优化手段我的经验是单独优化一个层次往往效果有限线上系统我会组合来用先靠索引优化和多路召回来保证覆盖率再用 Rerank 保证精度如果用户提问质量比较差再额外加上查询优化。检索是 RAG 的命脉要理解为什么检索优化这么重要先想清楚一件事LLM 只能根据送进去的 context 来回答检索召回的内容就是整个系统的天花板。生成层做得再好如果检索没把相关内容找回来LLM 也是巧妇难为无米之炊。反过来只要检索能稳定地召回准确、相关的 chunk生成质量自然差不到哪里去。你可能会问那优化生成层没用吗当然有用但投入产出比完全不同。生成层的优化比如调 prompt、换模型是锦上添花而检索层的优化是从根本上提升系统的能力上限。所以在 RAG 系统里检索优化是投入产出比最高的环节没有之一。四层优化全貌RAG 检索优化可以从四个层次来理解每一层解决的问题不同优化手段也不同索引层决定知识怎么「存」也就是文档切割的粒度和方式直接影响向量的语义质量查询层决定问题怎么「转」在检索之前对用户的 query 做加工让它更容易命中知识库召回层决定知识从哪里「找」用多条不同的检索路径并行捞取候选互补各自的盲区重排序层决定候选里谁「最相关」对粗召的候选集做精排保证进入 prompt 的都是真正有用的内容。这四层是递进关系可以用一个比喻来理解索引层决定「仓库里放了什么」查询层决定「用什么钥匙开门」召回层决定「从哪几扇门进去找」重排序层决定「把找到的东西里最好的几件带出来」。下面依次展开说。第一层索引优化先从最底层的索引说起因为索引是所有后续优化的基础如果知识存的方式就有问题后面再怎么优化查询和检索都是白搭。索引优化是 Chunking 策略的延伸但它聚焦于一个更核心的矛盾检索用的粒度和 LLM 读的粒度天然是矛盾的。理解这个矛盾要从 chunk 承担的两个角色说起。一个 chunk 需要同时完成两个任务。第一个任务是「检索时被找到」这要求向量语义尽量聚焦。把一篇文章压成一个向量这个向量里混合了太多不相关的语义用户问细节问题时这个笼统的向量很可能和问题向量距离较远就检索不到了所以检索需要小粒度的 chunk每个 chunk 语义聚焦。第二个任务是「被 LLM 读懂」这要求有完整的上下文。断章取义的几句话 LLM 往往答不好如果文档里前一段定义了术语、后一段才是真正的解释只给 LLM 后一段它可能看不明白所以 LLM 需要大粒度的 chunk上下文完整。很多人以为直接把 chunk 切小就好了检索会变准但切小之后 LLM 拿到的是碎片化的信息回答质量反而下降。这就是两难困境小 chunk 检索准但内容太碎大 chunk 内容完整但检索时语义稀释。解决这个矛盾的核心思路叫 Small-to-Big也就是小块检索、大块使用。具体有三种实现方式反过来关键词检索BM25也有自己的盲区它只会数词频不理解语义。用户问「怎么退货」文档里写「申请售后」词不重叠BM25 完全召不到但向量检索能处理这种同义表达。两种检索方式的盲区恰好互补这就是多路召回的出发点。不只走一条路同时打开多扇门把各路结果汇总起来覆盖更多的可能性。典型的三路并行是这样的第三层召回优化查询优化是从「问题」这边想办法召回优化则是从「检索路径」这边想办法。即使 query 已经改写得很好了如果只走一条检索路径还是会漏掉一些内容。单一的向量检索有一个根本局限它只擅长语义相似对精确词语匹配效果差。比如用户问「M4 Pro 芯片的性能跑分」这里的「M4 Pro」是一个精确的产品型号如果知识库里就是这么写的向量检索反而不如直接关键词匹配来得准因为向量模型可能把「M4 Pro」和「苹果最新处理器」的向量拉近但就是找不到包含字符「M4 Pro」的那条记录。Parent-Child Chunking是最直接的方案。把文档切成两个版本一份是细粒度的子 chunk比如 150 token 一个一份是粗粒度的父 chunk比如 500 token 一个每个子 chunk 通过 parent_id 关联到对应的父 chunk。入库时只给子 chunk 建向量索引检索时用子 chunk 的向量来匹配精度高命中之后根据 parent_id 取出对应的父 chunk把父 chunk 塞给 LLM 阅读上下文完整。这样就做到了「检索用小的阅读用大的」两全其美。摘要索引Summary Index的思路稍有不同它不是切割文档而是让 LLM 为每一段内容生成一段摘要用摘要来建向量索引。为什么这样做因为文档原文有时候表述很散而摘要是对核心意思的提炼语义更聚焦在向量空间里和用户的问题会更接近命中率更高。检索时用摘要的向量匹配命中后把原始段落塞给 LLM 阅读。多粒度分层索引则更激进同时建章节级、段落级、句子级三层索引。不同类型的问题适合不同粒度「什么是 RAG」这种宽泛的概念性问题用章节级就够了「退款申请需要几个工作日」这种细节性问题用句子级更精准。系统根据问题类型自动选择合适的粒度去检索能覆盖更多类型的用户需求。第二层查询优化索引优化解决的是「知识怎么存」的问题但即使索引建得再好用户的提问方式和知识库里的表述方式之间还是会存在鸿沟这不是存储端的问题而是查询端的问题。来看一个具体的例子用户问「苹果手机咋截图」知识库里写的是「iPhone 截图操作方法」。两句话意思一样但向量相似度可能不高前者是口语中文后者是正式书面语表达风格的差异会拉开向量距离检索就容易漏。你可能会想不是已经用向量检索了吗语义理解应该能处理这种差异吧实际上向量检索虽然比关键词检索好很多但口语和书面语的 Embedding 距离依然比人们直觉上以为的要远尤其是在短文本场景下信息量本来就少表达差异对相似度的影响被放大了。查询优化就是在检索之前先对用户的 query 做加工让它在向量空间里离正确文档更近。主要有四种方法。Query 改写是最基础的方法用 LLM 把口语化、有歧义的 query 转化成更正式、更精准的书面表达。比如「它为什么这么贵」这个「它」指代不明LLM 结合对话历史把它改写成「iPhone 15 Pro Max 定价偏高的原因是什么」改写之后的 query 就更容易命中文档里的相关内容了。多 Query 扩展Multi-Query解决的是另一个问题用户的提问角度和文档的描述角度对不上。比如用户问「怎么退货」文档里写的是「售后申请流程」两种说法角度不同向量相似度可能偏低。Multi-Query 的做法是用 LLM 把一个问题扩展成 3~5 个不同角度的问法每种问法单独去检索最后把结果合并去重。只要有一种问法和文档对上了就能把正确内容召回来。可以用「撒网捕鱼」来理解一个问题扩展成多个问法就像多撒几条鱼线只要有一条钓上来了就算成功。有一点需要注意原始问题本身一定要保留在检索列表里不能只用改写版本因为改写过程中可能会丢失一些原始细节原始问题反而最精准。HyDEHypothetical Document Embeddings假设文档嵌入是一种更有创意的方法。正常情况下我们用问题的向量去匹配文档的向量但问题和文档本来就是两种文体天然有距离。HyDE 的做法是先让 LLM 根据问题生成一段「假设的答案」然后用这段假设答案的向量去检索而不是用原始问题的向量。假设答案和文档都是陈述性文字风格更接近向量距离也更近命中率更高。需要注意的是如果 LLM 生成的假设答案方向错了反而会把检索带偏所以一般在知识库领域比较明确的场景下效果更稳定。Step-back Prompting后退提问解决的是「问题太具体但知识库里只有通用背景」的情况。比如用户问「为什么 transformer attention 要除以 sqrt(d_k)」知识库里可能没有这道题的直接答案但有「attention 机制的数学原理」方面的内容。Step-back 就是先把具体问题往上抽象一层生成一个更通用的背景问题去检索把背景知识检索回来再结合背景知识回答具体问题两步走反而比直接查更准。三路各自捞出一批候选但它们的分数没法直接比较向量相似度是 0~1 的余弦值BM25 是 TF-IDF 分数量纲完全不同。你可能会想那归一化之后加权不就行了实际上各路分数的分布差异很大归一化效果不稳定工程上也更复杂。这时候需要一个统一的融合算法RRFReciprocal Rank Fusion倒数排名融合是目前最常用的方案。RRF 的思路很简单不看原始分数只看排名。对每一路结果排名第 1 的 chunk 贡献的分数最高排名越靠后贡献越低。把同一个 chunk 在所有路径里的得分加起来就是它的综合分。这样在多路检索里都排名靠前的 chunk最终综合分就高。各路检索的分数没法直接比就像百米赛跑和游泳比赛的成绩单位不同、没有可比性但排名是通用的语言综合各路排名来打分才公平。公式是score(chunk) 求和(1 / (k rank))其中 k 是平滑参数通常取 60。这个 k 的作用是加一个「保底分」不让排名靠后的候选因为偶尔失误就完全被淘汰数值选 60 是实践中发现效果最稳定的经验值。RRF 最大的优点是实现简单、不需要训练、计算量极小工程落地成本几乎为零但融合效果在大多数场景下都很好是多路召回的标配融合方案。第四层重排序经过前面三层索引优化、查询优化、多路召回。检索质量已经比朴素 RAG 好了很多但还差最后一步。多路召回之后候选 chunk 可能有 20~30 个这些 chunk 里难免混入一些不太相关的内容直接全塞给 LLM 会出问题一是 token 消耗暴涨成本直线上升二是上下文太长LLM 在处理长文本时容易出现「Lost in the Middle」的现象也就是只关注开头和结尾中间的内容容易被忽略。所以需要一个精排步骤从 20~30 个候选里挑出最相关的 3~5 个这就是 Rerank 的作用。你可能会问向量检索不是已经排过序了吗为什么还需要再排一次要理解 Rerank 为什么比向量检索更准需要先理解两种不同的模型结构。向量检索用的是 Bi-encoder 结构query 和 chunk 各自独立编码成向量再算余弦相似度。这个结构的优点是速度极快因为 chunk 的向量可以提前计算好存库检索时只要算一次 query 的向量然后做距离比较就行缺点是 query 和 chunk 是分开编码的模型没办法看到两段文字之间的具体词语关联相关性判断不够精准。Rerank 用的是 Cross-encoder 结构把「query chunk」拼成一对输入让模型整体看这一对的相关性。Cross-encoder 能看到 query 中每个词对 chunk 的影响、chunk 里哪些词最能回答 query相关性判断精度远高于 Bi-encoder。代价是每一个候选 chunk 都要单独跑一次 Cross-encoder速度慢所以只适合对小规模候选集做精排不适合大规模召回阶段。打个比方来理解两者的区别Bi-encoder 像是只看了两个人的简历就判断他们合不合适合作而 Cross-encoder 是把两个人放在一个房间里观察他们怎么交流、怎么配合判断当然更准确但代价是需要花更多时间观察每一个人。Rerank 的流程很清晰多路召回得到 20~30 个候选 chunkCross-encoder Rerank 模型逐一给每个「(query, chunk)」对打相关度分按分数降序排列后取 top-3 到 top-5 拼入 prompt。常用的开源 Rerank 模型有 BGE-Reranker-v2BAAI 出品中英双语效果都很好、BCE-Reranker不想自己部署的话也可以用 Cohere Rerank 或 Jina Reranker 的 API。在实际效果上加了 Rerank 之后最终答案质量通常有明显提升是成本效益最高的优化手段之一。四层优化怎么组合一个典型的生产级搭配Parent-Child 索引 向量 BM25 多路召回 Rerank 精排。这三层组合基本能覆盖大多数场景的检索质量问题。如果用户提问质量比较差口语化、指代不清再额外加上 Query 改写。从另一个角度来记这四层索引层保证「存进去的知识可以被找到」查询层保证「搜索的姿势是对的」召回层保证「不漏掉该找到的内容」Rerank 层保证「送进 LLM 的是真正有用的内容」。每一层各司其职组合起来才能把检索质量做到高水准。