跳到主要内容

PDF 识别流水线:逐阶段的模型

30 秒导读: 一页 PDF 里没有"段落""表格""标题"这些概念——只有像素和散落的字符。Docling 的 StandardPdfPipeline 把每一页依次交给一串各司其职的模型:先取页图、补 OCR、检测版面框、识别表结构,再把框组装成元素、排出阅读顺序、定标题层级,最后对图片做富化。本章逐个讲这些模型:它们统一的调用接口流水线顺序、每个模型解决的小问题关键源码

本章聚焦 docling/models/stages/ 下被 StandardPdfPipeline 依次调用的各阶段模型——这是整个项目工程含量最高的地方。流水线的线程编排(多线程分阶段、背压、超时)不在这里讲,见 03-pipelines.md;产物 DoclingDocument 的数据模型与 RAG 导出见 05-datamodel-output-rag.md


1. 这是什么(零基础也能懂)

一句话定义: 一条识别流水线——把"一页 PDF"从像素/字符,一步步还原成结构化的"文档"。

它要解决的问题。 PDF 是"打印格式",不是"结构格式"。文件里存的是"在坐标 (x,y) 画字符 A、画一条线、贴一张位图",没有"这是标题""这是表格第 2 行第 3 列"这类信息。要做 RAG / 检索,就必须把这些语义结构重新识别出来。

怎么解决:分工。 Docling 不用一个大模型硬吞整页,而是排一支专家小队,每人只干一件小事,产物交给下一个人:

阶段模型干的那一件事
page_preprocessing渲染页图 + 抽 PDF 自带的文本单元(cell)
ocr给"图片型"区域补文字(扫描件、位图)
layout检测版面框:哪块是标题/正文/表格/图
table_structure把"表格框"识别成行列单元格
page_assemble把框 + 文字组装成 TextElement / Table / Figure
reading_order排出人类阅读顺序,生成 DoclingDocument
heading_hierarchy给标题定层级(用 PDF 书签/编号/字号)
enrichment(富化)事后加料:代码/公式、图片分类、图片描述、图表转表

一句话直觉: 像一条装配线——毛坯(一页像素)从一端进,每个工位拧一个螺丝(OCR、版面、表格……),另一端出来一个装好的文档。工位之间只靠一个标准托盘(Page 对象)传递,谁也不用知道上一位怎么干的。


2. 顶层全景:统一接口 + 固定顺序

2.1 所有阶段模型长一个样:BasePageModel

这是整条流水线能"即插即换"的关键。每个页级模型都实现同一个接口——吃 (ConversionResult, 一批 Page),吐"同一批 Page"(就地改过):

# docling/models/base_model.py:38
class BasePageModel(ABC):
@abstractmethod
def __call__(
self, conv_res: ConversionResult, page_batch: Iterable[Page]
) -> Iterable[Page]:
pass

因为签名统一,流水线可以把模型当积木塞进线程阶段,循环调用而不关心里面是 OCR 还是版面检测(见 03 的线程编排)。conv_res 是整份文档的上下文(用来记时、记信心分、报错),page_batch 是要处理的页;模型把预测结果挂到 page.predictions.* 上再 yield page

记住这条铁律:页级模型不返回新对象,而是就地丰富 Page 并原样 yield。后面每个模型都遵守它。

2.2 富化模型是另一个接口:GenericEnrichmentModel

图片分类、代码公式、图表这类"针对某个已生成的文档元素"的模型,吃的不是 Page 而是 DoclingDocument 里的 NodeItem:

# docling/models/base_model.py:146
class GenericEnrichmentModel(ABC, Generic[EnrichElementT]):
elements_batch_size: int = settings.perf.elements_batch_size

def is_processable(self, doc, element) -> bool: ... # 这个元素归我管吗?
def prepare_element(self, conv_res, element): ... # 备料(可能裁剪出图像)
def __call__(self, doc, element_batch) -> Iterable[NodeItem]: ... # 处理并回填

两个接口的分工很清楚:

接口何时跑吃什么典型成员
BasePageModel识别阶段(逐页)一批 Pagepreprocessing / ocr / layout / table / assemble
GenericEnrichmentModel富化阶段(逐元素)DoclingDocumentNodeItemcode_formula / picture_classifier / picture_description / chart

