news 2026/10/12 2:27:35

RAG 分块只调 chunk_size?我用 BM25 实测 5 种切法:重叠买的是 recall@1,块长买的是多跳

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAG 分块只调 chunk_size?我用 BM25 实测 5 种切法:重叠买的是 recall@1,块长买的是多跳

摘要

调 RAG 几乎人人都改过 chunk_size,但很少有人回答:分块粒度买到的到底是什么。本文用纯标准库实现字符 bigram 分词 + BM25(k1=1.5、b=0.75),在 21 段语料、14 条查询(8 单跳 + 6 多跳)上实测 5 种切法:固定 50 字、固定 500 字、50/15 重叠滑窗、段落聚合 ≤90 与 ≤180 字,指标含 recall@1、recall@3 与每查询 top3 送进模型的 token 成本。结论:重叠的增益主要落在 recall@1(0.357→0.571);多跳吃的是块长——固定 50 字合并 top3 只 0.643,500 字大块 1.000,但成本 113→748。粒度不是玄学,是量得出来的交换比。


一、问题:分块的"粒度"其实是三个旋钮

很多教程把分块说成一个参数(chunk_size),实际它同时动了三件事:

  1. 检索单元的大小——BM25/向量检索打分的对象,决定相关词密度;
  2. 上下文的完整性——一个事实会不会被拦腰切断;
  3. 索引与成本——块数影响检索开销,块长影响送进模型的 token。

这三个旋钮在"固定块长"里是绑死在一起的:调一个就全变。所以下文故意把实验排成三组对照——同块长比"有无重叠"(旋钮 2)、同策略比"聚合上限"(旋钮 3)、极小块比极大块(旋钮 1),让每个旋钮的影响能单独读出来。

原始文档

切块策略

固定长度 50 字

固定长度+重叠

段落边界聚合

BM25 / 向量索引

top-k 检索结果

送进模型的上下文

召回质量 与 token 成本

(Mermaid 若平台不渲染,等价文字:切块策略 → 索引 → top-k → 上下文 → 质量与成本,三个策略只在前段分叉。)

二、实验设计

  • 语料:21 个段落,7 个主题 × 3 段,主题间故意设置近义干扰(如"缓存"同时出现在 KV 缓存、前缀缓存、语义缓存三个主题里)。
  • 查询:14 条 = 8 条单跳(答案在某一个主题里)+ 6 条多跳(top3 合并必须覆盖 2 个主题才算命中,缺一个记未命中)。
  • 检索器:BM25 标准公式,中文用相邻字符 bigram 免分词库,英文/数字整串成词。全部本地计算,不调任何线上 API。
  • 判定:块是否"属于"某主题,按段落 bigram 在块中的覆盖率 ≥0.6。

核心评测代码(完整见demos/chunking_retrieval_eval.py,约 190 行):

defchunk_fixed(paras,size):"""固定长度、无重叠:句子会被拦腰切断"""text="".join(pfor_,pinparas)return[text[i:i+size]foriinrange(0,len(text),size)]defchunk_overlap(paras,size,overlap):"""固定长度 + 重叠滑窗:跨边界句子至少完整出现在某一块"""text="".join(pfor_,pinparas)step=size-overlapreturn[text[i:i+size]foriinrange(0,len(text),step)]defchunk_para(paras,max_size):"""段落边界聚合:连续段落拼到接近上限,绝不在句子中间切"""out,cur=[],""for_,pinparas:ifcurandlen(cur)+len(p)>max_size:out.append(cur);cur=pelse:cur+=pifcur:out.append(cur)returnoutdefevaluate(name,chunks,paras,queries,k=3):idx=BM25(chunks)ct=[chunk_topics(c,paras)forcinchunks]# 每块覆盖的主题集合r1,rk,cost=[],[],0forgolds,qinqueries:top=idx.topk(q,k)union=set().union(*(ct[i]foriintop))r1.append(1ifct[top[0]]>=set(golds)else0)# 首位命中rk.append(1ifunion>=set(golds)else0)# 多跳:合并覆盖全部金标准主题cost+=sum(len(tokenize(chunks[i]))foriintop)returnr1,rk,cost/len(queries)

三、实测结果(Python 3.11.5 / Windows,3 轮)

分块策略块数平均块长(字)recall@1recall@3top3 平均 token
fixed(50)2047.80.3570.643113.4
fixed(500)2477.50.8571.000748.0
overlap(50/15)2848.40.5710.714117.5
para(≤90)1563.70.5710.786152.6
para(≤180)7136.40.5710.929341.5

该实现完全确定性(无随机、无网络),3 轮结果一致,故区间为单点;策略间差异即实验变量本身。

三个可读出来的结论:

  1. 重叠买的是 recall@1。同样是 50 字窗口,加 15 字重叠后首位命中率 0.357 → 0.571(+60%),而每查询成本只从 113.4 涨到 117.5(+3.6%)。因为被切断的句子至少能在一块里完整出现,首位不再总把"半个答案"排第一。
  2. 多跳买的是块长,但价格是线性的。fixed(50) 的 recall@3 只有 0.643——6 条多跳查询里近一半没法在 top3 内凑齐两个主题;块长到 500 后 recall@3=1.000,但 top3 平均 token 从 113 涨到 748,6.6 倍。这正是父文档检索(小块命中、大块送上下文)想解耦的东西:本实验里 para(≤180) 用 341.5 的中间成本换到 0.929。
  3. 段落边界对 recall@1 没有额外加成(与 overlap 同为 0.571),它的价值在 recall@3(0.786/0.929 > 0.714):不切断句子让"相关但不完整"的块少了,合并覆盖更稳。
  4. 收益随参数不是单调堆积的。从 para(≤90) 到 para(≤180),recall@3 涨 0.143(0.786→0.929),成本涨 189 token(152.6→341.5);从 fixed(50) 到 fixed(500),recall@3 涨 0.357,成本涨 635 token。把五档排开就能看到一条"边际成本/边际收益"曲线,肘部出现在 para(≤180) 附近——这就是选块长该看的图,而不是单独的某个 recall。

