RAG 的第一步不是 Embedding,而是高质量文档解析:文档加载模块设计实战

8 阅读13分钟

支持 20+ 文档格式、Docling Serve + fallback loaders、异步任务、结果持久化与预览。我想解决的不是“文件能不能传上来”,而是“文档能不能被稳定、标准化地接入后续 RAG 链路”。

上一篇我先整体介绍了 RAG Pipeline Hub 这套工程化 RAG 工作台。这一篇开始,正式进入模块拆解。

我选择先写“文档加载”,原因很简单:很多人以为 RAG 的核心是 Embedding 和检索,但真正决定下游质量上限的,往往是文档加载。

如果入口阶段就把文档解析坏了、结构丢了、表格没了、图片上下文没了,后面无论你分块多精细、向量模型多高级、检索链路多复杂,最后都只能在低质量输入上做优化。

所以这一篇我想聊的不是“怎么上传文件”,而是:

在一个面向真实业务流程的 RAG 项目里,文档加载模块到底应该解决什么问题,以及我在这个项目里是怎么设计它的。

项目地址:

  • GitHub: https://github.com/qingni/rag-pipeline-hub

为什么文档加载经常被低估

很多 RAG 项目的起手式都很类似:

  1. 上传 PDF
  2. 提取纯文本
  3. 直接切块
  4. 送去向量化

这套流程对“验证一个最小可行链路”没问题,但一旦进入真实场景,文档加载马上就会变成一个高频踩坑点。

因为你面对的从来不是一种文档,而是各种杂合输入:

  • 结构规整的 PDF
  • 扫描件 PDF
  • Word 文档
  • Excel / 多 Sheet 表格
  • PPT / 旧版 Office 文档
  • HTML 页面
  • JSON / XML / CSV
  • TXT / Markdown

不同格式的问题完全不同:

  • 有的文本好提,但表格结构容易丢
  • 有的文档页面信息很重要,但纯文本抽取会打平
  • 有的文件本身是扫描件,没有 OCR 根本拿不到可用内容
  • 有的内容里图片是关键上下文,但传统加载只会直接忽略
  • 有的文档很大,如果同步解析,接口很容易阻塞

所以文档加载的问题,从来不只是“读文件”,而是:

  • 如何稳定支持多格式
  • 如何在解析质量和系统稳定性之间做平衡
  • 如何把不同来源的结果统一成后续模块可消费的结构
  • 如何让用户看到解析结果,而不是只相信一个黑盒过程

这也是我把文档加载作为整个系列第一个模块来展开的原因。

文档加载模块到底在负责什么

RAG Pipeline Hub 里,我给文档加载模块的定义不是“上传模块”,而是:

把多种异构文件,转换成统一标准化文档结果的入口模块。

这个定义里有两个关键词很重要。

1. 异构文件

输入不是单一格式,而是各种不同结构、不同复杂度、不同解析方式的文档。

2. 统一标准化结果

输出不能是“每种加载器返回各自一套格式”,而必须变成统一结构,供后续分块、向量化、检索复用。

所以这个模块真正要交付的,不是“上传成功”,而是下面这些内容:

  • 可复用的标准化文本结果
  • 页级信息
  • 表格、图片、公式等结构化元素
  • 文档元数据
  • 处理统计信息
  • 可预览、可持久化、可追踪的解析结果

只有这样,后面的分块模块才能基于稳定输入做策略选择,向量化模块也才能基于统一结果做批处理和推荐。

真实项目里,文档加载的难点到底是什么

如果从工程实现的角度看,我觉得这个模块至少有 4 个核心难点。

1. 解析质量和稳定性是冲突的

高质量解析器通常更强,但也更重、更复杂,部署和调用成本更高。

轻量解析器通常更稳、更快,但在复杂文档、扫描件、表格、图片等场景下效果会明显下降。

这意味着你很难只靠一个加载器解决所有问题。

2. 文档格式多,最佳解析策略并不统一

PDF、DOCX、XLSX、HTML、JSON、TXT 的最佳解析方式本来就不同。
如果一套策略打天下,通常只能得到“平均能用,但不够好”的结果。

3. 大文件和复杂文档不能总走同步链路

解析一个复杂 PDF 或 Office 文档,时间可能远超普通 API 的舒适区间。
如果把这些工作都塞进同步请求里,系统吞吐、用户体验和失败率都会很难看。

4. 解析结果如果不落盘,就很难复用和调试

很多项目处理完文档后只把文本暂存在内存里,然后直接进入分块。
这种方式在 demo 阶段很省事,但一旦要排查问题、对比解析效果、给用户做预览,马上就会不够用。