reading_orderheading_hierarchy 两个"文档级"模型两边都不属于——它们吃整个 conv_res / DoclingDocument,是普通类(见 §5、§6)。

2.3 固定的流水线顺序

StandardPdfPipeline._init_models 一次性建好所有重模型(docling/pipeline/standard_pdf_pipeline.py:590),然后 _create_run_ctx 把前五个页级模型串成线程阶段,顺序写死在 wiring 里:

# docling/pipeline/standard_pdf_pipeline.py:727
preprocess.add_output_queue(ocr.input_queue)
ocr.add_output_queue(layout.input_queue)
layout.add_output_queue(table.input_queue)
table.add_output_queue(assemble.input_queue)
assemble.add_output_queue(output_q)

组装完的页汇总后,再在 _assemble_document 里跑两个文档级模型(standard_pdf_pipeline.py:1008):

conv_res.document = self.reading_order_model(conv_res) # 排序 + 生成 DoclingDocument
conv_res.document = self.heading_hierarchy_model(conv_res) # 定标题层级

一张图看清全程(从左到右,页图进、文档出;虚线是"事后"的富化):

一页 PDF


┌───────────────┐ 取页图 + 抽 PDF 文本单元(cell)
│ preprocess │ PagePreprocessingModel
└──────┬────────┘

┌───────────────┐ 给图片型区域补文字
│ ocr │ BaseOcrModel(easyocr/tesseract/rapid/mac/auto…)
└──────┬────────┘

┌───────────────┐ 检测版面框 → Cluster(标题/正文/表/图)
│ layout │ LayoutModel.predict_layout
└──────┬────────┘

┌───────────────┐ 表格框 → 行列单元格
│ table │ TableStructureModel.predict_tables
└──────┬────────┘

┌───────────────┐ Cluster → TextElement/Table/Figure
│ assemble │ PageAssembleModel
└──────┬────────┘
▼ (所有页汇总)
┌───────────────┐ 阅读序 + 生成 DoclingDocument
│ reading_order │ ReadingOrderModel
└──────┬────────┘

┌───────────────┐ 标题层级(书签 > 编号 > 字号)
│ heading_hier. │ HeadingHierarchyModel
└──────┬────────┘

DoclingDocument ┈┈┈▶ 富化(逐元素,可选)
code_formula / picture_classifier
picture_description / chart_extraction

3. page_preprocessing:取页图 + 抽文本单元

它要解决的小问题: 后面所有模型都需要两样东西——页面的位图(给视觉模型看)和 PDF 自带的文字单元(programmatic cells,能直接读的字)。这个阶段就是把这两样准备好挂到 Page 上。

__call__ 对每页做两步:渲染图 + 解析 cell(docling/models/stages/page_preprocessing/page_preprocessing_model.py:37):

# page_preprocessing_model.py:44
with TimeRecorder(conv_res, "page_parse"):
page = self._populate_page_images(page) # 渲染并缓存页图
if not self.options.skip_cell_extraction:
page = self._parse_page_cells(conv_res, page) # 从 backend 抽文本单元

_parse_page_cells 还顺手给这页打一个解析质量分:对每个 cell 的文本跑 rate_text_quality,再取 10% 分位数作为 parse_score(page_preprocessing_model.py:85)。质量分的作用是标记"这页 PDF 抽出来的字是不是垃圾"——比如出现 GLYPH<...> 占位符、 替换符、/G12/G13 这种字体乱码就直接判 0 分:

# page_preprocessing_model.py:120 rate_text_quality
blacklist_chars = ["�"]
if (any(text.find(c) >= 0 for c in blacklist_chars)
or self.GLYPH_RE.search(text) # GLYPH<00A3>
or self.SLASH_G_RE.search(text) # /G12/G13...
or self.SLASH_NUMBER_GARBAGE_RE.match(text)):
return 0.0

关键细节: page.get_image(scale=1.0) 会把页图放进内部缓存,后续 layout/table 阶段再取同一张图就不用重渲染。skip_cell_extraction 是给"纯 VLM 处理"留的旁路——那条路不需要 PDF 文本单元。


