Il problema con i tuoi LLM è quasi sempre un problema di dati

Ho perso il conto delle volte che un cliente mi ha detto: "Il modello sbaglia troppo, possiamo passare a GPT-5? O magari Claude Opus 5? Forse Gemini ci darebbe risposte migliori." La risposta, quasi sempre, è che il modello c'entra poco. Il modello vede quello che gli dai, e se gli dai dati sporchi, contraddittori o mal strutturati, ti restituisce risposte sporche, contraddittorie o mal strutturate. Funziona così, e non c'è prompt engineering che tenga.

Negli ultimi due anni abbiamo lavorato su parecchi progetti AI custom: assistenti per il customer support, agenti che leggono CRM aziendali, sistemi RAG su knowledge base interne, automazioni che processano documenti legali. Il pattern si ripete con una regolarità che ormai trovo divertente. Si comincia con grandi aspettative sul modello, si finisce a sistemare le pipeline di ingestion. E quando le pipeline diventano pulite, magicamente anche il modello "diventa più intelligente".

Voglio raccontare cosa significa nella pratica, perché il discorso "data quality" rischia di restare astratto se non si entra nei dettagli sgradevoli.

Cosa intendiamo davvero quando diciamo "allucinazione"

Il termine allucinazione è diventato un contenitore vago. Quando un cliente dice "il modello allucina", in genere sta descrivendo uno di questi scenari:

Il modello inventa un dato che non esiste, tipo un numero d'ordine o un'email, anche se il dato non è da nessuna parte nel contesto. Questa è l'allucinazione classica, ed è quasi sempre colpa di un retrieval che ha pescato male o di un prompt che chiede al modello di rispondere anche quando non sa.

Il modello prende un dato che esiste ma lo riporta sbagliato. Per esempio confonde la data di consegna con la data dell'ordine. Quasi sempre il problema è nel come quel dato è stato strutturato a monte: due campi simili, etichette ambigue, formati diversi nello stesso JSON.

Il modello risponde in modo coerente ma in contraddizione con la knowledge base. Magari c'è un documento del 2023 che dice una cosa e uno del 2025 che la contraddice, e il sistema RAG li recupera entrambi senza priorità temporale. A questo punto il modello "sceglie" un po' a caso.

Il modello dà risposte che cambiano tra una sessione e l'altra anche se la domanda è la stessa. Qui di solito si tratta di chunking instabile o di embedding che pescano fonti diverse a parità di query.

Nessuno di questi problemi si risolve cambiando modello. Si risolve a monte, lavorando sui dati prima ancora che il modello li veda.

Il mito del "modello più potente risolve tutto"

Capita di sentire l'argomento opposto: con un modello abbastanza grande, il rumore nei dati viene assorbito. È vero in parte, e solo per task molto generalisti. Quando lavori su domini specifici, dove la risposta giusta dipende da come è strutturato un singolo campo del CRM, nessun modello compensa una pipeline di ingestion mal fatta.

Faccio un esempio concreto. Un cliente nel settore assicurativo ci ha chiesto un assistente che leggesse le polizze in PDF e rispondesse a domande dei clienti. Hanno provato per mesi con GPT-4, poi Claude, poi Gemini. Sempre risposte parzialmente sbagliate. Quando siamo arrivati noi, abbiamo guardato la pipeline: i PDF venivano estratti con una libreria Python che faceva merge dei caratteri di tabelle complesse, generando stringhe incomprensibili tipo "PremioAnnuo1.234,56€DurataAnni10". Nessun modello sa cosa farsene di questo input. Abbiamo cambiato la strategia di estrazione, separato le tabelle dal testo libero, normalizzato i numeri. Stesso modello, stesso prompt, accuratezza salita dal 60% al 94%.

Non è un caso isolato. È quasi sempre così.

Le quattro cause più frequenti di output sporco

Dopo aver fatto autopsie su parecchi sistemi rotti, riconosco quattro pattern ricorrenti.

1. Schema implicito anziché esplicito

I dati arrivano dal CRM o dal database del cliente con uno schema che "tutti sanno" ma che non è scritto da nessuna parte. Il campo "stato" ha cinque valori possibili in produzione, ma a volte ce n'è un sesto che è un residuo di una vecchia migrazione. Il campo "note_cliente" è un free text dove qualcuno ha messo il numero di telefono, qualcun altro la lingua preferita, qualcun altro ancora un commento del 2018 che non c'entra più nulla.

