首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >混合检索RAG:多路召回+Reranker重排模型实战

混合检索RAG:多路召回+Reranker重排模型实战

作者头像
陆业聪
发布2026-07-17 10:43:39
发布2026-07-17 10:43:39
50
举报

📰 科技要闻

• Android 17 曝出对应用内存做出更严格限制,后台驻留策略进一步收紧,长期挂后台的服务类应用要重新评估存活策略。

• SK海力士CEO预警"史上最大存储短缺"将在明年到来,并考虑推出"内存即服务"模式,AI算力扩张正把硬件供应链逼到极限。

• 微软内测无广告、更纯粹的 Windows Search,系统级搜索体验正在被重新审视——这跟我们今天要聊的"检索质量"某种意义上是同一个命题。

有个问题问得特别扎心:"向量检索和关键词检索都上了,到底该信哪个?"说实话,这个问题我们团队自己也纠结了很久。最开始的方案很粗暴:向量检索召回 Top 20,ES 关键词检索召回 Top 20,两边结果直接拼一起丢给大模型,让它自己去"消化"。结果就是模型上下文里塞了一堆重复、甚至互相矛盾的片段,回答质量没见涨,token 成本倒是先涨上去了。

我当时就纳闷了,两路召回明明各有各的优势,怎么合并到一起反而变差了?后来才想明白问题出在哪:向量检索和关键词检索用的是两套完全不同的评分体系,向量检索给的是余弦相似度,取值在 0~1 之间;ES 的 BM25 分数理论上没有上界,可能是 3.2,也可能是 27.8。这两个分数直接放一起比大小,就跟拿身高的"厘米数"跟体重的"公斤数"去排序一样,压根不是一个量纲,谁排在前面全看运气。这一篇就把"多路召回怎么正确合并"、"Reranker 怎么选怎么部署"这两件事彻底讲清楚。

单路召回为什么不够用

在动手做融合之前,得先想明白一个问题:为什么非要多路召回,单路就不能把质量做到位吗?我们线上跑了三个月的数据,两种单路方案各有一批"翻车案例",而且是系统性的、不是偶发的。

向量检索的盲区:语义漂移

向量检索擅长"语义相近",但对精确匹配特别不敏感。举个我们真实踩过的例子:用户问"CVE-2024-3094 影响哪些版本",向量检索会召回一堆讲"漏洞影响范围""安全补丁"的相关文档,但很可能漏掉那篇标题里精确写着这个 CVE 编号、内容却是纯表格没什么语义描述的公告文档——因为向量模型对编号、型号这类字符串的语义表达能力天生就弱,编号本身在向量空间里几乎是"噪声"。这就是我上一篇提过的问题:专有名词、编号、精确术语,向量检索天生吃力。

关键词检索的盲区:同义替换与口语化提问

反过来,BM25 这类关键词检索的短板也很明显。用户问"服务突然挂了怎么排查",文档里写的是"进程异常退出后的故障定位方法",两句话意思一样,但共同出现的关键词几乎为零,BM25 直接抓瞎。我们做过统计,纯关键词检索在"口语化提问"场景下的 Top-5 召回率只有向量检索的 60% 左右,这个差距在客服场景尤其致命,因为用户很少会用文档里的"官方措辞"来提问。

一句话总结:向量检索管"意思相近",关键词检索管"字面精确",两者互补而不是互相替代。这也是为什么"混合检索"不是锦上添花,而是生产级 RAG 系统的标配,几乎没有例外。

多路召回架构:谁跟谁并行查

确定要走混合路线之后,第一个要设计的是召回架构。我们最终采用的是三路并行召回,不是串行——串行意味着一路查完再查另一路,延迟会累加,用户体感会很差:

用户 Query

↓ 并行分发

路径A → Milvus向量检索 Top 30,管语义相近

路径B → ES BM25检索 Top 30,管字面精确

路径C → 知识图谱检索 Top 10,管实体关系 (下一篇详讲,本篇先占位)

↓ 三路结果汇总

RRF融合排序

Reranker精排

Top 5 送入LLM

知识图谱这一路我们暂时先占位,下一篇会专门讲 Neo4j 和 Graph RAG,本篇聚焦在向量+关键词双路融合,以及最关键的 Reranker 精排这一步。

结果融合:RRF 算法到底在干什么

前面提到分数量纲不一致的问题,业界的标准解法是 RRF(Reciprocal Rank Fusion,倒数排名融合)。它的核心思路特别聪明:不看原始分数,只看排名(rank),排名是天然可比的,第 1 名不管在哪一路都是"最好",不存在量纲问题。

公式很简单:

RRF_score(d) = Σ 1 / (k + rank_i(d))

