第 7 章 混合检索:RAG 召回率的关键一跃


文档摘要

第 7 章 混合检索:RAG 召回率的关键一跃 第 6 章的四种检索各有盲区:语义检索漏掉精确关键词,关键词检索漏掉同义表达。生产级 RAG 的标配,是把它们融合起来——这就是混合检索(Hybrid Search)。本章解决一个核心问题:怎么用 Zvec 的多向量查询 + 重排序,把召回率推到上限。 7.1 为什么单一检索不够:两段失败案例 先看两个真实的 RAG 翻车现场,理解为什么必须融合: 案例 A(纯语义检索翻车):知识库里有"报错码 的处理方法"。用户问"ERR4093 怎么解决"。稠密向量把 当成一串普通字符,和别的错误码"拉得不够开",结果 top-5 里没召回这条——专有标识是语义检索的盲区。 案例 B(纯关键词检索翻车):用户问"怎么退货"。知识库里写的是"商品退款流程"。

第 7 章 混合检索:RAG 召回率的关键一跃

第 6 章的四种检索各有盲区:语义检索漏掉精确关键词,关键词检索漏掉同义表达。生产级 RAG 的标配,是把它们融合起来——这就是混合检索(Hybrid Search)。本章解决一个核心问题:怎么用 Zvec 的多向量查询 + 重排序,把召回率推到上限。

7.1 为什么单一检索不够:两段失败案例

先看两个真实的 RAG 翻车现场,理解为什么必须融合:

案例 A(纯语义检索翻车):知识库里有"报错码 ERR_4093 的处理方法"。用户问"ERR_4093 怎么解决"。稠密向量把 ERR_4093 当成一串普通字符,和别的错误码"拉得不够开",结果 top-5 里没召回这条——专有标识是语义检索的盲区

案例 B(纯关键词检索翻车):用户问"怎么退货"。知识库里写的是"商品退款流程"。关键词检索搜"退货"找不到"退款"——同义表达是关键词检索的盲区

纯语义检索 纯关键词检索 ┌──────────┐ ┌──────────┐ 专有名词 │ ❌ 漏 │ │ ✅ 准 │ (ERR_4093) │ │ │ │ ├──────────┤ ├──────────┤ 同义表达 │ ✅ 懂 │ │ ❌ 漏 │ (退款/退货) │ │ │ │ └──────────┘ └──────────┘ │ │ └─────► 融合 ◄──────────┘ (混合检索) ✅ 两类都抓住

💡 混合检索的哲学:让每种检索做它擅长的事,再用重排序把多路结果合并。业界经验:语义 + 关键词混合检索,通常比单一方式召回率高 10%~30%,是 RAG 效果提升性价比最高的一招。

7.2 多向量查询:一条 query,多条路线

Zvec 的混合检索,本质是单次 query() 里传入多条 Query 路线,每条路线检索一个向量空间,再用 reranker(重排序器) 融合。这正是第 6 章那条"单路线内 vector 与 fts 互斥"铁律的解法——要结合,就用多路线。

collection.query(queries=[路线1, 路线2, ...], reranker=融合策略) │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ 路线1: 稠密向量 路线2: 稀疏向量 (路线3: FTS,可选) Query(dense,vec) Query(sparse,vec) │ │ ▼ ▼ 各自召回 topk 候选(每路线独立检索) │ │ └───────┬───────┘ ▼ reranker 融合重排 │ ▼ 返回最终 topn 结果

⚠️ topktopn 是两个不同的数,这是混合检索最容易混淆的点:

  • topk(在 query() 里):每条路线各自召回多少候选(喂给 reranker 的原料)。
  • topn(在 reranker 里):融合重排后最终返回多少条。

通常 topk > topn——每路线多召回些,让 reranker 有更多原料精选。

7.3 两种重排序策略:Weighted vs RRF

reranker 决定怎么把多路线的分数合并。Zvec 提供两种策略,理解它们的区别是选对策略的关键

策略 融合依据 适用场景 参数
WeightedReRanker 归一化后的分数加权求和 各路分数大致可比、且你知道哪路更重要 weights(各向量权重)、metric
RrfReRanker 仅看排名位置,不看分数 各路度量/尺度不同、想无脑稳健 rank_constant k

