and expensive. The fast parser misses the table the answer lives in. Run the heavy one on every page of a 200-page report and you pay for 200 pages to rescue three. Run the cheap one everywhere and the single flattened table sinks the answer. Neither setting is right for the whole document, because the document is not uniform: most pages are plain text a fast parser handles fine, and a few carry the tables that need the expensive one. The page itself should decide how hard it gets parsed.
This article is the first of two parts on adaptive parsing, in Part III of Enterprise Document Intelligence, a series that builds an enterprise RAG system from four bricks: document parsing, question parsing, retrieval, and generation. This part builds the escalation cascade and the cheap checks that decide when a parse is good enough; the second part, Loop engineering with adaptive parsing in action: parsing flat tables with Azure and figures with a vision LLM (link to come), walks the deeper parsers in action.
📓 The runnable companion runs the cheap checks yourself: you call pre_parse_signals on each page of the Attention paper, print the per-page flags (char_count, image_count, the flat-table fingerprint on line_df), and watch which pages route to a heavier parser before a single dollar is spent on one. On GitHub: doc-intel/notebooks-vol1.

PyMuPDF parses a page in five milliseconds for free. A vision LLM on the same page can cost ten thousand times more and take ten seconds. When the question lives in plain prose, the cheap parser wins. When the answer hides inside a table, a figure, a scanned region, or a flattened layout, the cheap parser silently returns nothing useful and the LLM confidently answers the wrong question.
Running the heaviest parser on every page is wasteful. Running the cheapest one everywhere is wrong. The trick is to start cheap and escalate only when something says the cheap parse missed the answer. That signal has to come from the pipeline itself: a chain of checks against the parse output, the question, and the LLM’s own footprint when it tries to use what came out.
The way around this is a feedback loop the pipeline builds for itself. Start with the cheapest parser. Evaluate its output at every check of the pipeline. Escalate to a deeper parser only when at least one check flags the cheap parse as insufficient for the question.
The evaluation is not one check at one place. It is a cascade:
- pre-parsing metadata routes most of the corpus before any extraction
- parsing-time outputs flag flattened tables and opaque figures
- retrieval scoring catches anchors that drift
- generation flags what survived all of the above
Each check is cheaper than the next, more reliable than the next, and asks its own question: “did the parser produce enough to answer?”.
This part walks the cheap end of that cascade: at each check, what indicator fires, what it costs to compute, what it buys. Together they decide, before any expensive parser runs, whether a page needs one. Two recurring examples thread through both parts, both from the Attention paper (Vaswani et al. 2017, under the arXiv non-exclusive distribution license): Table 3 (page 9, a flat-parsed grid where every cell is its own line) and Figure 1 (page 3, a diagram body that PyMuPDF returns empty). Here we see how each is detected for free; the second part escalates them end to end.
1. When cheap parsing isn’t enough
Adaptive parsing runs in two phases. Phase one is initialisation: pick a baseline parser per document, based on what the document is (native PDF, scan, Word export). In a platform context this fires at ingestion: every new document gets its baseline parse on arrival, so the system always has a first textual layer to work with, even for scanned PDFs where the baseline has to be an OCR engine because PyMuPDF would return empty. Phase two is the cascade: run cheap deterministic checks on the baseline’s output and escalate the pages that fail to a deeper parser. Most pages stay on the baseline; the few that fail a check get the richer treatment they actually need.
The pipeline always starts cheap on phase one. PyMuPDF on native PDFs, a free OCR engine on scans. Both produce the same line_df shape, so the rest of the pipeline (question parsing, retrieval, generation) does not know which parser ran. That works most of the time. It fails on a small but predictable set of content shapes, and phase two has several places to catch the failure, ordered from cheap deterministic checks on the parser’s outputs to a final-line-of-defence LLM call at generation time.
1.1. Cheap parsing: PyMuPDF for native PDFs, free OCR for scans
Article 5A (what to read in a PDF) introduced PyMuPDF (imported as fitz) as the default for native-digital PDFs: 5 seconds per document, no API calls, no cost, and a clean line-level extraction with bounding boxes. The Attention paper, the NIST Cybersecurity Framework (US Government work, public domain in the US, see NIST copyright statement), most office exports, most arXiv papers, almost every contract authored in Word or LaTeX: PyMuPDF is the right starting point.
The other half of the corpus is scans: photographed pages, faxed documents, image-only PDFs. PyMuPDF returns empty text on those. A free OCR engine like Tesseract or PaddleOCR fills the gap: same line_df shape out, but slower (minutes per document) and weaker on structure. Free OCR reads paragraph text reasonably well. It struggles on tables, multi-column layouts, and degraded scans. We mention the scanned case here for completeness; the worked examples in this article both use a native PDF so we can isolate the parsing-failure mechanism without OCR noise on top.
OCR has no cheap-good tier. Where native PDF extraction is near-lossless at the PyMuPDF tier, character recognition on a scan starts weak no matter the engine. Free OCR (Tesseract, EasyOCR) gets readable text out of clean modern scans, then degrades fast on skewed pages, low DPI, mixed languages, historical fonts, tight column gutters, anything below 200 DPI. Docling uses EasyOCR by default for character recognition, so the OCR-level errors are the same: what Docling adds is layout on top of those characters, not better characters. Cloud document-AI services (Azure Document Intelligence, AWS Textract, Mistral Document AI) recognise characters better and add layout, at a few cents per page. Vision LLMs read damaged scans the others give up on, at a few cents to a dime per page depending on the model.
The asymmetry matters for the initialisation phase. On a native corpus the cheap baseline is genuinely good. On a scan corpus the cheap baseline is genuinely weak, and the cascade will fire on most pages rather than the few that PyMuPDF would miss. On a scan-heavy corpus, picking a paid tier as the baseline upfront is often cheaper end-to-end than running per-page escalations.
Docling as a stronger baseline, when the budget and the hardware allow it. Docling is open source, free at runtime, layout-aware. It subsumes the PyMuPDF + Tesseract split: one call handles native PDFs and OCR’d scans, returns structured tables, sections, and image captions out of the box. Tempting as a universal baseline, but the cost shape is real. Docling pulls a few hundred MB of layout and TableFormer models on first install. At runtime it is one to two orders of magnitude slower than PyMuPDF on native PDFs (Article 5quinquies measured around twenty-seven seconds per page on a 1974 scan), and that latency assumes a working GPU. On CPU it is noticeably slower again. On a 500-page corpus that means minutes to an hour of parsing time per document, not the five seconds PyMuPDF gives. The install itself can also fight corporate SSL inspection.
The right question is not “can I use Docling as the baseline?” but “does the operational envelope (corpus size, GPU access, ingestion latency budget) let me afford it?”. Three regimes show up:
- Small corpus, GPU available, layout quality matters: Docling as baseline pays off. The cascade below rarely fires because the baseline already returned structured output.
- Large corpus, no GPU, or strict ingestion latency: PyMuPDF + Tesseract stays the baseline. Docling enters the cascade as an escalation tier (Camelot / Docling row in the cost gradient), invoked only on pages that failed a check, not on the whole document.
- Mixed corpus: baseline-per-document. Native PDFs go through PyMuPDF, scans through Tesseract or Docling depending on availability. The initialisation phase already chose the right tool per document type; the cascade catches the leftover failures.
The two-phase architecture absorbs all three. Pick the strongest baseline the operational envelope lets you afford; let the cascade handle whatever the baseline still misses.
The reason this matters is the cost gradient between parser tiers. On a 500-page document end-to-end:

The gradient makes parsing-on-demand pay off. Keep most pages on PyMuPDF; pay for a deeper parser only on the pages a question needs.
1.2. The failure cases cheap parsing can’t handle
Cheap parsing is fast and free, and it leaves a recognisable set of content invisible or scrambled. Five shapes come up regularly:
- Flattened tables: The most common failure on native PDFs. PyMuPDF returns each cell as a separate line, in geometric order, with column structure erased. “Self-Attention” and “O(n² · d)” sit on lines 9 and 10 of the page with nothing connecting them: the LLM cannot pair the row label with its value. Detected at parsing-time by the flat-table fingerprint (Section 3.2.2); walked end-to-end in the second part.
- Figures and diagrams: PyMuPDF extracts the page text around an image but the image itself is opaque to it. “Figure 1 shows the encoder-decoder architecture” survives; the architecture diagram does not. Detected at parsing-time by the opaque-figure check (Section 3.2.3); walked end-to-end in the second part.
- Multi-column layouts: A two-column page where the parser walks columns geometrically instead of logically produces interleaved sentences from both columns. The text is technically there; the reading order is broken.
- Degraded OCR: Scanned pages with low resolution, skew, or compression artefacts produce character-level errors: l/I/1 confusion, fragmented words, missing punctuation. The LLM can sometimes recover the meaning, sometimes hallucinates around the noise.
- Embedded objects: Equations rendered as vector paths, signatures as images, checkboxes, watermarks: objects the parser may return as placeholders, garbled text, or nothing at all.
This part picks two of these (flattened tables, figures) for the worked walkthroughs the second part develops, because they have the cleanest reproductions on a public document. The mechanism is the same for the other three. The parsing-time deterministic checks (document parsing brick, section 3) flag the failure shape, and the pipeline routes the page to the right deeper parser. The LLM signal at generation time (generation brick, in the second part) is the safety net for the cases the deterministic checks did not catch upstream.
One more reason to parse lazily, in numbers. An enterprise corpus averages 30 to 100 pages per document, 1 to 3 pages relevant to any given question, and fewer than 10 questions per document over its lifetime. Multiply these out and roughly 90% of any page parsed at ingestion is never consumed by any answer. Deep-parsing every page upfront pays the bill for pages no one will ever read.
Here is the whole article in one picture. The rest of the text walks each piece of this cascade, but it helps to have the full map first.