对文档 d,把它在每一路召回结果里的排名 rank_i 取倒数再累加,k 是个平滑常数(通常取 60),防止排名靠前的文档权重过大导致分数差距失真。排名越靠前,1/(k+rank) 越大,贡献越高;一篇文档如果同时在向量检索和关键词检索里都排前几名,它的融合分数会明显高于只在一路里靠前的文档——这正是我们想要的效果:两路都认可的结果,可信度更高。

代码实现比想象中简单,不需要任何额外的机器学习组件:

代码语言:javascript
复制
def rrf_fusion(
vector_results: list[str],
bm25_results: list[str],
k: int = 60
) -> dict[str, float]:
"""对两路召回结果做RRF融合"""
scores: dict[str, float] = {}for rank, doc_id in enumerate(
vector_results, start=1
):
scores[doc_id] = scores.get(doc_id, 0) \
+ 1 / (k + rank)for rank, doc_id in enumerate(
bm25_results, start=1
):
scores[doc_id] = scores.get(doc_id, 0) \
+ 1 / (k + rank)return dict(
sorted(
scores.items(),
key=lambda x: x[1],
reverse=True
)
)

这段代码上线之后,我们做了个简单的 A/B 对比:单路向量检索 Top-5 命中率 76%,单纯拼接两路结果(不做融合,直接各取一半)是 79%,用 RRF 融合之后到了 87%。数字看着差距不算夸张,但线上人工抽检发现,RRF 融合之后"两路都同意"的文档基本不会出现明显跑偏的情况,这种"一致性验证"的效果,光看命中率数字是感受不到的。

一个容易踩的坑:k 值不能瞎设。k 太小(比如 k=1),排名第 1 和第 2 之间的分数差会被放大得离谱,几乎变成"赢家通吃";k 太大(比如 k=1000),所有文档的分数差都被压平,融合基本等于随机排序。业界经验值 k=60 是从信息检索领域的大量实验里得出的,没有特殊场景不建议改。

RRF 融合之后为什么还需要 Reranker

这里必须承认一个事实:RRF 效果已经不错了,我们上线初期确实动过"到这一步就够了"的念头。但跑了两周线上数据之后发现一个规律性的问题——RRF 本质上还是在"拼凑"两个独立打分系统的排名,它并不真正理解 query 和文档之间的语义关系,它只是在做"投票",两路都排前面的文档得票高,但两路排名本身的质量它无法校正。

Reranker(重排模型)解决的正是这个问题:它是一个专门训练用来判断"query 和某段文档到底有多相关"的模型,直接吃 query 和候选文档的原始文本,输出一个精确的相关性分数——不依赖任何召回阶段的排名信息,是真正意义上"重新审判"一遍。

Bi-Encoder vs Cross-Encoder:为什么 Rerank 不能用向量检索代替

这里有个我一开始没搞懂的地方:向量检索用的 Embedding 模型和 Reranker 模型,都是"判断相关性",为什么不能只用向量检索?答案在于两者的模型架构完全不同。

Embedding 模型是 Bi-Encoder 架构:query 和文档分别独立编码成向量,之后算个余弦相似度。这种架构的好处是可以提前把文档向量算好存库里,检索时只需要编码 query,速度极快,能支撑百万级候选集的检索。但代价是 query 和文档在编码阶段互相看不到对方,模型只能靠"两个独立向量的几何距离"去近似相关性,精度天然有损失。

Reranker 用的是 Cross-Encoder 架构:query 和文档拼接成一个整体输入模型,让模型在自注意力层里充分交互,逐词判断相关性,精度明显更高。代价是没法预计算,每个候选文档都要跟 query 现场拼接一次做推理,计算量比 Bi-Encoder 高一个量级,这也是为什么 Reranker 只能用在"粗筛之后的少量候选"上,不可能拿去做百万级的初筛。

Bi-Encoder(Embedding)

query和doc独立编码,算余弦距离, 速度快,可预计算, 适合百万级初筛

Cross-Encoder(Reranker)

query和doc拼接后一起编码, 自注意力充分交互,精度更高, 速度慢,只能用于精排少量候选

三款 Reranker 实测对比

选型上我们对比了三个:智源的 BGE-Reranker-v2-m3、Cohere 的 Rerank 3 API,以及自己拿业务数据微调过的交叉编码器。用的还是上一篇提到的那 300 条客服问答标注集,这次测的是 Rerank 之后的 Top-5 准确率:

模型

Top-5准确率

单次延迟(20候选)

部署方式

仅RRF(无Rerank)

87.0%

-

-

BGE-Reranker-v2-m3

93.4%

约80ms

私有化部署

Cohere Rerank 3

94.1%

约200ms(含网络)

API调用

业务数据微调交叉编码器

96.7%

约60ms

私有化部署

