周六晚上,小明要办一场家宴,宴请口味各异的客人。他列好菜谱后请了三位买手去菜市场采购:第一位买手只认菜名,菜谱写“鲈鱼”就绝不买“鲈鲤”;第二位买手凭经验,看到“肉质鲜嫩、适合清蒸”的鱼就会多留意;第三位买手按菜谱的原料特征逐项打钩,配料表里有“葱姜料酒”就去凑齐。三位买手各带回一篮子候选食材,可小明不能分开用三份菜,他得把三份候选清单合并成一份去采购,才能又全又准。 这套思路放在 RAG(检索增强生成)系统里,就是“多路召回融合”:向量检索、关键词检索、稀疏向量检索各显神通,分别拉回一批候选文档,再用融合算法合并排序,让最终进入大模型的文档覆盖得又广又准。单路召回就像只派一位买手,难免漏掉关键食材;多路互补再融合,召回率自然翻倍。

岗位关联

掌握多路召回融合,你就能设计出覆盖多种检索意图的 RAG 系统,在真实项目中用可控的延迟换取显著提升的召回率,这是检索工程师的核心竞争力。

目录
  1. 可视化演示
  2. 多路召回概述:三位买手的分工
  3. 向量检索:凭“口感”认菜的买手
  4. 关键词检索:只认菜名的买手
  5. 稀疏向量:按字典清单打钩的买手
  6. RRF 融合:不聊价格,只聊名次
  7. 加权和融合:论信任程度给分
  8. 工程权衡:多请买手的代价
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示向量/关键词/稀疏向量多路召回架构、RRF融合算法和召回率对比。交互切换多路、RRF和召回视图。

本节配有交互动效(进入视口后自动加载,约 3KB)。

多路召回架构 向量检索:语义相似 关键词:精确匹配 稀疏向量:混合 Q→{Vec,Kwd,Spr}→Fusion 三路并行召回后融合排序

多路召回概述:三位买手的分工

为什么不能只派一位买手?因为任何一位买手的能力再强,都有自己的盲区——只认菜名的买手听不懂“滑嫩弹牙的鱼”这种描述,凭口感的买手认不出“第 48 号标准调料包”。菜谱越复杂,单一买手漏采的风险越大。

在 RAG 系统里,“检索”是一条流水线:先“召回”再从海量文档中快速捞回可能相关的候选,再“重排”精挑细选,最后交给大模型生成回答。召回这一步不求十全十美,只求“千万别漏掉正确答案”,否则后面的重排和生成再强也无济于事。

衡量召回阶段的关键指标叫召回率。召回率回答的问题是:真正相关的文档,到底被抓回来多少?

多路召回的思路,就是多请几位特点不同的买手,把彼此的盲区互相补上,让“相关文档集”尽可能落在“召回文档集”里面。这就是召回率翻倍的第一个秘诀:先别谈精,先谈全。

Recall=∣Drel∩Drecall∣∣Drel∣\text{Recall}=\frac{|D_{\text{rel}}\cap D_{\text{recall}}|}{|D_{\text{rel}}|}
召回率 = 被召回的“真正相关文档”数 ÷ 全部“真正相关文档”数。分母固定,分子越大越接近 1。
  1. 相关文档集公式里的 $D_{\text{rel}}$ 代表所有真正符合用户问题的文档,相当于家宴菜谱真正需要的全部食材。
    Drel={所有真正相关的文档}D_{\text{rel}}=\{\text{所有真正相关的文档}\}
  2. 召回文档集$D_{\text{recall}}$ 是系统实际拉回的候选文档,对应三位买手各自买回来的食材篮。
    Drecall={检索系统实际召回的文档}D_{\text{recall}}=\{\text{检索系统实际召回的文档}\}
  3. 取交集分子是“既相关又被召回”的文档数量,只在一方出现的文档不会进入分子。
    ∣Drel∩Drecall∣|D_{\text{rel}}\cap D_{\text{recall}}|
  4. 算占比比值越接近 1 越理想,说明相关文档几乎全部被捞回,没有漏网之鱼。
    ∣Drel∩Drecall∣∣Drel∣∈[0,1]\frac{|D_{\text{rel}}\cap D_{\text{recall}}|}{|D_{\text{rel}}|}\in[0,1]

