数据分块Chunk策略与实践
作者:程序员马丁
Ragent AI —— 从 0 到 1 纯手工打造企业级 Agentic RAG,拒绝 Demo 玩具!AI 时代,助你拿个offer。
为什么不能把整篇文档直接丢给大模型?
假设你在一家电商公司做开发,老板让你搞一个智能客服系统。公司有一份 200 页的《客服知识库》,涵盖退货政策、物流规则、会员权益、售后流程等内容。用户问“买了 7 天的商品还能退吗?”,系统要能从知识库里找到答案并回复。
最直觉的做法——把整份知识库的文本一股脑塞给大模型,然后让它回答。
听起来很合理,但实际跑起来你会撞上两堵墙。
1. 大模型的上下文窗口限制
大模型在处理文本时,有一个上下文窗口的概念。你可以把它理解成大模型的工作台——它一次能摊开看的纸张数量是有限的。
拿目前主流的模型来说,上下文窗口大概在 128k 到 1M 个 token 之间(token 是什么后面会解释,这里你先理解成大约等于一个汉字或单个英文)。128K token 听起来很多,但一份 200 页的知识库文档,纯文本量轻松超过 30 万字,远远超出窗口上限。
直接后果:文本塞不进去,或者会触发输入长度限制/截断/费用显著增加。
从 128K token 到 1M token,跨度还不到一年,进步确实非常快。但接下来是否还需要继续把上下文做得更大,仍有待观察:从成本、延迟到实际效果来看,token 并不是越多越好。
就算未来模型的窗口越来越大,能装下整份文档了,还有第二个更本质的问题。
2. 检索精度的问题——大海捞针
假设你有一个上下文窗口无限大的模型,把整份知识库都塞进去了。用户问“生鲜商品支持七天无理由退货吗?”,模型需要从 30 万字里找到相关的那几段话。
这就像你去图书馆找一句话,但图书管理员把整个图书馆的书全摊在你面前说自己找。信息 太多,噪音太大,模型很容易走神——要么找不到重点,要么把不相关的内容混进回答里。
即使上下文做到 1M,很多在线客服场景仍会偏向 RAG,因为全量长提示会显著拉高成本与响应延迟,且吞吐更差。
在 RAG 的架构中,做法不是把所有文本都丢给模型,而是先检索出最相关的几段文本,只把这几段喂给模型。这样模型拿到的上下文精准、干净,回答质量自然就上去了。
但问题来了:要做检索,你得先有可以被检索的单元。一整份知识库是没法作为检索单元的——粒度太粗了。你需要把它切成一段一段的小块,每一块聚焦一个相对完整的知识点。
这就是分块(Chunking)要干的事。
分块到底在干什么
1. 分块在 RAG 流程中的位置
上一篇我们用 Apache Tika 从 PDF、Word 等文件中提取出了纯文本。但纯文本只是原材料,还不能直接用于检索。分块是紧接着文本提取之后的一步,它把长文本切成适合检索的小段。
整个数据准备阶段的流程是这样的:

分块之后的下一步是向量化——把每个文本块转成一组数字(向量),方便计算机做相似度检索。向量化的原理和实现下一篇单独讲,这里你只需要知道:分块的质量直接决定了后续检索的质量。块切得好,检索就准;块切得烂,后面怎么优化都救不回来。
2. 几个关键参数:chunkSize、overlap
在动手切之前,有两个参数你必须搞清楚:chunkSize(块大小)和 overlap(重叠量)。
2.1 chunkSize 怎么理解
chunkSize 就是每个块的长度上限。比如你设 chunkSize = 200,意思是每个块最多包含 200 个字符(或 200 个 token,取决于你用什么单位,后面会说)。
chunkSize 设多大合适?这没有标准答案,但有一个基本的权衡:
- 块太大(比如 2000 字):每个块包含的信息多,但检索时容易混入不相关的内容,精度下降。就像用户问退货政策,结果返回了一整章包含退货、换货、维修的内容,模型还得自己从里面挑。
- 块太小(比如 50 字):每个块很精准,但可能把一个完整的意思切断了,上下文丢失。就像把一条退货规则从中间劈开,前半句和后半句单独看都不知道在说什么。
一般来说,200 到 1000 个字符是比较常见的范围,具体取决于你的文档类型和检索需求。
2.2 overlap 是什么,为什么需要它
overlap(重叠)是指相邻两个块之间共享的文本长度。
打个比方:你在看一本小说,每次只能记住一页的内容。如果你严格按页翻,第 1 页看完翻到第 2 页,那第 1 页最后一句话和第 2 页第一句话之间的联系就断了。但如果你每次翻页时,把上一页最后几行重新看一遍,这几行就是重叠的部分,它帮你保持了上下文的连贯性。
用一个具体的例子来看。假设知识库里有这样一段退货政策:
自签收之日起 7 天内,商品未经使用且不影响二次销售的,消费者可申请七天无理由退货。生鲜食品、定制商品、贴身衣物等特殊品类不适用此规则,具体以商品详情页标注为准。
如果 chunkSize = 40,不加 overlap,切出来可能是:
- 块 1:
自签收之日起 7 天内,商品未经使用且不影响二次销售的,消费者可申请七天 - 块 2:
无理由退货。生鲜食品、定制商品、贴身衣物等特殊品类不适用此规则,具体 - 块 3:
以商品详情页标注为准。
注意看,“七天无理由退货”这个关键词被切成了两半,分别落在块 1 和块 2 里。用户搜“七天无理由退货”,两个块都匹配不完整。
2.2.1 不加 overlap 会丢失什么
上面的例子已经很直观了——不加 overlap,相邻块的边界处一定会丢失上下文。如果用户恰好问的问题涉及到边界处的内容,检索就可能找不到完整的答案 。
加上 overlap = 15 之后,切出来变成:
- 块 1:
自签收之日起 7 天内,商品未经使用且不影响二次销售的,消费者可申请七天 - 块 2:
消费者可申请七天无理由退货。生鲜食品、定制商品、贴身衣物等特殊品类不适 - 块 3:
特殊品类不适用此规则,具体以商品详情页标注为准。
块 2 的开头和块 1 的结尾有重叠,“七天无理由退货”在块 2 里是完整的了。
当然,overlap 也不是越大越好。overlap 太大意味着大量重复文本,存储和计算成本都会上升。一般经验是 overlap 设为 chunkSize 的 10%~25%。
2.3 用什么单位:字符 vs token
你可能注意到了,前面说 chunkSize 的时候,有时说字符,有时说 token。这两个东西不一样,有必要区分一下。
字符(Character)就是你肉眼看到的每一个符号。“你好”是 2 个字符,"Hello"是 5 个字符,空格和标点也各算 1 个字符。
Token 是大模型实际处理文本的最小单位。大模型不是一个字一个字地读文本的,它会先把文本切成一个个 token。对于英文,一个常见单词通常是 1 个 token,长一点的单词可能被拆成 2-3 个 token。对于中文,一个汉字通常是 1-2 个 token。
举个例子:
| 文本 | 字符数 | 大约 token 数 |
|---|---|---|
| 七天无理由退货 | 7 | 7-10 |
| Hello World | 11 | 2 |
| 退货政策 | 4 | 4-6 |
为什么要关心这个?因为大模型的上 下文窗口是按 token 算的,不是按字符算的。如果你用字符数来设 chunkSize,实际消耗的 token 数可能比你预期的多(尤其是中文场景)。
不过在入门阶段,用字符数来设 chunkSize 完全够用。等你对 token 有了更深的理解,再考虑切换到基于 token 的分块也不迟。
主流分块策略详解
下面我们用同一段示例文本,来演示几种主流的分块策略。先把示例文本贴出来,后面反复会用到:
一、退货政策
自签收之日起 7 天内,商品未经使用且不影响二次销售的,消费者可申请七天无理由退货。生鲜食品、定制商品、贴身衣物等特殊品类不适用此规则,具体以商品详情页标注为准。退货运费由消费者承担,如因商品质量问题退货,运费由商家承担。
二、换货政策
自签收之日起 15 天内,商品存在质量问题的,消费者可申请免费换货。换货时需提供订单编号、商 品照片及问题描述。换货商品将在审核通过后 3 个工作日内寄出,届时会通过短信通知物流单号。
三、会员权益
普通会员享受 9.5 折优惠,每月可领取 2 张满 200 减 20 的优惠券。黄金会员享受 9 折优惠,每月可领取 4 张满 200 减 30 的优惠券,且退换货享受免运费服务。钻石会员享受 8.5 折优惠,每月可领取 6 张满 200 减 50 的优惠券,退换货免运费,并享有专属客服通道。
四、物流配送
默认使用顺丰快递,偏远地区使用邮政 EMS。订单金额满 99 元包邮,未满 99 元收取 8 元运费。大件商品(家具、家电等)使用德邦物流,配送费根据商品重量和收货地址单独计算。预计配送时间:一线城市 1-2 天,二三线城市 2-4 天,偏远地区 5-7 天。
五、售后服务
所有商品享受 1 年质保服务。质保期内出现非人为损坏的质量问题,可免费维修或更换。超出质保期的维修服务按成本价收取配件费和人工费。如需售后服务,请拨打客服热线 400-XXX-XXXX 或在 APP 内提交售后工单。
这段文本大约 600 个字符,包含 5 个章节,每个章节讲一个独立的业务规则。接下来看看不同的分块策略会把它切成什么样。
1. 固定大小分块(Fixed Size Chunking)
1.1 原理
这是最简单粗暴的方式:不管文本内容是什么,每隔固定数量的字符就切一刀。
假设 chunkSize = 100,overlap = 0,上面那段示例文本会被这样切:
- 块 1:从第 1 个字符开始,取 100 个字符
- 块 2:从第 101 个字符开始,再取 100 个字符
- 块 3:从第 201 个字符开始,再取 100 个字符
- ……依此类推
用一张图来理解:

固定大小分块完全不关心文本的语义——它不管你这一刀切在段落中间还是句子中间,到了字数就切。这既是它的优点(简单),也是它的缺点(可能切断语义)。
1.2 Java 代码实现
import java.util.ArrayList;
import java.util.List;
public class FixedSizeChunker {
/**
* 固定大小分块——最基础的分块方式,按字符数硬切
*/
public static List<String> chunk(String text, int chunkSize) {
List<String> chunks = new ArrayList<>();
int start = 0;
while (start < text.length()) {
int end = Math.min(start + chunkSize, text.length());
chunks.add(text.substring(start, end));
start = end;
}
return chunks;
}
public static void main(String[] args) {
String text = "自签收之日起 7 天内,商品未经使用且不影响二次销售的,"
+ "消费者可申请七天无理由退货。生鲜食品、定制商品、贴身衣物等"
+ "特殊品类不适用此规则,具体以商品详情页标注为准。"
+ "退货运费由消费者承担,如因商品质量问题退货,运费由商家承担。";
List<String> chunks = chunk(text, 40);
for (int i = 0; i < chunks.size(); i++) {
System.out.println("=== 块 " + (i + 1) + " ===");
System.out.println(chunks.get(i));
System.out.println();
}
}
}
跑一下这段代码,你会看到每个块都是严格的 40 个字符,切割位置完全不考虑句子边界。有些块的开头可能是上一句话的尾巴,读起来很突兀。
1.3 优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 实现极其简单,性能好,不需要任何 NLP 处理 |
| 缺点 | 完全忽略文本结构,容易把句子、段落从中间切断,导致语义不完整 |
| 适合 | 文本结构不重要 的场景,比如日志文件、纯数据文本;或者作为其他策略的兜底方案 |
| 不适合 | 有明确段落结构的文档(知识库、产品手册、政策文件),因为切断语义会严重影响检索质量 |
2. 重叠分块(Overlapping Chunking)
2.1 原理
重叠分块是对固定大小分块的直接改进。核心思路很简单:切块的时候,相邻两个块之间留一段重叠区域,这样即使切割点落在句子中间,重叠部分也能保证关键信息不会被完全切断。
还是用退货政策那段文本,chunkSize = 100,overlap = 25:
- 块 1:从第 1 个字符开始,取 100 个字符
- 块 2:从第 75 个字符开始(100-25=75),取 100 个字符
- 块 3:从第 150 个字符开始,取 100 个字符
- ……
用图来看就是这样:

和固定大小分块相比,重叠分块多了一个 overlap 参数,但效果提升明显——边界处的信息不再完全丢失。
2.2 Java 代码实现
import java.util.ArrayList;
import java.util.List;
public class OverlappingChunker {
/**
* 重叠分块——在固定大小的基础上,相邻块之间保留重叠区域
*/
public static List<String> chunk(String text, int chunkSize, int overlap) {
if (overlap >= chunkSize) {
throw new IllegalArgumentException("overlap 必须小于 chunkSize");
}
List<String> chunks = new ArrayList<>();
int step = chunkSize - overlap;
int start = 0;
while (start < text.length()) {
int end = Math.min(start + chunkSize, text.length());
chunks.add(text.substring(start, end));
start += step;
}
return chunks;
}
public static void main(String[] args) {
String text = "自签收之日起 7 天内,商品未经使用且不影响二次销售的,"
+ "消费者可申请七天无理由退货。生鲜食品、定制商品、贴身衣物等"
+ "特殊品类不适用此规则,具体以商品详情页标注为准。"
+ "退货运费由消费者承担,如因商品质量问题退货,运费由商家承担。";
List<String> chunks = chunk(text, 40, 10);
for (int i = 0; i < chunks.size(); i++) {
System.out.println("=== 块 " + (i + 1) + " ===");
System.out.println(chunks.get(i));
System.out.println();
}
}
}
对比固定大小分块的代码,核心区别就一行:start += step 而不是 start = end。step = chunkSize - overlap,每次前进的步长比块大小小一点,这样相邻块就自然产生了重叠。
2.3 优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 实现简单,有效缓解边界处的语义断裂问题 |
| 缺点 | 仍然不看文本内容,只是用重叠来弥补;overlap 会导致存储量增加 |
| 适合 | 大多数通用场景的入门方案,尤其是你还没确定用什么策略的时候 |
| 不适合 | 对语义完整性要求很高的场景,比如法律条款、合同文本 |
3. 递归分块(Recursive Chunking)
3.1 原理
递归分块是目前实践中最常用的策略。它的思路可以用一句话概括:先尝试用最大的分隔符切,切完如果某个块还是太大,就换一个更小的分隔符继续切,直到所有块都在 chunkSize 以内。
具体来说,它维护一个分隔符列表,按优先级从高到低排列,比如:
["\n\n", "\n", "。", ",", " ", ""]
切割过程是这样的:

这个先粗后细的过程就是递归的含义——不是一刀切到底,而是逐层细化。
为什么这种方式好?因为它尽最大努力保留文本的结构。能按段落切就按段落切,段落太长了才按句子切,句子还太长才按逗号切……只有在万不得已的时候才会像固定大小分块那样按字符硬切。
拿我们的电商知识库来说,如果 chunkSize 设成 200,递归分块会先尝试按章节(空行)切割。退货政策那一段大约 150 字,没超过 200,就完整保留为一个块。如果某个章节特别长超过了 200 字,才会进一步按句号切成更小的块。
3.2 优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 兼顾了语义完整性和块大小控制,是目前最通用的分块策略 |
| 缺点 | 分隔符列表需要根据语言调整(中文和英文的标点不同);依赖文本中存在合理的分隔符 |
| 适合 | 绝大多数场景,尤其是你不确定该用什么策略的时候,递归分块是最安全的默认选择 |
| 不适合 | 对分块有特殊要求的场景,比如代码文件(需要按函数/类来切)、表格数据(需要按行来切) |