WeightedReRanker:加权融合

把每路线的相似度分数归一化后,按你给的权重加权求和。适合你清楚"语义更重要还是关键词更重要"的场景

result = collection.query( topk=5, # 每路线各召回 5 个候选 queries=[ zvec.Query(field_name="dense", vector=dense_vec), # 语义路线 zvec.Query(field_name="sparse", vector=sparse_vec), # 关键词路线 ], reranker=zvec.WeightedReRanker( topn=3, # 融合后最终返回 3 个 metric=zvec.MetricType.IP, # 用于分数归一化的度量 weights={ "dense": 1.2, # 给语义更高权重(多数 RAG 这样配) "sparse": 1.0, }, ), )

RrfReRanker:倒数排名融合(RRF)

RRF(Reciprocal Rank Fusion)完全不看分数,只看每条结果在各路线里的排名。排名越靠前,贡献越大;公式 RRF(r) = 1 / (k + r + 1),其中 r 是排名、k 是衰减常数。适合各路度量不可比、或你不想调权重的场景

result = collection.query( topk=5, queries=[ zvec.Query(field_name="dense", vector=dense_vec), zvec.Query(field_name="sparse", vector=sparse_vec), ], reranker=zvec.RrfReRanker( topn=3, rank_constant=60, # k:越大,排名靠前者优势越弱(更"民主") ), )

💡 怎么选? 经验法则:

  • 知道哪路更重要 → Weighted(如 RAG 通常语义为主,dense:1.2, sparse:1.0)。
  • 各路度量不可比 / 懒得调参 → RRF(稳健、无需权重,是"无脑可用"的默认选择)。
  • 很多团队最终选 RRF,正是因为它不依赖分数标定、对异常值不敏感。

7.4 端到端:用 bge-m3 做一次真正的混合检索

理论讲完,来个能跑的真实案例。BGE-M3 是混合检索的理想模型——它一次推理同时输出稠密向量(语义)和稀疏向量(关键词),免去维护两个模型。下面是完整链路:

import zvec from FlagEmbedding import BGEM3FlagModel # ① 加载 bge-m3:一次推理,稠密+稀疏双输出 model = BGEM3FlagModel("BAAI/bge-m3", use_fp16=True) def embed_hybrid(texts: list[str]): out = model.encode(texts, return_dense=True, return_sparse=True, return_colbert_vecs=False) dense = out["dense_vecs"].tolist() sparse = [{int(k): float(v) for k, v in d.items()} for d in out["lexical_weights"]] return dense, sparse # ② 建库:稠密(COSINE) + 稀疏(IP) 双向量,覆盖语义+关键词 schema = zvec.CollectionSchema( name="rag_hybrid", fields=[zvec.FieldSchema(name="text", data_type=zvec.DataType.STRING, index_param=zvec.FtsIndexParam(tokenizer_name="jieba")), zvec.FieldSchema(name="source", data_type=zvec.DataType.STRING, index_param=zvec.InvertIndexParam())], vectors=[ zvec.VectorSchema(name="dense", data_type=zvec.DataType.VECTOR_FP32, dimension=1024, index_param=zvec.HnswIndexParam(metric_type=zvec.MetricType.COSINE)), zvec.VectorSchema(name="sparse", data_type=zvec.DataType.SPARSE_VECTOR_FP32, index_param=zvec.HnswIndexParam(metric_type=zvec.MetricType.IP)), ], ) kb = zvec.create_and_open(path="./rag_hybrid", schema=schema) # ③ 入库:一次 embed 拿到两套向量,分别写入两个字段 chunks = ["报错码 ERR_4093 表示连接超时,请检查网络。", "商品退款流程:登录后在订单页申请。"] dense_vecs, sparse_vecs = embed_hybrid(chunks) kb.insert([ zvec.Doc(id=f"c{i}", vectors={"dense": dense_vecs[i], "sparse": sparse_vecs[i]}, fields={"text": chunks[i], "source": "faq.md"}) for i in range(len(chunks)) ]) kb.optimize() # ④ 混合检索:稠密+稀疏双路线,RRF 融合 q_dense, q_sparse = embed_hybrid(["ERR_4093 怎么解决?"]) # 注意:query 也用同一模型 hits = kb.query( topk=10, # 每路线各召回 10 queries=[ zvec.Query(field_name="dense", vector=q_dense[0]), zvec.Query(field_name="sparse", vector=q_sparse[0]), ], reranker=zvec.RrfReRanker(topn=5, rank_constant=60), # 融合后取 5 ) for h in hits: print(h.id, round(h.score, 4), h.fields["text"][:40])

💡 bge-m3 的价值:稠密负责抓住"解决/处理/排查"这类语义关联,稀疏负责精确锁定"ERR_4093"这个标识。两者经 RRF 融合后,那条报错处理文档会稳定出现在 top-5——而单用任何一路都可能漏掉它。这就是混合检索的威力。

7.5 也可以把 FTS 加入融合

除了稠密+稀疏双向量路线,你也可以把全文检索(FTS)作为一条独立路线纳入融合。思路完全一样——多路线 + reranker:

from zvec.model.param.query import Fts, Query hits = kb.query( topk=10, queries=[ zvec.Query(field_name="dense", vector=q_dense[0]), # 语义路线 zvec.Query(field_name="content", fts=Fts(match_string="ERR_4093")), # 关键词路线(FTS) ], reranker=zvec.RrfReRanker(topn=5, rank_constant=60), )

⚠️ 注意区分稀疏向量路线FTS 路线:稀疏向量需要一个稀疏 Embedding 模型(如 bge-m3 稀疏头)生成查询向量;FTS 路线直接传文本字符串(Fts(match_string=...)),零模型成本。轻量场景用 FTS 路线更省事;追求召回上限用稀疏向量路线(能学到更智能的词项权重)。

7.6 混合检索的调优心法

调优点 怎么做 影响
每路线 topk 调大 query()topk 给 reranker 更多原料,可能提质量但增加成本
最终 topn 调 reranker 的 topn 控制最终喂给 LLM 的段数(RAG 通常 3~8)
Weighted 权重 weights 字典 知道哪路更重要时微调(如语义 1.2 / 关键词 1.0)
RRF 的 k rank_constant k 大则排名靠前者优势弱(更民主),k 小则头部优势强
每路线的索引参数 Queryparam 如语义路线调 ef、稀疏路线也可调

💡 RAG 混合检索推荐起步配置topk=10~20(每路线)、topn=5、reranker 用 RRF(rank_constant=60)。跑通后再根据召回/延迟细调。先用 RRF 兜底,效果不够再上 Weighted 精调

7.7 动手实验

  1. 盲区对照:准备 5 条语料(含专有名词 + 同义表达),分别用纯稠密、纯稀疏、混合检索查同一批问题,统计各自的命中率,亲眼看混合检索的优势。
  2. 策略对比:同一个查询,分别用 WeightedReRanker(dense:1.2/sparse:1.0)和 RrfReRanker,对比最终 top-5 的差异,体会两种策略的风格。
  3. topk/topn 实验:固定 reranker,把每路线 topk 从 5 调到 20,观察最终 topn=5 的结果稳定性变化——体会"原料充足度"对融合质量的影响。

本章小结

  • 单一检索都有盲区:语义漏专有名词、关键词漏同义表达;混合检索是生产级 RAG 标配。
  • Zvec 混合检索 = Query 路线 + reranker 融合topk 是每路线候选数,topn 是最终返回数,二者不同。
  • 两种 reranker:Weighted(加权分数,需知权重)/ RRF(仅看排名,稳健免调参)——起步用 RRF。
  • 给了 bge-m3 端到端混合检索完整案例:一次推理双输出,稠密(COSINE)+稀疏(IP) 双路线 + RRF。
  • 也可把 FTS 作为一条路线纳入融合(零模型成本的关键词召回)。
  • 推荐起步配置:topk=10~20 / topn=5 / RRF rank_constant=60

检索讲完了,接下来是怎么把这些能力工程化、稳定上线。详见第 8 章


发布者: 作者: 转发
评论区 (0)
U