热闻岛
返回全网热点

RAG 多路召回:别把答案押在一条检索链上

4小时前10 阅读
RAG 多路召回:别把答案押在一条检索链上配图
从向量检索、BM25、多 Query,到 RRF 融合与 Rerank 精排 一、多路召回解决的是“单一路径漏答案”的问题 在 RAG 系统里,生成答案之前,最关键的一步是把正确证据召回来。很多系统早期只做向量检索:用户问题转成向量,到向量

从向量检索、BM25、多 Query,到 RRF 融合与 Rerank 精排

RAG 多路召回:别把答案押在一条检索链上配图

一、多路召回解决的是“单一路径漏答案”的问题

在 RAG 系统里,生成答案之前,最关键的一步是把正确证据召回来。很多系统早期只做向量检索:用户问题转成向量,到向量库里找相似度最高的 Top-K,再把这些 Chunk 塞给大模型。这个方案能跑起来,但很快会遇到天花板。

因为没有任何一种检索方式是全能的。向量检索擅长语义相似,但对型号、缩写、数字、错误码这些精确词不敏感;BM25 关键词检索擅长字面匹配,但遇到同义词、表达差异、用户口语化提问时又容易漏。多路召回的核心,就是同时走几条检索路线,再把候选结果融合、去重、精排,最后交给 LLM。

RAG 多路召回:别把答案押在一条检索链上配图

二、多路召回的完整链路

一个生产级多路召回链路,通常不是“查三遍然后拼起来”这么简单,而是由 Query 处理、并行召回、候选池、融合排序、精排、上下文组装几部分组成。

RAG 多路召回:别把答案押在一条检索链上配图

这个链路有一个很重要的思路:召回阶段可以“宽一点”,让更多可能相关的候选进入池子;但进入 Prompt 之前一定要“严一点”,通过 RRF、去重、Rerank、权限过滤和 Token 预算控制,把真正可靠的证据留下来。

三、三条最常见的召回路线

小林面试笔记这一章的核心主线是三路召回:向量检索、BM25 关键词检索、多 Query 扩展。它们不是重复建设,而是负责不同类型的问题。

RAG 多路召回:别把答案押在一条检索链上配图

1. 向量检索:负责语义覆盖

向量检索把用户问题和文档 Chunk 转成 Embedding,在向量空间里找最近邻。它最大的价值是语义泛化:用户问“怎么退货”,文档写“申请售后流程”,虽然字面不完全一样,但语义接近,仍有机会命中。

RAG 多路召回:别把答案押在一条检索链上配图

但向量检索的短板也很明显:型号、数字、错误码、配置项、API 名称、版本号这类短 Token 很容易被语义模型弱化。比如“RTX 4090 功耗”这种问题,用户真正关心的是“RTX 4090”这个精确词,BM25 往往比向量检索更稳。

2. BM25:负责精确词命中

BM25 是传统搜索里非常经典的稀疏检索方法,本质上基于词频和逆文档频率来判断“这个词是否能代表这篇文档”。它对产品型号、数字、专有名词、错误码这类内容很敏感。

RAG 多路召回:别把答案押在一条检索链上配图

中文场景下,BM25 的效果很依赖分词。比如“显卡功耗”“配送时效”“M4 Pro”这种词,如果分词策略不稳定,召回也会受影响。所以工程里通常要配合同义词词典、停用词表、行业词库和字段权重。

3. Multi-Query:负责多角度覆盖

有些召回失败不是因为语义模型不行,也不是因为关键词缺失,而是用户问题和文档写法站在不同角度。用户问“产品多久能送到”,文档标题可能叫“配送时效说明”。这时可以用 LLM 生成多个检索版本,让每个版本分别去检索。

RAG 多路召回:别把答案押在一条检索链上配图

Multi-Query 的收益来自“扩大表达覆盖面”,代价是多一次或多几次 LLM 调用,以及更多检索请求。因此它适合问法变化大的客服、售后、制度问答,不适合极低延迟、强成本约束的链路默认全开。

