资讯详情

资讯详情

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

摘要调 RAG 几乎人人都改过 chunk_size但很少有人回答分块粒度买到的到底是什么。本文用纯标准库实现字符 bigram 分词 BM25k11.5、b0.75在 21 段语料、14 条查询8 单跳 6 多跳上实测 5 种切法固定 50 字、固定 500 字、50/15 重叠滑窗、段落聚合 ≤90 与 ≤180 字指标含 recall1、recall3 与每查询 top3 送进模型的 token 成本。结论重叠的增益主要落在 recall10.357→0.571多跳吃的是块长——固定 50 字合并 top3 只 0.643500 字大块 1.000但成本 113→748。粒度不是玄学是量得出来的交换比。一、问题分块的粒度其实是三个旋钮很多教程把分块说成一个参数chunk_size实际它同时动了三件事检索单元的大小——BM25/向量检索打分的对象决定相关词密度上下文的完整性——一个事实会不会被拦腰切断索引与成本——块数影响检索开销块长影响送进模型的 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:isize]foriinrange(0,len(text),size)]defchunk_overlap(paras,size,overlap):固定长度 重叠滑窗跨边界句子至少完整出现在某一块text.join(pfor_,pinparas)stepsize-overlapreturn[text[i:isize]foriinrange(0,len(text),step)]defchunk_para(paras,max_size):段落边界聚合连续段落拼到接近上限绝不在句子中间切out,cur[],for_,pinparas:ifcurandlen(cur)len(p)max_size:out.append(cur);curpelse:curpifcur:out.append(cur)returnoutdefevaluate(name,chunks,paras,queries,k3):idxBM25(chunks)ct[chunk_topics(c,paras)forcinchunks]# 每块覆盖的主题集合r1,rk,cost[],[],0forgolds,qinqueries:topidx.topk(q,k)unionset().union(*(ct[i]foriintop))r1.append(1ifct[top[0]]set(golds)else0)# 首位命中rk.append(1ifunionset(golds)else0)# 多跳合并覆盖全部金标准主题costsum(len(tokenize(chunks[i]))foriintop)returnr1,rk,cost/len(queries)三、实测结果Python 3.11.5 / Windows3 轮分块策略块数平均块长(字)recall1recall3top3 平均 tokenfixed(50)2047.80.3570.643113.4fixed(500)2477.50.8571.000748.0overlap(50/15)2848.40.5710.714117.5para(≤90)1563.70.5710.786152.6para(≤180)7136.40.5710.929341.5该实现完全确定性无随机、无网络3 轮结果一致故区间为单点策略间差异即实验变量本身。三个可读出来的结论重叠买的是 recall1。同样是 50 字窗口加 15 字重叠后首位命中率 0.357 → 0.57160%而每查询成本只从 113.4 涨到 117.53.6%。因为被切断的句子至少能在一块里完整出现首位不再总把半个答案排第一。多跳买的是块长但价格是线性的。fixed(50) 的 recall3 只有 0.643——6 条多跳查询里近一半没法在 top3 内凑齐两个主题块长到 500 后 recall31.000但 top3 平均 token 从 113 涨到 7486.6 倍。这正是父文档检索小块命中、大块送上下文想解耦的东西本实验里 para(≤180) 用 341.5 的中间成本换到 0.929。段落边界对 recall1 没有额外加成与 overlap 同为 0.571它的价值在 recall30.786/0.929 0.714不切断句子让相关但不完整的块少了合并覆盖更稳。收益随参数不是单调堆积的。从 para(≤90) 到 para(≤180)recall3 涨 0.1430.786→0.929成本涨 189 token152.6→341.5从 fixed(50) 到 fixed(500)recall3 涨 0.357成本涨 635 token。把五档排开就能看到一条边际成本/边际收益曲线肘部出现在 para(≤180) 附近——这就是选块长该看的图而不是单独的某个 recall。四、避坑指南拿通用基准的最优块长直接抄。本实验里 recall1 从 0.357 到 0.857 全部由切法产生同一语料换个主题分布结论就变。块长必须在你自己的语料 自己的查询集上量别抄博客里的 512。词法检索的结论直接套到向量检索。本文用 BM25 是因为它可完全本地复现bigram 词法对字面重合敏感向量对语义相近敏感两者的块长最优值不同。但重叠修 recall1、块长修多跳这个机制是通用的。只报 recall3 不报成本。top3 全命中但每查询送 748 token 的策略可能不如送 341 token、0.929 的策略——多出来的命中率是用每次推理的上下文窗口和费用买的报告要成对出现。多跳查询不单独统计。把单跳和多跳混在一个总 recall 里你会看不见小块策略在多跳上系统性失败这件事——本实验里这正是 fixed(50) 与 fixed(500) 差距最大之处。忘了块还有索引成本这一维。overlap(50/15) 块数 20→2840%线上向量库的存储与检索开销同步膨胀收益表里没体现是因为词法检索这项几乎免费。父文档检索落地时把重叠内容重复拼进上下文。用小块命中、父块送上下文策略时两个相邻小块常命中同一个父块朴素拼接会让同一段文字出现两遍——召回分数没涨token 白付。装配阶段必须先按父块 ID 去重、再按命中分数排序截断。五、复现方法与读数口径运行python demos/chunking_retrieval_eval.pyPython 3.11.5 / Windows零第三方依赖脚本自带 3 轮重复与区间汇总留档输出在chunking_retrieval_eval.transcript.txt。为什么敢给区间为单点这套实现没有随机数、没有网络、没有计时器三轮结果必然逐位一致——不确定性全部来自策略本身这正是词法检索适合做 A/B 的原因。注意换成语义向量检索后同一查询会有抖动届时必须给真实验的置信区间不能沿用单点写法。迁移到你的语料只需替换CORPUS每段标注主题与QUERIES金标准主题集合 查询文本。多跳查询的判定规则保持不变top3 合并必须覆盖全部金标准主题缺一即 0。读数注意本文的 recall1 统计首位块是否足以回答单跳查询recall3 统计前 3 块合并含多跳双主题。两个指标一个看排序质量、一个看上下文装配质量混报会掩盖问题。六、结论分块粒度不是玄学是两个可以量出来的交换比重叠窗口用约 4% 的上下文成本修 recall1本实验 0.357→0.571块长修多跳合并覆盖0.643→1.000但成本随块长近似线性上涨113→748 token/查询。工程上别在一个 chunk_size里做取舍——检索粒度用小块保首位命中上下文粒度用父块/聚合块保多跳本次实测 para(≤180) 恰好处在成本-召回的肘部0.929 / 341.5。所有数字来自本机 3 轮实跑换到你的语料上请重跑同一套脚本再下结论。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →