你以为调好 Embedding 模型就万事大吉了?不,真正决定 RAG 天花板的,往往是你根本没认真对待的那一步——文档切分。
一、引言:一个被严重低估的变量
很多开发者搭建 RAG 系统时,会花大量时间在模型选型、Prompt 工程、向量数据库调优上,却把文档切分这一步处理得极为草率:RecursiveCharacterTextSplitter(chunk_size=500) 一行代码糊上去,跑通了就上线。
结果问题接踵而至——检索回来的内容要么语义不完整,要么冗余噪声太多,LLM 生成的答案要么缺少关键信息,要么前言不搭后语。
Chunk Size 是 RAG 系统中最核心的超参数之一,它直接影响:
- 检索阶段的召回精度(Recall)与准确率(Precision)
- 送入 LLM 的上下文质量
- 向量索引的规模与检索延迟
- Token 消耗与成本
本文将系统拆解主流分块算法、重叠策略的原理与选型逻辑,并在文末提供一张可直接落地的决策表。
二、Chunk Size 的影响机制:一场精度与完整性的博弈
在深入算法之前,先建立一个核心认知框架。
Chunk 越小:向量语义越聚焦,检索精度越高,但单个 Chunk 可能缺乏足够上下文,导致 LLM 拿到片段信息无法作答。
Chunk 越大:上下文信息越完整,但向量包含多个语义,检索时"语义稀释"严重,相关性得分下降,噪声信息也会干扰 LLM 推理。
这是一个经典的 Precision vs. Recall 权衡,不存在放之四海皆准的最优解。
Chunk Size ↑ → 语义覆盖广 → 检索噪声↑,精度↓
Chunk Size ↓ → 语义聚焦 → 上下文不足,完整性↓
💡 反直觉结论 #1:Chunk 越大不一定越好。实验表明,在问答类任务中,512 token 的 Chunk 比 1024 token 的 Chunk 在 MRR(平均倒数排名)指标上平均高出 12-18%,因为大 Chunk 引入的语义噪声会压低相关文档的向量相似度排名。
三、主流分块算法详解
3.1 固定长度切分(Fixed-Size Chunking)
最简单粗暴的方式:按字符数或 Token 数硬切。
from langchain.text_splitter import CharacterTextSplitter
splitter = CharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separator="" # 不考虑任何分隔符
)
chunks = splitter.split_text(text)
优点:实现简单,索引规模可预测,处理速度快。
缺点:完全不考虑语义边界,极易在句子中间截断,产生语义残缺的片段。
适用场景:日志数据、结构高度规则的流水数据、对语义完整性要求不高的初期原型验证。
3.2 递归字符切分(Recursive Character Splitting)
LangChain 中最常用的默认方式,按优先级尝试一组分隔符(\n\n → \n → . → → ``),尽量在自然边界处切割。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64,
separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)
chunks = splitter.split_documents(documents)
优点:在保证 Chunk 大小可控的前提下,尽量保留自然段落边界,适应性强。
缺点:仍然是基于规则的启发式方法,无法真正理解语义连贯性。
适用场景:绝大多数通用文本场景的首选,覆盖 80% 的 RAG 工程需求。
3.3 基于文档结构切分(Structure-Aware Splitting)
针对 Markdown、HTML、PDF 等有明确结构的文档,按标题层级、段落标签等结构元素切分,最大程度保留文档的逻辑单元。
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
chunks = splitter.split_text(markdown_text)
# 每个 chunk 的 metadata 中自动携带所属标题层级
# chunk.metadata = {"h1": "安装指南", "h2": "环境配置"}
LlamaIndex 提供了更完整的结构感知能力:
from llama_index.core.node_parser import HierarchicalNodeParser, SentenceSplitter
parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128] # 三层粒度,从粗到细
)
nodes = parser.get_nodes_from_documents(documents)
优点:Chunk 与文档的逻辑结构高度对齐,Metadata 可用于过滤与精排,结构信息不丢失。
缺点:依赖文档本身结构质量,对格式混乱的文档效果退化严重。
适用场景:技术文档、API 文档、产品手册、知识库文章——只要文档有良好的 Markdown/HTML 结构,优先使用这种方式。
3.4 语义切分(Semantic Chunking)
最智能也最重的一种方式:使用 Embedding 模型计算相邻句子之间的语义相似度,在相似度骤降的位置进行切分,确保每个 Chunk 内部语义连贯。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
splitter = SemanticChunker(
embeddings=OpenAIEmbeddings(),
breakpoint_threshold_type="percentile", # 或 "standard_deviation"
breakpoint_threshold_amount=95 # 在相似度跌入前 5% 的位置切分
)
chunks = splitter.split_text(text)
优点:切分位置由语义决定,Chunk 内部主题一致性最高,理论上检索质量最优。
缺点:每次切分都需要调用 Embedding 模型,离线处理成本是固定切分的 10-50 倍;Chunk 大小不可控,可能产生极大或极小的片段。
适用场景:学术论文、长篇报告、法律文书等语义跳转明显的长文档;对检索质量要求极高、可接受较高预处理成本的场景。
💡 踩坑经验:语义切分并非总是最优解。在一个内部知识库项目中,我们用语义切分替换递归切分后,检索召回率反而下降了 8%。原因是技术文档中大量的代码块和表格破坏了句子相似度的计算,导致切分位置错乱。教训:语义切分对文档质量高度敏感,上线前务必对目标语料做人工抽样验证。
算法横向对比
| 算法 | 语义完整性 | 实现复杂度 | 处理速度 | Chunk 大小可控性 | 适用场景 |
|---|---|---|---|---|---|
| 固定长度 | ★☆☆☆☆ | ★☆☆☆☆ | ★★★★★ | ★★★★★ | 原型验证、日志数据 |
| 递归字符 | ★★★☆☆ | ★★☆☆☆ | ★★★★☆ | ★★★★☆ | 通用文本(首选) |
| 结构感知 | ★★★★☆ | ★★★☆☆ | ★★★★☆ | ★★★☆☆ | Markdown/HTML 文档 |
| 语义切分 | ★★★★★ | ★★★★☆ | ★★☆☆☆ | ★★☆☆☆ | 长文档、高质量语料 |
四、重叠策略深度解析
4.1 为什么需要 Overlap?
想象文档中有一句关键信息:
"因此,方案 A 的总成本为 230 万元,低于方案 B 的 280 万元。"
如果这句话恰好被切分到 Chunk N 的末尾和 Chunk N+1 的开头,那么任何一个单独的 Chunk 都无法完整回答"方案 A 的成本是多少"这个问题。
Overlap(重叠) 的本质是:让相邻 Chunk 共享一部分内容,作为语义衔接的"缓冲区",防止关键信息因切分位置不当而被割裂。
4.2 不同 Overlap 比例的影响
以 512 token 的 Chunk Size 为基准:
| Overlap 比例 | Overlap Token 数 | 效果分析 |
|---|---|---|
| 0%(无重叠) | 0 | 索引最小,但边界信息丢失风险高,不推荐用于问答场景 |
| 10% | ~51 | 轻量级保护,适合段落结构清晰的文档 |
| 15-20% | ~77-102 | 工程实践的黄金区间,在信息完整性和索引膨胀间取得平衡 |
| 30%+ | ~154+ | 索引规模膨胀明显(Chunk 数增加约 40%+),重复内容干扰检索排序,通常不推荐 |
4.3 实践建议
核心公式参考:
推荐 Overlap = Chunk Size × 10%~20%
示例:
- Chunk Size = 256 → Overlap = 25~50
- Chunk Size = 512 → Overlap = 50~100
- Chunk Size = 1024 → Overlap = 100~200
注意:Overlap 过大会导致多个 Chunk 检索得分相近,Top-K 结果中出现大量重复内容,反而稀释了有效信息密度。如果你发现检索结果"换汤不换药",大概率是 Overlap 设置过高。
五、调优建议:不同场景的推荐配置
| 场景 | 推荐 Chunk Size | 推荐 Overlap | 推荐算法 |
|---|---|---|---|
| 客服 FAQ 问答 | 128–256 token | 10% | 递归字符 / 结构感知 |
| 技术文档检索 | 512 token | 15% | 结构感知(Markdown Header) |
| 法律/合同文本 | 256–512 token | 20% | 语义切分 |
| 学术论文 | 512–1024 token | 15% | 语义切分 / 递归字符 |
| 代码库检索 | 按函数/类切分 | 0–5% | 结构感知(AST-based) |
| 新闻/短文章 | 256 token | 10% | 递归字符 |
通用调优流程:
- 从 递归字符切分 + 512 token + 15% overlap 作为基线
- 用 20-50 条标注问答对,计算 Hit Rate 和 MRR
- 在 [256, 512, 1024] 三档 Chunk Size 上各跑一遍,对比指标
- 根据文档结构特点,决定是否切换到结构感知或语义切分
- 固定算法后,在 [10%, 15%, 20%] 三档 Overlap 上微调
六、总结与最佳实践 Checklist
在你下次部署 RAG 系统之前,对照这份清单:
- 是否分析了目标文档的结构特点(Markdown?PDF?纯文本?)
- Chunk Size 是否根据 Embedding 模型的最优输入长度做过对齐?(大多数模型最优区间在 128–512 token)
- 是否保留了 Chunk 的 Metadata(所属章节、文档来源、页码)?
- Overlap 是否设置在 10%–20% 的合理区间?
- 是否用标注数据集做过基线评估,而非凭感觉调参?
- 对于长文档,是否考虑了层级索引(粗粒度检索 + 细粒度精排)?
分块策略选型决策表
文档有清晰的 Markdown/HTML 结构?
↓ 是 → 优先使用【结构感知切分】
↓ 否
文档是代码库?
↓ 是 → 使用【AST-based 切分】(LlamaIndex CodeSplitter)
↓ 否
文档语义跳转明显(学术/法律)且预处理成本可接受?
↓ 是 → 使用【语义切分】
↓ 否
→ 使用【递归字符切分】作为通用默认方案
Chunk Size 选择:
短问答场景 → 128–256
通用知识库 → 512
需要大量上下文的推理任务 → 512–1024
Overlap:
默认从 15% 开始,根据评估指标上下浮动
参考资料
- Evaluating the Ideal Chunk Size for a RAG System using LlamaIndex — LlamaIndex Blog, 2024
- RAGAS: Automated Evaluation of Retrieval Augmented Generation — arXiv:2309.15217
- Chunking Strategies for LLM Applications — Pinecone Engineering Blog, 2024
- LangChain 官方文档:Text Splitters
- LlamaIndex 官方文档:Node Parsers & Text Splitters