深色模式
文档切分策略 Chunking
摘要:本文面向搭建 RAG 索引的工程师。切分(chunking)直接决定召回的粒度与信噪比:切得太碎会割裂语义、丢失上下文;切得太大则引入噪声、浪费上下文窗口。我们对比固定窗口、递归字符、语义、句子窗口、父子分块五种策略,并给出中文与代码场景的可复制配置。
切分没有银弹
最优 chunk 大小高度依赖文档类型与嵌入模型上下文长度。以 bge-m3 为例,序列长度上限 8192 token,但实践中 256–512 token 的 chunk 往往比塞满 8192 的 chunk 召回更精准。所有参数都应通过 RAG 评测 验证,而非拍脑袋。
核心概念
Chunking 是把长文档拆成若干"片段(chunk)"的过程,每个 chunk 独立向量化并被检索。关键变量:
- chunk_size:单块 token/字符数
- chunk_overlap:相邻块重叠量,缓解边界割裂
- 分块边界:按字符、句子、段落、标题层级、语义
架构与原理
五种策略对比
| 策略 | 机制 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 固定窗口 | 每 N 字符一刀切 | 简单、可预测 | 切断句子、语义割裂 | 快速原型 |
| 递归字符 | 按 \n\n/\n/. 优先级切 | 尽量不打断结构 | 仍可能切句 | 通用文本 |
| 语义 | 按嵌入相似度断点切 | 语义完整 | 慢、需调阈值 | 长论述 |
| 句子窗口 | 检索句子、扩窗口喂生成 | 召回准、上下文全 | 索引/查询两次 | 精细 QA |
| 父子 | 小块检索、父块喂生成 | 兼顾精准与上下文 | 需双索引 | 生产首选 |
生产首选:父子分块 + 句子窗口
检索用小块保证精准,生成用大块/父块保证上下文完整,是 Advanced RAG 的标配。LlamaIndex 的 SentenceWindowNodeParser 与 HierarchicalNodeParser 直接提供这两种能力。
生产实践与配置
1. 递归字符切分(LangChain,中文需自定义分隔符)
中文没有空格分词,英文 \n\n/. 分隔符对中文不友好。需加入中文标点与换行:
python
# pip install langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
# 中文优先按段落/句号/分号/逗号切
separators=["\n\n", "\n", "。", ";", ",", ""],
chunk_size=512, # 字符数;中文约 256-400 token
chunk_overlap=64, # 重叠约 12.5%,缓解边界割裂
length_function=len,
)
chunks = splitter.split_text(long_doc_text)
print(f"切出 {len(chunks)} 块,首块长度 {len(chunks[0])}")1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2. 父子分块(LlamaIndex)
python
# pip install llama-index>=0.11
from llama_index.core.node_parser import HierarchicalNodeParser
from llama_index.core import VectorStoreIndex
node_parser = HierarchicalNodeParser.from_defaults(
chunk_size=2048, # 父块(喂生成)
child_block_size=512, # 子块(用于检索)
chunk_overlap=64,
)
nodes = node_parser.get_nodes_from_documents(documents)
index = VectorStoreIndex(nodes)
# 检索命中子块,自动回溯父块作为上下文1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
3. 代码切分(按语法,而非字符)
代码应按函数/类切分,避免把半个函数喂给模型:
python
from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
py_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=800,
chunk_overlap=80,
)
code_chunks = py_splitter.split_text(python_source)1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
验证
bash
# 检查分块分布是否健康(避免大量超长/超短块)
python -c "
import statistics, sys
sizes = [len(c) for c in chunks]
print('块数', len(sizes), '均值', statistics.mean(sizes),
'中位', statistics.median(sizes), '最大', max(sizes))
"1
2
3
4
5
6
7
2
3
4
5
6
7
经验基线(非实测,需评测验证)
- 通用中文文档:
chunk_size=256–512 token,overlap=10–15%。 - 法律/合同长句密集:可放大到
512–1024 token,配合句子窗口。 - 表格/代码:按结构切,避免跨边界。
回滚与清理
改切分策略要重建索引
chunk_size / overlap / 分隔符变化会改变所有向量,必须全量重建索引并蓝绿切换(见 llm-rag.md 回滚章节)。不要对混合旧版 chunk 的集合直接增量更新。
故障排查
- 召回到错误段落:chunk 过大混入无关内容 → 缩小 chunk_size 或改父子分块。
- 答案缺上下文:chunk 过小、overlap 不足 → 增大 overlap 或句子窗口扩窗。
- 中文切得乱:未配置中文分隔符 → 见上方递归切分配置。
- 索引暴涨:overlap 过高或文档重复 → 控制 overlap、做去重(SimHash/MinHash)。
安全与合规
切分阶段的数据安全
切分在数据落库前发生,若源文档含个人敏感信息(PII),切分后仍以明文存于向量库。应在切分前做脱敏/分级,并按 tenant_id 打标,供检索期过滤(见 llm-rag.md 安全章节)。切分日志不要打印完整 chunk 内容。
成本与性能
- 切分本身几乎零成本(CPU 即可),瓶颈在随后的 Embedding 调用。
- 减小 chunk_size 会增加 chunk 总数 → 嵌入调用次数线性上升、向量库存储上升;需权衡召回质量与嵌入/存储成本。
- 语义切分需对每个候选断点做嵌入推理,显著更慢,仅建议在离线索引阶段对高价值语料使用。