深蓝黑背景封面。左侧是一张双路召回汇合图:顶部卡片「用户查询」向下分出两条并行通道——左通道青色卡片「向量检索:语义相近的都能捞到」,注「同义改写也不怕」;右通道紫色卡片「关键词检索 BM25:词必须命中」,注「型号、编号、专有名词靠它」;两路箭头共同汇入底部青色高亮卡片「RRF 融合排序 → 召回更全的候选集」,注「只看名次不看分数,两路互相补短板」。右侧大标题「RAG 为什么要把向量检索和关键词检索混着用」,副标题「混合检索:补上纯向量召回的盲区」。左上标注「面试经典 · 检索增强」,右上圆角胶囊标注「Day 06 · 11-23」。 图 1:双路召回各管一段盲区,RRF 负责把两路捏成一份名单。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。RAG 系统的第一道质量关口是召回——检索不到对的材料,后面生成再强也是白搭。而「为什么向量检索之外还要混一路关键词检索」正是考察召回理解的经典题。

向量检索的强项和它的三个盲区。

向量检索把查询和文档都编码成向量,按语义距离捞相近的块。它最大的优点是抗改写:「怎么退款」和「退货流程是什么」字面完全不同,向量空间里却是近邻,同义、口语化、跨语言表达都能命中。但语义相近不等于该命中,纯向量检索有三类典型盲区:

  • 型号与编号:查「XYZ-3000 的驱动」,语义上相近的文档一大把,但用户要的就是这个词,向量分数再高也替代不了字面命中;
  • 专有名词与罕见术语:训练语料里出现得少的词,嵌入容易被稀释成「普通词」,区分度不足;
  • 长尾关键词组合:多个条件同时约束时,向量只能给出整体语义的折中,精确匹配某个冷门条件的那篇可能被挤出前列。

关键词检索:BM25 补的正是这一块。

关键词路线的代表是 BM25,思路与向量正相反:词必须命中,再按统计特征打分。它的公式可以拆成三个直觉:词频饱和——一个词出现 10 次不比出现 3 次重要十倍(k1 控制饱和曲线);逆文档频率——越稀有的词越值钱;长度归一——长文档天然词多,要按篇幅打折(b 控制归一强度)。Elasticsearch 至今默认用 BM25 作为相似度算法(默认 k1=1.2、b=0.75),经过几十年检验,在「精确词命中」场景上依然很难被向量替代。

融合的难点:两路分数根本不可比。

最直接的想法是各取 top 然后按分数加权合并,但这条路有个暗坑:向量相似度和 BM25 分数的量纲、分布完全不同,直接相加等于拿厘米和公斤做加法,归一化参数还得随着数据分布反复调。

工程上更常用的是 RRF(Reciprocal Rank Fusion,倒数名次融合):干脆扔掉原始分数,只看名次。每个文档的融合分数是它在各路人马里名次的倒数之和:

score(d) = Σ 1 / (k + rank)

rank 从 1 开始,k 是平滑常数,通常取 60。这个公式 2009 年 SIGIR 上的一篇论文提出后,因为「无需调参、无需分数归一、效果稳定超过单路」成了混合检索的标配——名次本身就是统一量纲,两路原始分数完全不用见面。

用代码把 RRF 算一遍。

假设向量路召回前三为 d1、d2、d3,关键词路前三为 d2、d5、d1,用 Node.js 22.15.0 实测(node rrf.js):

function rrf(rankings, k = 60) {
  const score = new Map();
  for (const list of rankings)
    list.forEach((doc, i) =>
      score.set(doc, (score.get(doc) || 0) + 1 / (k + i + 1)));
  return [...score.entries()].sort((a, b) => b[1] - a[1]);
}
const vector = ['d1', 'd2', 'd3'];
const keyword = ['d2', 'd5', 'd1'];
for (const [doc, s] of rrf([vector, keyword]))
  console.log(doc, s.toFixed(6));

实测输出:

d2 0.032522
d1 0.032266
d5 0.016129
d3 0.015873

d2 在两路都靠前,总分反超向量路的第一名 d1;只在单路出现的 d5、d3 排在其后。这正是混合检索想要的行为:两路交叉验证过的文档稳定上浮,单路独有的候选也不会被另一路的高分挤掉。

深蓝黑背景概念图。左侧紫色描边卡片「纯向量检索的三个盲区」,列出:型号与编号「语义不像,但就是要这个词」、专有名词「嵌入里被稀释成普通词」、长尾关键词「向量空间里没有近邻」。右侧是 RRF 融合打分示例:两张名次表——向量路名次为 d1 第 1、d2 第 2、d3 第 3,关键词路名次为 d2 第 1、d5 第 2、d1 第 3;中间公式「score(d) = Σ 1 / (k + rank)」,融合结果以青色高亮「d2 两路都靠前,总分最高」,其后依次 d1、d5、d3。底部总结条:k 常取 60,名次靠前的文档稳定上浮,且不需要把两路原始分数归一化——名次本身就是统一量纲。 图 2:左边列盲区,右边算 RRF,两路名次一融合,d2 浮出水面。

落地时再补两个工程细节。

一是每路各取多大的窗口:窗口太小,融合没有原料,一般每路各取几十条候选再融合;二是融合之后通常还要接一道精排(Rerank),用交叉编码器对候选逐条重打分,RRF 负责「召回更全」,精排负责「排得更准」,两者分工不同。

面试答题建议按「盲区 → 补短 → 融合」三步走:先讲纯向量检索在型号、专名、长尾词上的失分案例,再讲 BM25 的词命中与统计打分如何互补,最后落在 RRF 的名次融合公式和 k=60 的约定上,补一句精排收尾。能主动指出「两路原始分数不可比,所以用名次而不是分数融合」,就把这道题从背诵题答成了理解题。你在项目里用的是哪路融合策略?有没有遇到过向量分数和关键词分数打架的情况?欢迎聊聊。