Table of contents#
Open Table of contents
一、先把两件事分开:「切法」和「参数」#
大多数关于 chunking 的讨论把这两件事混在一起,于是得出「512 就够了」「overlap 调 20%」这类 听上去很具体、其实没头没尾的结论。
| 是什么 | 举例 | |
|---|---|---|
| 切法(strategy) | 切分的依据是什么 | 字符窗口 / 标点 / 段落 / 条款标记 / 语法树 / 语义相似度 / 表格行 |
| 参数(parameter) | 同一套切法下的旋钮 | 窗口大小、overlap、断点阈值、合并目标长度、最小块长度 |
候选集 = 切法 × 参数矩阵。所以「切分策略怎么选」这个问题,严格说是两个问题:
- 候选集里放哪几种切法(这决定你的上限);
- 每种切法取哪几档参数(这决定你能不能把上限拿到)。
第一问靠结构判断,第二问靠实验。下面先把第一问答完。
二、7 种切法速览#
按「边界从哪来」分,这 7 种落在四个族上:
几何 语言学 文档结构 语义
│ │ │ │
① 固定长度 ② 递归字符切分 ④ 条款/结构感知 ⑤ 语义切分
③ 段落边界 ⑥ 语法树(AST)
⑦ 容器型(表格/会话)
| # | 切法 | 边界从哪来 | 主要旋钮 | 额外依赖 | 成本 | 一句话失效模式 |
|---|---|---|---|---|---|---|
| ① | 固定长度 fixed | 字符窗口 | size / overlap | 无 | 零 | 把完整条款劈成两半,且错得很隐蔽 |
| ② | 递归字符 recursive | 分隔符优先级 | size / overlap / 分隔符序列 | 无 | 零 | 无标点 / 表格 / 编号列表 → 退化成固定切分 |
| ③ | 段落边界 paragraph | 空行 | 合并目标长度 | 解析保留段落 | 零 | 条款天然跨段落 → 被切碎 |
| ④ | 条款/结构感知 clause-aware | 条款标记 | 上限长度 / 二次切分 | 解析保留结构(或正则) | 低 | 标记解析不出来 → 返回空结果 |
| ⑤ | 语义切分 semantic | 句向量相似度断点 | 断点阈值 / buffer / 模型 | embedding 模型 | 中 | 阈值敏感、慢,而且它并不理解文档结构 |
| ⑥ | 语法树 AST | 语法节点 | 节点粒度 | 语言解析器 | 中高 | 语法错误 → 解析失败(必须有 fallback) |
| ⑦ | 容器型 table / session | 表结构 / 对话轮次 | 整表 or 按行 / 窗口大小 | 版面或对话结构 | 低中 | 表格根本不是切的问题,是解析的问题 |
④ 和 ⑦ 是同一族的两个变体:先认出一个「容器」,再决定容器内怎么切。 区别是 ④ 的容器(条款)在正文里就有标记,⑦ 的容器(表格、会话)要靠版面或对话结构才能拿到。
下面逐个拆。每一种都回答三个问题:它到底怎么切、旋钮在哪、它在什么条件下会坏。
三、逐个拆:怎么切 · 旋钮 · 什么时候坏#
3.1 ① 固定长度:最省事,也是最容易悄悄坏掉的#
怎么切:字符窗口,步长 step = size − overlap,从 0 开始往后推,最后一块截断。
def split_by_size(text, size, overlap):
step = max(1, size - max(0, overlap))
return [text[i:i + size] for i in range(0, len(text), step)]
旋钮:size(窗口)、overlap(重叠)。
失效模式:
- 把完整条款劈成两半。合同里「违约责任」这一条可能横跨两个 chunk——检索命中了前半段, 后半段在另一个 chunk 里,答案就断了。
- 边界不对齐自然断点。chunk 起点落在段落中间,前置上下文丢一半,靠补前缀也补不回来。
- 大块冗余爆炸。窗口开到 2048 字符时,命中块里约 94% 的字符与答案无关。
实测(中文法律语料,全库检索 top-5):
| 切法 | size | overlap | 条款切断率 | 段落对齐率 | 命中块冗余 | recall@5 | precision@5 |
|---|---|---|---|---|---|---|---|
| 固定长度 | 512 | 0 | 18.6% | 9.6% | 80.5% | 0.749 | 0.0342 |
| 固定长度 | 1024 | 0 | 8.4% | 17.7% | 88.9% | 0.865 | 0.0214 |
| 固定长度 | 2048 | 0 | 2.7% | 32.0% | 93.7% | 0.935 | 0.0128 |
对齐率 = chunk 起点落在段落起点的比例。固定切分在 512 档只有 9.6%—— 也就是说 90% 的 chunk 都是从一个段落的中间开始的。
它唯一的价值是兜底:当没有任何结构信息时,它至少不会崩、不会丢内容、不会花一分钱。 但「不崩」和「能用」是两回事——固定切分唯一确定的是长度,唯一不确定的是边界落在哪, 而检索要恰恰是后者。
3.2 ② 递归字符切分:同尺寸下最稳的通用解#
怎么切:维护一个有序的分隔符列表(段落 → 换行 → 句号 → 分号 → 逗号 → 空格 → 单字), 先用最高优先级的分隔符把文本切开,切不动了就降级到下一级;然后把小片段贪心合并到目标长度。
SEPARATORS = ["\n\n", "\n", "。", ";", ",", " ", ""] # 优先级从高到低
关键在两处:分隔符是有序的(先试粗的,再试细的),合并是贪心的(塞得下就塞,塞不下就起新块)。
旋钮:size(目标长度)、overlap、separators(自定义分隔符序列)。
失效模式:
- 依赖标点,遇到没有标点的输入会一路退化到字符级 = 等同固定切分。
表格、
(一)(二)这种编号列表、无标点的 OCR 结果,都是这种输入。 这是递归切分最大的软肋——它的边界质量有下限,但下限就是固定切分。 - 贪心合并导致长度不均:512 档实测均长 420,但最长能到 1200。
- overlap 复制内容占预算:对 precision 没有任何收益,纯粹是索引体积和上下文预算的损耗。
实测:与固定切分同尺寸配对比较,9 组配对全部成立,无一例外:
| 指标 | 固定切分 | 递归切分 |
|---|---|---|
| 条款切断率(512 档) | 18.6% | 0.9% |
| 段落对齐率 | 8% ~ 32% | 93.6% ~ 99.3% |
| recall@5(512 → 2048) | 0.749 → 0.935(+18.6 点) | 0.849 → 0.947(+9.8 点) |
| precision@5(512 → 2048) | 0.0342 → 0.0128(掉 62%) | 0.0485 → 0.0136(掉 72%) |
一句话:递归切分的价值不是「切得更准」,是边界质量有下限—— 最差情况退化成固定切分,好的时候边界全部落在自然断点上。
3.3 ③ 段落边界:最容易被误解的一种#
怎么切:按空行把文档拆成段落,段落合并到目标长度;单个段落超长再按字符切开。
旋钮:合并目标长度(是否合并段落本身就是个开关)。
常见误解:「段落 = 条款」。实测不成立:
| 指标 | 段落切分(语料原生段落,均长 125 字符) | 递归切分 512 |
|---|---|---|
| 条款切断率 | 1.8%(不是 0) | 0.9% |
| 命中块冗余 | 18.8%(全矩阵最低) | 73.8% |
| precision@5 | 0.1172(全矩阵最高) | 0.0485 |
| recall@5 | 0.647(全矩阵最低) | 0.849 |
一条法条被拆到两个自然段时,按段切就会把它切碎;而段落切分的 precision 是 2048 大块的 9.2 倍 (0.1172 vs 0.0128),代价是 recall 只有 68%(0.647 vs 0.947)。
它在候选集里的正确位置:不是「小块的默认策略」,而是小粒度一端的基准线—— 用来标定「块切到多小会碎掉」。任何声称要拿小块上生产的人,都该先和这条线比一比。
3.4 ④ 条款 / 结构感知:合同与法规域的默认解#
怎么切:用正则或解析器识别条款边界(第一条、一、、1.2.3 这类标记),
一个条款 = 一个 chunk,条款号单独存字段。
三个必须处理的细节:
| 细节 | 为什么 |
|---|---|
| 超长条款要二次切分 | 一条 3000 字符的「违约责任」必须再切一刀,但切出来的每一块要继承条款号 |
| 条款号存独立字段 | 只塞进正文,检索时既没法过滤也没法在答案里精确引用 |
| 上下文前缀(标题 + 条款号) | 把标题拼进 chunk 文本,recall +1.6 ~ +4.0 点,precision 相对 +5% ~ +13% |
硬前提:解析结果必须保留结构。只拿到一整段纯文本,路由器就只能靠正则碰运气—— 正则能认出「第一条」,但认不出「本合同解除条件」到底是标题还是正文。
失效模式:
- 条款标记解析不出来(扫描件、格式怪、标记不统一)→ 返回空 → 必须有 fallback 链接住;
- 一条条款本身就是一整章(有些合同这么写)→ 二次切分触发得比预期频繁。
成本:几乎为零。边界质量最高。
一句话:这是「赌对了最赚」的一种切法——前提是解析层真的把结构留下来了。 拿纯文本硬怼正则,是把结构感知退化成了「带一点正则的固定切分」。
3.5 ⑤ 语义切分:最贵,也最被高估#
怎么切:句子级 embedding → 相邻句余弦相似度 → 相似度掉到阈值以下的地方断一刀。
旋钮:断点阈值、buffer 大小、用哪个 embedding 模型。
失效模式:
- 阈值敏感:换个 embedding 模型,同一套阈值的断点位置就完全不同,参数基本要重标。
- 慢:每个文档要多跑一遍 embedding。
- 它并不理解文档结构——它只知道「这两句话像不像」,不知道「这是一张表格」。
- 它解决的不是主要矛盾。语义切分的卖点是「块内不要混进无关内容」, 但实测里真正拖垮效果的是相邻块之间高度重复——检索一次返回一堆互相重复的证据, 把上下文预算吃光。语义切分不保证后者,贪心合并反而会制造相邻重叠。
什么时候值得用:自由文本(病历、访谈记录、新闻)——没有任何可解析结构的时候。 这时候「按语义断句」确实比「按标点断句」更贴近人的理解。
一句话:语义切分是兜底的兜底——只有在「没有结构」且「不在乎钱」时才值得付这个成本。
3.6 ⑥ 语法树切分:代码域的唯一正确答案#
怎么切:解析成语法树,按节点切(函数 / 类 / 方法),节点超长则在节点内部再切。
旋钮:节点粒度(到函数还是到类)、节点内切分策略。
失效模式:
- 语法错误 → 解析失败。真实代码库里一定存在语法错误或半成品文件, 所以必须有 fallback(→ 递归切分),这条路是刚性的。
- 节点粒度差异巨大:一个 5000 行的类和十个 3 行的函数,切出来的块完全不是一个量级。
成本:解析器本身不贵,但要按语言维护;收益是代码检索里唯一能同时保住 「函数完整」和「引用可定位」的切法。
3.7 ⑦ 容器型切分:表格与会话#
表格:两种模式——整表一块(WHOLE_TABLE),或按行拆成「表头 + N 行」的小块。
表头必须重复进每一行块,否则行块脱离表头根本没法解读。
会话:session 作为容器,内部按主题断点切;主题边界检测不到 → 退化成按轮次窗口切。
失效模式:表格这块,真正的难点不是切,是解析——跨页表格、合并单元格、扫描件里的表格, 解析不出结构,切得再对也没用。表格类输入几乎必然触发「递归切分退化成固定切分」那条软肋。
成本:低到中(版面分析或多轮主题检测)。
四、候选集怎么组出来:切法 × 参数矩阵#
把上面的切法摆进候选集,要处理两件事:哪些参数是共享的,哪些切法在当前语料上根本组不出配置。
共享旋钮(对所有切法都成立):
size/target:目标块长度overlap:重叠长度min size:小于它就并进相邻块(滤碎片)- 上下文前缀开关
切法私有旋钮:
| 切法 | 私有旋钮 |
|---|---|
| 递归字符 | 分隔符序列 |
| 段落 | 是否合并段落 |
| 语义 | 断点阈值、buffer、embedding 模型 |
| AST | 节点粒度 |
| 容器型 | 整表 / 按行、轮次窗口大小 |
一个真实跑过的矩阵(3 种切法 × 3 档 size × 3 档 overlap = 27 组, 2,267 道题、单核 CPU、148 秒跑完):
size
512 1024 2048
┌─────────┬─────────┬─────────┐
ov 0% │ ✓ │ ✓ │ ✓ │
ov 10% │ ✓ │ ✓ │ ✓ │
ov 20% │ ✓ │ ✓ │ ✓ │
└─────────┴─────────┴─────────┘
fixed / recursive / paragraph 各一套
为什么完整候选集是 40~60 组而不是 27 组:因为真实候选集里有 7 种切法, 但不是每种都能乘满 3×3:
- 有些切法只有一个有效旋钮(比如容器型的表格模式,size 基本被表格大小锁死);
- 有些切法在当前语料上没有输入(语料里没有代码,就组不出 AST 的配置;全是散文,就组不出 session 的配置);
- 贵的切法不能全量跑——语义切分每组都要多付一遍 embedding,AST 每组都要付一遍解析。
所以候选集的正确形态是分层排:
| 层 | 切法 | 组数 | 单组成本 |
|---|---|---|---|
| 零成本层 | 固定长度 / 递归 / 段落 / 条款感知 | 满配 | 毫秒 |
| 中成本层 | 语义 / 容器型 | 少量精选 | 秒级 |
| 高成本层 | AST | 按需 | 秒级 + 语言解析器 |
五、把实测结论映射回「切法」#
实测数据容易读成「512 比 1024 好」这种参数级结论,但真正能指导设计的,是切法级结论—— 换参数也成立的那种。
| 结论 | 级别 | 依据 |
|---|---|---|
| 递归切分在同尺寸下全面压制固定切分 | 切法级 | 9 组配对全部成立,四项指标全赢 |
| 固定切分的价值只剩兜底 | 切法级 | 同上 |
| overlap 是保险不是杠杆 | 切法级(分场景) | 固定切分上 ov0→ov20% 切断率 18.6%→6.8%;递归切分上几乎无影响 |
| 标题 metadata 传播有效 | 切法级(与切法无关) | 任何切法都该做:recall +1.6 ~ +4.0 点 |
| 块越大召回越好 | 参数级,且是度量偏差的产物 | 大块 recall 0.947 但命中块 94% 字符无关 |
| size 的影响远大于 overlap | 参数级 | 18.6 点 vs 3.3 点 |
怎么用这张表:
- 切法级结论决定「哪些切法值得进候选集」——不值得进的直接砍掉,省掉整个参数矩阵;
- 参数级结论决定「每种切法取哪几档」;
- 最实用的一条是那条跟切法无关的:上下文前缀这种 metadata 传播, 不管你最后选哪种切法都该做——它是这份矩阵里投入产出比最高的一项。
六、落到一个真实系统上#
如果你正在一个自研知识库模块里实现切分,这几个坑是清单级的:
| 要点 | 怎么落 |
|---|---|
| 切法要可插拔 | 每个切法实现同一个接口,路由只依赖「输入 + 配置 → 一批块」,不依赖具体实现 |
| fallback 链要显式 | 结构感知 → 段落 → 递归 → 固定,每一级都要能接住上一级的失败 |
| 超长块要继承元数据 | 二次切分出来的每一块,条款号、标题、页码都要带着走 |
| 别忘了兜底切法 | 固定切分不是「零风险默认」,但它必须有——它是最后一道网 |
| 解析要留结构 | 切分器拿到的是有结构的块序列,不是一整段纯文本;否则结构感知这条路根本走不通 |
常见默认配置长这样(目标 500 字符、上限 1200、下限 150、overlap 50 字符、上下文前缀开),
注意这四个数不是一个东西:target 是「合并到什么大小」,max 是「超了必须切」,
min 是「太小要并」,overlap 是「相邻块重复多少」。把它们混成一个 chunk_size 是很多实现的起点错误。
还有一个容易漏掉的缺口:很多实现里,「结构感知切不开」这条路的下一级兜底是按字符硬切—— 也就是说,超长条款最终是被固定长度切开的。这正好落在实测里被全面压制的那条上: 固定切分在 512 档会切断 18.6% 的条款,而递归切分只有 0.9%。 所以 fallback 链里必须真的有一级递归切分(按标点降级合并),而不是拿固定切分顶替—— 固定切分只能待在链的最后,当最后一道网。
收尾#
如果只带走一件事:切法的选择顺序是「这份文档有没有可解析的显式结构」→「切法」→「参数」。
结构在,切法就是确定答案(合同按条款、代码按语法树、表格按表行); 结构不在,切法才轮得到「递归 vs 语义」这种选择题——而这两者都不便宜,也不比另一种强多少。 真正该省力气的地方是参数:它不改变上限,只决定你离上限有多远。
附:素材来源(发布前删掉)#
- 切法清单与路由表:一份内部 chunk 策略评测方案稿(四层漏斗评测设计)的 L0 候选集部分, 以及对「领域 → 策略」静态映射的评审结论(对应本篇第一、二、四节)。
- 切法级实测:在一个开源中文法律 RAG 基准上跑的切分矩阵实验——3 种切法 × 3 档 size × 3 档 overlap = 27 组配置、2,267 道题、单核 CPU 148 秒。评测口径为纯 BM25 检索(自实现,字符 bigram 分词), 刻意不用向量,以免语义模型的能力盖过切分效应。对应本篇第三节各表与第五节。 ⚠️ 该数据集没有明确的 LICENSE,正文按惯例不点名、不引用其原始文档,只讲方法与自己的测量口径。
- 落地现状:一个自研知识库模块的切分实现(条款感知为主 + 段落 + 固定兜底 + 上下文前缀), 其中的 fallback 链、参数语义、「解析未保留 block 结构」、以及「超长条款目前是按字符硬切、 链里没有递归切分这一级」这几点对应本篇第六节。
- 公开可查的引用:Legal-DC 基准论文、HOPE(chunk 间语义独立性)、LREC 2026 adaptive chunking、 MoC (ACL 2025)、RAGAS(Context Precision / Recall)。