文档切分(chunking)是 RAG 知识库构建中直接影响检索质量的关键步骤。切得太粗,检索到的片段包含过多无关信息,干扰模型判断;切得太细,又可能丢失必要的上下文,导致模型“断章取义”。递归分块和父子分块是两种在实践中被广泛验证有效的切分策略,它们分别从不同角度平衡了上下文完整性与检索精准度。
递归分块:用分隔符层级保持语义完整
递归分块的核心思路是:按照文档自身的结构层级(如段落、句子、短语),由粗到细逐步切分,尽量让每个 chunk 在长度限制内保持语义的完整性。
很多文档天然存在层级分隔符,例如:
\n\n(空行)通常表示段落边界;\n(换行)可能表示句子或条目;。!?表示句子结束;- 空格(对英文)表示单词边界。
递归分块会先尝试用最高层级的分隔符(如双换行)切分文本。如果切出的片段仍然超出预设的最大长度,就“降级”使用下一级分隔符(如单换行)继续切分,以此类推,直到每个片段都在长度限制以内。
一个真实的处理例子:设定 chunk 最大长度为 500 字符,先用 \n\n 将一篇政策文档切成若干段落。某个段落长 800 字符,超出限制,递归分块器会只对这一段落用 \n 再次切分,得到若干句子,每个句子长度均小于 500,切分完成。这样我们不会把一个段落强行拦腰截断,尽可能让每个 chunk 是一个完整的小语义单元。
实用要点:
- 对于中文,分隔符可以设置为
\n\n、\n、。、;等,语言本身的特点决定句子切分通常用标点。 - 该策略能显著减少因切分导致的语义割裂,例如“根据第三条……”和后半句“可以申请补偿”被分在不同的 chunk 中,检索时只命中一半,造成理解困难。
- 许多成熟的文本分割库(如 LangChain 的
RecursiveCharacterTextSplitter)原生支持这种递归逻辑,只需配置好分隔符列表和 chunk 大小即可。
父子分块:保留上下文的同时精准检索
父子分块解决的是另一个典型矛盾:检索时希望片段小而精准,但生成时需要足够多的上下文来理解片段含义。
比如一份技术手册中有一句“该接口的超时时间默认为 30 秒”,这句话本身很精准,但如果没有周围的章节标题、前提说明,检索到的“孤零零一句话”可能不够——模型可能不清楚这个“接口”指的是什么,回答就容易出错。
父子分块的策略是:
- 父块:把文档按照较大的粒度切分(例如每个完整章节、或者包含较大上下文的段落),这些父块携带了丰富的背景信息。
- 子块:在父块内部进一步切分出较小的片段(例如具体的一句话、一个段落),这些子块用于做向量索引和相似度检索。
- 检索与召回逻辑:用户查询时,系统通过向量相似度找到最相关的子块,但在返回给大模型时,不仅返回该子块本身,还会把包含这个子块的父块一并(或选择性地)附上,作为更完整的上下文。
这样,系统既能利用小段落的精准检索优势,又确保生成模型拿到的信息有足够的背景,不至于误解。
实际应用中的变体:
- 合并相邻块:在检索到子块后,简单地将它与文档中前后相邻的一两个子块拼接起来一起提供给模型,这是一种轻量级的上下文增强做法,可视为父子分块的简化版。
- 元数据关联:在向量数据库中,子块的元数据里存储其所属父块的 ID 或文本,检索时通过元数据快速取得父块内容。
实用建议:
- 父块大小可以设定为 1000~2000 字符,作为章节或大段落;子块大小 200~500 字符,确保检索相关性。
- 对于天然有标题结构的文档,可以将“标题+正文段落”作为一个父块,子块则是正文中的每个小段,这样上下文始终能自带章节主题。
- 注意控制最终输入给模型的 token 总量,避免因父子块同时拼接导致超过模型上下文窗口。可以只附加父块中与子块最相邻的若干句子,而非整个父块。
递归分块和父子分块常常结合使用:先用递归分块生成结构良好的基本单元,再根据需求构建父子层级关系。通过这样的分块设计,RAG 系统能够在检索粒度和上下文完整性之间找到切实可用的平衡点。