四、避坑指南

  1. 拿通用基准的"最优块长"直接抄。本实验里 recall@1 从 0.357 到 0.857 全部由切法产生,同一语料换个主题分布结论就变。块长必须在你自己的语料 + 自己的查询集上量,别抄博客里的 512。
  2. 词法检索的结论直接套到向量检索。本文用 BM25 是因为它可完全本地复现;bigram 词法对"字面重合"敏感,向量对"语义相近"敏感,两者的块长最优值不同。但"重叠修 recall@1、块长修多跳"这个机制是通用的。
  3. 只报 recall@3 不报成本。top3 全命中但每查询送 748 token 的策略,可能不如送 341 token、0.929 的策略——多出来的命中率是用每次推理的上下文窗口和费用买的,报告要成对出现。
  4. 多跳查询不单独统计。把单跳和多跳混在一个总 recall 里,你会看不见"小块策略在多跳上系统性失败"这件事——本实验里这正是 fixed(50) 与 fixed(500) 差距最大之处。
  5. 忘了块还有"索引成本"这一维。overlap(50/15) 块数 20→28(+40%),线上向量库的存储与检索开销同步膨胀;收益表里没体现,是因为词法检索这项几乎免费。
  6. 父文档检索落地时把重叠内容重复拼进上下文。用"小块命中、父块送上下文"策略时,两个相邻小块常命中同一个父块,朴素拼接会让同一段文字出现两遍——召回分数没涨,token 白付。装配阶段必须先按父块 ID 去重、再按命中分数排序截断。

五、复现方法与读数口径

  • 运行:python demos/chunking_retrieval_eval.py(Python 3.11.5 / Windows,零第三方依赖),脚本自带 3 轮重复与区间汇总,留档输出在chunking_retrieval_eval.transcript.txt。
  • 为什么敢给"区间为单点":这套实现没有随机数、没有网络、没有计时器,三轮结果必然逐位一致——不确定性全部来自策略本身,这正是词法检索适合做 A/B 的原因。注意:换成语义(向量)检索后同一查询会有抖动,届时必须给真实验的置信区间,不能沿用单点写法。
  • 迁移到你的语料:只需替换CORPUS(每段标注主题)与QUERIES(金标准主题集合 + 查询文本)。多跳查询的判定规则保持不变:top3 合并必须覆盖全部金标准主题,缺一即 0。
  • 读数注意:本文的 recall@1 统计"首位块是否足以回答单跳查询",recall@3 统计"前 3 块合并(含多跳双主题)"。两个指标一个看排序质量、一个看上下文装配质量,混报会掩盖问题。

六、结论

分块粒度不是玄学,是两个可以量出来的交换比:重叠窗口用约 4% 的上下文成本修 recall@1(本实验 0.357→0.571);块长修多跳合并覆盖(0.643→1.000),但成本随块长近似线性上涨(113→748 token/查询)。工程上别在"一个 chunk_size"里做取舍——检索粒度用小块(保首位命中),上下文粒度用父块/聚合块(保多跳),本次实测 para(≤180) 恰好处在成本-召回的肘部(0.929 / 341.5)。所有数字来自本机 3 轮实跑,换到你的语料上请重跑同一套脚本再下结论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 2:26:39

macOS Codex 通过 SSH 连接 Windows 失败

macOS Codex 通过 SSH 连接 Windows 失败 问题 macOS Codex 通过 SSH 连接 Windows,界面显示“SSH 连接失败”,日志包含: ** WARNING: connection is not using a post-quantum key exchange algorithm. ** This sessionmay be vulnerable to…

作者头像 李华
网站建设 2026/10/12 2:26:23

无人机编队组合导航的核心原理与工程要点

目录 一、为什么要组合导航 二、INS 惯导原理 三、机械编排 四、松耦合/紧耦合/深耦合 五、组合导航中的 EKF 六、编队场景的组合导航 七、常见问题与排障 本文面向无人机编队开发者,系统梳理组合导航的核心原理与工程要点。编队飞行对位置精度、更新频率和一致性要求极高,单…

作者头像 李华
网站建设 2026/10/12 2:26:08

程序员转AI Agent:你的工程经验是巨大优势,附完整学习路线!

随着AI Agent岗位的兴起,许多程序员开始思考转型。文章指出,后端开发、业务系统开发者及架构师都有机会切入此领域。关键在于利用现有工程经验结合AI能力,而非单纯学习算法。文章提出五步学习路线,强调AI应用落地能力的重要性&…

作者头像 李华
网站建设 2026/10/12 2:25:30

AIOps实战15:运维Agent与Copilot,怎么让AI安全地动手干活

AIOps实战15:运维Agent与Copilot,怎么让AI安全地动手干活我是老计。这一篇是 LLM for Ops 的重头戏,也是我做 K8sChat 的核心,运维 Agent。前面讲的日志分析、RAG,大模型都还只是在动嘴,帮你分析、帮你答。…

作者头像 李华
网站建设 2026/10/12 2:23:07

本地Agent接入自组AI硬件:Muse Gadgets实现语音交互设备直连

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 2:23:04

STM32F1深度实战:架构、时钟、外设与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华