Enterprise document processing fails in predictable ways. Not because the LLM cannot read a contract. Because the document that arrives is a scanned PDF from 1998, the table spans two pages, and the extraction prompt assumes clean digital text. The LLM hallucinates a clause that does not exist. Nobody notices until month three.
Document intelligence is 30% LLM and 70% document pipeline. Here is how we build the 70%.
The Document Input Problem
Enterprise document collections are never clean. You will encounter:
- Native PDFs with proper text layers (the easy case)
- Scanned PDFs where the "text" is an image (requires OCR)
- PDFs generated from Word with strange encoding artifacts
- Password-protected PDFs (requires a pre-processing step)
- Word documents with tracked changes still embedded
- Excel sheets being used as contract trackers
- Email attachments that are images of documents photographed on a phone
A production pipeline needs to handle all of these. Any of them can arrive on any given day.
The Parsing Stack
Stage 1: Format Detection
Input → MIME type detection + content inspection
Route:
├── Native PDF → PyMuPDF (fitz) for text extraction
├── Scanned PDF → AWS Textract or Azure Document Intelligence
├── DOCX/DOC → python-docx + mammoth for HTML conversion
├── XLSX → openpyxl for structured data extraction
└── Images (JPG/PNG) → AWS Textract or GPT-4o vision
Stage 2: Layout Analysis
→ Table detection and extraction (preserve structure)
→ Header/footer removal
→ Section boundary identification
→ Page number removal
Stage 3: Text Normalization
→ Unicode normalization
→ Whitespace cleanup
→ Ligature resolution (common in older PDFs)
→ Date format normalization
Stage 4: Quality Gate
→ Confidence score from OCR engine
→ Character error rate estimation
→ Flag low-confidence documents for human review
The quality gate in Stage 4 is critical. AWS Textract returns confidence scores per word. Documents where average confidence falls below 85% get routed to a human review queue rather than proceeding to LLM extraction. It is better to have a human review 5% of documents than to have the LLM confidently extract wrong data from blurry scans.
Getting Reliable Structured Output
The worst way to do structured extraction is a natural language prompt like "Extract the contract start date, end date, and total value." The LLM might return a date in any format, with any label, sometimes as part of a sentence and sometimes as a JSON field.
Use structured output mode (available in GPT-4o and Claude). Define a Pydantic model or JSON schema and force the LLM to return data that matches it:
from pydantic import BaseModel, Field
from typing import Optional
from datetime import date
class ContractExtraction(BaseModel):
effective_date: Optional[date] = Field(
description="Contract start/effective date in YYYY-MM-DD format"
)
expiry_date: Optional[date] = Field(
description="Contract end/expiry date in YYYY-MM-DD format"
)
total_value: Optional[float] = Field(
description="Total contract value as a number, no currency symbols"
)
currency: Optional[str] = Field(
description="Three-letter ISO currency code, e.g. USD, GBP, EUR"
)
counterparty_name: Optional[str] = Field(
description="Full legal name of the counterparty"
)
governing_law: Optional[str] = Field(
description="Jurisdiction governing the contract"
)
renewal_type: Optional[str] = Field(
description="Auto-renewal, manual renewal, or none"
)
Every field is Optional. This is intentional. If the information is not in the document, return null rather than hallucinating a value. A null result is useful. A confident wrong result causes real problems downstream.
The Validation Layer
Structured output constrains the format but does not guarantee accuracy. You still need a validation step that checks logical consistency:
- Effective date should precede expiry date
- Total value should be positive and within a plausible range for your document type
- Counterparty name should match a known vendor/client in your system
- Currency should be a valid ISO code
Validation failures get routed to a human review queue with the extracted values and the validation error flagged. The human corrects the extraction and that correction gets logged. Over time, you accumulate a dataset of correction examples that can be used to improve the prompt or fine-tune a smaller model.
Clause Detection vs. Field Extraction
These are different problems and you should not try to solve them with the same prompt.
Field extraction is structured: find the date, the value, the party names. These are specific, bounded answers.
Clause detection is semantic: does this contract contain a limitation of liability clause? Is there a non-compete? What are the termination provisions? These require the LLM to reason about meaning, not just locate text.
For clause detection, we use a two-step approach. First, retrieve the sections of the document most likely to contain the clause using semantic search over the document's chunks. Second, ask the LLM to analyze those specific sections and determine presence, location, and key terms. Asking the LLM to analyze an entire 80-page contract for every clause type at once is expensive, slow, and less accurate than targeted retrieval followed by focused analysis.
What "90% Accuracy" Means
When a vendor says their document intelligence system is 90% accurate, ask: accurate on what? On extracting the effective date from a clean, digital, English-language contract from the last five years? Probably fine. On extracting the automatic renewal clause from a scanned 2003 contract in a three-column layout? Very different story.
Before you set an accuracy target, define the cost of an error. For contract start dates used in a renewal tracking system, a 3% error rate might be acceptable because a human reviews anything flagged as expiring within 30 days. For total value used in financial reporting, a 3% error rate is not acceptable.
This architecture works in production for legal, insurance, and finance teams.
Book a call and we will show you how this architecture applies to your document types and volume.