一篇文章被切成 100 块还怎么保持语义:RAG 分块的底层逻辑
摘要:RAG 系统的性能,很大程度上取决于你如何切分文档。块太大,检索不精准;块太小,语义不完整。本文拆解 RecursiveCharacterTextSplitter 的递归切割原理、Overlap 重叠窗口机制、五大分块策略的适用场景,以及最佳实践指南。
📑 目录
- 为什么必须分块?RAG 的性能瓶颈
- 分块的两个核心概念:Size 与 Overlap
- 递归切割:谁超标,谁单独挨刀
- 重叠窗口:牺牲结尾,保全开头
- 五大分块策略对比
- 实战代码:LangChain 分块完整流程
- 最佳实践指南
- 一点总结
- 互动讨论
为什么必须分块?RAG 的性能瓶颈
在 RAG 系统中,文档经过 Loader 加载后,得到的是一个或多个 Document 对象。但 Document 的 pageContent 可能包含数千甚至数万字的正文------整个网页、整篇论文、整份报告。

直接把整个 Document 喂给 LLM 会带来一系列问题:
| 问题 | 说明 |
|---|---|
| 成本过高 | 整篇文档直接传 LLM,Token 消耗巨大 |
| 超出限制 | 可能超过模型的 Token 上限(如 128K) |
| 检索精度下降 | 无关信息稀释有效信号,检索命中率低 |
| 向量质量差 | 超大块向量化后语义模糊,相似度匹配不精准 |
分块(Chunking)的作用就是:把长文档切成语义完整的短片段,让每个片段都能被精准检索和利用。