所以对我来说,文档加载模块必须同时解决:

  • 质量
  • 稳定性
  • 可扩展性
  • 可观察性

我在这个项目里的设计目标

围绕上面这些问题,我给文档加载模块定了几个明确目标:

  1. 支持尽可能多的业务常见文档格式
  2. 复杂文档优先保证解析质量
  3. 主解析器失败时必须可降级
  4. 大文件和复杂解析支持异步处理
  5. 所有结果尽量统一成标准结构
  6. 解析结果可持久化、可预览、可复用

如果用一句话总结,就是:

我想把文档加载做成一个“稳定的标准化入口”,而不是一组彼此割裂的文件解析脚本。

当前这套加载链路是怎么设计的

从架构上看,这个模块大致分成 5 层:

  1. 前端层:上传、进度显示、文档列表、预览、结果展示
  2. API 层:暴露文档上传、加载、结果查询等接口
  3. Service 层:负责流程控制、解析策略选择、任务管理
  4. Loader 层:负责不同格式的具体解析实现
  5. 数据层:负责原始文档、数据库记录和 JSON 结果存储

可以把它理解成这样一条链:

用户上传文档
  -> 根据格式与策略选择解析方式
  -> 调用主解析器或专用加载器
  -> 生成标准化结果
  -> 保存到 JSON 结果文件和数据库
  -> 前端展示预览,供下游分块模块继续消费

这里最关键的不是“层次多”,而是职责清晰

  • 前端负责交互,不承担解析逻辑
  • API 负责暴露接口,不承载复杂判断
  • Service 负责编排,不直接硬编码某一种解析方式
  • Loader 负责具体能力实现,便于后续扩展和替换
  • 数据层负责结果保存,为复用和调试提供基础

这种拆法最大的好处是,后面你无论是想接更多格式、替换主解析器、优化某类文档的加载效果,还是把加载结果接给其他模块,都不会把整个系统搅成一锅粥。

为什么我选择 Docling Serve + fallback loaders

这是这个模块最关键的设计点。

如果只说一句话,那就是:

高质量解析能力和系统可用性,我都要。

为什么需要 Docling Serve

对于复杂文档,尤其是:

  • 结构复杂的 PDF
  • 需要 OCR 的扫描件
  • 包含表格的文档
  • Office 文档
  • HTML 和图片类内容

一个高质量主解析器带来的提升非常明显。

在这个项目里,我选择用 Docling Serve 作为主解析器,原因主要有几个:

  • 独立 HTTP 服务模式,避免把重型解析能力直接塞进 Backend 进程
  • 支持同步和异步调用
  • 支持 OCR
  • 对复杂格式的统一处理能力更强
  • 更适合作为复杂文档的默认入口

简单说,它更适合承担“主战场”的工作。

为什么还必须要有 fallback

但如果工程上只依赖一个主解析器,也会有很现实的问题:

  • 服务可能不可用
  • 某些格式解析可能失败
  • 某些简单文档没必要走重链路
  • 某些环境可能不方便部署完整解析服务

所以我没有把 Docling Serve 当成唯一方案,而是把它放进一套“主解析器 + 专用加载器降级链”里。

当前项目里,不同格式会走不同主策略:

  • PDF / DOCX / DOC / PPT / 复杂格式 优先走 Docling Serve
  • XLSX / XLS / PPTX 会根据格式策略走更合适的原生解析器或补充链路
  • HTML / JSON / CSV / TXT / Markdown / XML 直接走专用加载器
  • 主解析器失败时,再按 fallback 链自动降级

也就是说,这不是一个“单解析器架构”,而是一个解析策略架构

这是我觉得很多文档处理系统最容易被忽视的一点:
真正需要设计的,不只是某个 loader,而是在不同文档场景下,谁优先、谁兜底、谁适合直接处理

异步处理为什么非常重要

文档加载还有一个很容易被忽视的问题:不是所有解析任务都适合同步完成。

尤其是下面这些场景:

  • 大文件 PDF
  • 扫描件 OCR
  • 复杂 Office 文档
  • 带大量结构化元素的文件

如果这些任务都在接口里同步执行,常见后果就是:

  • 请求时间过长
  • FastAPI 工作线程被占住
  • 用户只能一直等
  • 错误恢复和进度反馈都很差

所以在这个项目里,文档加载模块支持异步任务模式。

Docling Serve 为例,它会先提交异步转换任务,返回 task_id,后续由 LoadingService 负责轮询状态、获取结果并完成保存。

这背后的好处很直接:

  • 避免在 API 请求里长时间阻塞
  • 更容易展示进度和任务状态
  • 更适合处理大文件和慢任务
  • 方便做超时控制和失败恢复

