文档切分策略

Chunking 是 RAG 的基础:切太大引入噪声,切太小丢失上下文。本文详解各类切分策略与最佳实践

13 min read Part of RAG · Ch. 3
← 上一层级:学习路径 · Part 02 · RAG 体系化进阶

文档切分策略

RAG 的第一步,通常不是“检索”,而是先把长文档切成一批可检索的小块(chunks),再对每个块做 Embedding 存入索引。切分策略会直接影响后面的检索质量:切太大,无关内容混进来;切太小,语义被割裂、上下文丢失。LangChain 官方文档也把 text splitting 定义为:把大文档拆成既能单独被检索、又能放进模型上下文窗口的小块。(docs.langchain.com)

延伸阅读

  • LangChain 的 Text Splitters 文档把 RecursiveCharacterTextSplitter 作为通用文本的推荐起点。(docs.langchain.com)
  • LlamaIndex 的 Ingestion / Node Parser 文档长期强调:切分要尽量保留段落和句子边界。(LlamaIndex OSS Documentation)
  • 一些近年的研究也表明,基于结构或语义的切分,常常比纯固定长度切分更利于检索质量。(arXiv)

为什么 Chunking 重要

flowchart LR
  A["文档切分策略"]
  A --> B["分类:检索增强 (RAG)"]
  A --> C["关键词:RAG"]
  A --> D["关键词:Chunking"]
  A --> E["关键词:文档切分"]
  A --> F["关键词:Embedding"]

先看定义:Chunk 是检索的粒度,也是注入 LLM 的上下文单元。切分策略不是一个预处理小细节,而是整个 RAG 系统的地基。

很多人刚做 RAG 时,会把主要精力放在:

  • 用哪个 Embedding 模型
  • 用哪个向量数据库
  • top-k 取多少
  • 要不要 rerank

但在真实系统里,一个很常见的现象是:

切分不好,后面全链路都在“补救”。

因为 RAG 里的大多数组件,其实都默认你已经把知识拆成了“合理的检索单元”。如果 chunk 本身就不合理,那么:

  • 再好的 embedding 也只能表示一个糟糕的片段
  • 再强的检索也只能召回糟糕的片段
  • 再贵的 LLM 也只能基于糟糕的片段作答

所以 chunking 的本质不是“把长文切短”,而是:

把文档改造成适合检索和生成协作的知识单元。

切太大和切太小分别会怎样

维度切太大切太小
检索容易匹配到一大块里其实不相关的部分语义碎片化,召回不稳定
生成噪声多,模型难以抓住重点上下文不足,模型难以推理和引用
成本每个 chunk 更重,注入上下文更贵chunk 数暴增,索引和检索成本上升
引用一次引用范围太大,不够精确一次引用太碎,不利于阅读和追溯

目标不是“尽量大”或“尽量小”,而是在语义完整检索粒度之间找到平衡。


一个先建立起来的核心原则

在具体策略之前,先记住三个非常实用的原则。

原则 1:切分首先服务检索,而不是服务阅读

人读文档时喜欢看完整页面、完整章节。 但检索系统不一定需要这么大的单位。

一个好的 chunk,不一定是“最适合人从头到尾读”的,而是:

  • 足够自洽
  • 足够可匹配
  • 足够短,便于进入 prompt
  • 足够长,不会失去关键前后文

原则 2:尽量沿自然边界切,切不动再硬切

这是 LangChain RecursiveCharacterTextSplitter 背后的核心思想:先按段落、再按行、再按空格、最后才按字符硬切。LangChain 文档明确说明,这个 splitter 会按给定分隔符列表依次尝试切分,默认分隔符就是 ["\n\n", "\n", " ", ""]。(docs.langchain.com)

原则 3:没有通用最优 chunk size,只有“对你的文档和 query 更合适”的 size

这也是为什么 chunking 一定要结合评估,而不是只靠经验值。研究和实践都表明,不同文档结构、不同任务目标、不同检索方式,对粒度的敏感度都很高。(arXiv)


1. 固定大小切分(Fixed-size Chunking)

先看定义:按固定字符数或 token 数切分,是最简单、最容易实现的基线方案。

最朴素的做法,就是给文档设定一个固定长度,例如:

  • 每块 500 字符
  • 或每块 256 tokens
  • 或每块 512 tokens

然后机械切分。

# 伪代码
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]

优点

  • 实现简单
  • 行为可预测
  • 容易做 baseline
  • 适合快速原型