向量检索:凭“口感”认菜的买手

那位凭经验的买手,看的是“感觉相似”。客人说“想吃上次那种滑嫩弹牙的鱼”,买手不纠结具体菜名,货架上的冷冻鲈鱼和鲜活鲈鲤都可能入选,因为它们的口感、做法在记忆里是相近的。

RAG 里对应向量检索:把问题和文档都丢进嵌入模型(Embedding Model),转成一串数字向量。语义相近的文本在向量空间里距离就近,哪怕问题里没有文档中的任何原词,只要意思接近就能命中。这正是向量检索最独特的价值:扛得住同义改写和口语化表达。

数学上常用余弦相似度来衡量两个向量的“像不像”:

但“口感买手”也有盲区:只认感觉,不认准字。客人点名“三文鱼”时,它可能因为“大马哈鱼口感相似”而漏掉写着“进口挪威三文鱼”的摊位——于是需要那位只认菜名的精确买手来补位。

sim(q,d)=q⋅d∥q∥ ∥d∥=∑i=1nqidi∑i=1nqi2⋅∑i=1ndi2\text{sim}(\mathbf{q},\mathbf{d})=\frac{\mathbf{q}\cdot\mathbf{d}}{\|\mathbf{q}\|\,\|\mathbf{d}\|}=\frac{\sum_{i=1}^{n}q_i d_i}{\sqrt{\sum_{i=1}^{n}q_i^2}\cdot\sqrt{\sum_{i=1}^{n}d_i^2}}
余弦相似度把两个向量夹角的“像不像”转成 -1 到 1 之间的数,越接近 1 说明语义越接近。
  1. 向量点积两个向量的对应维度相乘后累加,衡量它们在同一方向上的共振强度。
    q⋅d=∑i=1nqidi\mathbf{q}\cdot\mathbf{d}=\sum_{i=1}^{n}q_i d_i
  2. 向量模长每个向量的长度,用来消除文本长短带来的尺度影响。
    ∥q∥=∑i=1nqi2\|\mathbf{q}\|=\sqrt{\sum_{i=1}^{n}q_i^2}
  3. 余弦相似度点积除以两个模长之积,把得分压缩到 -1 到 1 的固定区间。
    cos⁡(θ)=q⋅d∥q∥ ∥d∥\cos(\theta)=\frac{\mathbf{q}\cdot\mathbf{d}}{\|\mathbf{q}\|\,\|\mathbf{d}\|}
  4. 结果解读接近 1 表示语义几乎相同,接近 0 表示无关,负值表示语义相反。
    sim(q,d)∈[−1,1]\text{sim}(\mathbf{q},\mathbf{d})\in[-1,1]

关键词检索:只认菜名的买手

第二位买手绝对较真:菜谱写“鲈鱼”就要“鲈鱼”,鱼摊写“鲈鲤”他看都不看。优点是精准、不跑偏;缺点很致命——听不懂“那种滑嫩弹牙的鱼”这种模糊描述,一个字对不上就彻底放弃。

RAG 里对应关键词检索,工业界最经典的是 BM25。它统计两个信号:词频(TF)表示词在文档里出现多少次,逆文档频率(IDF)表示这个词有多稀有。词出现越多越加分,但“的”“了”这类遍地都是的词权重被压得很低,防止它们刷屏。

BM25 的打分公式长这样:

“菜名买手”的价值在于:当用户问题里带有明确实体和编号——比如“合同 HT-2024-001”“鲈鱼清蒸做法”——它能一个字不差地命中。它的盲区是认不出同义改写,于是需要按特征打钩的第三位买手。