四、多路召回还能继续扩展:多索引、多向量、多粒度

在复杂知识库里,多路召回不一定只有 Dense、BM25、Multi-Query。文档可能同时有正文、小 Chunk、父文档、摘要、标题、表格、图片 OCR、问答对、元数据。不同索引承载不同检索视角。

RAG 多路召回:别把答案押在一条检索链上配图

例如,父子 Chunk 能解决“命中细节但缺上下文”的问题;摘要向量能解决“文档太长,细节向量分散”的问题;标题/问题向量能解决“用户问法与正文写法不一致”的问题;元数据索引能解决权限、时间、部门、产品线过滤问题。

五、结果怎么合并:RRF 是工程里最常用的融合方法

多路召回后会得到多份结果列表:向量检索有相似度分数,BM25 有关键词相关分数,多 Query 可能有多个子列表。问题是,这些分数并不在同一个量纲上,直接相加很容易出问题。

RRF(Reciprocal Rank Fusion)绕开了这个问题:它不比较原始分数,只比较“排名”。一个文档如果在多路结果里都排得靠前,融合分就高;如果只在一路靠前,分数也会被保留,但优先级相对低。

RAG 多路召回:别把答案押在一条检索链上配图
def reciprocal_rank_fusion(results_by_route, k=60):
"""
results_by_route: {
"dense": ["docA", "docB", "docC"],
"bm25": ["docC", "docA", "docD"],
"multi_query": ["docA", "docE", "docC"]
}
return: [(doc_id, fused_score), ...]
"""
scores = {}
sources = {}