4. OCR:多引擎 + 工厂选型

它要解决的小问题: PDF 里有些内容是图片(扫描件、截图、贴的位图),没有文字层。OCR 阶段的活是:找出这些图片区域,识别里面的字,再把新字并回页面——同时别覆盖已经有的、可靠的 PDF 原生文字。

4.1 共同骨架:BaseOcrModel

所有 OCR 引擎(easyocr / tesseract / rapid / mac / nemotron / kserve)都继承 BaseOcrModel,共享两段核心逻辑,只有"真正识别文字"那步各写各的。

第一步:算出该 OCR 哪些矩形。 get_ocr_rects 把页面上所有位图矩形画进一张二值图,膨胀 10 像素让相邻位图连成块,再取连通域当作候选 OCR 区域(docling/models/base_ocr_model.py:40)。三种结果:

位图覆盖率返回
force_full_page_ocr 或 覆盖率 > 0.75整页一个大矩形(当扫描件处理)
高于 bitmap_area_threshold各个位图区域的框
太低空(这页基本是文字,不 OCR)

第二步:去重合并。 OCR 出来的字若和已有的 PDF 原生字空间重叠,就丢掉 OCR 版本——用 R-tree 做空间索引加速这个"是否与已有 cell 相交"的判断:

# base_ocr_model.py:116 _filter_ocr_cells
idx = index.Index(properties=p) # R-tree
for i, cell in enumerate(programmatic_cells):
idx.insert(i, cell.rect.to_bounding_box().as_tuple())
# 任何与已有 cell 相交的 OCR cell 都被丢弃(弱但有效的判据)

例外:force_full_page_ocr 模式下 PDF 原生字被认为不可靠,直接全用 OCR 结果替换(base_ocr_model.py:173 _combine_cells)。

4.2 auto:按平台+已装库自动挑引擎

OcrAutoModel 是"我不想选引擎"的默认:构造时按优先级逐个 try-import,谁装了就用谁(docling/models/stages/ocr/auto_ocr_model.py:27)。选择级联(命中即停):

darwin? → 试 ocrmac(苹果系统自带 OCR)
linux? → 试 nemotron
仍无? → 试 rapidocr(onnxruntime 后端)
仍无? → 试 easyocr
仍无? → 试 rapidocr(torch 后端)
仍无? → 警告:没有可用 OCR 引擎

跑的时候它只是个转发壳——把 __call__ 委托给选中的真引擎(auto_ocr_model.py:145)。没启用或没引擎就原样放行页面。

4.3 引擎怎么被"注册"和"选中":OCR 工厂

具体用哪个引擎,由 pipeline_options.ocr_options类型决定——EasyOcrOptionsEasyOcrModel,TesseractOcrOptionsTesseractOcrModel,以此类推。这套"选项类型 → 模型类"的映射由工厂管理(详见 §8)。_make_ocr_model 就是拿工厂按选项造实例:

# standard_pdf_pipeline.py:655 _make_ocr_model
factory = get_ocr_factory(
allow_external_plugins=self.pipeline_options.allow_external_plugins
)
return factory.create_instance(options=self.pipeline_options.ocr_options, ...)

5. layout:版面框检测 → Cluster

它要解决的小问题: 页面上一堆文字和图,哪块是标题、哪块是正文、哪块是表格、哪块是图?这是"读懂文档结构"的第一步,也是后面所有阶段的骨架。

5.1 统一接口在基类,推理在子类

BaseLayoutModel.__call__ 只是薄薄一层——把批量页转给 predict_layout,再把预测挂回每页(docling/models/base_layout_model.py:29):

# base_layout_model.py:34
pages = list(page_batch)
predictions = self.predict_layout(conv_res, pages)
for page, prediction in zip(pages, predictions):
page.predictions.layout = prediction
yield page

真正的检测在 LayoutModel.predict_layout:调 docling_ibm_modelsLayoutPredictor.predict_batch 得到一批带 label + 置信度 + bbox 的框,包成 Cluster(docling/models/stages/layout/layout_model.py:154):

