跳到主要内容

创建日期:2026-09-23 | 最近更新:2026-09-26 本文全部数字为本机实测(脚本与真实输出 source/rag-boundary-lab/run.log,复现命令 bash source/rag-boundary-lab/run.sh)。我的猜测与被否证的过程都写在正文里。 ⚠️ 本次运行 GPU 被别的任务占满,脚本自动退回 CPU 跑(bench_boundary.py 里的 pick_device())——所以本文不引用任何耗时类结论。

检索能力边界:同一批问题,四种检索组合各在哪失效

一句话:「用哪种检索」不该拍脑袋决定。同一批问题喂给稠密向量、BM25、混合(RRF)、混合+重排,四种组合在每一类问题上的胜负是不一样的——这张表就是「边界」的样子。

入门篇讲了链路有五个环节,第 1 篇拆了重排,第 2 篇拆了切片,第 3 篇拆了「余弦为什么能测语义」。这四篇里反复出现同一个判断:架构思路比代码能力重要,而架构思路的前提是知道每种机制的能力边界。 这篇把「边界」从判断变成数据:22 条 query 分四类,四种检索组合并排测,看谁在哪一类上失效。

0. 先给结论​

主表(22 条 query,语料 1479 块,格式 hit@1 @ hit@3):

query 类别条数① 稠密② BM25③ 混合(RRF)④ 混合+重排
A 语义型(同义改写)61/6 @ 3/62/6 @ 4/62/6 @ 4/62/6 @ 5/6
B 精确串型(编号/型号/日期串)61/6 @ 2/62/6 @ 3/61/6 @ 3/64/6 @ 5/6
C 数值表格型(哪一天哪个价)42/4 @ 2/40/4 @ 4/42/4 @ 3/43/4 @ 4/4
D 条件否定型(条款句式高度相似)65/6 @ 6/63/6 @ 5/65/6 @ 5/63/6 @ 5/6
合计229/22 @ 13/227/22 @ 16/2210/22 @ 15/2212/22 @ 19/22

三条读法:

  1. 不存在「最好的检索方式」,只存在「在某类问题上更好」。 稠密在 D 类上 5/6、在 A 类上只有 1/6;BM25 在 C 类上 top-1 全灭,但 top-3 全中。
  2. 堆叠能提高均值,但不保证每一类都变好。 混合+重排的总分最高(12/22、19/22),可它在 D 类上是四种组合里最差的(3/6)。
  3. 有些边界谁都过不去。 A 类四条组合的最好成绩只有 2/6——这不是选型问题,是这类 query 本身的难度。

1. 实验怎么做的​

1.1 语料:三类异构文档混在一个库里(1479 块)​

来源块数切法
技术 Markdown(本专题自己的三篇)149递归 256 token / 20% 重叠
制度问答条款(source/rag-lab/cases.py 的 6 组)30一条一款,不切
数值表格(真实价格统计表,前 100 行)1300句子化:一格一句

现实里的知识库就是这样拼出来的——这也是为什么「选一种检索就完事」几乎从来不是正确答案。

补一句:第 2 篇用的是 400 行表格(5200 块),这次只取 100 行——因为 5200 块会把语料压成 97% 都是表格,各类问题的对比就不公平了。这个坑是实测踩出来的,写在这里。

1.2 四种组合​

  • ① 稠密:bge-small-zh-v1.5,query 加指令前缀,余弦相似度;
  • ② BM25:经典 k1=1.5 / b=0.75。中文不用分词库,用单字 + 相邻 bigram 当词(网络 会切成 网、络、网络);
  • ③ 混合:①② 的结果用 RRF(k=60) 融合——只看位次,不看分数;
  • ④ 混合 + 重排:对 ③ 的 top-20 用 cross-encoder(bge-reranker-base)重排。

1.3 五类 query 与指标口径​

A/B/C/D 四类在主表里;E 类(污染型)单独在 §3 做,因为它要换语料。

指标口径必须交代清楚,否则数字没意义:

  • 命中判定:gold 是一个片段集合——凡包含该片段的块都算正确答案。为什么不用「唯一字符串」?因为切分带 20% 重叠,同一句话天然会落进两个块;强行要求唯一会逼着把 gold 扩到跨越块边界,反而变成「哪个块都不完整包含」。
  • hit@1 / hit@3:排名前 1 / 前 3 里有没有包含 gold 的块。名次取包含 gold 的块里最好的那个。
  • 标注自检:22 条 gold 全部能定位到块(落在 1~6 个块里,多出来的都是重叠造成的同句重复)。

2. 主结果:逐类看边界​

2.1 A 语义型:四种组合全面失败,而且我的解释被自己否证了​

A 类的 query 是同义改写(例如问「为什么块切得太大反而检索不到」,原文写的是「块越大,命中越低」)。直觉上这是稠密向量的主场——实测结果是最多 2/6:

query稠密BM25RRF重排
为什么块切得太大反而检索不到?61242
怎么让每个块知道自己出自哪一节?1311
多路召回的结果应该怎么合并?2113
块太长的后果是什么?1056088—
不同问题之间的重排分数能直接比大小吗?4333

最后那条「块太长的后果是什么?」四种组合全部没进前 50 —— 它要的是一句埋在长块里的短语。

我当时的第一个猜测是:1300 个「2014年X月X日,X市的鸡蛋批发价格为 …」块长得太像,把向量空间淹了。 这个猜测可以直接检验——把表格块全部拿掉再测一遍:

语料:1479 块 → 179 块(去掉 1300 个表格块)

类别 稠密 全量 稠密 去表格 BM25 全量 BM25 去表格
A 语义型 1/6 1/6 2/6 2/6
B 精确串型 1/6 1/6 2/6 2/6
D 条件否定型 5/6 5/6 3/6 1/6

结果把我的猜测否证了。 去掉 97% 的语料,A 类的命中数一点没变(1/6 → 1/6)。所以 A 类的失败不是表格噪声,而是这类「短 query + 抽象表述 + gold 是长块里的一个短语」本身难——我在文章里保留这个被否证的猜测,因为它比结论更能说明「猜因 → 消融 → 认账」这个过程。

顺带一个副产品:D 类的 BM25 从 3/6 掉到 1/6。说明 BM25 在 D 类上的那 3 分,有 2 分是靠表格块“撞”出来的——去掉噪声后它的真实水平更低。

2.2 B 精确串型:赢的是重排,不是 BM25​

B 类 query 里带着精确串(型号、编号、日期、数值),这是教科书上「该上关键词检索」的场景。实测:

query稠密BM25RRF重排
Anthropic 上下文增强把 top-20 失败率降到多少?22131
十万条语料全量重排在 3090 上外推多久?1111
bge-reranker-base 的模型体积有多大?8471
中文版 bge 小模型的体积是多少?2517141
康熙部首字符的 Unicode 区间是什么?2222
RRF 复算里 ID 101 的最终分数是多少?414139—

重排 4/6,是四种里唯一像样的。 而 BM25 只有 2/6,原因在分词:"1.1 GB" 被切成 1、1、gb 这类高频碎片,"0.03252247" 更是被拆散——BM25 的「精确匹配」优势,只在「词表里有这个词」时才成立。cross-encoder 是看着 query 和文档一起打分的,反而更能抓住这串数字。

这条与直觉相反,也是这篇最想讲的:别把「BM25 擅长精确匹配」当成免检结论,得看你的分词器把那个串切成了什么。

2.3 C 数值表格型:BM25 的 top-1 全灭,但 top-3 全中​

query稠密BM25RRF重排
2014年01月04日天津市的鸡蛋批发价格是多少?1312
2014年02月16日天津市的鸡蛋批发价格是多少?20351
2014年03月22日天津市的鸡蛋批发价格是多少?19331
2014年04月25日天津市的鸡蛋批发价格是多少?1311

BM25 的 0/4 @ 4/4 很有信息量:它总能把正确答案放进前三(日期串匹配上了),但排不到第一——因为 2014年、01月、天津市 这些词在所有行里都出现,区分度不够。

跟第 2 篇对比一下还有个规模效应值得记:第 2 篇用 400 行表格时,稠密在同类问题上 hit@1 = 0/4;这次 100 行,稠密 2/4。同样的模型、同样的 query 类型,候选池小了 4 倍,结果就从「全灭」变成「一半」——边界不是固定的,它随规模滑动。

2.4 D 条件否定型:稠密最好,重排帮倒忙​

D 类是同一批条款句式高度相似、只有条件不同的情况(第 1 篇的那 6 组 hard case):

query稠密BM25RRF重排
报销多久能到账?31167
员工培训的费用公司给报销吗?1313
请事假会扣钱吗?1212
哺乳期的女员工可以安排夜班吗?1111

稠密 5/6 是最高的,而重排只有 3/6——它是四种组合里最差的。 也就是说:重排在这类语料上是帮倒忙的。这和第 1 篇的结论能对上:那一篇里重排的价值体现在「拉开分差」,而不是「提升 top-1」;这一篇给出了它的另一面——在某些 query 类型上,它会把你原本排对的东西挪下去。

2.5 合计:堆叠提均值,但不保证每一类​

hit@1 hit@3
稠密 9/22 13/22
BM25 7/22 16/22
混合(RRF) 10/22 15/22
混合+重排 12/22 19/22

混合+重排的双指标都是最高,但它没有任何一类拿到第一。 它是个「稳」的选择,不是「强」的选择。如果你知道自己的 query 八成是 D 类,那只用稠密就够,加两层反而更差。

3. 污染型:两种检索的边界不在同一个地方​

E 类要换语料:把 Markdown 复制一份,按第 2 篇从真实 PDF 里抽出来的那批康熙部首形近字做定向替换(用→⽤、文→⽂、一→⼀……),query 仍然是正常汉字——模拟「用户正常打字,但库里躺着的是 PDF 抽出来的文本」。

