Lazy loaded image
Agent丨RAG 中长文档切片的粒度和分块策略如何设计与优化?
Words 7778Read Time 20 min
2026-4-14
type
Post
status
Published
date
Apr 14, 2026
slug
agent8
summary
tags
Agent
category
icon
password
在 RAG 系统里,长文档切片不是一个简单的“每 500 tokens 切一刀”的问题。它本质上是在三个目标之间做权衡:
第一,切片要足够小,方便向量模型表达清楚语义。
第二,切片要足够完整,不能把一个定义、步骤、表格、条款、代码函数切断。
第三,切片要能被检索系统稳定召回,并在进入 LLM 上下文时保留必要的上下文链路。
成熟 RAG 项目基本都已经从“固定长度切片”走向了“结构感知 + 递归切分 + 语义合并 + 父子块检索 + 后处理重排”的组合策略。LangChain 官方文档也建议多数场景从 RecursiveCharacterTextSplitter 开始,因为它在保持上下文完整和控制 chunk 大小之间比较均衡;其实现会优先按段落、换行、空格等层级递归切分。(LangChain 文档)

一、为什么长文档切片会直接决定 RAG 效果?

RAG 的检索对象通常不是整篇文档,而是文档切出来的 chunk。切片过小,会导致上下文残缺;切片过大,会导致 embedding 表达被多个主题稀释,检索命中后也会把大量无关 token 塞进 LLM。Chroma 的 chunking 评测指出,切片策略会显著影响检索表现,某些策略之间 recall 差异可达到 9%;同一报告还强调,过大的 chunk 会稀释相关信息,过小的 chunk 又可能无法捕获必要上下文。(Chroma)
Cohere 的 chunking 指南也给出了类似结论:小块通常更利于精确检索,但可能缺上下文;大块上下文更多,但会降低检索有效性;overlap 能缓解边界切断问题,但会引入冗余。(Cohere Documentation)
因此,一个好的 chunk 至少要满足四个条件:
语义完整:一个知识点、条款、步骤、FAQ、代码函数最好不要被切断。
粒度可控:每个 chunk 不应超过 embedding 模型和 rerank 模型的有效表达范围。
元数据完整:保留文档名、标题路径、页码、表格编号、段落位置、父子关系。
可评测优化:chunk size、overlap、top_k、rerank、父子块策略要通过检索评测调参,而不是拍脑袋。

二、成熟 RAG 项目的切片设计对照

1. LangChain:递归字符切分,优先保持大结构

LangChain 的 RecursiveCharacterTextSplitter 默认 separators 是:
源码逻辑大致是:先找当前文本中能命中的最高优先级 separator,然后切开;如果某个 split 仍然超过 chunk_size,再用下一级 separator 递归处理;小片段再通过 _merge_splits 合并成接近目标长度的 chunk。源码中也提供了按编程语言定制 separator 的能力,例如 Python 会优先按 classdef 等结构切分。(GitHub)
可以把它理解成:
这个实现的关键不是“按字符切”,而是:
先尊重结构边界,再满足长度约束。
能按段落切就不按句子切,能按句子切就不按词切,最后才退化到字符级硬切。
这也是大多数通用知识库的默认推荐方案。

2. LlamaIndex:句子优先、metadata-aware、支持父子关系

LlamaIndex 的 SentenceSplitter 更强调自然语言边界。它的源码里会先检查 chunk_overlap 不能大于 chunk_size,然后按段落分隔符、句子 tokenizer、正则、空格、字符等顺序逐级切分。它还提供 split_text_metadata_aware,会先计算 metadata 占用的 token,再用 effective_chunk_size = chunk_size - metadata_len 得到真正可用于正文的长度。(GitHub)
这点非常重要。很多 RAG 系统会把标题路径、文件名、页码、章节名一起塞进 embedding 文本或 prompt。如果你设置 chunk size 是 512,但 metadata 占了 120 tokens,正文实际只剩 392 tokens。LlamaIndex 这类 metadata-aware 设计能避免 chunk 表面不超长、实际进入模型却超长的问题。
LlamaIndex 还提供 HierarchicalNodeParser。它默认可以把文档切成多层节点,例如 2048、512、128 三层 chunk;叶子节点用于向量检索,父节点保存在 docstore 中,检索时可以通过 Auto Merging Retriever 把多个命中的小块合并回更大的父块。(Developer Documentation)
这对应了 RAG 里很经典的矛盾:
检索时想要小块,因为小块更精确。
生成时想要大块,因为大块上下文更完整。
父子块方案就是为了解决这个矛盾。

