Architettura di un Sistema di Elaborazione Documenti con AI: Guida Tecnica
Come è fatto internamente un sistema di elaborazione documenti con AI? Questa guida tecnica spiega l'architettura layer per layer, con diagrammi, schema del database, e le decisioni di design che fanno la differenza in produzione.
Vista d'insieme: i servizi del sistema
┌──────────────────────────────────────────────────────────────┐
│ DOCKER COMPOSE STACK │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ telegram- │ │ email- │ │ API REST │ │
│ │ bot │ │ monitor │ │ FastAPI :8000 │ │
│ └──────┬───────┘ └──────┬───────┘ └─────────┬──────────┘ │
│ │ │ │ │
│ └────────────────┬┘ │ │
│ │ │ │
│ ┌───────────▼──────────────────────▼──────────┐ │
│ │ PROCESSOR │ │
│ │ Classificazione + Estrazione AI │ │
│ │ Validazione + Confidence Score │ │
│ └───────────────────────┬──────────────────────┘ │
│ │ │
│ ┌───────────────────────▼──────────────────────┐ │
│ │ OUTPUT ROUTER │ │
│ │ Dead-letter queue + Retry logic │ │
│ │ 18 adapter: Webhook/CRM/ERP/Gestionale │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────────┐ │
│ │ Ollama │ │ Dashboard Streamlit :8501 │ │
│ │ LLM server │ │ Monitoring + Impostazioni │ │
│ │ GPU accelerated │ │ Revisione documenti │ │
│ └──────────────────┘ └──────────────────────────────────┘ │
│ │
│ Volume persistente: /data/ │
│ SQLite DB + archivio PDF + results JSON + config │
└──────────────────────────────────────────────────────────────┘
Schema del database
Il database SQLite contiene lo storico di ogni documento elaborato. Le tabelle principali:
CREATE TABLE documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_name TEXT NOT NULL,
file_hash TEXT UNIQUE, -- SHA256 per deduplicazione
file_path TEXT, -- Percorso nell'archivio
source TEXT, -- 'telegram' | 'email' | 'api'
received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
processed_at DATETIME,
status TEXT DEFAULT 'pending', -- 'pending' | 'processing' | 'done' | 'error'
-- Risultati estrazione
tipo_documento TEXT, -- 'fattura' | 'ddt' | 'ordine' | ...
result_path TEXT, -- Percorso al JSON con tutti i campi estratti
confidence INTEGER, -- 0-100
audit_status TEXT, -- 'VALIDATED' | 'NEEDS_REVIEW' | 'ERROR'
-- Math check
math_ok BOOLEAN,
math_delta REAL,
-- Revisione umana
reviewed_by TEXT,
reviewed_at DATETIME,
review_notes TEXT,
-- Dispatch adapter
dispatched_adapters TEXT, -- JSON array: ['webhook', 'zucchetti']
dispatch_errors TEXT -- JSON: {'sap_b1': 'Connection timeout'}
);
Il Processor: cuore del sistema
Il processor è il servizio che trasforma il documento grezzo in dati strutturati. Le sue responsabilità:
Step 1: Preprocessing del documento
Il PDF viene convertito in immagini PNG con pdf2image (DPI configurabile, default 200). Per ogni pagina, il sistema applica: deskewing automatico, normalizzazione del contrasto, riduzione del rumore. Le immagini vengono codificate in base64 per l'invio al modello VLM.
Step 2: Classificazione tipo documento
Il VLM riceve la prima pagina e un prompt di classificazione. Risponde con il tipo documento: 'fattura', 'ddt', 'ordine_acquisto', 'nota_credito', 'contratto', 'altro'. Questa classificazione determina lo schema di estrazione da usare nel passo successivo.
Step 3: Estrazione strutturata
Il VLM riceve tutte le pagine del documento e un prompt specifico per il tipo documento. Il prompt descrive esattamente i campi da estrarre e il formato JSON richiesto. Il modello risponde con il JSON strutturato.
Step 4: Validazione
Math check: verifica che imponibile + IVA = totale con tolleranza configurabile. Validazione formato P.IVA (11 cifre + checksum), CF, IBAN, date (ISO 8601). Calcolo confidence score (0-100) basato su completezza campi e risultato validazioni.
Step 5: Audit status
VALIDATED: confidence ≥ 85 e math check OK → pronto per dispatch automatico. NEEDS_REVIEW: confidence 60-84 o math check fallito → entra in coda revisione umana. ERROR: confidence < 60 o campi obbligatori mancanti → richiede intervento manuale.
Il Router e la dead-letter queue
Il router riceve il risultato validato dal processor e lo invia agli adapter configurati. Il pattern architetturale fondamentale è la dead-letter queue:
┌─────────────────────────────────────────────────┐
│ ROUTER: _safe_dispatch() │
│ │
│ Per ogni adapter configurato: │
│ try: │
│ adapter.dispatch(result) ───→ ✅ Successo │
│ except Exception as e: │
│ → salva in failed_dispatches.jsonl │
│ → continua con il prossimo adapter │
│ │
│ Failed dispatches: │
│ { │
│ "doc_id": 1234, │
│ "adapter": "sap_b1", │
│ "error": "Connection timeout", │
│ "timestamp": "2026-03-15T10:30:00Z", │
│ "payload": {...} │
│ } │
│ │
│ Retry: manuale via dashboard o API │
│ Auto-retry: configurabile ogni N minuti │
└─────────────────────────────────────────────────┘
Questo pattern garantisce che un adapter fallito non blocchi gli altri. Se SAP B1 è irraggiungibile, il documento viene comunque inviato a Salesforce, all'email e al webhook. Solo il dispatch SAP B1 finisce nella dead-letter queue per il retry successivo.
Decisioni di design critiche
Perché SQLite invece di PostgreSQL?
SQLite è sufficiente per volumi PMI (<50.000 documenti/anno) e azzera la complessità operativa. Nessun servizio database separato da mantenere. Il file .db è copiabile direttamente per i backup. Per volumi enterprise (>500.000 documenti/anno), la migrazione a PostgreSQL è prevista ma non necessaria per il 95% dei casi.
Perché Ollama come LLM server?
Ollama gestisce il lifecycle del modello (download, versioning, serving con GPU), espone un'API REST compatibile con OpenAI, gestisce la VRAM e il batching. Alternative valutate: llama.cpp diretto (meno funzionale), vLLM (ottimo ma più complesso per singolo modello), LocalAI (più versatile ma overhead maggiore).
Perché Docker Compose invece di Kubernetes?
Kubernetes è sovradimensionato per un'installazione single-node in una PMI. Docker Compose offre orchestrazione sufficiente, deploy in <30 minuti, familiarità diffusa, update senza downtime con restart dei singoli container. Kubernetes ha senso solo per deployment multi-nodo o cloud managed (EKS, GKE).
Perché JSON per i risultati invece di colonne DB?
Lo schema di estrazione è diverso per ogni tipo documento (fattura vs DDT vs contratto). Salvare tutto in JSON permette di aggiungere nuovi tipi documento e nuovi campi senza migrazioni dello schema del database. Solo i metadati (confidence, status, tipo) sono in colonne DB per permettere query efficienti.
Il layer di integrazione: 18 adapter
Ogni adapter implementa una sola interfaccia: riceve il payload JSON del documento e lo invia al sistema target. Gli adapter sono indipendenti: fallire in uno non blocca gli altri.
| Categoria | Adapter | Protocollo |
|---|---|---|
| Output generico | Webhook | HTTP POST JSON |
| CSV/Excel | File append | |
| SMTP | ||
| Google Sheets | Sheets API v4 | |
| Fatturazione IT | FatturaPA XML | Generazione XML SDI |
| Fatture in Cloud | REST API | |
| CRM | Salesforce | REST API + OAuth2 |
| HubSpot | REST API Private App | |
| Odoo | XML-RPC | |
| Gestionali IT | Zucchetti | REST / CSV |
| TeamSystem | Digital Hub API | |
| Mexal | Tracciato ASCII | |
| Enterprise | Microsoft 365 | Graph API + OAuth2 |
| SAP Business One | Service Layer REST |
Monitoring e metriche chiave
Le metriche da monitorare in produzione:
- 📊 Throughput: Documenti elaborati/ora. Alert se cala >20% rispetto alla baseline.
- 🎯 Tasso revisione manuale: % documenti con audit_status = NEEDS_REVIEW. Obiettivo <10%. Aumenti anomali segnalano problemi di qualità dei documenti in input.
- ❌ Dead-letter queue size: Numero di dispatch in errore in attesa di retry. Alert se supera 50.
- ⏱️ Latenza elaborazione: Tempo medio dal ricevimento al completamento. Aumenti indicano problemi GPU o Ollama.
- 📈 Confidence score medio: Tendenza nel tempo. Calo progressivo può indicare drift nei tipi di documento.
Questa è l'architettura che alimenta DataUnchain. Open source, deployabile in 30 minuti.
Esplora DataUnchain →