Appearance
第13章 Hybrid Search:向量召回与关键词召回的融合
"Fusion is not a compromise. It's a way to get properties neither parent had." — 稀疏与稠密融合的核心观念
本章要点
- Hybrid Search 不是"先 dense 后 BM25"的串联,是两路并行召回后的排序融合
- 四类主流融合方法:RRF(简单且强)、权重线性(可调但要标定)、ColBERT 重打分(精度上限)、纯 rerank(让 cross-encoder 做最终仲裁)
- RRF 是生产默认——无需标定、跨量纲稳健、代码 20 行
- 选型看三个因素:候选规模 / 是否有 rerank / 延迟预算
- 融合前各路独立好是前提——一路召回差到底、融合救不回来
13.1 为什么不是简单相加
已知 dense 和 BM25 各自产出 top-50 候选带分数。最朴素的融合:两个分数加一起取 top-k。问题:
- 分数不同量纲:BM25 分数无上限(典型 5-30)、cosine 相似度 [-1, 1]——直接加 BM25 淹没 cosine
- 分布偏移:同一个 query 下 BM25 分数的尺度和另一个 query 不同(term 稀有度差异)——跨 query 的绝对分数不可比
- 分数不校准:BM25=10 的 doc 不等于"相关性 = 10"、cosine=0.85 也不等于"相关 85%"——都是相对排名意义上的信号
融合方案的核心就在绕开这些坑。
13.2 RRF:最朴素也最强的方案
RRF(Reciprocal Rank Fusion,Cormack 2009):不看分数、只看排名。
text
RRF_score(doc) = Σ_{each ranker} 1 / (k + rank(doc))k 是常数(典型 60),rank 是该 doc 在某路召回里的位置。
直觉
排第 1 贡献 1/61、排第 2 贡献 1/62……排名越靠前贡献越大、衰减快。两路都排前的 doc 合计分数最高。
例子
query 查询两路召回:
| doc | dense rank | BM25 rank | RRF 分 (k=60) |
|---|---|---|---|
| A | 1 | 10 | 1/61 + 1/70 = 0.0306 |
| B | 5 | 1 | 1/65 + 1/61 = 0.0317 |
| C | 3 | 3 | 1/63 + 1/63 = 0.0317 |
| D | 1 | ∞ (未命中) | 1/61 = 0.0164 |
| E | ∞ | 1 | 1/61 = 0.0164 |
两路都能命中的 C 和 B 分数最高——两个独立信号达成共识的 doc 更可信。这正是 hybrid 的本质。
RRF 的工程优势
- 无标定:不用训练、不用业务数据调参,直接跑
- 跨量纲稳健:BM25 / cosine / ColBERT score 都能融合
- 代码极简:20 行 Python 搞定
- 效果强:多次公开 benchmark 里 RRF 接近或超过精调的线性加权
Elasticsearch 8.9+、Qdrant、Milvus 都内置了 RRF API。默认 k=60(来自 Cormack 原论文)、实践中 k ∈ [30, 80] 都稳。
RRF 的局限
- 丢失了信号强度:排第 1 和第 2 的 RRF 分数相差很小,但如果实际分数差巨大(0.95 vs 0.3),RRF 无法体现
- 两路贡献固定等权——如果一路明显更可信(domain-specific),RRF 没法调
多数场景 RRF 够用。追求极致质量或有明确权重信号时用下节的线性加权。
13.3 权重线性融合
思路:把各路分数归一化到同一量纲后加权求和。
text
score(doc) = α × norm(dense_score) + (1-α) × norm(BM25_score)归一化方法
- Min-Max:
(x - min) / (max - min)把分数压到 [0, 1]。简单但对异常值敏感 - Z-score:
(x - mean) / std转正态分布。稳健但需要统计量 - Softmax:
exp(x) / Σ exp(x)转概率分布。适合概率解释场景
生产常用 min-max per query——每个 query 下各路的 top-k 分数独立归一。
α 的选择
α = dense 权重(1-α = BM25 权重)。典型 α=0.5-0.7——dense 略重但 BM25 不忽略。
标定方法:在 gold set 上扫 α ∈ {0.3, 0.4, ..., 0.9},看哪个值 recall@10 最高。不同 domain 的最优 α 差别可以很大:
- 通用问答:α=0.6-0.7(dense 主导)
- 法律 / 医疗:α=0.4-0.5(BM25 更重)
- 代码检索:α=0.5-0.6(均衡)
线性融合 vs RRF 的选择
- 已有业务 gold set 能标定:线性融合精度可能略高 1-3 个点
- 没有 gold set / 追求稳健:RRF
生产推荐:MVP 阶段用 RRF 快速起步;业务成熟后做 A/B 对比线性融合、取胜者。
13.4 rerank 作为融合器:让 cross-encoder 做仲裁
更激进的方案:不显式融合,让 cross-encoder reranker(第 14 章)直接对两路 union 的候选重打分。
流程:
- Dense 召回 top-k1(如 50)
- BM25 召回 top-k2(如 50)
- Union 去重 → 候选集 top-100 左右
- Cross-encoder 对这 100 条 (query, chunk) 重新打分
- 取 top-k(如 5)
这种方式下 "融合" 其实是 rerank 阶段一次性完成 的——候选集只要足够宽就行、不用关心各路分数。
优点
- 逻辑简单:没有权重、没有 k 常数、没有归一化
- 精度上限最高:cross-encoder 看 (query, doc) 对全文、比任何静态融合更准
- 容错强:某一路召回差、另一路能补——rerank 从 union 里挑即可
缺点
- 延迟:rerank 100 个候选比 20 个多 5 倍耗时
- 成本:cross-encoder 推理不便宜
- rerank 必须有:没有 rerank 能力的系统走不通这条路
何时选这条路
- 对质量要求最高、延迟可接受(500ms+)的场景
- 已经有稳定 rerank 服务
- 融合权重难调、不想维护
13.5 ColBERT 重打分:精度上限
ColBERT v2(第 9 章提过)可以作为融合器——把 dense + BM25 的 union 候选用 ColBERT 的多向量相似度重打分。
ColBERT 的 late-interaction 捕捉到 query 每个 token 和 chunk 每个 token 的匹配关系——比 single-vector cosine 精细得多、比 cross-encoder 轻量。
BEIR 上 ColBERT v2 重打分比 RRF + 普通 rerank 高 1-3 个点。但:
- 需要在索引时存储 per-token 向量(存储膨胀 30-100×)
- 部署需要 ColBERT 推理引擎
- 生态不如 cross-encoder rerank 成熟
实操:超大规模 + 追极致质量的少数场景用;绝大多数团队用 RRF 或 rerank 仲裁就够了。
13.6 融合前的预处理:去重
两路召回会有同一个 doc 的重复出现——必须去重后再融合。去重维度:
- chunk_id 完全相同:必然去重
- 同 doc 的不同 chunk:保留各自、但 chunk 多的 doc 容易在融合里占便宜——生产通常限制"每个 doc 最多保留 3 chunk"
- 内容高度相似:用 minhash / simhash 检测、相似度 > 0.9 合并
去重发生在融合之前还是融合之后有讲究:
- 之前去重:融合只处理独立 chunk、排序干净
- 之后去重:保留每路独立信号、再综合——容错好
生产常见:同 chunk_id 之前去重(精确)、同 doc 多 chunk 之后聚合。
13.7 多路召回的一般化
Hybrid search 不必只有 dense + BM25 两路。生产 RAG 常见三路以上:
- Dense embedding
- BM25 / SPLADE
- Metadata filter(结构化查询走 SQL-like)
- Graph search(如果有实体图)
- Keyword alert(特殊术语专门召回)
N 路召回用 RRF 融合天然支持(Σ of N rankers)。每加一路候选集扩大、融合分数越鲁棒。但:
- 延迟增加(并行但有网络 overhead)
- 成本增加(每路都要维护独立索引)
实操:默认 2 路(dense + BM25)。需求明确时按场景加特定路,不要"先加上看看"。
13.8 实测:融合方案效果对比
某企业 FAQ RAG 在 gold set(500 条 QA)上的对比(典型数字、仅示意):
| 方案 | recall@10 | MRR | P99 延迟 |
|---|---|---|---|
| 仅 Dense | 0.76 | 0.54 | 25ms |
| 仅 BM25 | 0.71 | 0.51 | 15ms |
| RRF (50+50) | 0.85 | 0.62 | 35ms |
| 线性 (α=0.6) | 0.86 | 0.63 | 35ms |
| Union + rerank | 0.89 | 0.68 | 280ms |
| Union + ColBERT | 0.90 | 0.69 | 120ms |
观察:
- Hybrid(任一方案)比单路强 10+ 个点——融合收益远大于选哪种融合
- RRF 和线性差别不大、RRF 无需标定所以更推荐
- rerank 和 ColBERT 再多 4-5 点,成本是延迟翻 4-8 倍
取舍原则:先上 hybrid(RRF 就行)、再判断加不加 rerank。
13.9 RRF 的进阶:加权 RRF 和带 offset RRF
经典 RRF 每路等权,但生产里常需要 两路不等权——某路更可信时应该占主导。Weighted RRF(wRRF):
text
wRRF(doc) = Σ_i w_i / (k + rank_i(doc))w_i 是第 i 路的权重。例子:dense=0.6、BM25=0.4 时、wRRF 保留 RRF 的跨量纲稳健性、同时反映各路的可信度。
带 offset 的 RRF:1 / (k + rank + offset_i)——给特别可信的一路降低 k 让其分数更陡。这比 wRRF 更细——但调参复杂。多数场景用 wRRF 就够。
k 常数的调优
RRF 的 k=60 是 Cormack 原论文经验值。实际对 k 做扫描:
| k 值 | 特性 | 适用 |
|---|---|---|
| k=10 | 前几名权重极大、排名末尾几乎不贡献 | top-k 小、追求精度 |
| k=30 | 前 20 名有影响、之后迅速衰减 | 中间档、次常用 |
| k=60 | 标准选择、平滑衰减 | 通用 |
| k=100 | 衰减很平缓、各排名都有贡献 | 候选集大、权重分散 |
gold set 上扫 k ∈ {10, 30, 60, 100}、看 recall@k 峰值。典型最优 k 在 30-80 之间。
13.10 动态融合:按 query 类型切换
不同 query 适合不同融合权重。例子:
- 查询
"订单 OD-12345":包含订单号——BM25 权重应加大 - 查询
"怎么理解私有化部署":概念性问题——dense 权重应加大 - 查询
"某商品价格":结构化——metadata filter 优先
实现 query classifier(基于规则 or 小模型)将 query 打标、按标签选融合权重或路由:
python
def route(query):
if has_structured_id(query):
return {"bm25": 0.8, "dense": 0.2}
if is_conceptual(query):
return {"bm25": 0.3, "dense": 0.7}
return {"bm25": 0.4, "dense": 0.6} # default这种"自适应融合"比全局一个 α 强 2-5 个点。但增加一个组件(classifier)维护成本、需要业务稳定后做。
13.11 MMR:多样性重排
融合产出 top-k 后、还有一个常被忽视的问题——多样性。Rerank / 融合只管相关度、不管 top-k 内部是否过于相似。举例:问"企业版功能"、top-5 可能都是关于 SSO 的 chunk——其他功能(LDAP / 审计 / API 网关)根本没出现。
MMR(Maximal Marginal Relevance,Carbonell & Goldstein 1998)的解法:
text
MMR_score(d) = λ × rel(d, q) - (1-λ) × max_{d' ∈ selected} sim(d, d')选下一个 chunk 时、平衡两件事:
rel(d, q):和 query 的相关度max sim(d, d'):和已选 chunk 的最大相似度
相关但和已选 chunk 相似度高的——降权。λ 典型 0.5-0.8——偏向相关度但保留一定多样性。
MMR 适用场景
- 广覆盖查询:"企业版有什么功能"——需要多个独立功能点、不是同一功能重复讲
- 对比场景:"A 和 B 的区别"——要同时召回 A 和 B 的内容
- 研究式问题:"这个领域的主流方案有哪些"——需要多流派各自代表
不适用场景:单一事实查询("企业版 SSO 价格")——重复的正确答案反而是信号增强、不是冗余。
何时接入 MMR
生产 RAG 里 MMR 有三个可能位置:
- hybrid 融合之后 / rerank 之前:对融合 top-30 做 MMR、留 15 条送 rerank。不让 rerank 看到冗余候选
- rerank 之后 / packing 之前:rerank top-10 做 MMR 留 top-5。最常见位置
- 不用 MMR + rerank 精调:部分场景 rerank 训练时已考虑多样性、不需要额外 MMR
多数项目先上 rerank、看 badcase 里是否有"同质答案堆积"、再考虑加 MMR。不要过早优化。
13.12 融合里的常见坑
坑 1:两路 top-k 不等
dense 召回 50、BM25 召回 20——融合时两路排名量纲不同。对齐方法:要么两路都召回相同 k、要么 RRF 用完整排名(不是 top-k 裁剪后的)。
坑 2:分数归一化跨 query 污染
把所有 query 的分数一起 min-max——某 query 分数被另一 query 的极值"归一化"到误值。必须 per-query 归一化。
坑 3:BM25 分数 NaN
空召回或单 term 命中时 BM25 分数可能极端值。融合前过滤 NaN 和 inf。
坑 4:融合后 chunk 数量失衡
某路多的 chunk 在融合里占上风、另一路的独特信号被稀释。限制每路最多贡献 X 条候选后再融合。
坑 5:融合分数当相关度 threshold
"融合分 < 0.5 过滤掉"——这是错的。融合分数不是相关度量纲——只能做排序、不能做阈值过滤。
13.13 工程实现:融合服务的位置
Hybrid search 的融合逻辑应该放在哪一层?三种架构:
架构 A:应用层融合
应用代码分别调 dense 库和 BM25 库、内存里做 RRF、返回融合结果。
- 优点:逻辑在代码里、容易改
- 缺点:两次网络往返、延迟差
- 适合:中小规模、自定义逻辑多的项目
架构 B:向量库原生融合
现代向量库(Qdrant 1.10+、Milvus 2.4+、Weaviate)原生支持 hybrid search——一次 API 调用内部做两路召回 + 融合返回。
- 优点:一次 RPC、低延迟、维护成本低
- 缺点:融合算法受限于向量库提供的(通常只有 RRF)
架构 C:独立融合服务
给融合逻辑一个独立 microservice——调各个检索组件、做融合、对外统一接口。
- 优点:融合逻辑集中、可独立迭代、可加复杂路由
- 缺点:多一跳、多一个服务要维护
选型:MVP 用 A、中期用 B、大型系统选 C。
13.14 融合的可观测性
生产 Hybrid 系统要监控的指标:
dense_only_recall / bm25_only_recall / hybrid_recall:分别跑、看融合收益overlap_rate:两路 top-20 的重合率——过低说明两路看的是完全不同的东西(可能有一路异常)、过高说明两路冗余bm25_contrib_rate:hybrid 最终 top-5 里来自 BM25 独有召回的比例——这个高说明 BM25 在补 dense 的短板fusion_latency:融合本身的延迟(通常 <5ms、远高就是代码实现问题)
每天看一次、异常告警。融合逻辑默默退化是 hybrid 系统最隐蔽的故障。
13.15 融合参数的离线调参工作流
前面几节给出了 α、k、各路权重的常见取值,但具体项目里这些参数怎么调才靠谱?一次性拍脑袋标一个 α 就上线是反模式——业务数据分布不断变化,融合参数同样要持续回归。
调参 pipeline
六步不可跳。最常见的错误是从 "在 train 上扫出最好的 α" 直接上线——等于把 dev/test 污染吞了。
Gold set 的防过拟合
调参 gold set 必须分三份:
- train(60%):参数扫描用
- dev(20%):选最佳参数组合
- test(20%):最终评估、不参与任何决策
Train 和 dev 选错的后果是参数"在你的 gold 上最好但上线差"。test 锁定的分数才有对外承诺的价值。
跨 domain 子集:如果业务跨多个 domain(技术文档 / 销售 FAQ / 法律条款),gold set 要保证每个 domain 都有代表样本、最终参数在每个 domain 都不退化。只看总体 recall 会掩盖某个小 domain 被牺牲的情况。
Query log replay 发现 badcase
Gold set 永远不完整——真实用户问的问题总会超出你的想象。生产 hybrid 调参必做的补充:
- 每周抽线上 query log 样本 N 条(通常 200-500 条)
- 人工或 LLM 辅助标:"top-5 里有正确答案吗"
- 累加到 gold set 的 badcase 池、定期回归
Badcase 池比初始 gold set 更能反映当前业务——gold set 是守卫已知、badcase 池是发现未知。
调参频率
不是调一次就永久生效的。触发重调的信号:
- 业务数据分布变:新产品线上线、文档体量翻倍、用户群体变化
- 嵌入模型升级:dense 分布变了、α 需重标
- 监控指标退化:recall@10 在 gold 上下降 > 2%
- badcase 池累积够:新增 badcase ≥ 50 条时重新扫参
典型节奏:MVP 阶段每月一次、稳定期每季度一次、模型升级后立即调。
Shadow 与 A/B 的分工
shadow 和 A/B 都是线上验证,作用不同:
- shadow:复制真实流量到新参数、对比检索结果(召回集交并差)、不影响线上。发现"新参数和旧参数的召回差异"——定位是否存在结构性退化
- A/B:小流量(1-5%)真实发到新参数、对比业务指标(点击率、答案满意度、session 时长)。确认"新参数带来真实价值"
只跑 shadow 不上 A/B——没有真实业务指标背书、不敢全量。只跑 A/B 不做 shadow——发现退化时已经影响用户、回滚成本高。
常见反模式
- 一次性调参就停:参数和业务同步演化、静态参数会悄悄退化
- 只看总体 recall:掩盖 domain 倾斜、要拆分看
- 在 train 集过拟合 α:正确做法是 train 扫、dev 选、test 锁
- badcase 只修不回归:修完某个 badcase 但没加进 gold——下次同类问题可能又错
- 调参日志不落库:每次调参的参数、gold 分数、决策人、时间都要留档、回滚时才有依据
调参是工程纪律不是艺术直觉——每一次参数变动都要可追溯、可回滚、可归因。
13.16 边界 query 下的 hybrid 失效与防线
前 15 节默认 query 是"正常长度 + 正常表达"的查询。生产里大量查询位于边界:一个字、十句话、纯代码符号、中英混杂。这些边界 query 让 dense 或 sparse 其中一路失效、hybrid 融合也救不回来——除非针对性地加防线。
四类边界 query 与各自失效模式
失效 1:超短 query
典型例子:"SSO"、"退款"、"价格表"。一两个词、信息极少:
- Dense 路:embedding 1-2 词产生的向量噪声大、召回漂移到同主题但不相关的 chunk
- BM25 路:命中一堆高频词 chunk、精度不够
- 融合:两路都差、RRF 融合是"差 + 差"
防线:短 query 强制走 query expansion(第 15 章同义词扩展 + HyDE),把 1-2 词扩到 5-10 词再送两路召回。判定阈值:query token 数 < 3 时自动扩展。
失效 2:超长 query
典型例子:用户粘贴一整段错误日志("求帮忙分析一下下面这段报错,2026-04-20 14:23 ERROR...")、或描述详细到几百字:
- Dense 路:长输入 embedding 有饱和——信号被稀释、向量表达不如短 query 尖锐
- BM25 路:stop word 多、低 IDF 词淹没关键词
- 融合:两路都精度下降
防线:query 摘要——用小 LLM 把长 query 压缩成 15-30 词关键摘要,原 query 和摘要都送检索、两次结果 RRF 融合。典型实现:Haiku 级模型做压缩、延迟 150-300ms、收益远大于成本。
失效 3:多实体对比 query
典型例子:"企业版和专业版 SSO 有什么区别"、"A 产品 vs B 产品的定价":
- Dense 路:embedding 把"对比"语义压成一个点、召回通常偏向其中一个实体(embedding 空间里出现更多的那个)
- BM25 路:某实体的 chunk 数多时、BM25 被它主导
- 融合:单路压倒多路
防线:query 拆解(第 15 章 §15.3)—— LLM 把"A vs B"拆成两个子 query:
- "A 的 SSO 功能"
- "B 的 SSO 功能"
各自跑 hybrid、结果 union 去重、统一送 rerank。RRF 融合发生在拆解之后,不是之前。
判定标准:query 里含对比词(vs、区别、对比、差异)且识别出 ≥ 2 个已知实体。
失效 4:代码 / 技术符号 query
典型例子:"UserService.login()"、"getUserById"、"SELECT ... WHERE user.id = ?":
- Dense 路:embedding 对代码符号的语义空间稀疏、
getUser和getUsers可能映射到同一点、或相反——完全跑偏 - BM25 路:如果 tokenizer 按空格切、
UserService.login被切成UserService+login——丢失整体符号 - 融合:dense 路帮倒忙、BM25 路切错
防线:第 12 章讨论的 代码专用 tokenizer + 精确符号匹配路径:
- query classifier 识别代码 query(驼峰命名、含
.或::、反引号包裹) - 走独立的符号索引(tree-sitter AST 切词)
- 精确匹配的 chunk 加权放大(×2-3),hybrid 融合后仍保留精确命中的 top 位置
代码 query 是 hybrid 不能套用通用公式的典型——必须分路由。
判定和路由
四类边界 query 要在 query understanding 阶段识别、走不同 pipeline:
python
def route_hybrid(query):
tokens = tokenize(query)
if len(tokens) < 3:
return "short" # → 扩展 + hybrid
if len(tokens) > 50:
return "long" # → 摘要 + hybrid
if has_comparison_entities(query):
return "multi_entity" # → 拆解 + hybrid union
if is_code_like(query):
return "code" # → 符号索引 + 精确加权
return "normal" # → 标准 hybrid路由的代价是一次 classifier(小模型 + 规则 < 20ms)、收益是边界 query 的 recall 提升 10-20 点。
边界 query 的可观测性
各类 query 的占比要监控——不同业务分布不同:
- 客服 FAQ 里短 query 占 40%+
- 代码搜索里代码 query 占 80%+
- 研究文献里长 query 占 30%
每类 query 分别跑 gold set 看 recall——总体 recall 好看、某类 query 悄悄崩掉的情况常见。分 segment 指标是防止这类"局部失败全局看不见"的关键。
为什么 hybrid 不是自动解
前面章节把 hybrid 描述成"把 dense 和 sparse 融合就完事"——这节是反驳:融合不自动抹平输入侧的问题。query 表达本身是检索质量的上游、hybrid 是下游。上游有结构性偏差,下游融合只能减少恶化、不能治本。
成熟 RAG 的检索链路是 query classification → 按类路由 → 每类用合适的召回 → 按类融合——远比"无脑跑 dense + BM25 再 RRF"复杂。这也是为什么第 15 章 Query Rewrite 和本章 Hybrid 要独立成章——它们解决不同层的问题、都是必要的。
13.17 学习排序融合:从静态权重到动态模型
§13.3 的线性加权和 §13.9 的 weighted RRF 都用静态权重——α=0.6、k=60 这类值一次标定长期使用。这种静态融合有上限:权重是全局最优、但单个 query 可能需要完全不同的权重。学习排序融合(Learning to Fuse, LTF)用小模型动态生成融合分数、精度再提 3-8 点。这是推荐系统标准路线、RAG 领域 2025 年后逐步普及。
静态权重的上限
看一个真实场景:
- query A "订单号 OD-12345 状态":BM25 权重应该 0.9(精确匹配主导)
- query B "用户登录态保持机制":dense 权重应该 0.8(概念检索)
用全局 α=0.6 ——A 被 dense 部分拖累(没精确命中的 chunk 占了 top 位)、B 被 BM25 部分干扰(字面不相关的 chunk 排到前面)。一种权重不可能同时最优化两种 query。
学习融合的核心思路
不直接用权重、而是用机器学习模型从多个信号里学出最终排序:
text
final_score = f(dense_score, bm25_score, rerank_score,
query_type, chunk_type, freshness,
has_exact_match, query_length, ...)f 是 gradient-boosted tree(LightGBM / XGBoost)或小神经网络。输入是各路分数 + query/chunk 特征、输出是相关度。
模型 per-query 动态加权——query 含精确 ID 时模型学会"把 BM25 分抬高"、概念 query 时"压低 BM25 抬 dense"——不需要手工定规则。
特征工程:融合模型的核心
静态融合只用"各路分数"作输入、学习融合可以加多维特征:
| 特征类 | 示例 | 作用 |
|---|---|---|
| 原始分数 | dense_score, bm25_score, rerank_score | 基础信号 |
| Query 特征 | length, has_numeric, has_code_symbol, lang | 区分 query 类型 |
| Chunk 特征 | freshness, doc_type, authority_score | 多目标 rerank(ch14) |
| 交互特征 | exact_term_hit_count, query_chunk_overlap | 精确匹配信号 |
| 历史特征 | click_rate, CTR_per_chunk | 用户反馈沉淀 |
| 上下文特征 | is_first_turn, previous_rerank_score | 多轮会话 |
特征越丰富、模型越能学到细微的权衡。但特征工程成本高——真正 SOTA 的融合模型往往几十个精心设计的特征。
训练数据从哪来
学习融合模型需要 (query, chunk, relevance) 三元组作训练数据:
- 人工标注:高质量、量少(千到万级)。gold set 扩展版
- 用户点击 / 采纳:规模大(日百万级)、但有点击偏差(排前的 chunk 更容易被点、哪怕不更相关)
- LLM-as-judge 标注:LLM 批量打分、规模中、成本中
- 隐式反馈:用户后续是否追问、采纳答案——"好答案"的代理信号
生产典型组合:30% 人工 + 50% LLM-judge 扩展 + 20% 点击反馈。多来源让模型不至于只拟合某一类偏差。
模型选择
- LightGBM:生产首选。训练快、推理快(< 1ms per rank)、特征解释清楚
- XGBoost:类似 LightGBM、社区生态强
- 小神经网络 MLP:可处理特征间非线性交互、但推理慢 10×
- LambdaRank:专门为排序设计、比分类 / 回归 loss 更对齐排序目标
多数 RAG 用 LightGBM + LambdaMART——精度够好、工程可控。
训练和推理 pipeline
python
# 训练
features = extract_features(query_chunk_pairs) # N × D 矩阵
labels = get_relevance(query_chunk_pairs) # 0-4 五级标注
groups = group_by_query(query_chunk_pairs) # LambdaRank 需要分组
model = lgb.LGBMRanker(...)
model.fit(features, labels, group=groups)
# 推理(每次 RAG 请求)
def fuse(query, candidates):
feats = [extract_features(query, c) for c in candidates]
scores = model.predict(feats)
return sorted(zip(candidates, scores), key=lambda x: -x[1])推理开销典型 1-3ms per query——可忽略。核心工作量在特征提取和训练。
何时上学习融合
不是所有项目都该上——判断依据:
- 有足够训练数据(gold set 至少 5000 条 + 点击 10 万+)→ 值得
- 静态融合已经到瓶颈(过去几轮调参收益 < 1 点)→ 值得
- query 分布多样、单一权重明显次优 → 值得
- 团队有 ML 工程师维护模型 → 必要条件
- MVP 阶段 → 不要上、静态 RRF 先跑通
过早上学习融合是典型过度工程——训练数据不够、模型过拟合、生产效果还不如 RRF。
学习融合的冷启动
新上线项目没点击数据、学习融合怎么冷启动?
- 阶段 1:静态 RRF、收集 3-6 个月点击和标注
- 阶段 2:用 LLM-as-judge 扩展 gold set 到几万条、训初版模型
- 阶段 3:模型上线 shadow + A/B、逐步放量
- 阶段 4:稳态运营、月度重训
这套路径是推荐系统三十年的标准工序、RAG 直接复用就行。
可观测性
学习融合模型需要监控:
- 特征分布漂移:某特征的均值 / 方差随时间变化——数据分布变了、模型可能退化
- 预测分数分布:输出分数是否稳定、突增突降说明模型 ill-conditioned
- feature importance 漂移:哪些特征被模型重视——变化说明业务在变
- 模型版本 vs 静态基线:每次重训和上一版、以及静态 RRF 基线对比、保证没退化
反模式
- 过早上学习融合:数据不够、过拟合、效果不如 RRF
- 只用 dense/bm25 分数作特征:忽视其他信号、模型学不到 per-query 差异
- 不做 shadow 验证:直接上线、badcase 暴涨
- 模型版本混用:多个版本并行服务、结果不稳定
学习融合是 "RAG 足够成熟了再上" 的优化——它是高 ROI 但需要前置投入的技术、不适合所有项目。
13.18 Early fusion vs late fusion:融合的两种哲学
前面 17 节讲的所有融合方法(RRF、线性加权、rerank 仲裁、ColBERT、学习排序)本质都属于同一类——late fusion:多路各自独立检索、最后合并。IR 领域还有另一条路线:early fusion——在检索之前就把信号融合、走统一的索引和打分。两者是融合技术的两大哲学、各有适用场景。理解这层区别让融合选型从"选方法"升到"选哲学"。
两种哲学的定义
- Late fusion:多路独立跑、结果层面合并。前 17 节全部属于这类
- Early fusion:把不同信号(词项、语义、上下文)编码到同一个向量空间或索引、只跑一次检索
Early fusion 的典型例子
SPLADE(ch12 §12.6)是最有名的 early fusion——把 BM25 词项权重和 BERT 语义信号融合到同一稀疏向量里:
text
SPLADE 向量维度 = vocabulary_size(几万)
每维度 = 该词对文档的相关度(学出来的权重)
多数维度 = 0(稀疏)、保留词汇匹配能力
非零维度学到了同义词扩展、语义相关性查询时只用 SPLADE 一路检索——既有 BM25 的词匹配能力、又有 dense 的语义扩展——不需要两路融合。
其他 early fusion 路线:
- ColBERT 的统一模型:同时学词项和语义
- 多向量 per chunk + 学习排序特征:把 dense + sparse 向量作同一模型特征
- Unified retrieval models(UNT, Tevatron 等):学术界 2024-2025 研究方向
Late fusion 和 early fusion 的对比
| 维度 | Late fusion | Early fusion |
|---|---|---|
| 实现复杂度 | 低(现有组件拼) | 高(新模型 / 新索引) |
| 训练成本 | 低 | 高(需要大规模训练) |
| 推理延迟 | 两路并行、取最慢 | 一路、理论上更快 |
| 信号灵活度 | 高(随意加路) | 低(固定在训练里) |
| 可解释性 | 高(每路独立) | 低(黑盒) |
| 精度上限 | 高(但受融合方法限制) | 理论上更高(学到的融合) |
| 生态成熟度 | 高(2-3 年成熟) | 低(仍在发展) |
两者不是"更好"的关系、是不同哲学:late fusion 是模块化工程、early fusion 是端到端学习。
实战中的选择
| 场景 | 推荐 |
|---|---|
| 一般 RAG 项目 | Late fusion(RRF 为主) |
| 大规模 + ML 团队支持 | Late + Early 混合(SPLADE + rerank) |
| 追求极致精度的学术 | Early fusion SOTA |
| 通用型 + 零训练预算 | Late fusion only |
| 特定领域 + 有标注数据 | 训练 early fusion 模型 |
多数生产项目选 late——成熟、灵活、低风险。Early fusion 是"研究前沿"、适合特定场景和有资源的团队。
SPLADE 作为 early fusion 的实战
§12.6 已讨论过 SPLADE。从 fusion 视角看:
- 传统 late fusion:dense + BM25 两个索引 + RRF
- SPLADE early fusion:一个 SPLADE 索引取代 dense + BM25——单路检索
SPLADE 的生产案例显示它在 BEIR 等 benchmark 上和 dense + BM25 + RRF 打平、延迟更低——但部署比 late fusion 复杂(需要 SPLADE 模型推理 + 稀疏倒排索引)。
这是 early fusion 的典型权衡:合一减少延迟和复杂度、但引入模型依赖。
混合 early + late
实际最有意思的路线是混合:
- Early fusion 内部已经融合词项 + 语义(如 SPLADE)
- 外部再和 dense embedding 做 late fusion
- Rerank 作为最后的融合器
text
SPLADE 检索 → top-50
+ Dense 检索 → top-50
→ RRF 融合 → top-30
→ Rerank → top-5三层信号叠加——early fusion 里有词项 + 部分语义、late fusion 外部加全量 dense、rerank 最终仲裁。精度经常接近或超过纯 late fusion。
训练一个 early fusion 模型需要什么
如果想自己训 early fusion(如自定义的 SPLADE 变体):
- 大规模 (query, relevant_doc) 对:几百万级、MS MARCO / 自家日志
- 强 GPU:8 × A100 训 3-5 天
- ML 工程师:懂 IR + transformers 的至少 1 人
- 评估 gold set:10k+ 条、多领域覆盖
投入门槛高——所以商用 early fusion 少。开源 SPLADE 已经做过基础工作、多数团队直接用预训练的就好。
未来趋势:Unified Retrieval
2025-2026 年学术前沿的 unified retrieval models 想把更多信号都塞进一个模型:词项 + 语义 + graph + metadata + 时间 + 个性化——一个模型统一检索。理论最优、但仍在研究。
生产短期(1-2 年)主流仍是 late fusion——模块化工程的稳健性胜过理论最优。但长期(3-5 年)early fusion 和 unified retrieval 可能逐步普及——跟进这个方向、但不追最新。
这节的实践价值
多数读者不会训练 early fusion 模型——但理解这层区别能:
- 看 SOTA 论文时知道它在哪个哲学体系
- 判断新工具 / 新模型的价值(是 late 的新融合方法?还是 early 的新统一模型?)
- 选型时不把 "统一 vs 模块化" 想成对错、而是 trade-off
知识层次上、这是融合技术的顶层分类——比个别方法更长寿、值得记住。
13.19 Hybrid 和 rerank 的协同优化:别让两者互相掣肘
§13.4 已经讨论过 rerank 可以作融合器的特殊方案。一般工程里、hybrid 融合和 rerank 是两个独立阶段:hybrid 产出 top-30、rerank 精排到 top-5。但两个阶段独立调优经常互相掣肘——融合的某个选择让 rerank 发挥不出、rerank 的某个特性又被融合前的过滤抹杀。这节把两者的协同讲清楚、让整条 pipeline 达到全局最优。
独立调优的问题
独立调优典型问题:
- Fusion top-30 给 rerank、但里面重复很多:rerank 浪费计算在重复 chunk 上
- Rerank 的优势 query(如长 chunk)被 fusion 过滤了:fusion 阶段没保留、rerank 看不到
- Fusion 权重偏向 dense、但 rerank 模型专精代码搜索:两者方向不一致、彼此抵消
三种协同模式
模式 A:保守独立
- Hybrid 追求 recall@30 最高
- Rerank 在 top-30 上精排
- 简单、多数项目的默认
适合:团队分工明确、不需要极致精度
模式 B:融合 → rerank → 反馈调融合
- Rerank 的输出作为 fusion 的训练信号
- 定期用 rerank 结果反调 fusion 权重
- 长期协同、慢但稳
适合:有 ML 能力、长期优化项目
模式 C:端到端联合
- 用 LTR 模型同时学 fusion 权重 + rerank 分数
- 训练时把两阶段放一起优化
- 最 SOTA、复杂度最高
适合:大公司、研究性质项目
多数项目用模式 A 即可;有 ML 团队的 B;有研究预算的 C。
Fusion top-k 的正确选法
Fusion 给 rerank 多少 chunk?常见误区:
- 太少(top-10):rerank 可能错过相关候选
- 太多(top-100):rerank 成本高、延迟高、噪声稀释信号
正确选法:看 rerank 的"召回收益曲线":
python
def find_fusion_topk(gold_set):
for k in [10, 20, 30, 50, 100]:
hybrid_topk = fusion(queries, top_k=k)
rerank_top5 = rerank(hybrid_topk, top_k=5)
recall = recall_at_5(rerank_top5, gold_set)
print(f"fusion_top{k} → rerank_recall@5 = {recall}")典型结果:
text
fusion_top10 → rerank_recall@5 = 0.87
fusion_top20 → rerank_recall@5 = 0.93
fusion_top30 → rerank_recall@5 = 0.94 ← 拐点
fusion_top50 → rerank_recall@5 = 0.945
fusion_top100 → rerank_recall@5 = 0.946拐点通常在 top-30 ~ top-50——再多 rerank 也榨不出更多召回。选拐点、不要盲目大 top-k。
Fusion 去重给 rerank 腾空间
Fusion 阶段的严格去重(§13.6)让 rerank 更有效:
- Fusion top-30 有 5 个同文档相邻 chunk——rerank 的 5 个 slot 浪费了
- Fusion 去重后 top-30 都是独立 chunk——rerank 能看到 30 个真不同候选
去重前后、rerank 后 recall@5 的差距可能 3-5 点。
Rerank 偏好和 fusion 策略的对齐
不同 rerank 模型有不同"口味"——fusion 策略要配合:
- 通用 rerank(bge-reranker):对各类 chunk 差别不大、fusion 可以 RRF 简单融合
- 代码专用 rerank:对代码 chunk 敏感、fusion 阶段应保留更多代码类 chunk
- 长 context rerank:能处理 1000+ token chunk、fusion 不用过早截断
选 rerank 模型后、回头调 fusion 的 top-k 和过滤——两者对齐。
Rerank 结果反馈给 fusion
高级协同——用 rerank 分数训练 fusion 权重:
python
# 收集数据: (query, candidate, rerank_score)
training_data = []
for query in query_log.sample(10000):
hybrid_top30 = fusion(query, top_k=30)
rerank_scores = rerank(query, hybrid_top30)
for cand, score in zip(hybrid_top30, rerank_scores):
training_data.append({
"query": query,
"dense_score": cand.dense_score,
"bm25_score": cand.bm25_score,
"rerank_score": score, # 作 ground truth
})
# 训 LightGBM 学 fusion weights
model = LGBMRanker()
model.fit(
X=[[d["dense_score"], d["bm25_score"]] for d in training_data],
y=[d["rerank_score"] for d in training_data],
)用 rerank 的"高分"作为 gold 学 fusion——让 fusion 的 top 更贴近 rerank 想要的。
延迟预算的联合分配
Hybrid 和 rerank 都占延迟、预算要联合分配:
| 方案 | Fusion 延迟 | Rerank 延迟 | 总延迟 | 召回效果 |
|---|---|---|---|---|
| A:大 top-k fusion + 小 rerank | 200ms | 200ms | 400ms | 中 |
| B:小 top-k fusion + 大 rerank | 80ms | 400ms | 480ms | 好 |
| C:中 top-k fusion + 中 rerank | 120ms | 300ms | 420ms | 最好 |
C 通常最优——两阶段各吃一半预算、不是一阶段吃光。
Hybrid 和 rerank 的共同评估
独立评估:
- Hybrid:recall@k(k=30 或 50)
- Rerank:NDCG@5、MRR
联合评估(推荐):
- End-to-end recall@5:经过 fusion → rerank 后的最终 top-5 的 recall
- End-to-end latency:两阶段总延迟
- Cost per query:两阶段总成本
单看某一阶段指标容易局部最优——看端到端才是真效果。
协同优化的 A/B
联合 A/B 比独立 A/B 复杂:
- 不只改 fusion 或只改 rerank、要组合测
- 如:fusion_v1+rerank_v1 vs fusion_v2+rerank_v2
- 2×2 矩阵:fusion 两版 × rerank 两版 = 4 种组合
- 看哪个组合 end-to-end 最好、不只各自
这让 A/B 的流量需求翻倍——但避免"各自最优、组合次优"的陷阱。
共同调优的一个真实经验
某团队经验:
- 独立调优、fusion recall@30 从 0.88 → 0.93(进步 5 点)
- 但 end-to-end recall@5 只从 0.82 → 0.83(进步 1 点)
根因:fusion 改进引入的新 chunk 是"表面相似但 rerank 打低分"的——rerank 压掉了、整体没收益。
改为联合调优:
- 每次 fusion 改动、看 rerank 后的 end-to-end
- 只保留真正提升 end-to-end 的 fusion 改动
- 最终 end-to-end recall@5 从 0.82 → 0.89
提升 7 点——联合视角找到真正有价值的改动。
协同的工程组织
团队组织要配合:
- 不要"fusion 团队" vs "rerank 团队":互相 finger-pointing
- 一个 owner 负责 end-to-end 召回质量:fusion 和 rerank 都在 scope 里
- 共同 OKR:两组共享 end-to-end recall 指标、而不是各自 owner 自己的指标
组织设计不到位、技术协同也跑不起来——这是 ch22 §22.10 组织学在 hybrid 上的具体应用。
何时不追协同
如果项目规模小、优化 ROI 低:
- MVP 阶段:先各自能用、后期再协同
- QPS 极低:优化省的钱不如团队精力成本
- 业务简单:通用 fusion + 通用 rerank 够用
协同优化是高 ROI 的最后一公里——前提是前面九公里做好了。不要跳过前面就上协同。
13.20 Hybrid 的冷启动:没有数据时怎么调参
前面几节讲了 hybrid 的各种调参方法(§13.15 离线 gold set、§13.17 学习排序)——但项目刚上线时没有任何数据:没 gold set、没反馈、没 badcase 库。这时怎么调 hybrid 参数?这节讲 cold start 阶段的 hybrid 实践——从"用默认值起步"到"数据飞轮转起来"的完整路径、和 ch14 rerank cold start 呼应。
Cold start 的 hybrid 挑战
这时候 "理论最优" 没用——先跑起来、再慢慢调。
冷启动的默认参数
没数据时、用行业经验值作起点:
python
default_hybrid_config = {
"fusion_method": "RRF", # 最稳健、零参数
"rrf_k": 60, # 经典值(Cormack 原论文)
"dense_top_k": 30, # 够给 rerank
"bm25_top_k": 30, # 同上
"after_rrf_top_k": 30, # 送 rerank
"rerank_model": "bge-reranker-v2-m3",
"final_top_k": 5,
}这套参数是行业平均配置——多数业务跑起来没问题。别过早调——没数据调不准。
用 MVP 数据快速迭代
上线后 2-4 周、收集初步数据:
- 日常 query log
- 用户点击引用 / 采纳
- 管理员抽样 review
这些数据构成初版 gold set(几十到几百条)——粗糙但比没强。用它跑一次参数扫描、看哪些默认值偏离业务最优。
分阶段的 hybrid 迭代
每阶段有明确的数据门槛——达到才升级、不然维持当前档。
Cold start 的可观测性
Cold start 阶段更需要 instrumentation:
- 每路召回的贡献:dense vs BM25 各自召回的 chunk 比例
- RRF 排序的离散度:top-5 是集中在某一路还是均衡
- Badcase 的分布:哪类 query 表现最差
这些数据让团队快速理解自己的业务 query 分布——比看 gold set 快。
Cold start 的快速反馈方法
没正式 gold set、怎么快速评估改动?
方法 A:员工 dogfood
- 团队自己用 RAG、遇到问题立即报
- 报的问题进 "内部 badcase 库"
- 改动后在这些 case 上验证
质量不如正式 gold set、但比没强、启动快。
方法 B:对比新旧
- 改动前后、对同一批 query 各跑一遍
- 人工比对两个版本的答案
- 看改动是否"肉眼可见地更好"
比定量慢、但低门槛。
方法 C:LLM-as-judge 早期版
- 用通用 LLM 作 judge、粗略比较新旧
- 不等建 gold set、立刻用
- 后续用正式 gold set 校准 LLM judge
这三种方法都是过渡——cold 期拿来用、warm 后切到正式评估。
别做的事
Cold start 阶段别做:
- 学习排序:没点击数据、训不了
- 按 query type 切换:query type 分布还不知道
- 跨查询的复杂 tuning:没数据支持
做了这些是过早优化——浪费精力、结果不稳。
和 Rerank / Query Rewrite 的 cold start 联动
Ch14 §14.18 讲过 rerank cold start、§15 的 rewrite 也有类似阶段——整个 RAG 系统的 cold start 要协调:
- Week 1-4:三者都用默认、先跑
- Month 2-3:hybrid 调 RRF k、rewrite 试 HyDE、rerank 保留通用
- Month 4-6:三者用累积数据分别优化
- Month 6+:协同优化
别试图在 month 1 就优化三者——一个一个来、先建数据飞轮。
Cold start 的指标期望
期望 cold start 阶段的指标:
| 阶段 | recall@10 | faithfulness | 用户满意度 |
|---|---|---|---|
| T+0(默认) | 0.75-0.85 | 0.75-0.82 | 60-70% |
| T+3 月(基础调优) | 0.85-0.90 | 0.82-0.87 | 70-80% |
| T+6 月(数据驱动) | 0.88-0.93 | 0.85-0.90 | 75-85% |
| T+12 月(成熟) | 0.91-0.96 | 0.88-0.93 | 80-90% |
注意:
- T+0 就达标 80%+ 满意度——默认配置 RRF + 通用 rerank 足够
- 追求极致(95%+)要 6-12 月 + 数据飞轮
团队对 cold start 的期望管理
对业务 / 老板解释:
- "上线第一天完美不现实"——设定合理期望
- "6 月内看到持续提升"——给长期承诺
- "需要反馈和时间"——要业务配合
没这层沟通、cold start 阶段质量普通、团队被 KPI 压——气氛紧张。
Cold start 的 ROI
Cold start 阶段的投入产出:
- 投入:搭 pipeline、默认配置——1-2 人月
- 产出:一个跑得动的 hybrid RAG
- 后续提升:靠 6-12 月的数据和迭代
别指望 cold start 一次就完美——是持续过程的起点。
一个 cold start 的真实节奏
某企业 RAG 的真实节奏:
- Week 1-2:MVP 上线、用 RRF 默认、内部 dogfood
- Week 3-4:建 100 条 gold set、发现 recall 偏低
- Month 2:分析发现 BM25 权重不够、调 wRRF (α=0.5)、recall +3 点
- Month 3:上线 rerank(通用 bge)、NDCG +5 点
- Month 4:发现代码 query 特别差、加代码专用路由
- Month 6:gold set 扩到 500 条、开始系统化 A/B
- Month 12:学习排序上线、再 +3 点
这套 12 月节奏不是快捷路径——是耐心走的路径。想跳步的基本都走不到 month 12。
对新 RAG 项目的建议
从零建 RAG 的团队:
- 第一周别调参:跑起来、观察基础指标
- 第一月建最小 gold set:100 条 manually 标的
- 前三月专注 hybrid + rerank:不上 graph / 多向量等高级技术
- 六月后考虑个性化 / 学习:有数据才有价值
耐心是 cold start 的核心技能——急于一次到位几乎必失败。
13.21 跨书关联:融合是推荐系统的老问题
推荐系统里"多路召回 + 融合"的历史和 IR 并列——Netflix、YouTube、Amazon 三十年都在做这件事。RRF 本身也是来自推荐领域的经典。RAG 的 hybrid search 是在搜索 + 推荐两条脉络的交汇点——这也是为什么做过搜推的工程师上手 RAG 极快。
《LangGraph 设计与实现》讨论 multi-agent 融合多个子 agent 的结果时、核心也是融合——只是信号变成了 agent 的子回答。融合的通用哲学:多个独立不完美信号的共识比单个信号的最优更稳健。
13.22 本章小结
- Hybrid = 两路召回并行 + 融合排序——不是串联
- 四类融合:RRF(默认首选)、权重线性(可调需标定)、rerank 仲裁(最准需 rerank)、ColBERT(精度上限)
- RRF 的优势:无标定、跨量纲、代码简单、效果强
- 融合效果通常比单路强 10+ 个点——先上 hybrid、再优化融合
- 动态融合(按 query 类型切权重)是下一步的优化
- 多路召回可以扩到 3+ 路——但每加一路都有成本
下一章讨论 Rerank——把 top-50 候选用 cross-encoder 精打细算成 top-5。