3. Haystack:显式 split_by / split_length / split_overlap / split_threshold

Haystack 的 DocumentSplitter 比较工程化。它直接把切分维度暴露为参数:split_by 可以是 wordsentencepassagepagelinefunctionsplit_length 表示每个 chunk 包含多少个单位;split_overlap 表示重叠多少个单位;split_threshold 表示太短的片段会被并回前一个片段。(GitHub)
这说明生产级 RAG 不能只看 chunk_size,还要看“chunk size 的单位是什么”:
单位
适合场景
风险
character
简单、稳定、语言无关
容易切断语义
token
适合控制模型输入
对标题、表格、代码结构不敏感
sentence
FAQ、问答、短知识点
长句、列表、表格不好处理
paragraph / passage
文档、报告、说明书
段落过长时仍需二次切
page
PDF 溯源、页码引用
一页可能包含多个主题
function
代码库 RAG
需要语言解析器或规则
Haystack 还会在 metadata 中加入 source_idpage_number,用于追踪 chunk 来源。(docs.haystack.deepset.ai) 这一点在企业知识库里非常关键,因为回答不仅要“答对”,还要能定位原文。

4. Unstructured:元素级 chunking,表格和标题特殊处理

Unstructured 的 chunking 更偏“文档解析优先”。它不是直接把纯文本切开,而是先把文档解析成 TitleNarrativeTextTable 等 element,然后再按 element 合并。它的 basic 策略会尽量合并连续 element 直到接近 max_characters;超长 element 会单独切分;Table 会被隔离,不和普通文本混在一个 chunk 里。(docs.unstructured.io)
它的 by_title 策略会保留章节边界:遇到新的 Title 就关闭当前 chunk 并开始新 chunk,避免一个 chunk 跨越两个章节;同时可以选择是否尊重 page boundary,并能合并过短的小章节。(docs.unstructured.io)
这给我们的启发是:
PDF、Word、HTML、Markdown 这类长文档,最好先做结构解析,再做切片。
表格、标题、列表、图片说明、代码块不应该和普通段落一视同仁。

5. Dify:工程参数完整,但也暴露了 separator-first 的问题

Dify 的数据模型里有清晰的 SegmentationConfig:包含 delimitermax_tokenschunk_overlap。它的 custom / hierarchical 模式会使用 FixedRecursiveCharacterTextSplitter,automatic 模式会使用 EnhanceRecursiveCharacterTextSplitter,并配置类似 ["\n\n", "。", ". ", " ", ""] 的 separators。(GitHub)
但是 Dify 社区 issue 也暴露了一个典型问题:如果切片逻辑过度依赖 separator,尤其是把单换行 \n 当成强边界,那么小说、转写稿、Markdown 等包含大量短行的文档会被切成非常碎的小块,导致上下文丢失。社区里有人提出 “Buffer-First Splitting”:先把文本累积到接近 max_length,再向后或向前寻找合适 separator 切开。(GitHub)
另一个 Dify issue 提到,自定义分隔符在某些模式下不支持正则,导致中文连续文本无法按 [。?!;\n] 这类规则切分,而是退化成硬截断。(GitHub)
这两个问题非常值得借鉴:
separator 不是越多越好。
separator 应该有强弱优先级。
单换行通常是弱边界,不能默认等同于段落边界。
对中文、日文、代码、表格,必须有专门规则。

三、长文档切片的整体架构图

这个流程里最容易被忽略的是 D、F、I、L 四步。
很多系统只做了:
原文 → 固定长度切片 → embedding → 检索
但成熟 RAG 更接近:
原文 → 文档结构解析 → 结构感知切片 → metadata 增强 → 多粒度索引 → 检索后上下文扩展。

四、切片粒度到底该怎么选?

1. 不同文档类型的推荐起点