# layout_model.py:201
for ix, pred_item in enumerate(page_predictions):
label = DocItemLabel(pred_item["label"].lower().replace(" ", "_")...)
cluster = Cluster(id=ix, label=label,
confidence=pred_item["confidence"],
bbox=BoundingBox.model_validate(pred_item), cells=[])

5.2 后处理才是精华:把字塞进框

原始框只是"某处有个标题",里面哪些文字属于它还没定。LayoutPostprocessor 负责把 preprocessing 抽出的 cell 分配进各个 cluster、消解重叠、修正边界(layout_model.py:220),并就地更新 page.cells / page.parsed_page。同时算出这页的 layout_scoreocr_score(信心分)。

LayoutModel 顶部那几张 label 清单(layout_model.py:29 TEXT_ELEM_LABELS / TABLE_LABELS / FIGURE_LABEL…)是贯穿后续阶段的分类词表——page_assemble 就靠它判断一个 cluster 该变成文本、表还是图。


6. table_structure:把表格框识别成行列

它要解决的小问题: layout 只说"这里有个表"。但表格真正的价值在结构——几行几列、哪个单元格跨行跨列、哪些是表头。这一步用 TableFormer 模型把表格图 + 里面的文字单元还原成结构化单元格。

TableStructureModel.predict_tables 只挑 layout 标成 TABLE/DOCUMENT_INDEX 的 cluster 处理(docling/models/stages/table_structure/table_structure_model.py:178)。对每个表:先取表内的词级 cell 当 token,连同放大 2 倍(≈144dpi)的表格图喂给预测器:

# table_structure_model.py:258
tf_output = self.tf_predictor.multi_table_predict(
page_input, [tbl_box], do_matching=self.do_cell_matching
)
# ...
num_rows = table_out["predict_details"].get("num_rows", 0)
num_cols = table_out["predict_details"].get("num_cols", 0)
otsl_seq = table_out["predict_details"].get("prediction", {}).get("rs_seq", [])

关键细节:

  • do_cell_matching=True 时把模型输出的单元格对齐回 PDF 原生文字;False 时直接从 backend 按 bbox 取文字(get_text_in_rect)。
  • 产物 otsl_seqOTSL 表结构序列(一种紧凑的表格结构标记语言),连同 TableCell 列表存进 TableStructurePrediction.table_map,键是 cluster id——page_assemble 靠这个 id 把结构接回对应的表框。
  • mode(fast / accurate)决定加载哪套权重;MPS 上被强制降到 CPU(table_structure_model.py:83,注释说 MPS 更慢原因未知)。

7. page_assemble:Cluster → TextElement / Table / Figure

它要解决的小问题: 前面得到的是一堆"框 + 预测"。这一步把它们组装成语义元素——每个 cluster 按 label 变成一个 TextElementTableFigureElementContainerElement,填进 page.assembled

PageAssembleModel.__call__ 遍历这页的 cluster,按 §5.1 的 label 词表分派(docling/models/stages/page_assemble/page_assemble_model.py:153):

cluster.label 属于组装成补的料
TEXT_ELEM_LABELSTextElement拼接 cell 文字 + 匹配超链接
TABLE_LABELSTabletablestructure.table_map 接回结构
FIGURE_LABELFigureElement从图片分类结果接回(若有)
CONTAINER_LABELSContainerElement表单 / KV 区

文字元素这条路最典型——把 cluster 里非空 cell 的文字收集起来、清洗、找超链接:

# page_assemble_model.py:172
if cluster.label in LayoutModel.TEXT_ELEM_LABELS:
textlines = [cell.text.replace("\x02", "-").strip()
for cell in cluster.cells if len(cell.text.strip()) > 0]
text = self.sanitize_text(textlines) # 去连字符、规范标点、展开连字
hyperlink = self._match_hyperlink(cluster.bbox, page)
text_el = TextElement(label=cluster.label, id=cluster.id, text=text, ...)

