跳到主要内容
blog-hexo-astro
返回

chunk 有几种切法:把候选集里的 7 种策略逐个拆开

Table of contents#

Open Table of contents

一、先把两件事分开:「切法」和「参数」#

大多数关于 chunking 的讨论把这两件事混在一起,于是得出「512 就够了」「overlap 调 20%」这类 听上去很具体、其实没头没尾的结论。

是什么举例
切法(strategy)切分的依据是什么字符窗口 / 标点 / 段落 / 条款标记 / 语法树 / 语义相似度 / 表格行
参数(parameter)同一套切法下的旋钮窗口大小、overlap、断点阈值、合并目标长度、最小块长度

候选集 = 切法 × 参数矩阵。所以「切分策略怎么选」这个问题,严格说是两个问题:

  1. 候选集里放哪几种切法(这决定你的上限);
  2. 每种切法取哪几档参数(这决定你能不能把上限拿到)。

第一问靠结构判断,第二问靠实验。下面先把第一问答完。


二、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(重叠)。

失效模式:

  1. 把完整条款劈成两半。合同里「违约责任」这一条可能横跨两个 chunk——检索命中了前半段, 后半段在另一个 chunk 里,答案就断了。
  2. 边界不对齐自然断点。chunk 起点落在段落中间,前置上下文丢一半,靠补前缀也补不回来。
  3. 大块冗余爆炸。窗口开到 2048 字符时,命中块里约 94% 的字符与答案无关。

实测(中文法律语料,全库检索 top-5):

切法sizeoverlap条款切断率段落对齐率命中块冗余recall@5precision@5
固定长度512018.6%9.6%80.5%0.7490.0342
固定长度102408.4%17.7%88.9%0.8650.0214
固定长度204802.7%32.0%93.7%0.9350.0128

对齐率 = chunk 起点落在段落起点的比例。固定切分在 512 档只有 9.6%—— 也就是说 90% 的 chunk 都是从一个段落的中间开始的。

它唯一的价值是兜底:当没有任何结构信息时,它至少不会崩、不会丢内容、不会花一分钱。 但「不崩」和「能用」是两回事——固定切分唯一确定的是长度,唯一不确定的是边界落在哪, 而检索要恰恰是后者。

3.2 ② 递归字符切分:同尺寸下最稳的通用解#

怎么切:维护一个有序的分隔符列表(段落 → 换行 → 句号 → 分号 → 逗号 → 空格 → 单字), 先用最高优先级的分隔符把文本切开,切不动了就降级到下一级;然后把小片段贪心合并到目标长度。

SEPARATORS = ["\n\n", "\n", "。", ";", ",", " ", ""]   # 优先级从高到低

关键在两处:分隔符是有序的(先试粗的,再试细的),合并是贪心的(塞得下就塞,塞不下就起新块)。

旋钮:size(目标长度)、overlap、separators(自定义分隔符序列)。

失效模式:

  1. 依赖标点,遇到没有标点的输入会一路退化到字符级 = 等同固定切分。 表格、(一)(二) 这种编号列表、无标点的 OCR 结果,都是这种输入。 这是递归切分最大的软肋——它的边界质量有下限,但下限就是固定切分。
  2. 贪心合并导致长度不均:512 档实测均长 420,但最长能到 1200。
  3. 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@50.1172(全矩阵最高)0.0485
recall@50.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%

硬前提:解析结果必须保留结构。只拿到一整段纯文本,路由器就只能靠正则碰运气—— 正则能认出「第一条」,但认不出「本合同解除条件」到底是标题还是正文。

失效模式:

成本:几乎为零。边界质量最高。

一句话:这是「赌对了最赚」的一种切法——前提是解析层真的把结构留下来了。 拿纯文本硬怼正则,是把结构感知退化成了「带一点正则的固定切分」。

3.5 ⑤ 语义切分:最贵,也最被高估#

怎么切:句子级 embedding → 相邻句余弦相似度 → 相似度掉到阈值以下的地方断一刀。

旋钮:断点阈值、buffer 大小、用哪个 embedding 模型。

失效模式:

  1. 阈值敏感:换个 embedding 模型,同一套阈值的断点位置就完全不同,参数基本要重标。
  2. 慢:每个文档要多跑一遍 embedding。
  3. 它并不理解文档结构——它只知道「这两句话像不像」,不知道「这是一张表格」。
  4. 它解决的不是主要矛盾。语义切分的卖点是「块内不要混进无关内容」, 但实测里真正拖垮效果的是相邻块之间高度重复——检索一次返回一堆互相重复的证据, 把上下文预算吃光。语义切分不保证后者,贪心合并反而会制造相邻重叠。

