prebuilt-layout model), a richer package that recovers what fitz cannot. That gap is where we start.本文是《企业文档智能》系列中的解析伴侣,该系列从四个模块构建企业RAG系统。第5篇文章(文档解析)使用PyMuPDF(fitz)构建了解析器。本伴侣保持相同目标和相同的关系表,并将引擎替换为Azure Layout(预构建布局模型),这是一个更丰富的包,能够恢复fitz无法处理的内容。我们正是从这一差距开始。

PyMuPDF (fitz) is fast, free, and exact on clean prose. It also goes blind in three places, and each one is where enterprise RAG quietly breaks.PyMuPDF(fitz)速度快、免费且对清晰文本处理精确。但它在三个地方会失效,而每个失效点正是企业RAG悄然崩溃之处。
The table on page 14 of a contract. Fitz reads the cells one by one and concatenates them. The column structure is gone. “Renewal fee 500 Setup fee 200” lands in the chunk. Your model is asked to guess which number is which fee.合同第14页的表格。Fitz逐个读取单元格并拼接它们。列结构消失了。“续费500 设置费200”被放入块中。你的模型需要猜测哪个数字对应哪种费用。
The scanned amendment glued to the end of the document. Fitz reads the native pages and returns empty strings on the scanned ones. The user gets no answer on the amendment because the parser never read it.扫描的修正案粘贴在文档末尾。Fitz 读取原始页面,对扫描页面返回空字符串。用户无法获得修正案的答案,因为解析器从未读取它。
The figure with text inside. A chart with axis labels. A signed seal stamp. A screenshot of a spreadsheet. Fitz returns the bbox of the image. The text inside is gone.包含文本的图形。带有轴标签的图表。带有签名的印章。电子表格的截图。Fitz 返回图像的边界框。其中的文本丢失了。
Azure Document Intelligence reads all three. It’s a proprietary Microsoft Azure cloud service governed by Microsoft’s Online Services Terms. The prebuilt-layout model returns native table cells (rows, columns, headers), OCR text for every page (native or scanned), figures with the text inside them, and paragraph roles (title, sectionHeading, figureCaption, tableCaption). One call. The same relational tables as fitz, half of them enriched.Azure Document Intelligence 能读取所有三种情况。这是一个专有的 Microsoft Azure 云服务,受 Microsoft 在线服务条款约束。预构建布局模型返回原生表格单元格(行、列、标题)、每页的 OCR 文本(原生或扫描)、包含文本的图形以及段落角色(标题、章节标题、图形标题、表格标题)。一次调用。与 fitz 相同的关系表,其中一半得到了丰富。
The downstream pipeline does not care which engine produced the dict. Retrieval, generation, annotation read rows. They never read the PDF.下游管道不关心哪个引擎生成了字典。检索、生成、注释读取行。它们从不读取 PDF。