score(D,Q)=∑i=1nIDF(qi)⋅f(qi,D) (k1+1)f(qi,D)+k1(1−b+b∣D∣avgdl)\text{score}(D,Q)=\sum_{i=1}^{n}\text{IDF}(q_i)\cdot\frac{f(q_i,D)\,(k_1+1)}{f(q_i,D)+k_1\left(1-b+b\frac{|D|}{\text{avgdl}}\right)}
BM25:对查询里的每个词,先用 IDF 衡量词的稀有程度,再乘上一个被文档长度调节过的词频项,最后全部累加。
  1. 查询词遍历对查询里的每一个词项分别算贡献,再统一相加,对应“菜名里的每个字都要对一遍”。
    ∑i=1n(⋅)\sum_{i=1}^{n}(\cdot)
  2. 稀有度加权IDF 让常见词权重低、稀有词权重高,越独特的词越能区分食材。
    IDF(qi)=log⁡N−df(qi)+0.5df(qi)+0.5\text{IDF}(q_i)=\log\frac{N-\text{df}(q_i)+0.5}{\text{df}(q_i)+0.5}
  3. 词频饱和项出现次数越多分越高,但增长越来越慢,防止某个词刷屏把整份文档顶上去。
    f(qi,D)(k1+1)f(qi,D)+k1\frac{f(q_i,D)(k_1+1)}{f(q_i,D)+k_1}
  4. 长度归一化用文档长度除以平均长度来打折,长文档不容易因为“话多”而占便宜。
    1−b+b∣D∣avgdl1-b+b\frac{|D|}{\text{avgdl}}

稀疏向量:按字典清单打钩的买手

第三位买手随身带一本“特征清单册”(对应词表)。每到一个摊位他就翻册子打钩:这摊有“葱”,打一钩;有“姜”,再打一钩;有“鲈鱼”,重点打两钩。回来交差时,他交的不是菜名也不是口感描述,而是一串“哪些特征出现、出现几次”的钩子列表。

RAG 里这就是稀疏向量(Sparse Vector):向量的维度是词表里的每个词,文档拥有哪些词,就在对应维度上填入权重(通常用词频 × IDF)。因为大部分维度是 0,所以是“稀疏”的。它夹在向量检索和关键词检索之间:既用了词的统计信息,又保留了“哪些特征被命中”的可解释性。

两篇文档是否匹配,看它们打钩的重叠程度,数学上就是内积:

三位买手各有所长,也各有盲区,谁都不能独立完成任务。现在问题来了:三份候选清单,怎么合并成一份?这就到了整篇文章最核心的融合环节。

s(q,d)=∑t∈Vxq,t⋅xd,t,xd,t=TF(t,d)×IDF(t)s(q,d)=\sum_{t\in\mathcal{V}}x_{q,t}\cdot x_{d,t},\qquad x_{d,t}=\text{TF}(t,d)\times\text{IDF}(t)
稀疏向量打分 = 查询和文档共现词项的权重相乘再累加,语义上相当于“两张特征钩子清单对一对”。
  1. 词表空间$\mathcal{V}$ 是所有可能出现的词构成的高维空间,对应那本厚厚的特征清单册。
    V={t1,t2,…,tm}\mathcal{V}=\{t_1,t_2,\ldots,t_m\}
  2. 词项权重每个词维度上的值由词频与 IDF 共同决定,出现越多、越稀有,权重越大。
    xd,t=TF(t,d)×IDF(t)x_{d,t}=\text{TF}(t,d)\times\text{IDF}(t)
  3. 内积匹配查询和文档共现的词越多,内积越大,对应“打钩重叠”越多。
    s(q,d)=∑t∈Vxq,t xd,ts(q,d)=\sum_{t\in\mathcal{V}}x_{q,t}\,x_{d,t}
  4. 可解释性每一分都能追溯到具体共现词,方便分析“为什么命中”。
    共现词集={t∣xq,t>0, xd,t>0}\text{共现词集}=\{t\mid x_{q,t}>0,\ x_{d,t}>0\}

RRF 融合:不聊价格,只聊名次

小明拿到三份清单,犯了难:买手 A 说鲈鱼最好,买手 B 压根没买鲈鱼;买手 C 把豆腐排在第二,买手 A 却把豆腐排在第十。三份清单的打分标准各不相同——有的写价格,有的写新鲜度——根本没法直接比大小。

