系列导读:本文是"检索与 RAG"系列的第二篇——在《RAG检索增强生成——让大模型学会翻书作答》认识了 RAG 之后,本文深挖 RAG 的检索环节:为什么纯 BM25 词法检索会"权重大但不相关",以及两阶段检索如何救赎。本系列阅读顺序:

  1. 《RAG检索增强生成——让大模型学会翻书作答》(认识 RAG)
  2. 本文《权重大却不相关——BM25检索失效场景与两阶段救赎》(深挖检索环节)
  3. 《RAG原理就一句话——检索、拼接、加工》(回归本质)

一、一个让人抓狂的现象:分数越高,越不对

前司做经典 OA 系统时,我负责文档检索模块。文章通过 BM25 分片、倒排索引计算权重,前端搜索按权重排序返回"相关性最高的文档"。

一开始很顺。直到有一天,运营同事拿着截图来找我:

"搜'差旅报销流程',排第一的是一篇《2024 年度行政费用总结》——里面'报销'两个字出现了 27 次。我们想看的《差旅费报销实施细则》,排到了第七页。"

我第一反应是权重算错了。查了一遍公式,没错——BM25 给那篇总结打了 31.7 分,给细则打了 12.4 分,算法"忠实"地执行了它该做的事。

问题不在实现,在算法本身:BM25 衡量的是"查询词在文档里的统计显著性",而不是"文档和查询是不是一回事"。这俩大多数时候正相关,一旦岔开,就是"权重大但不相关"。

后来这套检索迁入 AI Agent(RAG 场景),同样的毛病原样出现——因为检索器根本没换,而 Agent 还会把它放大。这篇就把它彻底拆开讲透。

二、先看懂 BM25 到底在算什么

2.1 公式拆解

BM25 的经典公式:

1
score(D, Q) = Σ  IDF(qᵢ) × [f(qᵢ, D) × (k₁ + 1)] / [f(qᵢ, D) + k₁ × (1 − b + b × |D| / avgdl)]

三个因子,全是词法统计

因子 含义 隐含假设 失效场景
IDF(qᵢ) 词在语料中的稀有度 稀有词出现 = 更相关 生僻词偶然出现 → 分数虚高
TF 项 词在文档中的出现频率 重复越多 = 越相关 关键词堆砌 → 注水文胜出
长度归一化 文档越长惩罚越大 短文档更精准 分片太短 → 分数失真

一句话:BM25 是优秀的"词法匹配器",但它是"语义盲"。

2.2 分片带来的额外失真

我们当时用 BM25 分片(把长文切成片段,每片独立索引)。这个设计本身合理——长文档整篇算分,词频会被稀释。但它引入了两个新问题:

  1. 短分片分数虚高:长度归一化在极短文本上不稳定。一段 30 字的"错误日志"分片,BM25 分数可能碾压一篇 500 字真正讲原理的文章。
  2. 上下文截断:分片脱离母文档语境。一篇《系统调优指南》开头的"问题背景"分片,单独看可能跟查询高度重合,但它根本不是用户想要的内容。

三、"权重大但不相关"的四类典型成因

3.1 词频堆砌(TF 幻觉)

搜"mysql 索引优化",A 文把"mysql""索引"各重复 20 次;B 文是篇深入讲 B+ 树的硬核文章,"索引"只出现 3 次。

  • A 的 BM25 分:31.7
  • B 的 BM25 分:12.4
  • 用户真正想要的:B

词频是内容质量的差代理——注水文的词频永远比干货高。

3.2 一词多义(Polysemy)

"苹果"在《苹果发布会》里是公司,在《苹果种植技术》里是水果。BM25 只看字面,两种文档都会进 top 结果,混进一半"不相关"。

3.3 同义异形(Synonym)

搜"购车",文档写"买车"——字面零重合,BM25 直接给低分甚至 0。真正相关的文档被埋没,用户看到的 top 结果其实是"矮子里拔将军"。

3.4 分片断章取义

见 2.2。分片提高了索引粒度,却牺牲了语义完整性——这是 OA 场景里最容易踩、又最容易被忽略的一个。

四、为什么迁入 AI Agent 后问题被放大了?

把 BM25 检索搬进 Agent(RAG),本以为是"升级",结果是"原样继承 + 放大器":

1. 检索器没换,毛病原样继承。 Agent 的 RAG 底层还是那套 BM25 / ES 检索,检索逻辑不变,"权重大但不相关"的文档照样进 top-k。