这个结果跟上一篇 Embedding 选型的结论呼应上了:Rerank 这一步比 Embedding 更值得投入微调成本。原因也好理解——Embedding 微调是给"通用向量空间"做局部调整,收益有天花板;而 Reranker 本身就是"专门判断相关性"的模型,用业务真实的 query-doc 对做微调,相当于直接教它认识你的业务语言,收益立竿见影。我们最终的方案是:BGE-Reranker-v2-m3 打底,拿线上积累的用户反馈数据(点赞/点踩+人工复核)持续微调,三个月后准确率追平甚至超过了 Cohere 的商业 API。

我的判断:数据敏感、有一定 ML 工程能力,选 BGE-Reranker 私有化部署+持续微调,长期收益最大;追求最快上线、不在乎境外调用,Cohere API 是省心之选;两者延迟都在可接受范围,别为了几十毫秒的差异纠结太久,先上线再迭代。

部署与延迟优化:批量推理是唯一解

Reranker 的延迟问题一开始把我们坑得不轻。最初的实现是每个候选文档单独调一次模型,20 个候选串行推理,单次请求延迟飙到 1.6 秒,用户体感直接崩了。这玩意儿坑了我们一整天去排查,最后才反应过来问题根本不在模型本身,而在调用方式上——20 次串行的小请求,GPU 大部分时间在等数据传输,真正计算的时间占比可能连 30% 都不到。

解法是老套但有效的批量推理(Batching),把 20 个 query-doc 对拼成一个 batch 一次性丢进模型:

代码语言:javascript
复制
from FlagEmbedding import FlagRerankerreranker = FlagReranker(
"BAAI/bge-reranker-v2-m3",
use_fp16=True
)def rerank(
query: str, docs: list[str]
) -> list[float]:
"""一次批量推理,而非逐个调用"""
pairs = [[query, d] for d in docs]
scores = reranker.compute_score(
pairs, batch_size=32
)
return scores

改完之后,20 个候选的批量推理延迟从 1.6 秒降到了 85 毫秒,差了将近 20 倍。这里的教训特别值得记一下:GPU 推理服务几乎所有延迟问题的第一反应都应该是"我有没有在做批量",而不是急着换更小的模型或者加更多机器。

缓存策略:别让重复问题浪费算力

批量推理解决了单次请求的延迟,但我们发现线上有大量高频重复问题(客服场景尤其明显,"怎么退款"这类问题一天能问几百遍)。对这种情况,我们加了一层基于 query 语义相似度的缓存——不是精确字符串匹配缓存(用户措辞千变万化,命中率极低),而是拿 query 的 Embedding 向量做近似去重,语义高度相似的 query 直接复用之前的 Rerank 结果:

代码语言:javascript
复制
async def rerank_with_cache(
query: str, docs: list[str]
):
q_vec = await embed_query(query)
# 查缓存:语义相似度>0.95视为同一query
cached = await semantic_cache.get(
q_vec, threshold=0.95
)
if cached:
return cachedscores = await rerank(query, docs)
await semantic_cache.set(
q_vec, scores, ttl=3600
)
return scores

上线之后统计缓存命中率大概在 35% 左右,也就是三分之一的 Rerank 请求直接省掉了,GPU 负载明显降下来了。这里有个细节要提醒:相似度阈值设 0.95 是我们业务场景(客服问答,措辞相对集中)跑出来的经验值,如果你的场景 query 多样性很高,这个阈值可能需要调得更宽松,否则缓存命中率会很低,白白多一层查询开销。

端到端评测:混合检索到底值不值得这套复杂度

说了这么多架构和代码,最后还是要回答一个务实的问题:这套"三路召回+RRF+Reranker"的复杂度,值不值得?拿同样 300 条标注数据做完整的端到端对比,结果是这样的:

方案

Recall@5

MRR

P99延迟

纯向量检索

76.2%

0.68

45ms

纯BM25检索

71.5%

0.63

20ms

双路+RRF融合

87.0%

0.79

70ms

双路+RRF+Reranker

96.7%

0.94

155ms

数字很说明问题:从单路到"双路+RRF+Reranker",Recall@5 提升了 20 个百分点,MRR(衡量正确答案排名靠前程度的指标)提升了近 40%,代价是延迟从 45 毫秒涨到 155 毫秒。对绝大多数知识库问答场景,多出来的这 110 毫秒完全可以忽略——用户根本感知不到 45ms 和 155ms 的区别,但检索质量的提升会直接反映在大模型回答的准确率上,这才是真正影响用户体感的部分。

我的判断:只要你的知识库超过几千篇文档、用户提问方式多样,混合检索+Reranker几乎是必选项,不是可选优化。真正需要犹豫的场景只有一种——超低延迟要求(比如语音实时交互,容不下 100ms 以上的检索环节),这种情况才值得牺牲一部分精度去换速度。

这一篇把"两路召回怎么合理融合、Reranker怎么选怎么部署"讲完了,但还留了个尾巴——知识图谱这一路召回该怎么接进来,多跳关系推理的场景 RRF 还适用吗?下一篇聊 Neo4j 和 Graph RAG,把三路召回真正补齐。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档