LangChain RAG-Fusion:一次提问生成多个查询,倒排融合重排

发布时间:2026/10/12 11:20:00
LangChain RAG-Fusion:一次提问生成多个查询,倒排融合重排
个人主页 for_ever_love__ 欢迎各位大佬莅临其他栏目: 大模型开发从0到1 其他栏目: iOS项目总结大全 其他栏目: 我想学python了 其他栏目: iOS UI 文章目录LangChain RAG-Fusion一次提问生成多个查询倒排融合重排一、先看它和 MultiQuery 的区别二、RRF 公式2.1 为什么 k60三、手写实现3.1 doc_id 怎么定四、接进 LCEL 链五、并发优化六、和重排的配合七、成本账八、什么时候用九、常见坑十、小结LangChain RAG-Fusion一次提问生成多个查询倒排融合重排MultiQueryRetriever查询改写那篇讲过是多查几遍、结果合并。RAG-Fusion 更进一步多查几遍然后按被多少路命中重新排序。差别在哪前者是取并集后者是被多路共同命中的排更前。后者通常更准。一、先看它和 MultiQuery 的区别假设用户问报销多久到账生成 3 个改写查询各自召回查询A「报销到账时间」 → [文档5, 文档2, 文档9] 查询B「报销多久能收到钱」→ [文档2, 文档7, 文档5] 查询C「费用报销周期」 → [文档5, 文档3, 文档2]MultiQuery取并集去重按第一次出现的顺序返回[5, 2, 9, 7, 3]。文档 5 排第一只是因为它恰好在 A 里排第一。RAG-FusionRRF 融合文档A 中排名B 中排名C 中排名RRF 分数文档51311/611/631/61 0.0328文档22131/621/611/63 0.0327文档93——1/63 0.0159文档7—2—1/62 0.0161文档3——21/62 0.0161排序5 2 7 3 9。关键差异文档 5 和 2 因为被三路都命中分数远高于其他。这就是共识效应——多个视角都认为相关的大概率真相关。二、RRF 公式score(d) Σ_i 1 / (k rank_i(d))rank_i(d)文档 d 在第 i 路查询中的排名从 1 开始k平滑常数默认 60只在该路命中才计入未命中不加分。2.1 为什么 k60看k60时的贡献值排名1/(60rank)10.0163920.0161350.01538200.01250第 1 名和第 20 名的差距不到 1.4 倍。这意味着单路排名的影响被压缩了被几路命中的影响被放大了——这正是融合想要的效果。如果k调小比如 k1第 1 名得 0.5、第 2 名得 0.33头部优势极大行为接近只信第一路。三、手写实现LangChain 没有专门的 RAGFusionRetriever但实现很简单fromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParserfromlangchain_openaiimportChatOpenAI llmChatOpenAI(modelgpt-4o-mini,temperature0)GEN_PROMPTChatPromptTemplate.from_template( 针对下面的问题生成 4 个不同表述的检索查询。 要求分别从「专业术语」「口语表达」「原因角度」「操作角度」切入 覆盖用户可能的用词习惯。每行一个不要编号。 问题{question} )gen_chainGEN_PROMPT|llm|StrOutputParser()defreciprocal_rank_fusion(rank_lists:list[list],k:int60)-list:rank_lists: [[doc_id, ...], [doc_id, ...], ...]scores{}forrank_listinrank_lists:forrank,doc_idinenumerate(rank_list,1):scores[doc_id]scores.get(doc_id,0)1/(krank)# 按分数降序returnsorted(scores.keys(),keylambdax:-scores[x])defrag_fusion_retrieve(question:str,top_k:int5):# 1. 生成多个查询rawgen_chain.invoke({question:question})queries[q.strip()forqinraw.strip().split(\n)ifq.strip()]queriesqueries[:4]# 保险起见截断# 2. 每个查询各自检索可以并发rank_lists[]forqinqueries:docsvectorstore.similarity_search(q,k10)rank_lists.append([doc_id_of(d)fordindocs])# 3. RRF 融合fused_idsreciprocal_rank_fusion(rank_lists)[:top_k]# 4. 取回完整文档return[doc_by_id[i]foriinfused_ids]3.1 doc_id 怎么定RRF 需要能标识同一个文档。最简单的方式是用内容哈希importhashlibdefdoc_id_of(doc)-str:returnhashlib.md5(doc.page_content.encode()).hexdigest()更推荐入库时就给每个 chunk 一个稳定 idfori,chunkinenumerate(chunks):chunk.metadata[chunk_id]f{chunk.metadata[source]}::c{i}这样 id 可读、可溯源比哈希好用。四、接进 LCEL 链fromlangchain_core.runnablesimportRunnableLambda,RunnableParallel,RunnablePassthroughdeffusion_retrieve(inputs):qinputs[question]rawgen_chain.invoke({question:q})queries[x.strip()forxinraw.strip().split(\n)ifx.strip()][:4]rank_lists[]forsqinqueries:docsvectorstore.similarity_search(sq,k10)rank_lists.append([d.metadata[chunk_id]fordindocs])idsreciprocal_rank_fusion(rank_lists)[:5]return[DOC_BY_ID[i]foriinids]chain(RunnableParallel({context:RunnableLambda(fusion_retrieve)|RunnableLambda(format_docs),question:RunnablePassthrough(),})|prompt|llm|StrOutputParser())注意fusion_retrieve接收的是{question: ...}字典因为RunnableParallel会把整个 dict 传给它我这里的question是RunnablePassthrough的结果实际上传进来是{question: ...}。五、并发优化4 个查询串行检索太慢。用异步并发importasyncioasyncdefasync_fusion_retrieve(question:str,top_k5):rawawaitgen_chain.ainvoke({question:question})queries[x.strip()forxinraw.strip().split(\n)ifx.strip()][:4]# 并发检索resultsawaitasyncio.gather(*[vectorstore.asimilarity_search(q,k10)forqinqueries])rank_lists[[d.metadata[chunk_id]fordindocs]fordocsinresults]idsreciprocal_rank_fusion(rank_lists)[:top_k]return[DOC_BY_ID[i]foriinids]延迟从4 × 单次检索降到1 × 单次检索。六、和重排的配合RAG-Fusion 和 rerank 是互补的作用位置RAG-Fusion提升召回率别漏检索层rerank提升精度排准检索后推荐组合# 1. RAG-Fusion 召回宽fusedawaitasync_fusion_retrieve(question,top_k20)# 2. rerank 精排窄finalreranker.compress_documents(fused,question)[:3]Fusion 给 rerank 提供高质量的候选集rerank 再把 top-20 排成 top-3。这个组合比单用任一个都强。七、成本账一次 RAG-Fusion 的开销项目数量生成多查询1 次 LLM 调用向量检索4 次可选rerank1 次模型推理对比普通 RAG1 次检索、0 次额外 LLM延迟增加约 1 次 LLM 调用时间检索本身很快可并发。省成本的办法查询数从 4 降到 3召回率损失很小缓存多查询结果相似问题复用简单问题不走 Fusion先用规则判断报销多久到账这种明确的直接检索就够。八、什么时候用场景用不用用户提问用词和文档差异大强烈推荐召回率低相关的搜不到推荐已经用了 rerank 且效果不错可以叠加先试试延迟要求极严1s慎用检索已经很准收益有限判断方法拿你的测试集跑一遍对比单查询 top-10 召回率和Fusion top-10 召回率提升超过 10% 就值得上。九、常见坑现象原因解法融合后结果变少每路k太小每路检索 k10~20doc_id 对不上用内容哈希但内容有差异入库时给稳定 chunk_id效果没提升生成的查询太相似prompt 里指定不同角度延迟高串行检索用asyncio.gather并发噪声变多某个改写跑偏了降到 3 个查询或加 rerank十、小结RAG-Fusion 多查询生成 RRF 倒排融合和 MultiQuery 的区别是按被多少路命中排序而不只是取并集RRF 公式Σ 1/(60 rank)k60 压缩单路排名影响、放大共识效应手写实现生成 3~4 个查询 → 各自检索 → RRF 融合 → 取回文档⚠️要提前给每个 chunk 稳定的 id用asyncio.gather并发检索延迟不随查询数线性增长和 rerank 是互补的Fusion 负责召回率rerank 负责精度成本多 1 次 LLM 调用可以把查询数降到 3 或加缓存。到这里 RAG 深水区的三篇多向量、自查询、时间加权、元数据过滤、引用溯源、RAG-Fusion就讲完了。下一篇进入 Agentic RAG——让 Agent 自己决定要不要检索、检索几次。