2. 分数被当成"可信度"。 Agent 拿到 top-k 后,LLM 默认"高分 = 可靠",于是基于错误上下文自信生成。更糟的是——LLM 会把噪声上下文润色得比原文更可信。一个本可被用户一眼识破的错误,经模型"包装"后反而更具迷惑性。

3. 混合检索融合策略太糙。 如果用了 BM25 + 向量检索,但只是固定权重加权求和、没有重排,词法高分照样压过语义相关的结果。

一句话:garbage in, garbage out——检索器给错上下文,模型生成再漂亮也是错的。

五、解法:两阶段检索 + 配套改造

5.1 核心方案:粗召回 + 语义重排

这是性价比最高、也最直接的解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
用户查询


第一阶段:粗召回(BM25 + 向量混合)
│ 取 50~100 个候选,容忍噪声

候选集


第二阶段:语义重排(cross-encoder / LLM 打分)
│ 对每个「查询-文档对」独立计算语义匹配度

取 top-5~10 交给 Agent / 前端展示

为什么有效:第一阶段的 BM25 负责"别漏"(召回),第二阶段的 rerank 负责"别错"(精排)。关键词堆砌的文档在第一阶段能进候选(词法确实匹配),但第二阶段会被语义匹配度刷掉——因为重排器看的是语义,不是词频。

实现上,第一阶段用 bi-encoder 向量检索(如 bge、E5 类模型),第二阶段用 cross-encoder(如 bge-reranker)——前者快但精度低,后者精度高但要对每对查询-文档独立计算,所以只能用在第二阶段的小候选集上。

5.2 配套改造一:RRF 融合,不看原始分

BM25 和向量检索的分数尺度完全不同(一个几十分、一个零点几),直接加权求和没有意义。改用 Reciprocal Rank Fusion(RRF)

1
score(D) = Σ  1 / (k + rankᵢ(D))    其中 k = 60

只看"文档在各自检索器里的排名",不看原始分数——从根源上杜绝 BM25 的高分霸权

5.3 配套改造二:查询改写(Query Rewrite)

让 LLM 先把用户 query 改写再喂给检索器:

  • 同义词扩展:"购车" → "购车、买车、提车、购车流程"
  • 术语归一:"五险一金基数" → "社保缴费基数、公积金基数"
  • 拆长句 / 去口语

这一步能显著缓解"同义异形"和"一词多义"。

5.4 配套改造三:语义分片

别再机械按字数切。按语义边界切(段落、小节),并带重叠窗口(如相邻分片重叠 10%~20%),让每个分片保留上下文。检索到分片后,返回时带上母文档标题和前后文,Agent 能理解完整语境。

六、效果与踩坑记录

改造后的实际效果(OA 检索模块 + Agent RAG):

指标 改造前(纯 BM25) 改造后(两阶段)
首屏相关率(人工标注) ~52% ~91%
"关键词堆砌"误排数 每周 15+ 次 基本归零
长文档冷门段落召回 明显改善
单次检索延迟 20ms +180ms(可接受)

踩过的坑:

  1. rerank 别对大候选集全量做——cross-encoder 是逐对计算,50 个候选 × 全量会拖垮延迟。粗召回控制在 100 以内,精排只取前 20 再排。
  2. RRF 的 k 值别乱调——k=60 是经验值,k 太小排名前几位权重过大,k 太大退化为平均排名。
  3. 查询改写会改变召回分布——改写太激进(扩出太多同义词)反而引入噪声,建议"保守改写 + 保留原 query 作为一路检索"。
  4. 分片重叠别贪多——重叠超过 30% 会导致同一内容被多次命中,去重成本上升。

七、总结:别让"权重"背锅

回到最初的困惑——算法没算错,是"权重"这个概念承载了它不该承载的期待

检索器 擅长 盲区
BM25(词法) 精确词匹配、速度快、零训练 语义、同义、多义、堆砌
向量检索(语义) 同义/近义、语义相似 精确专名、冷门词、数字
重排器(rerank) 精排,交叉语义判断 只能在小候选集上用

BM25 不是坏工具,是"单一工具"。真正的问题是我们拿一个词法匹配器,去承担"语义相关性"的全部职责。

三个记忆点:

  1. BM25 算的是统计显著性,不是相关性——权重大但不相关是设计使然,不是 bug
  2. Agent 会放大检索错误——检索器给错上下文,LLM 把它包装得更可信
  3. 两阶段是标配——粗召回管"别漏",语义重排管"别错",RRF 管"别偏"

工程师视角的一句话总结:把"相关性"的评判权,从词法统计手里,交给语义模型——BM25 继续干它擅长的事(快速召回),把"是不是真的相关"这道判断题,交给真正懂语义的重排器。