Quando dai questi dati al modello senza schema esplicito, il modello prova a inferire e sbaglia. La soluzione non è prompt engineering. È prima di tutto definire uno schema, validarlo all'ingresso, e rifiutare o quarantenare i record che non lo rispettano.

2. Contesto temporale assente

Quasi nessun cliente conserva la dimensione temporale dei propri dati nel modo giusto. Le knowledge base sono pieni di documenti che dicono cose diverse a distanza di anni, ma vengono indicizzati tutti insieme senza un criterio di anzianità. Il sistema RAG pesca i chunk per similarità semantica, e di colpo il modello vede contemporaneamente la procedura del 2021 e quella del 2025.

Una pipeline di ingestion seria deve tracciare almeno due cose: quando il documento è stato creato e quando è stato l'ultimo update. E poi il retriever deve poter usare questa informazione per pesare o filtrare. Senza questo, hai costruito un generatore di contraddizioni.

3. Chunking che spezza il significato

Il chunking è la pratica di dividere documenti lunghi in pezzi più piccoli per poterli indicizzare. Quasi sempre viene fatto male: si tagliano i documenti ogni N caratteri, magari rispettando i paragrafi, ma senza pensare alla semantica. Il risultato è che metà di una procedura finisce in un chunk, l'altra metà in un altro, e il retriever spesso ne pesca solo uno. Il modello vede metà istruzione e improvvisa il resto.

Il chunking dovrebbe rispettare la struttura logica del documento: sezioni, sottosezioni, blocchi che hanno senso da soli. E ogni chunk dovrebbe portarsi dietro un po' di metadata: titolo della sezione padre, contesto del documento di origine, eventuali riferimenti incrociati. Costa di più in fase di ingestion, ma cambia tutto in fase di retrieval.

4. Mescolanza di sorgenti senza identità

Un assistente per il customer support tipico legge da almeno tre posti: knowledge base interna, ticket storici, documentazione di prodotto. Se questi tre flussi finiscono nello stesso indice senza un campo "source_type", il modello non distingue una FAQ ufficiale da un commento informale di un agente in un ticket vecchio. La risposta diventa una via di mezzo tra le due cose, che spesso è peggio di entrambe.

Ogni record che entra in una pipeline AI dovrebbe avere almeno: source, source_type, autore (se esiste), timestamp, e un livello di affidabilità implicito o esplicito.

Come strutturiamo le pipeline di ingestion nei nostri progetti

Quando in Bajara partiamo con un progetto AI custom, di solito dedichiamo i primi due o tre sprint quasi interamente alla pipeline di ingestion. Il cliente all'inizio si lamenta che "non si vedono risultati", ma poi quando arriva il modello e funziona al primo colpo capisce perché. Provo a descrivere come la organizziamo.

Fase 1: audit della sorgente

Prima ancora di scrivere codice, guardiamo i dati così come sono. Estraiamo un campione random, anche solo 200 record, e li leggiamo a mano. Questa fase è noiosa ma indispensabile. Trovi sempre cose tipo:

  • Campi che non vengono più usati ma restano nello schema
  • Convenzioni di naming inconsistenti tra utenti diversi
  • Caratteri Unicode strani copiati da Word
  • Date in formati diversi nello stesso campo
  • Valori sentinella tipo "N/A", "n/a", "NA", "-", "0", "" usati a caso per significare "vuoto"

Nessuno strumento automatico cattura tutte queste cose. Bisogna leggere e prendere appunti. Da questo audit nasce la lista dei normalization step che la pipeline dovrà fare.

Fase 2: definizione dello schema target

Una volta visti i dati grezzi, definiamo lo schema che il modello vedrà. Non è lo schema della sorgente. È una sua versione pulita, normalizzata, con tipi precisi e valori enumerati dove ha senso. Lo scriviamo in TypeScript o in Pydantic, e lo committiamo. Questo schema diventa il contratto.

A questo punto si decide cosa fare dei record che non rispettano lo schema. Le opzioni in genere sono tre: scartarli, ripararli automaticamente, mandarli in una coda di review umana. Quale scegliere dipende dal dominio. Per un assistente di supporto clienti, tendiamo a scartare e loggare. Per dati medici o legali, tutto va in review umana, sempre.

Fase 3: pipeline di trasformazione

La pipeline vera e propria. Ogni step è una funzione pura che prende un record sporco e ne restituisce uno pulito, oppure lo scarta. Tipicamente ci sono passaggi per:

Normalizzare i tipi. Date in ISO 8601. Numeri con il punto decimale. Boolean veri boolean, non stringhe "yes"/"no". Telefoni nel formato E.164.

Risolvere i riferimenti. Se un record ha un foreign key, risolvilo all'ingresso e aggiungi i campi denormalizzati che il modello vedrà. Risparmi join, riduci ambiguità.

Sanitizzare il testo libero. Rimuovere caratteri di controllo, normalizzare gli spazi, gestire le entità HTML che si sono infilate. Mai HTML grezzo nei chunk.

Arricchire con metadata. Aggiungere source, timestamp, lingua, eventualmente entità nominate estratte. I metadata sono ciò che permetterà al retriever di filtrare in modo intelligente.

Validare contro lo schema. L'ultimo step è sempre una validazione finale che blocca tutto ciò che non rispetta il contratto.

Fase 4: chunking semantico

Per documenti lunghi, dopo la normalizzazione viene il chunking. Noi tendenzialmente seguiamo la struttura naturale del documento: heading di livello uno e due delimitano i chunk principali, e quando un chunk supera una certa dimensione (in genere 800-1200 token) lo dividiamo ulteriormente cercando un break naturale. Ogni chunk porta nel suo metadata il path completo dei suoi heading padre, così che il retriever possa restituirlo con il suo contesto gerarchico.

Fase 5: indicizzazione e versioning

L'indice vettoriale (Pinecone, Qdrant, pgvector, quello che usate) viene popolato dai chunk. Ma noi versioniamo l'intero processo: ogni run di ingestion ha un suo ID, e i record indicizzati portano questo ID. Quando aggiorniamo la pipeline, possiamo confrontare le due versioni dell'indice e capire cosa è cambiato. Questa cosa salva la vita quando un cliente dice "ieri rispondeva bene, oggi no".

Fase 6: monitoring continuo

Una pipeline non è "finita". Ogni settimana arrivano dati nuovi, e ogni settimana qualcosa si rompe. Loggate sempre: quanti record sono stati scartati, quanti normalizzati, quanti hanno richiesto review. Se il numero di scartati cresce, qualcosa è cambiato a monte (magari il cliente ha fatto una modifica al CRM senza dirvelo). Se cresce la review humana, lo schema target probabilmente va aggiornato.

RAG e dati strutturati: un equivoco frequente

Spesso si parla di RAG come se fosse la soluzione magica per portare dati custom dentro un LLM. È uno strumento utile, ma viene applicato anche dove non serve. Se i tuoi dati sono strutturati (un CRM, un database transazionale, un ERP), RAG è quasi sempre la scelta peggiore.

Per dati strutturati funziona molto meglio dare al modello strumenti per interrogarli direttamente: function calling che mappa su query SQL parametrizzate, oppure su API interne ben definite. Il modello formula la query, riceve i risultati strutturati, e li usa per rispondere. Niente embedding, niente ambiguità di retrieval, niente chunking. I dati restano sempre nella loro forma canonica, e il modello vede esattamente quello che vede uno sviluppatore quando interroga il database.

RAG ha senso quando i dati sono testuali e non strutturati: documentazione, knowledge base, ticket storici scritti in linguaggio naturale, contratti. In questi casi il retrieval semantico è insostituibile. Ma anche qui, la qualità della pipeline di ingestion che genera i chunk indicizzati determina il 90% del risultato finale.

Una buona regola pratica: per ogni progetto, mappare le sorgenti dati in due colonne, "strutturate" e "non strutturate". Le prime andranno via tool calling, le seconde via RAG. Mescolare i due approcci nello stesso flusso, con un orchestratore che decide quale usare in base alla domanda, è dove di solito si vince.

Validazione: non fidarti del modello, sempre

Un'altra cosa che ripeto spesso ai team con cui lavoriamo: il modello può sbagliare anche con dati perfetti. Quindi all'uscita serve sempre uno strato di validazione.

