混合检索 + 重排序
纯向量检索常漏掉精确关键词匹配;纯 BM25 又漏掉改写表述。混合检索同时跑两路、合并候选列表,再由 reranker 在 LLM 看到内容前修正最终顺序。
为何用混合?
bi-encoder 向量检索擅长语义相似 ——「vehicle」能匹配「car」。BM25 擅长词面重叠 —— 产品 SKU、法律条款编号、错误码。企业 RAG 里,正确 chunk 往往需两种信号。混合检索加宽召回;重排序在合并后的短名单上提供精度。
混合检索找到更多对的干草;重排序在里头找到针。
检索—合并—重排序范式
1. BM25 top 50 ─┐
├─▶ dedupe + merge ─▶ ~80 unique candidates
2. Vector top 50 ─┘
3. Cross-encoder rerank on merged list ─▶ top 5–10 for LLM
单路检索时宁可召回更宽 —— 每路可返回 40–60 条。去重后仍希望有 50–100 个唯一 chunk 进入 reranker。top-k 默认值见 为 RAG 加重排序。
融合策略
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 倒数排名融合(RRF) | 分数 = 各列表 Σ 1/(k + rank) | 默认首选 —— 无需分数归一化 |
| 线性组合 | α·向量分 + (1-α)·BM25 分 | 两路分数已在你数据上标定 |
| 并集 + 仅 rerank | 拼接列表、去重、跳过融合 | reranker 够强且延迟预算允许 |
最小 Python 示例
from rank_bm25 import BM25Okapi
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-base")
def hybrid_rerank(query, corpus_chunks, vector_hits, bm25_hits, top_n=5):
# Merge by chunk id, preserve text
seen, merged = set(), []
for chunk_id, text in vector_hits + bm25_hits:
if chunk_id in seen: continue
seen.add(chunk_id)
merged.append(corpus_chunks[chunk_id])
pairs = [(query, c) for c in merged]
scores = reranker.predict(pairs)
ranked = sorted(zip(scores, merged), reverse=True)
return [c for _, c in ranked[:top_n]]
Elasticsearch 混合查询、Weaviate 混合搜索、LlamaIndex QueryFusionRetriever 等框架封装同一思路 —— 多后端召回,再可选 rerank。
调优建议
- 每路过度召回。BM25 30 条、向量 30 条时,重叠后唯一数会少于 60 —— 每路目标应更高。
- reranker 是均衡器。好的 cross-encoder 看过所有候选后,融合顺序不那么关键。
- 用你自己的数据度量。评测集同时含词面与语义查询时混合收益最大 —— 见 评测 reranker。