1. Where fitz is blind1. Fitz 的盲区
Four cases. In each one, fitz misses and Azure works.四种情况。在每种情况下,Fitz 失败而 Azure 成功。
1.1. Tables: fitz returns flat words, Azure returns cells1.1. 表格:Fitz 返回扁平单词,Azure 返回单元格
A contract table has rows and columns. The label “Renewal fee” sits in column 1, the value 500 sits in column 2. Fitz reads the page top to bottom and emits one line per text segment. The four cells of a row come back as four loose words. Sometimes the cells from the row below get mixed in if the y-coordinates are close. The chunker downstream sees a soup of words. The row-and-column structure that makes a table a table is gone.合同表格有行和列。标签“续费”位于第1列,值500位于第2列。Fitz从上到下读取页面,每个文本段输出一行。一行的四个单元格返回为四个独立的词。有时,如果y坐标接近,下面行的单元格会混入。下游的分块器看到一堆词。使表格成为表格的行列结构消失了。
Azure’s prebuilt-layout model detects each table as a structured object. result.tables is a list of tables, each with cells indexed by (row_index, column_index). The header row is flagged (cell.kind == "columnHeader"). The cell content is the cell text, exactly as the author typed it. We flatten the table into markdown rows so it lives inside line_df like any other content. A four-cell row “Renewal fee | 500 | Setup fee | 200” becomes one line_df row with that markdown text. The header row gets a | --- | --- | ... | separator so a downstream model reads the structure back.Azure的预建布局模型将每个表格检测为结构化对象。result.tables是一个表格列表,每个表格的单元格由(row_index, column_index)索引。标题行被标记(cell.kind == "columnHeader")。单元格内容就是作者输入的单元格文本。我们将表格展平为markdown行,使其像其他内容一样存在于line_df中。一个四单元格行“续费 | 500 | 安装费 | 200”变成line_df中的一行,包含该markdown文本。标题行会有一个| --- | --- | ... |分隔符,以便下游模型读取结构。
1.2. Images: fitz returns the bbox, Azure returns the text1.2. 图像:fitz返回边界框,Azure返回文本
Many PDFs have figures with text inside them. Architecture diagrams with box labels. Charts with axis ticks and legends. Signed seal stamps. Embedded screenshots of spreadsheets. Fitz returns each image as a bbox and the raw bytes. The text inside is invisible to the parser.许多PDF包含带有文本的图形。带有框标签的架构图。带有轴刻度和图例的图表。盖章印记。嵌入的电子表格截图。Fitz将每个图像返回为边界框和原始字节。其中的文本对解析器不可见。
Azure’s OCR runs on every page, including the pixels inside figure regions. For each figure, we collect every Azure word whose bbox sits inside the figure region and join them as ocr_text. “Multi-Head Attention Concat Linear h” now lives in image_df.ocr_text for the figure on page 4 of the Attention paper. Retrieval can match a question about “multi-head attention” even when the answer is text inside a figure.Azure的OCR对每一页运行,包括图形区域内的像素。对于每个图形,我们收集所有边界框位于图形区域内的Azure词,并将它们合并为ocr_text。“多头注意力 拼接 线性 h”现在存在于Attention论文第4页图形的image_df.ocr_text中。检索可以匹配关于“多头注意力”的问题,即使答案在图形内的文本中。

1.3. Scanned pages: fitz returns nothing, Azure returns OCR1.3. 扫描页面:fitz返回空,Azure返回OCR
A 30-page native contract gets a 10-page scanned amendment glued at the end. Fitz reads the native pages and returns empty strings for the scanned ones. The parser does not flag this. The downstream pipeline silently covers 75% of the document. The user has no idea 25% is missing.一份30页的原生合同末尾附有10页的扫描修订。Fitz读取原生页面,对扫描页面返回空字符串。解析器不会标记这一点。下游管道静默覆盖了文档的75%。用户不知道缺失了25%。
Azure runs OCR on every page regardless of source. Native pages and scanned pages come back through the same result.pages[i].lines path with the same shape. The parsing_method column on line_df lets downstream code tell which engine produced which rows. The parsing_summary dict has a n_pages field that matches the document’s actual page count, not just the pages with native text.Azure对每一页运行OCR,无论来源如何。原生页面和扫描页面通过相同的result.pages[i].lines路径返回,具有相同的结构。line_df上的parsing_method列让下游代码知道哪些行由哪个引擎生成。parsing_summary字典有一个n_pages字段,匹配文档的实际页数,而不仅仅是具有原生文本的页数。

1.4. Captions and headings: fitz uses regex, Azure has explicit roles1.4. 标题和标题:fitz使用正则表达式,Azure有显式角色
Fitz detects figure / table captions by regex on the start of each line (^Figure \d+\b, ^Table \d+\b). It works when captions look like “Figure 2” and misses the rest (“Fig. 2”, multi-line wraps). It also has false positives: a body-text sentence that starts with “Figure 2” gets picked up as a caption when it is a mention.Fitz 通过每行开头的正则表达式(^Figure \d+\b, ^Table \d+\b)检测图形/表格标题。当标题形如“Figure 2”时有效,但会遗漏其他形式(如“Fig. 2”、多行换行)。同时存在误报:正文中以“Figure 2”开头的句子会被误判为标题。