2. A cascade of evaluation checks
Parsing quality is a multi-check decision. The pipeline starts cheap and asks, at each check, whether the parser produced enough for the question. Every check has a cost; every check that flags a problem can route the affected page to a deeper parser. The pipeline pays only for the quality the question needed.
The pipeline’s four bricks (document parsing, question parsing, retrieval, generation, introduced in Articles 5 to 8) each own one to three of the nine evaluation checks. The grouping makes the cascade legible: every check belongs to a brick, every brick has its own cost profile and its own characteristic check. The cascade diagram in section 1 shows the full mapping.
The two recurring examples make the cascade concrete.
- Table 3 on page 9 of the Attention paper is a flat-parsed grid: every cell on its own line with no column anchor surviving. Text density alone misses it at check 1; the deterministic check-2 fingerprint catches it cleanly (flat-table signature), and check 7 (the LLM flag) confirms it, with the caveats developed in the second part.
- Figure 1 on page 3 is a diagram body that PyMuPDF returns empty. Catchable at check 1 (lower-than-average text density combined with an embedded image), at check 2 (opaque figure region with zero extracted chars), and at check 7.
The cheapest check that fires wins, and the pipeline never asks the slower checks a question the faster checks already answered.
The data model that supports it: Whatever check triggers escalation, the schema for the rerun is the same. Add one column, parsing_method, to the relational tables from Article 5. The escalation logic becomes: write new rows with a deeper method on the affected pages. Mixed documents, audit trail, and caching all fall out of the schema for free.
page_df is the canonical place because parsing always happens per page. After a typical adaptive run on the Transformer paper, where the cascade flagged page 6 (Table 1, Maximum path lengths, caught by the check-2 flat-table fingerprint) and page 3 (Figure 1, caught by the char-density signal at check 1 and the opaque-figure signal at check 2), page_df looks like this. This section uses Table 1 to show the shape of the escalated tables; the full end-to-end walkthrough in the second part takes a second flat table, Table 3 on page 9 (Variations on the Transformer architecture), so you see the pattern on two different real tables rather than one.