污染密度按真实水位校准:38 个字形近字共替换 839 处 / 32079 字符 = 2.62%,而第 2 篇那份真实 PDF 是 261/10432 ≈ 2.5%。

query稠密(干净)稠密(污染)BM25(干净)BM25(污染)
[靶向] 一格一句是什么意思?10316
[靶向] 一份中英混排的文档怎么切?191411
[靶向] 什么时候该用 top-k 截断?2236
[靶向] 块越大越好吗?1112
[靶向] 大模型和向量检索谁更擅长数字?504612
[对照] 康熙部首是什么?1111
hit@1 合计2/62/65/62/6

同一个污染,两边的伤害完全不是一个量级:

  • 稠密:2/6 → 2/6,名次基本不变(甚至有 3 条略微变好);
  • BM25:5/6 → 2/6,掉了 3 个 top-1;
  • 对照组(不含被污染的字)两边都不动 —— 这条对照很重要,它说明变化确实来自污染,不是噪声。

机制也清楚(这段是解释,不是官方原文):

污染字符的下场后果
稠密形近字被分词器映射成 [UNK],但那一个字符的语义丢了、周围字还在衰减:整句意思还带得动
BM25用 和 ⽤ 是两个完全不同的 token,单字和 bigram 全部对不上断链:精确匹配直接失效

这解释了混合检索为什么在脏数据上更稳:两条腿不会同时断。 注意这与 §2.2 的结论并不矛盾——BM25 在干净数据上是 A/B/C 类的补位者,脏数据上则要靠向量兜底,两者的强弱项正好互补。

4. 这张表怎么用​

你的情况建议依据
query 以「条款/条件判断」为主稠密单独用就够;加混合是保平,加重排可能变差D 类:稠密 5/6 vs 重排 3/6
query 里常出现型号、编号、日期串混合加重排;别指望 BM25 单独解决B 类:重排 4/6,BM25 2/6
要检索表格里的具体数值优先结构化过滤(把日期/地区当 filter),向量只做兜底C 类:BM25 top-1 全灭
数据来自 PDF/扫描件,没清洗干净先清洗(第 2 篇);清洗不了就靠混合互相兜底E 类:BM25 掉 3 个 top-1,稠密不动
不确定自己的 query 构成混合+重排起步,然后按类别抽样评测,再决定砍掉哪层合计:12/22 @ 19/22 最稳

最后一行是重点:这张表不是给你照抄的答案,是给你抄的方法。 你的语料、你的 query 分布、你的模型版本,都会让数字不一样——唯一能确定的是「先知道边界再选架构」这个顺序。

5. 这份实测自己的边界(诚实交代)​

把话说全,这张表也有它的适用范围:

  1. 样本量小:主表 22 条、污染表 6 条。单条差异可能只是运气(第 2 篇里我已经吃过一次这个亏)。能看的是量级和方向,不是小数点。
  2. 单一语言、单一模型:全部是中文 + bge-small-zh-v1.5 + bge-reranker-base。换模型、换语言,边界会移动(§2.3 已经证明它还会随语料规模移动)。
  3. 指标只测「能不能召回」,不测「能不能回答」:hit@k 问的是「gold 片段有没有被捞出来」,没问「块里的信息够不够回答」。后者要另一套评测(也就是 route map 里的下一篇)。
  4. BM25 用的是简化分词(单字 + bigram,无词典)。接上真正的分词器和业务词典,它在 B 类上的表现大概率会变好——§2.2 的结论要按「这个分词配置下」来读。
  5. 没测的:其他融合算法(加权、RSF)、query 改写/扩展、多轮对话里的检索、延迟与成本(第 1 篇测过一部分)。
  6. 本次跑在 CPU 上(GPU 被占满,脚本自动降级),所以文中没有引用任何耗时数字。

6. 来源与核对​

本文的结论全部来自本机实测,没有引用新的外部文档。用到的外部事实与对应留档:

事实来源留档
RRF 公式 score(d) = Σ 1/(k + rank_i(d))、k 默认 60milvus-docs rrf-ranker.mdsource/rag-lab/official-milvus-rrf-ranker.md(复算误差见第 1 篇)
康熙部首形近字的真实存在与后果本机真实 PDF 抽取实测source/rag-chunking-lab/(第 2 篇)
BM25 的 k1=1.5 / b=0.75经典默认值,本次直接采用— (未引用具体来源;它是 BM25 文献里的常见取值)

实测脚本与真实输出:source/rag-boundary-lab/ retrievers.py(BM25 / RRF 实现)、bench_boundary.py(主表 + 消融)、bench_pollution.py(污染对照)、run.log。

复现环境:python 3.10、torch 2.10.0+cu126、transformers 5.7.0、openpyxl 3.1.5。跑法 bash source/rag-boundary-lab/run.sh。