Azure’s paragraphs field has role labels: each paragraph in the result carries a tag like "figureCaption", "tableCaption", "title", or "sectionHeading" that tells us what kind of block it is, without any regex. "figureCaption" and "tableCaption" populate object_registry directly. "title" and "sectionHeading" rebuild the TOC. The tag is Azure’s layout model naming the block’s function; fitz has no equivalent. The (object_type, object_id) join key is still extracted by the same regex on the caption text so cross_ref_df joins back the same way.Azure 的段落字段包含角色标签:结果中的每个段落都带有如“figureCaption”、“tableCaption”、“title”或“sectionHeading”等标签,无需正则即可标识块类型。“figureCaption”和“tableCaption”直接填充 object_registry。“title”和“sectionHeading”重建目录。该标签是 Azure 布局模型对块功能的命名;fitz 没有等效功能。(object_type, object_id) 连接键仍通过相同正则从标题文本提取,因此 cross_ref_df 的连接方式相同。
The TOC is the more interesting case. Fitz’s build_toc_df reads native bookmarks (doc.get_toc()). When the PDF has no native bookmarks, fitz returns an empty TOC. This is the common enterprise case: Word exports, scanned documents, PDFs from form generators. Azure reconstructs the TOC from paragraph roles. Every "title" paragraph becomes a level-1 entry, every "sectionHeading" paragraph becomes level-2. The hierarchy comes from the order they appear. This is not perfect, but it produces a usable TOC where fitz would produce nothing.目录是更有趣的情况。Fitz 的 build_toc_df 读取原生书签(doc.get_toc())。当 PDF 没有原生书签时,fitz 返回空目录。这是常见的企业场景:Word 导出、扫描文档、表单生成器生成的 PDF。Azure 通过段落角色重建目录。每个“title”段落成为一级条目,每个“sectionHeading”段落成为二级条目。层级由出现顺序决定。这并不完美,但能生成可用的目录,而 fitz 则无法生成任何内容。
2. Same contract, richer data2. 相同契约,更丰富的数据
One function. The same tables as parse_pdf, in the same shape. One Azure call shared by every builder. That call is small: point the SDK at the document with one model_id, prebuilt-layout. (The other prebuilt model, prebuilt-read, is OCR only; the layout model is the one that also returns tables, paragraph roles, and reading order.)一个函数。与 parse_pdf 相同的表格,相同结构。每个构建器共享一次 Azure 调用。该调用很小:将 SDK 指向文档,使用一个模型 ID prebuilt-layout。(另一个预构建模型 prebuilt-read 仅用于 OCR;布局模型同时返回表格、段落角色和阅读顺序。)
from azure.ai.documentintelligence import DocumentIntelligenceClient
from azure.ai.documentintelligence.models import AnalyzeDocumentRequest
from azure.core.credentials import AzureKeyCredential
client = DocumentIntelligenceClient(endpoint, AzureKeyCredential(key))
# "Layout" = the prebuilt-layout model (NOT prebuilt-read, which is OCR only)
with open("contract.pdf", "rb") as f:
poller = client.begin_analyze_document(
"prebuilt-layout",
AnalyzeDocumentRequest(bytes_source=f.read()),
)
result = poller.result() # tables, paragraph roles, OCR, reading order
parse_pdf_azure_layout is the Azure twin of parse_pdf: same call shape, same dict of tables out, so every downstream brick reads it without knowing which engine ran. The body is worth a look, because it is the shape every engine in the series follows: make one call, then one small builder per table, and reuse the engine-agnostic builders for the tables that only need line_df.parse_pdf_azure_layout 是 parse_pdf 的 Azure 对应版本:相同的调用结构,相同的输出字典,因此所有下游模块无需知道运行引擎即可读取。其主体值得一看,因为它展示了系列中每个引擎遵循的结构:进行一次调用,然后为每个表格使用一个小型构建器,并重用仅需 line_df 的表格的引擎无关构建器。
def parse_pdf_azure_layout(pdf_path):
result = analyze_pdf(pdf_path) # one call, prebuilt-layout
line_df = azure_layout_pdf_to_line_df(pdf_path, result=result)
image_df = build_image_df_azure_layout(result) # + ocr_text
toc_df = build_toc_df_azure_layout(result) # paragraph roles
object_registry = build_object_registry_azure_layout(result) # role tags
page_df = build_page_df(line_df) # reused fitz builder (line_df only)
cross_ref_df = build_cross_ref_df(line_df) # reused fitz builder (line_df only)
return {"line_df": line_df, "image_df": image_df, "toc_df": toc_df,
"object_registry": object_registry, "page_df": page_df,
"cross_ref_df": cross_ref_df, "span_df": pd.DataFrame(),
"parsing_summary": parsing_summary}
Reading it top to bottom: one analyze_pdf makes the Azure call once, then one small builder per table reads that shared result, and the two tables that only need line_df, page_df and cross_ref_df, are produced by the very same fitz builders the native parser uses. The dict at the end is the contract every engine returns.从上到下阅读:一次 analyze_pdf 进行 Azure 调用,然后每个表格的小型构建器读取该共享结果,而仅需 line_df、page_df 和 cross_ref_df 的两个表格由原生解析器使用的相同 fitz 构建器生成。末尾的字典是每个引擎返回的契约。