page_df after one adaptive run: flagged pages carry two rows, one per method – Image by authorTwo design decisions are baked in. First, escalation adds new rows, it does not replace. The PyMuPDF row for page 6 stays alongside the Azure row: “PyMuPDF tried, the cascade flagged the parse, Azure was called and succeeded”. Second, the context_structured column on page_df lets downstream queries pick the right row: SELECT * FROM page_df WHERE page_num=6 AND context_structured=True returns the trusted parse without any re-run.
line_df also carries parsing_method. Lines from PyMuPDF coexist with lines from the deeper parser on the same page. PyMuPDF produced the prose around Table 1; Azure produced the markdown rows of the table itself; both stay in line_df. The retrieval brick reads line_df and sees a uniform shape, regardless of which parser produced which row.
Images live in line_df too, as rows with type='image' and a placeholder text. This is the bit that makes the figure case (in the second part) work mechanically the same as the table case. The opaque-figure signal at check 2 already flags such pages from line_df alone (image present, zero extractable chars inside its bbox). When the cascade flags page 3, the pipeline picks the row from image_df, sees parsing_method='not_parsed', and calls a vision LLM. The vision output gets appended to line_df as new text rows with parsing_method='vision_gpt4o', and the image_df row is updated.

line_df with text, table, image-placeholder rows, each tagged by parsing_method – Image by authorTargeted re-parsing is one function call, method-agnostic, that returns new rows with parsing_method=method already populated:
def enrich(pdf_path, line_df, page_df, pages, method):
"""Add layer-2 rows for `pages` using `method` ; keep existing rows for audit."""
if method == "azure_layout":
from docintel.parsing.pdf.azure_layout import azure_layout_pdf_to_line_df
new_lines = azure_layout_pdf_to_line_df(pdf_path, pages=pages)
elif method == "vision_gpt4o":
new_lines = vision_extract(pdf_path, pages=pages)
# new_lines already carries parsing_method=method
line_df = pd.concat([line_df, new_lines], ignore_index=True)
page_df = append_page_rows(page_df, pages, method)
return line_df, page_df
The line_df schema from Article 5 is unchanged for downstream bricks: retrieval and generation read line_df, group by page_num, and never need to know which parser produced which line. The only contract that changed is “the parser identity travels with the row”.
3. Document parsing: free deterministic checks
Document parsing owns three of the nine checks: pre-parsing (check 1, before any extraction runs), parsing-time (check 2, on PyMuPDF’s own outputs), and post-parsing (check 3, on chunks downstream of line_df). All three are deterministic, free, and produce a verdict in milliseconds without any LLM call. Together they catch most of the failure shapes the article cares about, including both Case A (the flat table, Table 3 on page 9) and Case B (the opaque figure, Figure 1 on page 3), the two cases the second part walks end to end.
3.1. Check 1: pre-parsing
The cheapest evaluation point sits before any text-extraction call runs. PyMuPDF can already report per-page metadata in milliseconds: how many characters it can extract, how many embedded images each page carries, what producer string the PDF declares. These signals route most of the corpus before the parsing decision becomes interesting.
3.1.1. Per-page text density and image count
A native-PDF page typically extracts 2000-4000 characters of text. Pages that come in well below that, paired with one or more embedded images, are signalling that “the page is mostly a figure” without any deeper inspection.

For Figure 1 on page 3, this signal alone is enough to flag: 1827 chars vs the document’s 2633-char mean, plus one embedded image. The pipeline could route page 3 to a vision LLM without ever calling Azure or running a generation pass. For Table 3 on page 9, the same signal does not fire: 2973 chars (above mean), zero embedded images. Check 1 catches one of the two examples for free; check 2 will catch the other.
3.1.2. PDF metadata, producer string, and a tier hint
The PDF metadata adds a second cheap classifier. The Attention paper’s producer is pdfTeX-1.40.25, which says LaTeX-authored, native, no OCR needed. A scanner-produced PDF would say Adobe Scan, Canon, OmniPage. That single string routes 80% of an enterprise corpus to the right tier before any text extraction runs. The creation date, the embedded font list, and the presence of an XMP block add coarse but useful classifiers.
pre = pre_parse_signals("data/paper/1706.03762v7.pdf")
pre["producer"] # -> "pdfTeX-1.40.25"
pre["is_scanner_output"] # -> False
pre["pages"].head()
# page_num char_count image_count image_bboxes
# 1 2858 0 []
# 2 4256 0 []
# 3 1827 1 [(196.6, 72.0, 415.4, 394.4)] <-- Figure 1
# 4 2508 2 [(175.0, 94.0, 239.0, 221.3),
# (346.8, 82.7, 467.0, 267.3)] <-- Figure 2
# 5 3193 0 []
The result of this check is not the answer to a question. It is a per-page judgment: “this page is normal”, “this page has a missing image body”, “this whole document is scanned and needs OCR upfront”. The judgment is free and it ships with the parsing_method column on page_df.
3.2. Check 2: parsing-time
PyMuPDF runs next. Its outputs already carry deterministic signals about the layout it just walked. Three checks fire from line_df alone, none of them needing an LLM call.
3.2.1. Per-page column count and reading-order risk
PyMuPDF extracts every line with bounding-box coordinates. Clustering line x-coordinates per page produces a column count. A well-structured document has a stable column count from page to page. Variance is the routing signal.

