数据截至 (上游 commit 3b55ae605435)
递归切块逐行走读
本章目标:讲清 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_fields 在 recursive.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.py | RecursiveChunker |
| 递归主循环 | src/chonkie/chunker/recursive.py | _recursive_chunk |
| 本级切分 | src/chonkie/chunker/recursive.py | _split_text |
| 碎片合并(Rust) | src/chonkie/chunker/recursive.py | _merge_splits、chonkie_core.merge_splits |
| 偏移量计算 | src/chonkie/chunker/recursive.py | _make_chunks |
| token 计数缓存 | src/chonkie/chunker/recursive.py | _estimate_token_count |
| 降级规则 / 默认 5 级 | src/chonkie/types/recursive.py | RecursiveRules、RecursiveLevel |
| 配方加载 | src/chonkie/types/recursive.py | RecursiveRules.from_recipe |