第7章:文档切割——Chunking 的策略与最佳实践
系列导读:本章是 RAG 的进阶篇。很多人以为 chunking 就是「切成 500 token」,但实际上它是影响检索质量最关键的工程决策之一。不好的 chunking 策略会让检索系统形同虚设——合适的内容检索不到,不合适的内容塞给 LLM,最终答案质量大打折扣。
一、为什么 chunking 决定 RAG 生死?
RAG 系统里有一个核心事实:
LLM 最终回答的质量,取决于你放进 prompt 里的参考资料质量。
参考资料哪来的?从向量库里检索来的。 向量库里存的是 chunk,chunk 是切割出来的。
如果 chunk 把一段重要信息中间切断了,或者把两段无关内容塞进了一个 chunk,检索出来的就是垃圾。垃圾进,垃圾出。你放再贵的 Embedding 模型、再牛的 LLM,都救不回一块切烂的 chunk。
二、Chunking 的四大基础策略
策略1:固定大小 + 重叠(Fixed Size with Overlap)
原文:今天天气很好,小明去公园散步。他在湖边看到一只白鹭...
|-------chunk1-------|--------chunk2-------|
今天天气很好,小明去 明去公园散步。他在湖边
(size=12, overlap=4) 看到一只白鹭在水面上
(size=12, overlap=4)
适用场景:通用文本,写起来最快,作为基线方案再用别的方法优化。 参数建议:每 chunk 500-1000 token 是常见起点;重叠 10-20% 防止信息在切分处丢失。 问题:机械切分会切断语义边界。一个句子被拦腰截断,LLM 收到 chunk 后理解困难。
优点:实现极其简单,不需要解析文档结构,可移植性好。几乎所有初级 RAG 教程都用这个方案。
策略2:按语义边界切割(Semantic Chunking)
观察文档结构,在天然边界处切:
- Markdown/HTML 的标题、段落
- 文档的章节层次
- 知识库的主题切换
【原文】
# 第一章 Java 基础
第一节 变量和数据类型
变量是程序中...
第二节 控制流
if 语句用于...
### 切分点:标题边界
chunk1 = "第一节 变量和数据类型\n变量是程序中..."
chunk2 = "第二节 控制流\nif 语句用于..."
适用场景:结构化的知识库、技术文档、产品手册。 优势:语意完整,检索命中后 LLM 能有完整上下文。 挑战:不同文档格式解析不一致,需要自定义切分逻辑。比如 PDF 就没有天然的分段标记,需要先解析出文本结构再决定切割点。
策略3:按代码逻辑切割
代码不是普通文本。函数、类、接口这些天然边界就是最合理的 chunk 粒度:
def calculate_discount(price, user_type):
"""根据用户类型计算折扣"""
if user_type == "vip":
return price * 0.8
return price
class PaymentHandler:
"""支付处理器"""
def process(self, order_id):
...
chunk1 = "def calculate_discount(...)" # 函数级
chunk2 = "class PaymentHandler" # 类级
适用场景:代码仓库的 RAG 问答。 优势:每个 chunk 保留了一个逻辑单元(一个函数/类),检索命中后 LLM 可以完整理解这个单元的实现。
策略4:父子切割(Parent-Child Chunking)【生产级推荐】
RAG 系统里有一个经典矛盾:
- chunk 越小,检索匹配越精准(粒度细)
- chunk 越大,LLM 阅读时上下文越完整(信息多)
父子切割的解决思路:
- 用小块做检索(精准匹配)
- 匹配命中后,返回这个 chunk 对应的大块(完整上下文)
父 chunk(大块):
第3章 Python 数据类型
3.1 列表操作:列表是 Python 中最常用的数据类型之一...
[包含完整的章节内容]
子 chunk(小块):
列表 append 方法使用说明
append(x) 在列表末尾添加元素...
检索 "append 怎么用" → 命中子 chunk → 返回父 chunk 到 LLM
适用场景:几乎所有生产级 RAG 都在用或应该用这个策略。 实现要点:子 chunk 需要存储指向父 chunk 的引用 ID;检索时先查子 chunk,命中后取回对应的父 chunk 全文。
三、Chunk 的 Metadata 设计
每个 chunk 存储时,除了 text 和 vector,还需要丰富的 metadata:
{
"text": "列表 append 方法使用说明...",
"vector": [0.1, 0.02, ...],
"metadata": {
"source_file": "python_guide.md",
"chapter": "3.1",
"tags": ["data_structure", "list"],
"doc_type": "api_reference",
"page_number": 42,
"created_at": "2026-01-15",
"parent_id": "chunk_3_1_parent" // 父子切割时使用
}
}
Metadata 的作用:
- 过滤:只检索某个文件类型或某个分类,缩小搜索范围
- 溯源:LLM 生成的答案可以标注来源出处
- 去重:避免同一来源的 chunk 全部进答案
- 父子关联:维系父子切割的关系映射
四、切分粒度的黄金法则
没有绝对的「最优粒度」,取决于你的检索目标:
| 检索目标 | 推荐粒度 | 原因 |
|---|---|---|
| 回答事实性问题 | 句子/段落级 | 需要精确匹配术语 |
| 回答解释性问题 | 段落/章节级 | 需要完整上下文才能理解 |
| 代码问答 | 函数/类级 | 保留逻辑单元,问答精准到函数 |
| 支持多跳推理 | 父子结构 | 小块检索精准,大块总结完整 |
经验法则:先按合理的中等粒度(500-1000 token)做基线,然后通过检索日志分析「有没有该命中没命中」「命中的是否相关」,再逐步调优。不要盲目追求小粒度——检索精度提升了,但 LLM 理解难度可能增加了。
五、常见 bad case 及解决
| 问题 | 表现 | 解决 |
|---|---|---|
| 语义被切断 | 切在句子中间,LLM 收到后理解出错 | 以语义边界为切分优先 |
| 信息密度不均匀 | 有些 chunk 太空泛,有些太密集 | 混合策略,或过滤短 chunk |
| 多语言混合 | 中英混杂切分点不准确 | 按 tokenizer 的 token 计数而非字符数 |
| 过细噪音多 | 切太小导致检索到很多无关碎片 | 加最低长度阈值或使用父子结构 |
| 过粗精度低 | 切太大细节信息被「平均掉」 | 减小 chunk 大小或加摘要 |
PDF 文档的 chunking 特别难:解析出来的文本往往丧失段落结构,表格和代码块混在一起。对于这类文档,通常需要先做一次结构解析(用 PDF 解析器提取段落、表格、列表信息),然后再做语义 chunking。
六、本章小结
- Chunking 是 RAG 系统里最关键也最被低估的工程决策
- 四大策略:固定大小(基线)、语义边界(结构化文档)、代码逻辑(代码库)、父子结构(兼顾精度+完整上下文)
- Metadata 设计能让你在检索时做精准过滤、溯源和去重
- 没有统一的「最好粒度」,先跑基线、有数据再优化,比一把梭效果更稳定
- 生产级推荐:父子切割(小块检索 + 大块返回),解决精度与完整性的天然矛盾
思考题
- 父子切割虽然好用,但实现上需要做父-子映射关系管理。你怎么设计这个映射系统,让它既高效又容错?
- 对于 PDF 解析出来的文本(排版丢失、表格混乱),你有什么 chunking 策略?