parse_pdf, with per-row diffs vs fitz – Image by author与 parse_pdf 相同的表格,每行与 fitz 的差异——图片由作者提供3. What each table gains3. 每个表格的增益
3.1. line_df gains table-cell rows, image OCR, selection marks3.1. line_df 获得表格单元格行、图像 OCR、选择标记
A 4-column “Schedule of Charges” table becomes 6 rows in line_df: the header row, the markdown separator, and four data rows.一个 4 列的“收费表”在 line_df 中变成 6 行:标题行、Markdown 分隔符和四个数据行。

line_df row; column structure carried inside the markdown text – Image by author每个源行变成 line_df 的一行;列结构包含在 Markdown 文本中 – 作者图片We keep the cells inside line_df instead of adding a separate table_cells_df. One table for every downstream brick to read; paragraph lines and table rows look the same on the way out. The cost: per-cell queries need a markdown parse step. For RAG questions this is fine. The retriever matches keywords on the row text. The LLM reads the markdown directly.我们将单元格保留在 line_df 中,而不是添加单独的 table_cells_df。每个下游砖块读取一个表格;段落行和表格行在输出时看起来相同。代价是每个单元格的查询需要一次 Markdown 解析步骤。对于 RAG 问题,这没问题。检索器根据行文本匹配关键词。LLM 直接读取 Markdown。
OCR text from inside images also lands in line_df as extra rows. Azure’s result.pages[i].lines already includes lines that fall inside figure regions, so the line-builder picks them up automatically. Selection marks (checkboxes) become single-character lines: [x] for selected, [ ] for unselected. Forms with check-the-box fields become queryable.图像内部的 OCR 文本也作为额外行进入 line_df。Azure 的 result.pages[i].lines 已经包含位于图形区域内的行,因此行构建器会自动拾取它们。选择标记(复选框)变成单字符行:[x] 表示选中,[ ] 表示未选中。带有复选框字段的表单变得可查询。
3.2. image_df gains an ocr_text column3.2. image_df 新增 ocr_text 列
Same row, new column. For each detected figure, we list every Azure word whose bbox overlaps the figure region by at least 50% and join them as ocr_text.同一行,新列。对于每个检测到的图形,我们列出所有边界框与图形区域重叠至少 50% 的 Azure 单词,并将它们连接为 ocr_text。

