面试知识库
高 进阶

混合检索与RRF融合#

一句话答案#

混合检索 = 稀疏检索(BM25 / 学习型稀疏)管字面精确匹配 + 稠密向量管语义匹配,多路各自召回后按排名融合(RRF:Σ 1/(k+rank),k 常取 60),或按归一化后的分数加权融合;RRF 不看分数尺度,所以不用调归一化,是默认首选。

核心要点

RAG分块与召回策略 已经给出「BM25 + 向量 → RRF → Cross-Encoder」这条链路和 RRF 公式,本篇讲每一环的机制:BM25 为什么这样打分、稀疏/稠密/多向量各自的表示方式、RRF 的 k 到底在调什么、各家数据库怎么实现、多路召回的去重和配额。

1. BM25:三个因子各管一件事#

score(D, Q) = Σ_{q∈Q} IDF(q) · tf(q,D)·(k1+1) / ( tf(q,D) + k1·(1 − b + b·|D|/avgdl) )
IDF(q)(Lucene 版)= ln( 1 + (N − n(q) + 0.5) / (n(q) + 0.5) )
plaintext
因子管什么机制
TF 饱和(k1)词出现多次加分,但有上限tf→∞ 时该项趋近 k1+1;k1 越小越快饱和,k1=0 退化成「出现/不出现」二值
长度归一(b)长文档天然词多,要打折b=1 完全按 `
IDF越稀有的词越有区分度出现在几乎所有文档里的词 IDF 接近 0;1+ 保证 IDF 非负

ES 里 BM25 参数怎么调、function_score 怎么混业务分,见 ES查询DSL与评分机制;倒排结构见 倒排索引原理。

对 RAG 分块的影响:chunk 长度比较均匀时,b 的作用变小;chunk 里有大量模板化重复文本(页眉页脚、免责声明)时,这些词 IDF 很低,基本不干扰排序,但会拉高 avgdl。

2. 稀疏 vs 稠密:各自擅长什么#

维度稀疏(BM25 / 学习型稀疏)稠密(bi-encoder 向量)
表示词表维度的稀疏向量,非零项 = 出现的词固定维度(如 768/1024)稠密向量
强项订单号、型号、人名、代码标识符、罕见术语同义改写、口语化提问、跨语言
弱项同义词、换说法就匹配不上(词汇鸿沟)精确字符串、数字、没见过的新词;长文档被压成一个向量有信息损失
可解释性能看到哪个词贡献多少分基本不可解释
冷启动无需训练,分词器对就能用领域偏移时要微调,见 Embedding原理与选型

学习型稀疏(SPLADE 一类):用 Transformer 给每个词表维度学一个权重,能给文档「扩展」出原文没有但相关的词,仍然走倒排索引。它介于 BM25 和稠密向量之间。

3. BGE-M3 的三种输出#

BGE-M3 一个模型一次前向同时产出三种表示:

输出怎么来打分方式
dense[CLS] 位置的隐状态,归一化query 与文档向量点积
sparse(lexical weights)每个 token 的隐状态过一个线性层 + ReLU,得到该 token 的权重;同一 token 取最大query 与文档共同出现的 token 权重乘积求和,类似可学习的 BM25
multi-vector(ColBERT 式)保留每个 token 的向量Late Interaction:对 query 每个 token 取与文档所有 token 的最大相似度再求和(MaxSim)
MaxSim(q, d) = Σ_i  max_j  (q_i · d_j)
plaintext

多向量精度通常高于单向量,但存储量按 token 数放大,一般只在精排或小库里用。三路分数可以加权求和,权重要在自己的评测集上调,见 检索评测指标与离线评测。

4. 融合方法一:RRF(Reciprocal Rank Fusion)#

RRF(d) = Σ_{r ∈ 各路结果}  1 / (k + rank_r(d))      rank 从 1 开始;某路没召回 d,则这一路贡献 0
plaintext

为什么不直接把分数相加:

  • 尺度不同:BM25 无上界,受 query 词数和语料统计影响;余弦相似度在 [-1,1],实际分布常挤在很窄的区间。
  • 分布随 query 变:同一个 BM25 分数 12.3,对短 query 可能很高,对长 query 很一般。直接相加时,数值大的那一路会压过另一路。
  • RRF 只用名次,不用管这些问题,也不用标注数据去调权重。

k=60 在调什么:k 控制「头部名次」和「中部名次」的差距。

krank=1 贡献rank=10 贡献效果
01.0000.100第一名权重极大,一路的 top1 能压过另一路的多个名次
600.01640.0143曲线很平,被多路同时召回比在单路排得高更重要

k=60 来自 Cormack 等人 2009 年的 RRF 原论文,是经验值。k 越大越偏向「多路共识」,k 越小越相信各路的头部结果。

RRF 的代价:丢掉了分数里的「置信度」信息。一路 top1 的分数远高于 top2(非常确定),RRF 也只按名次算;某路整体质量很差时,RRF 默认也给它同样的话语权(可以给每路乘权重 w_r / (k + rank) 缓解)。

5. 融合方法二:分数归一化 + 加权#

fused(d) = α · norm(s_dense(d)) + (1 − α) · norm(s_sparse(d))
plaintext
归一化做法坑
min-max每个 query 的候选集内 (s−min)/(max−min)对离群值敏感;候选只有 1 条时分母为 0;某路没召回的文档怎么补分要约定
z-score(s−μ)/σ候选集小时 μ、σ 不稳
基于分布(如 Qdrant 的 DBSF)用均值 ± 3σ 作为上下界再归一同样依赖候选集分布

有标注集、能调 α 时,加权融合可能比 RRF 效果好,因为它保留了分数差距;没有评测集时先用 RRF。

6. 各家怎么实现混合检索#

具体参数名和支持版本以各自官方文档为准。

引擎稀疏侧融合方式
Elasticsearch原生 BM25;也支持学习型稀疏(ELSER 等)retriever 里的 rrf(rank_constant 即 k,默认 60;rank_window_size 即每路取多少参与融合;新版本可给子 retriever 加权重),或 linear retriever 做归一化加权;也可以 query + knn 同时写,分数按 boost 线性相加。许可档位以 Elastic 订阅页为准
OpenSearch原生 BM25;也支持神经稀疏检索hybrid 查询 + search pipeline:normalization-processor(min-max / L2 归一化 + 加权),2.19 起可用 score-ranker-processor 做 RRF
Qdrantnamed sparse vector(可配 IDF 修正,外部生成 BM25/SPLADE 权重)Query API:多个 prefetch 各自召回,外层 fusion: rrf 或 dbsf;RRF 可调 k 和每路权重;也可以外层再用多向量做重排
Milvussparse 向量字段;内置 BM25 全文检索函数(从文本字段自动生成稀疏向量)hybrid_search 传多个 AnnSearchRequest,用 RRFRanker(k) 或 WeightedRanker(w1, w2),新版本也可用 Function 形式定义 ranker
Weaviate内置 BM25Fhybrid 查询的 alpha 控制权重,fusionType 可选 rankedFusion(按名次)或 relativeScoreFusion(按归一化分数,1.24 起为默认)
pgvectorsparsevec 类型;关键词侧常用 PG 全文检索没有内置融合,在 SQL(CTE 里分别取名次再算 1/(k+rank))或应用层做 RRF

在应用层自己融合也很常见:两个库各查一次,Python 里跑上面的 rrf()。好处是能自由加路、加权、做配额;代价是多一次网络往返,而且每路取多少条要自己控制。

7. 多路召回:去重与配额#

flowchart LR
  Q[query] --> A[BM25 top-50]
  Q --> B[dense top-50]
  Q --> C[其他路 如标签/改写 query]
  A --> A2[路内按 chunk_id 去重]
  B --> B2[路内按 chunk_id 去重]
  C --> C2[路内按 chunk_id 去重]
  A2 --> E[RRF 融合]
  B2 --> E
  C2 --> E
  E --> F[同文档折叠 + 配额]
  F --> G[rerank 候选 top-20~50]
  • 去重键:优先用 chunk_id;同一内容被多次入库(多版本、转载)时,再加内容哈希或近似去重(SimHash/MinHash)。每一路内部要在融合之前去重,否则同一 chunk 在同一路里重复出现会被多算分;跨路的合并交给 RRF 本身,不要在融合前把各路合成一份去重,否则「被多路同时召回」的加分就没了。
  • 同文档折叠:多个 chunk 来自同一篇文档时,可以每篇只保留最高分的 N 个,避免一篇长文档占满上下文;需要全文时再用 Parent Document 扩展(见 RAG分块与召回策略)。
  • 每路取多少(融合窗口):窗口太小,某路排在 51 名的好文档没有机会被另一路「投票」救回来;窗口太大,尾部噪声进入候选,rerank 成本上升。常见做法是每路 top-50100 进融合,融合后 top-2050 进 rerank。
  • 配额 / 保底:某一路对特定 query 类型特别重要时(如 query 里有编号就强依赖 BM25),可以给该路保底名额:融合结果里至少保留它的 top-3。
  • 一路返回空:RRF 天然容忍(该路贡献 0);min-max 加权要特殊处理,否则会除以 0 或把另一路的分数整体抬高。
  • 精确条件不要当检索路:品牌、价格区间、平台这类硬约束放 filter,不作为一路打分参与融合。

面试回答(2分钟版)

混合检索就是把稀疏检索和稠密检索并起来用。稀疏这边主要是 BM25,它有三个因子:IDF 让稀有词更重要;TF 用 k1 做饱和,词出现十次和出现五次差别不大;再用 b 按文档长度打折,免得长文档占便宜。它擅长订单号、型号、专有名词这种要求精确匹配的场景。稠密检索是双塔向量,擅长同义改写和口语化提问,但对精确字符串和新词不敏感。BGE-M3 这类模型一次能同时出 dense、sparse lexical weights 和 ColBERT 式多向量三种表示。两路召回之后要融合,我默认用 RRF,每个文档的分数是各路 1/(k+名次) 求和,k 常取 60。它只看名次,不用管 BM25 分数没有上界、余弦分数挤在窄区间这种尺度问题,也不需要标注数据来调权重。k 越大,曲线越平,越奖励被多路同时召回的文档。如果有评测集,也可以用 min-max 归一化后加权,保留分数差距,但要处理离群值和某一路返回空的情况。工程上还要先按 chunk_id 去重再融合,做同文档折叠,控制每路进融合的窗口和进 rerank 的条数。另外,价格、平台、评分这类精确需求是硬约束,应该放进 filter,而不是作为一路打分参与融合。结合项目时可以讲:哪些场景用了混合检索、哪些只用单路加 filter,以及用什么数据做的决定。

追问与易错

追问方向:

  • “RRF 的 k 调大调小分别有什么效果?” → k 小时头部名次权重悬殊,更相信各路的 top1;k 大时 1/(k+rank) 曲线变平,被多路同时召回的文档胜出。k=60 是原论文的经验值,有评测集可以在 10~100 之间扫一遍。
  • “为什么不能把 BM25 分数和余弦分数直接相加?” → BM25 无上界且随 query 长度和语料变化,余弦在 [-1,1] 且分布很窄,直接相加时 BM25 那一路会压过向量。要么用 RRF 只看名次,要么先按 query 做归一化再加权。
  • “BM25 的 k1、b 设成 0 分别会怎样?” → k1=0 时 TF 项恒为 1,只看词是否出现,退化成加权的布尔匹配;b=0 时不做长度归一,长文档因为词多而占便宜。
  • “BGE-M3 的 sparse 输出和 BM25 有什么区别?” → 两者都只在 query 和文档共同出现的 token 上打分,但 BGE-M3 的每个 token 权重是模型学出来的(隐状态 → 线性层 → ReLU),不依赖语料统计的 IDF;它和 BM25 一样只在原文出现的 token 上打分、不做词扩展,跨语言匹配要靠 dense 那一路。BM25 无需训练、可解释性更好。
  • “ColBERT 多向量为什么准,为什么不用它做全库召回?” → 它保留每个 token 的向量,打分用 MaxSim 做 token 级对齐,信息损失比单向量少;但存储量按 token 数放大,全库召回的索引和计算成本高,一般用于精排或小库。
  • “min-max 归一化有什么坑?” → 它是按单个 query 的候选集算的:离群值会把其他分数压扁;候选只有 1 条时分母为 0;某路没召回的文档要约定记 0 还是记该路最低分,不同约定排序会不同。
  • “融合窗口(每路取多少条)怎么定?” → 窗口要够大,让一路排得靠后的文档有机会被另一路救回来,但窗口越大尾部噪声越多、rerank 越慢;一般每路 50~100 进融合,再用 Recall@N 在评测集上确认。
  • “Qdrant / Milvus / ES 的混合检索分别怎么写?” → Qdrant 用 Query API 的多个 prefetch 加外层 fusion(rrf/dbsf);Milvus 用 hybrid_search 加 RRFRanker 或 WeightedRanker;ES 用 retriever.rrf,参数 rank_constant 和 rank_window_size;OpenSearch 用 hybrid 查询加 search pipeline(归一化处理器或 2.19 起的 RRF 处理器);Weaviate 用 hybrid 的 alpha 和 fusionType。版本差异以官方文档为准。
  • “什么时候不需要混合检索?” → query 基本都是自然语言、语料里几乎没有编号或专有名词时,dense 单路可能够用;有硬约束(价格、平台)时应该用 filter,不应该加一路 BM25。结论要靠评测集比较来定。

易错点:

  • ❌ “RRF 一定比加权融合好” → RRF 的优势是不用调参、不怕尺度问题;有评测集能调权重时,加权融合可能更好,因为它保留了分数差距。
  • ❌ “先融合再去重” → 同一 chunk 在同一路里出现多次会被重复计分,应该每路内部先去重再融合;跨路不要预先合并去重,由 RRF 累加各路贡献。
  • ❌ “混合检索能补救分块问题” → 答案被切碎到多个 chunk 里时,哪一路都召回不全,要回到分块策略去改。