精华藏在 sanitize_text(page_assemble_model.py:106):

  • 断词还原:上一行以 - 结尾且两侧都是字母数字,判为跨行断词,去掉连字符拼起来;否则行尾补空格。
  • 连字(ligature)展开:把 fi fl ff 这类排版连字符还原成 fi fl ff,并吸收 PDF 解析器在连字和后半词之间插的假空格("fi eld" → "field"),靠 _LIGATURE_MAP + _LIGATURE_RE(page_assemble_model.py:31)。
  • 标点规范:弯引号 → 直引号、· 等。

表和图走"接回"逻辑:Tablepage.predictions.tablestructure.table_map.get(cluster.id) 取结构,取不到就退化成一个空壳表(page_assemble_model.py:200)——保证元素一定存在,细节可缺。


8. reading_order:定序 + 生成 DoclingDocument

它要解决的小问题: 到这里所有页的元素都有了,但它们是按检测顺序排的,不是人读的顺序(多栏排版、图文穿插会乱)。这一步排出正确阅读序,并首次生成最终产物 DoclingDocument

ReadingOrderModel 是文档级模型(不吃 Page 吃整个 conv_res)。__call__docling_ibm_modelsReadingOrderPredictor 一次做四件事(docling/models/stages/reading_order/readingorder_model.py:443):

# readingorder_model.py:445
page_elements = self._assembled_to_readingorder_elements(conv_res)
sorted_elements = self.ro_model.predict_reading_order(page_elements=page_elements)
el_to_captions_mapping= self.ro_model.predict_to_captions(sorted_elements=sorted_elements)
el_to_footnotes_mapping = self.ro_model.predict_to_footnotes(sorted_elements=sorted_elements)
el_merges_mapping = self.ro_model.predict_merges(sorted_elements=sorted_elements)

四个预测的含义:

预测干什么
predict_reading_order排出元素的先后顺序
predict_to_captions哪些文字是某图/表的标题(caption)
predict_to_footnotes哪些是脚注
predict_merges哪些相邻元素该合并成一个(跨行/跨栏的同一段)

然后 _readingorder_elements_to_docling_doc 按排好的序,把每个元素 add_text / add_table / add_picture 进一个新建的 DoclingDocument(readingorder_model.py:153)。caption/footnote/merge 三个映射在这里被消费:被标为 caption 的元素不单独出现,而是挂到父图/表下;被 merge 的合并进前一个元素——_merge_elements 还专门处理软连字符 U+00AD(拼接时去掉、不加空格,readingorder_model.py:429)。

产物边界: 这一步之后 conv_res.document 才第一次是完整的 DoclingDocument。它的数据模型和导出见 05-datamodel-output-rag.md


9. heading_hierarchy:给标题定层级

它要解决的小问题: layout 只会把区域标成 SECTION_HEADER,不带层级——所以 PDF 路径出来的每个标题默认都是 level=1,层级被压平了("第一部分""1.1 小节"挤在同一层)。这一步专门重排标题的 level

HeadingHierarchyModel 紧跟 reading_order 跑,只改写标题的 level(外加一个特例:把被误判成 list-item 的标题提升为标题),不增删不重排(docling/models/stages/heading_hierarchy/heading_hierarchy_model.py:394)。三个信号按优先级叠加,高优先级不被低的覆盖:

优先级信号依据源码
1bookmarksPDF 书签/目录——文档自己声明的层级,最权威_infer_from_bookmarks:309
2numbering编号规则 PART I → 1. → 1.1 → (a) → (i)_infer_from_numbering:177
3style字号(从解析出的 cell 高度近似),越大层级越高_infer_from_style:226

叠加逻辑很干净——书签先播种,后面两个只用 setdefault 补空,绝不覆盖:

# heading_hierarchy_model.py:454
for i, heading in enumerate(headings): # 1. 书签最权威,先坐实
level = bookmark_levels.get(id(heading))
if level is not None:
levels[i] = level
if self.options.use_numbering: # 2. 编号补空
for i, level in _infer_from_numbering(headings, self.options).items():
levels.setdefault(i, level)
if self.options.use_style and parsed_pages: # 3. 字号兜底
for i, level in _infer_from_style(...).items():
levels.setdefault(i, level)

