1.数据加载
(1)在 RAG 系统中,数据加载器(Data Loader)处于最前端,主要完成以下三个核心任务:
-
内容提取 :将 PDF、Word、Markdown、HTML 等不同格式的原始文档转换为可处理的纯文本。
-
元数据抽取:在解析时同步提取文档的来源、页码、标题、作者、创建时间等关键信息。
-
结构化整理:将文本与元数据封装成统一的数据结构(如 Document 对象),便于后续的切分、向量化和入库。
(2)为什么选择 Unstructured 库?
-
统一接口:提供了统一的接口来处理多种文档格式,无需为每种格式编写不同的解析代码。
-
结构识别:能够自动识别文档中的标题(Title)、正文(NarrativeText)、列表(ListItem)、表格(Table)等元素。
-
元数据保留:在提取内容的同时,保留了元素在原文档中的层级和属性。
(3)数据加载的关键流程(以partition_pdf为例)
python
from unstructured.partition.pdf import partition_pdf
# PDF文件路径
pdf_path = "../../data/C2/pdf/rag.pdf"
# 使用Unstructured加载并解析PDF文档
# 识别 PDF 里的段落边界、标题、表格
elements = partition_pdf(
filename=pdf_path,
content_type="application/pdf",
strategy="hi_res",
languages=["chi_sim", "eng"]
)
-
格式检测:识别文件类型。
-
策略选择:
fast(快速):基于规则提取,速度快但对复杂格式识别准度一般。hi_res(高分辨率):利用深度学习模型(如 LayoutParser)识别文档布局,适合处理复杂 PDF 或带有表格、图片的文档。ocr_only:识别扫描件。
-
元素组装:将解析出的块转换成标准的 Element 对象列表。
(4)与主流框架的集成
-
LangChain :提供了
UnstructuredFileLoader等封装,方便直接输出 LangChain 的Document对象。 -
LlamaIndex :通过
UnstructuredReader将解析结果对接到数据索引中。
(5)总结
-
数据加载 ≠ 文本读取:高质量的 RAG 依赖于对文档物理结构(如标题、段落、表格)的深度理解。
-
元数据的价值:元数据不仅用于溯源,在后续的"过滤检索"和"混合搜索"中起着至关重要的作用。
-
工具选择 :对于简单文本可使用标准库,但对于格式复杂、包含表格的非结构化文档,Unstructured 是目前最强大的开源解决方案之一。
2.文本分块
(1)为什么要进行文本切分?
-
模型限制:Embedding 模型和 LLM 都有最大 Token 限制(如 512 或 8k),无法一次性处理超长文档。
-
检索粒度:切分得太粗(如整章),检索结果噪声多;切分得太细(如一句话),会丢失上下文背景。
-
语义完整性:理想的切分应当让每一个 Chunk(块)都表达一个相对独立的语义含义。
(2)size与overlap
-
Chunk Size(块大小) :每个文本块包含的字符数或 Token 数。
-
Chunk Overlap(块重叠) :相邻两个块之间重复的内容部分。
- 作用:为了防止语义在切分点被强行打断。重叠部分能保证前一个块的结尾和后一个块的开头在上下文上是连贯的。
(3)常见的切分策略
-
基于字符的递归切分(Recursive Character Splitting) :
- 工具 :LangChain 的
RecursiveCharacterTextSplitter。 - 逻辑 :按照一个层级列表(如
["\n\n", "\n", " ", ""])递归尝试切分。它会先尝试按段落切,如果段落太大再按行切,以此类推。 - 优点 :这是目前最推荐的通用方案,能尽可能保持段落和句子的完整性。
- 工具 :LangChain 的
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
loader = TextLoader("../../data/C2/txt/蜂医.txt", encoding="utf-8")
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
# 针对中英文混合文本,定义一个更全面的分隔符列表
separators=["\n\n", "\n", "。", ",", " ", ""], # 按顺序尝试分割
chunk_size=200,
chunk_overlap=10
)
chunks = text_splitter.split_documents(docs)
-
基于 Token 固定大小的切分:
- 逻辑:直接根据模型能处理的 Token 数量进行切分。
- 适用场景:当你需要严格确保不超出 Embedding 模型的输入限制时使用。
-
特定结构切分(Structure-aware) :
- Markdown 切分:根据标题层级(# ## ###)切分。
- 代码切分:根据编程语言的函数、类定义进行切分。
- HTML 切分 :根据标签结构(如
<div>,<p>)切分。
-
语义切分(Semantic Chunking) ------ 进阶方案:
- 逻辑:利用 Embedding 模型计算句子间的相似度,当相邻句子的语义差异超过设定阈值时,才进行切分。
- 优点:完全基于内容意义进行边界判定,不依赖硬性的字符数。
python
from langchain_experimental.text_splitter import SemanticChunker
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.document_loaders import TextLoader
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
# 初始化 SemanticChunker
text_splitter = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile" # 也可以是 "standard_deviation", "interquartile", "gradient"
)
loader = TextLoader("../../data/C2/txt/蜂医.txt", encoding="utf-8")
documents = loader.load()
(4)如何选择策略
-
根据文档类型:短文(如社交媒体)切分要细;长文(如论文)切分可以稍大,并辅以较大的 Overlap。
-
根据问答任务:
- 如果问题通常针对细节(如"某参数是多少?"),Chunk Size 建议小一点。
- 如果问题涉及总结或跨度较大的逻辑,Chunk Size 需要大一点。
-
观察召回率:通过调优 Size 和 Overlap 的数值,在测试集上观察检索召回率(Recall)。
(5)总结
-
切分是平衡的艺术:在"上下文完整性"和"检索精确度"之间寻找平衡。
-
首选递归切分 :在没有特殊需求时,
RecursiveCharacterTextSplitter是最佳出发点。 -
不要忽略重叠度:Overlap 是保留跨块语义的连通性,通常设置为 Chunk Size 的 10%-20%。
参考文章 DataWhale All-in-RAG | 大模型应用开发实战一:RAG技术全栈指南