文档类型
推荐 chunk size
overlap
推荐策略
FAQ / 客服问答
128–256 tokens
0–32
问答对整体切片
技术文档 / API 文档
300–600 tokens
50–100
标题路径 + 段落递归切
产品手册 / 操作指南
400–800 tokens
80–150
步骤块完整保留
法律合同 / 政策制度
512–1024 tokens
100–200
条款级 + 父子块
论文 / 研究报告
512–1000 tokens
100–200
section-aware + 摘要增强
Markdown 知识库
300–700 tokens
50–120
header-aware
代码库
200–600 tokens
0–80
函数 / 类 / import 结构切
表格密集文档
不固定
0
表格单独 chunk,必要时生成表格摘要
会议纪要 / 转写稿
300–700 tokens
50–120
speaker-aware / topic-aware
这里的数字不是标准答案,而是调参起点。Chroma 的评测显示,chunk size、overlap、embedding 模型、策略之间存在明显交互;比如在 text-embedding-3-large 上,400 tokens 左右的递归切分和语义聚类策略表现较好,但换成 all-MiniLM-L6-v2 后,overlap 对小上下文模型的 recall 更重要。(Chroma)

2. 一个实用判断公式

可以先用下面这个经验公式估算:
chunk_size ≈ 单个答案证据所需 token × 1.5 到 2.5
例如:
查询类型
单个答案证据长度
推荐 chunk
“这个参数是什么意思?”
50–100 tokens
128–256
“如何完成某个配置?”
150–300 tokens
300–600
“某条制度适用条件是什么?”
300–500 tokens
512–900
“总结某章节核心观点”
600–1000 tokens
父子块 / section chunk
检索粒度应该贴近“用户问题需要的最小证据单元”,而不是贴近“文档平均段落长度”。

五、分块策略一:递归结构切分

递归结构切分适合大多数通用文档。核心思路是:先按强结构切,再按弱结构切,最后才硬切。
中文技术文档可以使用这样的 separator 优先级:
但注意:不要简单地“遇到 separator 就切”。更稳的做法是:
这就是 Dify 社区 issue 里提到的 separator-first 问题的反方向设计:不要让弱 separator 主导切片,而应该让 max_tokens 和语义完整性共同决定边界。(GitHub)

六、分块策略二:结构感知切片

对长文档来说,先解析结构比直接切文本更重要。
推荐先把文档转成统一的 DocumentElement
然后按 element 类型处理:
Unstructured 的 chunking 策略就体现了这种思路:先把文档解析为元素,Table 单独处理,by_title 策略保留 section boundary,避免一个 chunk 跨多个章节。(docs.unstructured.io)

七、分块策略三:标题路径注入

对于技术文档、制度文档、Markdown 文档,标题路径非常关键。
例如原文结构是:
产品配置
└── 权限管理
└── API Key 权限说明
└── “Admin Key 可以创建和删除知识库。”
如果只 embedding 最后一段:
Admin Key 可以创建和删除知识库。
这个 chunk 的语义是不完整的。更好的 embedding 文本应该是:
这样做有三个好处:
embedding 更容易知道这个片段属于什么主题。
rerank 时上下文更完整。
LLM 回答时可以引用章节路径,提高可信度。
但标题路径会占 token,所以要采用 LlamaIndex 那种 metadata-aware 思路,把 metadata token 从 chunk size 里扣掉。LlamaIndex 的 split_text_metadata_aware 就是根据 metadata 长度计算 effective_chunk_size。(GitHub)

八、分块策略四:父子块 / 多粒度索引

父子块是长文档 RAG 里非常重要的一种策略。
LlamaIndex 的 Auto Merging Retriever 就是类似思路:先检索 leaf nodes,如果多个 leaf nodes 指向同一个 parent,并超过阈值,就递归合并成更大的上下文。其默认层级示例是 2048、512、128 三层,leaf nodes 进入向量索引,其他节点保存在 docstore。(Developer Documentation)
这种方案适合:
法律合同:检索命中某个条款,但回答需要整个章节。
技术手册:命中某个参数,但需要前后配置步骤。
论文报告:命中某个实验结论,但需要方法、指标、上下文。
代码库:命中某个函数,但需要类定义和调用关系。
父子块的推荐参数:
层级
作用
推荐大小
child chunk
用于 embedding 和精确召回
200–500 tokens
parent chunk
用于补充上下文
1000–2500 tokens
section chunk
用于章节级问答 / 总结
3000+ tokens 或按标题切
注意:不要直接把 parent chunk 全部 embedding 后检索,否则召回会变粗。更好的方式是:
child 负责召回,parent 负责生成上下文。

九、分块策略五:邻接块扩展

