如果把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系统的性能。

问题一:信息被“稀释”,检索精度下降

嵌入模型的工作原理可以这样理解:

  1. 分词:把一段文字拆成一个个小单位(token)

  2. 向量化:每个token都变成一个向量

  3. 池化:把所有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):

  1. 先按段落分割:默认用\n\n(两个换行)把文本分成段落

  2. 再智能合并:把段落一个一个地“攒”起来,每攒到接近chunk_size就生成一个块

  3. 处理超长段落:如果一个段落本身就超过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步走):

  1. 分句:把文本拆成一个个句子

  2. 上下文嵌入:每个句子不是独立编码,而是带着前后N个“邻居”一起编码,让向量包含上下文信息

  3. 计算距离:计算每对相邻句子之间的语义距离(余弦距离)

  4. 找断点:用统计方法找出语义跳跃最大的位置(如“前95%的差异都在XX以下,超过这个值的就是断点”)

  5. 合并成块:在断点位置切开,把切出来的部分合并成块

断点识别方法(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 适用范围
内容内容内容...
# 第二章 报销流程
...

工作原理:

  1. 定义标题层级:告诉分块器 # 是一级标题,## 是二级标题

  2. 按标题分组:每个标题下的所有内容(直到下一个同级或更高级别标题)聚合为一个逻辑块

  3. 注入元数据:每个块都会带上“标题路径”,例如 {"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个中文字)
黄金法则 让每个块恰好说清楚一件小事

给你的建议

  1. 不要跳过数据清洗:加装个“过滤器”往往能提升20%以上的检索效果。

  2. 始终保留元数据:加载时多记一笔“页码”和“来源”,后续溯源时你会感谢自己。

  3. 先小规模测试:别一上来就灌入10万份文档,先用10份测试,确保流程通畅再扩展。

  4. 分块大小要实验:没有“万能大小”,建议从300字符起步,根据检索效果调优。

五、本章小结

核心要点 一句话总结
数据准备很重要 它决定了后续所有环节的上限,“垃圾进,垃圾出”
数据加载做什么 解析格式 → 抽取元数据 → 标准化输出
怎么选加载器 新手推荐Unstructured,生产环境可考虑LlamaParse
分块是什么 把长文档切成多个小段,每个段是一个独立的知识单元
为什么分块 满足模型限制、提升检索精度、避免信息淹没
不是越大越好 大块稀释语义、检索不准、LLM“中间丢失”
通用首选分块 递归字符分块(RecursiveCharacterTextSplitter)

如果本文对你有帮助,欢迎点赞、收藏、转发~有任何问题,欢迎在评论区留言交流!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