Valutare un RAG: Metriche, Dataset di Eval e LLM-as-a-Judge
Ogni volta che si mette in piedi un sistema basato su LLM — un assistente interno, un chatbot sui documenti aziendali, un RAG sul catalogo prodotti — arriva prima o poi la domanda: «funziona?». La risposta tipica è una demo con cinque domande scelte a mano, a cui il sistema risponde bene. È una risposta rassicurante e quasi inutile.
Il problema non è la buona fede di chi la fa: è che una demo non è una misura. Se le domande le hai scelte tu, se le hai scelte perché sapevi che il sistema le gestiva, se le hai scelte dopo aver visto gli output, non stai valutando il sistema: stai raccontando una storia. Questo articolo prova a mettere ordine su come si valuta un RAG in modo ripetibile, con strumenti semplici e senza trasformare la valutazione in un progetto infinito.
Perché «a occhio sembra funzionare» non è una valutazione
Un sistema RAG ha due parti che possono fallire in modo indipendente: il retrieval (trova i documenti giusti?) e la generazione (scrive una risposta corretta e fedele a quei documenti?). Un output sbagliato può dipendere da entrambe, e senza separarle non sai cosa stai aggiustando.
C'è poi il problema della ripetibilità. Gli LLM non sono deterministici: la stessa domanda può dare risposte diverse. Se non fissi un insieme di casi e non salvi i risultati, non puoi dire se una modifica ha migliorato o peggiorato il sistema. Puoi solo dire che ti è sembrato meglio, che è esattamente il tipo di giudizio che vorresti evitare quando il sistema tocca dati veri.
Costruire un dataset di eval: piccolo, ma rappresentativo
Non servono migliaia di esempi. Un insieme di 50-100 casi ben scelti è già sufficiente per accorgersi dei problemi grossi — sempre che i casi siano rappresentativi e non tutti facili. L'idea è avere un golden set: una raccolta stabile di domande, con il comportamento atteso, che usi come riferimento nel tempo.
Da dove prendere i casi
- Dai log reali, se il sistema è già in uso: le domande che le persone fanno davvero sono la fonte più onesta. Anonimizza e riscrivi dove serve.
- Dai documenti: per ogni sezione importante della knowledge base, scrivi una domanda a cui quella sezione dovrebbe rispondere.
- Dai casi limite: domande ambigue, domande con refusi, domande che mescolano due argomenti.
- Dai casi fuori dominio: domande a cui il sistema non deve rispondere. Sono i più trascurati e i più utili: un RAG che risponde a tutto è peggio di uno che ammette di non sapere.
Cosa mettere in ogni riga
Una tabella semplice basta e avanza. Un esempio di struttura:
| id | domanda | tipo | documento atteso | risposta attesa / criteri |
|---|---|---|---|---|
| 001 | «Come si richiede una sostituzione?» | fattuale | resi.md#sostituzioni | deve citare la procedura e i tempi |
| 002 | «Quanto costa il modello X?» | fuori dominio | nessuno | deve dichiarare di non avere l'informazione |
| 003 | «Problema con ordine e fattura insieme» | multi-argomento | 2 documenti | deve coprire entrambi i punti |
Due regole pratiche:
- Il golden set non si tocca per far salire il punteggio. Se lo modifichi ogni volta che una modifica peggiora i risultati, stai solo spostando il bersaglio.
- Versiona il dataset insieme al codice, così sai a quale versione corrisponde un certo punteggio.
Metriche per il retrieval
Prima di guardare le risposte, guarda se il sistema recupera i documenti giusti. Se il documento corretto non entra nel contesto, nessun prompt salverà la generazione.
Tre metriche semplici e sufficienti nella maggior parte dei casi:
- Hit rate@k (o recall@k): la frazione di domande per cui almeno un documento corretto compare nei primi k risultati.
- MRR (Mean Reciprocal Rank): la media di 1/rank del primo documento corretto. Premia i documenti giusti che arrivano in alto.
- Precision@k: quanti dei k documenti recuperati sono effettivamente utili. Utile per accorgersi se stai riempiendo il contesto di rumore.
In formule, in forma testuale:
hit_rate@k = casi con almeno un documento corretto nei primi k / casi totali
MRR = media di (1 / posizione del primo documento corretto)
Un frammento di codice per calcolarle, assumendo che ogni caso abbia un insieme di id attesi e una lista di id recuperati:
def hit_rate_at_k(cases, k):
ok = 0
for c in cases:
retrieved = c["retrieved"][:k]
if any(doc in c["expected"] for doc in retrieved):
ok += 1
return ok / len(cases)
def mrr(cases):
total = 0.0
for c in cases:
rank = None
for i, doc in enumerate(c["retrieved"], start=1):
if doc in c["expected"]:
rank = i
break
total += 1 / rank if rank else 0.0
return total / len(cases)
Sui numeri: se su 60 casi il tuo hit rate@5 è, per esempio, 0,72, il primo dato interessante non è il valore in sé ma la lista dei 17 casi mancati. Guardarli uno per uno è spesso più utile di qualsiasi media.
Metriche per la generazione
Qui non esiste un numero unico. Le metriche classiche di confronto tra testi — exact match, similarità a livello di token — funzionano male su risposte discorsive in italiano: due frasi corrette possono non condividere quasi nessuna parola.
Quello che conta davvero si riduce a poche dimensioni:
- Fedeltà (faithfulness): la risposta è supportata dai documenti recuperati, o inventa?
- Pertinenza: risponde alla domanda posta, o va fuori tema?
- Correttezza: rispetto a una risposta attesa o a criteri espliciti, il contenuto è giusto?
- Completezza: manca qualche punto richiesto?
- Forma: cita le fonti? usa il tono giusto? rispetta i limiti di lunghezza?
Per i casi fuori dominio la metrica è binaria: ha rifiutato o no. È il tipo di metrica più facile da calcolare e più facile da ignorare, ed è un errore.
LLM-as-a-Judge: come usarlo senza autoinganni
Valutare a mano 60 risposte è fattibile una volta. Farlo a ogni modifica del prompt, del chunking o del modello, no. Da qui l'idea di far valutare le risposte a un LLM più potente, con una rubrica esplicita.
Funziona, ma ha distorsioni note e va usato con attenzione:
- Bias di posizione: in un confronto a coppie, tende a preferire la prima risposta. Randomizza l'ordine.
- Bias di verbosità: tende a preferire risposte lunghe. Esplicitalo nella rubrica come penalità.
- Auto-preferenza: un modello giudica meglio i testi prodotti da modelli simili a sé. Se puoi, usa un giudice diverso dal generatore.
- Instabilità: due esecuzioni possono dare giudizi diversi. Ripeti e guarda la varianza, oppure usa una scala ridotta.
Una rubrica secca, con pochi livelli, è meglio di una scala 1-10 dove i gradi intermedi non significano nulla. Esempio di prompt di valutazione, da adattare:
JUDGE_PROMPT = """Sei un valutatore. Ricevi una DOMANDA, dei DOCUMENTI
recuperati e una RISPOSTA. Valuta la RISPOSTA su due assi, con un
intero da 1 a 3 (1 = insufficiente, 2 = parziale, 3 = buono):
- FEDELTA: ogni affermazione e' supportata dai DOCUMENTI?
- PERTINENZA: la RISPOSTA risponde alla DOMANDA?
Penalizza le risposte inutilmente lunghe. Se la domanda e' fuori
dominio, il punteggio massimo si ottiene solo se la RISPOSTA dichiara
esplicitamente di non avere l'informazione.
Rispondi solo in JSON: {{"fedelta": int, "pertinenza": int,
"motivo": "una frase"}}
DOMANDA: {question}
DOCUMENTI: {context}
RISPOSTA: {answer}
"""
Due contromisure che valgono più di qualsiasi prompt furbo: verifica un campione a mano (20 risposte sono sufficienti per capire se il giudice è allineato al tuo giudizio) e non usare lo stesso modello per generare e giudicare, quando puoi evitarlo.
Un flusso di lavoro ripetibile
La valutazione diventa utile quando è noiosa: stessa procedura, ogni volta. Una struttura minima potrebbe essere questa:
eval/
dataset/golden_v1.jsonl
runs/2025-06-01_baseline/
runs/2025-06-03_chunk400_overlap80/
judge.py
run_eval.py
E il ciclo è sempre lo stesso:
- Fissa il golden set e il codice di valutazione.
- Esegui il sistema su tutti i casi e salva output, documenti recuperati e metriche.
- Guarda il dettaglio dei casi peggiori, non solo la media.
- Cambia una cosa alla volta (chunking, k, prompt, modello).
- Riesegui e confronta.
Il confronto tra due configurazioni è la parte che dà più informazioni. Una tabella di esempio, con numeri inventati a scopo illustrativo:
| Configurazione | hit@5 | fedeltà media | rifiuti corretti | latenza p95 |
|---|---|---|---|---|
| baseline, chunk 800 | 0,72 | 2,4 | 9/15 | 1,1 s |
| chunk 400 + overlap 80 | 0,81 | 2,6 | 13/15 | 1,4 s |
Nota una cosa: il secondo sistema è migliore su tutte le metriche di qualità ma più lento. La decisione non è tecnica, è di prodotto: dipende da quanto pesa quel mezzo secondo in più per chi lo usa.
Errori comuni
- Valutare solo i casi facili. Se tutti i punteggi sono alti, il dataset è troppo semplice.
- Cambiare il dataset per far salire il numero. È il modo più rapido per non imparare nulla.
- Guardare solo la media. La distribuzione dei casi peggiori contiene quasi sempre tutte le informazioni utili.
- Confondere retrieval e generazione. Un recall basso non si risolve con un prompt migliore.
- Dimenticare costo e latenza. Un sistema più preciso ma tre volte più lento può essere un peggioramento.
- Dimenticare i casi fuori dominio. Sono quelli che, in produzione, fanno più danni.
Conclusione
Valutare un RAG non richiede un framework complicato. Richiede tre cose: un golden set stabile e rappresentativo, metriche separate per retrieval e generazione, e un flusso ripetibile che salvi i risultati per confrontarli nel tempo.
I punti chiave:
- Una demo con cinque domande non è una valutazione: serve un insieme di casi fissato prima, non scelto dopo.
- Includi sempre i casi fuori dominio: un sistema che ammette di non sapere è più affidabile di uno che risponde a tutto.
- Misura prima il retrieval: se i documenti giusti non entrano nel contesto, la generazione non può salvarli.
- L'LLM-as-a-Judge è utile per scalare, ma va controllato con un campione valutato a mano e usato con una rubrica breve.
- Guarda i casi peggiori, non solo la media: è lì che stanno le prossime modifiche da fare.
Foto di copertina di James Harrison su Unsplash.