数据准备 —— RAG系统的“食材采购与切配”
如果把RAG比作一家餐厅,数据准备就是采购食材 + 洗菜切菜的环节。食材不新鲜、不干净,再厉害的厨师也做不出好菜;切得太粗或太碎,都会影响最终的口感。RAG系统也一样——喂进去的是垃圾,生成出来的只能是垃圾。
一、数据准备概述
1.1 为什么数据准备如此重要?
很多人刚开始接触RAG时,会把注意力都放在检索算法或大模型选择上,觉得“加载文档不就是读个文件嘛,有什么难的?”
事实恰恰相反。 数据准备是整个RAG系统的“地基”,地基没打好,上面盖的楼再漂亮也是危楼。
三个容易被忽视的真相:
真相一:现实世界的文档“脏”得很
你手上的PDF可能是扫描件(本质是图片,没有文字),Word文档里可能嵌套了表格,网页里夹杂着广告和导航栏……这些“噪音”如果不清洗,会直接污染知识库。
真相二:文档结构里藏着重要信息
“标题”和“正文”的权重不同,“表格”和“图片”里可能有关键数据。如果只是简单地把所有文字揉成一团,这些结构信息就全丢了。
真相三:元数据是溯源的命根子
回答完问题,用户追问“这个结论出自哪一页?”——如果你在加载时没记录页码,就只能哑口无言。
💡 一个原则:Garbage In, Garbage Out(垃圾进,垃圾出) 。你给系统的文档质量,直接决定了系统回答的质量。
1.2 数据准备的两大核心环节
数据准备包含两个紧密相连的步骤:
| 环节 | 通俗理解 | 核心任务 |
|---|---|---|
| 数据加载 | “采购食材” | 把PDF、Word、网页等五花八门的文件,统统“翻译”成程序可处理的文本 |
| 文本分块 | “切配食材” | 把长文档切成大小适中、语义完整的小块,方便后续向量化和检索 |
两者环环相扣:加载决定了“有什么”可用,分块决定了“怎么用”得好。 本章将依次详解这两个环节。
二、数据加载 —— 把文档“读”进系统
2.1 数据加载的三大核心任务
一个合格的文档加载器,在RAG管道中需要完成三件事:
| 任务 | 通俗解释 | 为什么重要 |
|---|---|---|
| 格式解析 | 把PDF、Word、网页等五花八门的文件,统统“翻译”成纯文本 | 大模型只认识文本,不认识格式 |
| 元数据抽取 | 记录这份文档的来源、作者、页码、创建时间等“身份证信息” | 让回答可以溯源,增强可信度 |
| 数据结构化 | 把文本和元数据整理成统一的格式,方便后续切分和入库 | 标准化是自动化的前提 |
2.2 主流文档加载器速览
当前RAG生态中有大量文档加载器,各有侧重。下表帮你快速了解它们的特点和适用场景:
| 工具名称 | 一句话定位 | 最擅长处理 | 亮点 | 潜在不足 |
|---|---|---|---|---|
| PyMuPDF4LLM | PDF→Markdown转换专家 | 科研文献、技术手册 | 开源免费,支持OCR和表格识别,GPU加速 | 专注PDF,不支持其他格式 |
| TextLoader | 极简纯文本加载器 | .txt纯文本文件 | 轻量高效,零依赖 | 只能读纯文本,功能单一 |
| DirectoryLoader | 批量目录处理能手 | 混合格式文档库 | 一键加载整个文件夹,自动识别格式 | 需要配合其他加载器使用 |
| Unstructured | 万能格式转换器 | PDF、Word、HTML、Excel等 | 统一接口,智能解析文档结构 | 对复杂版面解析仍有提升空间 |
| FireCrawlLoader | 网页实时抓取工具 | 在线文档、新闻页面 | 实时获取最新网页内容 | 依赖网络,不适合离线场景 |
| LlamaParse | PDF深度解析利器 | 法律合同、学术论文 | 解析精度极高,专攻复杂PDF结构 | 商业API,需要付费 |
| Docling | 企业级文档处理方案 | 企业合同、财务报告 | 模块化设计,IBM生态兼容 | 社区生态相对较小 |
| Marker | 快速PDF转换工具 | 科研文献、电子书 | GPU加速,转换速度快 | 主打PDF转Markdown,功能聚焦 |
| MinerU | 多模态集成解析专家 | 学术文献、财务报表 | 集成LayoutLMv3+YOLOv8,支持图文表混合解析 | 对硬件要求较高 |
⚠️ 选型建议:新手可以从 Unstructured 或 PyMuPDF4LLM 入手,它们开源免费、社区活跃、资料丰富。企业级应用可考虑 LlamaParse 或 Docling,精度和稳定性更有保障。
2.3 重点推荐:Unstructured —— 当前最流行的文档加载方案
在众多文档加载器中,Unstructured 是目前社区应用最广泛、生态最成熟的方案之一。它像一个“万能文件阅读器”,不管扔给它什么格式,它都能帮你把文字“掏”出来,还帮你分好类。
2.3.1 Unstructured 的核心优势
① 格式覆盖面广
支持 PDF、Word(.docx)、Excel(.xlsx)、HTML、Markdown、PPT、图片等数十种格式。一套API走天下,不用为每种格式单独写代码。
② 智能识别文档结构
它不仅能提取文字,还能自动识别哪些是标题、哪些是正文、哪些是列表、哪些是表格。这个能力对后续的“智能分块”至关重要——比如我们可以告诉系统“不要把表格和标题切在一起”。
③ 保留元数据
加载过程中,它会自动记录页码、文档来源、创建时间等信息,为后续的“溯源”打好基础。
2.3.2 Unstructured 支持的文档元素类型
Unstructured 在解析文档时,会给每一段文字打上一个“标签”,告诉系统“这段文字是什么角色”。下表列出它支持的标签类型:
| 元素类型 | 通俗解释 | 在RAG中的用途 |
|---|---|---|
| Title | 文档的标题 | 可作为知识块的“主题标签”,提升检索精度 |
| NarrativeText | 正常的正文段落 | 最主要的知识来源 |
| ListItem | 列表项(如“1. 第一步”、“- 注意事项”) | 保持列表的完整性,避免切碎 |
| Table | 表格 | 表格数据需要特殊处理(如转成Markdown表格)才能保留结构 |
| Image | 图片(记录图片的位置和描述) | 可后续接入多模态模型进行理解 |
| Formula | 数学公式 | 科研场景下需要保留LaTeX格式 |
| Header | 页眉 | 通常可忽略,避免干扰正文 |
| Footer | 页脚 | 通常可忽略,避免干扰正文 |
| PageNumber | 页码 | 用于溯源——回答时带上“详见第X页” |
| CompositeElement | 复合元素(由多个连续元素组合而成) | 分块策略的产物,保持语义完整性 |
| Address | 物理地址 | 在特定场景(如合同处理)中有用 |
| EmailAddress | 电子邮箱 | 同上 |
💡 CompositeElement 是什么? 简单说,就是当系统把“标题+正文”或“多个列表项”合并成一个整体时产生的“大块头”。这个设计是为了避免把逻辑上属于同一段的内容拆散,影响语义完整性。
2.3.3 新手入门:3分钟体验Unstructured
安装:
pip install unstructured
# 如果需要解析PDF,还需要额外安装
pip install "unstructured[pdf]"
最简示例 —— 加载PDF文件:
from unstructured.partition.pdf import partition_pdf
# 解析PDF文档,自动识别结构
elements = partition_pdf(filename="company_policy.pdf")
# 遍历并打印每个元素
for elem in elements:
print(f"类型: {elem.category}") # 看看这是标题还是正文?
print(f"内容: {elem.text[:50]}...") # 打印前50个字
print(f"页码: {elem.metadata.page_number}") # 看看它在哪一页
print("-" * 30)
输出示例:
类型: Title 内容: 第一章 总则... 页码: 1 ------------------------------ 类型: NarrativeText 内容: 为规范公司报销流程,特制定本制度... 页码: 1 ------------------------------ 类型: Table 内容: | 报销类型 | 审批人 | 额度上限 | 页码: 3 ------------------------------
进阶技巧 —— 针对不同格式使用不同加载器:
from unstructured.partition.docx import partition_docx
from unstructured.partition.html import partition_html
from unstructured.partition.pptx import partition_pptx
# Word文档
doc_elements = partition_docx(filename="report.docx")
# HTML网页
html_elements = partition_html(filename="article.html")
# PPT演示文稿
ppt_elements = partition_pptx(filename="presentation.pptx")
三、文本分块 —— 把文档“切”成知识单元
3.1 什么是文本分块?
文本分块,简单说就是把长文档切成小段。这些被切出来的小段,叫做“块”(Chunk),它们是后续向量检索和模型生成的基本单位。
💡 一句话理解:就像把一整本《百科全书》拆成一个个词条,查资料的时候只需要翻到对应的词条页,不用抱着整本书从头翻到尾。
分块示意图:
[原始长文档]
┌─────────────────────────────────────────────────────────────┐
│ 第一章 总则...(此处省略10000字)...第四章 附则 │
└─────────────────────────────────────────────────────────────┘
↓ 分块
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 块1 │ │ 块2 │ │ 块3 │ │ 块4 │
│ 第一章 │ │ 第二章 │ │ 第三章 │ │ 第四章 │
│ 总则 │ │ 组织架构 │ │ 业务流程 │ │ 附则 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
3.2 为什么文本分块如此重要?
很多新手会问:“直接把整篇文档喂给大模型不行吗?为什么要费劲切分?”
答案藏在RAG系统的两个核心组件里。
3.2.1 满足模型的“饭量”限制
RAG系统里有两个“吃饭”的组件,它们的“饭量”(上下文窗口)都是有限的:
| 组件 | 通俗理解 | 饭量限制 | 超过限制会怎样? |
|---|---|---|---|
| 嵌入模型 | 负责把文字转成向量 | 通常512个token(约400个中文字) | 超出的部分被直接“扔掉”,信息丢失 |
| 大语言模型 | 负责读资料、写答案 | 几千到上百万token不等 | 装不下这么多内容,要么报错,要么“消化不良” |
⚠️ 注意:嵌入模型的限制通常比大模型严格得多(512 vs 128K)。所以,块的大小首先要满足嵌入模型的要求,否则向量化这一步就出问题了。
3.2.2 为什么“块”不是越大越好?
假设嵌入模型最多能处理 8192 个 token,是否应该把块切得尽可能大(比如8000个token)呢?答案是否定的。块的大小并非越大越好,过大的块会严重影响RAG系统的性能。
问题一:信息被“稀释”,检索精度下降
嵌入模型的工作原理可以这样理解:
-
分词:把一段文字拆成一个个小单位(token)
-
向量化:每个token都变成一个向量
-
池化:把所有token的向量“压缩”成一个单一的向量,代表整段文字的语义
问题就出在“压缩”这一步。一个768维的向量,要概括一整段文字的所有信息。如果这段文字有8000个token,包含了好几个不同的主题,那么这个向量只能“模糊地”表示所有主题,任何一个主题的细节都被稀释了。
🎯 打个比方:就像用一张照片概括一部2小时的电影——你只能记得“这是一部动作片”,但根本记不住具体情节。
问题二:关键信息被“淹没”在中间
有研究(Liu et al., 2024)发现一个现象叫 “Lost in the Middle”(中间丢失) :当LLM处理非常长的上下文时,它倾向于更好地记住开头和结尾的信息,而忽略中间部分。
如果你给LLM塞了几个巨大的块,里面夹杂了大量无关信息,关键信息恰好被埋在中间,模型很可能会“视而不见”,导致回答质量下降,甚至产生幻觉。
问题三:主题混乱,检索失败
一个好的文本块应该聚焦于一个明确、单一的主题。如果一个块里塞了太多不相关的内容,它的语义向量就会变得“模糊”,谁都搜不准。
举个栗子🌰:
假设有一篇关于《王者荣耀》英雄鲁班七号的攻略文档,包含三个主题:
-
技能介绍
-
推荐出装
-
背景故事
❌ 糟糕的分块:把这三个主题全部塞进一个巨大的块里。
用户问:“鲁班七号怎么出装?”——这个大块虽然包含了出装信息,但被技能和故事严重“稀释”,检索时相关性得分很低,根本搜不出来。
✅ 优秀的分块:把三个主题各切一个独立块。
用户问:“鲁班七号怎么出装?”——“推荐出装”这个块精准命中,一击即中。
3.3 主流分块策略详解
LangChain 提供了丰富的文本分割器(Text Splitters),下面介绍几种最核心的策略。
3.3.1 固定大小分块 —— “一刀切”
这是最简单直接的方法:不管内容是什么,按固定的字符数(或token数)切块。
工作原理(LangChain的CharacterTextSplitter):
-
先按段落分割:默认用
\n\n(两个换行)把文本分成段落 -
再智能合并:把段落一个一个地“攒”起来,每攒到接近
chunk_size就生成一个块 -
处理超长段落:如果一个段落本身就超过
chunk_size,会发出警告但仍然保留
📝 注意:LangChain的实现并不是严格的“固定大小”,而是“段落感知的自适应分块”——它会尽量保持段落完整,只在必要时才切断。
| 优点 | 缺点 |
|---|---|
| 实现简单,速度快 | 可能在语义边界处切断文本 |
| 计算开销小 | 对长段落处理不友好 |
适用场景:日志分析、纯文本预处理、对语义完整性要求不高的场景。
3.3.2 递归字符分块 —— “层层递进”(⭐通用首选)
这是LangChain最推荐的通用分块方法。它通过一组有层次的分隔符(段落→句子→词语→字符)递归地切割文本,在满足大小限制的同时,尽可能保留语义完整性。
工作原理(重要!):
分隔符列表:["\n\n"(段落), "\n"(换行), "。"(句子), ","(逗号), " "(空格), ""(字符)]
处理逻辑:
1. 先用最高优先级的分隔符(段落)切分
├─ 如果切出来的片段都 ≤ chunk_size → 直接作为块
└─ 如果某个片段 > chunk_size → 用下一个分隔符(换行)继续切这个片段
├─ 如果切出来的子片段都 ≤ chunk_size → 作为块
└─ 如果某个子片段 > chunk_size → 用下一个分隔符(句子)继续切...
└─ ……直到分隔符用尽,保留下超长块
关键区别:遇到超长段落,固定大小分块只会“警告并保留”,而递归分块会继续用更细粒度的分隔符切分,直到满足大小要求。
💡 针对中文的优化配置:
中文没有空格分隔词语,需要额外添加中文标点作为分隔符:
separators = [
"\n\n", "\n", # 段落
"。", "!", "?", # 句子结束
",", ";", ":", # 从句
" ", "\u200b", # 空格、零宽空格
"" # 最终方案:按字符切
]
💡 针对编程代码的优化:
LangChain 提供了针对不同编程语言的预设分隔符:
from langchain.text_splitter import RecursiveCharacterTextSplitter, Language
# 针对Python代码的优化
text_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON, # 也支持 Java, C++, JavaScript 等
chunk_size=500,
chunk_overlap=50
)
| 优点 | 缺点 |
|---|---|
| 尽可能保持语义完整性 | 实现比固定大小复杂 |
| 对长文本处理效果好 | 需要合理配置分隔符列表 |
| 通用性强,适用于大多数场景 | 对特定格式(如Markdown)不是最优 |
适用场景:通用文档处理、大多数RAG项目的首选方案。
3.3.3 语义分块 —— “按意思切”
这是一种更智能的方法:在语义主题发生显著变化的地方切分,让每个块内部高度连贯。
工作原理(5步走):
-
分句:把文本拆成一个个句子
-
上下文嵌入:每个句子不是独立编码,而是带着前后N个“邻居”一起编码,让向量包含上下文信息
-
计算距离:计算每对相邻句子之间的语义距离(余弦距离)
-
找断点:用统计方法找出语义跳跃最大的位置(如“前95%的差异都在XX以下,超过这个值的就是断点”)
-
合并成块:在断点位置切开,把切出来的部分合并成块
断点识别方法(4种):
| 方法 | 通俗理解 | 适用场景 |
|---|---|---|
| percentile(百分位法) | 选最“跳脱”的5%位置作为断点 | 通用场景,默认选项 |
| standard_deviation(标准差法) | 超过“平均值+3倍标准差”的视为断点 | 差异值分布比较均匀的文本 |
| interquartile(四分位距法) | 用统计学中的IQR识别异常值 | 数据分布有离群点的场景 |
| gradient(梯度法) | 检测语义变化的“拐点” | 法律、医疗等语义平稳的文档 |
| 优点 | 缺点 |
|---|---|
| 每个块内部语义高度一致 | 计算量大,速度慢 |
| 无需预设分隔符,自适应 | 需要调用嵌入模型,有API成本 |
| 最适合长文档、复杂内容 | 对短文档效果不明显 |
适用场景:长篇文章、学术论文、法律文书等语义丰富的文档。
3.3.4 基于文档结构的分块 —— “按章节切”
对于有明确结构标记的文档(Markdown、HTML、LaTeX),可以利用这些标记实现更智能的分割。
以Markdown为例:
# 第一章 总则 ← 一级标题(Header 1) ## 1.1 制定目的 ← 二级标题(Header 2) 内容内容内容... ## 1.2 适用范围 内容内容内容... # 第二章 报销流程 ...
工作原理:
-
定义标题层级:告诉分块器
#是一级标题,##是二级标题 -
按标题分组:每个标题下的所有内容(直到下一个同级或更高级别标题)聚合为一个逻辑块
-
注入元数据:每个块都会带上“标题路径”,例如
{"Header 1": "第一章 总则", "Header 2": "1.1 制定目的"}
🎯 这个元数据太有用了! 当用户问“报销流程”时,系统不仅找到了相关段落,还知道它来自“第二章 → 2.3 报销流程”,回答时可以附带“根据《公司制度》第二章第2.3节……”,可信度瞬间拉满!
⚠️ 注意事项:按标题分割可能导致某个章节内容过长(如“第三章”有1万字),超过模型上下文限制。
解决方案:两阶段分块——先按标题分大块,再对每个大块用RecursiveCharacterTextSplitter切小块,小块会继承大块的标题元数据。
| 优点 | 缺点 |
|---|---|
| 完美保留文档逻辑结构 | 仅适用于结构化文档 |
| 元数据增强检索和溯源 | 可能产生超长块,需二次处理 |
| 提升回答的可信度 | 需要了解文档的格式规范 |
适用场景:Markdown文档、HTML网页、技术手册、维基百科等。
3.4 其他开源框架中的分块策略
3.4.1 Unstructured:先“理解”再“分割”
Unstructured采用“先理解后分割”的策略:
-
分区(Partitioning) :先把文档解析成带标签的“元素”(标题、正文、列表、表格等)
-
分块(Chunking) :基于元素进行智能组合,而不是对纯文本操作
两种核心方法:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| basic | 连续组合元素,尽量填满每个块 | 通用场景 |
| by_title | 把标题视为新章节的开始,强制在此处分块 | 报告、书籍等结构化文档 |
by_title 方法的效果类似于LangChain的MarkdownHeaderTextSplitter,但适用范围更广(不限于Markdown格式)。
3.4.2 LlamaIndex:面向“节点”的精细控制
LlamaIndex将处理单元称为“节点(Node)”,提供了丰富的解析器:
| 类型 | 代表解析器 | 核心思路 |
|---|---|---|
| 结构感知型 | MarkdownNodeParser、CodeSplitter | 根据文档结构切分 |
| 语义感知型 | SemanticSplitterNodeParser | 找语义“断点”切分 |
| 上下文窗口型 | SentenceWindowNodeParser | 存句子+前后N句的“窗口”,检索时送窗口 |
SentenceWindowNodeParser 特别值得一提:它把文档切成单句,但每个句子的元数据里存着前后相邻的N个句子。检索时,用单句的向量精准匹配,但送给LLM的是包含上下文的完整窗口——兼顾了检索精度和上下文质量。
3.4.3 ChunkViz:可视化分块工具
本章开头的那张“文本分块示意图”,就是通过 ChunkViz 生成的。它可以把你的文档和分块配置可视化,用不同颜色的色块展示每个块的边界和重叠部分,是学习和调试分块策略的好帮手。
四、分块策略速查表
| 分块方法 | 一句话定位 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|---|
| 固定大小分块 | 一刀切,简单粗暴 | 实现简单、速度快 | 可能切断语义 | 日志分析、纯文本预处理 |
| 递归字符分块 | 层层递进,优先保语义 | 平衡性好,通用性强 | 需配置分隔符 | 通用首选,大多数RAG项目 |
| 语义分块 | 按意思切,智能自适应 | 语义完整度高 | 计算量大、慢 | 长文章、学术论文 |
| 结构分块 | 按章节切,保留逻辑 | 结构完美、可溯源 | 仅限结构化文档 | Markdown/HTML文档 |
分块大小:多大最合适?
| 考量因素 | 建议 |
|---|---|
| 嵌入模型限制 | 块大小 < 嵌入模型的上下文窗口(通常512 token) |
| 检索精度 | 块越大,语义越稀释,检索越不准 |
| 推荐起步值 | 中文场景:200~500个字符(约150~400个中文字) |
| 黄金法则 | 让每个块恰好说清楚一件小事 |
给你的建议
-
不要跳过数据清洗:加装个“过滤器”往往能提升20%以上的检索效果。
-
始终保留元数据:加载时多记一笔“页码”和“来源”,后续溯源时你会感谢自己。
-
先小规模测试:别一上来就灌入10万份文档,先用10份测试,确保流程通畅再扩展。
-
分块大小要实验:没有“万能大小”,建议从300字符起步,根据检索效果调优。
五、本章小结
| 核心要点 | 一句话总结 |
|---|---|
| 数据准备很重要 | 它决定了后续所有环节的上限,“垃圾进,垃圾出” |
| 数据加载做什么 | 解析格式 → 抽取元数据 → 标准化输出 |
| 怎么选加载器 | 新手推荐Unstructured,生产环境可考虑LlamaParse |
| 分块是什么 | 把长文档切成多个小段,每个段是一个独立的知识单元 |
| 为什么分块 | 满足模型限制、提升检索精度、避免信息淹没 |
| 不是越大越好 | 大块稀释语义、检索不准、LLM“中间丢失” |
| 通用首选分块 | 递归字符分块(RecursiveCharacterTextSplitter) |
如果本文对你有帮助,欢迎点赞、收藏、转发~有任何问题,欢迎在评论区留言交流!
更多推荐


所有评论(0)