The same column on a fitz-produced image_df is empty. The fitz parser does not OCR images. When parsing_method == "fitz", the ocr_text column is there for shape parity but stays blank. Downstream code that checks ocr_text != "" works the same whether the row came from fitz or Azure.fitz 生成的 image_df 上的同一列为空。fitz 解析器不进行图像 OCR。当 parsing_method == "fitz" 时,ocr_text 列存在以保持形状一致,但保持空白。检查 ocr_text != "" 的下游代码无论行来自 fitz 还是 Azure 都同样工作。
3.3. toc_df gets reconstructed from paragraph roles3.3. toc_df 从段落角色重建
When the PDF has native bookmarks, the fitz build_toc_df is exact and free: it reads what the author wrote. When it doesn’t (most enterprise documents), fitz returns an empty toc_df and downstream stages lose the section structure.当 PDF 具有原生书签时,fitz 的 build_toc_df 精确且免费:它读取作者编写的内容。当没有时(大多数企业文档),fitz 返回空的 toc_df,下游阶段失去章节结构。
The Azure builder walks result.paragraphs, filters by role in {"title", "sectionHeading"}, and assembles a TOC. Level 1 = title, level 2 = sectionHeading. The hierarchy comes from the order paragraphs appear in the document. The same start_page, end_page, start_y, breadcrumb columns as the fitz TOC. The lookback pass that computes end_page (the next peer-or-ancestor’s start_page, or total_pages for the last section) is identical to the fitz one; the only difference is where the rows come from.Azure 构建器遍历 result.paragraphs,按角色过滤 {"title", "sectionHeading"},并组装目录。级别 1 = title,级别 2 = sectionHeading。层次结构来自段落出现在文档中的顺序。与 fitz 目录相同的 start_page、end_page、start_y、breadcrumb 列。计算 end_page(下一个同级或祖先的 start_page,或最后一个章节的 total_pages)的回溯过程与 fitz 相同;唯一的区别是行的来源。
The reconstruction is not perfect. Azure cannot tell sub-section levels apart beyond sectionHeading. The hierarchy you get is two-deep at most. For most enterprise queries this is enough: a chunk stamped “Schedule of Charges” lets the LLM ground its answer to the right section even without the full Article 14 > Schedule of Charges path.重建并不完美。Azure 无法区分 sectionHeading 之外的子章节级别。你得到的层次结构最多两层。对于大多数企业查询,这足够了:标记为“收费表”的块让 LLM 即使没有完整的“第 14 条 > 收费表”路径也能将答案定位到正确的章节。
3.4. object_registry gets caption-role detection3.4. object_registry 获得标题角色检测
Fitz detects captions by regex anchored at the start of a line: ^Figure \d+\b, ^Table \d+\b. Two failure modes. False negatives when the caption format differs (Fig. 2. instead of Figure 2, or a multi-line wrap that pushes the number off the first line). False positives when a body-text sentence happens to start with “Figure 2 shows…”.Fitz 通过锚定行首的正则表达式检测标题:^Figure \d+\b, ^Table \d+\b。两种失败模式。当标题格式不同时(例如 Fig. 2. 而不是 Figure 2,或多行换行将编号推到第一行之外)会出现假阴性。当正文句子恰好以“Figure 2 shows…”开头时会出现假阳性。
Azure skips the regex problem. Its paragraphs field tags "figureCaption" and "tableCaption" explicitly. We read the role directly. The (object_type, object_id) join key into cross_ref_df is still pulled from the caption text by the same regex the fitz builder uses, so the join works the same with either engine. The win is recall: Azure catches captions fitz misses. The cost stays the same (one Azure call, the result is reused across builders).Azure 跳过了正则表达式问题。其段落字段明确标记了 "figureCaption" 和 "tableCaption"。我们直接读取角色。用于连接 cross_ref_df 的 (object_type, object_id) 键仍然通过 fitz 构建器使用的相同正则表达式从标题文本中提取,因此无论使用哪个引擎,连接方式相同。优势在于召回率:Azure 捕获了 fitz 遗漏的标题。成本保持不变(一次 Azure 调用,结果在构建器之间重用)。
3.5. parsing_summary gains Azure-specific stats3.5. parsing_summary 增加了 Azure 特定统计信息
Three new fields land in the doc-level synthesis dict:文档级合成字典中新增三个字段:
n_tables_detected: how many tables Azure found (zero on a pure-prose document, non-zero on a contract with tables).n_tables_detected:Azure 检测到的表格数量(纯文本文档为零,包含表格的合同为非零)。n_figures: how many figures the layout model identified.n_figures:布局模型识别的图形数量。n_selection_marks: how many checkboxes (filled or empty) Azure detected across all pages.n_selection_marks:Azure 在所有页面上检测到的复选框数量(已填充或空)。
These three counts make routing a document easy. A 30-page document with n_tables_detected = 18 looks like a contract and the table structure matters. A document with n_selection_marks = 0 is probably not a form. A document with n_figures = 0 is text-only; no point running image OCR.这三个计数使得文档路由变得简单。一份 30 页的文档,n_tables_detected = 18,看起来像合同,表格结构很重要。n_selection_marks = 0 的文档可能不是表单。n_figures = 0 的文档仅为文本,无需运行图像 OCR。
3.6. page_df and cross_ref_df: unchanged3.6. page_df 和 cross_ref_df:未更改
Two tables stay the same shape. page_df and cross_ref_df are built from line_df alone, so the engine that produced line_df is irrelevant. One implementation, two engines, no drift.两个表保持相同结构。page_df 和 cross_ref_df 仅从 line_df 构建,因此生成 line_df 的引擎无关紧要。一种实现,两个引擎,无差异。
span_df is empty under Azure. The layout model does not expose sub-line typography (per-word bold or italic). When you need spans for heading detection or term emphasis, stay on fitz for that document. The two engines complement each other.在 Azure 下,span_df 为空。布局模型不暴露子行排版(逐词加粗或斜体)。当需要跨度进行标题检测或术语强调时,请对该文档继续使用 fitz。两个引擎互补。
4. The parsing_method column: provenance for adaptive parsing4. parsing_method 列:自适应解析的来源
Every per-row table from parse_pdf_azure_layout carries parsing_method == "azure_layout". Every per-row table from parse_pdf (the fitz one) carries parsing_method == "fitz". Same column, same name, both engines. The point is downstream.来自 parse_pdf_azure_layout 的每行表格都带有 parsing_method == "azure_layout"。来自 parse_pdf(fitz 版本)的每行表格都带有 parsing_method == "fitz"。相同的列,相同的名称,两个引擎。关键在于下游处理。

