RAG 召回率从 68% 到 91%:混合检索 + 重排序的完整方案
纯向量搜索的天花板
在之前的文章里,我讨论过用 ES 的 kNN 做语义检索,以及用 MySQL 9.2 的 VECTOR 类型做 RAG。这些方案在中小规模场景下够用,但有一个容易被忽略的问题:纯向量搜索的召回率不够高。
举一个真实的例子。用户问:”Spring Cloud 微服务网关的限流策略”。
- 向量检索召回:理解了”微服务”和”限流”的语义关系,但可能漏掉了精确匹配”Spring Cloud Gateway”这个词的文档。
- BM25 关键词检索召回:精确匹配了”限流”这个词项,但无法理解”网关”和”Gateway”是同一个东西。
两条路各有盲区。在企业知识库场景,80% 的查询同时需要语义理解和关键词精确匹配。
参考数据(基于某企业客服知识库,5 万文档):
| 检索方式 | Recall@10 | Precision@5 |
|---|---|---|
| 纯向量检索 | ~68% | ~64% |
| 纯 BM25 | ~72% | ~78% |
| 混合检索 | ~91% | ~87% |
混合检索把召回率从 68% 提到 91%,提升 31%。这就是为什么”混合检索 + 重排序”正在成为企业级 RAG 的标配。
混合检索:BM25 + kNN 双路召回
基本思路
混合检索的核心是多路召回 + 融合排序:
1 | 用户查询 |
两路独立检索,各召回 K 个结果(通常 K=2050),然后融合排序选出最终 Top-K(通常 K=510)。
ES 里的实现
ES 天然支持混合检索——一个查询里同时走 BM25 和 kNN:
1 | { |
ES 内部会自动做分数融合。但这里有个问题:BM25 分数和 kNN 余弦相似度分数的量级完全不同。BM25 可能是 015,kNN 相似度是 01。直接相加,BM25 会压倒 kNN。
怎么解决分数不可比
两种主流方案:
方案 1:ES 内置 RRF(推荐)
ES 8.8+ 支持 RRF(Reciprocal Rank Fusion),直接在查询里用:
1 | { |
方案 2:应用层手动融合
如果你的 ES 版本不支持 RRF retriever,可以在应用层做:
1 | public List<Document> hybridSearch(String query, float[] queryVector) { |
RRF 融合:为什么比线性加权好
RRF 公式
RRF(Reciprocal Rank Fusion)的核心思想:不看分数,只看排名。
1 | RRF_Score(doc) = Σ 1 / (K + rank_i(doc)) |
其中 K 是衰减因子(通常取 60),rank_i(doc) 是文档在第 i 路检索结果中的排名。
为什么 RRF 比线性加权好
线性加权:Score = 0.6 × vectorScore + 0.4 × bm25Score
问题:分数分布不均匀。向量检索的第 1 名和第 2 名分数差可能只有 0.01,但 BM25 的第 1 名和第 2 名差 3 分。线性加权后 BM25 的排名波动会主导最终结果。
RRF:Score = 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318
第 1 名和第 5 名的 RRF 分数差很小(0.0164 vs 0.0154),不会因为某一路检索的分数波动而剧烈影响最终排名。
| 融合方式 | 优点 | 缺点 |
|---|---|---|
| 线性加权 | 简单直观 | 分数不可比,需归一化 |
| RRF | 排名融合,分数不可比也无所谓 | 丢失分数信息(只看排名) |
| 曼哈顿距离 | 保留更多信息 | 实现复杂,收益有限 |
结论:大多数场景下 RRF 就够了。 它简单、鲁棒、已在 ES 和多种搜索引擎中验证。
权重调节
有时候两路检索的权重不一样。比如内部知识库场景,语义检索更重要(用户描述模糊),权重 0.6:0.4;而文档精确检索场景,关键词更重要,权重 0.3:0.7。
RRF 可以加权重:
1 | Weighted_RRF_Score(doc) = Σ w_i / (K + rank_i(doc)) |
参考值:
| 场景 | 向量权重 | BM25 权重 |
|---|---|---|
| 内部知识库 | 0.6 | 0.4 |
| 客服 RAG | 0.7 | 0.3 |
| 文档精确检索 | 0.3 | 0.7 |
| 通用场景(不知道选什么) | 0.5 | 0.5 |
重排序:Cross-Encoder 的精确打击
为什么混合检索还不够
混合检索解决了召回率问题(从 68% 到 91%),但 Precision@5 只到 87%。这意味着前 5 个结果里还有 1 个不太相关。
原因:双编码器(Bi-Encoder)的精度天花板。 向量检索用的是双编码器——查询和文档分别编码成向量,然后算余弦相似度。这种方式速度快,但无法捕捉查询和文档之间的细粒度交互。
Cross-Encoder 怎么不同
重排序用的是交叉编码器(Cross-Encoder)——把查询和文档拼在一起,一次性输入模型:
1 | [CLS] 用户查询 [SEP] 文档内容 [SEP] → 模型 → 相关性分数 |
Cross-Encoder 直接对 (Query, Document) 对做相关性评分,精度远超双编码器的余弦相似度。
代价是速度慢:每个 (Query, Document) 对都要一次完整的模型推理。所以重排序只对混合检索的 Top-K(通常 20~50 个)做,不对全库做。
重排序模型选择
| 模型 | 部署方式 | 延迟 | 适合场景 |
|---|---|---|---|
| Cohere Rerank v3 | API 调用 | ~50ms/对 | 快速接入,无本地部署 |
| BGE-Reranker-base | 本地 ONNX | ~5ms/对 | 数据敏感,需要本地化 |
| BGE-Reranker-large | 本地 GPU | ~2ms/对 | 高性能场景 |
| ms-marco-MiniLM | 本地 ONNX | ~8ms/对 | 英文场景 |
推荐:先用 Cohere Rerank API 跑通流程,再考虑本地部署 BGE-Reranker。
在 Spring AI 里怎么接
Spring AI 2.0 的 Advisor 链是接入重排序的天然位置:
1 | // 概念设计(非生产代码) |
RerankerAdvisor 的核心逻辑:
1 | public class RerankerAdvisor implements Advisor { |
关键设计决策:
- 重排序只对 Top 20~50 做,不对全库做——Cross-Encoder 慢
- 重排序后只保留 Top 5~10——给 LLM 的 context 不能太长
- 放在 RAG Advisor 之后——先召回再精排
完整 RAG 管道的成本账
| 环节 | 延迟 | 成本 |
|---|---|---|
| BM25 检索 | ~10ms | 低(ES 内置) |
| kNN 检索 | ~45ms | 中(HNSW 查询) |
| RRF 融合 | <1ms | 几乎为零 |
| Cross-Encoder 重排序 | ~50ms × 20对 = ~100ms | 中高(模型推理) |
| LLM 生成 | ~500ms | 高(token 费用) |
混合检索 + 重排序增加了约 150ms 延迟,换来召回率从 68% → 91%、精度从 64% → 87%。
这个代价值不值? 取决于场景:
- 如果 LLM 生成本身要 500ms+,多 150ms 做精排是值得的——垃圾进垃圾出,召回不准,LLM 生成再快也没用。
- 如果是实时对话场景(要求 P99 < 200ms),可以跳过重排序,只做混合检索 + RRF。
什么时候该上重排序
| 场景 | 推荐 |
|---|---|
| 内部知识库(5万文档以下) | 混合检索 + RRF 就够,跳过重排序 |
| 客服 RAG(精度要求高) | 混合检索 + 重排序 |
| 法律/医疗文档检索(精度极其关键) | 混合检索 + 重排序 + 人工审核 |
| 实时对话(延迟敏感) | 只做混合检索,跳过重排序 |
| 文档量 > 100 万 | 混合检索 + 异步重排序 |
核心判断:重排序解决的是”前 5 个结果里混入了不相关文档”的问题。如果你的 RAG 经常出现”LLM 答非所问”,第一步不是换大模型,而是检查召回质量。
意味着什么
RAG 的瓶颈不在生成,在检索。 很多人调 RAG 的第一反应是”换更大的模型”或”优化 prompt”。但真正的问题往往是召回质量——如果检索回来的文档不对,GPT-5 也答不对。
混合检索是企业级 RAG 的最低门槛。 纯向量搜索的 68% 召回率在生产环境是不可接受的。BM25 + kNN + RRF 是性价比最高的改进。
重排序是”最后 10% 的精度”的标配。 如果业务对精度有要求(客服、法律、医疗),重排序不是可选项。
Spring AI 的 Advisor 链是做这件事的正确位置。 不要在业务代码里硬编码检索逻辑,而是把混合检索和重排序封装成 Advisor,通过链式组合控制管道。
一句话:RAG 不是”向量搜索 + LLM”这么简单。召回(多路 + 融合)→ 精排(Cross-Encoder)→ 生成(LLM),这三步管道才是企业级 RAG 的完整形态。
