Chunking 与文档切分策略
为什么切分直接决定检索质量
切分(Chunking)是 RAG(检索增强生成)流水线的第一步,也是最容易被低估的一步。当用户提出一个问题时,检索系统能否找到正确的上下文,完全取决于切分后每个 chunk 是否恰好包含回答所需的信息。如果切分策略不当,即使 embedding 模型再强大、检索算法再精准,也无从找回正确答案。
切分策略的影响体现在三个核心维度:
- 召回精度:chunk 太小会丢失跨段落的关键信息,导致检索无法覆盖答案;chunk 太大则会引入过多噪声,稀释关键信息在向量空间中的显著性。
- 上下文窗口利用率:切分决定了模型每次只能看到 chunk 内容的窗口大小。切分不合理会导致窗口要么装不满(浪费 token 成本),要么装不下(被迫截断)。
- 语义一致性:同一主题的内容应该落在同一个 chunk 内,不同主题的内容应该被隔开。如果切分破坏了语义边界,检索回来的内容会牛头不对马嘴。
因此,切分不是”把文档切成小块”的技术活,而是直接影响最终回答质量的策略决策。
Chunk 太大和太小分别会带来什么问题
Chunk 太大
当 chunk 超过合理规模时,主要面临以下问题:
1. 向量稀释(Vector Dilution) 一个 chunk 包含的语义主题越多,其在向量空间中的位置就越”中庸”。Embedding 模型会将 chunk 压缩为一个固定维度的向量,如果 chunk 同时包含”A公司的财报”和”B公司的财报”,检索”苹果的营收增长”时,这个 chunk 既匹配 A 又匹配 B,在余弦相似度排序中可能排不到最前面。关键信息被稀释在大量无关内容中。
2. 上下文窗口浪费 假设模型上下文窗口为 128k tokens,而一个 chunk 被切成 50k tokens。回答一个问题可能只需要 5k tokens 的相关上下文,剩余 45k tokens 的噪声不仅浪费了窗口空间,还会被模型当作相关内容来理解,增加推理负担和幻觉风险。
3. 检索粒度过粗 大 chunk 对应的检索粒度是”段落级”甚至”章节级”,但用户的问题往往是具体的。当用户问”ResNet 的残差连接为什么能缓解梯度消失”时,返回整篇深度学习论文的 chunk 远不如只返回残差连接那一段来得精准。
Chunk 太小
1. 语义不完整 如果把一句话切成独立的 chunk,embedding 模型难以捕捉跨句子的因果关系和指代含义。例如,“Transformer 提出了自注意力机制”和”它可以并行处理序列”这两个句子分开后,单独检索前者会丢失”它”指代的是什么。
2. 上下文碎片化 RAG 的一个常见问题是回答缺乏连贯性。当 chunk 太小、检索回来的多个 chunk 之间缺乏衔接时,模型生成的回答会出现跳跃和不一致。
3. 标题和元数据被剥离 小切分容易把标题、表格说明、公式注释等元信息丢掉,而这些往往是最重要的检索信号。缺少上下文的 chunk 就像没有标题的新闻段落——你知道它讲了什么,但不知道它是关于什么的。
理想 chunk 大小的经验值
| 应用场景 | 推荐 chunk 大小 | 备注 |
|---|---|---|
| 通用文本(文章、报告) | 500–1000 tokens | 含 token 计算,一般 300–800 中文字符 |
| 代码片段(函数/类级) | 200–500 tokens | 保持函数签名和完整逻辑 |
| 学术论文(按节) | 1000–2000 tokens | 可包含标题+摘要+正文的一节 |
| 知识库问答 | 300–600 tokens | 保证一个 chunk 包含完整问答对 |
| 对话式检索 | 200–400 tokens | 短小精悍,减少噪声干扰 |
具体数值需要根据文档的平均句子长度、主题密度以及下游模型的窗口大小来调优。一个实用的原则是:chunk 应该包含足以回答大多数中等复杂度问题的信息,但又不包含太多跨主题的内容。
Overlap 的作用与设计
相邻 chunk 之间保留重叠区域(overlap)是处理信息边界丢失的常用手段。
为什么需要 Overlap
文本的语义边界并不总是整齐的。一句话的结尾可能引用了下一句的概念,一个段落的结论依赖于前一段的论证。如果严格按边界切分,这些跨边界的依赖关系就会被切断。Overlap 通过在相邻 chunk 之间引入冗余来缓解这个问题。
假设 chunk 大小为 500 tokens,overlap 为 100 tokens:
Chunk 1: [0, 500) → tokens 0–499
Chunk 2: [400, 900) → tokens 400–899(重叠100 tokens)
Chunk 3: [800, 1300) → tokens 800–1299(重叠100 tokens)
这样即使语义边界落在 token 400 附近,检索时至少有一个 chunk 包含了完整的上下文。
Overlap 的代价
- 存储和计算成本线性增长:overlap 比例为 20% 时,存储增加约 20%,embedding 计算量也同步增加。
- 检索冗余:可能把高度相似的多个 chunk 都检回来,增加去重负担。
- 并非越多越好:过大的 overlap 会导致 chunk 之间缺乏差异性,检索结果重复率高。
Overlap 设计建议
| 场景 | Overlap 比例 | 说明 |
|---|---|---|
| 语义边界清晰(按段落切分) | 10–15% | 防止边界信息丢失 |
| 语义边界模糊(按句子切分) | 20–30% | 需要更多冗余来桥接语义 |
| 长文档且主题连贯 | 固定 overlap tokens(如 50–100) | 而不是按比例 |
| 代码(函数级切分) | 不推荐 overlap | 函数边界本身是语义边界 |
切分粒度对召回率、精确率、上下文一致性的影响
切分粒度是召回率(Recall)和精确率(Precision)之间权衡的核心变量。
粒度越粗(chunk 越大):
- 召回率提升:相关内容的概率增加,因为大 chunk 更容易包含正确答案的完整上下文。
- 精确率下降:噪声内容增多,检索结果中不相关的比例上升。
粒度越细(chunk 越小):
- 精确率提升:chunk 内容集中,检索结果噪声少。
- 召回率下降:正确答案被切分到多个 chunk,可能漏掉关键信息。
上下文一致性(指检索回来的多个 chunk 在叙事上是否连贯):
- 粒度不一时会导致不一致。例如一段是”ResNet 解决了退化问题”,另一段是”VGG 使用了小卷积核”——两段完全不相关,但可能因为都提到”卷积神经网络”而被同时检索回来。
- 同一主题的内容应该尽量在同一 chunk 内,通过按标题层级或语义切分来保证。
实际工程中往往采用分层切分策略:用较大的 chunk(如整个章节)做初次检索,再用较小的 chunk(如段落级)做重排序或精匹配。
常见切分方式
固定长度切分(Fixed-Length Chunking)
最简单的策略,按 token 数量或字符数量硬性切分。
优点:实现简单、计算确定性好、对所有文档统一处理。
缺点:完全无视语义边界,很可能把一句话从中间截断,或者把两个不相关的短段落合并到一个 chunk。
适用场景:文档结构单一、内容均质、对召回精度要求不高的粗筛阶段。
按段落切分(Paragraph Chunking)
以段落(通常以换行或空行)为自然边界进行切分。
优点:保留了段落的语义完整性,embedding 能较好地捕捉段落主题。
缺点:段落长短不一,短段落单独成 chunk 时内容过少,长段落可能包含多个主题。
适用场景:新闻、博客、报告等以段落为主要叙事单位的文档。
按标题层级切分(Hierarchical Heading Chunking)
根据文档的层级结构(h1/h2/h3 标题)来决定切分边界。
优点:天然保留了文档的逻辑结构,检索时可以同时利用标题做上下文理解。
缺点:依赖文档有清晰的标题结构,网页或笔记类文档适用,但扫描版 PDF 或纯文本需要先做结构解析。
适用场景:技术文档、教程、wiki、Markdown 笔记等结构化文档。
语义切分(Semantic Chunking)
通过 embedding 模型计算相邻段落/句子之间的语义相似度,当相似度低于阈值时在当前位置切分。
优点:切分边界由语义内容驱动,而非人为设定的规则。能够处理跨段落的长距离依赖。
缺点:需要额外的 embedding 计算和相似度聚类,pipeline 复杂度提升;阈值选择需要调参。
适用场景:需要高质量切分的生产环境,尤其是文档主题多样、段落长度不均的情况。
滑动窗口切分(Sliding Window Chunking)
以固定步长(step)滑动窗口生成 chunk,窗口大小和 step 的差值即为 overlap。
优点:保证相邻 chunk 之间的上下文连贯,通过 overlap 覆盖边界。
缺点:chunk 之间可能高度相似,导致检索结果冗余;计算成本比固定切分高。
适用场景:长文本且语义连贯的文档,如技术书籍、长篇报告、论文全文。
结构化文档与非结构化文档的切分差异
非结构化文档
指没有预定义格式的纯文本,如小说、新闻、聊天记录。这类文档的切分策略相对简单,主要依赖:
- 段落边界(空行检测)
- 句子边界(NLTK/spaCy 分句)
- 固定 token 长度(适合 embedding 模型的最佳输入长度)
非结构化文档的核心挑战是缺乏显式的语义边界提示,因此语义切分和滑动窗口是更合适的选择。
结构化文档
指具有明确格式的文档,典型类型包括:
| 文档类型 | 结构特征 | 推荐切分策略 |
|---|---|---|
| Markdown / 笔记 | 标题层级、代码块、列表 | 按标题层级切分,保留标题作为 chunk 元数据 |
| HTML / 网页 | DOM 树结构、标题标签 | 按标签层级切分,保留 h1–h3 标题 |
| PDF(扫描) | 视觉布局、栏、段落 | 先做布局分析(表格检测、栏检测),再按区域切分 |
| JSON / API 文档 | 嵌套结构、字段定义 | 按对象或 endpoint 切分 |
| 表格数据 | 行/列结构 | 按行或按单元格组切分,保留列名作为元数据 |
| 代码仓库 | 文件、类、函数 | 按文件或函数级切分,保留文件名和行号 |
结构化文档切分时,元数据(标题、来源、层级)必须作为 chunk 的附属信息保存,而不是丢弃。这些信息在检索时是关键的上下文信号,在回答生成时可以帮助模型理解 chunk 的来源和主题。
代码、论文、网页、笔记分别适合什么切分策略
代码
代码的语义边界非常明确——一个函数或一个类应该尽量保持完整,不应该被截断。
- 切分单位:函数级(Python def、Java method)是首选,不推荐按行数切分。
- 元数据:文件名、函数签名、所在行号、语言类型。
- 特殊处理:长函数(超过 500 tokens)可以考虑按内部逻辑块再切,但要在 chunk 元数据中记录这是哪个函数的一部分。
- 工具选择:推荐使用 tree-sitter 等代码解析工具,而非简单的文本分句。
学术论文
论文有标准的结构(摘要、引言、方法、实验、结论),每个部分讨论的主题相对独立。
- 粗切分:按一级标题(Section)切分,chunk 大小约 1000–2000 tokens。
- 精切分:对于关键章节(如 Related Work、Experiment),可以按段落再切。
- 元数据:论文标题、作者、发表于哪个会议/期刊、章节标题。
- 特殊处理:参考文献列表单独处理(如果需要检索);图表说明单独成 chunk;公式尽量保留 LaTeX 源码而非截图。
网页
网页的 DOM 树结构是天然的切分依据,但需要清理大量导航栏、广告、页脚等无关内容。
- 切分前:必须清洗文档,去掉导航、侧边栏、广告、版权信息。
- 切分单位:按 article、section 或主要内容区块切分。
- 元数据:页面标题、URL、域名、主要标题(h1/h2)。
- 特殊处理:多语言页面需要先检测语言,同一语言的内容才放在一起;表格和列表要作为独立 chunk 提取。
笔记(Obsidian / Notion 类)
个人笔记往往结构松散,但会使用 # 标题、[[双链]]、- 列表等标记。
- 切分单位:按最高级标题(h1)切分为独立的 chunk,如果一个 h1 下内容过长(超过 2000 tokens),再按 h2 或自然段落细分。
- 元数据:笔记标题、创建日期、标签(#tag)、反向链接(backlinks)。
- 特殊处理:双链
[[笔记名]]指向的内容应在切分时解析为文本,而不是保留为链接格式,否则 embedding 时会丢失语义。 - 上下文问题:笔记类文档的上下文往往需要跨笔记检索(如
[[相关笔记]]的内容应该作为本笔记 chunk 的补充上下文),这种情况建议在切分后将相关笔记的链接内容也并入 chunk。
工程流程
第一步:文档清洗(Pre-processing)
在切分之前,必须对原始文档做清洗,否则会把大量噪声带入 chunk:
通用清洗步骤:
- 编码检测与统一:转换为 UTF-8,处理乱码。
- 去重与标准化:去除重复的页眉页脚、水印、版权声明(对每页都出现的固定文本做差分检测)。
- 结构解析:提取标题层级、段落、列表、表格、代码块。
- 去除无关内容:导航栏、广告、追踪脚本、HTML 标签(除非保留结构信息)。
- 格式化处理:将 LaTeX、Markdown、HTML 实体转换为可读文本;保留表格的行列结构(可用 CSV 或 JSON 表示)。
- 语言检测:多语言文档按语言分段,同一语言的内容放在一起。
文档类型特定的清洗:
| 文档类型 | 特殊清洗步骤 |
|---|---|
| 布局分析(两栏/三栏检测)、表格检测、公式检测、页眉页脚去除 | |
| 网页 | DOM 净化(去掉 nav/aside/footer/ad)、富文本转 plain text |
| 扫描件 | OCR、版面恢复 |
| 代码 | 格式化(统一缩进)、注释保留(可选)、依赖分析 |
第二步:定义 Chunk 元数据
每个 chunk 生成后,应该附带元数据字段。元数据不仅是检索的辅助信号,也是回答时模型理解上下文来源的依据。
推荐元数据字段:
| 字段名 | 说明 | 是否必须 |
|---|---|---|
source | 文档来源(文件名/URL) | 必须 |
title | chunk 所在文档的标题 | 必须 |
heading | chunk 对应的章节标题 | 推荐 |
chunk_index | 当前 chunk 在文档中的序号 | 推荐 |
total_chunks | 文档被切分的总 chunk 数 | 推荐 |
language | 内容语言 | 推荐 |
document_type | 文档类型(pdf/web/markdown 等) | 推荐 |
created_at | 文档创建时间 | 可选 |
authors | 作者(论文类文档) | 可选 |
page_numbers | 来源页码(PDF) | 可选 |
tags | 从标题或内容中提取的关键词标签 | 可选 |
backlinks | 反向链接(笔记类文档) | 可选 |
第三步:控制 Chunk 长度与重叠
_chunk 长度_和_overlap_是两个核心超参数,建议按以下步骤调优:
- 确定基线:先按经验值设定 chunk 大小(如 512 tokens),不做 overlap。
- 评估召回效果:用一批代表性的查询(Query)测试,看检索回来的 chunk 是否包含正确答案。
- 分析失败案例:
- 如果正确答案被截断 → 减小 chunk 大小 或增大 overlap。
- 如果检索结果噪声太多 → 增大 chunk 之间的差异性(用语义切分替代固定切分)。
- 如果跨 chunk 的信息丢失 → 增大 overlap。
- 设定最终参数:记录每次调参后的 Precision@K 和 Recall@K(K 通常取 3 或 5),选择最优组合。
第四步:抽样检查(Spot Check)
切分完成后,不能完全依赖自动评估,必须做人工抽样检查。建议流程:
- 随机抽样:从每个文档类型中随机抽取 5–10 个 chunk,检查语义是否完整。
- 边界检查:重点检查 chunk 边界附近的内容,看是否有被截断的句子或被拆散的列表。
- 元数据检查:确认元数据中的标题、页码、章节信息是否与 chunk 内容一致。
- 检索模拟:用几个典型查询手动检索,确认返回的 chunk 是否真的包含答案。
- 记录问题:建立切分质量评分表,定期回顾和迭代切分策略。
切分策略选择表
| 文档类型 | 结构化程度 | 内容特征 | 推荐切分策略 | Chunk 大小参考 | 必要元数据 |
|---|---|---|---|---|---|
| 新闻/博客 | 低 | 段落叙事连贯 | 段落切分 + 语义合并 | 500–800 tokens | 标题、发布日期、作者 |
| 技术文档/教程 | 高 | 层级结构清晰 | 按标题层级切分 | 800–1500 tokens | 标题、章节路径、层级 |
| API 文档 | 高 | 结构化定义 | 按 endpoint/对象切分 | 200–500 tokens | API 路径、方法名、版本 |
| 学术论文 | 高 | 章节主题独立 | 按 section 粗切 + 段落精切 | 1000–2000 tokens | 标题、作者、会议/期刊 |
| 代码仓库 | 高 | 函数/类边界明确 | 按函数/类切分 | 200–500 tokens | 文件名、函数签名、语言 |
| 知识库问答 | 中 | QA 对应关系强 | 按 QA 对切分 | 300–600 tokens | 问题类型、来源系统 |
| 网页 | 变化大 | 内容混杂 | DOM 净化 + 按内容区块切分 | 500–1000 tokens | URL、页面标题、主要标题 |
| 个人笔记 | 中 | 主题分散、双链引用 | 按 h1 切分 + 反向链接注入 | 500–1500 tokens | 笔记标题、标签、双链 |
| 表格数据 | 高 | 行列结构 | 按行或行列组切分 | 取决于表格宽度 | 列名、表头、来源 |
| 合同/法律文本 | 高 | 条款编号清晰 | 按条款/章节切分 | 500–1500 tokens | 条款编号、合同编号、日期 |
Chunk 设计注意事项清单
语义完整性
- Chunk 不应截断完整的句子或列表项
- 段落级主题应该独立成 chunk,不与其他主题混合
- 函数/类/方法的签名和实现不应被分到不同 chunk
元数据完整性
- 每个 chunk 必须包含 source(来源)字段
- 标题结构信息应作为元数据保留,不应丢弃
- 对于跨 chunk 的内容,应在元数据中标记前向/后向引用关系
长度控制
- Chunk 长度应控制在目标 embedding 模型的最佳输入范围内(通常 256–1024 tokens)
- 长度分布的方差不应过大,避免过短 chunk(< 50 tokens)过多
- 过长的 chunk 应考虑二次切分,同时保留原始 chunk 索引
噪声控制
- 切分前应去除页眉页脚、水印、版权声明等重复内容
- 导航栏、广告、追踪脚本等无关内容应在清洗阶段去除
- 代码注释是否保留应基于具体场景决定(注释多的代码可以考虑去掉注释后切分)
Overlap 设计
- 语义边界不清晰的文档应使用 overlap(建议 15–25%)
- 语义边界清晰的文档(如按标题切分)可以不使用 overlap
- Overlap 不应过大,否则检索结果重复率高
上下文一致性
- 同一主题的段落应尽量在同一 chunk 内
- 检索回来的多个 chunk 应该有某种程度的叙事连贯性
- 可以通过在 chunk 末尾添加”前情提要”或”上文提到”来增强上下文衔接
检索评估
- 建立评估集(包含 Query、相关文档、预期答案)
- 用 Precision@K 和 Recall@K 评估切分策略的效果
- 定期做人工 spot check,记录常见失败模式并迭代优化
切分与检索效果的深层联系
回到最初的观点:切分直接决定检索质量。为什么?
检索的本质是在向量空间中找到与 Query 最相似的 chunk。如果 chunk 的语义边界合理,每个 chunk 就是一个”信息原子”——既包含完整的知识点,又不混入其他知识点。检索时找到正确的 chunk,就等于找到了正确的知识点,回答质量就有了保证。
反之,如果 chunk 切分混乱——把不相关的内容捏在一起,把相关的内容拆得七零八落——无论检索算法多么精准,都无法弥补切分策略的缺陷。这就是为什么在实际 RAG 项目中,调优切分策略往往比调优检索算法带来的收益更大。
最后,切分策略不是一次设定终身使用的。随着文档库的变化(新增文档类型、文档主题漂移)、用户查询模式的改变(如从宽泛问题变成具体问题),切分策略都需要重新评估和调整。建立一套切分质量的监控机制,定期抽检和迭代,是保持检索系统长期效果的关键。