缺点

  • 可能在句子中间切断
  • 可能把同一段的上下文拆开
  • 对结构化文档不友好
  • 对代码、表格、标题层级等内容破坏较大

LangChain 的 character splitter 文档也明确说明,字符切分是最简单的方法,但它本质上只是按字符分隔符和长度来拆,不保证语义边界。(docs.langchain.com)

适用场景

固定大小切分适合:

  • 非结构化纯文本
  • 快速验证 RAG 原型
  • 暂时没有更好的结构信息
  • 需要先建立一个可对照的 baseline

它不一定是最优方案,但经常是最好的起点。


2. 滑动窗口(Sliding Window)

先看定义:固定大小切分的最大问题是边界断裂,滑动窗口通过 overlap 来缓解这个问题。

滑动窗口本质上还是固定大小切分,只是相邻 chunk 之间保留一定重叠。例如:

  • chunk_size = 500
  • overlap = 50

那么前一个 chunk 的结尾 50 个 token,会出现在下一个 chunk 的开头。

为什么 overlap 有用

因为很多信息恰好卡在边界上。 如果没有 overlap,就可能发生这种情况:

  • 问题关键词在 chunk A 末尾
  • 真正答案在 chunk B 开头

这样单个 chunk 看都不完整,检索和生成都容易失真。

Overlap 的作用,就是给边界加一点缓冲,让跨边界信息更容易被保留下来。

优点

  • 实现仍然简单
  • 对边界断裂有明显缓解
  • 常常比纯 fixed-size 稳

缺点

  • 数据冗余增加
  • 索引体积更大
  • 检索时可能召回很多高度相似块
  • 后处理时更容易出现重复内容

overlap 设多大合适

没有绝对标准,但一个非常常见的经验区间是:

  • overlap 取 chunk_size 的 10%–20%

它通常足够提供边界连续性,又不至于冗余太夸张。

先看定义:Overlap 是保险,不是越多越好。


3. 语义切分(Semantic Chunking)

先看定义:与其按长度硬切,不如尽量按段落、标题、语义单元来切。

固定大小切分的最大问题,是它不关心内容结构。 语义切分则反过来:优先尊重内容本来的组织方式。

常见做法包括:

  • 按段落切
  • 按标题层级切
  • 按句子切,再合并成语义完整块
  • 按文档元素类型切(例如标题、正文、表格、列表、代码块)

为什么语义切分通常更优

因为一个好的 chunk,最重要的是:

本身就能独立表达一个相对完整的意思。

如果一个 chunk 刚好就是:

  • 一个定义
  • 一个 FAQ 问答
  • 一个政策条款
  • 一个函数说明
  • 一个小节的核心内容

那么它就更容易:

  • 被正确检索
  • 被准确引用
  • 被模型正确理解

一些针对金融报告和复杂文档的研究也发现,基于元素类型或更结构化的切分方式,往往能带来更好的检索和回答效果。(arXiv)

缺点

  • 实现更复杂
  • 依赖文档解析质量
  • 对格式混乱的原始资料不一定稳定
  • 很难有一个统一策略覆盖所有文档类型

所以语义切分通常更强,但也更“工程化”。


4. Recursive Splitting:为什么它成了通用默认选项

先看定义:Recursive splitting 的思路不是追求“最聪明”,而是追求“在简单和效果之间取得最好平衡”。

LangChain 官方文档把 RecursiveCharacterTextSplitter 作为 generic text 的推荐起点。它会优先尝试较自然的边界来切:

  1. 先按段落 \n\n
  2. 再按行 \n
  3. 再按空格
  4. 最后才按字符硬切

LangChain 官方文档明确写了这一点。(docs.langchain.com)

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", " ", ""]
)

chunks = splitter.split_text(document)

为什么它经常是个好起点

因为它同时满足几件事:

  • 比纯 fixed-size 更尊重自然边界
  • 比复杂语义分块更容易落地
  • 可调参数少,原型阶段很好用
  • 文档结构一般时也能工作

所以如果你没有特别强的文档结构信息,或者想先做一个比较稳的 baseline,recursive splitting 往往是很合理的默认方案。


5. 按文档类型选择策略,比“统一策略打天下”更重要

先看定义:不同文档的天然结构差异很大,chunking 最有效的优化之一,不是微调参数,而是按文档类型分策略。