很多时候,异步能力本身并不会直接提升“解析质量”,但它会显著提升系统在真实使用中的可用性。

为什么我坚持把结果标准化并持久化

这个模块还有一个我很在意的设计点:解析结果不是一次性中间变量,而是可复用的数据资产。

如果加载完马上进分块,当然也能跑通,但你会失去很多非常关键的能力:

  • 不能方便地做解析结果预览
  • 不能快速回看某次加载到底抽取了什么
  • 不能对比不同加载器效果
  • 不能让分块模块直接复用已有结果
  • 不能积累历史记录和统计信息

所以这个项目会把加载结果保存成 JSON 文件,同时在数据库中保留文档信息和处理结果记录。

这意味着文档加载的输出不只是文本,而是一套更完整的标准结果,里面通常包括:

  • full_text
  • pages
  • tables
  • images
  • metadata
  • processing stats

这么做的价值很大:

  1. 前端可以直接预览解析结果
  2. 分块模块可以直接基于结果继续处理
  3. 出问题时可以回溯具体哪一步解析出了问题
  4. 后续如果要做评估、对比、推荐,也有基础数据可用

这也是为什么我一直强调:

文档加载模块不是“上传成功”,而是“生成标准化结果成功”。

文档预览为什么不是锦上添花,而是必要能力

如果你做过 RAG 调优,就会发现一个很常见的痛点:

明明最后问答效果不好,但你不知道问题到底出在:

  • 文档没解析对
  • 表格丢了
  • 页级结构丢了
  • 图片上下文没保住
  • 某些字段被错误清洗了

所以我认为文档加载模块一定要支持预览。

在这个项目里,前端会提供:

  • 文档上传
  • 文档列表
  • 进度展示
  • 文档预览
  • 结果预览

也就是说,加载模块不是一个后台黑盒任务,而是一个用户能看见结果、能确认质量、能继续决策下一步的工作台。

这件事对后续模块特别重要。
因为只有用户看见了解析效果,才知道接下来该选什么分块策略、是否要重试、是否要更换加载器,或者是否要调整文档来源。

这一层和“普通上传功能”到底有什么区别

如果只看表面,很多人会觉得这个模块无非就是“上传文件 + 解析一下”。

但如果按照工程视角看,它和普通上传功能的差别其实很大。

普通上传关注的是:

  • 文件有没有成功传上来
  • 文件放在哪里
  • 文件元信息有没有记录

而这里的文档加载关注的是:

  • 该用什么解析策略
  • 是否需要高质量主解析器
  • 主解析器失败后怎么降级
  • 结果能不能统一结构
  • 能不能异步处理
  • 能不能预览
  • 能不能持久化
  • 能不能给下游模块稳定复用

所以它更像一个“文档标准化处理中心”,而不是一个上传控件。

这个模块当前最值得关注的几个点

如果只挑几个最能代表这一层价值的能力,我会选这些:

1. 多格式接入

支持常见业务文档格式,为真实场景打基础。

2. Docling Serve + fallback

主解析器负责质量,fallback 负责可用性。

3. 异步任务与进度追踪

让复杂文档解析更适合真实系统,而不是只适合本地 demo。

4. 结果标准化

把不同来源、不同格式的解析结果统一成后续模块可消费的结构。

5. 结果持久化与预览

让加载模块从“黑盒处理”变成“可观察、可回溯、可复用”的工作台。

我对这个模块的一个核心判断

如果只让我总结一句话,我会说:

RAG 的第一步不是上传文件,也不是调用 Embedding,而是把文档稳定、完整、标准化地变成后续链路可以信任的输入。

这件事做不好,后面所有优化都很容易变成“在脏数据上做精细调参”。

这也是我为什么愿意在文档加载这一层投入这么多工程设计的原因。
它看起来不像检索、Reranker、Prompt 那么“AI 味”浓,但它决定了整条链路的地基是否稳。

下一篇写什么

文档加载解决的是“输入从哪里来,以及怎么标准化”。
接下来的关键问题就是:

这些标准化结果,应该怎么切,才能更适合后续检索?

所以下一篇我会继续写:

《Chunk 不只是 chunk_size:我在 RAG 里做了 6 种分块策略和智能推荐》

项目地址:

  • GitHub: https://github.com/qingni/rag-pipeline-hub

如果这篇文章对你有帮助,欢迎:

  • 点个 star
  • 提个 issue
  • 留言说说你最常踩坑的文档格式

如果你也做过文档解析、OCR、表格提取或多格式加载,欢迎交流你踩过的坑。我很确定,这一层远比很多人想象中更值得认真做。