Se il modello deve restituire un JSON con uno schema specifico, validatelo con Zod, Pydantic, o l'equivalente del vostro stack. Se non passa, retry. Se restituisce un'entità del database (un ID utente, un numero d'ordine), verificate che esista davvero prima di usarla. Se cita una fonte, controllate che la citazione sia tra i chunk passati nel contesto, altrimenti scartatela.

Questo livello di validazione costa qualche centinaio di millisecondi e qualche linea di codice in più, ma è la differenza tra un sistema che ogni tanto sbaglia in produzione e uno che si auto-corregge prima di mostrare l'errore all'utente.

Caso pratico: un assistente CRM per il sales

Per rendere il discorso meno astratto, racconto un progetto recente. Cliente nel B2B, vendita di software gestionali per PMI. Volevano un assistente per il sales team che, dato il nome di un prospect, riassumesse tutta la storia di interazioni: chiamate, email, proposte fatte, opportunità aperte e chiuse.

Prima versione, fatta da loro con un consulente esterno: prendeva tutti i record dal CRM, li concatenava in un unico testo, lo passava al modello. Risultato: assistente che inventava dettagli, confondeva clienti diversi se avevano nomi simili, citava email che non esistevano.

Cosa abbiamo fatto noi:

Mappato i record del CRM in uno schema target esplicito (entità Prospect, Interaction, Opportunity, ognuna con i suoi campi tipizzati).

Definito un endpoint interno che, dato un prospect_id, restituisce la sua storia completa in JSON strutturato, già ordinata cronologicamente, con date in formato ISO e campi normalizzati.

Esposto questo endpoint al modello via function calling. Il modello, quando l'utente chiede di un prospect, chiama la funzione, riceve i dati strutturati, e li usa per generare il riassunto.

Aggiunto uno strato di validazione: ogni entità citata nel riassunto deve avere un ID corrispondente nei dati ricevuti. Se non ce l'ha, l'output viene rigenerato.

Stesso modello che usavano prima. Accuratezza passata da circa il 70% a oltre il 97%. Tempo di sviluppo della pipeline: tre settimane. Tempo speso a "tunare il prompt" prima di chiamarci: quattro mesi.

Il punto non è che siamo più bravi (anche se ci piace pensarlo). Il punto è che avevano scelto l'architettura sbagliata per il tipo di dato che avevano. RAG su dati strutturati non funziona bene, e nessun prompt lo sistema.

Cosa controllare prima di dare la colpa al modello

Se siete a un punto in cui il vostro sistema AI dà risposte deludenti, prima di valutare un cambio di modello vi suggerirei di passare in rassegna queste domande, in ordine.

I dati che arrivano al modello hanno uno schema esplicito e validato? Se rispondete "in genere sì, tranne quei tre campi liberi", la risposta è no. Quei tre campi sono probabilmente la fonte di gran parte degli errori.

Per ogni record, c'è un timestamp e una source chiaramente identificabili? Se il modello vede contemporaneamente dati di anni diversi senza priorità, le contraddizioni sono inevitabili.

Il chunking dei vostri documenti rispetta la struttura semantica? Se state ancora dividendo ogni 500 caratteri, c'è margine enorme di miglioramento.

State usando RAG su dati che dovrebbero passare via function calling? Per qualunque dato che vive in un database relazionale, la risposta è quasi sempre sì.

C'è uno strato di validazione che controlla l'output del modello prima di mostrarlo all'utente? Se no, state delegando al modello la responsabilità di non sbagliare, ed è una responsabilità che il modello non può portare.

Quante percentuali di errore della vostra pipeline di ingestion vi state portando appresso senza monitorare? Se non avete un dashboard che vi dice quanti record vengono scartati ogni giorno, state navigando alla cieca.

In genere, dopo aver risposto a queste sei domande con onestà, è chiaro dove intervenire. E quasi mai la risposta è "cambiamo modello".

Il messaggio per chi sta partendo adesso

Se state iniziando un progetto AI custom in questo periodo (qui in Bajara stiamo accompagnando parecchie aziende italiane in questa fase, principalmente su CRM, supporto clienti e document automation) il consiglio che mi sento di dare è uno solo: spendete più tempo sui dati di quanto vi sembri ragionevole. Almeno il doppio.

I demo che girano in due giorni con un prompt fortunato sono ingannevoli. Quello che funziona in produzione, sotto carico, con dati reali sporchi, è quasi sempre un sistema costruito attorno a una pipeline di ingestion solida e a contratti chiari tra i componenti. Il modello è importante, ma è il pezzo più sostituibile. Le pipeline, una volta fatte bene, restano. E se domani esce un modello migliore, basta cambiare l'API client per beneficiarne.

Chi lavora bene con l'AI non è chi ha il modello più potente. È chi ha i dati più puliti e la disciplina per tenerli tali nel tempo. Tutto il resto è dettaglio.