什么时候值得用:自由文本(病历、访谈记录、新闻)——没有任何可解析结构的时候。 这时候「按语义断句」确实比「按标点断句」更贴近人的理解。

一句话:语义切分是兜底的兜底——只有在「没有结构」且「不在乎钱」时才值得付这个成本。

3.6 ⑥ 语法树切分:代码域的唯一正确答案#

怎么切:解析成语法树,按节点切(函数 / 类 / 方法),节点超长则在节点内部再切。

旋钮:节点粒度(到函数还是到类)、节点内切分策略。

失效模式:

  1. 语法错误 → 解析失败。真实代码库里一定存在语法错误或半成品文件, 所以必须有 fallback(→ 递归切分),这条路是刚性的。
  2. 节点粒度差异巨大:一个 5000 行的类和十个 3 行的函数,切出来的块完全不是一个量级。

成本:解析器本身不贵,但要按语言维护;收益是代码检索里唯一能同时保住 「函数完整」和「引用可定位」的切法。

3.7 ⑦ 容器型切分:表格与会话#

表格:两种模式——整表一块(WHOLE_TABLE),或按行拆成「表头 + N 行」的小块。 表头必须重复进每一行块,否则行块脱离表头根本没法解读。

会话:session 作为容器,内部按主题断点切;主题边界检测不到 → 退化成按轮次窗口切。

失效模式:表格这块,真正的难点不是切,是解析——跨页表格、合并单元格、扫描件里的表格, 解析不出结构,切得再对也没用。表格类输入几乎必然触发「递归切分退化成固定切分」那条软肋。

成本:低到中(版面分析或多轮主题检测)。


四、候选集怎么组出来:切法 × 参数矩阵#

把上面的切法摆进候选集,要处理两件事:哪些参数是共享的,哪些切法在当前语料上根本组不出配置。

共享旋钮(对所有切法都成立):

切法私有旋钮:

切法私有旋钮
递归字符分隔符序列
段落是否合并段落
语义断点阈值、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:

所以候选集的正确形态是分层排:

层切法组数单组成本
零成本层固定长度 / 递归 / 段落 / 条款感知满配毫秒
中成本层语义 / 容器型少量精选秒级
高成本层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 点

怎么用这张表:


六、落到一个真实系统上#

如果你正在一个自研知识库模块里实现切分,这几个坑是清单级的:

要点怎么落
切法要可插拔每个切法实现同一个接口,路由只依赖「输入 + 配置 → 一批块」,不依赖具体实现
fallback 链要显式结构感知 → 段落 → 递归 → 固定,每一级都要能接住上一级的失败
超长块要继承元数据二次切分出来的每一块,条款号、标题、页码都要带着走
别忘了兜底切法固定切分不是「零风险默认」,但它必须有——它是最后一道网
解析要留结构切分器拿到的是有结构的块序列,不是一整段纯文本;否则结构感知这条路根本走不通

常见默认配置长这样(目标 500 字符、上限 1200、下限 150、overlap 50 字符、上下文前缀开), 注意这四个数不是一个东西:target 是「合并到什么大小」,max 是「超了必须切」, min 是「太小要并」,overlap 是「相邻块重复多少」。把它们混成一个 chunk_size 是很多实现的起点错误。

还有一个容易漏掉的缺口:很多实现里,「结构感知切不开」这条路的下一级兜底是按字符硬切—— 也就是说,超长条款最终是被固定长度切开的。这正好落在实测里被全面压制的那条上: 固定切分在 512 档会切断 18.6% 的条款,而递归切分只有 0.9%。 所以 fallback 链里必须真的有一级递归切分(按标点降级合并),而不是拿固定切分顶替—— 固定切分只能待在链的最后,当最后一道网。


收尾#

如果只带走一件事:切法的选择顺序是「这份文档有没有可解析的显式结构」→「切法」→「参数」。

结构在,切法就是确定答案(合同按条款、代码按语法树、表格按表行); 结构不在,切法才轮得到「递归 vs 语义」这种选择题——而这两者都不便宜,也不比另一种强多少。 真正该省力气的地方是参数:它不改变上限,只决定你离上限有多远。


附:素材来源(发布前删掉)#



上一篇
大模型配置设计架构思考