Mario Marino

progettista e sviluppatore di applicativi gestionali e sistemi basati su LLM

Validazione documentale automatica

Una pipeline fra tre sistemi, dove i modelli leggono e il codice decide

Periodo
dal 2025
Ruolo
progettazione, sviluppo, test e definizione del contratto
Tecnologie
Python · Google Cloud Vision · GPT-4o · modelli di visione · ExifTool · PyMuPDF · n8n
Stato
in produzione

a cosa serve

Chi rendiconta un progetto carica documenti: fotografie che devono provare un'attività avvenuta in un certo luogo e in una certa data, scontrini e ricevute che devono diventare righe di spesa. Verificarli a mano significa aprirli uno per uno, leggere date e importi, trascriverli e confrontarli con quello che il progetto dichiara. È il tipo di lavoro in cui un errore non si vede: una data fuori periodo o un importo trascritto male non hanno un aspetto diverso da un dato giusto.

Questa pipeline lo fa al posto della persona, e usa tre modelli per farlo: uno che legge le parole da un'immagine, uno che guarda una fotografia e ne descrive il contenuto, uno che interpreta i campi rimasti ambigui nel testo. Il risultato torna al gestionale come un insieme di semafori e di righe precompilate: chi controlla vede subito dove guardare, invece di aprire tutto.

La decisione, però, resta fuori dalla portata dei modelli. Il servizio produce fatti — cosa c'è scritto sul documento, cosa dicono i metadati della fotografia, dove il dato non torna col contesto dichiarato — e non un giudizio di ammissibilità. Quello lo calcola il gestionale, in modo deterministico. È il vincolo attorno a cui è costruito tutto il resto, e la ragione per cui una pipeline che usa tre modelli resta verificabile da una persona.

L'ho progettata e sviluppata da solo, contratto fra i sistemi compreso.

come è fatta

Tre sistemi in fila, e un quarto elemento che non esegue niente ma tiene insieme gli altri: un repository fatto di soli contratti, senza una riga di codice applicativo.

gestionale n8n servizio di analisi repository dei contratti carica il file, decide l'esito trasporto, nessuna logica legge i documenti, non giudica nessun codice applicativo: dice quali campi il servizio restituisce, cosa ciascuno può contenere, e cosa fare di un valore che non si conosce callback autenticato: il risultato torna al gestionale
n8n sta nel percorso ma resta fuori dal contratto: trasporta la richiesta e la risposta senza obbligarsi su cosa contengono. Il contratto vincola i due sistemi che si scambiano l'analisi.

Al caricamento di un file parte un'elaborazione asincrona, e il risultato torna al gestionale come callback autenticato. Da lì diventa un sistema di semafori nell'interfaccia: colori che dicono, per ogni documento, quali controlli sono passati e quali no.

il contratto è la sostanza

Il lavoro che pesa di più non è nessuno dei tre stadi: è aver scritto il contratto fra i due sistemi che si scambiano l'analisi. Quali campi restituisce il servizio, cosa può contenere ciascun blocco, cosa il gestionale può aspettarsi di trovare quando legge il callback, e cosa succede quando arriva un valore che non conosce.

Un contratto del genere nasce di solito da una trattativa fra chi scrive i due lati. Qui l'ho scritto da solo, per sistemi su cui intervengo io stesso: è una scelta di progettazione, guardando insieme i vincoli di chi produce il dato e di chi lo consuma, e non la mediazione fra due parti che difendono interessi diversi. Il rischio di questa condizione è noto — chi scrive tutti e due i lati può far combaciare le cose per come le ha in testa oggi — e lo contengo tenendo il contratto in un repository suo, dove una modifica si scrive prima di essere implementata da una delle due parti.

il confine passa fra i fatti e la decisione

La regola che governa tutto il documento è che il produttore non emette giudizi, e le sue conseguenze sono più concrete di quanto sembri.

La classificazione della natura di una spesa, per esempio, è una proposta: il servizio dice se una riga somiglia a un alloggio, a un viaggio, a un pasto, o dichiara di non saperlo. Non emette i codici del database del gestionale, e non perché sia scomodo: una scelta di quel dominio non deve attraversare il confine. Tradurre la proposta nel vocabolario interno è responsabilità di chi consuma, e il valore «non lo so» non ha corrispondente — resta all'operatore.