有些问题命中的 chunk 本身不完整,但答案就在前后一个 chunk。
例如:
chunk 8:介绍参数含义。
chunk 9:介绍参数默认值。
chunk 10:介绍参数示例。
如果 query 是“这个参数怎么配置”,只命中 chunk 9 可能不够。因此可以在检索后做邻接扩展:
Dify 的 DocumentSegment 模型里也有 previous_segmentnext_segment 这类邻接关系属性,说明生产系统通常需要保存 chunk 的顺序关系,而不是只保存孤立向量。(GitHub)

十、分块策略六:语义切分与 Late Chunking

1. 语义切分

语义切分不是按固定字符或句子,而是通过 embedding 判断相邻句子的语义相似度,发现主题突变点后切开。
简化流程:
它适合主题跳跃明显的文档,比如会议纪要、访谈、咨询报告、长篇博客。但它的成本更高,而且对 embedding 模型敏感。
Chroma 的评测中,语义聚类 chunker 在部分指标上表现突出;LLMSemanticChunkertext-embedding-3-large 实验里取得了最高 recall,但报告也指出 LLM chunker 的耗时可能从几乎瞬时上升到几十分钟,是实际系统必须考虑的成本。(Chroma)

2. Late Chunking

传统方式是:
先切 chunk,再分别 embedding。
Late Chunking 是:
先用长上下文 embedding 模型编码整篇或大段文本,再在 token embedding 层面对 chunk 做 pooling。
Jina 的 Late Chunking 说明中提到,它先让 transformer 处理尽可能长的文本,让 token 表征包含全文上下文,然后再对较小片段做 mean pooling,生成带上下文的 chunk embedding。其示例评测显示,在多个 BEIR 数据集上,Late Chunking 相比传统 chunking 有提升。(GitHub)
Late Chunking 适合:
指代关系多的长文档,比如“该城市”“上述方法”“本条规定”。
上下文跨 chunk 依赖明显的论文、法律、百科类文档。
使用长上下文 embedding 模型的系统。
但它也有工程限制:
需要 embedding 模型支持长上下文。
实现复杂度更高。
增量更新不如普通 chunk embedding 简单。
对超大文档仍然需要先做大段窗口化。

十一、不同内容类型的特殊处理

1. 表格

表格不要直接按普通文本切。推荐三种方式:
表格类型
处理方式
小表格
整表作为一个 chunk
大表格
按行组切片,保留表头
复杂表格
生成表格摘要 chunk + 原表格 chunk
示例:
Unstructured 明确将 Table element 隔离,不和其他 element 合并;超长 table 会被切成多个 TableChunk。(docs.unstructured.io)

2. 代码

代码 chunk 应优先按 AST 或语言结构切:
import / package 信息可以作为 metadata。
class 尽量保持完整。
function 尽量保持完整。
超长函数可以按注释块、逻辑块继续切。
embedding 文本里补充文件路径、类名、函数签名。
LangChain 的 get_separators_for_language 就体现了这个思路:Python 优先按 classdef,JavaScript / TypeScript / Java / Go 等也有各自语言结构 separator。(GitHub)

3. PDF

PDF 的关键不是切片,而是解析质量。必须保留:
页码
标题层级
表格结构
图片 caption
脚注
坐标或 layout 信息
分栏顺序
Haystack 的 DocumentSplitter 会给 split 后的文档添加 source_idpage_number,这类元数据对 PDF 溯源很重要。(docs.haystack.deepset.ai)

4. 中文文档

中文没有天然空格,不能照搬英文 word splitter。推荐 separator:
但中文标点切分要特别注意:
标点应该尽量保留在前一个句子末尾。
overlap 不要从标点后面开始,否则容易出现 chunk 以句号、逗号开头。
自定义 separator 最好支持正则,而不是只支持字面量。
对连续短句,应该先合并到目标 token 附近,而不是一句一个 chunk。
Dify 的相关 issue 就反映了中文正则分隔和硬截断问题:用户期望按 [。?!;\n] 切分,但实际没有按正则处理,导致中间截断。(GitHub)

十二、一个可落地的自适应 Chunker 设计

下面是一个工程实现骨架,适合放进 RAG 项目里作为基础 chunker。
这个 chunker 的核心设计点是:
先结构化解析,再按类型处理。
先递归切出候选片段,再 buffer-first 合并。
metadata 进入 chunk 渲染,但要计入 token 长度。
overlap 只从尾部取完整 unit,不从字符中间截。
tiny chunk 自动并回,避免召回无意义碎片。
表格和代码预留特殊处理入口。

十三、RAG 切片参数如何优化?

1. 不要只调 chunk_size

