文章 / Agent

从关键词到语义:为什么 RAG 一定要做混合检索

问「邮费是多少」,知识库里明明有「运费怎么算」,系统却答「未找到」。这不是模型笨,是检索方式选错了。

2026-10-13 约 12 分钟
RAG检索

一个真实失败案例

先看一个我实际遇到过的场景。知识库里有这么一条:

「运费怎么算?」

—— 单笔订单满 99 元免运费;不满 99 元的订单,全国统一收取 8 元运费。

knowledge_docs/运费说明.md

用户问的是:「邮费是多少?」

如果检索用的是关键词匹配(SQL 里最常见的那种 WHERE question LIKE '%关键词%'), 系统会拿「邮费」去比对,而文档里写的是「运费」—— 两个字对不上,结果是查不到, 模型只能回答「抱歉,我没有找到相关信息」。

这就是 RAG 落地时最常见的第一个坑:能答的题,却因为检索没找着而答不上。

关键词检索:死在同义表达

关键词检索的原理非常直接:把问题拆成词,去文档里找有没有这些词。 常见的实现是 BM25 —— 它比简单的 LIKE 匹配聪明一些,会考虑词频和文档长度,但本质仍然是「字面匹配」。

它的优势在哪

  • 精确匹配极准:查型号 W40、订单号 20260901001,字面匹配一次就中,不会被打岔。
  • 无需训练、无需模型:建索引快,成本低。
  • 可解释:为什么召回这一条,能明确说出是哪个词命中的。

它什么时候会失灵

一旦用户的说法和文档的写法不一致,它就完全无能为力。这类情况在中英文里都极其常见:

  • 同义词:邮费 / 运费,退钱 / 退款,保修 / 质保
  • 口语化:这东西坏了想换 / 申请换货流程
  • 语序不同:怎么退 / 退款怎么申请
  • 跨语言:用户写英文,文档是中文

注意

这类失败最危险的地方在于:它不报错。系统正常返回「没有找到」,你从日志上看不出任何异常, 只有用户知道答案明明就在库里。

向量检索:死在精确细节

向量检索换了个思路:不比对字,而是比对意思。 先把每段文本送进嵌入模型,变成一个高维向量;检索时把问题也变成向量,然后找余弦相似度最高的几段。

这样一来,「邮费」和「运费」在向量空间里几乎挨在一起,上面那个失败案例就解决了。 但向量检索有自己的死穴 —— 它对精确的、没有语义的数字和代号非常不敏感。

假设商品规格里有四个型号:饮水机 W20 / W40 / W60,猫砂盆 LP50。 用户问「W40 的滤芯多久换一次」。 向量检索很可能把 W20、W60 的段落也一起召回 —— 因为它们语义上几乎一样, 都是「饮水机滤芯更换周期」。

换成关键词检索,W40 这个字符串会精确命中,一次就到位。

所以两种方式的问题正好相反:

  • 关键词:抓得准,但认不出同义说法
  • 向量:认得同义说法,但分不清近似型号

两种方式的实测对比

光讲道理不够直观。下面这个组件可以切换检索方式,看同一个问题在不同方式下的召回结果 —— 数据来自我在项目里实测的真实分数。

召回结果对比 · 同一个问题 点击切换
退货政策 · 七天无理由 doc:退货政策.md
—
售后手册 · 联系方式 doc:售后手册.md
—

BM25 拿「邮费」去比对,但文档里写的是「运费」。两个字对不上 —— 正确答案「运费说明」压根没有被召回。注意系统不会报错,只会说「没找到」。

可以看到:关键词方式下「运费说明」压根不在结果里;向量方式能召回它,但顺带把两条不相关的也拉进来了; 混合检索加上重排之后,正确答案稳定排在第一位。

RRF:怎么把两路结果合起来

既然两种方式各有长短,自然的想法就是把它们同时跑,再把两份结果合成一份。问题是:怎么合?

最朴素的思路是「按分数加权」。但这里有个麻烦:BM25 的分数范围可能是 0 到 20 多, 向量相似度是 0 到 1,两个刻度完全不可比。硬加权的话,一方的分数会直接淹没另一方。

所以更常用的是 RRF(Reciprocal Rank Fusion,倒数排名融合)。 它干脆不看分数,只看排名:

# RRF 的核心:只用排名,不用原始分数
def rrf_score(ranks, k=60):
    """ranks: 该文档在各路检索中的排名(从 1 开始)"""
    return sum(1.0 / (k + r) for r in ranks)

# 例:某文档在 BM25 排第 1、在向量检索排第 5
rrf_score([1, 5])  # = 1/61 + 1/65 ≈ 0.0318

# 另一文档在两边都排第 3
rrf_score([3, 3])  # = 1/63 + 1/63 ≈ 0.0317

注意上面这两行的对比:一个第 1 名 + 一个第 5 名,得分几乎等于两个第 3 名。 这正是 RRF 想要的效果 —— 它奖励「在多个榜单里都靠前」的文档,而不是「在某一个榜单里特别突出」的。

提示

常数 k 一般取 60(来自原论文的经验值)。它的作用是削弱第一名和后面名次的差距, 让融合结果更平滑。k 越小,头部排名的影响越大。

别急着上重排

说到这里要泼一盆冷水:混合检索不是所有场景都需要,重排更是。 Anthropic 在他们的工程博客里反复强调一个原则:

we recommend finding the simplest solution possible, and only increasing complexity when needed.

(建议先找最简单的方案,只在确实需要时才增加复杂度。)

Anthropic Engineering · Building Effective Agents

判断要不要加,最直接的方法是先做评估集。我自己的做法是:

  1. 手工标注 20-30 条问题,标出「正确答案应该来自哪一段」
  2. 分别用纯关键词、纯向量、混合检索跑一遍,记录正确答案进入 Top-K 的比例
  3. 如果纯向量的准确率已经够了,就停在那里 —— 每多一层,就多一份延迟和成本

我是这样做的:先收录一批「换说法提问」的样本,跑纯向量检索,发现同义问题基本能召回, 但型号类问题的精确度明显不够 —— 这时候才决定引入 BM25 做双路。

注意

重排模型通常要额外加载几百 MB 到几 GB 的权重,明显增加首字延迟。 如果检索本身的准确率还不到 80%,先优化切块策略,而不是加重排。

顺带一提,切块策略本身也会影响召回质量:把章节标题一起带进 chunk, 比只切正文段落效果好 —— 标题本身就是这一段内容的「语义摘要」。

小结

  • 关键词和向量不是「谁替代谁」,而是各自解决不同的问题
  • 同义表达靠向量,精确型号靠关键词,两个都要就双路召回再用 RRF 融合
  • RRF 的价值在于只看排名不看分数,天然规避了不同量纲无法比较的问题
  • 每加一层都要有评估数据支撑,不要凭感觉堆复杂度