一个聪明的办法是:别管分数,只看排名。这就是 RRF 融合(Reciprocal Rank Fusion,倒数排名融合):某样食材在每份清单里的名次越靠前,就给它发越多“名次分”,最后把几份清单的名次分全部相加。

这就像小明对三位买手说:“别跟我报价了,就说你们各自觉得哪几样最值得买,按推荐顺序排个队。”名次这种“软信息”天然可比,完全绕开了各路分数尺度不一致的难题——这是 RRF 在工业界广为流行的根本原因。

RRF(d)=∑r∈R1k+rankr(d)\text{RRF}(d)=\sum_{r\in\mathcal{R}}\frac{1}{k+\text{rank}_r(d)}
RRF:文档 $d$ 在每条检索路的结果里排第几名,就贡献 $1/(k+\text{名次})$ 分;把所有路的贡献加起来,就是最终得分。
  1. 候选路集合$\mathcal{R}$ 是所有检索路(向量、关键词、稀疏向量等)的集合,对应三位买手。
    R={向量路,关键词路,稀疏向量路}\mathcal{R}=\{\text{向量路},\text{关键词路},\text{稀疏向量路}\}
  2. 名次贡献文档在某一路排第几,就贡献一个倒数分,名次越靠前分数越大。
    1k+rankr(d)\frac{1}{k+\text{rank}_r(d)}
  3. 平滑参数 k$k$ 常用 60,它让名次靠后的文档惩罚不会过于剧烈,避免微小抖动改变结局。
    k=60k=60
  4. 跨路累加文档在所有路中的贡献全部相加,多路都靠前的文档分数叠加,稳居前列。
    RRF(d)=∑r∈R1k+rankr(d)\text{RRF}(d)=\sum_{r\in\mathcal{R}}\frac{1}{k+\text{rank}_r(d)}

加权和融合:论信任程度给分

但小明很快发现一个隐患:三位买手的靠谱程度不一样。买手 A 认得所有食材,从不出错;买手 C 对新上市的食材不太熟,偶尔买错。如果三份清单完全一视同仁,岂不是把靠谱和不靠谱混为一谈?

加权和融合(Weighted Sum)的思路是:先把各路分数换算到同一把尺子上(归一化),再按信任程度乘以权重相加。比如用户搜“苹果”,既想要 Apple 产品也想要水果,就给商品路权重 0.7、百科路权重 0.3,让两种意图都被照顾到。

归一化最常用 min-max 缩放,把每路分数压到 0 到 1 之间:

小明最后决定:买鱼听买手 A 的(权重 0.6),买菜叶听买手 C 的(权重 0.4),其余五五开。加权和的价值就是把“信任”变成可调节的数字;代价则是必须事先校准各路分数,工程上比 RRF 更费劲。

sfuse(d)=∑r∈Rwr s^r(d),s^r(d)=sr(d)−smin⁡smax⁡−smin⁡s_{\text{fuse}}(d)=\sum_{r\in\mathcal{R}}w_r\,\hat{s}_r(d),\qquad\hat{s}_r(d)=\frac{s_r(d)-s_{\min}}{s_{\max}-s_{\min}}
加权和:把每路原始分数缩放到 [0,1],再乘上这路的权重,最后把各路加起来得到融合分。
  1. 原始分数不同路的分数量纲不同,价格分和新鲜度分不能直接相加,必须先统一尺子。
    s1(d), s2(d), …s_1(d),\ s_2(d),\ \ldots
  2. 极值归一化用 min-max 缩放把每路分数映射到 0 到 1,消除量纲差异。
    s^r(d)=sr(d)−smin⁡smax⁡−smin⁡\hat{s}_r(d)=\frac{s_r(d)-s_{\min}}{s_{\max}-s_{\min}}
  3. 信任权重$w_r$ 是给第 $r$ 路的权重,越大说明越信任这一路(常人工设定或离线调参)。
    wr∈[0,1],∑rwr=1w_r\in[0,1],\quad \sum_{r}w_r=1
  4. 加权求和归一化分乘权重再相加,得到每篇文档的最终融合分。
    sfuse(d)=∑r∈Rwr s^r(d)s_{\text{fuse}}(d)=\sum_{r\in\mathcal{R}}w_r\,\hat{s}_r(d)

