RAG 文本分块(Chunking)策略详解
所属阶段:第四阶段 · RAG 深入知识
主题:Chunking 策略、chunkSize / overlap 设置、父子分块(Parent-Child Chunking)
一、为什么需要分块(Chunking)?
在 RAG(Retrieval Augmented Generation)系统中,文档通常不能整篇直接送入大语言模型(LLM),原因有四个:
- 上下文窗口限制:LLM 有固定的最大输入长度(如 8K、32K、128K tokens),整篇长文档超出限制。
- 检索粒度问题:用户的问题是局部性的(如"第 3 章讲了什么"),整篇文档的向量表示会被大量无关内容稀释,导致向量相似度计算不精准。
- 语义噪声:文档越长,单个向量里包含的语义越混杂,检索时"语义模糊",命中率下降。
- 回答质量:送入过多无关上下文会干扰 LLM,产生幻觉或答非所问。
分块的目的:把长文档切成若干语义完整、大小适中的小片段(chunk),每个 chunk 单独向量化(embedding)并存入向量数据库。检索时只找最相关的几个 chunk,作为上下文送给 LLM。
一句话总结:Chunking 是 RAG 检索质量的上限——分块不合理,后面的 Embedding、Rerank、生成再好也救不回来。
二、常见的分块策略有哪些?
按"如何切"分类,主要有以下几类策略:
1. 固定长度分块(Fixed-size Chunking)
最基础、最常用的策略。
- 做法:按固定的 token 数或字符数切分文本,不关心语义边界。
- 变体:固定字符数 / 固定 token 数 / 固定词数。
- 优点:简单、速度快、开销小,能精确控制 chunk 大小。
- 缺点:容易在句子中间硬切,破坏语义完整性。
- 适用场景:代码、日志、结构化数据等语义边界不明显的文本;快速原型验证。
2. 递归字符分块(Recursive Character Text Splitter)
LangChain 等框架的默认方案,也是目前实践中最常用、性价比最高的一种。
- 做法:用一个分隔符列表
["\n\n", "\n", " ", ""]递归切分——先按段落切,段落太长再按句子切,句子还太长再按空格/字符切,直到每块大小满足要求。 - 优点:在尽量保留语义结构(段落/句子)的同时,强制保证 chunk 大小可控。
- 适用场景:绝大多数通用文档(网页、文章、Markdown、报告)。
3. 基于文档结构的分块(Document-based / Structure-aware Chunking)
按照文档本身的语义单元切分。
- 按段落切分:以空行、换行为界。
- 按标题层级切分:根据 Markdown / HTML 的标题(H1、H2、H3…)划分章节,再对每个章节分块,并把标题信息带上(常用于 Markdown 文档)。
- 按代码结构切分:如
LanguageCodeSplitter按函数、类、导入块切分 Python / JS 代码。 - 优点:chunk 语义最完整,贴合文档逻辑。
- 缺点:需要知道文档结构,不同文档类型要写不同的解析器。
4. 语义分块(Semantic Chunking)
利用 Embedding 模型判断句子的语义突变点,在"话题变化"的地方切分。
- 做法:把文本切成句子,逐句计算 embedding 相似度;当相邻句子相似度骤降(断崖)时,认为话题切换,在此处分块。
- 优点:chunk 语义高度内聚,检索质量好。
- 缺点:慢(需要逐句 embedding)、成本高;阈值需要调。
- 适用场景:对检索精度要求高、文本语义复杂的长文。
5. 滑动窗口分块(Sliding Window Chunking)
分块时让相邻块之间有重叠,保证边界处的信息不丢失。
- 做法:本质是固定大小 +
overlap(重叠 token 数)。 - 优点:避免一个完整的句子/概念恰好被切在边界上而丢失语义。
- 适用场景:几乎所有的固定/递归分块都会搭配滑动窗口使用。
6. 父子分块(Parent-Child Chunking / 分层分块)
进阶策略,核心思想是"用小子块检索,用大块/父块生成"。详见本文第四部分。
7. Agentic / 智能分块(LLM-based Chunking)
用 LLM 判断哪里是语义边界来切分。
- 做法:提示 LLM 识别文档的语义单元(章节、主题段),按其切分。
- 优点:语义边界最准确。
- 缺点:慢、贵、不稳定。
- 适用场景:高质量内容库、离线预处理。
策略对比表
| 策略 | 语义保真 | 速度/成本 | 实现复杂度 | 典型场景 |
|---|---|---|---|---|
| 固定长度 | 低 | 高/低 | 低 | 原型、代码 |
| 递归字符 | 中高 | 高/低 | 低 | 通用文档(默认) |
| 结构感知 | 高 | 中/中 | 中 | Markdown/HTML/代码 |
| 语义分块 | 高 | 低/高 | 中高 | 复杂长文 |
| 滑动窗口 | 中(配合用) | 高/低 | 低 | 通用 |
| 父子分块 | 高 | 中/中高 | 中 | 需要精确检索+完整上下文 |
| LLM 智能分块 | 最高 | 低/高 | 高 | 高质量离线库 |
三、chunkSize 和 overlap 如何设置?
3.1 概念定义
- chunkSize:每个 chunk 的目标大小,单位通常是 token 数(少数按字符/词)。
- overlap:相邻两个 chunk 之间重叠的 token 数,用于保证切点附近的语义不丢失。
举例:文本 0~1000 tokens,chunkSize=400, overlap=100:
- chunk1 = [0, 400)
- chunk2 = [300, 700) ← 与 chunk1 重叠 100 tokens(300~400)
- chunk3 = [600, 1000)
3.2 为什么需要 overlap?
- 一个完整句子或概念可能恰好被切在边界上,导致该 chunk 语义不完整。
- 用户问题引用的内容可能在 chunk 交界处,overlap 保证这段内容在至少一个 chunk 里完整出现。
- 检索时,目标内容能同时在相邻 chunk 中被命中,提高召回率。
3.3 chunkSize 怎么选?
核心权衡:越大越完整但越模糊,越小越精准但越碎片化。
| chunkSize | 优点 | 缺点 | 适合 |
|---|---|---|---|
| 小(100~200 tokens) | 检索精准、向量语义聚焦、成本低 | 上下文信息不足,容易碎片化 | 问答型、FAQ、短句密集 |
| 中(300~500 tokens) | 平衡点,语义较完整 | 中庸 | 绝大多数通用场景(推荐起点 400~500) |
| 大(800~2000 tokens) | 上下文完整,LLM 理解充分 | 向量被稀释、检索变糊、占用上下文窗口 | 长文档总结、需要大段上下文 |
设置原则:
- 以模型上下文窗口为约束:
chunkSize + 检索返回的块数 × (重叠冗余)必须远小于 LLM 上下文上限,否则放入几个 chunk 就超限。 - 以文档类型为准:段落规整的文档适合 300~500;代码函数块按函数切;问答/FAQ 常按"一问一答"成块。
- 以检索实验为准:没有绝对标准,用一组代表性问题做召回评估,调参看命中率。
3.4 overlap 怎么选?
- 经验法则:通常为
chunkSize的 10%~25%。chunkSize=400 → overlap=40~100chunkSize=1000 → overlap=100~250
- 调大(如 30%~50%):边界信息重要、文档语义连续性强的场景(小说、论文)。
- 调小(5% 以内):追求存储/检索效率、去重能力,避免冗余。
- 注意:
- overlap 越大,向量库存储量和检索冗余越大。
- overlap 不是越大越好,过大反而引入大量重复向量,降低检索区分度。
- 若使用父子分块,overlap 的重要性会下降(父块天然提供完整上下文)。
3.5 一些经验参数(起点)
| 文档类型 | chunkSize | overlap |
|---|---|---|
| 网页/文章 | 400~500 | 50~80 |
| Markdown 章节 | 500 | 80~100 |
| 学术论文 | 1000~1500 | 150~300 |
| FAQ / 问答对 | 一问一答成块 | 0~20 |
| 代码 | 按函数/类 | 0(跨函数重叠无意义) |
四、父子分块(Parent-Child Chunking)是什么?
4.1 核心思想
"用小的子块(child chunk)做向量检索,用大的父块(parent chunk)做 LLM 上下文。"
它解决了 RAG 一个经典矛盾:
- 小块 → 向量语义聚焦、检索精准,但上下文太短、信息不全;
- 大块 → 上下文完整,但向量被稀释、检索不准。
父子分块把两者结合:检索阶段用"精准的子弹"(子块),生成阶段用"完整的粮草"(父块)。
4.2 具体做法
- 划分父块(Parent):把文档切成较大的语义块,如 1500~2000 tokens,按章节/段落划分,语义完整。
- 拆分子块(Child):把每个父块再切成更小的块,如 200~300 tokens(子块之间可以有 overlap)。
- 建立映射:每个子块记录其父块的 ID 或直接内嵌父块文本。
- 检索流程:
- 用户提问 → 对问题 embedding → 在子块索引里检索 Top-K 最相似的子块;
- 找到命中的子块 → 回溯其父块 → 把父块(完整上下文)作为 LLM 的上下文输入。
4.3 图解
文档
└── Parent 1(1500 tokens,完整章节)
│ ├── Child 1a(300 tokens)──→ 向量入库
│ ├── Child 1b(300 tokens)──→ 向量入库
│ └── Child 1c(300 tokens)──→ 向量入库
└── Parent 2(1800 tokens)
├── Child 2a ...
└── ...
检索:query → 向量匹配 Child 1b(命中)
生成:把 Child 1b 的 Parent 1 整段送进 LLM
4.4 优点
- 检索精度高:子块向量语义集中,避免大块语义稀释。
- 上下文完整:LLM 拿到的是大块,理解充分、回答更全面。
- 能回答问题边界的内容:命中子块后自动带上整段父块,不怕信息恰在子块边界。
- 比单纯调大 chunkSize 更有效:单纯调大 chunk 会让向量变糊,父子分块"鱼与熊掌兼得"。
4.5 缺点 / 注意事项
- 实现复杂度增加:需要维护父子映射关系(数据库需存 parent_id,或用两个集合)。
- 存储冗余:父块文本可能被重复存储或需二次读取。
- 检索延迟:多一步"子块命中 → 回溯父块"。
- 父块不能无限大:父块也要受 LLM 上下文窗口约束(一个父块 + 其他上下文要能放得进)。
4.6 变体:Hierarchical Index(分层索引)
有些实现还会做第 3 层:文档 → 章节(中层)→ 句子(小块)。检索时先命中小块,再向上回溯到中层(如章节标题),把"标题 + 相关内容"给 LLM,帮助 LLM 理解所在章节语境。
4.7 落地方式(以主流框架为例)
- LangChain:
ParentDocumentRetriever(官方内置,天然支持父子分块)。 - LlamaIndex:
HierarchicalNodeParser+AutoMergingRetriever(检索后自动把相邻小节点合并成父节点)。 - 自建:建两个集合——
child_embeddings(子块向量 + parent_id)和parent_texts(父块文本),检索子块后按 parent_id 查父块。
五、如何选择合适的分块策略?(决策流程)
1. 文本是否结构规整(标题/段落/代码)?
├─ 是 → 优先用结构感知分块(Markdown/HTML/代码 splitter)
└─ 否 → 用递归字符分块(默认)
2. 是否需要极高检索精度 + 完整上下文?
├─ 是 → 采用父子分块(Parent-Child)
└─ 否 → 普通 chunkSize + overlap 即可
3. 是否有预算/时间做慢处理?
├─ 有 → 可上语义分块 / LLM 智能分块提升质量
└─ 无 → 递归 + 滑动窗口即可
4. 无论哪种,都要:
用真实问题做召回评估 → 调 chunkSize / overlap → 对比回答质量
六、实践建议与经验总结
- 先用递归字符分块起步(
chunkSize=400~500, overlap=50~100),快速验证整体 RAG 效果。 - 文档有结构就用结构感知分块,能明显提升质量。
- 检索不准时优先怀疑分块,而不是先换 Embedding 模型。
- 要求精确检索 + 完整上下文时,直接上父子分块,这是目前工程上性价比最高的进阶方案。
- overlap 设置为 chunkSize 的 10%~25% 作为默认。
- 所有参数必须实验验证:构造 20~50 个真实问题,统计"正确答案所在 chunk 是否被检索到"(Recall@K),用指标说话。
- 分块之后,还应配合 Hybrid Search(BM25 + 向量) 和 Rerank(重排) 进一步优化检索质量。
评论