The anomaly on page 10 is itself a parsing-quality signal. The detector did not produce a wrong count by accident, it produced it because the table broke the column model. That is information the pipeline can use to route page 10 to a layout-aware parser.
3.2.2. Flat-table fingerprint, caught on Table 3
When PyMuPDF flattens a table, the cells fall on their own lines with narrow bboxes immediately following the “Table N: …” caption.

The fingerprint is concrete: on page 9, lines 5-20 hold the table headers split across two physical rows of bboxes, and lines 21-33 hold the 13 cells of the base row each on its own line. The cells are short (1-4 chars), narrow (under 30 PDF points wide), and clustered vertically in a tight band right after a “Table 3: …” caption. A check that counts those properties on line_df[page_num=9] and trips a boolean flag runs in O(n_lines) without any LLM call. Table 3 is caught at this check.
3.2.3. Opaque figure regions, caught on Figure 1
PyMuPDF reports image bboxes at extraction time via page.get_image_info(). If a page has an image and the text extracted inside its bbox is empty, the figure body did not survive the parse.

The check is the same as the pre-parsing density check but more precise: instead of noticing the page has fewer characters, it pinpoints a specific rectangle with zero characters. Figure 1 is caught at this check. (Pre-parsing also catches it, more cheaply; check 2 confirms it and adds the bbox the next parser will need.)
3.2.4. Multi-column reading order, caught on BERT paper
The Attention paper is single-column, so it cannot illustrate this check. We swap in BERT (1810.04805) which is the standard ACL two-column format. assign_column_positions clusters line x-coordinates per page and labels each line “left” / “right” / “single” / “multi”. No hardcoded midpoint, the split is computed from the actual line distribution. When PyMuPDF walks such a page geometrically, it can interleave sentences across the column boundary; the column labels let the pipeline (or a deeper parser) restore the correct reading order.

