MultiHop-RAG实战:构建与优化多跳查询的检索增强生成系统

📅 发布时间:2026/7/10 2:33:59 👁️ 浏览次数:
MultiHop-RAG实战:构建与优化多跳查询的检索增强生成系统
1. 从“单步”到“多步”为什么你的RAG系统回答不了复杂问题如果你已经玩过一阵子RAG检索增强生成搭建过几个简单的问答机器人那你可能已经发现一个尴尬的事实当用户问一个简单、直接的问题时比如“苹果公司CEO是谁”你的系统对答如流。但一旦问题变得稍微复杂一点需要“拐个弯”比如“对比一下苹果和微软最近一个季度的营收增长率哪家更高”系统就很可能开始胡言乱语要么给出错误答案要么干脆说“我不知道”。这就是典型的“单跳查询”与“多跳查询”的区别。单跳查询就像你问路“最近的超市在哪”答案直接指向一个地点。而多跳查询则是“我想买做意大利面的材料哪家超市的番茄酱和意面既便宜又离我家最近”。要回答这个问题你需要先知道几家超市的位置第一跳再分别查询它们的番茄酱和意面价格第二跳最后综合距离和价格信息做出判断第三跳。这个过程需要串联多个信息片段进行推理。原始的RAG架构本质上是一个“检索-生成”的单跳过程。它把用户问题变成一个向量去知识库里找最相似的几个文本块然后把问题和这些文本块一起塞给大模型让它生成答案。这个流程对于多跳查询是“失明”的。因为多跳问题的答案往往分散在不同的文档、甚至文档的不同部分简单的向量相似度检索很难一次性把所有关键证据都精准地捞出来。更糟糕的是大模型在拿到这些零散、可能还不完全相关的证据后缺乏明确的指引去进行多步逻辑推理很容易“脑补”出错误结论。所以MultiHop-RAG这个基准数据集的提出就像是为RAG社区立下了一个新的挑战标杆。它明确告诉我们别只满足于回答简单问题了现实世界的复杂查询才是真正的战场。这个数据集基于新闻文章构建包含了推理、比较、时序和空问题等多种需要多步思考的问题类型精准地戳中了当前RAG系统的软肋。论文里的实验结果也很有说服力即便是用上最好的检索模型voyage-02加上重排序在需要召回4个关键证据时命中率也只有66%而即使把标准答案的证据直接喂给最强的GPT-4它的回答准确率也才89%。这说明无论是检索端还是生成端面对多跳查询我们都还有很长的路要走。2. 动手构建你的第一个MultiHop-RAG系统从理论到代码知道了问题所在接下来我们就要动手解决它。构建一个能处理多跳查询的RAG系统核心思想是把“一步到位”拆解成“分步执行”。下面我就结合代码带你走一遍核心流程。2.1 系统架构设计从“单车道”到“立交桥”传统的RAG是条“单车道”Query - Retriever - LLM - Answer。对于MultiHop-RAG我们需要设计一个“立交桥”系统。一个经典的架构是“查询分解-分步检索-综合推理”流水线。查询分解器负责把复杂的多跳问题拆分成几个顺序执行的子问题。比如“对比苹果和微软最近季度的营收增长率”可以分解为“苹果公司最近一个季度的营收增长率是多少”和“微软公司最近一个季度的营收增长率是多少”。子问题检索器针对每个子问题独立地从向量数据库中检索最相关的文档片段。推理协调器管理整个流程。它把第一个子问题的答案和原始问题结合起来形成新的上下文再去问第二个子问题如此递进。或者收集所有子问题检索到的证据一起交给最终生成器。最终答案生成器接收所有检索到的证据和原始问题生成最终答案。下面是一个高度简化的、使用LangChain和OpenAI API的实现框架from langchain.vectorstores import Chroma from langchain.embeddings import OpenAIEmbeddings from langchain.chat_models import ChatOpenAI from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate import logging # 1. 初始化组件假设你的知识库已向量化并存入Chroma embeddings OpenAIEmbeddings(modeltext-embedding-ada-002) vectorstore Chroma(persist_directory./your_chroma_db, embedding_functionembeddings) llm ChatOpenAI(modelgpt-4, temperature0) # 2. 定义一个简单的查询分解函数这里用LLM模拟实际可用更精细的模型 def decompose_query(complex_query): decomposition_prompt 请将以下复杂问题分解成一系列简单的、可以顺序回答的子问题。 复杂问题{query} 输出格式以数字列表的形式输出子问题每个子问题一行。 示例 输入谁写了《哈利波特》这部电影的第一部是哪年上映的 输出 1. 《哈利波特》这本书的作者是谁 2. 《哈利波特》系列电影的第一部是哪年上映的 prompt PromptTemplate(templatedecomposition_prompt, input_variables[query]) chain prompt | llm result chain.invoke({query: complex_query}) # 解析返回的文本得到子问题列表 sub_questions [q.strip() for q in result.content.split(\n) if q.strip() and q[0].isdigit()] # 去掉序号 sub_questions [q.split(. , 1)[1] for q in sub_questions] return sub_questions # 3. 分步检索与回答 def multi_hop_qa(complex_query, vectorstore, llm): sub_questions decompose_query(complex_query) accumulated_context final_answer for i, sub_q in enumerate(sub_questions): # 将累积的上下文前序答案也作为当前检索的参考 combined_query_for_retrieval f背景信息{accumulated_context}\n当前问题{sub_q} # 检索 docs vectorstore.similarity_search(combined_query_for_retrieval, k3) doc_text \n\n.join([d.page_content for d in docs]) # 回答子问题 qa_prompt PromptTemplate( input_variables[context, question], template基于以下信息回答问题。如果信息不足请说‘根据已有信息无法确定’。\n信息{context}\n问题{question}\n答案 ) qa_chain qa_prompt | llm sub_answer qa_chain.invoke({context: doc_text, question: sub_q}) # 累积上下文用于下一跳或最终合成 accumulated_context f子问题{i1}: {sub_q}\n答案: {sub_answer.content}\n\n logging.info(f子问题 {i1}: {sub_q}) logging.info(f检索到文档数: {len(docs)}) logging.info(f子答案: {sub_answer.content[:100]}...) # 4. 最终综合可选将所有子答案和原始问题交给LLM做最终梳理 synthesis_prompt PromptTemplate( input_variables[sub_answers, original_query], template以下是一系列子问题的答案请根据它们回答原始的综合问题。\n子问题答案汇总\n{sub_answers}\n原始问题{original_query}\n最终答案 ) synthesis_chain synthesis_prompt | llm final_answer synthesis_chain.invoke({sub_answers: accumulated_context, original_query: complex_query}) return final_answer.content # 使用示例 complex_question 苹果公司2023年第四季度的营收是多少这个数字相比微软同期的营收是更高还是更低 answer multi_hop_qa(complex_question, vectorstore, llm) print(最终答案, answer)这个框架虽然简单但清晰地展示了MultiHop-RAG的核心逻辑分解、迭代、合成。它不再试图一口吃成胖子而是像侦探破案一样一步步搜集线索最后拼出完整真相。2.2 关键组件选型与配置在具体实现时每个组件的选型都至关重要查询分解器你可以像上面一样用LLM如GPT-4做零样本分解也可以使用微调的小模型如T5甚至基于规则的方法。LLM灵活性高但成本也高小模型速度快但可能需要标注数据。我试过用gpt-3.5-turbo来做分解对于常见的问题模式效果不错性价比相对较高。检索器这是性能瓶颈之一。对于多跳查询单纯依赖嵌入模型做稠密检索Dense Retrieval往往不够。一定要加上重排序器。流程是先用嵌入模型召回大量候选文档比如Top-20再用一个更精细的交叉编码器模型Cross-Encoder对它们进行重排序选出最相关的Top-K个。论文中效果最好的组合就是voyage-02嵌入 bge-reranker-large重排序。你可以用SentenceTransformers库方便地使用BGE重排序模型。向量数据库选择支持高效相似性搜索和过滤的数据库。Chroma轻量易用适合原型快速验证Pinecone或Weaviate是成熟的云服务适合生产环境Milvus或Qdrant性能强大适合大规模部署。我个人的经验是初期用Chroma快速迭代想法用户量和数据量上来后再迁移到Milvus或Qdrant。大语言模型生成端的主力。闭源模型里GPT-4的推理能力遥遥领先是多跳推理的首选但价格昂贵。Claude 3系列尤其是Opus在长上下文和逻辑推理上表现也非常出色。开源模型方面Llama 3 70B、Mixtral 8x22B是强有力的竞争者但需要强大的GPU资源。对于成本敏感的场景可以尝试用GPT-3.5-Turbo或Claude Haiku处理分解和初步回答再用更强的模型做最终验证和合成。3. 性能优化实战让多跳RAG又快又准搭建出基础系统只是第一步让它真正可用、可靠还需要深入的优化。这里分享几个我踩过坑才总结出来的实战技巧。3.1 检索阶段的优化抓住“桥接”实体多跳查询之所以难检索是因为子问题之间靠一些“桥接”信息连接。比如问题“A公司的CEO毕业于哪所大学”桥接实体就是“A公司的CEO”。检索时如果只检索“A公司”可能找不到CEO信息只检索“大学”更是大海捞针。优化策略就是显式地利用这些桥接点。在查询分解后我们可以用NER工具或LLM从子问题中提取关键实体然后在检索时将这些实体作为元数据过滤器Metadata Filter或查询增强的一部分。# 伪代码利用桥接实体增强检索 def retrieve_with_bridge_entity(sub_question, previous_answer, vectorstore): # 1. 从上一个答案中提取可能成为桥接的实体例如人名、公司名 bridge_entity extract_entity_from_text(previous_answer) # 假设有一个实体提取函数 # 2. 构建增强查询 enhanced_query f{sub_question} 涉及实体{bridge_entity} # 3. 检索时可以尝试用实体进行过滤如果向量库支持元数据过滤 # 例如在Chroma或Weaviate中可以添加类似 metadata{mentioned_entity: bridge_entity} 的过滤器 # 如果不支持或过滤后结果太少则回退到纯语义检索 if bridge_entity: try: docs vectorstore.similarity_search(enhanced_query, k5, filter{entity: bridge_entity}) except: docs vectorstore.similarity_search(enhanced_query, k5) else: docs vectorstore.similarity_search(sub_question, k5) return docs此外分块策略也极其重要。对于多跳查询过小的块如128 token可能会把连贯的证据切碎导致信息丢失过大的块如512 token又会引入噪声降低检索精度。论文中使用了256 token的块这是一个不错的起点。我建议根据你的知识库特性做A/B测试。对于结构性强、段落分明的文档如财报、论文可以按章节或段落分块对于非结构化文本如新闻则建议使用语义分块工具如LangChain的RecursiveCharacterTextSplitter结合一些分隔符或者更高级的semantic-text-splitter它尝试在句子边界和语义完整性处进行切割。3.2 生成与推理优化设计引导思维的提示词即使检索到了所有正确证据大模型也可能“推理失败”。这时候提示词工程就是你的魔法杖。不要简单地把问题和证据堆在一起扔给模型而要设计引导其逐步推理的提示词结构。一个非常有效的模式是“Chain-of-Thought Self-Consistency”的变体。让模型先一步步说出它的思考过程最后再给出答案。对于比较类、推理类问题尤其有效。# 一个针对比较类多跳查询的优化提示词模板 comparison_cot_prompt 你是一位严谨的分析师。请基于以下信息通过逐步推理来回答问题。 信息片段 {evidence} 问题{question} 请按以下步骤思考 1. 首先从信息中分别找出与比较对象相关的具体数据和事实。 2. 然后确认这些数据是否具有可比性例如是否同一时间范围、同一计量单位。 3. 接着执行比较操作计算差值、比率或进行定性对比。 4. 最后基于比较结果给出明确的答案。 逐步思考过程 # 然后将这个提示词和问题、证据一起发给LLM并要求它最终以“答案”开头给出结论。对于时序查询例如“某产品发布后公司股价如何变化”提示词中要明确要求模型关注日期、时间顺序和事件之间的因果关系。你可以让模型先按时间线梳理事件再分析趋势。另一个技巧是让模型进行自我验证。在生成最终答案后追加一个步骤让模型根据证据检查答案中是否存在矛盾或无法证实的信息。这可以显著减少“幻觉”。3.3 评估与迭代用对指标持续改进优化不能凭感觉必须建立评估体系。对于MultiHop-RAG系统评估要分两层检索效果评估这是基础。使用MultiHop-RAG数据集提供的标准评估脚本关注HitsK前K个检索结果中包含所有必要证据的概率、MRR平均倒数排名等指标。特别要注意对于需要N个证据的问题HitsK要求K个结果里同时包含这N个证据这比单跳查询难得多。你可以分别评估每一跳子问题的检索准确率定位是哪个环节的检索出了问题。端到端答案评估这是终极目标。除了简单的准确率Accuracy对于生成式答案建议使用LLM作为裁判进行自动评估。例如让GPT-4根据标准答案和检索到的证据从“事实一致性”、“信息完整性”、“推理正确性”几个维度对系统生成的答案进行打分1-5分。这比单纯的字符串匹配更符合实际。在迭代过程中建立一个“错误分析集”非常有用。收集系统回答错误的问题案例人工分析失败原因是查询分解错了是某一跳的检索没找到证据还是模型推理出错针对每一类错误再制定具体的优化策略比如调整分解提示词、改进检索的元数据、或者增加推理的步骤指引。4. 进阶挑战与未来方向超越基础框架当你把基础的MultiHop-RAG系统跑通并优化到一定阶段后可能会遇到更深层次的挑战。这里探讨几个进阶话题和可能的解决思路。4.1 处理“信息缺口”与“矛盾证据”现实世界的数据充满噪声。你的知识库可能不完整存在信息缺口或者不同来源的信息可能存在矛盾。一个健壮的MultiHop-RAG系统需要能处理这些情况。对于信息缺口系统应该具备“自知之明”。当检索到的证据不足以回答某个子问题时生成器不应强行编造而应明确输出“根据现有信息无法确定”。这可以通过在提示词中强调“基于给定信息回答”以及让模型评估证据充分性来实现。更进一步系统可以记录下这些知识缺口为后续的知识库更新提供方向。对于矛盾证据这是更大的挑战。例如不同新闻对同一事件的报道可能有细微差别。系统需要能够识别矛盾并尝试进行可信度分析例如根据信息来源的权威性、发布时间等。在提示词中可以要求模型“如果发现信息冲突请指出冲突点并基于更可靠/更新的信息进行推理”。这实际上是将“事实核查”的能力部分赋予了RAG系统。4.2 引入图检索与推理当前主流方法基于向量检索本质是“扁平化”的语义匹配。但对于涉及复杂关系链的多跳查询知识图谱能提供更精确的路径。例如“爱因斯坦的导师的导师是谁”这类问题在图数据库里就是沿着“师生”关系边的两跳查询查询效率极高。未来的混合架构可能是系统首先判断问题类型如果是强关系型问题涉及大量实体和明确关系则路由到图检索模块如果是开放域、描述性问题则使用向量检索。甚至可以将知识图谱中的三元组和文本描述一起嵌入到同一向量空间实现混合检索。LangChain等框架已经提供了与Neo4j等图数据库集成的能力为这类探索提供了工具基础。4.3 让系统具备“学习”能力从交互中进化一个理想的MultiHop-RAG系统不应是静态的。它可以从与用户的交互中学习。例如查询重写如果用户初始查询很模糊导致检索效果差系统可以生成几个更清晰的查询变体让用户选择或者根据第一轮检索结果自动重写查询进行第二次检索。反馈学习当用户对答案给出“点赞”或“点踩”的反馈时系统可以记录这次交互。这些反馈数据可以用来微调重排序模型甚至微调用于分解或生成的LLM让系统越来越适应用户的提问风格和领域知识。主动澄清对于模棱两可的问题系统可以像人类一样主动提问澄清。比如用户问“他们的表现怎么样”系统可以反问“您指的是哪支球队的表现”。这需要系统具备一定的对话状态管理能力。实现这些能力意味着你的RAG系统从一个被动的问答机向一个主动的、持续进化的智能助手迈进。这其中的技术挑战很大但也是构建真正实用、好用的AI应用的关键。从我自己的项目经验来看构建MultiHop-RAG系统就像搭积木也是一个不断迭代调试的过程。没有一劳永逸的银弹最好的方法就是从最简单的流水线开始在一个像MultiHop-RAG这样的标准数据集上跑通基线然后针对性地测量、分析、优化每一个模块。过程中你会对嵌入模型、提示词、大模型的能力边界有更深刻的理解。最重要的是保持耐心多实验每一次失败都能让你离一个更聪明的系统更近一步。