Quando il vocabolario inganna: distinguere un ordine da un sollecito

Smistare la posta di una casella ordini sembra un problema da filtri. Cerchi “ordine”, “conferma”, “numero commissione” e separi. Per un’azienda alimentare italiana quella regola fissa lasciava fuori 168 ordini veri, circa il 14%. Sono 168 consegne che non sono partite.

La sfida: parole uguali, intenzioni diverse

Ogni mail finisce in ordine o non_ordine, ma i due errori non pesano uguale. Un falso positivo costa all’operatore i due secondi che impiega a scartarlo. Un ordine perso costa un ordine. Abbiamo quindi ottimizzato sul recall, la quota di ordini veri intercettati, e lasciato correre l’accuratezza complessiva.

La difficoltà vera sono le mail che parlano di ordini senza esserlo. Solleciti, avvisi di mancata consegna, chiusure per festività, risposte dentro conversazioni già evase: hanno tutte il vocabolario dell’ordine e nessuna è un ordine. Contro casi così una lista di parole-chiave non ha speranze.

Il secondo ostacolo non c’entrava con il modello. Il golden set, l’archivio incrementale di ogni mail vista su cui si basa il ri-addestramento, finiva in un percorso interno al container che nessuno aveva montato, e il fallback previsto non scattava mai. A fine job spariva tutto.

L’approccio tecnico: prima le etichette, poi il modello

L’architettura è ibrida. Un pre-filtro a regole fisse scarta mittenti esclusi e notifiche di sistema; il resto, fra oggetto, corpo e allegati, passa al modello.

Il modello è volutamente classico. TF-IDF, che pesa di più le parole rare e quindi informative, e sopra una regressione logistica bilanciata. Gli n-grammi di parola prendono i segnali espliciti; quelli di carattere reggono multilingua e refusi, visto che ord e din intercettano ordine, order e pedido senza doverli elencare.

Random Forest lo abbiamo scartato perché restituisce probabilità “a scalini”, scomode da tarare quando la soglia è la leva principale. Un LLM a runtime sarebbe stato costoso, non deterministico e francamente sproporzionato per una scelta binaria. Il classificatore rimasto pesa pochi megabyte e decide in millisecondi.

L’LLM però lo abbiamo usato eccome, solo da un’altra parte. Il primo dataset non nasce da mesi di annotazione manuale ma da 1.490 mail pre-etichettate da un modello generativo, ed è quel bootstrap ad aver prodotto il v1: PR-AUC 0,997 su 1.237 ordini e 253 non-ordini. Etichettare una volta sola è il caso in cui un LLM conviene. Farlo girare su ogni mail in arrivo, tutti i giorni, molto meno.

Il v1 è andato in produzione, dove gira ogni tre ore su quattro CPU virtuali e salva su disco ogni mail con la sua predizione. È stata questa abitudine a salvarci. Quando il golden set è servito e non c’era, lo abbiamo ricostruito dalle cartelle, riapplicando le stesse funzioni di estrazione testo dell’addestramento, così da ri-addestrare sul testo che il modello aveva letto davvero. Poi la parte noiosa, correggere a mano le etichette sbagliate. Il golden set è arrivato a 932 esempi e il v2 è nato da zero solo su quelli, etichette messe da persone su mail vere. Il difetto di persistenza intanto è stato corretto e rilasciato.

Il risultato

Il v2 ottiene recall 0,991 e precision 0,939, con PR-AUC 0,9966. Numeri belli, ma il guadagno vero si vede altrove. Sui 66 non-ordini che avevamo corretto a mano il v1 ne sbagliava ancora 7, il v2 solo 2. Il caso che ci ha convinti è un sollecito su un ordine già evaso: sopra una soglia di 0,18 una mail conta come ordine, e il v1 gli dava 0,618. Il v2 lo scarta con 0,136. Circa il 30% di falsi positivi in meno a parità di recall.

Restano tre casi che non prende nessuna versione: un inoltro d’ordine, un’aggiunta a un ordine già aperto, un documento di trasporto fatto di soli loghi. Sono mail con dentro pochissimo testo, e lì non c’è modello che tenga: per ora le guarda una persona. Il v2 intanto sta su un percorso separato in attesa del collaudo, la produzione non l’abbiamo toccata.

La lezione

Se dovessimo rifarlo, la cosa su cui staremmo più attenti non è la scelta del modello. Un classificatore in produzione è un processo che gira, sbaglia e impara dai propri errori, e si regge tutto sui dati che tornano indietro. Nel nostro caso a tenerlo in piedi mancava una riga di configurazione, e non ce ne saremmo accorti se non avessimo dovuto ri-addestrare.