第六篇 文档处理与数据清洗:从非结构化到结构化#
在RAG(检索增强生成)系统中,文档处理质量(ETL)直接决定了最终效果的上限。“Garbage In, Garbage Out” 是绝对真理。无论你的模型多么强大,如果喂给它的数据是破碎、混乱或含有噪声的,检索效果一定很差。
本篇不仅介绍工具的使用,更侧重于生产级文档处理方法论,对比 LangChain 和 LlamaIndex 的最佳实践,并涵盖最新的 PDF 解析技术(如 MinerU, LlamaParse)。
学习路径#
graph LR
A[ETL核心方法论] --> B[Loading<br/>多模态加载]
B --> C[Chunking<br/>智能切分]
C --> D[Metadata<br/>元数据增强]
D --> E[实战<br/>复杂PDF处理]
style A fill:#e1f5e1
style B fill:#fff4e1
style D fill:#ffe1e1
style E fill:#e1f5fePart 1: ETL 核心方法论#
在构建 LLM 应用时,我们遵循标准的 ETL (Extract, Transform, Load) 流程,但在向量数据库语境下,通常描述为:
- Load (加载): 将各种非结构化数据(PDF, HTML, MarkDown)统一为标准
Document对象。 - Split (切分): 将长文档切分为适合 Embedding 模型窗口(如 512/1024 tokens)的
Chunks。 - Embed (向量化): 将文本块转化为向量。
- Store (存储): 存入向量数据库。
关键数据结构对比:
| 概念 | LangChain | LlamaIndex | 说明 |
|---|---|---|---|
| 原始文档 | Document | Document | 包含 page_content (text) 和 metadata |
| 切分单元 | Document (chunk) | Node | LlamaIndex 的 Node 结构更丰富,包含关系信息 |
| 加载器 | BaseLoader | BaseReader | 接口命名不同,功能类似 |
Part 2: 数据加载 (Loading)#
2.1 LangChain 加载方案#
LangChain 的加载器生态非常丰富,适合处理异构数据源。
2.1.1 网页加载 (WebBaseLoader)#
最常用的网页加载器,基于 BeautifulSoup。
from langchain_community.document_loaders import WebBaseLoader
# 1. 加载单个网页
loader = WebBaseLoader("https://python.langchain.com/docs/get_started/introduction")
docs = loader.load()
print(f"Loaded {len(docs)} docs")
print(f"Content preview: {docs[0].page_content[:200]}")
print(f"Metadata: {docs[0].metadata}")
# 2. 并行加载多个网页
loader_multi = WebBaseLoader([
"https://www.google.com",
"https://www.baidu.com"
])
loader_multi.requests_per_second = 2 # 限流
docs_multi = loader_multi.aload() # 异步加载2.1.2 目录加载 (DirectoryLoader)#
适合加载本地知识库文件夹。
from langchain_community.document_loaders import DirectoryLoader
from langchain_community.document_loaders import TextLoader
# 加载指定目录下所有的 .md 文件
loader = DirectoryLoader(
'./knowledge_base',
glob="**/*.md",
loader_cls=TextLoader,
show_progress=True
)
docs = loader.load()2.2 LlamaIndex 加载方案#
LlamaIndex 的 SimpleDirectoryReader 是目前最强大的全能加载器,一行代码即可处理 PDF, Word, Excel, 图片等多种格式。
2.2.1 SimpleDirectoryReader (全能王)#
from llama_index.core import SimpleDirectoryReader
# 1. 基础用法:加载目录
reader = SimpleDirectoryReader(
input_dir="./data",
recursive=True, # 递归子目录
required_exts=[".pdf", ".docx"], # 指定后缀
filename_as_id=True # 使用文件名做ID
)
documents = reader.load_data()
print(f"Loaded {len(documents)} docs")
# 2. 自定义特定的加载器 (例如对 .pdf 使用特殊的解析逻辑)
from llama_index.readers.file import PDFReader
file_extractor = {".pdf": PDFReader()}
reader_custom = SimpleDirectoryReader(
"./data",
file_extractor=file_extractor
)2.2.2 LlamaHub (加载器生态)#
LlamaIndex 拥有世界上最大的数据加载器社区 LlamaHub。
# 例如:加载 Notion 数据
from llama_index.readers.notion import NotionPageReader
# 需要先在 Notion 申请 Integration Token
reader = NotionPageReader(integration_token="secret_xxx")
documents = reader.load_data(page_ids=["page_id_1", "page_id_2"])Part 3: 智能切分 (Chunking)#
切分策略直接影响检索的准确性。切分太碎会丢失上下文,切分太大则包含噪声。
LangChain vs LlamaIndex 切分器对比#
| 特性 | LangChain: RecursiveCharacterTextSplitter | LlamaIndex: SentenceSplitter |
|---|---|---|
| 默认行为 | 递归尝试分隔符 ["\n\n", "\n", " ", ""] | 优先按句子完整性切分,窗口滑动 |
| 优势 | 通用性强,适合代码、Markdown | 语义保留更好,适合纯文本 |
| 元数据 | 需手动维护 | 自动保留前后文关系 (Relationships) |
3.1 LangChain: 递归字符切分#
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 块大小
chunk_overlap=200, # 重叠部分,防止上下文丢失
separators=["\n\n", "\n", " ", ""] # 优先级从左到右
)
splits = text_splitter.split_documents(docs)3.2 LlamaIndex: 句子窗口切分#
LlamaIndex 的切分器通常称为 NodeParser。
from llama_index.core.node_parser import SentenceSplitter
# 默认分块器
splitter = SentenceSplitter(
chunk_size=1024,
chunk_overlap=20
)
nodes = splitter.get_nodes_from_documents(documents)
# 查看生成的 Node
print(f"生成了 {len(nodes)} 个节点")
print(nodes[0].get_content()) # 内容
print(nodes[0].relationships) # 关系(上一块、下一块的ID)3.3 高级策略:分层切分 (Hierarchical Chunking)#
对于长文档,父子索引 (Parent-Child Indexing) 是一种非常有效的策略:
- 父块: 大块(如 2048 tokens),用于给 LLM 生成答案,保留完整上下文。
- 子块: 小块(如 256 tokens),用于向量检索,提高精准度。
LlamaIndex 实现:
from llama_index.core.node_parser import HierarchicalNodeParser
node_parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128] # 三层切分
)
nodes = node_parser.get_nodes_from_documents(documents)
# 检索时,如果命中 128 的块,可以自动回溯到 2048 的父块内容Part 4: 元数据提取 (Metadata Extraction)#
单纯靠文本内容检索往往不够,我们需要结构化元数据(如标题、作者、摘要、关键词)来进行过滤(Pre-filtering)。
4.1 使用 LLM 提取元数据#
这是目前最灵活的方法。我们可以定义一个 Pydantic 模型,利用 LLM 的 Function Calling 能力提取元数据。
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAI
# 1. 定义元数据结构
class DocumentMetadata(BaseModel):
title: str = Field(..., description="The title of the document")
summary: str = Field(..., description="A one-sentence summary")
tags: list[str] = Field(..., description="Keywords for categorization")
sentiment: str = Field(..., description="Sentiment: positive, negative, or neutral")
# 2. 配置 LLM
llm = ChatOpenAI(model="gpt-4", temperature=0)
structured_llm = llm.with_structured_output(DocumentMetadata)
# 3. 提取函数
def extract_metadata(text_chunk):
return structured_llm.invoke(text_chunk)
# 测试
sample_text = """
LlamaIndex v0.10 发布了!这次更新带来了重大的架构变革,
将库拆分为 llama-index-core 和各种插件包。用户现在可以按需安装依赖,
大大减小了包体积。社区对此反应热烈。
"""
metadata = extract_metadata(sample_text)
print(metadata.json(indent=2))
# 输出:
# {
# "title": "LlamaIndex v0.10 Release",
# "summary": "LlamaIndex v0.10 introduces architectural changes splitting the core library from plugins for optimized dependency management.",
# "tags": ["LlamaIndex", "Release", "Python", "Architecture"],
# "sentiment": "positive"
# }4.2 LlamaIndex 自动化 Pipeline#
LlamaIndex 提供了 IngestionPipeline 来串联提取过程。
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.extractors import TitleExtractor, SummaryExtractor
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=1024, chunk_overlap=200),
TitleExtractor(nodes=5), #利用前5个节点生成标题
SummaryExtractor(summaries=["prev", "self", "next"]), # 生成摘要
]
)
nodes = pipeline.run(documents=documents)
# nodes[0].metadata 现在会自动包含 'document_title', 'section_summary' 等字段Part 5: 复杂文档实战 (PDF & Tables)#
真实世界的文档(尤其是 PDF)充满了挑战:多栏布局、表格、公式、图片。简单的 PyPDF 往往无法胜任。
5.1 解决方案对比矩阵#
| 方案 | 核心技术 | 优势 | 劣势 | 推荐场景 |
|---|---|---|---|---|
| LlamaParse | LLM Vision | SOTA 表格/布局识别,Markdown 输出 | 付费,云端处理 | 复杂表格、图文混排商业文档 |
| MinerU | Deep Learning | 开源免费,公式识别极强 (LaTeX) | 需 GPU,部署较重 | 学术论文、课本、公式密集型 |
| PyMuPDF | Rule-based | 极快,免费 | 复杂布局/表格无法处理 | 纯文本 PDF,简单发票 |
| Unstructured | Hybrid | 格式支持最全 | 速度较慢,依赖系统库多 | 格式杂乱的文档堆 |
5.2 LlamaParse 实战 (推荐)#
LlamaParse 是专为 RAG 设计的解析器,它直接将 PDF 转换为 Markdown,完美保留标题层级和表格结构。
# pip install llama-parse
import os
from llama_parse import LlamaParse
from llama_index.core import SimpleDirectoryReader
# 配置 API Key (https://cloud.llamaindex.ai/)
os.environ["LLAMA_CLOUD_API_KEY"] = "llx-..."
# 1. 配置解析器
parser = LlamaParse(
result_type="markdown", # 输出 Markdown
verbose=True,
language="zh", # 支持中文
gpt4o_mode=True # 开启高级多模态识别 (消耗更多 credit)
)
# 2. 结合 SimpleDirectoryReader 使用
file_extractor = {".pdf": parser}
reader = SimpleDirectoryReader(
"./complex_docs",
file_extractor=file_extractor
)
documents = reader.load_data()
# 3. 检查结果
print(documents[0].text[:500])
# 你会发现表格被转换为了 Markdown Table 格式:
# | 季度 | 营收 | 利润 |
# |-----|------|-----|
# | Q1 | 100 | 20 |5.3 MinerU 实战 (学术/公式场景)#
如果你处理的是包含大量数学公式的论文,MinerU (Magic-PDF) 是目前开源界的 SOTA。
注:MinerU 需要独立部署,以下为 Python 调用示例。
# 假设已在本地安装 magic-pdf
# pip install magic-pdf[full]
import os
# MinerU 通常通过命令行使用,但也可以封装为 Python 函数
def process_pdf_with_mineru(pdf_path):
output_dir = os.path.dirname(pdf_path)
# 调用 CLI (实际生产环境建议使用 API 服务模式)
os.system(f"magic-pdf -p {pdf_path} -o {output_dir}")
# MinerU 会生成 .md 文件
md_path = pdf_path.replace(".pdf", ".md")
if os.path.exists(md_path):
with open(md_path, "r") as f:
return f.read()
return None
# 读取后的 Markdown 内容可以直接喂给 LangChain/LlamaIndex
markdown_content = process_process_pdf_with_mineru("./paper.pdf")5.4 图片与图表处理 (Multimodal RAG)#
对于 PDF 中的图片,通常有两种策略:
- OCR 转文本: 使用 GPT-4o-vision 描述图片内容,存为文本。
- 多模态索引: 将图片直接作为 Embedding 存入(如 CLIP Embedding),支持文搜图。
策略 1 代码示例 (使用 GPT-4o 生成图片描述):
# 这是一个概念性示例
def process_image(image_bytes):
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
chat = ChatOpenAI(model="gpt-4o")
msg = HumanMessage(content=[
{"type": "text", "text": "请详细描述这张图片中的图表数据或关键信息。"},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
])
response = chat.invoke([msg])
return response.content总结#
文档处理是 RAG 系统中最 “脏” 但最 “重要” 的环节。
- Loading: 首选 LlamaIndex SimpleDirectoryReader,简单强大。
- Parsing: 复杂 PDF 首选 LlamaParse (商业) 或 MinerU (开源)。
- Chunking: 文本用 SentenceSplitter,代码用 RecursiveCharacterTextSplitter。
- Metadata: 务必提取 标题 和 摘要,这对检索排序至关重要。
做好 ETL,你的 RAG 系统就已经成功了一半。