工程权衡:多请买手的代价

小明一算账吓了一跳:请三位买手同时出门,菜确实买全了,但跑腿时间、车费、沟通成本都翻了几倍。要是每道菜都让三位买手跑一趟,晚宴怕是凌晨才开席。

工程上同理。多路召回意味着同时跑向量索引、BM25 索引、稀疏向量索引,每路都有独立计算开销和 IO 延迟。召回路数翻倍,延迟和资源成本往往跟着上涨,这是多路召回最直接的代价。

好在各路检索彼此独立,可以并发执行。总延迟近似等于最慢一路的耗时,加上融合开销和重排开销:

聪明的做法是“粗召回 + 细重排”:多路召回先把候选从几万压到几十个,重排只对这几十个精排;对高频问题再把结果缓存起来。买手多跑腿没关系,只要赶在开席前把菜交齐、主厨精挑细选不超时,这桌晚宴就能准时开席。

Ltotal≈max⁡r∈RLr+Lfuse+LrerankL_{\text{total}}\approx\max_{r\in\mathcal{R}}L_r+L_{\text{fuse}}+L_{\text{rerank}}
端到端延迟 ≈ 最慢那路检索的耗时 + 融合耗时 + 重排耗时。并发执行让“多路”不必变成“多倍延迟”。
  1. 最慢一路$\max_{r}L_r$ 体现并发优势——三路同时跑,总等待取决于最慢的买手。
    max⁡(L1,L2,L3)\max(L_1,L_2,L_3)
  2. 融合开销$L_{\text{fuse}}$ 是对少量候选做合并排序的耗时,在 RRF 下几乎可以忽略。
    Lfuse≈O(nlog⁡n)L_{\text{fuse}}\approx O(n\log n)
  3. 重排开销候选越多,重排模型推理次数越多,延迟随之上升。
    Lrerank∝candidatesL_{\text{rerank}}\propto\text{candidates}
  4. 延迟预算端到端延迟必须压在 SLA 以内,必要时削减路数或候选数。
    Ltotal≤LbudgetL_{\text{total}}\le L_{\text{budget}}

AI 应用场景

企业知识库智能问答

阶段:检索(召回)阶段

解决的问题:企业内部文档同时存在精确术语(“合同编号 HT-2024-001”)和模糊口语(“咱们去年和甲方签的那份协议”)。单一向量检索对精确编号不敏感,单一 BM25 又匹配不出口语语义,只有多路互补才能两头兼顾。

具体案例:某银行法务部门的知识库问答系统。员工问“去年跟启明科技签的采购框架协议还有效吗”——向量路召回了“采购框架协议”“合同有效期”等语义相近的文档,BM25 路召回了含“启明科技”"2024"等字词的文档,RRF 融合后把两份关键文档都排进 Top 3,大模型得以拼出完整回答。

电商搜索的意图分流

阶段:召回 + 重排流水线

解决的问题:用户搜“苹果”,既想要 iPhone 也想要水果。单路检索只能押注一种意图,另一类商品会被系统性漏掉,导致召回率低、转化率差。

具体案例:某电商平台搭建三路召回——向量路召回"iPhone 15"“苹果笔记本电脑”等语义商品,关键词路召回标题含“苹果”的应季水果,稀疏向量路召回同时包含“苹果+礼盒”等组合特征的店铺。加权和融合后,两类商品都进入候选池,再结合用户历史点击做个性化精排。

法律条文与学术论文检索

阶段:检索召回阶段

解决的问题:法规条文有固定编号(“第四十八条”),用户却常描述成“关于违约金的那条”。精确编号靠关键词路一秒钟命中,语义描述靠向量路理解意图,两者缺一不可。

