一篇文章被切成 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 调优上,却忘了------如果数据都没切对,再好的模型也白搭。

相关推荐
先吃饱再说8 小时前
花 600 万就能复刻百亿模型?蒸馏技术到底是什么
人工智能·llm
BraveWang9 小时前
【LangChain 1.x】06、Tool Calling|工具的四种定义方式与手动调用全流程
langchain
中微极客10 小时前
LangChain vs LlamaIndex:RAG框架选型深度对比
人工智能·langchain
项羽欧大哥11 小时前
LangChain 和 LangGraph 到底有什么区别?实际项目中应该怎么选
langchain·agent·langgraph
kiss strong12 小时前
AI学习-langchain
学习·langchain
带刺的坐椅12 小时前
用 Solon ReActAgent 落地发票识别与智能报销
java·llm·solon·react-agent
掉鱼的猫12 小时前
用 Solon ReActAgent 落地发票识别与智能报销
java·llm·agent
千桐科技12 小时前
🚀 qModel 算法模型平台v1.2.0 开源版发布!Python 模型接入链路全面升级,告别“能跑但上不了线”的尴尬
后端·算法·llm
又抓住你了13 小时前
LangChain概述与环境准备:从框架定位到第一个应用
langchain