Over the last two years I have processed roughly two million documents across five client projects: medical records for a Karachi cardiology clinic, customs forms for a Berlin logistics company, insurance claims for a Bahrain broker, property contracts for a Dubai property management firm, and vendor invoices for a Manchester manufacturer. The document types, languages, and quality varied enormously. The failure modes were surprisingly consistent.
Here is what I learned, including the things I got wrong.
The Architecture That Works
After iterating through several approaches, here is the architecture I now deploy for every document extraction pipeline:
Input Documents
|
v
Pre-processing Layer
(deskew, denoise, split multi-page, normalize resolution)
|
v
Route by Document Type
|
+-- Structured forms --> Template-based OCR (Tesseract / Azure Form Recognizer)
|
+-- Semi-structured --> Vision LLM extraction (GPT-4o / Claude 3.5 Sonnet)
|
+-- Handwritten --> Vision LLM + confidence scoring
|
v
Extraction Output (JSON)
|
v
Validation Layer
(schema check, cross-field validation,
confidence thresholds)
|
+-------------+-------------+
| |
Pass Fail
| |
Output queue Human review queue
The routing decision is the most important architectural choice. Many teams default to a single model for everything and then wonder why their accuracy on handwritten forms is poor, or why their costs on standard invoices are ten times higher than necessary.
Pre-processing: The Half Nobody Talks About
I wasted a full week on the Berlin logistics project trying to improve extraction accuracy with better models. The real problem was that the incoming customs forms were scanned at 150 DPI, slightly rotated, and had a noise pattern from an aging scanner. Switching from 150 to 300 DPI, adding deskewing (I use OpenCV's Hough line transform), and applying a simple denoising pass improved accuracy from 71% to 91% before I touched the extraction model at all.
Pre-processing steps that consistently improve accuracy:
- Resolution normalization to 300 DPI minimum
- Deskewing: correct rotation up to 15 degrees using Hough transform
- Denoising: bilateral filter for scanned documents, Gaussian for fax artifacts
- Contrast normalization: histogram stretching for washed-out scans
- Multi-page splitting: handle PDFs with separator pages and mixed content
- Border cropping: remove scanner frame artifacts that confuse layout parsers
This step takes two days to implement properly and saves months of model tuning. Do it before you evaluate any models.
Model Selection by Document Type
| Document Type | Recommended Tool | Accuracy Range | Cost per 1000 Pages |
|---|---|---|---|
| Standard invoices (digital PDF) | Azure Form Recognizer | 97-99% | $1.50 |
| Scanned structured forms | Tesseract + pre-processing | 90-95% | $0.10 |
| Semi-structured documents | GPT-4o Vision | 93-96% | $8.00 |
| Handwritten notes | GPT-4o Vision | 80-88% | $8.00 |
| Mixed or unknown quality | Vision LLM + confidence routing | Varies | $4-10 |
These numbers come from production deployments, not vendor benchmarks. The accuracy range reflects document quality variance. A clean digital invoice consistently hits 99%. A fax-forwarded-to-email invoice from a supplier with aging equipment hits 87%.
The cost difference between Tesseract and GPT-4o Vision is roughly 80x. For the Manchester project, which processed 15,000 invoices per month, routing all documents through GPT-4o Vision would have cost $1,200 per month versus $180 using the hybrid approach. The hybrid routes structured digital documents to Form Recognizer, flags ambiguous documents for Vision LLM, and uses Tesseract for the high-volume simple scans.
The Validation Layer: Where Most Pipelines Break
Extraction accuracy of 95% sounds acceptable until you realize that in a batch of 10,000 invoices, 500 have incorrect or missing data. If that data feeds directly into your accounting system without validation, you now have 500 accounting errors and no clean audit trail.
The validation layer does three things.
Schema validation: Does the output contain all required fields in the expected format? A date field that returns "March 15" instead of "2024-03-15" fails schema validation and routes to human review.
Cross-field validation: Do the fields make logical sense together? Invoice total should equal line items plus tax. A departure date should not precede an arrival date. These rules catch extraction hallucinations that pass format checks.
Confidence thresholding: Vision LLMs can return confidence scores when prompted. Any field below 0.85 confidence routes to human review regardless of format validity.
from dataclasses import dataclass
from typing import Optional
def validate_invoice(extracted: dict) -> tuple[bool, list[str]]:
errors = []
# Schema validation
required = ["vendor_name", "invoice_date", "total_amount", "line_items"]
for field in required:
if not extracted.get(field):
errors.append(f"missing_field:{field}")
# Cross-field validation
if extracted.get("line_items") and extracted.get("total_amount"):
computed = sum(item["amount"] for item in extracted["line_items"])
stated = float(extracted["total_amount"])
if abs(computed - stated) > 0.01:
errors.append(
f"total_mismatch:computed={computed:.2f},stated={stated:.2f}"
)
# Confidence thresholding
for field, conf in extracted.get("_confidence", {}).items():
if conf < 0.85:
errors.append(f"low_confidence:{field}={conf:.2f}")
return len(errors) == 0, errors
Real Cost Breakdown: Berlin Logistics Project (15,000 Forms/Month)
| Component | Tool | Monthly Cost |
|---|---|---|
| Pre-processing service | Self-hosted Python (t3.small) | $18 |
| OCR extraction | Azure Form Recognizer custom model | $67 |
| Validation exceptions (LLM) | GPT-4o Vision (flagged docs only) | $31 |
| Human review (2% of volume) | 4h/month at $20/h | $80 |
| Infrastructure | 2x t3.medium (AWS) | $60 |
| Total | $256/month |
The previous manual process cost approximately $2,400 per month in staff time. Payback period on the build cost of $4,800: two months.
The key insight is the hybrid routing. Running every document through GPT-4o Vision would cost $1,200 per month and deliver marginally better accuracy on the structured forms where Azure Form Recognizer already achieves 97%. Routing by document type reduces LLM costs by 85% with negligible accuracy loss.
What I Got Wrong
I underestimated pre-processing time. My initial project plan allocated two days for pre-processing and ten days for model selection and tuning. The reality inverted: eight days on pre-processing, four on model work. Every project since has front-loaded pre-processing.
I trusted vendor accuracy benchmarks. Azure Form Recognizer claims 98%+ accuracy on invoices. On the Manchester project, which included invoices from UK suppliers across a twelve-year archive, accuracy on older documents dropped to 84%. Vendor benchmarks use clean, modern documents. Your archive probably does not look like that.
I skipped confidence scoring in the first version. The Karachi clinic project initially routed all extraction output directly to their EHR without confidence thresholding. Three weeks in, a misread field on a medical form caused a near-miss incident. We rebuilt the validation layer in 48 hours. That was entirely preventable.
Production Reality
Two million documents in, the most reliable pipelines share three characteristics: aggressive pre-processing, conservative confidence thresholds (route to human review early rather than late), and a human review queue treated as a product feature rather than a fallback.
The best operators I have seen treat their human review queue as a training signal. Every corrected extraction feeds back into threshold calibration and custom model improvement. The human review rate on the Berlin logistics pipeline dropped from 8% in month one to 2% in month six, not because we removed the humans, but because we used them to improve the system.
The worst pipelines I have seen optimized for reducing human review to zero. A 0% human review rate on document extraction is a red flag, not a success metric. It means you have disconnected your quality signal and are flying blind.