具体案例:某法律科技公司把现行法规库做过路召回:BM25 对“第四十八条”这类编号精确匹配,向量路对“合同违约怎么办”这类口语查询做语义召回,最后用 RRF 合并,让法官和律师在检索时既不漏编号、也不漏语义。

Python 演示

python105 行
# -*- coding: utf-8 -*-"""多路召回融合演示: 向量 + BM25 + 稀疏向量, RRF 与加权和两种融合."""import math CORPUS = [    "苹果发布新一代智能手机",    "苹果种植技术有哪些要点",    "手机销量排名出炉",    "智能音箱市场份额报告",    "冬季苹果保存方法",    "国产手机品牌出海策略",] def tokenize(text):    # 简易分词: 按单字切分, 仅用于演示    return list(text) def build_idf(corpus):    n = len(corpus)    df = {}    for doc in corpus:        for w in set(tokenize(doc)):            df[w] = df.get(w, 0) + 1    idf = {}    for w, cnt in df.items():        idf[w] = math.log((n + 1) / (cnt + 0.5))    return idf def bm25(query, doc, idf):    q_terms = tokenize(query)    doc_terms = tokenize(doc)    dl = len(doc_terms)    avgdl = sum(len(tokenize(d)) for d in CORPUS) / len(CORPUS)    k1, b = 1.5, 0.75    score = 0.0    for t in q_terms:        tf = doc_terms.count(t)        if tf == 0:            continue        numerator = tf * (k1 + 1)        denominator = tf + k1 * (1 - b + b * dl / avgdl)        score += idf.get(t, 0) * numerator / denominator    return score def embed(text):    vec = {}    for w in tokenize(text):        vec[w] = vec.get(w, 0) + 1    return vec def cosine(vec_a, vec_b):    dot = sum(vec_a[w] * vec_b[w] for w in set(vec_a) & set(vec_b))    na = math.sqrt(sum(v * v for v in vec_a.values()))    nb = math.sqrt(sum(v * v for v in vec_b.values()))    if na == 0 or nb == 0:        return 0.0    return dot / (na * nb) def sparse_score(query, doc, idf):    qv = embed(query)    dv = embed(doc)    return sum(idf.get(w, 0) * qv[w] * dv[w] for w in set(qv) & set(dv)) def rrf_fuse(ranked_lists, k=60):    scores = {}    for lst in ranked_lists:        for rank, doc_id in enumerate(lst, start=1):            scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank)    return sorted(scores.items(), key=lambda x: x[1], reverse=True) def weighted_fuse(score_lists, weights):    doc_map = {}    for i, sc in enumerate(score_lists):        vals = list(sc.values())        vmin, vmax = min(vals), max(vals)        for doc_id, v in sc.items():            if vmax > vmin:                v = (v - vmin) / (vmax - vmin)            else:                v = 1.0            doc_map[doc_id] = doc_map.get(doc_id, 0) + weights[i] * v    return sorted(doc_map.items(), key=lambda x: x[1], reverse=True) def main():    query = "苹果手机"    idf = build_idf(CORPUS)    kw = {i: bm25(query, doc, idf) for i, doc in enumerate(CORPUS)}    vc = {i: cosine(embed(query), embed(doc)) for i, doc in enumerate(CORPUS)}    sp = {i: sparse_score(query, doc, idf) for i, doc in enumerate(CORPUS)}     keyword_rank = [d for d, _ in sorted(kw.items(), key=lambda x: x[1], reverse=True)[:3]]    vector_rank = [d for d, _ in sorted(vc.items(), key=lambda x: x[1], reverse=True)[:3]]    sparse_rank = [d for d, _ in sorted(sp.items(), key=lambda x: x[1], reverse=True)[:3]]     fused = rrf_fuse([keyword_rank, vector_rank, sparse_rank], k=60)    fused_w = weighted_fuse([kw, vc], [0.5, 0.5])     print("Keyword top3:", [CORPUS[i] for i in keyword_rank])    print("Vector  top3:", [CORPUS[i] for i in vector_rank])    print("Sparse  top3:", [CORPUS[i] for i in sparse_rank])    print("RRF     top3:", [(CORPUS[i], round(s, 4)) for i, s in fused])    print("Weighted top:", [(CORPUS[i], round(s, 4)) for i, s in fused_w]) if __name__ == "__main__":    main()
目的:在一个微型中文语料上实现三路召回(BM25、余弦向量、稀疏向量内积),再用 RRF 和加权和分别融合,直观对比最终排序差异。
  1. df[w] = df.get(w, 0) + 1统计每个词出现在多少篇文档里,是 IDF 的基础。
  2. score += idf.get(t, 0) * numerator / denominatorBM25 核心累加,每个查询词贡献一部分分数。
  3. dot = sum(vec_a[w] * vec_b[w] for w in set(vec_a) & set(vec_b))只对两个向量共有的词做点积。
  4. return dot / (na * nb)把点积除以长度,得到余弦相似度。
  5. scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank)RRF 融合核心:按排名发倒数分。
  6. v = (v - vmin) / (vmax - vmin)min-max 归一化,把分数压到 0 到 1。
  7. doc_map[doc_id] = doc_map.get(doc_id, 0) + weights[i] * v加权求和,把各路贡献累加。
  8. keyword_rank = [d for d, _ in sorted(kw.items(), key=lambda x: x[1], reverse=True)[:3]]取关键字路 Top 3 作为候选。
  9. fused = rrf_fuse([keyword_rank, vector_rank, sparse_rank], k=60)三路候选汇合,RRF 合并排序。