parsing_method – Image by author基于 fitz 的合同,第 14 页使用 Azure 重新解析;两个引擎通过 parsing_method 共存——图片由作者提供This is what adaptive parsing (Article 10) consumes. The default pass uses fitz. Pages that fail a pre-parse check (table region detected with no rows extracted, image-heavy page with sparse text, OCR layer with low quality) get re-parsed by Azure. The re-parsed rows replace or append to the original line_df rows. The parsing_method column keeps the trail.这就是自适应解析(第 10 条)所消耗的内容。默认传递使用 fitz。未能通过预解析检查的页面(检测到表格区域但未提取行、图像密集且文本稀疏的页面、低质量 OCR 层)将由 Azure 重新解析。重新解析的行将替换或追加到原始 line_df 行中。parsing_method 列保留轨迹。
Three downstream patterns the column enables:该列启用的三种下游模式:
- De-duplication: when the same page got both passes, keep azure rows over fitz rows (
df.sort_values("parsing_method").drop_duplicates(["page_num", "line_num"], keep="first")if"azure_layout" < "fitz"lexicographically, or use an explicit precedence map).去重:当同一页面经过两次传递时,保留 azure 行而非 fitz 行(如果 "azure_layout" < "fitz" 按字典序,则使用 df.sort_values("parsing_method").drop_duplicates(["page_num", "line_num"], keep="first"),或使用显式优先级映射)。 - Audit: a question that lands on a row with
parsing_method == "azure_layout"costs more to verify (Azure was needed). The answer’s confidence weighting can use this.审计:落在 parsing_method == "azure_layout" 行上的问题验证成本更高(需要 Azure)。答案的置信度权重可以利用这一点。 - Cost accounting:
(line_df.parsing_method == "azure_layout").any()per page tells you which pages went through Azure and how to bill the parsing time.成本核算:每页 (line_df.parsing_method == "azure_layout").any() 告诉你哪些页面经过了 Azure 以及如何计费解析时间。
5. Cost and latency5. 成本和延迟
Azure is not free. Three numbers matter.Azure 并非免费。三个数字很重要。
Latency: one page through prebuilt-layout returns in 2 to 4 seconds. A 30-page document takes 60 to 120 seconds. Fitz parses the same document in under a second. When the user is waiting for a query, parse with fitz first. Escalate to Azure only on pages fitz handled poorly.延迟:通过 prebuilt-layout 解析一页需要 2 到 4 秒。一份 30 页的文档需要 60 到 120 秒。Fitz 在不到一秒内解析同一文档。当用户等待查询时,首先使用 fitz 解析。仅在 fitz 处理不佳的页面上升级到 Azure。
Money: Azure charges per page. The prebuilt-layout tier is around US$10 per 1,000 pages today. A 30-page contract costs roughly US$0.30. Parsing 1,000 such contracts a day is US$300/day if every page goes through Azure. Restricting Azure to the pages that need it brings this down by 10x or more.费用:Azure 按页收费。目前 prebuilt-layout 层级约为每 1000 页 10 美元。一份 30 页的合同大约花费 0.30 美元。如果每天解析 1000 份这样的合同,且每页都通过 Azure,则每天花费 300 美元。将 Azure 限制在需要的页面上,可将成本降低 10 倍或更多。
Limits: the per-call PDF size limit is 500 MB or 2,000 pages, whichever comes first. Larger documents need to be split. The free tier (F0) allows 500 pages per month and is fine for development. Production usually needs S0.限制:每次调用的 PDF 大小限制为 500 MB 或 2000 页,以先到者为准。较大的文档需要拆分。免费层(F0)每月允许 500 页,适合开发。生产环境通常需要 S0。
The order of magnitude is stable: fitz is free, Azure costs roughly a cent per page. The exact tier prices change with region and time: treat the numbers above as a calibration, not a contract. Article 10 picks which engine runs.数量级是稳定的:fitz 免费,Azure 大约每页一美分。确切的层级价格随区域和时间变化:将上述数字视为校准,而非合同。第 10 条选择运行哪个引擎。
6. When to call which6. 何时调用哪个
Default to fitz. Escalate to Azure when a specific signal says fitz is not enough.默认使用fitz。当特定信号表明fitz不足时,升级到Azure。
Three signals worth wiring:三个值得接入的信号:
- The page has a table region but fitz extracted few or no row-like structures. Compute on
line_df: cluster lines by y-coordinate, look for runs of short uniform-spaced lines (a sign of cells). If the page metadata says “table detected” (from fitz’spage.find_tables()) but the line pattern does not look table-like, escalate.页面有表格区域,但fitz提取的行状结构很少或没有。在line_df上计算:按y坐标聚类线条,寻找短且均匀间隔的线条序列(单元格的标志)。如果页面元数据说“检测到表格”(来自fitz的page.find_tables()),但线条模式看起来不像表格,则升级。 - The page is image-heavy with sparse text.
image_dffor the page covers more than 80% of the page area andline_dfhas fewer than 10 rows on that page. Scanned page with no OCR layer, or a page that is one large diagram with text inside. Either case needs Azure.页面图像密集且文本稀疏。该页面的image_df覆盖超过80%的页面面积,且line_df在该页面上少于10行。扫描页面无OCR层,或页面是包含文本的大型图表。两种情况都需要Azure。 - The OCR quality score is low: When fitz’s
page.get_text("text")returns scrambled OCR (high ratio of Unicode replacement characters, low dictionary-word ratio), re-OCR with Azure. Thetext_quality_scoreis computed inpre_parse_signalsand read by the dispatcher.OCR质量得分低:当fitz的page.get_text("text")返回混乱的OCR(高比例的Unicode替换字符,低字典词比率)时,使用Azure重新OCR。text_quality_score在pre_parse_signals中计算,由调度器读取。
A fourth signal is simpler. If the document has no native TOC (fitz.toc_df.empty) and generation needs section context, run the document once through Azure to get a reconstructed TOC. One cost per document, not per query.第四个信号更简单。如果文档没有原生目录(fitz.toc_df为空)且生成需要章节上下文,则通过Azure运行一次文档以获取重建的目录。每个文档一次成本,而非每次查询。
Article 10 builds the full dispatcher. The parsing_method column is what lets every downstream stage read which engine ran on which row.第10篇文章构建了完整的调度器。parsing_method列让每个下游阶段都能读取哪个引擎处理了哪一行。
7. Conclusion7. 结论
Two engines, one contract: the same relational tables out, same downstream code regardless of which one ran.两个引擎,一个契约:输出相同的关系表,无论哪个引擎运行,下游代码相同。