for route_name, ranked_docs in results_by_route.items():
for rank, doc_id in enumerate(ranked_docs, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
sources.setdefault(doc_id, []).append({
"route": route_name,
"rank": rank,
})

fused = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [
{"doc_id": doc_id, "rrf_score": score, "sources": sources[doc_id]}
for doc_id, score in fused
]

RRF 的工程价值在于简单、稳定、不需要训练;缺点是它仍然是粗排,不会真正理解 Query 和 Chunk 是否可以互相回答。因此 RRF 后面通常还要接 Rerank。

六、融合前后要做去重、来源追踪和截断

同一个 Chunk 可能被向量检索召回,也可能被 BM25 召回,还可能被某个 Query 变体召回。不要简单删除重复项后什么都不记录。正确做法是按 chunk_id 去重,但保留“被哪些路召回、各自排名、原始分数、命中词、命中 Query”等来源信息。

RAG 多路召回:别把答案押在一条检索链上配图

去重不是丢信息,而是把多路命中变成强相关信号

def merge_candidates(route_results):
candidate_map = {}

for route, docs in route_results.items():
for rank, doc in enumerate(docs, start=1):
doc_id = doc["id"]
if doc_id not in candidate_map:
candidate_map[doc_id] = {
"id": doc_id,
"text": doc["text"],
"metadata": doc.get("metadata", {}),
"routes": [],
}

candidate_map[doc_id]["routes"].append({
"route": route,
"rank": rank,
"score": doc.get("score"),
})

return list(candidate_map.values())

七、为什么 RRF 后还要 Rerank?

RRF 能解决“多份列表怎么合并”,但它不能深度判断一段文本是否真正回答了问题。Rerank 模型通常把 Query 和 Chunk 放在一起打分,比单独的向量相似度更细。

RAG 多路召回:别把答案押在一条检索链上配图

Rerank 的代价是延迟和成本。所以推荐做法是:各路先召回较小的 TopK,比如每路 20~50;RRF 合并去重后保留 TopN;再把 TopN 送入 Rerank;最后只把最相关的 5~10 条放进 Prompt。

八、并行全跑还是 Router 按需选择?

多路召回不是每次都要全部打开。电商型号类问题,Dense + BM25 就很有收益;用户问题非常模糊时,再加 Multi-Query;权限、时间、部门强约束的系统,先做 Metadata 过滤更重要。

RAG 多路召回:别把答案押在一条检索链上配图
async def retrieve_with_router(query, intent):
tasks = []

# 通用语义召回,默认开启
tasks.append(dense_retriever.ainvoke(query))

# 有型号、数字、错误码时,开启 BM25
if intent.has_exact_terms:
tasks.append(bm25_retriever.ainvoke(query))

# 用户问题口语化、上下文缺失或问法多变时,开启 Multi-Query
if intent.need_query_expansion:
rewritten_queries = await rewrite_query(query, n=4)
for q in rewritten_queries:
tasks.append(dense_retriever.ainvoke(q))

route_results = await gather_with_timeout(tasks, timeout_ms=1200)
candidates = merge_candidates(route_results)
fused = reciprocal_rank_fusion(to_ranked_lists(candidates))
return await rerank(query, fused[:50])

九、如何评估多路召回是否真的有效?

多路召回不是组件越多越好。每加一路,都会带来额外延迟、成本、噪声和排查复杂度。因此必须用评测集说话。

RAG 多路召回:别把答案押在一条检索链上配图

建议准备一批真实用户问题,给每个问题标注正确证据 Chunk。离线评测 Recall@K、MRR、nDCG;线上观察 P95 延迟、Token 成本、无答案率、引用命中率和人工反馈。只有“正确证据更早进入候选池”,多路召回才算有效。

十、生产级架构:多路召回要可观测、可降级、可回放

线上 RAG 系统最怕的是:回答错了,但不知道是 Query Rewrite 错、BM25 没命中、向量召回不准、RRF 融合不合理,还是 Rerank 把正确证据排掉了。所以多路召回必须把每一步日志留清楚。

RAG 多路召回:别把答案押在一条检索链上配图

推荐每次检索都记录:原始 Query、改写 Query、各路 TopK、每个候选的 route、rank、score、RRF 分、Rerank 分、最终进入 Prompt 的证据,以及最终答案引用了哪些证据。这样才能做失败样本回放。

十一、怎么选型:不要一上来就“全家桶”

RAG 多路召回:别把答案押在一条检索链上配图

如果知识库里大量产品型号、文档编号、接口名、报错码,优先加 BM25;如果用户提问方式变化大,优先加 Multi-Query;如果证据相关性要求高,优先加 Rerank;如果有强权限和结构化字段,优先做 Metadata Filter,而不是让 LLM 自己判断。

十二、常见坑与规避方案

RAG 多路召回:别把答案押在一条检索链上配图

最常见的误区是把多路召回理解成“查得越多越好”。实际上,召回越宽,后面越要有融合、去重、精排和预算控制。否则正确证据可能召回了,但被大量噪声淹没,大模型仍然答错。

十三、面试回答模板

如果面试官问“什么是多路召回?具体怎么做?”,可以这样回答:

多路召回就是不依赖单一检索器,而是同时使用向量检索、BM25 关键词检索、多 Query 扩展、元数据过滤等方式,从多个角度召回候选。向量检索负责语义相似,BM25 负责精确词,多 Query 负责解决用户问法和文档写法不一致的问题。多路结果先按 chunk_id 去重,再用 RRF 这类排名融合算法合成统一候选,最后接 Rerank 精排,把最相关的证据放进 Prompt。上线时我会用 Recall@K、MRR、nDCG、P95 延迟和成本做评估,而不是只看答案主观感觉。

要点速读

从向量检索、BM25、多 Query,到 RRF 融合与 Rerank 精排 一、多路召回解决的是“单一路径漏答案”的问

  • 从向量检索、BM25、多 Query,到 RRF 融合与 Rerank 精排 一、多路召回解决的是“单一路径漏答案”的问
  • 更多细节仍在持续更新中
  • 更多细节仍在持续更新中