书签来源与时机: 书签(PDF outline)在 backend 还开着时就被 _build_document 提前抽出,存进 conv_res._pdf_outline(standard_pdf_pipeline.py:779),只在 use_bookmarks 时才抽。书签靠模糊匹配(标题 + 页码)对上检测到的标题——_match_score 会带/不带前缀编号各比一次,并对"一方包含另一方"加分(书签常被截断,heading_hierarchy_model.py:278)。跨页匹配要求更高阈值。一个被书签确信匹配的 list-item 会被就地提升成 SectionHeaderItem(layout 常把标题误判成列表项)。

编号解析的巧处: _parse_markerPART/CHAPTER/§/1.1/(a)/IV. 解析成家族 + 深度;单字母 I. 既像罗马数字又像字母,_resolve_ambiguous全文证据消歧(文档里有 II/III 就判罗马,有 B/F 就判字母,heading_hierarchy_model.py:133)。最后把实际出现的 (家族, 深度) 压缩成连续层级,这样从"1." 开头的文档不会被迫从 level 2 起步。


10. 富化阶段:事后给元素加料

它要解决的小问题: 有些信息不适合在识别流水线里做——它们针对具体的文档元素(某张图、某段代码),而且往往要跑重型 VLM。这些放到富化阶段:文档已经成型后,再逐元素处理。

富化模型是 GenericEnrichmentModel(§2.2)。_enrich_document 的循环很规整——对每个富化模型,遍历文档元素、prepare_element 备料、分批调用(docling/pipeline/base_pipeline.py:111):

# base_pipeline.py:123
for model in self.enrichment_pipe:
for element_batch in chunkify(_prepare_elements(conv_res, model),
model.elements_batch_size):
for element in model(doc=conv_res.document, element_batch=element_batch):
pass # 必须耗尽(生成器有副作用:就地回填 meta)

prepare_element 对图像类富化(BaseItemAndImageEnrichmentModel)还会从页图裁剪出元素图并按 expansion_factor 外扩一点(base_model.py:181)——Word/HTML 那种没有页图但有内嵌图的情况也兼顾了。

enrichment_pipe 的成员和顺序:code_formula 由 StandardPdfPipeline._init_models 插到最前(standard_pdf_pipeline.py:631),其余四个由基类 ConvertPipeline 装配(base_pipeline.py:177):

富化模型处理谁(is_processable)干什么
CodeFormulaVlmModelCodeItem 或 label=FORMULA 的 TextItemVLM 重识别代码/公式文本,提取代码语言
DocumentPictureClassifierPictureItem给图片分类(bar_chart / pie_chart / …)
PictureDescriptionBaseModelPictureItem给图片生成文字描述(本地 VLM 或 API)
ChartExtractionModelGraniteVision(V4)被分类为图表的 PictureItem把图表转成数据表(chart→CSV)

10.1 code_formula:VLM 重认代码与公式

CodeFormulaVlmModel.is_processable 只接代码块和公式(docling/models/stages/code_formula/code_formula_vlm_model.py:116)。__call__ 按元素类型给不同提示词(<code> / <formula>),批量跑 VLM engine,再后处理:剥掉 </code>/</formula> 等特殊 token,并从 <_python_> 这类前缀里抽出代码语言(code_formula_vlm_model.py:154 _extract_code_language)。

10.2 picture_classifier:图片分类(图表富化的前置)

DocumentPictureClassifier.__call__ 把每张图跑分类引擎,结果写进 item.meta.classification(docling/models/stages/picture_classifier/document_picture_classifier.py:123)。这一步是图表提取的前置条件——所以只要开了 do_chart_extraction,分类器会被自动启用(base_pipeline.py:160)。

10.3 picture_description:图片描述

PictureDescriptionBaseModel.__call__ 有两道过滤闸:图太小(面积占比低于 picture_area_threshold)就跳过;若配了分类白/黑名单,不匹配也跳过(docling/models/picture_description_base_model.py:65)。通过的才送去 _annotate_images(子类实现:本地 VLM 或调 API),描述写进 item.meta.description

10.4 chart_extraction:图表 → 数据表