A parser does not return text; it returns a model of the document. Azure makes that model richer (cell-level tables, OCR inside figures, captions tagged by role, TOC reconstructed without bookmarks) at 2 to 4 seconds and ~US$0.01 per page. Fitz costs nothing and runs in milliseconds. The routing rule is simple: fitz by default, Azure when an upstream signal says fitz is not enough. Article 10 wires the dispatcher.解析器不返回文本;它返回文档的模型。Azure使模型更丰富(单元格级表格、图形内的OCR、按角色标记的标题、无需书签重建的目录),每页耗时2到4秒,成本约0.01美元。Fitz免费且运行时间毫秒级。路由规则很简单:默认使用fitz,当上游信号表明fitz不足时使用Azure。第10篇文章连接了调度器。
8. Sources and further reading8. 来源与进一步阅读
The prebuilt-layout model behind parse_pdf_azure_layout is documented by Microsoft and rests on cell-level table extraction research (Smock et al. 2022) plus a paragraph-role layer that converts visual regions into structural roles. Docling (Article 5ter) is the open-source equivalent of the same cascade; it gives the same table contract on local hardware, useful when documents cannot leave the building.parse_pdf_azure_layout 背后的预构建布局模型由微软记录,基于单元格级表格提取研究(Smock 等人,2022)以及一个将视觉区域转换为结构角色的段落角色层。Docling(第 5ter 条)是相同级联的开源等价物;它在本地硬件上提供相同的表格契约,适用于文档不能离开建筑物的情况。
Same direction as the article:与文章方向相同:
- Microsoft, Azure AI Document Intelligence. Layout model. Official documentation for
prebuilt-layout, the model behindparse_pdf_azure_layout. The cell-level table output, paragraph roles, and OCR coverage all originate here.微软,Azure AI 文档智能。布局模型。预构建布局的官方文档,这是 parse_pdf_azure_layout 背后的模型。单元格级表格输出、段落角色和 OCR 覆盖均源于此。 - Smock, Pesala, Abraham, PubTables-1M / Table Transformer (TATR), CVPR 2022 (arXiv:2110.00061). The research behind the cell-level table extraction Azure ships; useful for understanding what
azure_layoutis doing under the hood.Smock、Pesala、Abraham,PubTables-1M / 表格转换器(TATR),CVPR 2022(arXiv:2110.00061)。Azure 提供的单元格级表格提取背后的研究;有助于理解 azure_layout 在底层做什么。
Different angle, different context:不同角度,不同背景:
- Auer et al., Docling Technical Report, IBM Research 2024 (arXiv:2408.09869). Open-source local equivalent of the Azure layout cascade. Same table contract; trades cloud cost for local compute. The right choice when confidentiality blocks the cloud upload that Azure requires.Auer 等人,Docling 技术报告,IBM 研究 2024(arXiv:2408.09869)。Azure 布局级联的开源本地等价物。相同的表格契约;用本地计算换取云成本。当保密性阻止 Azure 所需的云上传时,这是正确的选择。
Earlier in the series:系列早期内容:
- Document Intelligence: series intro. The four bricks; this is a parsing-side deep-dive on a richer engine.文档智能:系列介绍。四个基石;这是对更丰富引擎的解析端深入探讨。
- Baseline Enterprise RAG, from PDF to highlighted answer. The pipeline that consumes the tables Azure Layout fills.基线企业 RAG,从 PDF 到高亮答案。消耗 Azure Layout 填充的表格的管道。
- Embeddings Aren’t Magic: The Predictable Failure Modes of RAG Retrieval. How retrieval matches the cell text and figure OCR this engine recovers.嵌入并非魔法:RAG 检索的可预测失败模式。检索如何匹配此引擎恢复的单元格文本和图形 OCR。
- Rerankers Aren’t Magic Either: When the Cross-Encoder Layer Is Worth the Cost. Re-scoring the chunks built from these rows.重排序器也非魔法:交叉编码器层何时值得成本。对从这些行构建的块进行重新评分。
- RAG is not machine learning, and the ML toolkit solves the wrong problem. Why parsing is engineering, not model training.RAG 不是机器学习,机器学习工具包解决了错误的问题。为什么解析是工程,而不是模型训练。
- From regex to vision models: which RAG technique fits which problem. Which parsing technique fits which document.从正则表达式到视觉模型:哪种 RAG 技术适合哪种问题。哪种解析技术适合哪种文档。
- 10 common RAG mistakes we keep seeing in production. The production mistakes the four bricks are designed to avoid.我们在生产中不断看到的 10 个常见 RAG 错误。四个基石旨在避免的生产错误。
- Beyond extract_text: the two layers of a PDF that drive RAG quality. The first half of the parsing brick: the document’s nature, signals, and summary.超越文本提取:驱动RAG质量的PDF两层结构。解析砖的前半部分:文档的性质、信号和摘要。
- Stop returning flat text from a PDF: the relational shape RAG needs (link to come). The second half of the parsing brick: the relational tables every downstream brick reads.停止从PDF返回平面文本:RAG所需的关系形状(链接待提供)。解析砖的后半部分:每个下游砖读取的关系表。







