乐于分享
好东西不私藏

Day 16:RAG系统的文档切分策略:为什么切、怎么切、切多大?

Day 16:RAG系统的文档切分策略:为什么切、怎么切、切多大?

最近研究RAG系统,发现个有意思的事:很多人把精力都放在模型选择、检索算法上,却忽略了一个看似简单但影响巨大的环节——文档切分。

切分做得好,检索质量能提升30%以上;做得不好,用户问”什么是Transformer”,系统可能返回一段讲”Transformer发展历史”的文字,根本没提到核心定义。

这篇文章聊聊:为什么需要切分?有哪些切分策略?各自的优缺点是什么?如何选择适合自己的策略?


一、为什么必须切分文档?

RAG系统的核心流程是:文档 → Embedding → 向量数据库 → 检索 → 返回相关片段 → 喂给LLM生成回答。

如果直接把整篇文档丢进去,会遇到几个问题:

Embedding模型的”胃口”有限

所有Embedding模型都有上下文窗口限制。比如OpenAI的text-embedding-3-small最多8196 tokens,超过这个限制,多余的内容会被截断,直接丢弃。

问题来了:被截断的可能是文档最核心的部分。一篇5000字的技术文档,前3000字是背景介绍,后2000字才是核心内容。如果按token顺序截断,检索时根本找不到关键信息。

向量检索的本质是”局部匹配”

Embedding的工作原理是把文本压缩成一个固定长度的向量(比如1536维)。这个向量反映的是文本的”整体语义”。

如果把整篇文档压缩成一个向量,它会变得很”模糊”。就像把一张高清照片压缩成32×32像素的缩略图,细节全部丢失。检索时,用户问一个具体问题,系统只能拿这个模糊的整体向量去匹配,召回的可能是完全不相关的内容。

举个例子:

  • 文档内容:”第一章介绍Transformer原理,第二章讲BERT架构,第三章分析GPT
    系列…”
  • 用户问:”Transformer的注意力机制怎么计算?”
  • 如果用整篇文档的向量去匹配,系统可能返回整篇文档(因为整体语义确实相关),但用户要的只是第一章那几段。

切分后,每个chunk有独立的向量,检索精度大幅提升。

LLM的”Lost-in-the-Middle”问题

2023年有篇论文《Lost in the Middle》,发现一个现象:LLM处理长文档时,开头和结尾的信息记得清楚,中间的内容容易被”遗忘”。

即使你有200K context window的模型(比如Claude),把检索到的10个chunk直接堆在一起喂给它,中间位置的chunk可能被忽略。解决方案是控制每个chunk的长度,确保关键信息在LLM能”记住”的位置。


二、主流切分策略对比

目前主流的切分策略有五种,各有适用场景。

1. 固定大小切分

最简单直接的方式:按固定的token数量切分。