LlamaIndex 的 node parser 文档就明确展示了按 HTML、JSON、Sentence Window 等不同内容类型选择不同 parser 的思路。(LlamaIndex OSS Documentation)

常见文档类型与推荐思路

文档类型推荐策略原因
PDF按页 + 按段落 / 元素切分页码便于溯源,段落便于检索
HTML按 DOM 结构切分sectionp、标题标签本身就有语义
Markdown按标题层级切分###### 是天然边界
代码按函数、类、模块切分保持代码块完整比长度一致更重要
对话 / 日志按轮次或时间窗口切分保持上下轮关联
FAQ一问一答为最小单元每组 QA 都是天然 chunk

一个很实用的经验

不要追求“一个 splitter 处理所有文档”。 真实系统里,更常见也更有效的做法是:

  • Markdown 一套
  • HTML 一套
  • 代码一套
  • PDF / Office 文档一套
  • 聊天记录 / 工单一套

这会比试图用一个统一 chunk_size 更靠谱。


6. Chunk Size 到底该设多大

先看定义:大多数通用场景的有效区间,通常落在几百 token 到一千 token 左右,但真正的最佳值仍然要靠你的文档和 query 评估出来。

LangChain 的 token splitting 文档明确建议:切分时最好按 token 计数,并使用与目标模型一致的 tokenizer,因为模型上下文限制是按 token 而不是按字符算的。(docs.langchain.com)

一个实用的经验区间

场景常见起步范围说明
通用问答256–512 tokens平衡召回与噪声
长文档知识问答512–1024 tokens需要更多上下文
代码检索200–400 tokens更接近函数级或逻辑块级粒度
高结构文档不只看长度,更看边界标题和段落比固定长度更重要

为什么不要迷信“大一点上下文更全”

因为 chunk 越大,越容易把无关内容一起嵌进去。 这会带来两个后果:

  1. 检索命中一个主题时,同时带进很多无关段落
  2. LLM 读到的参考资料更杂,回答容易被干扰

所以 chunk size 不是越大越好,而是:

只大到足以保留必要语义,不大到明显引入噪声。


7. Token 和字符:为什么不要只按字符拍脑袋

很多人刚做 chunking,会直接按字符数切分。 这在原型阶段未尝不可,但正式系统里最好还是按 token 设计和校验。

因为:

  • 模型上下文限制按 token 算
  • embedding 模型输入限制也按 token 算
  • 不同语言 token 密度差异明显
  • 字符数相同,不代表 token 数相同

LangChain 官方文档也明确提醒:如果按 token 切分,应使用与模型一致的 tokenizer 来计数。(docs.langchain.com)

一个很实用的结论

按字符切可以作为粗略近似,但上线前最好用 tokenizer 验证真实 token 长度。

尤其在:

  • 中文
  • 混合中英文本
  • 代码
  • 表格型文本

这件事会更重要。


8. Metadata Enrichment:chunk 不只是一段文本

先看定义:一个好的 chunk,除了内容本身,还应该带足够的 metadata,用于过滤、引用和审计。

很多初学者只存:

  • chunk_text
  • embedding_vector

但生产里通常还会一起存:

元数据常见用途
source来源文档、URL、文件名
page_number页码引用
section_title所属章节
doc_type文档类型
created_at / updated_at新旧版本控制
tenant_id多租户隔离
language多语言过滤

这些 metadata 的价值至少体现在三方面:

1. 检索过滤

例如只搜某类文档、某个时间范围、某个租户。

2. 引用展示

回答时能显示“来自第几页、第几节”。

3. 审计溯源

知道模型到底参考了什么材料。

所以从系统角度看,chunk 不是一段裸文本,而是一个知识单元对象


9. Parent-Child Chunking:为什么很多高质量 RAG 不只存一层粒度

先看定义:Parent-Child 的核心思想是“用小块匹配,用大块生成”,兼顾检索精准度和上下文完整性。

一个很典型的问题是:

  • 小块检索更精准
  • 大块生成更好理解

Parent-Child Chunking 就是在解决这个矛盾。

基本思路

  • 存储时,把文档切成更小的 child chunks
  • 检索时,用 child chunks 做更精确匹配
  • 生成时,不一定只把 child chunk 给模型,而是返回它所属的 parent chunk

例如:

层级粒度
Child200 tokens
Parent800 tokens

查询时命中 child,但送给模型时给 parent,或者给 parent 的裁剪版本。

为什么这有价值