应该联合调这些参数:
参数
影响
chunk_size
召回粒度、embedding 表达密度
chunk_overlap
边界召回、索引冗余
separator 优先级
语义完整性
min_chunk_size
tiny chunk 数量
top_k
召回覆盖率
rerank_top_n
最终上下文质量
parent_size
生成上下文完整性
metadata 注入方式
检索语义和引用可信度
Chroma 的结果也说明,overlap 并非越大越好;在一些设置中减少 overlap 会改善 IoU,因为冗余信息会被惩罚。(Chroma) Cohere 则强调 overlap 能缓解切断问题,但代价是冗余。(Cohere Documentation)

2. 建议做网格实验

评测指标:
指标
解释
Recall@K
正确证据是否被召回
MRR
正确证据排名是否靠前
nDCG
多个相关证据的排序质量
Context Precision
送入 LLM 的上下文有多少是有用的
Answer Faithfulness
回答是否忠实于上下文
Token Cost
每次问答消耗多少上下文 token
Latency
检索、rerank、生成总耗时
Duplicate Ratio
overlap 导致的重复率
优化目标不是单纯最大化 recall,而是:
在可接受 token 成本下,让正确证据稳定进入 LLM 的前半部分上下文。

十四、推荐的生产级方案

如果你要做一个通用企业知识库,我建议默认采用下面这套方案:
默认配置可以这样:
配置项
推荐值
child_chunk_size
400–600 tokens
child_overlap
50–100 tokens
parent_chunk_size
1500–2500 tokens
parent_overlap
100–200 tokens
min_chunk_size
80–150 tokens
top_k
10–30
rerank_top_n
4–8
neighbor_window
0–1
metadata 注入
标题路径 + 文档名 + 页码
表格处理
单独 chunk + 表头复制
代码处理
函数 / 类级切分
对于中文知识库,我会把 。!?; 放在比 \n 更高的位置,避免 Markdown 或 PDF 抽取出的单换行造成过碎切片。

十五、常见错误

错误一:只按固定 token 数切

这种方式实现简单,但经常切断定义、步骤、表格、代码函数。适合作为兜底,不适合作为主策略。

错误二:overlap 设置过大

例如 chunk 512,overlap 256,看似上下文更完整,实际会导致索引膨胀、重复召回、rerank 干扰和上下文浪费。Chroma 的评测中也观察到,减少 overlap 有时会提升 IoU,因为它减少了冗余。(Chroma)

错误三:把标题单独切成 chunk

标题本身通常没有足够语义。更好的方式是把标题作为 heading_path 注入后续正文 chunk。

错误四:表格被拆散且丢失表头

大表格按行组切时,必须复制表头,否则每个 chunk 都会失去列含义。

错误五:metadata 不参与 token 预算

如果 chunk 正文 512 tokens,metadata 又加 100 tokens,最终 embedding 和 prompt 实际都是 612 tokens。LlamaIndex 的 metadata-aware 切分就是为了解决这个问题。(GitHub)

错误六:只看召回,不看答案质量

RAG 的目标不是“召回很多 chunk”,而是“让 LLM 看到正确、紧凑、低噪声的证据”。

十六、最终结论

长文档切片的最佳实践可以总结成一句话:
用小块做精确召回,用大块补充上下文,用结构信息保证语义完整,用评测数据持续调参。
更具体地说:
通用文档:从递归结构切分开始。
技术文档:标题路径 + 段落递归切。
PDF:先做 layout / element 解析,再切。
表格:独立处理,保留表头。
代码:按函数、类、语言结构切。
法律、政策、论文:使用父子块或层级 chunk。
指代关系强的长文档:考虑 Late Chunking。
中文文档:中文标点优先于单换行,避免 separator-first 过碎切片。
生产优化:用 Recall@K、MRR、nDCG、Context Precision、Answer Faithfulness、Token Cost 联合评测。
成熟 RAG 项目的源码和文档已经说明了一个趋势:切片策略正在从“固定长度切文本”演进为“结构感知的多粒度检索系统”。LangChain 的递归切分、LlamaIndex 的 metadata-aware 和层级节点、Haystack 的显式 split 参数、Unstructured 的 element chunking、Dify 的自定义 / 层级切片实践、Jina 的 Late Chunking、Chroma 的系统评测,最终都指向同一个工程原则:chunk 不是文本片段,而是 RAG 系统里的最小可检索知识单元。
回到首页