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相似度判断切分点。
原理:
-
把文档切成句子 -
计算每句话的Embedding -
计算相邻句子的相似度 -
相似度突然下降的地方(说明主题切换),作为切分点
优点:
-
按语义主题切分,chunk内容高度相关 -
适合主题转换频繁的文档 -
不依赖固定大小,更智能
缺点:
-
计算成本高(需要为每个句子生成Embedding) -
实现复杂,LangChain新版已移除内置支持 -
需要调整相似度阈值,调优难度大
适用场景:新闻合集、学术综述、多主题文档
替代方案:使用LlamaIndex的SemanticSplitterNodeParser
5. 上下文增强切分
这是Anthropic在2024年提出的最新方法,专门解决长文档上下文丢失问题。
核心思路: 传统切分后,每个chunk是孤立的片段。比如文档第10页的一段话,检索时系统只知道这段话的内容,不知道它属于哪篇文章、前面讲了什么。
Contextual Retrieval的做法:
-
切分文档 -
对每个chunk,让LLM生成一段”上下文描述”(解释这个chunk在整篇文档中的位置和作用) -
把上下文描述append到chunk前面 -
重新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信息完整,但召回精度下降,可能返回不相关内容
实测经验值:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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系统质量影响巨大。核心原则:
-
切分的本质:把模糊的整体向量变成精准的局部向量 -
推荐默认方案:递归切分 + 512 tokens + 100 overlap -
进阶方案:文档结构切分(技术文档)、语义切分(多主题文档)、上下文增强(长文档高精度) -
关键调优参数:chunk_size(平衡召回和完整)、overlap(避免边界丢失)
把切分做好了,RAG系统的检索质量就能上一个台阶。后续再优化Embedding模型、检索算法,效果会更明显。
参考资料:
-
Pinecone: Chunking Strategies for LLM Applications -
LangChain: RecursiveCharacterTextSplitter文档 -
Anthropic: Contextual Retrieval (2024) -
论文: Lost in the Middle (2023)
夜雨聆风