Appearance
三、文档加载
3.1 为什么要加载文档
RAG的核心是"先检索,再生成"。但检索的前提是——系统里得先有数据。企业的知识散落在各种格式的文件中:TXT、Markdown、Word、PDF、CSV、网页……这些原始文件格式各异,大模型无法直接使用。
因此,RAG的第一步就是把这些异构数据统一加载为标准格式,为后续的切分、嵌入、检索做好准备。LangChain实现和集成了众多文档加载器,方便从不同格式的文件中加载数据。可在 LangChain Document Loaders 查看所有集成的加载器。
3.2 Document 对象
LangChain将所有加载后的文档统一抽象为 Document 对象,不论原始文件是PDF还是网页,加载后都变成同一种结构:
元数据:
| 属性 | 说明 |
|---|---|
| page_content | 文本内容字符串 |
| metadata | 包含元数据的字典,如文档来源、页码等 |
| id | 可选,文档标识符 |
所有文档加载器都实现了 BaseLoader 接口,提供 load()(一次性加载)和 lazy_load()(惰性加载,适合大文件)两种方法。
3.3 加载器类型概览

3.4 加载 Markdown
Markdown 是一种半结构化、机器可读的文本标记格式,通过特定语法标记出标题、段落、有序列表、无序列表等结构信息。不同层级的文本在 Markdown 中有明确且统一的表示方式(如 #、##、- 等),解析库可以稳定地识别并利用这些结构。
可以使用 Unstructured 文档加载器来加载 Markdown 文件。Unstructured.io 对 Markdown 的解析流程大致为:
- 按照 Markdown 语法结构进行切分:标题、列表等会被切分成单独的 element;
- 对同一标题下的正文,再按段落进行切分:不同段落(通过空行或
\n区分)会被拆成多个 element。
python
# uv add markdown langchain-community "unstructured[md]"
from langchain_community.document_loaders import UnstructuredMarkdownLoader
def markdown_loader_demo(file_path: str):
"""
使用 UnstructuredMarkdownLoader 加载 Markdown 文件,
并按 elements 模式打印切分后的内容。
"""
loader = UnstructuredMarkdownLoader(
file_path,
encoding="utf-8",
mode="elements", # elements:按标题、段落等元素切分
)
docs = loader.load()
for i, doc in enumerate(docs):
print(f"=== Element {i} ===")
print(doc.page_content)
print("metadata:", doc.metadata)
print("============\n")
markdown_loader_demo("./assets/sample.md")3.5 加载Docx
现代 Word 文档(.docx 格式)本质上是一种半结构化、机器可读的文件格式。.docx 实际上是一个以 XML 为核心的压缩容器,内部的 XML 标签(例如 <w:p> 表示段落、<w:r> 表示文本运行块)严格规定了文档在物理层面的结构,解析库可以依靠这些标签精确提取段落、文本块等信息。
但与 Markdown 不同的是,Word 对“标题层级”的语义约束较弱:用户可以通过修改字体大小、加粗、颜色等方式“手动做出一个标题样式”,却不一定使用 Word 内置的“标题 1 / 标题 2”等样式。对于解析库来说,这些只是普通段落的格式变化,难以在不同文档之间用统一标准还原出清晰的标题层级结构,这也是 Word 解析的主要难点之一。
使用 Unstructured.io 的 Loader 解析 .docx 时,目前通常只会按照换行对文件进行切分成多个 element,不能可靠地区分“正文段落”和“各级标题”,因此会丢失语义化的标题层级信息:
python
# 依赖安装(在终端中执行一次即可,而不是在 Python 里执行)
# uv add langchain-community "unstructured[docx]"
from pathlib import Path
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
def word_loader_demo(file_path: str, start: int = 0, end: int = 20):
"""
使用 UnstructuredWordDocumentLoader 加载 Word 文档(.docx),
并打印指定区间的元素内容和元数据。
:param file_path: .docx 文件路径
:param start: 打印的起始索引
:param end: 打印的结束索引(不含)
"""
file_path = Path(file_path)
if not file_path.exists():
raise FileNotFoundError(f"文件不存在:{file_path.resolve()}")
loader = UnstructuredWordDocumentLoader(
file_path=str(file_path),
mode="elements",
)
docs = loader.load()
print(f"总共解析得到 {len(docs)} 个元素\n")
end = min(end, len(docs))
for i, doc in enumerate(docs[start:end], start=start):
print(f"=== Element {i} ===")
print(doc.page_content)
print("metadata:", doc.metadata)
print("============\n")
word_loader_demo("assets/sample.docx")对于标题层级信息敏感的.docx文件,上面的方式会丢失标题层级信息,此时可通过MinerU进行处理。
非结构化、半结构化、结构化的对比:
| 类型 | 示例 | 结构化程度 | 机器可读性 | 结构特点理解成一句话 |
|---|---|---|---|---|
| 纯文本 | .txt 随便写一篇文章 | 非结构化(几乎没标记) | 低(要靠规则/模型猜) | 一堆字堆在一起,哪里是标题全靠人眼看 |
| Markdown | .md 文档 | 半结构化 | 高(规则简单固定) | 用 # - * 等符号明显标出标题、列表、段落 |
| Word .docx | .docx 文档 | 半结构化 | 很高(XML 标签很细) | XML 里标出段落、文字块等,但“是不是标题”不统一 |
| 数据库 | SQL 表、Excel 严格表头 | 高度结构化 | 极高 | 列名、类型、每行含义都固定,机器最容易处理 |
3.6 加载 PDF
PDF存在多种来源格式(扫描版、电子文本版、混合版),多种布局格式(单列、双列、竖排),并包含段落、标题、页眉页脚、表格、数学公式、图片等各种元素。因此PDF解析存在很多挑战,对于复杂PDF,需要进行布局检测、文本提取、表格解析、公式识别等处理。
3.6.1 MinerU 介绍
MinerU 是一款将 PDF 转化为机器可读格式(如 Markdown、JSON)的工具,便于后续按任意结构进行抽取和处理。 为了从复杂版面中更准确地识别出标题、段落、图表等结构,MinerU 支持配置使用 VLM(Vision-Language Model,视觉语言模型)进行文档解析;其开源的 MinerU2.5 模型在多项文档理解基准测试中均达到 SOTA 水平。
MinerU2.5 采用两阶段解析策略:先在下采样图像上做高效全局布局分析,再在原始分辨率裁剪图像上对文本、公式、表格等进行细粒度识别 。
3.6.2 MinerU两阶段流程
两阶段解析策略流程:先全局再局部,先找块再看字。通过把"找结构"和"读内容"拆成两步,MinerU2.5 在保证解析精度的同时,显著降低了计算成本。

| 阶段 | 使用分辨率 | 主要任务 | 主要优势 |
|---|---|---|---|
| 阶段 1 | 低(下采样) | 全局版面分析:在缩小后的整页“缩略图”上识别各区域的类型与大致位置(标题、正文、表格、图片、公式等) | 计算量小、速度快,适合做全局结构 |
| 阶段 2 | 高(原始) | 局部内容识别:在原始分辨率上对已定位区域做 OCR、表格结构解析、公式识别等 | 识别精度高,只对局部区域精细计算 |
MinerU 支持对 PDF、Word、PPT、图片等进行解析,涵盖图像提取、OCR、公式和表格解析等功能。项目已完全开源,支持本地 Docker 部署(仓库地址:https://github.com/opendatalab/MinerU),也可通过官网 API 调用服务,使用前需申请 API_KEY 并配置到环境变量。
3.6.3 使用MinerU API上传文件
Miner解析(word)(pdf)---->MD文档中都是一级标题结构(下载mineru到本地 修改Minueru.json文件 修改模型辅助标题识别配置)
加载在线pdf
python
import requests
token = os.getenv('MINERU_TOKEN')
url = "https://mineru.net/api/v4/extract/task"
header = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
data = {
"url": "https://cdn-mineru.openxlab.org.cn/demo/example.pdf",
"model_version": "vlm"
}
res = requests.post(url,headers=header,json=data)
print(res.status_code)
print(res.json())
print(res.json()["data"])获取解析结果:
python
import requests
task_id = "62bc54b0-0369-4150-9d07-5dc8d3d281ed"
url = f"https://mineru.net/api/v4/extract/task/{task_id}"
header = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
res = requests.get(url, headers=header)
print(res.status_code)
print(res.json())
print(res.json()["data"])加载本地pdf
python
def mineru_upload_file_demo():
import requests
from pathlib import Path
if not token:
raise RuntimeError("环境变量 MINERU_TOKEN 未设置")
# 1. 申请上传 URL
url = "https://mineru.net/api/v4/file-urls/batch"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
}
# 本地文件路径列表(可扩展为多文件)
file_paths = [
r"assets\尚硅谷大模型技术之NLP1.0.2.pdf"
]
files_info = []
for i, p in enumerate(file_paths):
p = Path(p)
if not p.exists():
raise FileNotFoundError(f"文件不存在: {p}")
files_info.append({
"name": p.name,
"data_id": f"data_{i}", # 自己给每个文件一个 data_id 标识
})
data = {
"files":files_info,
"model_version": "vlm",
}
resp = requests.post(url, headers=headers, json=data)
if resp.status_code != 200:
print(f"申请上传 URL 失败,状态码:{resp.status_code},响应内容:{resp.text}")
return None
result = resp.json()
if result.get("code") != 0:
print(f"申请上传 URL 失败,reason: {result.get('msg')}")
return None
print(result)
batch_id = result["data"]["batch_id"]
urls = result["data"]["file_urls"]
print(f"申请上传 URL 成功,batch_id: {batch_id}")
print("file_urls:", urls)
# 2. 逐个上传文件到对应的临时 URL
for i, upload_url in enumerate(urls):
path = Path(file_paths[i])
with path.open("rb") as f:
res_upload = requests.put(upload_url, data=f)
if res_upload.status_code == 200:
print(f"{path.name} 上传成功")
else:
print(f"{path.name} 上传失败, 状态码: {res_upload.status_code}, 响应: {res_upload.text}")
return batch_id, files_info
mineru_upload_file_demo()获取解析结果
python
def mineru_check_result_demo(batch_id: str, max_retries: int = 30, interval: int = 3):
"""
轮询 MinerU 批量解析结果,直到任务完成或超过重试次数。
:param batch_id: 调用 /extract/task 时返回的 batch_id
:param max_retries: 最大轮询次数
:param interval: 每次轮询间隔秒数
"""
import os
import time
import requests
token = os.getenv("MINERU_TOKEN")
if not token:
raise RuntimeError("环境变量 MINERU_TOKEN 未设置")
url = f"https://mineru.net/api/v4/extract-results/batch/{batch_id}"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
}
for i in range(max_retries):
resp = requests.get(url, headers=headers)
if resp.status_code != 200:
print(f"请求失败,状态码:{resp.status_code},响应内容:{resp.text}")
return None
data = resp.json()
# 结构示例:data["data"]["extract_result"] 是列表
extract_list = data.get("data", {}).get("extract_result", [])
if not extract_list:
print("返回结果中没有 extract_result 字段:", data)
return None
item = extract_list[0]
state = item.get("state")
print(f"第 {i+1} 次查询,当前状态:{state}")
if state == "done":
full_zip_url = item.get("full_zip_url")
print("任务已完成,结果 zip 下载地址:", full_zip_url)
return full_zip_url
elif state in ("failed", "error"):
print("任务失败,返回信息:", item)
return None
# 还未完成,等待后重试
time.sleep(interval)
print(f"超过最大重试次数({max_retries}),任务仍未完成,请稍后重试或在控制台查看状态。")
return None
# 示例调用(替换为你自己的 batch_id)
mineru_check_result_demo("c9066450-dca3-4396-bdb3-55d0dac64ed2")MinerU解析后有多种输出格式,最简单的后续处理方式是通过解析后得到的Markdown文件,再利用Markdown解析器进一步解析。
文档加载完成后,我们得到了完整的Document对象。但一整篇文档往往太长,无法直接用于检索——下一步需要将其切分为更小的片段。