这张图展示了分块在整个 RAG 流程中的位置:
- Loader :从数据源加载内容,标准化为
Document - TextSplitter:将长文档切分成短片段(Chunks)
- 嵌入模型:向量化
- 向量数据库:存储
关键认知:分块的质量,直接决定了 RAG 系统的性能上限。
分块的两个核心概念:Size 与 Overlap
| 概念 | 含义 |
|---|---|
| Chunk Size(块大小) | 每个分块的尺寸上限,通常以字符数或 Token 数衡量。这是硬性上限(Hard Cap) ,每个块必须 ≤ 此值。 |
| Chunk Overlap(重叠窗口) | 相邻块之间重叠的部分,用于保留跨块上下文。 |
不存在"万能最优"的块大小。 最佳大小取决于文档类型和用户问题类型。
递归切割:谁超标,谁单独挨刀
RecursiveCharacterTextSplitter 执行的是 "分层降维打击" :
第一步:按最高优先级分隔符切分
用第一个分隔符(如 。)切分整个文档,得到一批块。
第二步:递归检查每个块
- ≤ chunkSize:保留,不再处理
- > chunkSize :单独拎出该块,用下一个分隔符(如
!)递归再切
第三步:重复第二步
直到所有块 ≤ chunkSize。
核心原则:谁超标,谁单独挨下一级刀子。 不是全盘重来,也不是只切最后一块。
举个例子,假设 chunkSize: 400,分隔符优先级:。→ !→ ?→ 空格:
text
arduino
【原文档】1000 字
↓ 用 "。" 切分
→ 块 A(600 字)✗ 超标 → 用 "!" 递归切
→ 块 A1(250 字)✓ 保留
→ 块 A2(350 字)✓ 保留
→ 块 B(400 字)✓ 保留
→ 块 C(0 字)✗ 空,丢弃
重叠窗口:牺牲结尾,保全开头
假设 chunkSize: 400,chunkOverlap: 100:
- 第 1 块:取 1 ~ 400 字符
- 第 2 块:从 300(即 400 - 100)开始,取 300 ~ 700 字符
- 第 3 块:从 600(即 700 - 100)开始,取 600 ~ 1000 字符
- ......
为什么要重叠?------ 解决"边界语义截断"问题。
如果一个关键句正好跨越了第 1 块和第 2 块的边界,没有重叠时,这句话会被切成两半,检索时两边都找不到完整的语义。有了重叠,跨边界的完整句子至少会在其中一个块中保留。
Overlap 的本质:牺牲前一块的完美结尾,换取下一块的开头上下文完整。
因为检索系统极少依赖"前一块的最后一句"去回答问题,而更依赖包含关键词的那一整块。
五大分块策略对比
在 splitter.md 中列出了五种分块策略:
| 策略 | 核心做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定尺寸分块 | 按固定字符数硬切 | 实现简单,块大小一致 | 会打断句子和语义边界 | 快速原型,格式统一的文档 |
| 递归分块 | 按自然分隔符切,超限递归再切 | 尊重文档结构,保持语义完整 | 实现略复杂 | 结构清晰的文档(技术文档、论文) |
| 语义分块 | 基于文本语义相似度切分 | 块内主题高度相关 | 需要调用 Embedding 模型,成本高 | 内容多变、对质量要求高的场景 |
| 结构分块 | 利用文档固有结构分块 | 保留层级关系和元数据 | 需要预处理提取结构 | HTML、Markdown、有明确大纲的文档 |
| LLM 分块 | 让 LLM 判断切分位置 | 最智能,能理解语义边界 | 成本极高,速度慢 | 小规模高质量文档,预算充足的场景 |
在大多数技术文档场景下,递归分块是最推荐的起点------它在实现复杂度和语义完整性之间取得了良好的平衡。
实战代码:LangChain 分块完整流程
在 index.mjs 中,完整的流程是:
javascript
javascript
import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
// 1. 爬取并加载为 Document
const cheerioLoader = new CheerioWebBaseLoader(
'https://juejin.cn/post/7660683463612743722',
{ selector: '.main-area p' }
);
const document = await cheerioLoader.load();
// 2. 分块
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400, // 每个 chunk 的大小上限(硬上限)
separators: ['。', '!', '?'], // 优先按这些分隔符切割
chunkOverlap: 100 // 重叠窗口大小
});
const splitDocuments = await textSplitter.splitDocuments(document);
console.log(splitDocuments);
代码解读:
| 参数 | 作用 |
|---|---|
chunkSize: 400 |
硬性上限,每个块不能超过 400 字符 |
separators: ['。','!','?'] |
切割优先级,先尝试句号,再问号/感叹号 |
chunkOverlap: 100 |
相邻块重叠 100 字符 |
splitDocuments() |
执行切割,返回 Document[] |
关于 chunkSize 的两个误区:
chunkSize 是硬性上限(Hard Cap),不是目标值。 系统保证每个块 ≤ chunkSize,但不保证每个块都接近 chunkSize。如果一段文本只有 50 字符,系统不会为了"凑满"而添加填充。
最佳实践指南
块大小建议
| 文档类型 | 推荐块大小 | 理由 |
|---|---|---|
| 新闻/博客文章 | 400~800 字符 | 段落较短,小粒度更精准 |
| 技术文档/论文 | 800~1500 字符 | 段落较长,需保留完整概念 |
| 对话/问答记录 | 200~400 字符 | 问题本身就是原子粒度 |
分隔符优先级设计
javascript
csharp
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400,
separators: ['\n\n', '\n', '。', '!', '?', ' ', ''],
// 顺序:段落 > 句号 > 其他标点 > 空格 > 强行截断
chunkOverlap: 100
});
分隔符的优先级应该遵循:从"语义边界强"到"语义边界弱" 。
- 段落分隔符(
\n\n):语义边界最强,优先尝试 - 句号/问号/感叹号(
。!?):句子边界 - 空格(``):词边界,语义边界最弱,最后尝试
- 空字符串(
''):强行截断,兜底方案
三个关键原则
- 块大小是硬上限,不是目标值------宁愿块稍小,也不能超限
- 重叠窗口要适中------太小(<50)解决不了截断问题,太大(>200)造成数据冗余
- 优先尊重自然语义边界------在句号、段落处切割,而不是强行截断
元数据保留
为每个块记录来源 URL、标题、章节路径等信息,便于检索时追溯引用。
在 CheerioWebBaseLoader 中,metadata 会自动记录 source(来源 URL),这是很好的实践。在分块后,这个元数据会被继承到每个子块中。
一点总结
分块 = 把长文档切成语义完整的短片段。
递归切割 = 谁超标,谁单独挨下一级刀子,直到全部 ≤ chunkSize。
重叠窗口 = 牺牲前一块的结尾,保全下一块的开头上下文。
块大小是硬上限(Hard Cap),不是建议值。
分块是 RAG 系统中"不起眼但致命"的环节。好的分块策略,要在"语义完整性"和"检索精度"之间找到平衡。
互动讨论
- 为什么块大小是"硬上限"而不是"目标值"? 你觉得块大小应该尽量接近上限还是可以远小于上限?
- 递归切割中,分隔符的优先级顺序会影响最终结果吗? 如果把
。放在最后会发生什么? - 如果 chunkOverlap 设得太大(如 200),会有什么问题? 数据冗余会如何影响检索?
- 固定尺寸分块和递归分块,在技术文档场景下哪个更优? 为什么?
- 在 RAG 系统中,分块质量如何影响最终回答的质量? 你能举一个因分块不当导致回答错误的例子吗?
📌 一点心得:分块是 RAG 系统中最容易被忽视的性能瓶颈。很多人把精力花在模型选择和 Prompt 调优上,却忘了------如果数据都没切对,再好的模型也白搭。