想象你要把一本 300 页的厚小说交给一个读书小组传阅,但每位组员一次只能读 30 页。为了让大家都能接上剧情,你决定把书拆成一叠叠小册子:第 1 叠是第 1~30 页,第 2 叠从第 26 页开始,一直读到第 55 页——每叠末尾都多夹了前一位读者看过的 5 页,当作“剧情便签”,保证下一位读者不至于一头雾水。这个“拆书装订”的过程,就是 RAG(检索增强生成)里最基础也最关键的一步:分块(Chunking)。“每叠 30 页”就是 chunk_size,“重复的 5 页剧情便签”就是 overlap。拆得好,每一位“读者”(AI 模型)都能顺畅理解;拆得不好,不是剧情断在中间,就是整本书被复制得又厚又贵。这篇内容,就是教你怎么把这本书拆得既省纸、又不断戏。
分块参数的调优是 RAG 从“能跑”到“好用”的分水岭。掌握 chunk_size 与 overlap 的调优方法,是每一位 RAG 应用工程师的必修课。
可视化演示
通过Chunk大小、重叠窗口和分块方法对比三个视角,展示RAG文档处理中分块参数对检索质量的影响与调优策略。
本节配有交互动效(进入视口后自动加载,约 3KB)。
分块基础概念
想象你要把一本 300 页的厚小说交给读书小组传阅,但每位组员一次只能读 30 页。你能做的,就是把书拆成一叠叠小册子:第 1 叠是 1~30 页,第 2 叠是 26~55 页……每叠之间有 5 页“重复交接区”,确保下一位读者不必从头猜剧情。这个“拆书”的动作,就是 RAG 中最关键的第一步——分块(Chunking)。
在 RAG 系统里,长的原始文档不能直接喂给大模型,因为大模型的“阅读上限”(上下文窗口)是有限的。于是我们把文档切成小片段,对每个片段做向量化(Embedding),存入向量数据库;用户提问时,系统先检索最相关的几个片段,再交给大模型组织答案。分块后的每个片段,就是检索时的最小单位——相当于递给读者的那一叠小册子。
分块决定了检索的粒度:块太大,向量里混入太多无关信息,检索出的片段重点模糊;块太小,单个片段信息量不足,语义被拦腰截断。分块质量直接影响“能否找回正确答案”和“找回的内容是否干净”。它是整条 RAG 流水线的第一刀,这一刀切歪了,后面所有环节都会跟着将就。
所以,拆书之前先算清楚“一本书会被拆成多少叠”。这份基础账目一旦建立,后面的参数调优就有了共同的“语言”——无论换哪种切法,你都知道自己手里的书页总数是守恒的。
- n 块数文档总共被切成多少份,直接决定向量库的规模。
- s 块大小每个分块包含多少字符或 token,也就是 chunk_size 参数。
- o 重叠量相邻分块之间共享的尾部内容长度,也就是 overlap 参数。
- 总长平衡式总长度等于 n 个块的长度之和,再减去 n−1 次重叠重复。
核心参数解析
读书小组有两条铁规矩:一是“每叠多少页”,二是“交接区重复几页”。页数太多,一叠小册子沉甸甸,读者读不完还抓不住重点;页数太少,每叠都在重翻旧内容,成本翻倍。交接区的页数也有讲究:太少了,下一位读者接不上剧情;太多了,整本书等于被复制了一小半。这两条规矩,就是分块参数里最核心的两个旋钮:chunk_size 和 overlap。
chunk_size 决定每个分块的长度上限,overlap 决定相邻分块首尾共享多少内容。两者共同决定“滑动步长”(step = chunk_size − overlap),也就是每个新块比前一块多出的新内容长度。overlap 的初衷是给边界处的语义打补丁——当一句话恰好在块尾被切断,它的开头还能在下一块里再出现一次,避免彻底丢失上下文。但补丁不是万能药:它增加存储与嵌入成本,也无法真正让被切断的句子变完整。
理解这两个旋钮,关键是看它们如何联动决定分块总数。块数越多,向量库越大,检索和存储成本越高;块数越少,每块越“臃肿”,检索精度越差。这个数量关系可以用下面这个公式精确表达。
调参从来不是“把页数调大一点”这么简单。chunk_size 与 overlap 像一对联动齿轮,转动任何一个,块数、成本、检索效果全都跟着变。先记住这张“账本”,后面无论采用哪种切法,你都能立刻算出它的开销。
- N 分块总数文档最终被切成的块数,决定向量库规模与检索延迟。
- L 文档总长原始文本的总长度,以字符数或 token 数计。
- o 重叠量相邻块共享的尾部长度,即 overlap。
- s−o 滑动步长每个新块相对于前一块引入的新内容长度。
- 向上取整最后一块哪怕只剩 1 页,也要单独算作一叠。
固定大小分块
最省事的拆书办法是什么?拿一把尺子,量好 30 页就切一刀,不管这一刀是不是正好落在剧情高潮那句话的中间。“反正 30 页一叠,规矩统一”——这就是传说中的“一刀切”。
对应到代码里,就是固定大小分块(Fixed-size Chunking):给定一个 chunk_size,按字符数或 token 数从头到尾机械地切;overlap 则机械地把前一块尾部的若干字符复制到下一块开头。它的优点极其直白:实现简单、速度最快、分块数量完全可预测,非常适合处理日志、商品信息这类“每条记录独立自洽”的文本。
缺点是结构性的:文本的语义单元(段落、句子)长度参差不齐,固定长度硬切,大概率把一句话甚至一个词劈成两半,产生大量“残句块”。检索时即使命中了残句块,大模型也很难理解。overlap 只能让切点附近的文字在下一块再出现一次,相当于给伤口贴创可贴——句子本身依然是断的。重叠多少合适,可以用重叠率来衡量。
对一本书来说,用尺子量着切等于无视纸张的褶皱和墨水的标点——虽然快,但拆出来的册子满纸都是被裁掉的半句话。固定大小分块的归宿,是那些“每一页本来就能独立阅读”的资料,比如错峰出行的通知、分条列目的价目表。给小说用这种切法,纯属暴殄天物。
- r 重叠率衡量“重复阅读”占每块的比例,是判断冗余程度的核心指标。
- o 重叠量相邻分块共享的尾部字符数或 token 数。
- s 块大小chunk_size 的字符数或 token 数。
- 百分比换算把比值乘以 100%,方便与 10%~20% 的经验区间对照。
递归字符分块
聪明的拆书匠不会死量页数,而是顺着书的“自然折痕”拆:先按大章节拆,一章太长就按小节拆,小节还长就按段落拆,段落再长就按句子拆……一层一层往里剥,直到每一叠都装得下为止。
这一策略在工程上的典型实现,就是递归字符分块(Recursive Character Text Splitting),LangChain 的 RecursiveCharacterTextSplitter 是最常用的代表。它维护一个由粗到细的分隔符列表,比如:段落空行 “\n\n“ → 换行 ”\n“ → 句号 ”。” → 问号/感叹号 → 逗号 → 空格 → 单字符。算法从最粗的分隔符开始切分,若某段仍超过 chunk_size,就换下一级更细的分隔符继续递归切分;切完后,再把相邻的小块合并,使每块尽量长但不超过上限。
这样“顺着折痕切”,句子和段落被劈开的概率大幅下降,同时块的利用率保持在高位。实际编码时有一个常见的坑:chunk_size 参数通常以 token 计,而文档长度我们习惯按字符看,两者需要换算。换算系数因语言而异,下面的公式给出了通用关系。
“顺着折痕拆书”听起来很聪明,但千万别迷信:折痕找得准,句子是完整了;可如果某一段话本身就有 2000 字,递归分块最后也会退化到按字符硬切。折痕是工具,不是魔法——它只是把“切在哪儿”的选择权从距离交给了结构。
- s_char 分块字符数我们直观感知的文本长度,以字符为单位。
- s_token 分块 token 数模型真正计费和处理的粒度。
- α 语言密度系数每个 token 平均覆盖的字符数,中文与英文差异明显。
- 近似换算关系字符数与 token 数通过语言密度系数互相换算。
语义分块
最讲究的读书会,会请一位“懂书的人”来拆:一场戏必须完整地交给同一位读者,哪怕这场戏只有 5 页,而下一场戏有 40 页。按剧情单元拆,而不是按页数或折痕拆——这就是语义分块的理想状态。
机器不会读剧情,但它有一个替代品:嵌入模型(Embedding Model)。具体流程是:先把文本切成句子,用嵌入模型把每个句子编码成一个向量;再计算相邻两个句子的余弦相似度——相似度高说明话题连续,相似度突然跌到阈值以下,说明“剧情转折”了,那里就是切点。切出的每一块,恰好是一段完整论述。
语义分块的最大优势是语义完整性极高,检索到的块往往就是用户想要的“那一段话”;代价也很明显:每个句子都要调用嵌入模型,计算成本高、延迟大,相似度阈值还得根据文档风格单独调,否则不是切得太碎就是切不动。相似度的计算方式正是这个方法的数学内核。
请“懂书的人”拆书最贵,也最贴心。如果你的业务是合同、论文这类“一句话都不能断”的场景,这份贵是值得的;但如果只是海量商品描述,语义分块就是杀鸡用牛刀。
- v_i 句子向量第 i 个句子的嵌入向量,把一段文字映射成数字坐标。
- 点积运算两个向量逐位相乘再求和,衡量它们的同向程度。
- 模长乘积把点积结果归一化,消除句子长度带来的偏差。
- 余弦相似度结果落在 [−1, 1],越接近 1 表示语义越连贯。
调优方法论
没有哪本书自带“标准拆法”。技术手册、菜谱、小说、合同,它们的“剧情单元”长度天差地别。所以拆书前要试装几个版本,拿给真正的读者反馈,而不是拍脑袋定页数。
RAG 的调优同样是实验驱动的:先定一组候选的 chunk_size 和 overlap,跑检索,看指标,再迭代。没人能一次报出最优参数,但方法论有章可循。工程实践中的推荐起点是:chunk_size = 250~500 token,overlap = chunk_size 的 10%~20%。然后根据文档类型微调:代码、表格这类结构强文本适合递归分块配小 overlap;散文、论文适合大块加适当重叠;条款型文本优先语义分块。
更系统的做法是网格搜索(Grid Search):把多个候选 chunk_size 与 overlap 组合成一张参数网格,逐一在小评估集上测试,用检索召回率(Recall)与答案相关性等指标选出最优组合。所谓“最优”,本质上就是在候选集合里求一个最大值。
试装几版给读者看,读者说哪版读得最顺,就按哪版装订——只不过这里的“读者”是一个带标准答案的评估集,“读得顺”是召回率和答案相关性。用数据投票,比任何经验都靠谱。
- 候选集 P由经验起始值扩展出的所有参数组合网格。
- Recall 召回率在特定参数下,相关文档被成功检索出的比例。
- argmax 求最大遍历候选集,挑出使召回率最大的那组参数。
- 最优参数组合调参的最终产出,即 s* 与 o*。
实战落地建议
真到开读书会那天,你还会发现一个隐藏成本:如果交接区重复的页数太多,整套书会“变厚”——本来 300 页的书,拆完可能变成 350 页。多出来的 50 页就是重叠的代价,对应到系统里就是存储空间和嵌入算力的浪费。
分块参数的实战选择,从来不是“质量单指标”问题,而是质量与成本的双指标权衡。落地时建议走四步:第一,先统计文档分布——平均段落长度、句子数、token 密度,据此确定初始参数区间;第二,构建小规模评估集,几百条带标准答案的问答即可;第三,在评估集上跑网格搜索,用“召回率 + 答案相关性 − 成本惩罚”综合评分;第四,上线后持续监控嵌入量、检索延迟与冗余率,用真实用户反馈迭代。
其中,冗余率是最容易被忽略的成本指标。它量化了“为了补边界而额外嵌入的内容”占有效新内容的比例,是计算账单的直接依据。
读书会开到最后,你会发现最好的拆法不是“越厚越好”,也不是“重叠越多越安全”,而是在“每叠装得下、剧情接得上、全书不虚胖”之间找到平衡。分块参数调优的终点,也正是这个平衡点。
- R 冗余率重叠带来的额外嵌入内容占有效新内容的比例。
- o 重叠量每个新块开头重复的上一块尾部内容。
- s−o 滑动步长每个新块真正引入的新内容长度。
- 比率含义比值越大,说明为了衔接付出的存储代价越高。
AI 应用场景
企业知识库智能问答机器人
阶段:文档加载与处理(分块与向量化索引)
解决的问题:产品手册通常有数千页,直接整体喂给大模型既超出上下文窗口,检索命中率也很低。传统做法是人工整理 FAQ,费时费力且难以覆盖长尾问题。采用递归字符分块,将 chunk_size 设为 300 token、overlap 设为 50 token 后,检索能从“大海捞针”变成“精准定位”。
具体案例:某 SaaS 公司的客服机器人需要回答关于 200 篇产品手册的提问。调优前用固定大小分块,检索召回率只有 62%;改用递归字符分块并调整 chunk_size 与 overlap 后,召回率提升到 81%,答案准确率同步上升,人工客服转接率下降约三成。
法律合同辅助审查
阶段:文档加载与处理(语义分块)
解决的问题:合同中的赔偿条款、免责条款必须完整呈现,任何“拦腰截断”都可能导致 AI 遗漏关键约束。语义分块按条款话题边界切分,确保每个块都是一个完整条款,显著降低漏答风险。
具体案例:某律所知识库用语义分块处理上千份合同,嵌入成本比固定分块高约 40%,但条款级问题的正确率从 71% 提升到 93%,客户对 AI 辅助审查的信任度大幅提升。
多语种混合文档检索
阶段:文档加载与处理(参数单位换算与适配)
解决的问题:中英混合的企业文档中,token 与字符的换算系数完全不同,直接用同一套 chunk_size 会导致中文块过短、英文块过长。利用语言密度系数 α 分别换算,再为不同语种设置独立的分块参数,可让每种语言的块长度落在同一 token 区间。
具体案例:某跨境电商平台的技术文档中英混杂,按语言分流后分别设置 chunk_size(中文 400 字符、英文 1200 字符),向量库总存储量下降 18%,检索准确率提升 12%。
Python 演示
# -*- coding: utf-8 -*-"""演示 chunk_size 与 overlap 对分块结果的影响,并对比三种分块策略"""import reimport math def fixed_chunk(text, chunk_size, overlap): """固定大小分块:每块 chunk_size 字符,相邻块共享 overlap 字符""" step = max(1, chunk_size - overlap) chunks = [] start = 0 while start < len(text): end = min(start + chunk_size, len(text)) chunks.append(text[start:end]) if end == len(text): break start += step return chunks def recursive_chunk(text, chunk_size, separators=("\n\n", "\n", "。", ",")): """递归字符分块:按分隔符从粗到细逐层切分,直到每块不超过 chunk_size""" def split_recursive(seg, seps, level): if len(seg) <= chunk_size or level >= len(seps): return [seg] if seg.strip() else [] parts = re.split("(" + re.escape(seps[level]) + ")", seg) buf = "" merged = [] for part in parts: if len(buf) + len(part) <= chunk_size: buf += part else: if buf: merged.append(buf) buf = part if buf: merged.append(buf) result = [] for item in merged: if len(item) > chunk_size: result.extend(split_recursive(item, seps, level + 1)) else: result.append(item) return result return split_recursive(text, separators, 0) def char_overlap_sim(a, b): """用字符重合度模拟嵌入相似度(真实场景应使用嵌入模型)""" sa, sb = set(a), set(b) if not sa or not sb: return 0.0 return len(sa & sb) / math.sqrt(len(sa) * len(sb)) def semantic_chunk(sentences, sim_func, threshold): """语义分块:相邻句子相似度低于阈值时切块""" chunks = [] current = [sentences[0]] for i in range(1, len(sentences)): if sim_func(sentences[i - 1], sentences[i]) < threshold: chunks.append("".join(current)) current = [sentences[i]] else: current.append(sentences[i]) chunks.append("".join(current)) return chunks if __name__ == "__main__": doc = ("RAG系统先把长文档切成小块,\n" "再对每个块做向量化并存入向量库。\n\n" "查询时,系统把问题也转成向量,\n" "检索与问题最相似的若干分块。\n\n" "最后,大模型基于这些分块生成答案,\n" "并附上引用来源。\n\n") * 5 print("文档总长度:", len(doc)) fixed = fixed_chunk(doc, 60, 15) print("固定分块块数:", len(fixed), "前2块:", [c[:10] + "..." for c in fixed[:2]]) rec = recursive_chunk(doc, 60) print("递归分块块数:", len(rec), "平均长度:", sum(len(c) for c in rec) // len(rec)) sents = [x for x in re.split("(?<=。)", doc.replace("\n", "")) if x] sem = semantic_chunk(sents, char_overlap_sim, 0.45) print("语义分块块数:", len(sem), "平均长度:", sum(len(c) for c in sem) // len(sem))
step = max(1, chunk_size - overlap)滑动步长是块大小减重叠量,且必须大于 0,否则会死循环。chunks.append(text[start:end])从当前起点切出一个 chunk_size 长度的窗口作为分块。start += step窗口向前滑动一个步长,从而产生相邻块之间的重叠。parts = re.split("(" + re.escape(seps[level]) + ")", seg)用带捕获组的正则切分,保留分隔符本身不丢失。if len(buf) + len(part) <= chunk_size:贪心合并小块,让每块尽量接近上限又不超限。result.extend(split_recursive(item, seps, level + 1))超长块降级到下一级分隔符递归处理。len(sa & sb) / math.sqrt(len(sa) * len(sb))用字符重合度模拟余弦相似度,值域在 0~1 之间。if sim_func(sentences[i - 1], sentences[i]) < threshold:相邻句相似度跌破阈值,判定为语义边界并切块。doc = (...) * 5用重复段落拼接模拟一份更长的文档,方便观察分块效果。print("固定分块块数:", len(fixed), ...)输出三种策略的块数与平均长度,直接对比差异。
常见误区
误区chunk_size 设大一点,每个块里的信息更多,检索肯定更准。我把 chunk_size 调到 2000 token,效果应该比 500 好很多吧?
正解块太大,一个向量里混入多个话题,检索时“平均”了所有重点,反而什么都匹配不准;而且可能超过嵌入模型的最大长度被截断。实践经验中 250~500 token 通常是甜区,具体要靠评估集验证,而不是直觉。
为何常见:直觉上“内容多 = 信息多”,却忽略了向量检索需要的是“主题纯净”的块,而不是信息堆砌的块。
误区overlap 越大,边界上下文保留得越多,那我直接把 overlap 设为 chunk_size 的一半,甚至让相邻块完全一样,总该更保险吧?
正解overlap 增大意味着嵌入总量 N × s 上升,存储与算力成本同步上升,冗余率 R = o/(s−o) 也会膨胀。overlap 只是给边界语义打补丁,并不能修复被切断的句子。10%~20% 是常用区间,超过后收益递减。
为何常见:把 overlap 当成“免费保险”,只看到它能补上下文,没算它带来的存储和成本账单。
误区固定用 chunk_size=512、overlap=64,所有项目都这么配,省事又不会出大错。文档类型不一样,真有那么大区别吗?
正解代码、合同、论文、客服会话的语义单元长度差异巨大。合同的一句话可能长达 200 字,代码的一行可能只有 20 字符。必须按文档类型和数据分布调整参数,并用评估集验证,不能复制经验值到底。
为何常见:项目交付压力大,习惯复制成功案例的参数,缺少“先看数据分布再定参数”的工程意识。
误区chunk_size=2000,不就是 2000 个汉字吗?中文一个字就是一个 token,直接按字数填就行。
正解中文一个 token 大约覆盖 0.8~1 个汉字,英文一个 token 约覆盖 3.5~4 个字符。框架里的 chunk_size 默认多以 token 计,填之前必须先换算,否则实际块长与你的预期可能差一倍。
为何常见:人习惯用“字数”感知文本长度,而模型按 token 计费和处理,两种单位之间的换算关系常被忽略。
误区语义分块最智能,一定比递归分块效果好。直接上语义分块,一步到位。
正解语义分块要为每个句子调用嵌入模型,计算成本高、延迟大,阈值还敏感;对海量短文本、商品描述这类场景性价比很低。大多数场景下,递归字符分块已经足够好,语义分块只适合“一句话都不能断”的高价值文档。
为何常见:认为“更先进 = 更有效”,忽略了成本约束和场景匹配,属于技术选型上的贪心。
面试题
Q1. chunk_size 通常以什么为单位?
- A.字符或 Token← 正确答案
- B.段落或章节
- C.字节或比特
- D.文件或目录
答案:A。chunk_size 是分块的长度参数,以字符数或 token 数计量;段落、章节属于语义结构单位,文件、目录属于存储单位,都不是长度单位。
Q2. overlap 在分块中的主要作用是什么?
- A.压缩文档体积
- B.保留边界上下文← 正确答案
- C.提升切分速度
- D.减少向量数量
答案:B。overlap 让相邻分块共享尾部内容,避免切点附近的语义被割裂。它不会压缩体积,反而增加存储,也不会提升速度或减少向量数量。
Q3. 固定大小分块最主要的问题是什么?
- A.切分速度过慢
- B.句子被拦腰切断← 正确答案
- C.无法预测块数量
- D.不支持并行处理
答案:B。固定大小分块按字符数硬切,完全无视句子的自然边界,极易产生残句块。它的优势恰恰是速度快、块数可预测且支持并行,问题出在语义完整性上。
Q4. 递归字符分块优先使用哪个分隔符?
- A.段落空行← 正确答案
- B.单个汉字
- C.逗号分号
- D.句号问号
答案:A。递归分块的分隔符列表由粗到细,段落空行是最粗的层级,优先使用;句子、子句、单词是后续的降级选择,单个汉字是最后兜底。
Q5. 语义分块判断切分边界的依据是什么?
- A.相邻句向量相似度← 正确答案
- B.分块的字符总数
- C.关键词出现频率
- D.标点符号数量
答案:A。语义分块把每个句子编码成向量,比较相邻句子的余弦相似度,相似度跌破阈值就切块。字符数、词频、标点都不是判断语义转折的依据。
Q6. overlap 设置过大会带来哪些影响?
- A.向量库冗余增加← 正确答案
- B.嵌入成本上升← 正确答案
- C.检索收益递减← 正确答案
- D.准确率必然提升
答案:ABC。overlap 增大使嵌入总量 N × s 上升,冗余率 R = o/(s−o) 变大,存储和成本都增加;边界补丁的效果边际递减。D 错在“必然”——冗余还可能引入噪声,准确率不升反降。
Q7. 某文档 L=10000 字符,chunk_size=500,overlap=100,分块数约为多少?
- A.大约 20 块
- B.大约 25 块← 正确答案
- C.大约 30 块
- D.大约 40 块
答案:B。滑动步长 s−o = 400,代入公式 N = ⌈(L−o)/(s−o)⌉ = ⌈(10000−100)/400⌉ = ⌈24.75⌉ = 25。属于典型的“账本”式计算,考察对块数公式的掌握。
Q8. 语义分块有哪些不足之处?
- A.计算开销与延迟高← 正确答案
- B.效果依赖嵌入模型← 正确答案
- C.无需设定阈值参数
- D.块长度不固定波动← 正确答案
答案:ABD。语义分块要为每个句子调用嵌入模型,成本高、延迟大;嵌入模型质量直接影响边界判断;块长随语义单元变化,波动大。C 错,相似度阈值正是它必须人工调的关键参数。
Q9. 中文文档目标 chunk_size=500 token,若每 token 约覆盖 0.8 字符,对应约多少字符?
- A.约 400 字符← 正确答案
- B.约 500 字符
- C.约 625 字符
- D.约 800 字符
答案:A。由换算公式 s_char ≈ α × s_token,α = 0.8,500 × 0.8 = 400 字符。这个换算关系决定了代码里填的 token 数与实际文本长度的对应,中英差异很大。
Q10. 分块参数调优时应该关注哪些指标?
- A.检索召回率← 正确答案
- B.答案相关性← 正确答案
- C.嵌入成本延迟← 正确答案
- D.原始文件大小
答案:ABC。调优的目标是质量与成本双指标平衡:检索召回率和答案相关性衡量质量,嵌入成本和延迟衡量开销。原始文件大小是输入信息,不是评估指标。