做 RAG 项目做到第三个月,我越来越确认一件事:检索效果的上限,往往不是模型决定的,而是解析环节决定的。PDF 里排好版的表格、双栏论文、页眉页脚、扫描件,任何一环处理不当,后面接多少向量化、重排都是白搭。docling 就是我在这个阶段捡到的一个顺手工具——它把 PDF、Word、PPT 和图片统一解析成结构化的 Markdown 和 JSON,能跑版面分析、能识别表格、能补 OCR,直接替我把“文档到结构化文本”这个最脏最累的活扛掉了。
这篇文章写给两类人:一类是在搭 RAG 检索链路、被 PDF 解析弄得焦头烂额的后端工程师;另一类是经常批量处理合同、论文、行业报告,希望把文档变成干净数据的算法同学。我会从安装、核心原理、接口设计、RAG 接入到避坑经验,把 docling 完整过一遍,尽量讲清楚每一步“为什么这么做”,而不只是贴文档。
1. 文档解析为什么这么难,docling 到底解决了什么
1.1 一张 PDF 里藏着的复杂版面
很多人第一次接触 PDF 解析,以为就是把文字提取出来。真正上手才发现,一页 PDF 里有标题、正文、表格、图片、页眉、页脚、脚注,还有横跨两栏的论文标题。传统的pdfminer或PyMuPDF直接调用page.get_text(),很容易按物理坐标顺序把文字读乱,双栏文章会左右两栏混在一起,表格文字会和正文连成一片。
更麻烦的是扫描件。整个文件根本没有文本层,只是一张图,必须 OCR。而 OCR 的输出天然就是“一行一行字”,没有标题、表格、段落的边界。如果直接把这种平文本拿去切块、做 embedding,检索质量会非常不稳定。docling 的定位就是把这些杂活统一接管:它把版面分析、阅读顺序还原、表格结构识别、方向校正、OCR 这些能力封装成一条完整 pipeline,输出是带结构层级的结果,而不是一堆裸文本。
1.2 RAG 场景需要“解析器”,而不是“提取器”
我曾经犯过一个典型错误:为了赶进度,直接拿正则表达式从 PDF 里抽文本,抽完就交给向量库。结果用户问“去年销售额是多少”,检索返回的 chunk 把表格拦腰截断,数字残缺不全,模型自然答错。
RAG 的检索质量高度依赖 chunk 质量。如果 chunk 把一个表格从中间切断,或者把标题和正文分离,那向量化之后语义就不完整。docling 把文档解析成树状结构,标题、段落、表格、列表都带着类型和层级信息,后面的切分策略就可以做得更聪明。比如表格整体作为一个 chunk,标题和紧随其后的段落合并成一个 chunk,这样检索命中的上下文才是“完整的一段意思”。这类能力是裸提取工具给不了的。
1.3 本地开源,模型推理不依赖云端
docling 是 IBM 开源的一个文档处理库,核心思路是调度多个模型配合工作:版面分析模型负责框出页面里的区域,TableFormer 负责还原表格结构,OCR 后端可以选用 EasyOCR 或者 Tesseract。模型权重默认从 Hugging Face 下载,下载之后在本地跑推理,不依赖云端 API。
这一点对我来说特别重要。很多企业项目对文档内容有保密要求,不能把 PDF 传到第三方解析服务里。docling 本地可跑,意味着整个解析链路可以闭环在公司内网,数据不出域。再加上它是 Python 库,可以直接嵌入现有的数据处理流水线,不用额外部署一个 HTTP 服务。当然,这也意味着你要自己管理模型下载、显存占用和 CPU/GPU 调度,后面我会专门讲。
2. 快速上手:10 分钟内拿到第一个结构化结果
2.1 安装与依赖选择
docling 支持 Python 3.9 及以上版本,建议在一个干净的虚拟环境里安装。我平时用uv建环境,省心很多:
python -m venv .venv source .venv/bin/activate pip install "docling[full]"这里我强烈建议直接安装[full]版本。如果你只装核心包,后面用到 OCR、图表识别时会发现缺了一堆依赖,还得回头补装。与其反复折腾,不如一步到位。安装过程中可能会拉取 torch、opencv、easyocr 这些比较大依赖,网络慢就等一会儿,属于正常现象。
2.2 三行代码解析一个 PDF
安装完成之后,第一次解析比我想象中简单得多。核心入口只有一个DocumentConverter:
from docling.document_converter import DocumentConverter converter = DocumentConverter() result = converter.convert("example.pdf") print(result.document.export_to_markdown())第一行实例化 converter,它会在内部初始化默认的版面分析和表格识别模型。第二行convert()读取文件、跑模型推理、还原结构。第三行把结构化文档导出成 Markdown。第一次运行需要下载模型权重,后续直接用缓存,速度会快很多。
我建议你把输出同时保存成 Markdown 和 JSON 两份。Markdown 方便人阅读,也方便后续喂给大模型;JSON 保留了每种元素的坐标、层级、类型,做精细切分和自定义处理时非常有用:
with open("output.md", "w", encoding="utf-8") as f: f.write(result.document.export_to_markdown()) with open("output.json", "w", encoding="utf-8") as f: f.write(result.document.export_to_dict())2.3 支持哪些输入格式
docling 不只处理 PDF,还支持 Word、PPT、HTML 和图片。我在项目里经常收到一堆.docx和.pptx的会议材料,过去要分开写两套解析代码,现在直接交给 docling 统一处理。它内部会先把这些格式转成同类中间表示,再做版面分析和结构化,输出格式保持一致。
如果你要传入的是图片,直接给图片路径或者图片字节流都可以。对于扫描版 PDF,docling 会自动识别是否需要 OCR;对于本身带文本层的高质量 PDF,它默认走文本提取路径,速度更快。
3. 核心能力拆解:版面、表格、OCR 与阅读顺序
3.1 版面分析:先让模型看懂页面结构
版面分析是整套 pipeline 的第一步。模型会把页面划分成不同区域,并为每个区域标注类型:标题、正文、表格、图片、公式、页眉页脚等。每个区域都会生成一个边界框(bbox),然后系统把这些区域按照阅读顺序重新排序。
这一步对双栏论文尤其关键。PDF 本身记录的文本位置是物理坐标,如果按坐标顺序读取,左栏第一行读完会跳到右栏第一行,导致整个文本逻辑混乱。docling 的版面分析先识别出“这一栏是正文、那一栏也是正文”,再把同栏内容按从上到下的顺序串起来,最后拼接成正确的阅读顺序。
注意:双栏 PDF 如果绕过版面分析直接提文本,几乎必然出现栏位交错。这也是为什么我前面强调,别把 docling 当成普通的“提取文字”工具。
3.2 OCR 到底什么时候开、什么时候关
docling 的 OCR 不是无脑启用的。它默认的策略是:如果 PDF 自带文本层,就优先用文本层;如果检测到扫描件,则自动启用 OCR。但在实测中,我更喜欢手动控制,因为自动判断偶尔会把“图片型 PDF”当成“无文本 PDF”处理,或者反过来,在有文本层但文本质量极差的情况下浪费 OCR 成本。
我一般用PdfPipelineOptions来控制:
from docling.datamodel.base_models import InputFormat from docling.datamodel.pipeline_options import PdfPipelineOptions, EasyOcrOptions from docling.document_converter import DocumentConverter, PdfFormatOption pipeline_options = PdfPipelineOptions() pipeline_options.do_ocr = True pipeline_options.ocr_options = EasyOcrOptions(force_full_page_ocr=True) converter = DocumentConverter( format_options={ InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options) } )do_ocr = True表示强制走 OCR。force_full_page_ocr = True表示整页图片都做 OCR,而不是只对没有文本层的区域做。如果文件是扫描件,我建议开启这一项。如果文件本身就是数字版 PDF,就关闭 OCR,速度会快一个量级。
EasyOCR 对印刷体中英文识别效果都不错,但速度偏慢。如果你有 GPU,记得把 torch 换成 CUDA 版本,并确认 OCR 能调用到 GPU。如果没有 GPU,批量解析大扫描件会比较煎熬,可以考虑改用 Tesseract 后端,速度会快一些,但识别精度略有下降。
3.3 TableFormer:表格识别的核心竞争力
docling 最让我意外的是表格识别。TableFormer 模型不仅能把表格区域框出来,还能把单元格之间的行列关系还原出来,输出 Markdown 表格结构。我拿一份带边框的财务报表做过测试,它能正确还原多列、多行、表头,甚至字段里的换行也处理得不错。解析出的 Markdown 可以直接贴进文档,几乎不需要二次修复。
表格识别开启方式也是通过PdfPipelineOptions。默认配置下表格结构识别是开启的,不需要额外设置。但对于复杂表格,像是带斜线表头、跨行合并单元格、无边框表格,输出会有小瑕疵。遇到这类极端格式,我通常会在导出后,用一段规则把 Markdown 表格里的坏行清理掉,再交给后续流程。整体来看,docling 的表格能力已经能覆盖绝大多数业务报表场景,远超普通 PDF 库。
3.4 方向校正与阅读顺序还原
扫描件里偶尔会出现整页旋转 90 度或 180 度的情况。docling 提供了方向校正能力,它会判断页面方向并自动旋转,避免识别出一堆颠倒文字。在PdfPipelineOptions里,相关开关是do_orientation_check,一般建议保持默认开启。
阅读顺序还原则是把版面分析出的区域排序,形成文档的逻辑流。输出结果里,每个 block 会带上parent关系,形成一棵树。这个树状结构对 RAG 切分极具价值,因为你可以根据层级关系判断某个段落属于哪个章节,或者哪些内容属于同一个表格块。这也是 docling 与其他“平铺式”解析工具的最大区别。
4. 在 RAG 管道中集成 docling 的实战思路
4.1 推荐接入方式:解析一次,复用多次
文档解析往往比较耗时,尤其是扫描件。我在实际项目中,会把 docling 放在文档进入系统的第一个节点,解析后把结果落盘成 JSON Lines 文件。后面不管是做测试、调 chunk 策略、还是换 embedding 模型,都不需要重新跑解析,直接从中间结果继续就行。
import json results = [] for file_path in file_list: result = converter.convert(file_path) doc_dict = result.document.export_to_dict() results.append({"source": file_path, "doc": doc_dict}) with open("parsed_docs.jsonl", "w", encoding="utf-8") as f: for item in results: f.write(json.dumps(item, ensure_ascii=False) + "\n")这样设计,解析和检索解耦,效率提升非常明显。我在一个 2000 份合同的数据集上跑过一次,纯解析大约需要 1.5 小时,但之后测试各种切分和向量化方案,只需要读取 JSONL,秒级迭代。
4.2 按结构切分,而不是按字数硬切
docling 导出 JSON 内部的字段结构清晰。text、label、prov这些字段会标记每个元素的内容、类型和来源坐标。基于这个结构,切分策略可以做得非常精细:
- 标题节点单独拎出来,作为文档的小节索引。
- 正文段落按段落切,段落太长的再按句号切。
- 表格节点整体作为一个 chunk,不拆分。
- 列表项合并成一个 chunk,避免单条列表项语义不完整。
这种做法的好处是,检索时命中的 chunk 语义完整。比如用户问“合同中的违约责任条款是什么”,检索系统可以精准命中“违约责任”标题下的整段内容,而不是被截断的碎片。
4.3 表格与正文分开建索引
我在实际项目里发现,表格块和正文块混在一起建索引,效果并不理想。因为表格的文本密度高、含义紧凑,向量化之后和普通段落差异很大。更稳妥的做法是:正文走常规 embedding 索引,表格单独建一个索引,或者给表格块加一个type=table的元数据字段,在检索时做过滤或加权。
docling 输出 JSON 时,每个元素都有类型信息,天然适合做这种区分。你只需要在构造 chunk 时,把label字段透传到元数据里。这样查询“年度营收对比”时,就可以优先召回表格块;查询“报告结论”时,则优先召回正文块。这个策略很实用,而且实现成本很低。
4.4 与 LangChain 等框架的对接
如果你在用 LangChain 或 LlamaIndex,也不需要额外开发太多胶水代码。把 docling 的 Markdown 输出作为文档内容,把结构元数据作为metadata,接入现有的DocumentLoader或者自定义 loader 即可。我自己更倾向于不绑定框架的通用做法:解析、切分、向量化都自己控制,把中间结果存成 JSONL,这样框架升级时不受影响。
5. 实测心得与避坑清单
5.1 我在真实项目里踩过的几个坑
第一个坑是扫描件没开 OCR。一开始我用默认配置去解析一批历史合同,结果导出的 Markdown 是空的。原因是这批合同是扫描件,docling 默认没有强制 OCR。后来我统一在PdfPipelineOptions里把do_ocr设为 True,问题解决。
第二个坑是批量处理时反复加载模型。如果对每个文件都新建一个DocumentConverter,模型权重会反复加载,耗时翻倍。正确做法是全局复用一个 converter 实例,docling 内部的模型缓存可以复用,速度提升明显。
第三个坑是输出 Markdown 里的表格缩进。docling 导出的表格在某些嵌套场景下会带较多空白缩进,直接喂给大模型会占用较多 token。后来我在预处理阶段加了清理逻辑,把表格前后的无效空白去除,token 消耗降了不少。
5.2 性能调优的几条路线
- 控制并发:
PdfPipelineOptions里有num_processes参数,可以指定 CPU 核心数。多核机器上批量解析,合理设置能显著提速。 - GPU 加速:OCR 和 TableFormer 都支持 CUDA。确认 torch 是 CUDA 版本,然后让模型迁移到 GPU 即可。
- 模型缓存:模型权重默认缓存在本机,第二次运行不会重复下载。如果你在内网环境,可以把模型目录提前放到共享盘,多台机器共用,避免每台机器都下载一遍。
5.3 什么场景适合 docling,什么场景不适合
适合的场景包括:RAG 索引前的文档清洗、扫描版合同和报告处理、包含大量表格的财务报表、跨格式的 Word/PPT/PDF 统一解析。docling 在这些场景下能大大节省人力。
不太适合的场景,我认为是数学公式密集的论文。docling 对公式的支持比较基础,无法输出 LaTeX 级别的公式结构。如果核心诉求是识公式,还是得用专门的公式识别工具。
另外,手写笔记为主的文档,docling 的效果也会受限。它的 OCR 面向印刷体设计,手写内容识别率不稳定,不建议作为主要方案。
6. 写在最后:我保留 docling 的几个理由
文档解析这个环节,往往是最容易低估、也最容易翻车的地方。项目上线前,所有人都关注模型效果,上线后才发现问题出在解析阶段:表格被切断、双栏乱序、扫描件全是空文本。docling 至少帮我把这些问题收敛到了一个可复现、可调试的工具范围内。
我最终保留 docling 而不是换回 PyMuPDF 加正则的方案,主要原因是它同时给了我三样东西:本地可运行的隐私友好性、直接可用的表格识别、以及保留文档结构的结构化输出。这三样东西组合在一起,让 RAG 的切分和检索环节有了更扎实的底座。
最后再分享一个我自己用下来的小习惯:解析完文档,我习惯把 Markdown 和带 bbox 的 JSON 各存一份,Markdown 用来喂给 LLM 和展示,JSON 用来做程序检索和切分。两份数据各司其职,后面再折腾管道时,会少很多返工。