ChartExtractionModelGraniteVision 只处理已被分类为 bar_chart/pie_chart/line_chart 的图(docling/models/stages/chart_extraction/granite_vision.py:98)——这就是为什么它依赖 10.2。__call__ 用 Granite-Vision 把图表图转成 CSV,再解析成 TableData 存进 item.meta.tabular_chart(granite_vision.py:216)。V4 版本更进一步,可同时跑 <chart2csv>/<chart2summary>/<chart2code> 三种提示(granite_vision.py:381)。


11. 工厂 + 插件:可插拔选型怎么工作

前面反复出现"由选项类型决定用哪个模型"。这套可插拔选型models/factories/ 的工厂 + pluggy 插件机制实现。它让"换 OCR 引擎""换版面模型"变成改一行选项,甚至第三方能塞进自己的引擎。

11.1 核心:选项类型 → 模型类 的字典

BaseFactory 内部就是一张 {选项类型: 模型类} 表。register 用模型的 get_options_type() 当键登记;create_instance传入选项的类型查表造实例(docling/models/factories/base_factory.py:54):

# base_factory.py:54 create_instance
_cls = self._classes[type(options)] # EasyOcrOptions -> EasyOcrModel
return _cls(options=options, **kwargs)

每类模型有个专属工厂,只是给了个"插件属性名":OcrFactory"ocr_engines"LayoutFactory"layout_engines"(factories/ocr_factory.py:9factories/layout_factory.py:5)。

11.2 内置引擎从哪来:defaults 插件

内置引擎清单写在 models/plugins/defaults.py——每个函数返回 {属性名: [模型类...]}:

# docling/models/plugins/defaults.py:11
return {"ocr_engines": [OcrAutoModel, EasyOcrModel, KserveV2OcrModel,
NemotronOcrModel, OcrMacModel, RapidOcrModel,
TesseractOcrModel, TesseractOcrCliModel]}

layout_engines / table_structure_engines / picture_description 同理(defaults.py:45/63/25)。

11.3 加载与外部插件闸门

get_*_factory(带 @lru_cache,建一次)在 factories/__init__.py:14 建工厂并 load_from_plugins。加载时通过 pluggy 的 setuptools entrypoints 发现所有插件——但默认只信任 docling 自带的:

# base_factory.py:101 load_from_plugins
if not allow_external_plugins and not plugin_module_name.startswith("docling."):
logger.warning("... will not be loaded because ... allow_external_plugins=false")
continue

allow_external_plugins 一路从 pipeline_options 透传到 get_ocr_factory(...) / get_layout_factory(...)(standard_pdf_pipeline.py:596/656/605)。这是安全边界:第三方引擎默认不加载,显式开启才行。

选型全景(以 OCR 为例):

pipeline_options.ocr_options (类型 = EasyOcrOptions)


get_ocr_factory(allow_external_plugins) # 建工厂 + 装插件
│ defaults.ocr_engines() 注册内置引擎
│ register(): key = cls.get_options_type()

factory.create_instance(options=ocr_options) # 按 type(options) 查表
│ _classes[EasyOcrOptions] → EasyOcrModel

EasyOcrModel 实例

12. 巧妙之处(可带走的技术)

  • 统一页级接口让流水线成为"积木架":BasePageModel.__call__(conv_res, page_batch) -> pages(base_model.py:38)签名统一,03 的线程编排才能对任意模型一视同仁。
  • 就地丰富、原样 yield:页级模型不造新对象,把预测挂到 page.predictions.*,降低对象搬运成本。
  • OCR 去重用 R-tree:空间相交判断从 O(n²) 降到近似对数级(base_ocr_model.py:119)。
  • 信心分沿途累积:preprocessing 出 parse_score、layout 出 layout_score/ocr_score、table 出 table_score,最后聚合成文档级信心——parse_score 取 10% 分位数,专门放大"最差那批页"的问题(page_preprocessing_model.py:85)。
  • 标题层级三信号带优先级、只 setdefault:书签 > 编号 > 字号,高优先级不被覆盖,且把稀疏层级压缩成连续级(heading_hierarchy_model.py:454)。
  • 富化按元素分类精准触发:chart 只碰被分类为图表的图,description 先按面积/分类过滤——省掉大量无谓的 VLM 调用(granite_vision.py:98picture_description_base_model.py:79)。
  • 外部插件默认关闭:allow_external_plugins 是显式安全闸门(base_factory.py:101)。