常见误区

误区我上线了多路召回,把向量、BM25、稀疏向量全加上了,大模型回答的准确率一定翻倍。

正解多路召回提升的是“候选覆盖”,也就是把正确答案捞进候选集的概率;最终准确率还取决于重排和生成。就像买手买全了食材,主厨手艺不行,菜照样难吃。若重排环节太弱,新增候选反而会引入噪声。

为何常见:容易把“召回率”和“准确率”混为一谈,毕竟都带“率”字,而且在简单评测集上候选变多确实常常带来明显上涨。

误区RRF 和加权和一样,得先把各路分数转到 0 到 1 再套公式,不然不公平。

正解RRF 完全不使用原始分数,只用排名。它的设计初衷恰恰是绕开各路分数尺度不可比的难题——就像三位买手按“推荐顺序”而非“报价”合并清单。归一化是加权和才需要做的事。

为何常见:习惯了加权和的流程,看到“融合”两个字就想当然先归一化,没注意到 RRF 公式里根本没有分数变量。

误区为了召回率最大化,我把能想到的检索方式全加上,路数越多系统越强。

正解每多一路都带来额外延迟、存储和维护成本;而且各路结果重复度高时边际收益递减,冗余候选还会拖累重排。工程上应做消融实验,找到“再加一路提升不明显”的拐点。

为何常见:直觉上“越多越全”,忽视了资源约束和边际收益递减规律,把多路召回理解成堆料。

误区加权和嘛,给向量 0.8、BM25 给 0.2,看着差不多就行,不用那么讲究。

正解权重本质上是各路“可信度”,拍脑袋定权重容易让某一路完全主导、另一路形同虚设。规范做法是在验证集上做网格搜索或贝叶斯调参,用召回率或 NDCG 选择最优权重。

为何常见:权重看起来只是一个 0 到 1 的小数,容易轻视它对融合结果的一票否决式影响。

误区把三路的 Top 10 直接丢给重排器,就算完成多路召回了。

正解直接拼接会产生大量重复候选(同一文档出现在多路中),顺序也冲突无序。需要去重 + 合并排序(RRF 或加权和),让重复出现、各路都靠前的文档真正排到前面。

为何常见:多路召回的字面意思让人误以为只差“拼接”这一步,忽略了“融合”才是把多路价值放大的关键。

面试题

Q1. 多路召回融合的首要目标是?

