ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

混合检索实战:向量与BM25双路召回及RRF融合重排全解析

混合检索实战:向量与BM25双路召回及RRF融合重排全解析 1. 为什么单路向量检索在真实业务里总差一口气做过 RAG 项目的人大概都有过这种体验Demo 阶段用几百条文档跑得挺顺一上真实业务、文档量级到几万几十万问题就全冒出来了。用户问XX 系统的超时重试策略是怎么配的向量检索返回的却是XX 系统整体架构概述语义上确实相关但用户要的那个精确参数根本没被召回。这不是 embedding 模型不行而是单路稠密向量检索本身的机制局限。稠密向量检索的本质是把 query 和文档都压到一个高维语义空间里算余弦相似度。它擅长的是意思相近比如你问怎么让程序跑得快一点它能召回性能优化实践这种文档。但它天然不擅长精确匹配——产品型号、错误码、函数名、专有名词、缩写这些 token 在 embedding 过程中权重会被稀释。你问ERR_5023 怎么解决向量检索很可能给你返回一堆常见错误处理的泛泛之谈而真正包含ERR_5023这个字符串的那篇排错手册反而排在后面。反过来传统关键词检索BM25、TF-IDF 这一类恰好相反。它对精确 token 极其敏感ERR_5023出现在文档里就能拿到高分但它完全不懂语义。用户问程序跑得慢怎么办文档里写的是性能瓶颈分析与调优字面一个词都不重叠BM25 直接抓瞎。所以真实业务里没有任何单路召回能同时覆盖语义泛化和精确命中这两类需求。这就是混合检索Hybrid Retrieval要解决的核心问题让向量库负责语义召回让搜索引擎负责关键词召回两路结果合并后再做重排把真正该排第一的文档顶上来。这篇内容我会把整条链路拆开讲查询怎么增强、双路怎么召回、结果怎么融合、重排怎么选型、上线后怎么调。适合已经跑通过基础 RAG、但召回率卡在瓶颈上不去的同学。如果你还在纠结RAG 知识库能不能存图片这种入门问题建议先把基础链路跑通再回来看。2. 查询增强别急着召回先把用户的问题翻译对很多人一上来就调召回参数其实召回质量的上限在查询进入检索器之前就已经被决定了。用户输入的那句话往往口语化、有指代、缺上下文直接拿去检索两路召回都会受影响。查询增强Query Enhancement就是在这个环节做文章。2.1 查询改写把口语问题变成检索友好的表达最基础的一步是查询改写Query Rewriting。用户问那个登录老是失败咋整直接检索效果很差因为那个咋整这些词对检索毫无贡献。改写目标是把 query 变成信息密度更高的形式比如用户登录失败 常见原因 排查。实操上有两种做法。一种是用 LLM 做改写prompt 大致是将以下用户问题改写为适合检索的查询语句保留关键实体和意图去掉口语化表达。另一种是规则同义词词典成本低但覆盖面有限。我一般建议先用 LLM 改写同时保留原始 query因为改写有可能丢信息两路都留着更稳。这里有个坑要提醒改写不是越标准越好。如果你把ERR_5023改写成错误码 5023 相关错误反而破坏了精确匹配需要的 token。所以改写 prompt 里一定要强调保留所有专有名词、错误码、型号、函数名原样不动。2.2 多查询扩展一个问题的多种问法单一改写还是可能漏召回因为同一个意图有很多种表达。多查询扩展Multi-Query的思路是让 LLM 针对原问题生成 3 到 5 个不同角度的查询每个查询都去召回一遍最后合并结果。比如原问题如何配置数据库连接池可以扩展出数据库连接池 配置参数connection pool 最大连接数 设置数据源连接池 超时配置这几个查询分别命中不同表述的文档合并后召回覆盖面明显变宽。代价是检索次数翻倍延迟上升。我的经验是扩展 3 个查询是性价比拐点再多边际收益递减延迟却线性增长。2.3 HyDE用假答案去检索真文档HyDEHypothetical Document Embeddings是个反直觉但很有效的技巧。它的做法是先让 LLM 针对问题编一个假设性的答案然后拿这个假答案去做向量检索而不是拿原问题。为什么有效因为问题和答案在语义空间里的分布是不一样的。用户问连接池满了怎么办这是疑问句而文档里写的是当连接池达到上限时可以调整 maxActive 参数……这是陈述句。疑问句和陈述句的 embedding 距离可能比想象中远。而 LLM 生成的假答案形态上更接近真实文档检索时自然更容易命中。HyDE 的注意事项是假答案可能包含幻觉但这不重要因为我们只用它来指路不用它的内容。真正返回给用户的是检索到的真实文档。不过 HyDE 会额外增加一次 LLM 调用延迟敏感的场景要权衡。2.4 查询增强的取舍不是全都上把上面几种全叠加延迟会爆炸。我的建议是按场景分层增强手段延迟增量召回提升适用场景查询改写低中几乎所有场景必做多查询扩展中高召回率优先延迟可接受HyDE中高中高问答型 query 为主指代消解低中多轮对话场景必做多轮对话场景还要额外做指代消解把它这个替换成上文的具体实体否则检索器根本不知道你在问什么。这一步经常被忽略但在客服、助手类应用里是刚需。3. 双路召回向量库和搜索引擎各自的分工边界查询准备好之后进入召回环节。混合检索的核心就是两路并行召回一路走向量库做语义检索一路走搜索引擎做关键词检索。这两路不是简单叠加而是各有明确分工。3.1 向量路稠密检索负责意思对得上向量路用 embedding 模型把 query 编码成向量在向量库里做近似最近邻搜索ANN。主流向量库有 Milvus、Qdrant、Weaviate、pgvector 等选型时重点看索引类型HNSW、IVF、过滤能力、以及和现有技术栈的契合度。向量路的关键参数是top_k。这个值设太小会漏召回设太大则把噪声灌进重排环节。我的经验是向量路 top_k 设 20 到 50因为后面还有重排兜底宁可多召回一些让重排去筛。embedding 模型的选择也很关键。中文场景下BGE、M3E、GTE 这几个系列都经过大量验证。要注意的是query 和 document 可能要用不同的编码方式很多模型比如 BGE提供了专门的 query instruction用错了会明显掉点。这个细节文档里经常一笔带过但实测影响不小。3.2 关键词路BM25 负责字面对得上关键词路用 BM25 或类似的稀疏检索算法。BM25 的核心思想是一个词在文档里出现次数越多、在所有文档里越罕见它的权重就越高。所以像ERR_5023这种罕见 token 能拿到极高权重精确命中。实现上可以用 Elasticsearch、OpenSearch 这类成熟搜索引擎也可以用轻量的 BM25 库比如 rank_bm25、bm25s。如果文档量不大几万条以内轻量库完全够用省去维护 ES 集群的成本。文档量上去了、需要分布式和复杂过滤再上 ES。BM25 有两个经典参数k1 控制词频饱和度b 控制文档长度归一化。默认 k11.2、b0.75 在多数场景够用但如果你的文档长度差异极大比如有的几百字有的几万字b 值得调一调否则长文档会因为词频高而被过度加权。3.3 中文分词关键词路的隐形杀手中文场景下BM25 的效果高度依赖分词质量。用默认的按字切分连接池会被切成连、接、池语义完全丢失。所以必须上专业分词器jieba、HanLP、IK 都是常见选择。但分词也有坑专有名词容易被切碎。比如向量数据库可能被切成向量数据库而用户搜的是向量数据库这个整体。解决办法是维护自定义词典把业务里的专有名词、产品名、术语都加进去。这个词典需要持续维护是关键词路效果的关键保障。另一个选择是字符级 n-gram比如 bigram。它不依赖分词对未登录词更鲁棒代价是索引膨胀、精确度略降。如果业务里新词、专有名词特别多n-gram 反而更省心。3.4 两路怎么并行工程上的细节两路召回要并行执行否则延迟叠加。用异步调用Python 的 asyncio、Java 的 CompletableFuture同时发起向量检索和关键词检索等两路都返回后再合并。这里有个容易忽略的点两路的超时和降级策略要独立。如果向量库挂了关键词路还能兜底返回结果反之亦然。我见过不少项目两路耦合在一起一路抖动整个检索就崩了。生产环境一定要做单路降级保证可用性。4. 结果融合RRF 为什么比加权求和更省心两路召回各自返回一个有序列表现在要把它们合并成一个列表。这一步叫结果融合Fusion做法直接决定最终召回质量。4.1 加权求和的老问题分数不可比最直觉的做法是加权求和final_score α * 向量分数 β * BM25分数。但这个做法有个致命问题——两路的分数根本不在一个量纲上。向量相似度通常在 0 到 1 之间而 BM25 分数可能是 0 到几十甚至上百取决于语料。你没法直接相加归一化又会引入新的偏差而且归一化方式对结果影响很大。更麻烦的是α 和 β 这两个权重需要针对每个业务调文档分布一变就得重调维护成本极高。4.2 RRF只看排名不看分数RRFReciprocal Rank Fusion倒数排名融合绕开了分数不可比的问题它只用排名不用分数。公式很简单RRF_score(d) Σ 1 / (k rank_i(d))其中rank_i(d)是文档 d 在第 i 路召回里的排名k 是个平滑常数通常取 60。一个文档如果在两路里都排得靠前它的 RRF 分数就会叠加得很高只在单路靠前的分数就低一些。RRF 的好处是无需调权重、无需归一化、对分数尺度不敏感。实测下来RRF 在大多数场景下能直接干过精心调参的加权求和而且几乎零维护。这也是为什么现在主流 RAG 框架LangChain、LlamaIndex的混合检索默认都用 RRF。4.3 k 值怎么选60 不是随便定的RRF 里的 k 值控制排名靠前的优势有多大。k 越小头部排名的权重越突出k 越大各排名之间的差距越平缓。k60 是原论文推荐的默认值实践中也基本够用。但如果你的场景特别看重头部精确命中比如错误码查询可以把 k 调小到 10 到 20让排第一的文档优势更明显。反过来如果两路召回质量都不太稳定想让结果更平均可以把 k 调大。这个参数值得在验证集上试几组。4.4 融合后的截断给重排留多少候选融合完会得到一个较长的列表接下来要交给重排。这里要决定截断到多少条。截太多重排没得选截太少延迟高且重排模型可能被噪声干扰。我的经验是融合后保留 30 到 50 条进重排重排后取 top 3 到 5 条给 LLM。这个比例在延迟和质量之间比较平衡。如果重排模型很强比如 cross-encoder可以适当多喂一些候选。5. 重排把真正该排第一的文档顶上来召回和融合解决的是文档在不在候选集里重排Rerank解决的是候选集里谁排第一。这两件事的难度完全不同用的模型也不一样。5.1 为什么召回模型做不好精排召回阶段用的双塔模型bi-encoderquery 和 document 是分开编码的各自算好向量再算相似度。这样做的好处是 document 向量可以离线预计算检索快。但代价是 query 和 document 之间没有交互模型看不到两者之间的细粒度匹配关系。重排用的 cross-encoder 则把 query 和 document拼在一起送进模型让它们充分交互。这样能捕捉到这个词是否真的对应那个词这种细粒度信号精度高得多。代价是没法预计算每条候选都要实时跑一遍所以只能用在候选集较小的精排阶段。5.2 重排模型选型BGE Reranker 是当前性价比之选中文场景下BGE Reranker 系列bge-reranker-base、large、v2-m3是目前用得最多的。base 版速度快large 版精度高v2-m3 支持多语言和更长输入。选型时主要看延迟预算和精度要求。如果延迟极其敏感可以考虑更小的模型或者蒸馏版如果精度优先且能接受 GPU 成本large 版值得上。还有一些商业重排 API比如 Cohere Rerank效果稳定但按调用量计费量大时成本要算清楚。5.3 重排的输入长度截断策略影响很大cross-encoder 对输入长度有限制通常 512 token。如果文档很长直接截断会丢信息。常见做法是按段落切分后分别打分取最高分作为文档分数或者把文档切成多个 chunk 分别重排。这里有个实操细节重排时用的文本片段最好和召回时用的 chunk 保持一致。如果召回用的是 512 字的 chunk重排却拿整篇文档去打分两者信号会对不上。保持粒度一致重排效果更稳。5.4 重排不是万能的什么时候可以省重排能显著提升精度但会增加延迟和成本。如果满足以下条件可以考虑先不上重排文档量很小几千条以内召回本身就很准延迟要求极严比如 100ms 以内预算有限跑不起 GPU但只要文档量上去了、召回开始出现相关但不精确的问题重排基本是必选项。我个人的经验是重排带来的精度提升往往比换更强的 embedding 模型更明显性价比很高。6. 全链路串起来一次完整请求的流转把前面几块拼起来一次完整的混合检索 RAG 请求大致是这样流转的查询增强接收用户 query做指代消解多轮场景、查询改写可选做多查询扩展或 HyDE得到 1 到 N 个检索 query。双路并行召回每个 query 同时发起向量检索和 BM25 检索各自返回 top_k 结果。两路独立超时、独立降级。结果融合用 RRF 把两路结果合并成一个有序列表截断到 30 到 50 条。重排cross-encoder 对候选逐条打分重新排序取 top 3 到 5 条。生成把重排后的文档拼进 prompt交给 LLM 生成答案。这条链路里每一步都有可调参数但不要一次全调。我的建议是先用默认参数跑通建立评估集然后按召回率→融合→重排的顺序逐个优化每次只动一个变量否则根本不知道是哪个改动起了作用。7. 上线后怎么调评估、监控与常见坑链路跑通只是开始真正难的是上线后持续调优。7.1 先建评估集再谈优化没有评估集的调参就是瞎调。评估集不需要很大100 到 200 条真实 query 配上标注的相关文档就够用。标注时至少标出必须召回的文档用于算召回率和应该排第一的文档用于算 MRR、NDCG。评估指标重点关注三个Recallk召回率看候选集全不全、MRR第一个相关文档的排名看头部准不准、NDCG整体排序质量。召回率低就查召回环节MRR 低就查重排环节分工明确。7.2 监控线上真实 query 的分布漂移线上 query 的分布会随时间漂移新词、新业务、新问法不断出现。要监控低分召回的比例和用户反馈点赞/点踩/追问一旦发现某类 query 召回质量下降就要回头补词典、补文档或调参数。关键词路的自定义词典尤其需要持续维护。我一般会定期把线上高频 query 里的新词捞出来人工确认后加进词典。这个工作琐碎但回报很高。7.3 几个我踩过的坑坑一两路 top_k 设成一样。向量路和关键词路的召回特性不同top_k 不该一刀切。向量路可以多召回一些语义噪声多但覆盖面广关键词路可以少一些精确但覆盖窄。我一般向量路 30、关键词路 20。坑二RRF 之后又做了一次归一化。RRF 分数本身就没有绝对意义只用于排序再做归一化纯属画蛇添足还可能引入偏差。融合后直接按分数排序截断就行。坑三重排模型和 embedding 模型不匹配。有些 embedding 模型训练时用的文本格式和重排模型不一致导致重排效果打折。尽量选同一系列或经过验证的组合比如 BGE embedding BGE reranker。坑四忽略 chunk 策略对召回的影响。chunk 切得太碎语义不完整切得太大噪声多。混合检索场景下chunk 大小还会影响 BM25 的文档长度归一化。我一般用 300 到 500 字、带重叠的切法具体要结合文档类型调。坑五把重排当召回用。有人图省事直接拿 cross-encoder 对全库文档打分跳过召回。这在文档量小时能跑量一大延迟直接爆炸。重排永远只用在候选集上这是它的定位。7.4 延迟优化的几个方向混合检索链路长延迟容易超标。优化方向按性价比排序并行化双路召回并行、多查询并行这是最直接的。缓存高频 query 的召回结果可以缓存命中率高时效果显著。模型量化/蒸馏embedding 和重排模型都可以量化精度损失可控。候选集裁剪融合后少喂一些给重排延迟立降。异步生成重排一完成就开始流式生成别等全部拼好再调 LLM。我个人在实际项目里的体会是混合检索的收益在文档量大、query 类型杂的场景下最明显。如果文档就几百条、query 也很规整单路向量检索可能就够了硬上混合检索反而增加复杂度。技术选型永远要看场景别为了先进而先进。这套链路我前后在几个项目里落地过每次调优的重点都不一样但先建评估集、再逐个变量调这个原则从没变过。
返回列表