跳到主要内容

递归切块逐行走读

本章目标:讲清 Chonkie 默认主力 RecursiveChunker 的核心思路与实现。它是「结构感知、零模型」甜区的代表,也是理解 Chonkie 编排风格的最佳样本。

1. 它要解决的小问题

固定 token 数硬切的毛病:可能把一句话、甚至一个词从中间劈开。我们更想优先在「自然边界」切——先尽量按段落分,段落太大再按句子分,句子还太大再按标点分……一级级降级,直到每块都不超过 chunk_size

这就是「递归」的含义:对每一级切出来仍然超标的碎片,用下一级规则再切一次。

2. 降级规则:RecursiveRules

规则是一个有序的级别列表 RecursiveRules(src/chonkie/types/recursive.py:112)。不传时,默认 5 级(recursive.py:118-150):

级别切法分隔符
0 段落按段落["\n\n", "\r\n", "\n", "\r"]
1 句子按句末[". ", "! ", "? "]
2 停顿按标点{ } " [ ] ( ) : ; , — | ~ - ... \ '` 等
3 词按空格whitespace=True
4 token按 token 数(无分隔符,直接编码切)

每一级是一个 RecursiveLevel(recursive.py:11-28),三种切法互斥(delimiters / whitespace / pattern 只能选一个,_validate_fieldsrecursive.py:30-60 强制校验)。第 4 级三者皆空 → 落到「按 token 编码硬切」。

怎么读这套规则

整篇文本
│ level 0:按段落切

[段落A][段落B(太大)][段落C]
│ 段落B 仍 > chunk_size → level 1:按句子切

[句1][句2(太大)]
│ 句2 仍 > chunk_size → level 2:按标点切 …… 以此类推

最终每块 ≤ chunk_size

命中即停:某块一旦 ≤ chunk_size,就不再往下切了。只有超标的块才继续降级。

3. 原理演示

核心递归逻辑可以这样直觉化(去掉细节):

# 示意,非源码(提炼 recursive.py:190-227 的递归骨架)
def recursive_chunk(text, level=0, offset=0):
if level >= len(rules): # 没有更细的规则了
return [make_chunk(text, offset)] # 当前文本整体成一块

splits = split_text(text, rules[level]) # 用本级规则切
merged = merge_short_splits(splits) # 把太短的碎片合并回来

chunks = []
for piece in merged:
if token_count(piece) > chunk_size: # 还太大
chunks += recursive_chunk(piece, level + 1, offset) # 下一级再切
else:
chunks.append(make_chunk(piece, offset))
offset += len(piece) # 累加偏移,保证索引对齐
return chunks

重点看两件事:(1) 超标才递归;(2) offset 一路累加,每块的 start_index 永远指向原始文本里的真实位置。

4. 真实实现:三处关键

4.1 切:_split_text

按本级规则选切法(recursive.py:120-143)。前两种委托给共享的 split_text_by_delimiters(见第 1 章);最后一级(无分隔符)直接走 tokenizer 的「编码 → 按 chunk_size 切片 → 批量解码」:

# 真实源码 recursive.py:138-142
encoded = self.tokenizer.encode(text)
token_splits = [encoded[i : i + self.chunk_size]
for i in range(0, len(encoded), self.chunk_size)]
splits = list(self.tokenizer.decode_batch(token_splits))

4.2 合并:_merge_splits(下沉 Rust)

切完常得到一堆比 chunk_size 小得多的碎片(比如每个短句),直接成块太碎。于是要把相邻碎片贪心合并到接近 chunk_size。这步的字符串拼接是热点,交给 Rust(recursive.py:168-188):

# 真实源码 recursive.py:183-188
if all(counts > self.chunk_size for counts in token_counts):
return splits, token_counts # 全都超标,无可合并,直接返回
result = chonkie_core.merge_splits(splits, token_counts, self.chunk_size)
return result.merged, result.token_counts

chonkie_core.merge_splits 接收「碎片列表 + 各自 token 数 + 目标大小」,返回合并后的 merged 与对应 token_counts(inferred:其实现不在本 clone,但从签名与调用点可推断它做的是「贪心装箱到 chunk_size」)。

4.3 偏移量的小聪明:_make_chunks

算一个块在原文里的起止位置,朴素做法是 text.find(piece)——慢且可能找错(相同文本出现多次)。Chonkie 不这么干:它一路累加 start_offset,块的终点直接用 start + len(text)(recursive.py:145-166):

# 真实源码 recursive.py:161-166
return Chunk(
text=text,
start_index=start_offset,
end_index=start_offset + len(text),
token_count=token_count,
)

注释明说这是「避免更慢的全文搜索」(recursive.py:148-149)。这依赖一个前提:切分不丢字符——所有碎片首尾相接就是原文,所以累加长度就等于真实偏移。

4.4 token 计数带缓存

递归过程会反复对相同/相似文本数 token,所以 _estimate_token_count 套了 @lru_cache(maxsize=4096)(recursive.py:114-118)。值得注意:函数名叫 estimate,但注释说明它返回真实计数——早期版本曾用估算做优化提示,现已统一为精确值(recursive.py:116-117)。

5. Recipe:把规则做成可复用配方

不想手写规则?用 from_recipe(recursive.py:74-112)。它从 Chonkie 的 HuggingFace「配方仓库」拉一份针对某语言/某格式(如 markdown)的 RecursiveRules:

# 示意,非源码(贴近 recursive.py:74-112)
chunker = RecursiveChunker.from_recipe(name="markdown", lang="en")

背后是 RecursiveRules.from_recipe(types/recursive.py:193-220)经 Hubbie 工具下载并反序列化成级别列表。Pipeline 里写 recipe="markdown" 也是走这条路(见第 4 章参数拆分)。

6. 边界与坑

  • 依赖「切分不丢字符」:偏移量计算建立在「碎片拼回即原文」上。include_delim="prev" 让分隔符留在前块,正是为了不丢字符。
  • 默认 tokenizer 是 "character"(recursive.py:39),即字符计数。这意味着默认 chunk_size=2048 指的是2048 个字符,不是 GPT token——想按真 token 切要显式传 tokenizer="gpt2" 之类。
  • 全超标短路:若所有碎片都已超 chunk_size,跳过合并直接递归下一级(recursive.py:183),避免无意义的 Rust 调用。

7. 代码地图

主题文件符号
递归切块器src/chonkie/chunker/recursive.pyRecursiveChunker
递归主循环src/chonkie/chunker/recursive.py_recursive_chunk
本级切分src/chonkie/chunker/recursive.py_split_text
碎片合并(Rust)src/chonkie/chunker/recursive.py_merge_splitschonkie_core.merge_splits
偏移量计算src/chonkie/chunker/recursive.py_make_chunks
token 计数缓存src/chonkie/chunker/recursive.py_estimate_token_count
降级规则 / 默认 5 级src/chonkie/types/recursive.pyRecursiveRulesRecursiveLevel
配方加载src/chonkie/types/recursive.pyRecursiveRules.from_recipe