一篇文章被切成 100 块还怎么保持语义:RAG 分块的底层逻辑

一篇文章被切成 100 块还怎么保持语义:RAG 分块的底层逻辑

摘要:RAG 系统的性能,很大程度上取决于你如何切分文档。块太大,检索不精准;块太小,语义不完整。本文拆解 RecursiveCharacterTextSplitter 的递归切割原理、Overlap 重叠窗口机制、五大分块策略的适用场景,以及最佳实践指南。

📑 目录

  • 为什么必须分块?RAG 的性能瓶颈
  • 分块的两个核心概念:Size 与 Overlap
  • 递归切割:谁超标,谁单独挨刀
  • 重叠窗口:牺牲结尾,保全开头
  • 五大分块策略对比
  • 实战代码:LangChain 分块完整流程
  • 最佳实践指南
  • 一点总结
  • 互动讨论

为什么必须分块?RAG 的性能瓶颈

在 RAG 系统中,文档经过 Loader 加载后,得到的是一个或多个 Document 对象。但 DocumentpageContent 可能包含数千甚至数万字的正文------整个网页、整篇论文、整份报告。

直接把整个 Document 喂给 LLM 会带来一系列问题:

问题 说明
成本过高 整篇文档直接传 LLM,Token 消耗巨大
超出限制 可能超过模型的 Token 上限(如 128K)
检索精度下降 无关信息稀释有效信号,检索命中率低
向量质量差 超大块向量化后语义模糊,相似度匹配不精准

分块(Chunking)的作用就是:把长文档切成语义完整的短片段,让每个片段都能被精准检索和利用。

这张图展示了分块在整个 RAG 流程中的位置:

  1. Loader :从数据源加载内容,标准化为 Document
  2. TextSplitter:将长文档切分成短片段(Chunks)
  3. 嵌入模型:向量化
  4. 向量数据库:存储

关键认知:分块的质量,直接决定了 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: 400chunkOverlap: 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):语义边界最强,优先尝试
  • 句号/问号/感叹号( ):句子边界
  • 空格(``):词边界,语义边界最弱,最后尝试
  • 空字符串(''):强行截断,兜底方案

三个关键原则

  1. 块大小是硬上限,不是目标值------宁愿块稍小,也不能超限
  2. 重叠窗口要适中------太小(<50)解决不了截断问题,太大(>200)造成数据冗余
  3. 优先尊重自然语义边界------在句号、段落处切割,而不是强行截断

元数据保留

为每个块记录来源 URL、标题、章节路径等信息,便于检索时追溯引用。

CheerioWebBaseLoader 中,metadata 会自动记录 source(来源 URL),这是很好的实践。在分块后,这个元数据会被继承到每个子块中。

一点总结

分块 = 把长文档切成语义完整的短片段。

递归切割 = 谁超标,谁单独挨下一级刀子,直到全部 ≤ chunkSize。

重叠窗口 = 牺牲前一块的结尾,保全下一块的开头上下文。

块大小是硬上限(Hard Cap),不是建议值。

分块是 RAG 系统中"不起眼但致命"的环节。好的分块策略,要在"语义完整性"和"检索精度"之间找到平衡。

互动讨论

  1. 为什么块大小是"硬上限"而不是"目标值"? 你觉得块大小应该尽量接近上限还是可以远小于上限?
  2. 递归切割中,分隔符的优先级顺序会影响最终结果吗? 如果把 放在最后会发生什么?
  3. 如果 chunkOverlap 设得太大(如 200),会有什么问题? 数据冗余会如何影响检索?
  4. 固定尺寸分块和递归分块,在技术文档场景下哪个更优? 为什么?
  5. 在 RAG 系统中,分块质量如何影响最终回答的质量? 你能举一个因分块不当导致回答错误的例子吗?

📌 一点心得:分块是 RAG 系统中最容易被忽视的性能瓶颈。很多人把精力花在模型选择和 Prompt 调优上,却忘了------如果数据都没切对,再好的模型也白搭。

相关推荐
AINative软件工程13 分钟前
LLM 多租户 Quota 工程实践:Token 配额、用量预警与自动熔断的生产设计
后端·llm·ai编程
To_OC9 小时前
我把《天龙八部》塞进向量数据库后,终于搞懂了 RAG 到底是个啥
人工智能·llm·agent
GoAI11 小时前
# AI Agent 记忆框架横向对比报告总结
人工智能·大模型·llm·多模态
AndrewHZ15 小时前
【LLM技术全景】阶段总结:技术原理篇核心知识回顾
人工智能·深度学习·算法·语言模型·大模型·llm·芯片开发
孙启超17 小时前
【AI应用开发】ReAct 原理是什么?和普通直接提问 LLM 差别在哪?
前端·人工智能·llm·agent·react·rag·ai应用开发
孙启超17 小时前
【AI应用开发】LangChain 中 Chain 和 Agent 核心区别?
java·人工智能·langchain·llm·rag·ai应用开发·agent loop
weixin_4713830319 小时前
03 Function Calling
llm·functioncalling
Wang's Blog20 小时前
AI Agent白手起家28: LangChain 五种提示词模板实战解析
大数据·人工智能·langchain
liferecords21 小时前
Qwen3.8-Max 开源了:该不该从 Claude 切过去?
开源·llm·qwen
GuWenyue1 天前
大模型幻觉无解?7步搭建Milvus+LangChain电子书RAG,私有小说精准问答零编造
人工智能·langchain·ai编程