因为很多问题需要的不是“命中一个关键词的小片段”,而是:

  • 该片段前后的上下文
  • 同一条规则的完整表述
  • 同一函数或同一段说明的整体语义

所以 Parent-Child 很适合那些对“检索准确”和“生成有上下文”都要求高的场景。

代价

  • 实现更复杂
  • 需要维护 chunk 之间的父子关系
  • 存储和召回逻辑更重

但在复杂知识库场景里,这类多粒度策略常常非常值得。


10. Sentence Window、Late Chunking、Contextual Retrieval:更高级的方向

除了常见的 fixed-size、recursive、semantic、parent-child,近两年还出现了一些更高级的变体。

Sentence Window

LlamaIndex 的 SentenceWindowNodeParser 就是一个典型例子: 它把单句作为基本节点,同时给每个节点附带左右句窗口。这样既能用句级粒度做精确匹配,又能保留邻近上下文。(LlamaIndex OSS Documentation)

Late Chunking / Contextual Retrieval

近年的研究开始尝试把“先整段编码、后局部取用”或“让 chunk 带更多全局背景”的方式纳入 RAG 流程。相关研究表明,这类方法有时能比传统早切分更好地保持全局语义,但代价通常更高。(arXiv)

Mix-of-Granularity

还有研究专门讨论“混合粒度”策略,也就是不同粒度 chunk 同时存在,按问题类型选用。相关论文的动机就是:单一粒度很难适配所有知识源和问题类型。(arXiv)

这些方向说明一件事:

Chunking 已经不只是预处理技巧,而是在逐步演化成检索系统本身的重要设计层。


11. 切分后的去重与过滤:别把垃圾一起送进索引

切分完成后,通常还建议做一轮简单后处理:

1. 去掉过短块

例如只有几个字、只有标点、只有页眉页脚。

2. 去掉高度重复块

尤其是 sliding window 或 PDF 解析后,常常会产生非常相似的 chunk。

3. 二次切分超长块

有些结构切分后仍然过长,需要再拆。

4. 过滤无意义内容

例如导航栏、版权声明、页脚说明、目录项。

这些步骤看起来不起眼,但对索引质量非常有帮助。


12. 一条真正实用的选择流程

如果你不想一开始就陷入“无限优化 chunking”的焦虑,可以按下面这条路线来:

第一步:先用一个稳妥 baseline

比如:

  • Recursive splitter
  • chunk_size 500 左右
  • overlap 50 左右

这通常足够让系统先跑起来。LangChain 官方也把 recursive splitter 作为多数场景的起点。(docs.langchain.com)

第二步:拿真实 query 做采样评估

不要只看“切出来像不像样”,要看:

  • 能不能检索到对的片段
  • 检索到的片段是否自洽
  • 最终回答有没有引用对内容

第三步:按文档类型拆策略

如果发现 Markdown 效果很好、PDF 很差,就不要再用统一策略硬扛。

第四步:再决定要不要上更复杂方法

例如:

  • parent-child
  • sentence window
  • hybrid granularity
  • contextual retrieval

先看定义:

先让 chunking 足够好,再决定有没有必要让它变得更复杂。


小结

Chunking 决定了检索粒度,也决定了模型最终能看到什么样的上下文。固定大小切分简单但容易破坏语义;滑动窗口通过 overlap 缓解边界问题;语义切分和 recursive splitting 更尊重自然边界;parent-child 和 sentence window 则是在更高要求场景下兼顾检索精度和上下文完整性。LangChain 官方推荐从 RecursiveCharacterTextSplitter 起步,LlamaIndex 也长期强调按句子、段落和结构保留自然边界。(docs.langchain.com)

最重要的是: Chunking 没有银弹,只有适不适合你的文档、问题和系统目标。 它不是一个纯“预处理参数”,而是整个 RAG 系统质量的基础。

核心概念速查

概念一句话
Fixed-size Chunking按固定字符或 token 数切分,实现简单但可能断句
Sliding Window固定大小 + 块间重叠,减少边界语义丢失
Semantic Chunking按段落、标题、主题等语义边界切分
Recursive Splitting先按自然分隔符切,不够再逐级细化,是通用起点
Parent-Child Chunking用小块命中、用大块生成,兼顾精准与上下文
Sentence Window用句子做细粒度匹配,同时保留邻近句窗口
Metadata每个 chunk 附带来源、页码、章节等结构信息
Chunk Size决定检索粒度和噪声水平,没有通用最优值