x0 values – Image by author3.2.5. Putting it together: one function, three flags
pre = pre_parse_signals("data/paper/1706.03762v7.pdf")
line_df = fitz_pdf_to_line_df("data/paper/1706.03762v7.pdf")
# Three pages, three signals, no LLM call
page_level_parsing_signals(line_df, page_num=3, pre_signals=pre)
# {'page_num': 3, 'flat_table': False, 'opaque_figure': True,
# 'col_anomaly': False, 'reasons': ['opaque_figure'], 'escalate': True}
page_level_parsing_signals(line_df, page_num=9, pre_signals=pre)
# {'page_num': 9, 'flat_table': True, 'opaque_figure': False,
# 'col_anomaly': False, 'reasons': ['flat_table'], 'escalate': True}
page_level_parsing_signals(line_df, page_num=10, pre_signals=pre)
# {'page_num': 10, 'flat_table': True, 'opaque_figure': False,
# 'col_anomaly': True, 'reasons': ['flat_table', 'col_anomaly_3'],
# 'escalate': True}
At the end of check 2, both Table 3 and Figure 1 are flagged for escalation without a single LLM call. The pipeline could enrich both pages with the right deeper parser now, before retrieval even starts. The remaining checks of the pipeline (retrieval, generation, post-gen) become last-resort safety nets, not the primary detection mechanism.
3.3. Check 3: post-parsing (chunk integrity, reading order)
After line_df is built, the pipeline chunks it for retrieval. A chunk that cuts mid-sentence or mid-table-row carries a low chunk_integrity_score that downstream prompts can read. Surya and PaddleStructure expose this directly; with line_df alone we can compute it from line numbers and bboxes (sentence-end punctuation in the last line, table-row alignment within the chunk).
The score is deterministic, cheap, and fires before any retrieval call. The article does not develop a worked example for check 3 because both Case A and Case B are already caught at checks 1 and 2; we mention it here so the panorama is complete.
4. Question parsing: routing by intent
Question parsing turns the question into a typed parsed_question (Article 6) and uses that to route the cascade. Two checks live here: check 4 (intent routing, one small LLM call on the question) and check 9 (user feedback, a slow human-driven loop that refines check 4 over time).
4.1. Check 4: intent routing
“In Table 3”, “what percentage”, “according to article 5” signal that the answer lives in a structured region. Route directly to table-aware retrieval, skip prose embeddings. Article 6 made the case for parsing the question into a typed object; routing by intent is one of its payoffs. The cost of this check is one small LLM call on the question (a few cents per thousand questions). The quality gain: the pipeline routes Table 3 questions to a table-aware parser without ever needing the LLM at check 7 to notice the table was flattened.
4.2. Check 9: user feedback and golden set
Check 9 is the long-loop check: a question that the user re-asks with slightly different wording, a golden set drift over time, an expert flagging a wrong answer. None of these fire inside a single pipeline run, but they refine the question parser (and, indirectly, every check that follows). Concretely: a recurring rewording captured in expert_keywords_df (Article 6) gets added to the question router, and the next question that uses that phrasing is correctly classified without the user having to retype it. The cost of this check is human time spent on golden-set curation; the quality gain compounds.
5. Retrieval: score gap and assembly checks
Retrieval takes the parsed question and the parsed corpus, and returns a small set of candidate chunks. Two checks fire here, both deterministic and both running on quantities the retrieval brick already computed.
5.1. Check 5: score gap and source diversity
Top-1 cosine of 0.45 is a poor anchor. Top-1 of 0.85 with top-2 at 0.83 is ambiguity. Both deterministic, both fire before generation. Reranker disagreement with the bi-encoder is itself a second-order signal. The cost is the cosine computation already done by retrieval; the quality gain is catching questions that landed nowhere clean.
5.2. Check 6: context assembly (gaps and contradictions)
Once the candidate chunks are picked, a last deterministic check compares them to the question’s parsed scope. Question covers 2020-2024, chunks cover 2020-2022: the gap is detectable before the LLM call. Same for chunks that contradict each other on a key fact. These checks run in milliseconds and are skipped only at the cost of fabrication-prone answers downstream.
5.3. Adaptive embedding granularity: page-default, line-on-demand
The same cheap-then-rich logic the cascade applies to parsing also applies to embedding. Parsing produced page_df and line_df (Article 5 Section 3.2 said “line_df keeps the door open” on this); retrieval now decides which granularity to embed, and when.
The default is page-level. One vector per page_df row, cosined against the question vector, top-K pages picked. Cheap, persistable, retrievable in milliseconds on corpora up to tens of thousands of pages. A typical enterprise contract is twenty to two hundred pages; a 500-document corpus carries forty to one hundred thousand page vectors, well within an in-memory cosine. This is what the minimal pipeline of Article 1 (minimal RAG) uses, and what most production retrieval still uses when the answer is clearly page-shaped.
Line-level on the whole corpus is the wrong default. A page averages 30 to 80 line_df rows in a contract, 50 to 120 in a research paper, sometimes 200 in a flattened table-heavy PDF. Embedding every line on the full corpus multiplies the vector count by that factor: forty thousand page vectors become two to ten million line vectors. The cost shows on two axes: compute at index time (one API call or one model pass per line, against a budget that was sized for pages) and storage at query time (the in-memory cosine no longer fits and the retrieval drops to a vector database with non-trivial latency). For corpora that fit page-level on a laptop, line-level pushes deployment into a dedicated vector store and changes the operations envelope.
The escalation pattern: page-then-line, only for the top-K pages. When the page-level Check 5 fires (score gap too small, candidates ambiguous, or top page is long and the answer is one line deep inside it), the retrieval brick falls back to a second pass. For the top-K pages only, embed every line on those pages (a few hundred vectors, not millions), run cosine against the question again, and pick the top lines. The result is a line-level anchor inside a page-level scope. Article 7 develops the page-then-line drill-down with worked examples; the same operation here becomes a check in the cascade: page-level passed but did not converge, so escalate to line-level on the surviving candidates.
The cost shape of this escalation is bounded. If page-level retrieval returns five candidate pages and each page has fifty lines, the drill-down embeds 250 lines per question. At rates around twenty thousand line embeddings per second on a local model, the added latency is a few hundred milliseconds. The vectors are computed on-the-fly and not persisted: this is the key difference with eager line-level indexing. The escalation pays only for the candidate pages, only for the questions that triggered it. Most questions never reach this branch.
When to also persist line-level vectors. Two cases earn the storage cost. First, corpora where almost every question is line-shaped (audit checklists against long compliance documents, claim matching against case files, contract clause lookup): the eager line index amortises across queries and the cascade collapses to a one-shot retrieval. Second, corpora where the page-level cosine systematically fails the Check 5 gap (questions whose answer is buried in long mixed pages where the page-level signal averages out): the eager line index avoids the per-question re-embed.
The pattern is the parsing cascade transposed to embeddings. Default to the cheap granularity (page-level vectors from page_df), drill down to the rich one only when a check fires (line-level vectors over the surviving candidates from line_df). Same data model, same persisted artefacts, same operational property: nothing pays the rich cost unless the cheap pass said it should.
6. Conclusion
The cascade turns parsing quality into a sequence of cheap questions. Pre-parsing metadata routes most of the corpus before any extraction runs. Parsing-time fingerprints flag flat tables and opaque figures from PyMuPDF’s own output. Intent routing and the retrieval score-gap catch cases the parser’s output alone cannot. Every check is deterministic, free, and runs in milliseconds, and every one that fires can route a single page to a deeper parser.
One column, parsing_method, makes the whole thing auditable: escalation adds rows, it never overwrites, so a page can carry its PyMuPDF parse and its Azure parse side by side. What none of these cheap checks can catch is the failure that only shows up when the LLM tries to use the text. That last line of defence, and the two escalations walked end to end (a flat table to Azure, a figure to a vision LLM), are the second part: Loop engineering with adaptive parsing in action: parsing flat tables with Azure and figures with a vision LLM (link to come).
7. Sources and further reading
The reference for what advanced parsing does and its per-page cost is Docling (Auer et al., Docling Technical Report, 2024). Table extraction as a separate problem from text extraction is grounded by Smock et al. (PubTables-1M / Table Transformer, CVPR 2022). The vision-LLM-per-page cost tier (~5-30 seconds on a GPU) is from Blecher et al. (Nougat, Meta 2023). The LLM-as-feedback-signal pattern the article uses to drive escalation is in the same family as Asai et al. (Self-RAG, ICLR 2024). The article reports a concrete negative result from an 18-run stress test: LLM self-evaluation as the binary parser-quality verdict is unreliable on its own; a separate-LLM groundedness check on the produced answer catches what the in-loop signal misses.
Earlier in the series:
What works, what breaks
- The minimal Enterprise RAG that never lies about its source: PDF in, highlighted answer out. The four-brick pipeline end to end: PDF in, highlighted answer out.
- Embeddings Aren’t Magic: The Predictable Failure Modes of RAG Retrieval. Where embedding similarity wins (synonyms, typos, paraphrase), where it predictably breaks (unknown terms, negation, term-vs-answer relevance), and how to use it anyway.
- RAG is not machine learning, and the ML toolkit solves the wrong problem. Why chunk-size sweeps and finetuning optimize the wrong thing; route by question type instead.
- From regex to vision models: which RAG technique fits which problem. Two axes, document complexity and question control, that pick the technique for each case.
Document parsing
- 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.
- Stop returning flat text from a PDF: the relational tables RAG needs. The second half of the parsing brick: the relational tables every downstream brick reads.
- When PyMuPDF can’t see the table: parse PDFs for RAG with Azure Layout. The same tables from Azure Layout: native table cells, OCR, paragraph roles.
- Parse PDFs for RAG locally with Docling: rich tables, no cloud upload. The same tables computed locally with Docling: TableFormer cells, nothing leaves the machine.
- Vision LLMs are PDF parsers too: reading charts and diagrams for RAG. Vision as a parser: the pictures become searchable text.
- Parse scanned PDFs for RAG with EasyOCR: free OCR gives you words, not a document. Where traditional OCR stops: text recovered, structure lost.
- Making a PDF’s images searchable for RAG, without paying to read them all. The image cascade: filter cheap, classify, describe only what is worth reading.
- Reconstructing the table of contents a PDF forgot to ship, so RAG can scope by section. Rebuilding toc_df when the PDF prints a contents page but ships no outline.
Question parsing
- RAG questions need parsing too: turn the user’s string into briefs for retrieval and generation. The thesis of question parsing: why a user string needs the same parsing as a document, and how it splits into a retrieval brief and a generation brief.
- What the question parser extracts from a user string: keywords, scope, shape, decomposition, clarification. The five families of columns the parser reads straight from the user’s question, with the code that fills each one.
- Dispatching the parsed RAG question: chunk strategy, model tier, activations, audit. The decisions the parser makes on top of the user string, using the document’s profile: dispatch, activations, full schema, the audit trail (pipeline_trace.json), and a broker-corpus walkthrough.
Retrieval
Generation
- Make RAG generation return a typed contract: citations, typed values, and self-checks (link to come). The answer schema as the contract: typed values, items with evidence spans, self-assessment fields, and the completeness signal the pipeline computes itself.
- Assemble each RAG generation prompt from a base prompt plus the rules each question needs (link to come). The dispatcher: a fixed BASE prompt plus the rules each question needs, the schema picked from the registry, and the full trace kept on every call.
- Validating the RAG answer before the user sees it: spans, quotes, and the feedback loop (link to come). The post-generation validator (spans, verbatim quotes, formats), not-found as a first-class answer, and the feedback loops that close the pipeline.
One-document pipelines
- A production RAG pipeline for PDFs: relational parsing, TOC retrieval, typed answers (link to come). Each of the four bricks upgraded one contract at a time: relational parsing, corpus-aware questions, TOC-routed retrieval, typed answers.
- Stop RAG hallucinations with context engineering: one pipeline, four very different PDFs (link to come). The four upgraded bricks wired into one call, run end to end on a paper, a compliance doc, and a broken-TOC document.
Same direction as the article:
- Auer et al., Docling Technical Report, IBM Research 2024 (arXiv:2408.09869). What advanced parsing does and what it costs.
- Smock, Pesala, Abraham, PubTables-1M / Table Transformer (TATR), CVPR 2022 (arXiv:2110.00061). Table extraction is a separate problem from text extraction. Supports the case for escalating selectively on tables.
- Blecher et al., Nougat: Neural Optical Understanding for Academic Documents, Meta 2023 (arXiv:2308.13418). Reference point for the vision-LLM cost tier: about 5 to 30 seconds per page on a GPU.
- Asai et al., Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection, ICLR 2024 (arXiv:2310.11511). The LLM-as-feedback-signal pattern the article uses to drive escalation is in the same family.
Different angle, different context:
- Faysse et al., ColPali: Efficient Document Retrieval with Vision Language Models, 2024 (arXiv:2407.01449). Vision-language model over the page image. The context is retrieval where the page image itself is the artefact, making the parsing-text-vs-tables distinction less relevant. Different from the per-page deterministic-first cascade defended here.
- Wang et al., DocLLM: A Layout-Aware Generative Language Model for Multimodal Document Understanding, JPMorgan 2024 (arXiv:2401.00908). Layout-aware LLM that processes the whole document without an upstream parser tier. Same family as ColPali.
- Kim et al., OCR-free Document Understanding Transformer (Donut), ECCV 2022 (arXiv:2111.15664). End-to-end OCR-free document understanding; useful contrast with the OCR-quality-scoring escalation tier the article describes.