系列定位 :本文属于「RAG 七层框架」系列的第 3 层(L3 · 文档理解与解析)上篇,聚焦通用文本与结构化文本的导入解析。下篇将覆盖图文/PDF/表格。
本文目标读者:正在搭建 RAG 系统的工程师,尤其是需要把「一堆格式不同的原始文档」变成「可检索的 Document 对象」的落地场景。
一、先说结论:为什么「导入」是 RAG 的第一道分水岭
很多人以为 RAG 的难点在检索、在生成,但真正决定系统上限的,往往是最不起眼的第一步------数据导入。原因很简单:
检索质量的上限,在数据进入索引之前就已经被决定了。
如果你把一份 PDF 用最粗暴的方式读出来,得到的是「一坨没有结构的纯文本」,那么后续无论你把分块做得多精细、向量模型选得多强,都救不回丢失的结构信息。反过来,如果导入阶段就保留了标题层级、段落关系、表格结构、父子元素链接,那 L4 分块和 L7 检索就有了坚实的输入基础。
这正是本文要讲透的核心命题:不同的 Loader,解析出来的结果完全不同。选对 Loader,等于给整个 RAG 系统打好了地基。
1.1 整体原则:先「读进来」,再「读出结构」
数据导入这件事,可以拆成两个动作:
- 读进来 :把各种格式的文件(txt / JSON / 网页 / Markdown / 图片 / PPT / PDF / 表格)变成统一的
Document对象。 - 读出结构:在读取的同时,尽可能保留文档的内在结构(标题、段落、表格、元素关系),而不是只给一坨文本。
这两步在工具层面往往被耦合在一起------很多 Loader 在「读进来」的同时就已经「读出结构」了。这也是为什么选型时不能只看「能不能读」,还要看「读成什么样」。
1.2 核心数据结构:LangChain 的 Document 对象
整个 LangChain 生态的数据导入,最终都收敛到一个统一的数据结构------Document。它只有两个核心字段,但这两个字段是理解后续所有环节的钥匙:
python
from langchain_core.documents import Document
doc = Document(
page_content="《民法典》婚姻家庭编共分五章......", # 实际文本内容
metadata={"source": "data/民法典/婚姻家庭编.txt", "doc_type": "法律法规", "date": "2021-01-01"}
)
| 字段 | 作用 | 在 RAG 中的价值 |
|---|---|---|
page_content |
保存实际文本内容,是文档的主要数据部分 | 这是后续被分块、被嵌入、被检索的主体 |
metadata |
存储文档相关元信息:来源路径、作者、日期、页码、标题层级等 | 这是后续过滤检索范围、溯源、展示 的关键 |
为什么 metadata 很重要? 因为元数据虽然不参与语义匹配,但它决定了检索的「范围控制」和「结果可信度」。举个例子:
- 在法律检索场景,
metadata里可以存「法规效力层级」「颁布年份」「章节条款号」,这样检索时就能先按「效力层级」过滤,再算相似度。 - 在企业知识库场景,
metadata里可以存「部门」「文档类型」「更新时间」,这样就能限定「只检索某个部门 2024 年之后的文档」。
一句话:page_content 决定「检索到」什么,metadata 决定「能不能精准检索到、检索到后能不能讲清楚来源」。
二、通用文本读取:从单文件到整个目录
这是最基础的场景,但也是最容易踩坑的。我们先从最简单的 txt 开始。
2.1 用 LangChain 读入单个 txt 文件
python
from langchain_community.document_loaders import TextLoader
loader = TextLoader("data/民法典/婚姻家庭编.txt")
docs = loader.load()
print(docs[0].page_content)
print(docs[0].metadata)
输出就是上面看到的 Document 对象。TextLoader 是最朴素的加载器,只负责把文本读进来,不做任何结构解析。它适合纯文本、无结构的场景。
2.2 读取目录中的所有文档
当文档散落在一个目录里,逐个加载显然不现实。LangChain 提供了批量读取的能力:
python
from langchain_community.document_loaders import DirectoryLoader
loader = DirectoryLoader(
"data/民法典/",
glob="**/*.txt", # 匹配所有 txt 文件
loader_cls=TextLoader, # 指定用哪种 Loader
show_progress=True # 显示加载进度
)
docs = loader.load()
DirectoryLoader 的关键参数:
| 参数 | 作用 | 建议 |
|---|---|---|
glob |
用通配符匹配文件名 | **/*.txt 递归匹配所有 txt;**/*.md 匹配 Markdown |
loader_cls |
指定用哪个 Loader 来解析每个文件 | 必须和文件类型匹配,否则解析结果错乱 |
show_progress |
显示加载进度 | 文件多时建议开启,方便观察 |
注意 :DirectoryLoader 本身不识别文件类型,它只是「批量调用你指定的 Loader」。所以如果你的目录里混着 txt、md、PDF,需要写一个根据扩展名分发 Loader 的逻辑,或者直接用 UnstructuredLoader(它自带了多格式识别能力,后面会讲)。
2.3 不同 Loader 解析结果不同的直观对比
这是整个数据导入最核心的认知,也是选型的起点。同一个内容,用不同 Loader 解析,结果天差地别:
| Loader | 解析结果 | 适用场景 |
|---|---|---|
TextLoader |
整个文件变成一个大 Document,无结构 | 纯文本、无结构 |
UnstructuredLoader |
按元素拆成多个 Document ,带 category(Title/ListItem/NarrativeText/Table...) |
需要保留结构的文档 |
UnstructuredMarkdownLoader(mode="elements") |
按 Markdown 结构拆成多个元素 ,带 category_depth(标题层级) |
Markdown、需要标题层级 |
关键洞察 :TextLoader 是「读进来」,Unstructured 系列是「读出结构」。后者多出来的 metadata["category"] 和 metadata["element_id"],正是 L4 分块和 L7 检索能「按结构处理」的前提。
三、结构化文本解析:JSON / 网页 / Markdown
这一节是本文的重头戏。结构化文本比纯文本复杂得多,因为它们自带「层级」「字段」「关系」,解析的好坏直接决定后续检索的精细程度。
3.1 JSONLoader:用 jq_schema 精确提取字段
JSON 文件最大的特点是有明确的字段结构 。如果直接用通用 Loader,会把整个 JSON 当成一坨文本读进来,丢失字段语义。JSONLoader 的杀手锏是 jq_schema------用 jq 语法精确地「挑出」你想要的字段。
python
from langchain_community.document_loaders import JSONLoader
# 1. 提取一条法条的信息(单条记录)
main_loader = JSONLoader(
file_path="data/法律/法律条文结构.json",
jq_schema='.mainArticle | "条款:" + .article + ",内容:" + .content',
text_content=True
)
main_article = main_loader.load()
print(main_article)
# 输出:[Document(metadata={'source': '...', 'seq_num': 1},
# page_content='条款:第一千零七十七条,内容:离婚冷静期......')]
# 2. 提取所有关联条款(数组遍历)
support_loader = JSONLoader(
file_path="data/法律/法律条文结构.json",
jq_schema='.relatedArticles[] | "条款:" + .article + ",要点:" + .summary',
text_content=True
)
related_articles = support_loader.load()
print(related_articles)
# 输出:3 个 Document,seq_num 分别为 1、2、3
jq_schema 是 JSONLoader 的灵魂,几个常用写法:
| jq_schema 写法 | 含义 |
|---|---|
.mainArticle |
取整个 mainArticle 对象 |
.mainArticle.article |
只取条款号字段 |
.relatedArticles[] |
遍历 relatedArticles 数组 |
| `.relatedArticles[] | "条款:" + .article` |
| `.mainArticle | "条款:" + .article + ",内容:" + .content` |
text_content=True 的作用:把提取出来的值强制转成字符串文本。如果不加,有些值(如数字、布尔)可能无法正确转成可嵌入的文本。
为什么 jq_schema 重要? 因为「挑字段」这个动作,本质上是在做数据清洗。你可以:
- 只挑与业务相关的字段(比如法律条文里的「效力层级」「条款内容」),过滤掉无关字段(比如内部 ID、时间戳)。
- 把多个字段拼接成一句语义完整的文本(比如把「姓名 + 背景」拼成一句话),这样嵌入时是语义连贯的,而不是碎片化的。
对企业落地的启发 :如果你的数据源是 JSON(比如从业务系统导出的 API 数据、配置数据),用 jq_schema 精确挑字段,比把整个 JSON 丢进去要好得多------既能控制索引体积,又能保证语义完整性。
3.2 网页解析:五种 Loader 的选型
网页是 RAG 最常见的输入源之一,但也是最「脏」的------有导航栏、广告、脚本、样式,还有复杂的嵌套结构。LangChain 提供了五种网页加载器,各有侧重。
| 加载器 | 底层实现 | 特点 | 适用场景 |
|---|---|---|---|
WebBaseLoader |
urllib + BeautifulSoup | 简单易用,适合基本网页内容抓取 | 单页、结构简单的网页 |
UnstructuredLoader |
Unstructured 解析 | 支持多种复杂网页结构,适合异构内容 | 内容复杂、需要保留结构的网页 |
RecursiveURLLoader |
递归抓取子链接 | 自动化链接抓取 | 大规模站点抓取 |
SitemapLoader |
从站点地图抓取 | 高效解析站点结构 | 有 sitemap 的站点 |
FirecrawlLoader |
可本地部署的 API 服务 | 灵活可扩展,托管版有免费额度 | 需要实时抓取和转换 |
3.2.1 WebBaseLoader:最朴素的网页抓取
python
import bs4
from langchain_community.document_loaders import WebBaseLoader
page_url = "https://example.com/wiki/中华人民共和国民法典"
loader = WebBaseLoader(web_paths=[page_url])
docs = loader.load()
doc = docs[0]
print(doc.metadata)
# {'source': 'https://example.com/wiki/中华人民共和国民法典',
# 'title': '中华人民共和国民法典 - 示例文档', 'language': 'zh'}
print(doc.page_content.strip())
WebBaseLoader 的坑 :它用 BeautifulSoup 把 HTML 里的 <script>、<style> 等标签剥掉,但不会做内容净化 。从上面的输出可以看到,page_content 里残留了大量 JS 代码(RLCONF={"wgBreakFrames":false...})。这些垃圾内容如果被嵌入,会严重污染向量空间。
解决办法 :用 WebBaseLoader 的 bs_kwargs 参数自定义 BeautifulSoup 的解析规则,或者干脆换成 UnstructuredLoader。
3.2.2 UnstructuredLoader:保留网页结构
python
from langchain_unstructured import UnstructuredLoader
page_url = "https://example.com/wiki/中华人民共和国民法典"
loader = UnstructuredLoader(web_url=page_url)
docs = loader.load()
for doc in docs[:5]:
print(f'{doc.metadata["category"]}: {doc.page_content}')
# 输出:
# Title: 中华人民共和国民法典
# ListItem: العربية
# ListItem: مصرى
# ListItem: Azərbaycanca
关键区别 :UnstructuredLoader 解析网页时,会给每个元素打上 category 标签(Title、ListItem、NarrativeText、Table 等)。这意味着网页被拆成了有语义的多个元素,而不是一整坨文本。
对企业落地的启发 :抓取官网、文档站、新闻页时,用 UnstructuredLoader 能保留标题和正文的层级,这样 L4 分块时就能「按标题分块」,而不是「按字数硬切」。
3.2.3 网页解析的进阶:递归抓取与站点地图
RecursiveURLLoader:从根 URL 开始,递归抓取所有子链接。适合知识库网站、文档中心这种「一个入口、很多子页面」的场景。SitemapLoader:直接读sitemap.xml,按站点地图抓取。效率高,但要求站点有 sitemap。FirecrawlLoader:一个可本地部署的 API 服务,能把网页转成干净的 Markdown。适合对抓取质量要求高、需要实时转换的场景。
3.3 Markdown 解析:保留标题层级的关键
Markdown 自带标题层级(#、##、###),这是 RAG 里最宝贵的结构信息。但用朴素 Loader 读 Markdown,会把整个文件变成一个大文本,标题层级就丢了。
3.3.1 默认模式:整个文件一个 Document
python
from langchain_community.document_loaders import UnstructuredMarkdownLoader
markdown_path = "data/民法典/婚姻家庭编.md"
loader = UnstructuredMarkdownLoader(markdown_path)
data = loader.load()
print(data[0].page_content)
# 输出:整个 Markdown 的内容,标题层级全部丢失
3.3.2 elements 模式:保留标题层级
python
loader = UnstructuredMarkdownLoader(markdown_path, mode="elements")
data = loader.load()
print(f"Number of documents: {len(data)}")
for document in data:
print(document)
mode="elements" 是 Markdown 解析的分水岭。它把 Markdown 拆成多个元素,并且每个元素都带上了关键的元数据:
python
# 第一个元素(标题)
page_content='婚姻家庭编 📖'
metadata={
'source': 'data/民法典/婚姻家庭编.md',
'category_depth': 0, # 标题层级!0 表示一级标题
'category': 'Title', # 元素类型
'element_id': 'b89add...', # 元素唯一 ID
'filetype': 'text/markdown',
'languages': ['zho'],
...
}
# 第二个元素(正文)
page_content='婚姻家庭编是调整因婚姻家庭产生的民事关系...'
metadata={
'source': '...',
'parent_id': 'b89add...', # 指向父元素(标题)的 ID!
'category': 'UncategorizedText',
'element_id': '4d1fd...',
...
}
两个最关键的元数据字段:
-
category_depth:表示标题层级。0是一级标题,1是二级标题,以此类推。这是 L4 分块时「按标题层级切分」的输入基础。 -
parent_id/element_id:表示元素之间的父子关系。正文元素的parent_id指向它所属标题的element_id。这是构建「父子块」「层级索引」的关键。
对企业落地的启发 :如果你要构建一个按章节组织 的知识库(比如技术文档、规章制度、法律条文),
mode="elements"是必须的。它让你可以在 L4 分块时按标题层级切,而不是按字数硬切,从而保证「一个章节的内容不被切碎」。
四、解析元素之间的关系:父子元素链接
这一节是「结构化解析」的精华,也是很多教程容易略过、但对 RAG 质量影响极大的技术点。
4.1 为什么需要「父子元素链接」?
在文档里,标题和它下面的正文、表格,天然存在「父子关系」。比如:
bash
# 婚姻家庭编 ← 父元素(Title)
离婚冷静期条款 📄 规定三十日内可撤回 ← 子元素(NarrativeText)
如果把这些元素平铺成一个个独立的 chunk,检索时就会出现问题:
- 检索「离婚冷静期包含什么」,可能只命中正文,但不知道它属于哪个章节。
- 检索「婚姻家庭编」,可能命中标题,但不知道下面有哪些内容。
父子元素链接就是解决这个问题的:把「标题」和「它下面的内容」绑定起来,检索时既能命中细节,又能找到所属的章节。
4.2 实战代码:构建父子元素关系
下面这段代码演示了如何遍历 Unstructured 解析出的元素,把「标题/表格」作为父元素,把「它下面的元素」作为子元素,构建父子关系:
python
from langchain_unstructured import UnstructuredLoader
from typing import List
from langchain_core.documents import Document
page_url = "https://example.com/wiki/中华人民共和国民法典"
def _get_setup_docs_from_url(url: str) -> List[def _get_setup_docs_from_url(url: str) -> List[Document]:
loader = UnstructuredLoader(web_url=url)
setup_docs = []
parent_id = None # 初始化 parent_id
current_parent = None # 用于存储当前父元素
for doc in loader.load():
# 检查是否是 Title 或 Table(作为父元素)
if doc.metadata["category"] == "Title" or doc.metadata["category"] == "Table":
parent_id = doc.metadata["element_id"]
current_parent = doc # 更新当前父元素
setup_docs.append(doc)
# 检查是否属于当前父元素
elif doc.metadata.get("parent_id") == parent_id:
setup_docs.append((current_parent, doc)) # 将父元素和子元素一起存储
return setup_docs
docs = _get_setup_docs_from_url(page_url)
for item in docs:
if isinstance(item, tuple):
parent, child = item
print(f'父元素 - {parent.metadata["category"]}: {parent.page_content}')
print(f'子元素 - {child.metadata["category"]}: {child.page_content}')
else:
print(f'{item.metadata["category"]}: {item.page_content}')
print("-" * 80)
这段代码的核心逻辑:
- 遍历 Unstructured 解析出的所有元素。
- 遇到
Title或Table,把它标记为「当前父元素」,并记录它的element_id。 - 遇到
parent_id指向当前父元素的元素,把它作为「子元素」,和父元素一起存成(parent, child)元组。 - 后续检索时,就能用这个父子关系做「父子块」检索(L4 的 Parent-Child Docs 技术)。
为什么这个技术点值得单独讲? 因为它是从「解析」跨到「分块」的桥梁 。很多资料把「解析元素关系」一笔带过,但它的直接用途,就是 L4 的「父子块索引」。这也是为什么我在系列里说:L3 的「结构保留」正是 L4 分块的输入基础。
五、选型决策:什么时候用哪个 Loader?
这是本文最实用的一节。我把通用文本/结构化文本场景的选型逻辑,浓缩成一张决策表。
5.1 速查选型表
| 你的输入 | 首选 Loader | 备选 | 关键注意点 |
|---|---|---|---|
| 纯 txt、无结构 | TextLoader |
UnstructuredLoader |
无结构,直接读即可 |
| 目录下多文件 | DirectoryLoader + 对应 Loader |
UnstructuredLoader |
需按扩展名分发 Loader |
| JSON 数据 | JSONLoader + jq_schema |
通用 Loader | 用 jq 挑字段,控制体积、保语义 |
| 单个网页 | WebBaseLoader |
UnstructuredLoader |
WebBase 会残留 JS,建议净化 |
| 复杂网页 | UnstructuredLoader |
FirecrawlLoader |
保留 category 结构 |
| 大批量站点 | RecursiveURLLoader / SitemapLoader |
FirecrawlLoader |
递归抓取或读 sitemap |
| Markdown | UnstructuredMarkdownLoader |
TextLoader |
必须用 mode="elements" 保留标题层级 |
| 需要保留结构 | Unstructured 系列 |
手动解析 | 检查 category / category_depth / parent_id |
5.2 三条选型铁律
铁律一:能保留结构,就不要只读文本。 纯文本 Loader(TextLoader、WebBaseLoader)只负责「读进来」,丢掉了标题、段落、表格关系。只要你的文档有结构(Markdown、网页、PDF、JSON),就应该用能「读出结构」的 Loader(Unstructured 系列、JSONLoader)。
铁律二:metadata 决定检索的「可控性」,别让它空着。 source、category、category_depth、parent_id、element_id 这些元数据,是后续 L4 分块、L7 检索、结果溯源的「基础设施」。选 Loader 时,要看它能产生哪些 metadata,而不是只看它能不能读。
铁律三:解析结果「可复现、可检查」,不要黑盒。 不同 Loader 解析结果差异巨大(P11 的核心结论)。上线前,一定要把解析结果 dump 出来看一眼 ,确认 category 标对了、parent_id 链接对了。否则等到检索环节才发现结构丢了,返工成本极高。
5.3 一个完整的选型决策流程
ini
你的文档是什么格式?
├── 纯文本(txt)→ TextLoader
├── JSON → JSONLoader + jq_schema(挑字段)
├── 网页 → 单页用 WebBaseLoader / 复杂用 UnstructuredLoader
│ └── 大批量 → RecursiveURLLoader / SitemapLoader / FirecrawlLoader
├── Markdown → UnstructuredMarkdownLoader(mode="elements")
├── 需要保留结构 → Unstructured 系列(检查 category / parent_id)
└── 其他格式(图片/PPT/PDF/表格)→ 见本系列下篇
六、企业落地视角:这套解析逻辑怎么用在法律/知识库场景?
作为系列的一贯风格,我把这套通用解析逻辑,映射到一个具体的企业落地场景------法律条文检索平台(这也是你正在搭建的七层 RAG 框架中 L3/L4 的核心任务)。
6.1 通用 Loader 能解决什么、不能解决什么
| 场景 | 通用 Loader 能做的 | 通用 Loader 做不到的 |
|---|---|---|
| 法律条文 PDF | 用 UnstructuredPDFLoader 把 PDF 转成带结构的元素 |
识别「效力层级」「新旧法时效」 |
| 法规条款 Markdown | 用 UnstructuredMarkdownLoader(mode="elements") 保留标题层级 |
识别「但书」「兜底条款」等法律结构 |
| 案例 JSON | 用 JSONLoader 挑出「案号」「裁判要旨」字段 |
识别「援引关系」(本案援引了哪条法条) |
| 条文表格 | 用 Unstructured 保留表格结构 |
识别「上位法/下位法」的效力关系 |
核心结论 :通用 Loader 解决的是「把文档变成有结构的文本 」这个通用问题。但法律知识真正有价值的部分------效力层级、新旧法时效、援引关系、条款结构(但书/兜底条款) ------是关系型的,通用 Loader 给不了。
6.2 这意味着什么?
这意味着在 L3/L4 层,你不能只依赖通用 Loader。你需要:
- 在 L3 用通用 Loader 做「结构保留」 :把 PDF、Markdown、JSON 转成带
category、parent_id、category_depth的元素。 - 在 L4 做「关系建模」 :在通用 Loader 产出的元素基础上,自研一层关系抽取,把「效力层级」「援引关系」「条款结构」建模成实体-关系三元组(GraphRAG 式)。
- L7 检索时:用「结构 + 关系」做约束检索,而不是纯向量相似度。
这也是为什么我说:法律检索平台的核心开发任务在 L3/L4,而不是 L7 。因为 L7 的 BM25 + 向量 + RRF + 领域 rerank 是成熟通用的,真正的差异化在「导入阶段建模了什么关系」。
七、小结与下篇预告
7.1 本文核心要点
- 导入是 RAG 的第一道分水岭:检索质量的上限,在数据进入索引之前就被决定了。
- Document 对象是统一载体 :
page_content决定检索到什么,metadata决定能否精准检索、能否溯源。 - 不同 Loader 解析结果不同 :
TextLoader只读文本,Unstructured系列读出结构(category/parent_id)。 - JSON 用
jq_schema挑字段:控制索引体积、保证语义连贯。 - 网页五种 Loader 各有侧重:单页用 WebBase,复杂用 Unstructured,大批量用 Recursive/Sitemap/Firecrawl。
- Markdown 必须用
mode="elements":保留category_depth(标题层级)和parent_id(父子关系)。 - 父子元素链接是 L3→L4 的桥梁:解析出的结构,正是 L4 分块和 L7 检索的输入。
- 企业落地要自研关系建模:通用 Loader 给不了法律场景的效力层级、援引关系,需在 L4 自研。
7.2 下篇预告
本文聚焦通用文本与结构化文本 (txt / JSON / 网页 / Markdown)。下一篇将进入图文与 PDF 解析,覆盖:
- 图片中的文字识别(
UnstructuredImageLoader、OCR) - PPT 中的文字提取
- 多模态大模型解析图文(这是 L3 里最前沿、也最值得关注的技术)
- PDF 九种工具对比(PyPDF / Unstructured / Marker / LlamaParser / PDFPlumber / PyMuPDF...)
- 三大类解析方法(规则 / 深度学习 / 多模态)
以及表格与数据库导入(CSV 自动切分、source 元数据定制、LlamaHub DatabaseReader 连库)。
本文是个人在七层 RAG 框架落地过程中的实践记录与选型笔记,欢迎交流指正。
八、延伸阅读:2026 年生态的四个重要增量
本节是我在项目之外的独立选型调研。经典方案胜在「稳」,但到了 2026 年,数据导入这个环节已经发生了四件大事。如果你正在做企业级 RAG,这四件事值得你重新审视选型。
8.1 增量一:LangChain 的 Loader 生态已经「拆包」了
我们早期代码大量用了 from langchain_community.document_loaders import ...。但在 LangChain 1.0 之后,这套生态发生了结构性变化:
langchain-community的 Loader 大量被拆分到独立包 。比如 Unstructured 相关的 Loader,现在有独立的langchain-unstructured包,需要单独安装:pip install -U langchain-unstructured,并配置对应的环境变量(如UNSTRUCTURED_API_KEY)。langchain-core成为公共底座 :Document、BaseLoader这些核心抽象从langchain_community抽到了langchain_core,所以现在导入Document应该用from langchain_core.documents import Document。- 新增了官方集成包 :
langchain-docling、langchain-firecrawl等,把新一代解析工具通过DoclingLoader、FirecrawlLoader等官方 Loader 接入。
对企业落地的启发 :写代码时不要再用「一把梭」的 langchain_community 大杂烩导入。按需装独立包,能显著减小依赖体积、避免版本冲突,也更符合 LangChain 1.0 的「模块化」设计哲学。
8.2 增量二:网页解析从「抓取」进化到「LLM 友好的转换」
我在项目里试过五种网页 Loader(WebBase / Unstructured / RecursiveURL / Sitemap / Firecrawl)。但 2026 年,网页解析的竞争焦点已经从「能不能抓」变成了「能不能转成 LLM 友好的干净 Markdown」。这里出现了几个以往资料少有提及的新选择:
| 工具 | 定位 | 核心优势 | 局限 |
|---|---|---|---|
Jina Reader (r.jina.ai) |
网页→Markdown 转换 API | 零配置、免注册、单 URL 快速读取,返回干净 Markdown | 无爬虫端点,大批量需自建抓取逻辑 |
| Crawl4AI | 开源爬虫框架 | 开源、可本地部署、LLM 友好输出、支持 JS 渲染 | 需自建基础设施 |
| Firecrawl | 抓取 + 转换一体化 API | 有爬虫端点、支持大规模站点、过滤控制强 | 托管版收费,自部署有成本 |
| Apify / Spider | 结构化数据抓取平台 | 适合抓结构化数据(非纯文本) | 面向数据抓取,非纯 RAG 场景 |
一个关键的选型判断(来自实测对比):
- 如果你要快速读单个 URL 、零配置跑起来 → Jina Reader 最省事。
- 如果你要批量抓取整个站点 、且对内容质量要求高(需要过滤导航/广告/脚本)→ Firecrawl 更强。
- 如果你要开源、可本地部署、完全掌控 → Crawl4AI。
对企业落地的启发 :WebBaseLoader 会残留 JS 代码(前面 3.2.1 已经演示过),这在 2026 年已经不是最优解。真正生产级的做法是:用 Firecrawl / Jina Reader / Crawl4AI 先把网页转成干净的 Markdown ,再喂给 UnstructuredMarkdownLoader 做结构解析。「网页→Markdown→结构化解析」这个两段式管道,比直接抓 HTML 更干净。
8.3 增量三:文档解析的「三巨头」格局------Docling / MinerU / Marker
这是 2026 年数据导入领域最大的变化。早期 PDF 解析方案还停留在「PyPDF / Unstructured / LlamaParser」的经典组合,但新一代的开源解析引擎已经崛起,形成了「三巨头」格局。
| 引擎 | 定位 | 核心优势 | 局限 |
|---|---|---|---|
| Docling | 开源项目,MIT 协议 | 输入覆盖最广(PDF/Word/PPT/HTML/EPUB/邮件),纯 CPU 友好,文档模型专为 RAG 设计,有活跃的 LangChain 集成 | 复杂公式弱于 MinerU,未上公开榜单、精度难客观引用 |
| MinerU | 开源项目 | 中文、日文、韩文复杂版式最强,公式转 LaTeX、合并单元格表格、高精度 OCR,pipeline(CPU)+ VLM(GPU)双路线 | 相对重,GPU 高精度路线成本高 |
| Marker | 开源项目(基于 VLM) | 2026 年把布局/OCR/公式都路由到单一 VLM(轻量模型),端到端简洁 | 依赖 GPU,中文/复杂版式不如 MinerU |
一个关键的架构趋势 (非常重要):2026 年,「布局识别 + OCR + 公式识别」这三层,正在被一个单一的视觉语言模型(VLM)取代 。Marker 用一个轻量 VLM 搞定,MinerU 也推出了 VLM 后端,Docling 增加了 --pipeline vlm 选项。这意味着:
- 过去:你要拼装 DocLayout-YOLO(布局)+ PaddleOCR(文字)+ UniMERNet(公式)三套模型。
- 现在:一个 VLM 就能端到端输出结构化的 Markdown。
对企业落地的启发:
- 如果你的文档以中文为主 (法律、政务、科研、中文企业文档)→ 我倾向用 MinerU,它在中文复杂版式上的表现更稳。
- 如果你的文档类型杂、要 CPU 上跑、追求开源宽松协议 → Docling 更合适。
- 如果你有 GPU 资源、追求端到端简洁 → Marker 或 MinerU 的 VLM 路线。
8.4 增量四:多模态解析,从「OCR」进化到「视觉理解」
早期资料里只简单提到「用大模型解析图文」,但 2026 年这一块已经成熟到可以直接上生产 。多模态大模型(GPT-4o、Qwen-VL、Claude、Gemini)不仅能 OCR,还能理解图表、表格、流程图、公式,直接输出结构化内容。
多模态解析的三种做法对比:
| 做法 | 原理 | 适用场景 | 成本 |
|---|---|---|---|
| 传统 OCR(Tesseract / PaddleOCR) | 纯文字识别,不理解语义 | 纯文字图片、扫描件 | 低 |
| 布局 + OCR 组合(Docling / MinerU pipeline) | 先布局识别,再分区 OCR | 复杂版式、表格、公式 | 中 |
| 多模态 VLM 直接解析(GPT-4o / Qwen-VL) | 视觉语言模型端到端理解 | 图表、流程图、跨页表格、需要语义理解的复杂文档 | 高 |
一个实用的工程建议 :多模态解析不是「非此即彼」的。真正生产级的做法是分层路由:
- 先用轻量规则判断:这张图/这一页是纯文字?还是含表格/图表/公式?
- 纯文字 → 走传统 OCR(便宜、快)。
- 含表格/图表/公式 → 走多模态 VLM(贵,但准确)。
- 关键业务文档(法律条文、财务报表)→ 强制走多模态 VLM,确保图表内容被正确解析。
对企业落地的启发 :多模态解析的成本是传统 OCR 的几十倍,但图表、公式、表格的解析质量 是纯 OCR 给不了的。如果你做的是法律/金融/科研这种「图表和公式是核心价值载体」的场景,该花的钱要花,但要通过「分层路由」把成本控制在刀刃上。
九、结语:L3 是「地基」,选型决定上限
回到开头的命题:检索质量的上限,在数据进入索引之前就被决定了。 这句话在 2026 年依然成立,而且比以往更深刻。
因为现在的工具链已经足够丰富------从经典的 TextLoader / JSONLoader / Unstructured,到新一代的 Docling / MinerU / Marker、Firecrawl / Jina / Crawl4AI、多模态 VLM------问题已经不是「能不能解析」,而是「你选对了没有」。
最后,把本文的核心判断浓缩成一句话,送给你做选型决策:
选 Loader 的本质,是选「你愿意在导入阶段保留多少结构」。保留的结构越多,后续分块和检索的精度上限就越高;但代价是解析成本、复杂度和对特定工具的依赖。
在 L3 层,这个「结构保留 vs 成本」的权衡,就是整个 RAG 系统最值得花时间打磨的地方。