RAG 文本分块(Chunking)策略详解

所属阶段:第四阶段 · RAG 深入知识
主题:Chunking 策略、chunkSize / overlap 设置、父子分块(Parent-Child Chunking)


一、为什么需要分块(Chunking)?

在 RAG(Retrieval Augmented Generation)系统中,文档通常不能整篇直接送入大语言模型(LLM),原因有四个:

  1. 上下文窗口限制:LLM 有固定的最大输入长度(如 8K、32K、128K tokens),整篇长文档超出限制。
  2. 检索粒度问题:用户的问题是局部性的(如"第 3 章讲了什么"),整篇文档的向量表示会被大量无关内容稀释,导致向量相似度计算不精准。
  3. 语义噪声:文档越长,单个向量里包含的语义越混杂,检索时"语义模糊",命中率下降。
  4. 回答质量:送入过多无关上下文会干扰 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 理解充分 向量被稀释、检索变糊、占用上下文窗口 长文档总结、需要大段上下文

设置原则

  1. 以模型上下文窗口为约束chunkSize + 检索返回的块数 × (重叠冗余) 必须远小于 LLM 上下文上限,否则放入几个 chunk 就超限。
  2. 以文档类型为准:段落规整的文档适合 300~500;代码函数块按函数切;问答/FAQ 常按"一问一答"成块。
  3. 以检索实验为准:没有绝对标准,用一组代表性问题做召回评估,调参看命中率。

3.4 overlap 怎么选?

  • 经验法则:通常为 chunkSize10%~25%
    • chunkSize=400 → overlap=40~100
    • chunkSize=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 具体做法

  1. 划分父块(Parent):把文档切成较大的语义块,如 1500~2000 tokens,按章节/段落划分,语义完整。
  2. 拆分子块(Child):把每个父块再切成更小的块,如 200~300 tokens(子块之间可以有 overlap)。
  3. 建立映射:每个子块记录其父块的 ID 或直接内嵌父块文本。
  4. 检索流程
    • 用户提问 → 对问题 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 落地方式(以主流框架为例)

  • LangChainParentDocumentRetriever(官方内置,天然支持父子分块)。
  • LlamaIndexHierarchicalNodeParser + AutoMergingRetriever(检索后自动把相邻小节点合并成父节点)。
  • 自建:建两个集合——child_embeddings(子块向量 + parent_id)和 parent_texts(父块文本),检索子块后按 parent_id 查父块。

五、如何选择合适的分块策略?(决策流程)

1. 文本是否结构规整(标题/段落/代码)?
   ├─ 是 → 优先用结构感知分块(Markdown/HTML/代码 splitter)
   └─ 否 → 用递归字符分块(默认)
2. 是否需要极高检索精度 + 完整上下文?
   ├─ 是 → 采用父子分块(Parent-Child)
   └─ 否 → 普通 chunkSize + overlap 即可
3. 是否有预算/时间做慢处理?
   ├─ 有 → 可上语义分块 / LLM 智能分块提升质量
   └─ 无 → 递归 + 滑动窗口即可
4. 无论哪种,都要:
   用真实问题做召回评估 → 调 chunkSize / overlap → 对比回答质量

六、实践建议与经验总结

  1. 先用递归字符分块起步chunkSize=400~500, overlap=50~100),快速验证整体 RAG 效果。
  2. 文档有结构就用结构感知分块,能明显提升质量。
  3. 检索不准时优先怀疑分块,而不是先换 Embedding 模型。
  4. 要求精确检索 + 完整上下文时,直接上父子分块,这是目前工程上性价比最高的进阶方案。
  5. overlap 设置为 chunkSize 的 10%~25% 作为默认。
  6. 所有参数必须实验验证:构造 20~50 个真实问题,统计"正确答案所在 chunk 是否被检索到"(Recall@K),用指标说话。
  7. 分块之后,还应配合 Hybrid Search(BM25 + 向量)Rerank(重排) 进一步优化检索质量。