Allo stesso modo un campo che il servizio non è riuscito a determinare arriva con un valore esplicito di non-determinato, e non semplicemente vuoto o assente: «non l'ho letto» è un'informazione diversa da «non c'è», e il consumatore la tratta come vuoto ma con priorità di revisione.

due famiglie di avvisi, che dicono cose diverse

Sono la parte del contratto che uso come esempio quando devo spiegare cosa distingue un contratto scritto da uno improvvisato.

Famiglia Cosa dice Cosa implica
Avvisi di lettura quanto il dato è certo: più date rilevate e presa la prima, più importi vicino a una parola-totale, valuta dedotta dal contesto invece che letta il dato potrebbe essere sbagliato
Avvisi di coerenza il dato è letto bene, ma non torna col contesto: data fuori dal periodo dichiarato, valuta diversa da quelle attese, città diversa da quella dell'evento il documento potrebbe non appartenere a questa trasferta

Tenerle separate serve a chi legge: un avviso di lettura mette in dubbio il numero, uno di coerenza mette in dubbio l'appartenenza del documento. Nessuna delle due invalida la riga — sono un invito alla revisione umana, non un esito — e gli avvisi di coerenza vengono emessi solo quando il confronto è davvero possibile: un campo non determinato o un contesto senza evento non producono incoerenze inventate.

la lezione sul versionare: una garanzia vale finché regge la sua premessa

Un giorno ho cambiato il significato di un codice di avviso senza aumentare il numero di versione del contratto. La motivazione non era pigrizia ed era verificata sul codice dell'altro lato: nessun ramo del consumatore dipendeva dal valore di quel codice — li univa tutti in una nota di testo e conservava il payload intero. Con quella premessa, cinque codici in più o un significato diverso non rompevano niente.

Ho scritto nel contratto anche la condizione a cui quella garanzia era appesa: vale finché resta vero che nessuno discrimina. È caduta lo stesso giorno, la sera: il consumatore ha iniziato a usare i codici per comporre la nota all'operatore. Nessuna decisione di business dipende ancora da un codice, ma la premessa non c'è più — quindi da quel momento la semantica dei codici è congelata, e cambiarne uno esistente richiede il numero di versione nuovo. Aggiungerne resta additivo, perché il consumatore elenca in coda i codici che non conosce invece di ignorarli.

La lezione non è sul versionamento in sé: è che una deroga si può concedere solo se si scrive accanto la condizione che la rende innocua, altrimenti sopravvive alla ragione per cui era stata accettata.

dalla foto ai fatti

Il percorso delle fotografie non usa affatto la lettura del testo: lavora sui metadati che la macchina fotografica scrive dentro il file, e serve a rispondere a due domande — questa foto è stata scattata quando dice il progetto, ed è stata scattata dove dice il progetto.

fotografia metadati del file luogo risolto controlli fatti verso il gestionale nessun modello la guarda data dello scatto, coordinate, dispositivo — lettura deterministica servizio esterno, tenuto separato la data cade nel periodo? il luogo coincide col paese atteso? più il perché di ogni esito la lettura del file e l'arricchimento esterno restano due passi: il primo è ripetibile, il secondo dipende da terzi e può mancare
Sul percorso delle fotografie nessun modello guarda l'immagine, a meno che non venga chiesto esplicitamente: i controlli lavorano su dati scritti dalla macchina fotografica.

Le scelte che contano sono quattro, e tutte hanno a che fare col non spacciare per prova qualcosa che non lo è.

La lettura del file e la risoluzione delle coordinate in un indirizzo sono due passi deliberatamente separati: il primo è deterministico e ripetibile, il secondo dipende da un servizio di terzi, può mancare, e porta con sé l'attribuzione della fonte. Tenerli insieme avrebbe reso non ripetibile anche la parte che non ha ragione di esserlo.

due fasi, e settantun secondi che non servivano a niente

Il percorso degli archivi di foto ha una struttura a due fasi che è nata da un difetto pagato in produzione, e lo racconto perché il modo in cui è stato corretto conta più del difetto.

Una validazione su un archivio di trentadue foto era rimasta appesa. La richiesta era durata circa centoventicinque secondi, e settantuno di quei secondi erano serviti a caricare le foto sul fornitore del modello, una alla volta — in una modalità in cui il modello non viene mai chiamato. I file venivano caricati, non usati da nessuno, e cancellati a fine elaborazione. Il tempo perso ha fatto sfondare il limite del trasporto, che ha chiuso la connessione pochi secondi prima che il servizio finisse.

