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 会优先按 class、def 等结构切分。(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 可以是 word、sentence、passage、page、line、function;split_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_id 和 page_number,用于追踪 chunk 来源。(docs.haystack.deepset.ai) 这一点在企业知识库里非常关键,因为回答不仅要“答对”,还要能定位原文。4. Unstructured:元素级 chunking,表格和标题特殊处理
Unstructured 的 chunking 更偏“文档解析优先”。它不是直接把纯文本切开,而是先把文档解析成
Title、NarrativeText、Table 等 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:包含 delimiter、max_tokens、chunk_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_segment 和 next_segment 这类邻接关系属性,说明生产系统通常需要保存 chunk 的顺序关系,而不是只保存孤立向量。(GitHub)十、分块策略六:语义切分与 Late Chunking
1. 语义切分
语义切分不是按固定字符或句子,而是通过 embedding 判断相邻句子的语义相似度,发现主题突变点后切开。
简化流程:
它适合主题跳跃明显的文档,比如会议纪要、访谈、咨询报告、长篇博客。但它的成本更高,而且对 embedding 模型敏感。
Chroma 的评测中,语义聚类 chunker 在部分指标上表现突出;
LLMSemanticChunker 在 text-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 |
示例:
2. 代码
代码 chunk 应优先按 AST 或语言结构切:
import / package 信息可以作为 metadata。class 尽量保持完整。function 尽量保持完整。超长函数可以按注释块、逻辑块继续切。embedding 文本里补充文件路径、类名、函数签名。
LangChain 的
get_separators_for_language 就体现了这个思路:Python 优先按 class、def,JavaScript / TypeScript / Java / Go 等也有各自语言结构 separator。(GitHub)3. PDF
PDF 的关键不是切片,而是解析质量。必须保留:
页码标题层级表格结构图片 caption脚注坐标或 layout 信息分栏顺序
Haystack 的
DocumentSplitter 会给 split 后的文档添加 source_id 和 page_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 系统里的最小可检索知识单元。
分享