from langchain_text_splitters import CharacterTextSplittersplitter = CharacterTextSplitter(    separator="\n\n",        # 按段落分割    chunk_size=512,          # 每个chunk最大512字符    chunk_overlap=50,        # 重叠50字符    length_function=len)chunks = splitter.split_text(text)

优点

  • 实现简单,计算成本低
  • chunk大小可控,便于预算token消耗
  • 适合结构简单、主题单一的文档

缺点

  • 可能切断语义完整性。一句话”Transformer的核心是注意力机制,计算公式为…”
    被切成两段,检索时两者可能都召回不了。
  • 对结构化文档(Markdown、代码)效果差

适用场景:短文本、简单文档、快速原型验证


2. 递归切分

这是LangChain的默认推荐方案。

核心思路:按分隔符层级递归切分。先尝试按段落切,如果某个段落超过chunk_size,再按句子切,如果句子还太长,按单词切,最后按字符切。

from langchain_text_splitters import RecursiveCharacterTextSplittersplitter = RecursiveCharacterTextSplitter(    chunk_size=512,    chunk_overlap=50,    separators=["\n\n""\n"" """]  # 分隔符优先级)chunks = splitter.split_text(text)

优点

  • 尽可能保留语义完整性(优先保持段落、句子完整)
  • 适应性强,能处理不同结构的文档
  • chunk大小可控,overlap避免信息丢失

缺点

  • 计算成本略高于固定切分
  • 对代码、Markdown等特殊结构仍不够智能

适用场景:大多数通用文档、技术文章、博客内容


3. 文档结构切分

针对特定格式文档,按其天然结构切分。

Markdown按标题切分

from langchain_text_splitters import MarkdownHeaderTextSplittermarkdown_document = "# 第一章\n## 1.1 Transformer原理\n..."headers_to_split_on = [    ("#""Header 1"),    ("##""Header 2"),    ("###""Header 3")]splitter = MarkdownHeaderTextSplitter(headers_to_split_on)chunks = splitter.split_text(markdown_document)

切分结果:每个chunk对应一个章节,保留完整的标题信息。

优点

  • 保留文档逻辑结构,检索结果更精准
  • 用户问”第一章讲了什么”,系统直接返回第一章的内容,而不是碎片化的段落
  • 适合技术文档、教程、学术论文

缺点

  • 需要文档有明确的结构标记
  • 对无结构文档(纯文本小说)不适用
  • 实现复杂度较高

适用场景:Markdown文档、技术博客、HTML网页、LaTeX论文


4. 语义切分

这是2023年Greg Kamradt提出的方法,核心思路:用Embedding相似度判断切分点。

原理:

  1. 把文档切成句子
  2. 计算每句话的Embedding
  3. 计算相邻句子的相似度
  4. 相似度突然下降的地方(说明主题切换),作为切分点

优点

  • 按语义主题切分,chunk内容高度相关
  • 适合主题转换频繁的文档
  • 不依赖固定大小,更智能

缺点

  • 计算成本高(需要为每个句子生成Embedding)
  • 实现复杂,LangChain新版已移除内置支持
  • 需要调整相似度阈值,调优难度大

适用场景:新闻合集、学术综述、多主题文档

替代方案:使用LlamaIndex的SemanticSplitterNodeParser


5. 上下文增强切分

这是Anthropic在2024年提出的最新方法,专门解决长文档上下文丢失问题。

核心思路: 传统切分后,每个chunk是孤立的片段。比如文档第10页的一段话,检索时系统只知道这段话的内容,不知道它属于哪篇文章、前面讲了什么。

Contextual Retrieval的做法:

  1. 切分文档
  2. 对每个chunk,让LLM生成一段”上下文描述”(解释这个chunk在整篇文档中的位置和作用)
  3. 把上下文描述append到chunk前面
  4. 重新Embedding并存储

示例

  • 原chunk:”注意力机制的计算公式是Attention(Q,K,V)=softmax(QK^T/√d_k)V”
  • 加上下文后:”本文第1章介绍Transformer架构。本节讲解自注意力机制的数学原理。注意力机制的计算公式是Attention(Q,K,V)=softmax(QK^T/√d_k)V”

优点

  • 解决chunk孤立问题,检索时能”理解”chunk在整篇文档中的位置
  • 对长文档(100+页)效果显著
  • Anthropic实测:检索准确率提升显著

缺点

  • 计算成本极高(需要为每个chunk调用LLM生成上下文)
  • 实现复杂,需要缓存优化
  • 适合对检索质量要求极高的场景

适用场景:法律文档、技术手册、长篇学术论文


三、Chunk大小怎么选?

切分策略定了,chunk大小(chunk_size)怎么选?

核心权衡:

  • Chunk太小 → 向量数量多,检索召回率高,但每个chunk信息碎片化,上下文不完整
  • Chunk太大 → 向量数量少,每个chunk信息完整,但召回精度下降,可能返回不相关内容

实测经验值:

Chunk大小(tokens)
特点
适用场景
128-256
信息碎片化,召回率高但上下文丢失
短问答、精确匹配
512
平衡点,推荐默认
大多数通用文档
1024
信息完整,召回精度略低
需要完整上下文的场景
2048+
几乎不切分,接近整文档
极短文档或需要全文的场景

Overlap怎么选:

Overlap是chunk之间的重叠部分,避免边界信息丢失。

推荐值:chunk_size的10-20%

  • chunk_size=512 → overlap=50-100
  • chunk_size=1024 → overlap=100-200

四、如何选择适合自己的策略?

根据文档类型和应用场景选择:

决策流程

1. 文档长度 < 500 tokens?   → 不需要切分,直接Embedding整文档2. 文档有明确结构(Markdown标题、HTML标签)?   → 用文档结构切分3. 文档主题频繁切换(新闻合集、综述论文)?   → 用语义切分4. 文档是长文档(100+页)且对检索质量要求极高?   → 用上下文增强切分5. 以上都不满足?   → 用递归切分 + chunk_size=512 + overlap=100

简单粗暴的推荐:

  • 80%场景:递归切分 + 512 tokens + 100 overlap
  • 技术文档:Markdown结构切分
  • 长文档高精度:Contextual Retrieval

五、实战建议

从简单开始

不要一开始就追求最复杂的方案。先用递归切分跑通流程,发现检索质量不够好,再针对性优化。

用真实数据测试

切分策略没有银弹,必须用你的真实文档测试。准备10-20个测试查询,看检索召回的chunk是否相关,如果不相关,调整chunk_size或切分策略。

关注边界问题

切分最容易出问题的是边界。检查:

  • 有没有一句话被切成两段?
  • 有没有关键信息刚好在边界被丢弃?
  • overlap够不够覆盖边界内容?

记录切分参数

每个项目记录使用的切分策略、chunk_size、overlap值,方便后续调优和复用。


总结

文档切分看似简单,但对RAG系统质量影响巨大。核心原则:

  1. 切分的本质:把模糊的整体向量变成精准的局部向量
  2. 推荐默认方案:递归切分 + 512 tokens + 100 overlap
  3. 进阶方案:文档结构切分(技术文档)、语义切分(多主题文档)、上下文增强(长文档高精度)
  4. 关键调优参数:chunk_size(平衡召回和完整)、overlap(避免边界丢失)

把切分做好了,RAG系统的检索质量就能上一个台阶。后续再优化Embedding模型、检索算法,效果会更明显。


参考资料

  • Pinecone: Chunking Strategies for LLM Applications
  • LangChain: RecursiveCharacterTextSplitter文档
  • Anthropic: Contextual Retrieval (2024)
  • 论文: Lost in the Middle (2023)