Due cose ho voluto scrivere per intero nella decisione. La prima è da quando a quando: il difetto era entrato tre settimane prima, con il lavoro che introduceva il caricamento unico delle immagini, e in quella finestra ogni elaborazione di soli metadati ha caricato l'intero archivio su un fornitore esterno senza che nessun modello lo guardasse. È il genere di data che serve avere quando qualcuno chiede da quando a quando, e non è ricostruibile a posteriori se non la si scrive subito.

La seconda è perché la correzione non è stata un controllo in più. Il varco mancante era uno solo e una riga lo avrebbe chiuso. Ma la ragione per cui mancava è che l'ordine del codice non diceva chi dipendesse da cosa: il caricamento stava in cima a tutto, mentre le due vere chiamate al modello erano già condizionate più in basso. Chi l'aveva scritto l'aveva messo dove stava l'estrazione dell'archivio — il posto giusto per estrarre, quello sbagliato per una chiamata di rete. Ora le due fasi sono esplicite e la fase dei soli metadati non riceve nemmeno il client del fornitore: non ha proprio il modo di contattarlo, invece di doverlo evitare per disciplina di chi legge il codice. La stessa svista, oggi, costa un errore nei test invece di settantun secondi in produzione.

quando un file non si può analizzare, si dice cosa fare

Un archivio poteva arrivare pieno di file in formato RAW — quelli che alcuni telefoni salvano accanto al JPEG — e tornare al gestionale come archivio vuoto, senza dire perché. Nella schermata non compariva nulla di utile, e la conclusione di chi guardava era che il sistema non funzionasse.

Due decisioni, entrambe piccole e con effetti sproporzionati.

Il controllo sul formato viene prima di quello sulla dimensione. Un file RAW da cinquanta megabyte sfora entrambi i limiti, e scartarlo come «troppo grande» è vero e inutile: chi legge lo comprime, riprova, e sbatte contro il formato con un errore diverso. Fra due ragioni entrambe corrette si dichiara quella su cui l'utente può agire.

E la ragione di scarto porta la via d'uscita, non solo il rifiuto: quali formati sono accettati, e come si esporta un RAW in un formato che conserva i metadati originali. Un motivo di scarto non è un codice di errore per chi sviluppa — è l'unica cosa che l'utente legge, e deve permettergli di risolvere senza aprire una segnalazione. Il testo è lungo per questo.

C'è anche un effetto sui conteggi che ho dichiarato perché cambia i numeri che qualcuno potrebbe confrontare: un RAW piccolo prima passava, veniva «elaborato» e produceva un esito negativo muto; ora è scartato come quello grande, con la stessa spiegazione. Si sposta dalla colonna degli elaborati a quella degli esclusi, ed è la classificazione onesta — un file che non sappiamo analizzare è escluso, non analizzato male.

dalla pagina alle righe di spesa

Il percorso più interessante è quello delle note spese: una pagina scansionata con più scontrini attaccati deve diventare una riga per ogni spesa. È il caso in cui la domanda «dove sta l'AI in questo sistema» ha una risposta precisa.

pagina scansionata lettura delle parole separazione dei documenti N documenti estrazione dei campi campi rimasti incerti controlli di coerenza righe verso il gestionale modello OCR: testo, riquadri, confidenza codice: geometria delle parole regole: data, totale, valuta, città modello testuale contro il contesto dichiarato vede l'immagine — è l'unico vede solo coordinate: nessuna immagine vede solo testo vede solo testo: non può leggere una città in un logo
I due modelli lavorano agli estremi opposti e il centro è codice deterministico. Quale stadio riceve l'immagine è la domanda che spiega quasi tutti i difetti trovati.

la separazione viene dopo la lettura, e non era il piano

Sulla carta il modulo ha una fase di segmentazione e poi una di lettura: prima si ritagliano i documenti dalla pagina guardando i pixel, poi si legge ciascuno. Nei fatti la segmentazione a pixel cerca bande scure fra un documento e l'altro e sui documenti veri non ne trova mai, perché gli scontrini sono carta chiara su fondo chiaro. Passa quindi la pagina intera al lettore, e i documenti si separano dopo, sulle coordinate delle parole.

