
1. RAG 管线里最容易被低估的一环做 RAG 的人都有一个共识检索质量决定生成质量的上限。但很多人把精力全砸在 embedding 模型选型、向量库调参、rerank 策略上却忽略了一个更底层的问题——你的文档到底被解析成了什么样子。我见过太多 RAG 项目死在第一步。PDF 丢进去解析出来一堆乱码表格跨页之后列全错位扫描件里的文字压根提取不出来一份 80 页的招标文件解析完章节层级全丢了检索的时候按段落召回结果召回的是一堆页眉页脚和无关的表格碎片。后面 embedding 再强、rerank 再精细也救不回来。这就是 RAG 管线里最痛的一环文档解析与结构化。IBM 开源的Docling就是冲着这个问题来的。它的定位很明确——把各种格式的文档统一解析成结构化的、保留版面信息的、适合下游 RAG 使用的中间表示。不是简单的文本提取而是保留页码、章节层级、段落边界、表格结构、阅读顺序这些对检索至关重要的元信息。这篇文章我会从实际使用的角度把 Docling 的能力边界、核心原理、实操流程、踩坑经验完整拆一遍。不管你是刚接触 RAG 的新手还是已经在做企业级知识库的老手应该都能从中找到可以直接抄作业的部分。2. 为什么文档解析是 RAG 的隐形杀手2.1 解析质量如何一步步毁掉整个管线先讲一个我实际遇到的案例。之前帮一个团队做合同知识库文档是 PDF 格式大概 3000 多份。第一版方案用的是最常见的PyPDF2加pdfplumber组合提取纯文本之后按固定长度切 chunk然后走 embedding 入库。上线之后问题立刻暴露。用户问某份合同的违约金比例是多少检索回来的内容里混着页眉的合同编号、页脚的页码、还有隔壁表格里的数字。模型拿到这些噪声生成的答案要么是错的要么直接说未找到相关信息。但人工去翻那份合同违约金条款明明就在第 7 页。排查下来根因有三个。第一PyPDF2提取的文本丢失了阅读顺序双栏排版的文档被按行拼接左右栏内容交错在一起。第二表格被拆成了零散的文本片段行列关系完全丢失。第三页眉页脚没有过滤每页都重复出现在向量空间里形成了大量噪声。这三个问题本质上都是解析阶段没有保留结构信息导致的。你后面用再好的 embedding 模型输入本身就是垃圾输出不可能好。2.2 传统解析方案的三个硬伤市面上常见的文档解析方案大致可以分成几类每一类都有明显的短板。第一类是纯文本提取库比如PyPDF2、pdfminer。优点是轻量、快、依赖少。缺点是只给你一串字符版面信息、阅读顺序、表格结构全部丢失。对于格式规整的单栏文档还能凑合用一旦遇到双栏、表格、图文混排就歇菜。第二类是商业 OCR 和文档理解 API。解析质量确实好表格识别、版面分析都做得不错。但问题是按页收费量大之后成本很高而且数据要传到第三方很多企业场景不允许。第三类是通用 OCR 工具比如 Tesseract。对扫描件有效但对原生 PDF 反而可能帮倒忙因为它会把本来清晰的文字重新识别一遍引入不必要的错误。而且 OCR 输出的是纯文本流同样丢失结构。第四类是大模型直接读文档。现在很多多模态模型支持直接输入 PDF让它输出结构化内容。效果在某些场景下不错但成本和延迟是硬伤而且输出格式不稳定不适合批量处理。2.3 Docling 切入的角度有什么不同Docling 的思路和上面几类都不一样。它把文档解析当成一个版面理解和结构重建的问题而不是简单的文字提取。具体来说Docling 内部会做这几件事先用版面分析模型识别出页面上的各个区域——标题、正文、表格、图片、页眉页脚、脚注然后判断阅读顺序确定这些区域应该按什么顺序读接着对表格区域做专门的结构识别还原行列关系最后把所有信息组装成一个带层级结构的文档对象保留页码、章节、段落这些元信息。这个中间表示可以直接导出成 Markdown、JSON、HTML 等格式。导出成 Markdown 的时候标题层级、表格、列表都会被正确保留这对于下游切 chunk 和 embedding 非常友好。关键区别传统方案输出的是字符串Docling 输出的是带结构的文档对象。这个差异在 RAG 场景下是决定性的。3. Docling 核心能力拆解与选型考量3.1 支持的格式与解析深度Docling 目前支持的输入格式覆盖了绝大多数 RAG 场景会用到的类型。PDF 是重点包括原生 PDF 和扫描件Office 系列支持 Word、PowerPoint、Excel还有 HTML、Markdown、AsciiDoc 这些标记语言图片格式也支持走 OCR 路径。解析深度上Docling 对 PDF 的处理是最完整的。它会做版面分析、阅读顺序判断、表格结构识别、图片区域标注。对 Word 和 PowerPoint因为源文件本身就有结构信息解析相对直接主要是把 Office 的文档模型转换成统一的中间表示。表格识别是 Docling 的一个亮点。它用的是基于 TableFormer 的模型能处理合并单元格、跨页表格、无边框表格这些复杂情况。我实测下来对于规整的财务表格和合同里的条款表格还原准确率相当高。3.2 和同类工具的横向对比为了让你更清楚 Docling 的定位我把它和几个常见的同类工具做个对比。工具核心能力结构保留表格处理部署方式适合场景PyPDF2纯文本提取无无本地库简单单栏文档pdfplumber文本基础版面弱基础本地库需要坐标信息的场景TesseractOCR无无本地扫描件文字提取MarkerPDF转Markdown中中本地快速转换Docling版面理解结构重建强强本地RAG 知识库构建商业文档API全能力强强云端预算充足的企业Marker 和 Docling 经常被放在一起比较。Marker 的优势是转换速度快、输出 Markdown 干净适合快速把 PDF 转成可读文本。但 Marker 在复杂版面分析和表格结构还原上不如 Docling 细致而且它不输出带页码和章节层级的结构化对象。如果你的 RAG 需要精确的引用溯源Docling 更合适。3.3 为什么选本地部署而不是调 APIDocling 是纯本地运行的模型权重下载到本地之后整个解析过程不依赖网络。这个特性在 RAG 场景下有几个实际好处。第一是数据安全。企业内部的合同、方案、招标文件往往涉及敏感信息不能传到第三方服务。本地解析从根上避免了这个问题。第二是成本可控。批量处理几万份文档如果走商业 API费用会非常可观。本地跑虽然需要一点算力但边际成本几乎为零。第三是可定制。Docling 的解析管线是模块化的你可以替换其中的版面分析模型、OCR 引擎、表格识别模型针对自己的文档特点做优化。商业 API 你只能接受它的黑盒输出。当然本地部署也有代价。首次运行需要下载模型权重大概几个 G解析速度受本机算力影响CPU 上跑大批量会比较慢有 GPU 会快很多。这个取舍需要根据你的实际场景来定。4. 实操从零搭建 Docling 解析管线4.1 环境准备与安装Docling 是 Python 包安装很直接。我建议用虚拟环境避免依赖冲突。python -m venv docling-env source docling-env/bin/activate # Windows 用 docling-env\Scripts\activate pip install docling如果你需要处理扫描件还要装 OCR 相关的依赖。Docling 默认集成了 EasyOCR也可以配置成 Tesseract。pip install docling[ocr]首次运行的时候Docling 会自动下载需要的模型权重。这些权重包括版面分析模型、表格识别模型、OCR 模型等总共大概几个 G。下载一次之后会缓存在本地后续运行不再需要网络。注意模型下载这一步在国内网络环境下可能会比较慢建议提前预留时间或者配置好 pip 的镜像源。模型缓存目录默认在用户目录下的.cache/docling里如果磁盘空间紧张可以通过环境变量指定到其他位置。4.2 最简解析流程先看一个最基本的用法把一份 PDF 解析成 Markdown。from docling.document_converter import DocumentConverter converter DocumentConverter() result converter.convert(sample.pdf) markdown_output result.document.export_to_markdown() with open(sample.md, w, encodingutf-8) as f: f.write(markdown_output)这几行代码背后Docling 做了完整的一套处理读取 PDF、逐页做版面分析、识别各个区域、判断阅读顺序、识别表格结构、组装文档对象、导出 Markdown。你不需要关心中间过程但理解这个过程对排查问题很有帮助。4.3 导出结构化 JSON 保留元信息Markdown 适合人读但如果你要做精细的 RAGJSON 格式更有用因为它保留了完整的结构信息。import json doc_dict result.document.export_to_dict() with open(sample.json, w, encodingutf-8) as f: json.dump(doc_dict, f, ensure_asciiFalse, indent2)导出的 JSON 里每个文本块都带有类型标注标题、正文、表格、列表等、页码、在页面上的位置坐标、层级关系。这些信息在切 chunk 的时候非常关键。举个例子你可以根据标题层级来切分章节让每个 chunk 对应一个完整的语义单元而不是机械地按字符数切。这样检索的时候召回的是完整的条款或段落而不是被截断的半句话。4.4 批量处理与性能调优实际项目里往往是成百上千份文档需要批量处理。Docling 支持批量转换而且可以配置并发。from docling.document_converter import DocumentConverter from pathlib import Path converter DocumentConverter() input_dir Path(./documents) output_dir Path(./parsed) output_dir.mkdir(exist_okTrue) for pdf_file in input_dir.glob(*.pdf): try: result converter.convert(str(pdf_file)) md result.document.export_to_markdown() out_path output_dir / (pdf_file.stem .md) out_path.write_text(md, encodingutf-8) print(f完成: {pdf_file.name}) except Exception as e: print(f失败: {pdf_file.name}, 原因: {e})性能方面有几个可以调的点。如果机器有 GPUDocling 会自动使用 GPU 加速速度能提升好几倍。如果只有 CPU可以通过调整批大小和并发数来优化吞吐。另外如果文档里没有扫描件可以关掉 OCR 环节省下不少时间。from docling.datamodel.pipeline_options import PdfPipelineOptions from docling.document_converter import PdfFormatOption from docling.datamodel.base_models import InputFormat pipeline_options PdfPipelineOptions() pipeline_options.do_ocr False # 原生PDF可以关掉OCR pipeline_options.do_table_structure True # 保留表格结构识别 converter DocumentConverter( format_options{ InputFormat.PDF: PdfFormatOption(pipeline_optionspipeline_options) } )实操心得如果你的文档里既有原生 PDF 又有扫描件建议先做一轮检测把扫描件单独拎出来走 OCR 路径原生 PDF 走快速路径。混在一起处理会拖慢整体速度。5. 把 Docling 接进 RAG 管线的完整方案5.1 基于结构的智能切分策略拿到 Docling 的结构化输出之后切 chunk 的策略可以比传统方式精细很多。我的做法是按文档的天然结构来切而不是按固定字符数。具体逻辑是这样的遍历文档对象遇到标题就开启一个新的 section把标题下的正文、列表、表格都归到这个 section 里。如果一个 section 太长再按段落边界二次切分。表格单独处理把表头和每一行拼成自然语言描述作为一个独立的 chunk。def build_chunks(doc_dict, max_chars800): chunks [] current_section {title: , content: [], page: None} for item in doc_dict.get(texts, []): label item.get(label, ) text item.get(text, ) page item.get(prov, [{}])[0].get(page_no) if label section_header: if current_section[content]: chunks.append(current_section) current_section {title: text, content: [], page: page} else: current_section[content].append(text) if sum(len(c) for c in current_section[content]) max_chars: chunks.append(current_section) current_section {title: current_section[title], content: [], page: page} if current_section[content]: chunks.append(current_section) return chunks这样切出来的 chunk每个都带有章节标题和页码。存进向量库的时候把这些元信息一起存进去。检索的时候不仅返回内容还能告诉用户这段话出自哪一章、哪一页。对于合同、方案这类需要精确溯源的文档这个能力非常关键。5.2 表格内容的特殊处理表格是 RAG 里最容易出问题的部分。传统方案要么把表格拆成零散文本要么直接丢掉。Docling 能还原表格结构但还原出来的还是二维结构需要转成适合 embedding 的自然语言。我的做法是把每一行转成一句描述表头作为字段名。比如一个违约金表格表头是违约情形和违约金比例某一行是逾期交付和0.5%就转成违约情形为逾期交付时违约金比例为 0.5%。def table_to_text(table_data): headers [cell[text] for cell in table_data[data][0]] rows [] for row in table_data[data][1:]: cells [cell[text] for cell in row] desc .join(f{h}为{c} for h, c in zip(headers, cells)) rows.append(desc) return \n.join(rows)这样转换之后表格内容就能被正常 embedding 和检索了。用户问逾期交付的违约金是多少能准确召回对应的行。5.3 元信息在检索溯源中的作用Docling 输出的元信息里页码和章节层级是最有价值的两个。页码让引用可溯源章节层级让检索结果有上下文。我在实际项目里的做法是每个 chunk 存三个额外字段page_no、section_path、doc_id。section_path是从顶层标题到当前章节的完整路径比如第三章 3.2 违约责任 3.2.1 违约金。检索的时候除了返回 chunk 内容还返回这些元信息。前端展示的时候可以显示出自《XX合同》第 7 页第三章 3.2 节。用户看到这个信任度立刻不一样。而且section_path还能用于检索后的上下文扩展。如果召回了 3.2.1 节的内容可以自动把同属 3.2 节的其他 chunk 也带出来作为上下文让生成的答案更完整。6. 常见问题与排查技巧实录6.1 解析结果乱码或文字错位这是最常见的问题通常有几个原因。如果 PDF 本身是扫描件必须开 OCR否则提取出来就是空白或乱码。如果 PDF 用了特殊字体编码可能需要额外的字体映射处理。如果是双栏排版检查阅读顺序判断是否正确Docling 大部分情况能处理对但极端版面可能需要手动干预。排查方法先用 Docling 导出 JSON看每个文本块的坐标和阅读顺序。如果发现左右栏内容交错说明阅读顺序判断有问题。这种情况可以尝试调整版面分析模型的参数或者把文档预处理成单栏。6.2 表格识别错误表格识别出错的表现是行列错位、合并单元格丢失、跨页表格断裂。Docling 的表格模型对规整表格效果好对无边框、嵌套、跨页的复杂表格可能出错。我的经验是对于特别重要的表格解析完之后人工抽查一遍。如果错误率高可以考虑针对这类表格单独训练或微调模型。另外跨页表格可以在预处理阶段先合并页面再交给 Docling 解析。6.3 处理速度慢的优化思路CPU 上跑大批量文档确实慢。优化方向有几个关掉不需要的环节比如原生 PDF 关 OCR、用 GPU 加速、批量并发处理、把文档按复杂度分流。我实测下来一份 50 页的原生 PDFCPU 上大概要几十秒GPU 上能压到几秒。如果文档量在几千份以上强烈建议上 GPU。6.4 常见问题速查表问题现象可能原因排查方法解决思路输出空白扫描件未开OCR检查PDF是否为图片型开启OCR选项文字乱码字体编码问题查看原始PDF字体配置字体映射阅读顺序错乱复杂版面检查JSON中坐标预处理为单栏表格行列错位复杂表格结构对比原表格人工校正或微调页眉页脚混入未过滤检查文本块标签按标签过滤处理速度慢CPU算力不足监控资源占用上GPU或分流章节层级丢失标题识别失败检查标题标签调整识别阈值避坑技巧批量处理之前先拿几份有代表性的文档做小样本测试确认解析质量再全量跑。全量跑完之后随机抽查 5% 的结果统计错误率。这个习惯能帮你省下大量返工时间。7. 几个容易被忽略的实操细节7.1 页眉页脚的过滤策略Docling 会把页眉页脚识别出来并打上标签但默认导出的时候可能还是会带上。在切 chunk 之前一定要按标签过滤掉这些内容。判断方法很简单如果某个文本块在每一页的相同位置都出现基本就是页眉页脚。def is_header_footer(item, page_height): prov item.get(prov, [{}])[0] bbox prov.get(bbox, {}) top bbox.get(t, 0) bottom bbox.get(b, 0) # 页面顶部5%或底部5%区域且文本很短 if top page_height * 0.05 or bottom page_height * 0.95: if len(item.get(text, )) 50: return True return False7.2 图片区域的标注与利用Docling 会识别出图片区域并标注位置。对于 RAG 来说图片本身不能直接 embedding但图片的位置信息有用。如果一段正文引用了如下图你可以通过位置关系把图片和正文关联起来。后续如果要做多模态 RAG这些图片可以单独走图像理解模型生成描述再和正文一起入库。7.3 多文档去重与版本管理企业知识库里经常有同一份文档的多个版本。Docling 解析出来的结构化内容可以用于做版本比对。比如对比两个版本的章节结构快速定位改了哪些条款。这个能力在合同管理和方案评审场景下很有价值。实现思路是把两个版本的 section_path 和内容做 diff输出变更列表。对于只改了数字或日期的可以进一步做细粒度比对。8. 从解析到检索的完整链路回顾把 Docling 接进 RAG 管线完整的链路是这样的文档进来Docling 做版面分析和结构重建输出带元信息的结构化对象然后按章节和段落切 chunk表格转自然语言过滤页眉页脚接着每个 chunk 带上页码和章节路径走 embedding 入库检索的时候向量召回加元信息过滤返回带溯源信息的结果最后把召回内容连同章节上下文一起喂给生成模型。这条链路里Docling 承担的是最前端的结构重建工作。它的输出质量直接决定了后面所有环节的效果上限。我在多个项目里对比过用 Docling 替换掉传统的纯文本提取方案之后检索的准确率有明显提升尤其是涉及表格和精确条款查询的场景。当然Docling 不是银弹。它对算力有要求对特别复杂的版面也可能出错需要配合人工抽查和针对性优化。但就目前开源方案而言它在文档结构化和 RAG 适配这块确实做到了一个很实用的平衡点。如果你正在做 RAG 项目尤其是涉及合同、方案、招标文件这类结构化要求高的文档我建议花点时间把 Docling 跑通试试。解析这一环做扎实了后面的检索和生成会省心很多。