13. 边界与局限

  • 表格接不回就退化成空壳表:table_map 里没有对应结构时,page_assemble 造一个 0 行 0 列的 Table(page_assemble_model.py:200)——元素在但结构缺。
  • 字号信号很粗:_infer_from_style 用解析 cell 的中位高度近似字号,并按四舍五入分桶(heading_hierarchy_model.py:226),对字号不规律的文档不可靠,所以它只当兜底。
  • MPS 上表结构强制走 CPU:table_structure_model.py:83 注释直言"不知为何 MPS 更慢",这是已知取舍。
  • 富化多为重型模型:code_formula / description / chart 都要 VLM,默认关闭(各自 do_* 开关),开启后显著变慢。
  • predict_mergesorig 合并不完整:_merge_elements 里两处 TODO 承认合并元素的 orig 字段处理不全(readingorder_model.py:434/437)。

14. 代码地图(导航索引)

主题文件路径符号
页级模型统一接口docling/models/base_model.pyBasePageModel.__call__
富化模型接口docling/models/base_model.pyGenericEnrichmentModelBaseItemAndImageEnrichmentModel.prepare_element
建模型 + 阶段顺序docling/pipeline/standard_pdf_pipeline.pyStandardPdfPipeline._init_models_create_run_ctx
文档级两步 + 富化调用docling/pipeline/standard_pdf_pipeline.py_assemble_document(reading_order/heading 调用)
富化循环 + pipe 装配docling/pipeline/base_pipeline.pyBasePipeline._enrich_documentConvertPipeline.__init__
取页图 + 抽 cell + 质量分docling/models/stages/page_preprocessing/page_preprocessing_model.pyPagePreprocessingModelrate_text_quality
OCR 骨架(区域/去重)docling/models/base_ocr_model.pyBaseOcrModel.get_ocr_rects_filter_ocr_cells_combine_cells
OCR 自动选引擎docling/models/stages/ocr/auto_ocr_model.pyOcrAutoModel
一个 OCR 引擎实例docling/models/stages/ocr/easyocr_model.pyEasyOcrModel
版面接口 + 推理docling/models/base_layout_model.py, docling/models/stages/layout/layout_model.pyBaseLayoutModel.__call__LayoutModel.predict_layout
版面 label 词表docling/models/stages/layout/layout_model.pyLayoutModel.TEXT_ELEM_LABELSTABLE_LABELS
表结构识别docling/models/stages/table_structure/table_structure_model.pyTableStructureModel.predict_tables
组装元素 + 文本清洗docling/models/stages/page_assemble/page_assemble_model.pyPageAssembleModelsanitize_text
阅读序 + 生成 DoclingDocumentdocling/models/stages/reading_order/readingorder_model.pyReadingOrderModel.__call___readingorder_elements_to_docling_doc
标题层级三信号docling/models/stages/heading_hierarchy/heading_hierarchy_model.pyHeadingHierarchyModel.assign_heading_levels_infer_from_bookmarks_infer_from_numbering_infer_from_style
代码/公式富化docling/models/stages/code_formula/code_formula_vlm_model.pyCodeFormulaVlmModel
图片分类富化docling/models/stages/picture_classifier/document_picture_classifier.pyDocumentPictureClassifier
图片描述富化docling/models/picture_description_base_model.pyPictureDescriptionBaseModel
图表提取富化docling/models/stages/chart_extraction/granite_vision.pyChartExtractionModelGraniteVisionChartExtractionModelGraniteVisionV4
工厂选型核心docling/models/factories/base_factory.pyBaseFactory.create_instanceregisterload_from_plugins
工厂入口(lru_cache)docling/models/factories/init.pyget_ocr_factoryget_layout_factoryget_table_structure_factory
内置引擎清单docling/models/plugins/defaults.pyocr_engineslayout_enginestable_structure_enginespicture_description

相关章节: index.md(Docling 总览) · 01-entry-dispatch.md(入口与分派) · 02-backends.md(后端层) · 03-pipelines.md(流水线层与线程编排) · 05-datamodel-output-rag.md(数据模型与 RAG 输出)