Non è un ripiego, ed è diventato il vincolo che governa ogni scelta futura di motore. Le coordinate sono un dato più affidabile dell'intensità dei pixel e non costano nulla in più: arrivano insieme al testo, con una sola chiamata. Ma qualunque alternativa dovrà restituire testo e geometria: senza riquadri salta la separazione, e una pagina con tre scontrini torna a produrre una riga sola. È anche il motivo per cui un modello linguistico generico non può fare da lettore di riserva — restituisce testo, non coordinate.

quando un blocco di testo è un documento

È il cuore della separazione, ed è un criterio semantico, non geometrico. Il motivo è misurato: dentro un singolo scontrino i vuoti fra intestazione, corpo e piede arrivano a 4,0 altezze-riga, mentre il vuoto fra due scontrini impilati scende a 3,2. Le due distribuzioni si sovrappongono, quindi nessuna soglia di spazio bianco separa i documenti da sola.

Il codice taglia allora volutamente fitto, alternando i due assi perché due scontrini affiancati ma sfalsati non si separano con una proiezione sola, e poi ricompone: un blocco vale come documento se contiene un importo e, insieme, una parola-totale o una data. Tutto il resto — carta intestata, piè di pagina, condizioni contrattuali — viene riattaccato al documento adiacente.

Il criterio serve in tutte e due le direzioni, e va letto così. Senza la richiesta dell'importo, il blocco di condizioni di un biglietto aereo diventerebbe una riga di spesa fantasma. Con essa, un documento vero che il criterio non riconosce viene declassato a frammento e riassorbito nel vicino: è così che nasce una fusione, e il testo finisce nella riga sbagliata invece di sparire.

il caso che ha mostrato dove sta la fragilità

Il criterio dipende da una lista di parole e da una forma di numero: è conoscenza, e la conoscenza può mancare. Sulle note spese in cinese il motore falliva sistematicamente, per due ragioni indipendenti scoperte misurando.

Il risultato: documenti persi dal 36,1% al 25,0%, righe attribuibili con certezza da 50 a 71, e ogni campo migliora in assoluto — data e totale da 41 e 42 letture corrette a 63.

La lezione generale conta più del cinese: il motore non era al suo limite, era cieco su una lingua. Quello che si è tolto è un difetto, non un vincolo, e il tetto vero dell'approccio deterministico resta quindi ignoto.

come si misura

Un comando esegue la pipeline vera su un campione annotato — 108 documenti su 37 pagine, da note spese reali in più lingue e valute — abbina ogni riga prodotta al documento che le corrisponde, e stampa le metriche. L'esito della lettura è tenuto in cache su disco, così separazione, estrazione e istruzioni al modello si possono rieseguire senza una sola chiamata a pagamento: la variante «solo regole» è gratuita e deterministica.

La separazione ha quattro esiti, non due, e la distinzione è ciò che rende la misura utile:

Esito Perché conta
Fuso due documenti in una riga sola: chi controlla se ne accorge, il totale è ambiguo
Perso nessuna riga lo copre: una spesa sparisce senza traccia
Eccesso un documento spezzato in due righe: produce spese che non esistono, e nessuno se ne accorge sommando
Riga fantasma non corrisponde a niente

L'esito «eccesso» è il motivo per cui un motore che migliora il conteggio medio va scartato lo stesso se sbaglia per eccesso. Un conteggio non lo mostra; questa colonna sì.

Sui campi gli esiti sono quattro per la stessa ragione: corretto, sbagliato, mancato — il documento lo dice, il motore no, e si vede — e inventato: il documento non lo dice e il motore risponde comunque, e non si vede. Astenersi dove il documento tace conta come risposta corretta, altrimenti un motore che tira a indovinare batterebbe uno che si astiene.

la colonna che ha cambiato le priorità

È «inventato» ad aver rivelato il difetto più grave, che nessun'altra misura avrebbe visto. Sulla città il modello produceva 17 valori inventati su 50, e non a caso: erano tutti la città dell'evento a cui la trasferta si riferiva. Su una ricevuta di taxi che l'indirizzo lo stampa per esteso, lo sovrascriveva con la città dell'evento — e nelle due scansioni della stessa pagina scriveva due città diverse, quindi non era nemmeno coerente con sé stesso.

