本文是【AI Agent从0到1开发学习】专栏系列文章更多内容持续更新中文章内容基于作者本人理解与实践如有纰漏与错误等问题烦请告知欢迎关注交流先用一句话回答多路召回就是针对同一个用户查询同时使用多种不同原理的检索策略去撒网然后把各路结果融合到一起从而兼顾语义理解和关键词匹配大幅提升召回的覆盖率和准确性。它解决的核心问题很直接——单一检索方式有盲区多路并行才能补齐短板。一、什么是多路召回在做 RAG检索增强生成或者搜索系统的时候召回指的是从海量文档中把和用户查询相关的候选项先捞出来。这是整个链路的第一步后续的精排、Rerank、乃至最终的大模型生成都依赖召回的质量。多路召回Multi-Channel Recall顾名思义就是不再只依赖一种检索方式而是同时走多条路每条路有自己的检索逻辑和匹配视角最终把各路命中的结果汇总融合。举个生活中的例子你要找一本讲分布式系统设计的书。如果你只按书名搜可能错过那些标题里没有分布式、但内容高度相关的书如果你只按内容语义搜可能漏掉那些关键词精准匹配的经典教材。两种方式各有盲区同时用两种甚至更多方式去搜才能最大程度把好书都找出来。在 RAG 场景下多路召回的典型结构是用户 Query 一进来同时触发向量检索、关键词检索、Query 扩展检索等多条路径各路独立返回 Top-K 结果最后通过 RRFReciprocal Rank Fusion等融合算法合并排序输出最终候选集。二、为什么单路召回不够用很多人刚做 RAG 的时候直接上一个向量检索Dense Retrieval就完事了。确实向量检索能捕捉语义相似性“深度学习能匹配到神经网络训练”这比关键词搜索聪明多了。但实际跑起来你会发现单路召回经常翻车原因主要有三个2.1 向量检索的语义偏差向量检索的原理是把 Query 和文档都映射到同一个向量空间按余弦相似度取最近邻。问题在于Embedding 模型不是万能的。当你问Python GIL 是什么的时候向量检索可能给你召回一堆讲Python 多线程的文档语义上确实相关但真正解释 GIL 机制的那篇可能被排到了十名开外。原因很简单——Embedding 模型在训练时对这种精准术语 概念定义的语义刻画不够细导致向量空间中的距离存在偏差。更极端的情况用户问的是一个缩写、行话、或者领域特有的黑话Embedding 模型压根没见过那向量检索基本就是瞎猜。这种时候关键词检索反而能精准命中。2.2 关键词检索的词汇鸿沟BM25 等关键词检索走的是另一条路分词、建倒排索引、按 TF-IDF 打分。它的优势是所见即所得——用户输入的词文档里必须有才能匹配上。这意味着精准匹配场景下 BM25 非常可靠。但关键词检索的死穴是词汇鸿沟用户说的词和文档里用的词不一致就匹配不上。用户问如何提升模型性能文档里写的是模型推理加速优化方案意思完全一样但 BM25 匹配不上因为提升和加速不是同一个词。这在中文场景下尤为严重同义表述太多了。2.3 任何单路都有系统性盲区检索方式擅长场景盲区向量检索语义相似、泛化匹配精准术语、缩写黑话、长尾概念关键词检索精准匹配、专有名词同义改写、语义理解、跨语言知识图谱检索实体关系、结构化推理开放域问答、非结构化文本每种检索方式都有自己的能力边界单路召回的召回率天花板是显而易见的。实际业务数据告诉我们单路召回的 Recall10 通常在 60%-75% 之间而多路组合后能稳定推到 85% 以上。这个差距在 RAG 场景下意味着用户问 10 个问题单路可能有 3-4 个因为没召回到关键文档而回答错误或者胡编。三、多路召回的具体实现分路策略下面进入实操部分。一个典型的多路召回方案至少包含三路检索每路各司其职多路召回架构Query 预处理后并行触发三路检索各路独立召回后经 RRF 融合输出第一路向量检索Dense Retrieval向量检索是多路召回里最核心的一路也是语义理解的主力军。基本流程将所有文档分 chunk 后用 Embedding 模型如 BGE、GTE、text-embedding-3 等编码为向量存入向量数据库Milvus、Qdrant、FAISS 等用户 Query 同样编码为向量在向量空间中按余弦相似度做 ANN 检索取 Top-K实现关键点Chunk 策略按固定 token 数切分如 512 token重叠窗口overlap设 50-100 token避免语义断裂。也可以按段落、章节切分保留结构信息。Embedding 模型选择中文场景推荐 BGE-M3 或 GTE-Qwen2英文场景推荐 text-embedding-3-large。模型选型对召回质量的影响非常大值得单独做一次评测。向量索引数据量小100万直接用 FAISS flat 索引精度最高数据量大用 HNSW 或 IVF-PQ牺牲一点精度换速度。# 向量检索核心代码示例伪代码fromsentence_transformersimportSentenceTransformerimportfaiss modelSentenceTransformer(BAAI/bge-m3)# 文档入库doc_embeddingsmodel.encode(doc_chunks,normalize_embeddingsTrue)indexfaiss.IndexFlatIP(doc_embeddings.shape[1])index.add(doc_embeddings)# 检索query_embeddingmodel.encode([user_query],normalize_embeddingsTrue)scores,indicesindex.search(query_embedding,top_k10)第二路BM25 关键词检索BM25 是信息检索领域的经典算法基于词频统计和文档长度归一化关键词匹配的守门员。基本流程对文档库建立倒排索引Elasticsearch、Whoosh 等用户 Query 分词后按 BM25 公式打分返回 Top-K 文档实现关键点分词器选择中文推荐 jieba 或 HanLP英文用标准 tokenizer。分词质量直接决定 BM25 的效果。同义词扩展对 Query 做同义词扩展能缓解词汇鸿沟问题比如性能优化扩展出提速、加速、调优。与 Elasticsearch 集成ES 内置 BM25开箱即用适合生产环境快速落地。# BM25 检索核心代码示例伪代码fromrank_bm25importBM25Okapiimportjieba# 分词tokenized_corpus[list(jieba.cut(doc))fordocindoc_chunks]bm25BM25Okapi(tokenized_corpus)# 检索tokenized_querylist(jieba.cut(user_query))scoresbm25.get_scores(tokenized_query)top_k_indicesscores.argsort()[::-1][:10]第三路多 Query 扩展召回这一路的思路是用户原始 Query 可能表述不够精确或者和文档的表述方式对不上那就让大模型帮忙改写或扩展出多个角度的 Query分别检索后合并。基本流程用 LLM 对原始 Query 生成 2-4 个改写/扩展 Query每个扩展 Query 分别走向量检索或 BM25 检索合并所有检索结果去重后作为第三路的输出Prompt 示例你是一个搜索查询改写专家。请根据用户的原始问题生成3个不同角度的搜索查询 用于在知识库中检索相关文档。要求 1. 保持核心意图不变 2. 使用不同的表述方式 3. 尝试从不同维度描述问题 原始问题{user_query} 请输出3个改写后的查询每行一个。实现关键点扩展数量控制2-4 个为宜太多会引入噪声且增加延迟。去重策略多 Query 检索的结果间可能有大量重复需要按文档 ID 去重。延迟控制LLM 改写 Query 有额外耗时通常 200-500ms对延迟敏感的场景可以用异步并行或者本地小模型替代。# 多 Query 扩展召回核心代码示例伪代码fromopenaiimportOpenAI clientOpenAI()defexpand_query(original_query:str,num_expansions:int3)-list:promptf根据以下原始问题生成{num_expansions}个不同角度的搜索查询。 保持核心意图不变使用不同表述方式。 原始问题{original_query}每行输出一个改写查询responseclient.chat.completions.create(modelgpt-4o-mini,messages[{role:user,content:prompt}],temperature0.7)expandedresponse.choices[0].message.content.strip().split(\n)#这里仅作示例因为#LLM可能返回带序号如“1. xxx”或空行简单split可能导致解析错误。实际需更健壮的解析return[original_query][q.strip()forqinexpandedifq.strip()]# 每个扩展 Query 分别检索all_results[]forqueryinexpand_query(user_query):resultsvector_search(query,top_k5)# 或 bm25_searchall_results.extend(results)# 去重seenset()unique_results[]forrinall_results:ifr.doc_idnotinseen:seen.add(r.doc_id)unique_results.append(r)四、结果融合RRF 算法三路检索各自返回了 Top-K 结果问题来了——怎么把这些结果合并成一个统一的排序列表这就要用到RRFReciprocal Rank Fusion互惠排序融合算法。4.1 RRF 核心思想RRF 的思路非常简洁不看分数只看排名。每个文档在每一路中都有一个排名rankRRF 根据排名给分排名越靠前分数越高最后把各路的分数加起来就是最终得分。公式RRF_Score(d)∑r∈R1krankr(d)\text{RRF\_Score}(d) \sum_{r \in R} \frac{1}{k \text{rank}_r(d)}RRF_Score(d)r∈R∑​krankr​(d)1​其中RRR是所有检索路的集合rankr(d)\text{rank}_r(d)rankr​(d)是文档ddd在第rrr路中的排名kkk是平滑常数通常取 60。三路检索各有各的排序RRF 根据各路排名加权求和输出融合排序4.2 为什么 k60kkk的作用是平滑。如果kkk太小比如k1k1k1排名靠前的文档会获得过大的优势排名稍后的文档几乎没机会如果kkk太大各路排名的差异被抹平融合就失去意义了。k60k60k60是原论文通过实验得出的推荐值在大多数场景下表现稳定。也可以基于自己的验证集调优。4.3 手动计算示例假设 Doc_A 在三路检索中的排名分别是第1名、第2名、第2名。那么它的 RRF 得分为RRF_Score(Doc_A)1601160216020.016390.016130.016130.04865\text{RRF\_Score}(\text{Doc\_A}) \frac{1}{601} \frac{1}{602} \frac{1}{602} 0.01639 0.01613 0.01613 0.04865RRF_Score(Doc_A)6011​6021​6021​0.016390.016130.016130.04865而另一个 Doc_E 只在向量检索中排第3其他路没进 Top-KRRF_Score(Doc_E)1603000.01587\text{RRF\_Score}(\text{Doc\_E}) \frac{1}{603} 0 0 0.01587RRF_Score(Doc_E)6031​000.01587可以看到RRF 天然偏好那些多路都靠前的文档而不只是某一路特别靠前的。这正是多路召回的核心优势——多方印证交叉验证。# RRF 融合核心代码示例defrrf_fusion(rankings_list:list[list[str]],k:int60)-list[tuple[str,float]]: rankings_list: 多路检索的排名列表每个元素是一路检索的doc_id排序列表 k: RRF平滑常数 返回: 按RRF分数降序排列的(doc_id, score)列表 rrf_scores{}forrankinginrankings_list:forrank,doc_idinenumerate(ranking,start1):ifdoc_idnotinrrf_scores:rrf_scores[doc_id]0.0rrf_scores[doc_id]1.0/(krank)# 按分数降序排列sorted_resultssorted(rrf_scores.items(),keylambdax:x[1],reverseTrue)returnsorted_results# 使用示例vector_ranking[Doc_A,Doc_C,Doc_E,Doc_B]bm25_ranking[Doc_B,Doc_A,Doc_D,Doc_F]multi_q_ranking[Doc_D,Doc_A,Doc_C,Doc_G]final_resultsrrf_fusion([vector_ranking,bm25_ranking,multi_q_ranking])fordoc_id,scoreinfinal_results:print(f{doc_id}:{score:.5f})五、实战建议理论和代码都有了最后聊聊我在实际项目中踩过的坑和总结的经验。5.1 先跑通单路再加多路不要一上来就搞三路召回。先用向量检索跑通整个 RAG 链路确认数据质量、Embedding 模型、Prompt 工程这些基础环节没问题再逐步加 BM25 和多 Query 扩展。多路召回是锦上添花不是雪中送炭——基础没打好多路只会放大噪声。5.2 各路 Top-K 参数要差异化三路检索不需要都取一样的 Top-K。向量检索通常取 Top-10~20BM25 取 Top-10关键词匹配的精度高不需要太多多 Query 扩展每路取 Top-5 再合并。总候选池控制在 50-80 个文档以内太多的话 Reranker 压力大延迟也上去了。5.3 Reranker 是多路召回的好搭档RRF 融合后的排序只是粗排精度有限。实际生产中几乎都会在 RRF 之后再加一个 Cross-Encoder Reranker如 bge-reranker-v2-m3对 Top-30 左右的候选做精排。粗排保召回率精排保准确率这是标准操作。5.4 留意延迟和成本多路召回意味着并行多次检索 一次 LLM 调用Query 扩展整体延迟会上升。几个优化方向向量检索和 BM25 可以并发调用而非串行Query 扩展用小模型或本地模型避免线上调用大模型的延迟设置合理的超时时间某路超时就用其余路的结果5.5 评估指标盯 Recall多路召回的优化目标很明确——提升召回率Recall。用 Recall5、Recall10、Recall20 这些指标来衡量不要盯着 MRR 或 NDCG 看那是 Reranker 的活。建议搭一个评估集标注 50-100 个 Query 和对应的相关文档定期跑评估看趋势。写在最后多路召回不是什么高深的技术本质就是别把鸡蛋放在一个篮子里。向量检索有语义理解的能力但缺精准度BM25 有精准匹配的能力但缺语义泛化多 Query 扩展能弥补表述差异但引入噪声——三路各有所长融合后互相补位这才是工程上靠谱的做法。如果你正在做 RAG 或者搜索相关的项目强烈建议从两路向量 BM25开始尝试跑一下对比实验用数据说话。效果提升大概率会让你觉得值得。