单选题 · easy

  • A.提升单路检索精度
  • B.提升召回覆盖率← 正确答案
  • C.显著降低计算开销
  • D.完全替代重排流程

答案:B。多路召回通过互补覆盖各类检索意图,核心是提高“正确答案进入候选集”的概率,即召回率。它不直接提升单路精度,也不负责降低开销或替代重排。

Q2. BM25 主要依赖哪类信号打分?

单选题 · easy

  • A.语义向量距离
  • B.词频与稀有度← 正确答案
  • C.图结构关系
  • D.用户点击行为

答案:B。BM25 用词频(TF)衡量词在文档中的出现次数,用逆文档频率(IDF)衡量词的稀有度。它不涉及向量距离、图关系或点击行为。

Q3. 向量检索最擅长解决哪类问题?

单选题 · easy

  • A.精确匹配词面
  • B.语义近义改写← 正确答案
  • C.词形还原处理
  • D.候选精排调序

答案:B。向量检索把文本映射到语义空间,“苹果手机”和"iPhone"即使没有共同词也能靠语义距离命中。精确匹配是关键词路的强项,重排是重排模型的职责。

Q4. RRF 融合的核心输入是什么?

单选题 · mid

  • A.各路候选排名← 正确答案
  • B.各路原始分数
  • C.归一化后概率
  • D.文档嵌入向量

答案:A。RRF 公式 $\frac{1}{k+\text{rank}}$ 只用排名,不读原始分数,也不要求归一化。它正是为了规避各路分数尺度不一致而设计的。

Q5. 加权和融合前通常需要先做什么?

单选题 · mid

  • A.各路分数归一化← 正确答案
  • B.各路排名倒序
  • C.词频逆文档统计
  • D.特征哈希降维

答案:A。不同检索路的分数量纲不同(价格分和新鲜度分不能直接相加),必须先做 min-max 归一化统一尺子,再乘权重相加。

Q6. RRF 公式中的 k 参数主要作用是?

单选题 · mid

  • A.平滑排名靠后的惩罚← 正确答案
  • B.控制各路权重比例
  • C.决定召回候选总数
  • D.设置向量检索维度

答案:A。k 加在分母上,用于降低名次靠后文档的惩罚幅度,避免排名抖动过分影响结果,常取 60。它不控制权重、候选数或向量维度。

Q7. 某文档在向量路排第 2、关键词路排第 3,RRF 取 k=60 时得分约为?

单选题 · extreme

  • A.约 0.0161
  • B.约 0.0320← 正确答案
  • C.约 0.0484
  • D.约 0.0645

答案:B。两块贡献分别是 $1/(60+2)=1/62\approx0.016129$ 和 $1/(60+3)=1/63\approx0.015873$,相加约等于 0.0320。选 B。

Q8. 下列哪些检索方式适合组成多路召回?

多选题 · mid

  • A.语义向量检索← 正确答案
  • B.关键词 BM25← 正确答案
  • C.稀疏向量检索← 正确答案
  • D.全量文档人工标注

答案:ABC。向量、BM25、稀疏向量都是可在线执行的检索路,各自擅长不同意图。全量人工标注是离线数据工程,不是在线检索方式,不适合作为召回路。

Q9. 关于 RRF 与加权和融合,正确的说法是?

多选题 · hard

  • A.RRF 只用排名不看分数← 正确答案
  • B.加权和需要分数归一化← 正确答案
  • C.两者都需要重排模型
  • D.两者都能合并多路结果← 正确答案

答案:ABD。RRF 用倒数排名免去归一化;加权和必须归一化再加权。两者都只是候选合并手段,不依赖重排模型,C 错误。

Q10. 多路召回融合在工程上需要考虑哪些问题?

多选题 · mid

  • A.重复候选去重合并← 正确答案
  • B.延迟与资源开销← 正确答案
  • C.召回率必然下降
  • D.重排候选数的控制← 正确答案

答案:ABD。同一文档可能被多路同时召回,需要去重合并;多路并发带来延迟和资源成本;候选增多会让重排变贵,需控制数量。召回率不会必然下降,C 错误。