La spiegazione sta nel diagramma qui sopra: quel modello non vede l'immagine, e una città stampata dentro un logo non gli arriva affatto: quel dato è fuori dal suo ingresso, quindi non c'è modo che lo legga. E quando non lo trova nel testo, tende a ripetere il contesto che gli è stato dato.

La città è stata tolta al modello invece di correggerlo con istruzioni più severe: da 27 letture corrette e 17 invenzioni si è passati a 35 corrette, zero sbagliate e una sola invenzione. Quando un componente non ha in ingresso l'informazione che gli si chiede, il rimedio non è chiedergliela meglio.

Insieme alla domanda è uscito anche il suggerimento: la città dell'evento non compare più nemmeno fra i riferimenti passati al modello, perché chiedere un dato e suggerirlo sono lo stesso canale. E una cosa peggiora, dichiarata: le astensioni sulla città passano da 4 a 14 su 50 righe. È peggio in apparenza e meglio in sostanza — un'astensione si vede in revisione, una città sbagliata no, e un avviso di incoerenza calcolato su una città inventata manda chi controlla a cercare un problema che non esiste.

i numeri di oggi

Misura Prima Oggi
Documenti persi (fusi o non letti) 39 — 36,1% 27 — 25,0%
Righe separate bene e attribuibili 50 71
Data · totale corretti 41 · 42 63 · 63
Valuta · città corretti 45 · 35 62 · 49
Natura della spesa corretta 47 68
Chiamate al modello · tempo 142 · 241 s 155 · 177 s

Due limiti del banco, da sapere prima di citarne un numero. La verità di riferimento è stata redatta leggendo le pagine e ratificata da una persona, non da un oracolo indipendente: un errore sistematico condiviso fra chi annota e la pipeline sarebbe invisibile. E non esiste un sottoinsieme di controllo — i difetti sono stati cercati sugli stessi documenti su cui si misura il miglioramento. Su un fatto di lingua il rischio è basso, ma la disciplina corretta è tenere da parte alcune pagine annotate e non guardarle mai.

C'è anche una conseguenza contro-intuitiva che vale la pena dichiarare: separare meglio i documenti aumenta il costo, perché si paga a riga e le righe diventano di più. Le chiamate sono salite da 142 a 155 proprio perché la separazione è migliorata.

prima si misura, poi si sostituisce

Il principio che governa gli sviluppi: prima si misura il motore che già gira, poi si affianca un'alternativa. Senza un riferimento numerico, sostituire uno stadio è un atto di fede e il risultato si giudica guardando le immagini, che è esattamente ciò che si voleva smettere di fare. Ogni fase ha una soglia di passaggio, e alla successiva si procede solo se la misura della precedente lo giustifica.

La fase sospesa si riapre a una condizione precisa, ed è l'unica cosa che la geometria non potrà mai coprire: leggere la città dai loghi, che al modello testuale non arriva.

l'input è una variabile del risultato

Siccome la separazione è geometria, la qualità della scansione conta quanto il codice. La composizione dei documenti ancora persi lo dice senza margini: nessuno di essi chiede un lettore migliore. Da qui quattro regole di acquisizione, che costano una pagina di istruzioni invece di un modello nuovo.

Sommando le quattro regole la soglia del 5% sarebbe raggiungibile senza aggiungere un modello. Ma è una stima da geometria e annotazione: nessuno ha ancora riscansionato quelle pagine seguendo le regole, quindi resta un'ipotesi con dei numeri dietro, non un risultato.

limiti dichiarati

quello che resta fermo

Intorno a tutto il flusso valgono alcune proprietà fisse, ed è la parte che dice fin dove un modello può arrivare dentro un processo che deve restare verificabile anche dopo che ha risposto.

I modelli osservano ed estraggono; la decisione sull'esito della validazione resta fuori dalla loro portata. I numeri economici restano calcolati in modo deterministico nel gestionale. Il livello di trasporto si limita a spostare la richiesta, senza applicare logica propria. Un callback che fallisce non deve lasciare un record a metà, in attesa di un esito che potrebbe non arrivare più.

stato

In produzione. Il campione annotato e i documenti di collaudo non sono versionati — sono note spese reali — e senza quei file il banco di prova non gira: dà un errore esplicito, non un risultato vuoto.