递归分块(Recursive Chunking)
递归分块(Recursive Chunking)
核心思想
递归分块的思路很简单:按照从大到小的分隔符优先级,依次尝试切分,直到块足够小为止。
不是一刀切,而是有层次地、递归地切。
具体过程
LangChain 的 RecursiveCharacterTextSplitter 是最典型的实现,默认分隔符优先级是:
["\n\n", "\n", "。", " ", ""]
↑最优先 ↑最后才用↑
执行逻辑是这样的:
第一步:用 "\n\n"(段落)切
→ 如果切出来的块 ≤ chunk_size,✅ 保留,不再继续切
→ 如果某块还是太大,⬇️ 对这块递归,用下一级分隔符
第二步:用 "\n"(换行)切
→ 够小了?✅ 保留
→ 还太大?⬇️ 继续递归
第三步:用 "。"(句号)切
→ 够小了?✅ 保留
→ 还太大?⬇️ 继续递归
第四步:用 " "(空格)切
→ 够小了?✅ 保留
→ 还太大?⬇️ 继续递归
第五步:用 ""(逐字符)切
→ 兜底方案,强制切到目标大小
举个例子
假设 chunk_size = 100 字,有这样一段文字:
第一章 产品介绍
本产品是一款智能助手。它可以帮助用户完成日常任务。支持语音和文字输入。
本产品支持多平台使用。包括iOS、Android和Web端。用户可以随时随地使用。用户数据会被加密存储,确保安全。我们采用了业界最先进的加密算法,符合国际安全标准,经过第三方机构认证。
第一步:用 \n\n 切段落
块A: "第一章 产品介绍" → 10字 ✅ 够小,保留
块B: "本产品是一款智能助手。它可以帮助..." → 35字 ✅ 够小,保留
块C: "本产品支持多平台使用。包括iOS...认证。" → 95字 ✅ 够小,保留(刚好)
如果块 C 超过了 100 字限制,就会递归:
第二步:对超大的块 C,用 \n 切换行(这里没有换行,继续)
第三步:用 。 切句子
块C1: "本产品支持多平台使用。" → 12字 ✅
块C2: "包括iOS、Android和Web端。" → 18字 ✅
块C3: "用户可以随时随地使用。" → 12字 ✅
块C4: "用户数据会被加密存储,确保安全。" → 16字 ✅
块C5: "我们采用了业界最先进...国际安全标准,经过第三方机构认证。" → 37字 ✅
每一块都够小了,停止递归。
和固定大小分块的对比
原文:
┌─────────────────────────────────────────┐
│ 第一段(完整段落,80字) │
│ │
│ 第二段第一句话。第二段第二句话。 │
│ 第二段第三句话(很长,整段共150字) │
└─────────────────────────────────────────┘
固定分块(chunk_size=100):
块1: [第一段全部 + 第二段第一句] ← 跨段落,语义混乱 ❌
块2: [第二段第二句 + 第三句]
递归分块(chunk_size=100):
块1: [第一段全部] ← 完整段落,语义清晰 ✅
块2: [第二段第一句 + 第二句] ← 按句子边界切,语义完整 ✅
块3: [第二段第三句]
递归分块尽量在自然语义边界处切割,只有不得已才往更细的粒度走。
为什么叫”递归”?
因为算法会对”还没切够小的块”反复调用自身,换用更细的分隔符再切一遍,直到满足条件为止。这是典型的递归思想:
def split(text, separators, chunk_size):
用 separators[0] 切分 text
for 每个子块:
if 子块 <= chunk_size:
保留 ✅
else:
split(子块, separators[1:], chunk_size) # 递归!
一句话总结
递归分块 = 优先在段落边界切,切不够小再在句子边界切,再不够再在词边界切——尽量保留自然语义,实在没办法才硬切。
这也是为什么它是 LangChain 等框架的默认推荐分块方式,在大多数场景下效果都不错。