数据加载
务必重视数据加载环节, “垃圾进,垃圾出 (Garbage In, Garbage Out)” ——高质量输入是高质量输出的前提。
文档加载器
数据加载是RAG系统中的第一步,文档加载器在 RAG 的数据管道中一般需要完成三个核心任务:
- 解析不同格式的原始文档:将 PDF、Word、Markdown 等内容提取为可处理的纯文本。
- 元数据:解析过程中同时抽取文档来源、页码、作者等关键信息作为元数据
- 把文本和元数据整理成统一的数据结构:方便后续进行切分、向量化和入库。
Unstructured文档处理库
Unstructured 是一个专业的文档处理库,专门设计用于RAG和AI微调场景的非结构化数据预处理,能识别和分类下列文档元素类型:
简单示例
# 能利用视觉模型或规则,分辨出哪句话是大标题,哪段是正文,哪里是一个表格。
from unstructured.partition.auto import partition
# PDF文件路径
pdf_path = "../../data/C2/pdf/rag.pdf"
# 使用Unstructured加载并解析PDF文档
# 通过 partition,可以精准地剥离出有用的信息。
# 返回类型:list[Element], Title、NarrativeText等都是Element的子类
elements = partition(
filename=pdf_path,
content_type="application/pdf"
)
# 打印解析结果
print(f"解析完成: {len(elements)} 个元素, {sum(len(str(e)) for e in elements)} 字符")
# 统计元素类型
from collections import Counter
types = Counter(e.category for e in elements)
print(f"元素类型: {dict(types)}")
# 显示所有元素
print("\n所有元素:")
for i, element in enumerate(elements, 1):
print(f"Element {i} ({element.category}):")
print(element)
print("=" * 60)
partition 函数参数解析:
filename: 文档文件路径,支持本地文件路径;content_type: 可选参数,指定MIME类型(如"application/pdf"),可绕过自动文件类型检测;file: 可选参数,文件对象,与 filename 二选一使用;url: 可选参数,远程文档 URL,支持直接处理网络文档;include_page_breaks: 布尔值,是否在输出中包含页面分隔符;strategy: 处理策略,可选 "auto"、"fast"、"hi_res" 等;encoding: 文本编码格式,默认自动检测。
在实际应用中,针对 pdf 的处理,目前更多选用的是 PaddleOCR、MinerU 等模型或工具。
文本分块
文本分块(Text Chunking) 的原理是将加载后的长篇文档,切分成更小、更易于处理的单元。这些被切分出的文本块,是后续向量检索和模型处理的基本单位。
上下文限制
- 嵌入模型 (Embedding Model) : 负责将文本块转换为向量。这类模型有严格的输入长度上限。因此,文本块的大小必须小于等于嵌入模型的上下文窗口。
- 大语言模型 (LLM) : 负责根据检索到的上下文生成答案。LLM同样有上下文窗口限制。检索到的所有文本块,连同用户问题和提示词,都必须能被放入这个窗口中。
嵌入模型工作流程
大多数嵌入模型都基于 Transformer 编码器。其工作流程大致如下:
- 分词 (Tokenization) : 将输入的文本块分解成一个个 token。
- 向量化 (Vectorization) : Transformer 为每个 token 生成一个高维向量表示。
- 池化 (Pooling) : 通过某种方法,将所有 token 的向量压缩成一个单一的向量,这个向量代表了整个文本块的语义。
文本块越长,包含的语义点越多,这个单一向量所承载的信息就越稀释,导致其表示变得笼统,关键细节被模糊化,从而降低了检索的精度。所以块的大小并非越大越好。
避免主题稀释导致检索失败
一个好的文本块应该聚焦于一个明确、单一的主题。如果一个块包含太多不相关的主题,它的语义就会被稀释,导致在检索时无法被精确匹配。
通过合理分块,可以有效提升检索的信噪比,确保了后续生成环节能得到最优质、最相关的上下文。
基础分块策略
固定大小分块 CharacterTextSplitter
根据LangChain源码,这种方法的工作原理分为两个主要阶段:
- 按段落分割:
CharacterTextSplitter采用默认分隔符"\n\n",使用正则表达式将文本按段落进行分割 - 智能合并:将分割后的段落依次合并。该方法会监控累积长度,当超过
chunk_size时形成新块,并通过重叠机制(chunk_overlap)保持上下文连续性,同时在必要时发出超长块的警告。
这种方法的主要优势在于实现简单、处理速度快且计算开销小。劣势在于可能会在语义边界处切断文本,影响内容的完整性和连贯性。
递归字符分块 RecursiveCharacterTextSplitter
这种分块器通过分隔符层级递归处理,相对与固定大小分块,改善了超长文本的处理效果。
- 寻找有效分隔符: 从分隔符列表中从前到后遍历,找到第一个在当前文本中存在的分隔符。如果都不存在,使用最后一个分隔符(通常是空字符串
"")。 - 切分与分类处理: 使用选定的分隔符切分文本,然后遍历所有片段:
- 片段大小不超过块:暂存到
_good_splits中 - 片段超过块大小:先将暂存的合格片段通过
_merge_splits合并成块,递归调用剩余的分隔符,继续对块进行分割;实在无法分割,保留成超大块。
- 片段大小不超过块:暂存到
固定大小分块遇到超长段落时只能发出警告并保留。,而递归分块会继续使用更细粒度的分隔符(句子→单词→字符)直到满足大小要求。
语义分块 SemanticChunker
语义分块(Semantic Chunking) 是一种更智能的方法,这种方法不依赖于固定的字符数或预设的分隔符。其核心是:在语义主题发生显著变化的地方进行切分。这使得每个分块都具有高度的内部语义一致性。
- 句子分割(Sentence Splitting):使用标准的句子分割规则(例如,基于句号、问号、感叹号)将输入文本拆分成一个句子列表。
- 上下文感知嵌入 (Context-Aware Embedding):对于列表中的每一个句子,这种方法会将其与前后各
buffer_size个句子组合起来,然后对这个临时的、更长的组合文本进行嵌入。这样,每个句子最终得到的嵌入向量就融入了其上下文的语义。 - 计算语义距离 (Distance Calculation):计算每对相邻句子的嵌入向量之间的语义差异(余弦距离)。
- 识别断点 (Breakpoint Identification):
SemanticChunker会分析所有计算出的距离值,并根据一个统计方法(默认为 percentile)来确定一个动态阈值。所有距离大于此阈值的点,都被识别为语义上的“断点”。 - 合并成块 (Merging into Chunks):根据识别出的所有断点位置,将原始的句子序列进行切分,并将每个切分后的部分内的所有句子合并起来,形成一个最终的、语义连贯的文本块。
基于文档结构的分块
对于具有明确结构标记的文档格式(如Markdown、HTML、LaTex),可以利用这些标记来实现更智能、更符合逻辑的分割。
Unstructured:基于文档元素的智能分块
- 分区 (Partitioning):将原始文档(如PDF、HTML)解析成一系列结构化的“元素”(
Element)。每个元素都带有语义标签,如Title(标题)、NarrativeText(叙述文本)、ListItem(列表项) 等。 - 分块 (Chunking):该功能建立在分区的结果之上。分块功能不是对纯文本进行操作,而是将分区产生的“元素”列表作为输入,进行智能组合。