混合检索RAG实战:向量库与搜索引擎联手补齐召回
发布时间:2026/9/29 18:42:02 作者:尧图编辑部 阅读量:1,286

1. 混合检索 RAG 到底在解决什么问题1.1 从一次“翻车”的问答说起去年我帮一个做工业设备维保的团队调他们的知识库问答系统。用户问“XX 型号液压泵启动异响怎么排查”系统返回的答案里赫然写着“请检查液压油位是否在刻度线以上”——听起来没毛病但那是另一款泵的保养手册内容。真正该返回的那份《XX 型液压泵故障代码 E07 处置指引》明明就在知识库里却排在第 14 位压根没进大模型的上下文窗口。问题出在哪他们的检索链路只有一路把文档切块、做向量化、存进向量库查询时用余弦相似度取 Top-K。这套做法在“语义相近”的场景下很好用但遇到型号、编号、专有名词、故障代码这类精确匹配需求时向量检索经常“答非所问”。因为嵌入模型把“E07”和“E10”编码成了几乎一样的向量它分不清这两个代码的区别。这就是混合检索 RAG 要解决的核心矛盾向量检索擅长语义泛化关键词检索擅长精确命中单靠任何一路都有盲区。所谓“混合检索”就是把向量库和搜索引擎两路召回的结果合并再用重排模型做一次精排最后喂给大模型生成答案。标题里说的“向量库和搜索引擎联手补齐召回”讲的就是这件事。这套方案适合谁我认为三类人最该认真看一是正在搭建企业知识库、但召回率始终上不去的工程师二是用 LangChain、Spring AI、LangChain4j 这类框架做 RAG 应用、发现“框架默认配置不够用”的开发者三是负责 RAG 项目落地、被业务方追问“为什么答不准”的技术负责人。如果你只是拿大模型做做闲聊这篇文章可以先收藏等真正做知识库时再翻出来。1.2 混合检索 RAG 的全链路长什么样先把全景图在脑子里立起来。一条完整的混合检索 RAG 链路我习惯拆成五个阶段查询增强 → 双路召回 → 结果融合 → 重排精排 → 生成回答。查询增强是入口负责把用户那句口语化的提问改写成更适合检索的形式比如补全指代、扩展同义词、拆解多跳问题。双路召回是核心一路走向量库做语义检索一路走搜索引擎做关键词检索两路并行、互不干扰。结果融合负责把两路返回的候选集去重、加权、合并成一个更大的候选池。重排精排用交叉编码器Cross-Encoder对候选池逐条打分把真正相关的顶上来。最后生成回答把精排后的 Top-N 文档塞进大模型的上下文让它基于事实作答。这个链路里每一环都在为下一环“减负”。查询增强让召回更准双路召回让覆盖面更广融合让候选更全重排让精度更高生成让答案更自然。任何一环偷懒最终答案的质量都会打折。我见过太多项目只做了“向量召回 直接生成”然后抱怨大模型幻觉严重——其实问题不在大模型而在前面四环根本没做扎实。提示混合检索不是“向量检索的替代品”而是“补丁”。它的价值在于用关键词检索的精确性补上向量检索在专有名词、数字、代码上的短板。理解这一点后面的参数调优才有方向。2. 查询增强让检索的“第一公里”不跑偏2.1 为什么原始查询不能直接拿去检索用户输入的问题和知识库里的文档往往存在巨大的“表达鸿沟”。用户问“这泵响得厉害咋整”文档里写的是“液压泵异常噪声故障诊断流程”。向量模型虽然能拉近一点距离但“响得厉害”和“异常噪声”之间的语义 gap加上“咋整”这种口语检索效果会明显下降。更麻烦的是指代和多跳问题。用户上一句问“XX 泵的故障代码有哪些”下一句问“那 E07 是什么意思”这里的“那”指代的是上一轮的 XX 泵。如果直接把“那 E07 是什么意思”丢给检索器它根本不知道在问哪个型号。还有一类问题需要多跳推理比如“和 E07 同系列的故障代码里哪个需要停机检修”这需要先查出 E07 属于哪个系列再查该系列的所有代码最后筛选出需要停机的。单次检索搞不定。查询增强就是在这个环节做文章。它不改变知识库只在查询侧做“翻译”和“拆解”让检索器拿到一个更清晰、更完整的查询意图。2.2 查询增强的四种常用手法我实际项目里用得最多的有四种按复杂度从低到高排第一种查询改写Query Rewriting。用一个小模型或规则把口语化查询改写成书面化、关键词密集的查询。比如“这泵响得厉害咋整”改写成“液压泵 异常噪声 故障 排查”。这一步可以用提示词让大模型做也可以用同义词词典做规则替换。我一般两者结合规则先处理高频口语词大模型兜底处理复杂句式。第二种查询扩展Query Expansion。给原查询补充同义词、近义词、上下位词。比如“液压泵”扩展出“液压油泵”“液压马达泵”“柱塞泵”。扩展的词不是随便加的要基于领域词表。工业场景里我建议维护一份领域同义词表比让大模型自由发挥靠谱得多。大模型容易扩展出“水泵”“气泵”这种跨领域的词反而引入噪声。第三种指代消解Coreference Resolution。把多轮对话里的代词还原成实体。这需要维护一个对话状态记录上一轮提到的实体。实现上可以用大模型做提示词里带上最近三轮的对话历史让它输出消解后的查询。注意指代消解要在查询改写之前做否则改写的查询里还是“那”“它”检索器照样懵。第四种查询分解Query Decomposition。把多跳问题拆成多个子查询分别检索后再合并。比如“和 E07 同系列的故障代码里哪个需要停机检修”拆成“E07 属于哪个系列”和“该系列需要停机检修的故障代码”。子查询可以并行检索也可以串行——串行时后一个查询依赖前一个的结果。我一般用串行因为多跳问题里后一跳往往需要前一跳的实体作为输入。2.3 查询增强的实操配置与避坑具体怎么落地我拿一个基于 LangChain 的项目举例。查询增强这一环我会单独封装成一个QueryEnhancer类内部按顺序调用四个处理器class QueryEnhancer: def __init__(self, llm, synonym_dict, history_window3): self.llm llm self.synonym_dict synonym_dict self.history_window history_window def enhance(self, query, history): # 1. 指代消解 query self._resolve_coreference(query, history) # 2. 查询改写 query self._rewrite(query) # 3. 查询扩展 query self._expand(query) # 4. 查询分解返回子查询列表 sub_queries self._decompose(query) return sub_queries指代消解的提示词我一般这么写“以下是最近三轮对话历史[history]。用户最新提问是[query]。请将提问中的代词如‘它’‘那’‘这个’替换为历史中对应的具体实体只输出替换后的提问不要解释。”实测下来这个提示词在中文场景下准确率能到 85% 以上。剩下的 15% 主要是历史里实体太多、模型选错的情况可以在提示词里加上“如果有多个候选实体选择最近一次提到的”。查询扩展这一步我强烈建议不要完全交给大模型。我踩过的坑是让大模型自由扩展“液压泵”它给我扩展出“水泵”“气泵”“油泵”前两个直接把检索带偏了。后来改成“大模型生成候选 领域词表过滤”只保留词表里存在的扩展词噪声立刻降下来。领域词表可以人工维护也可以从知识库的文档标题、关键词字段里自动抽取。查询分解有个细节要注意不是所有查询都需要分解。如果每个查询都拆简单问题会被拆成多个子查询检索次数翻倍延迟上去了效果却没提升。我的做法是先用一个轻量分类器判断“是否需要多跳”只有多跳问题才走分解流程。分类器可以用规则查询里是否含“和……同系列”“哪个”“分别”等词也可以用大模型做二分类。注意查询增强会引入额外的大模型调用延迟会增加 200-500ms。如果业务对延迟敏感可以把查询改写和扩展做成异步或者用更小的模型如 7B 级别来做。指代消解和查询分解对模型能力要求较高不建议用小模型。3. 双路召回向量库与搜索引擎的分工与协作3.1 向量检索的强项与软肋向量检索的原理是把查询和文档都映射到同一个高维向量空间用余弦相似度或内积衡量距离。它的强项是语义泛化用户问“泵不转了”文档写“液压泵无法启动”两者字面不重合但向量空间里距离很近能召回。这种“意会”能力是关键词检索做不到的。但向量检索有三个软肋。第一精确匹配弱。前面说的故障代码 E07 和 E10向量几乎一样分不开。第二对嵌入模型依赖极强。换一个嵌入模型召回结果可能天差地别。通用嵌入模型在垂直领域如医疗、法律、工业表现往往不如领域微调过的模型。第三长文档切块后语义稀释。一份 50 页的手册切成 500 字一块每块只承载局部语义查询和某一块的向量相似度可能不高但那一块恰恰包含答案。我实测过一个工业知识库纯向量检索的 Top-10 召回率只有 62%。也就是说10 个问题里有将近 4 个正确答案根本没进候选集。这个数字在业务场景里是不可接受的。3.2 关键词检索的精确性与局限关键词检索典型代表是 BM25 算法。它基于词频和逆文档频率打分查询里的词在文档里出现得越多、越稀有得分越高。它的强项是精确匹配查“E07”含“E07”的文档一定排前面查“XX 型号”含这个型号的文档不会被漏掉。BM25 的局限也很明显。第一不懂同义词。用户查“液压泵”文档写“液压油泵”BM25 认为这是两个不同的词召回不了。第二对词序不敏感。“泵液压”和“液压泵”在 BM25 眼里差不多但语义可能完全不同。第三短查询效果差。用户只输入“E07”BM25 只能靠这一个词打分区分度不够。但正是这些“局限”让 BM25 在精确匹配场景下反而成了优势。它不会像向量检索那样“自作聪明”地把 E07 和 E10 混为一谈。所以混合检索的思路不是让 BM25 去补向量的语义短板而是让 BM25 守住精确匹配的底线向量检索负责语义泛化两者各司其职。3.3 双路召回的工程实现工程上双路召回有两种实现方式。第一种是“并联”查询同时发给向量库和搜索引擎两路各自返回 Top-K然后在融合层合并。第二种是“串联”先用关键词检索粗筛再用向量检索精排或者反过来。我推荐并联因为串联会引入顺序依赖前一路的偏差会传导到后一路而且并联可以并行执行延迟更低。并联的架构里向量库我一般选 Milvus、Qdrant 或 pgvector。Milvus 适合大规模千万级以上Qdrant 轻量且过滤功能强pgvector 适合已经在用 PostgreSQL 的团队省一个组件。搜索引擎我选 Elasticsearch 或 OpenSearch它们内置 BM25还支持自定义分词器中文场景下配一个 IK 分词器就能用。两路的 Top-K 怎么定我的经验是向量路取 20关键词路取 20融合后候选池 40 条左右。为什么是 20 而不是 10因为重排模型需要足够的候选才能挑出真正相关的。如果只取 10重排的输入池太小精排的收益发挥不出来。但也不能太大40 条候选做交叉编码重排延迟已经到 300-500ms 了再大就影响体验。中文分词是关键词检索的关键。IK 分词器有两种模式ik_smart粗粒度ik_max_word细粒度。我一般用ik_max_word建索引用ik_smart查询。原因是建索引时细粒度能覆盖更多词组合查询时粗粒度避免把“液压泵”拆成“液压”和“泵”导致匹配过散。这个配置在 Elasticsearch 的 mapping 里设置{ settings: { analysis: { analyzer: { ik_max_word_analyzer: { type: custom, tokenizer: ik_max_word } } } }, mappings: { properties: { content: { type: text, analyzer: ik_max_word_analyzer, search_analyzer: ik_smart } } } }提示向量库和搜索引擎的文档 ID 必须对齐。我一般用同一个doc_id作为两边的主键融合时按doc_id去重。如果两边 ID 体系不一致融合层会非常痛苦。4. 结果融合与重排把“对的那条”顶上来4.1 融合策略RRF 为什么比加权求和更稳两路召回各返回 20 条怎么合并成一个候选池最直觉的做法是加权求和给向量得分和 BM25 得分各配一个权重加起来排序。但这里有个坑——两路的分数不在同一个量纲上。向量相似度是 0 到 1 之间的余弦值BM25 得分可能是 0 到 30 之间的任意实数。直接加权求和BM25 会碾压向量得分权重调起来极其痛苦。我推荐用RRFReciprocal Rank Fusion倒数排名融合。它的公式很简单RRF_score(d) Σ 1 / (k rank_i(d))其中rank_i(d)是文档 d 在第 i 路召回里的排名k是一个平滑常数通常取 60。RRF 只看排名不看原始分数所以天然规避了量纲问题。两路里都排前面的文档RRF 得分最高只在一路里排前面的得分次之。这个策略在工业场景里实测比加权求和稳定得多几乎不需要调参。RRF 的k值怎么选k越大排名差异被平滑得越厉害头部文档的优势越小。k60是文献里的经典值我实测下来在大多数场景都够用。如果业务特别看重头部精度可以调到 30如果希望候选池更分散可以调到 100。但说实话这个参数的影响远不如重排模型大不用太纠结。4.2 重排模型交叉编码器为什么比双塔强融合后的 40 条候选顺序还是粗排的结果不够准。这时候需要重排模型做精排。重排模型分两类双塔模型Bi-Encoder和交叉编码器Cross-Encoder。双塔模型就是向量检索用的那种查询和文档分别编码最后算相似度。它的优点是快文档向量可以预计算查询时只算查询向量。但缺点是查询和文档在编码阶段没有交互语义匹配的精度有限。交叉编码器把查询和文档拼在一起送进模型做一次完整的前向计算输出一个相关性分数。因为查询和文档在模型内部充分交互精度比双塔高一大截。代价是慢——40 条候选要算 40 次前向没法预计算。但 40 条的量级用 GPU 跑一次也就 100-200ms完全可以接受。我常用的重排模型是 BGE-Reranker 系列如bge-reranker-v2-m3和 Cohere Rerank。BGE-Reranker 开源、中文效果好适合私有化部署Cohere Rerank 是 API 调用省事但数据要出域。工业场景我一般选 BGE-Reranker因为数据敏感不能出内网。重排的 Top-N 怎么定我一般取Top-5 到 Top-8喂给大模型。为什么不是 Top-3因为大模型的上下文窗口现在都很大32K、128K多塞几条文档成本不高但能显著降低“答案在候选里但没被选中”的概率。我实测过Top-5 和 Top-8 的答案准确率差距在 3-5 个百分点但 Top-8 的上下文长度只增加了不到 2000 token性价比很高。4.3 重排后的生成环节上下文怎么组织重排选出 Top-N 后要把这些文档组织成提示词喂给大模型生成答案。这里有个细节文档的顺序很重要。大模型对上下文开头和结尾的内容注意力更强“迷失在中间”现象。所以我会把重排得分最高的文档放在最前面第二高的放在最后面中间的按得分降序排。这样头部和尾部都是高相关文档中间即使有噪声影响也小。提示词模板我一般这么写你是一个严谨的知识库问答助手。请仅基于以下参考资料回答问题不要编造资料中没有的信息。如果资料中没有答案请明确说“根据现有资料无法回答”。 参考资料 [1] {doc_1} [2] {doc_2} ... 用户问题{query} 请给出答案并在答案末尾标注引用的资料编号。“仅基于参考资料”这句话很关键。不加这句大模型会用自己的预训练知识“脑补”幻觉率飙升。加了之后实测幻觉率能降一半以上。要求标注引用编号是为了让用户能溯源也方便我们排查“答案错是因为检索错还是生成错”。5. 常见问题与排查技巧实录5.1 召回率上不去先查这三个地方问题一向量模型和领域不匹配。通用嵌入模型在垂直领域召回率低是常态。排查方法拿 50 个典型查询人工标注正确答案算纯向量检索的 Recall20。如果低于 70%基本可以确定是嵌入模型的问题。解决办法是用领域数据微调嵌入模型或者换一个在该领域表现更好的模型。微调需要标注数据成本高换模型成本低可以先试。问题二文档切块策略不合理。切块太大语义稀释切块太小上下文丢失。我一般按语义切块而不是固定字数。具体做法是先用规则按标题、段落切再用滑动窗口合并相邻小块保证每块 300-800 字。工业手册这种结构化文档按章节切效果最好。问题三关键词检索的分词器没配对。中文场景下用默认分词器按字切效果很差。必须配 IK 或 jieba 分词器。排查方法拿一个含专有名词的查询看 ES 的_analyze接口返回的分词结果。如果“液压泵”被切成“液”“压”“泵”说明分词器没配对。5.2 重排后精度反而下降这种情况我遇到过两次。第一次是因为重排模型的训练域和业务域不匹配。用通用重排模型去排工业文档它可能把“液压泵保养”排在“液压泵故障”前面因为前者在通用语料里更常见。解决办法是换一个在工业语料上微调过的重排模型或者自己标注一批数据微调。第二次是因为候选池里根本没有正确答案。重排只能在候选池里排序如果双路召回都没召回正确答案重排再强也没用。排查方法在融合后的候选池里人工找一下正确答案在不在。如果不在问题出在召回环节要回去调召回策略而不是怪重排。5.3 延迟太高怎么优化混合检索 RAG 的延迟主要来自四块查询增强的大模型调用、双路召回、重排、生成。我实测过一个中等规模知识库10 万文档各环节延迟大致如下环节延迟范围优化手段查询增强200-500ms用小模型、异步、缓存高频查询向量召回50-150ms建 HNSW 索引、减少 Top-K关键词召回30-100ms优化 ES 查询、加缓存融合10ms无重排100-300ms用 GPU、减少候选数生成500-2000ms用流式输出、小模型总延迟在 1-3 秒之间。如果业务要求 1 秒内优化重点在生成环节——用流式输出让用户先看到部分答案感知延迟会低很多。查询增强可以缓存相同或相似的查询直接复用增强结果省掉大模型调用。注意不要为了降延迟而砍掉重排。我试过砍掉重排直接生成答案准确率掉了 15 个百分点。重排是性价比最高的一环宁可砍查询增强也别砍重排。5.4 常见问题速查表现象可能原因排查方法解决方向专有名词查不到向量检索精确匹配弱看关键词路是否召回提高关键词路权重同义词查不到关键词检索不懂同义词看向量路是否召回加同义词扩展答案含无关内容重排精度不够看重排 Top-5 是否相关换重排模型或微调多轮对话答非所问指代未消解看增强后查询是否含代词加指代消解延迟超过 3 秒生成环节慢分环节打点流式输出、小模型幻觉严重提示词未约束看提示词是否含“仅基于资料”加约束、要求引用6. 我踩过的坑和几条实在建议第一个坑是过度依赖框架默认配置。LangChain 的EnsembleRetriever开箱即用但默认的融合策略是加权求和权重还得自己调。我一开始直接用它调了一周权重都没调好后来换成自己实现 RRF半天就稳定了。框架是脚手架不是黑盒关键环节该自己写就自己写。第二个坑是忽略文档预处理。知识库里的 PDF、Word 文档直接解析出来往往带一堆页眉页脚、乱码、表格错位。这些噪声会污染向量和关键词索引。我现在的流程是解析后先做清洗去页眉页脚、合并断行、表格转 Markdown再做切块。清洗这一步花的时间后面会以召回率的形式还回来。第三个坑是不做评估就上线。RAG 系统没有“感觉差不多”这回事。我建议至少准备 100 个问答对覆盖高频问题和边界情况每次改动都跑一遍评估看 Recall20、MRR、答案准确率三个指标。没有评估调参就是盲人摸象。最后分享一个实用技巧把重排得分作为置信度。如果重排 Top-1 的得分低于某个阈值比如 0.3说明知识库里可能没有相关内容这时候让大模型直接回答“根据现有资料无法回答”比强行生成一个错误答案要好。这个阈值需要在评估集上标定不同重排模型的分数分布不一样不能照搬。这套混合检索 RAG 链路我从最初的单路向量检索到加关键词召回再到加重排和查询增强前后迭代了大概四个月。每一步的收益都很实在加关键词召回召回率从 62% 提到 81%加重排答案准确率从 68% 提到 85%加查询增强多轮对话准确率从 55% 提到 78%。这些数字不是理论值是在真实业务评估集上跑出来的。如果你正在做类似的事希望这些经验能帮你少走点弯路。