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_window=3): 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 会碾压向量得分,权重调起来极其痛苦。
我推荐用RRF(Reciprocal Rank Fusion,倒数排名融合)。它的公式很简单:
RRF_score(d) = Σ 1 / (k + rank_i(d))其中rank_i(d)是文档 d 在第 i 路召回里的排名,k是一个平滑常数,通常取 60。RRF 只看排名,不看原始分数,所以天然规避了量纲问题。两路里都排前面的文档,RRF 得分最高;只在一路里排前面的,得分次之。这个策略在工业场景里实测比加权求和稳定得多,几乎不需要调参。
RRF 的k值怎么选?k越大,排名差异被平滑得越厉害,头部文档的优势越小。k=60是文献里的经典值,我实测下来在大多数场景都够用。如果业务特别看重头部精度,可以调到 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 个典型查询,人工标注正确答案,算纯向量检索的 Recall@20。如果低于 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 个问答对,覆盖高频问题和边界情况,每次改动都跑一遍评估,看 Recall@20、MRR、答案准确率三个指标。没有评估,调参就是盲人摸象。
最后分享一个实用技巧:把重排得分作为置信度。如果重排 Top-1 的得分低于某个阈值(比如 0.3),说明知识库里可能没有相关内容,这时候让大模型直接回答“根据现有资料无法回答”,比强行生成一个错误答案要好。这个阈值需要在评估集上标定,不同重排模型的分数分布不一样,不能照搬。
这套混合检索 RAG 链路,我从最初的单路向量检索,到加关键词召回,再到加重排和查询增强,前后迭代了大概四个月。每一步的收益都很实在:加关键词召回,召回率从 62% 提到 81%;加重排,答案准确率从 68% 提到 85%;加查询增强,多轮对话准确率从 55% 提到 78%。这些数字不是理论值,是在真实业务评估集上跑出来的。如果你正在做类似的事,希望这些经验能帮你少走点弯路。