«Funziona» è un'impressione, non una misura
Un sistema RAG provato su venti domande, con risposte che sembrano buone, ti dà un'impressione e nessun numero. L'impressione regge finché il corpus è piccolo e le domande sono quelle che hai pensato tu. Poi arrivano documenti che si somigliano, utenti che non usano le tue parole e un responsabile che chiede quanto è affidabile: a quel punto «funziona» non risponde più a niente. Per rispondere servono tre cose, e sono le stesse in ogni progetto.
- un insieme di domande scelte con un criterio, non a caso e non solo le più facili;
- per ciascuna, la risposta o il documento giusto, deciso da una persona prima di guardare l'uscita del sistema;
- un punteggio riportato con il suo intervallo di incertezza, non con tre decimali che nessuno saprebbe giustificare.
Le voci sono in ordine di costo: la prima si fa in un pomeriggio, la seconda in settimane di lavoro di chi conosce il dominio, la terza in un minuto di calcolo ma richiede di accettare che 0,85 su cento domande non è 0,85. Questo articolo apre la serie e costruisce il terreno comune: i modi in cui un RAG si rompe senza dare errori, le quattro metriche che li misurano, due controlli che non richiedono alcun modello e la matematica che dice quante domande servono. I passi successivi sono gli articoli su Ragas, su come costruire il golden dataset e sulle metriche del solo retriever.
Una precisazione sul metodo. I listati qui sotto usano un corpus inventato di dieci frasi su un prodotto software immaginario e un recupero lessicale TF-IDF con scikit-learn: nessun modello viene chiamato e nessun database vettoriale viene avviato. È un modello didattico, che mostra i meccanismi con numeri riproducibili; non misura nessun sistema reale, e le percentuali stampate non vanno lette come stime di quanto un RAG di produzione sia affidabile.
Quattro modi in cui un RAG sbaglia senza avvisare
Un RAG sbagliato non lancia eccezioni: restituisce un testo ben scritto. Per questo conviene classificare i guasti prima di scegliere le metriche, perché ogni metrica vede un guasto e ne ignora altri.
Il retriever non trova il documento. La domanda usa parole diverse da quelle del testo: «posso farmi restituire i soldi» contro «il rimborso è possibile entro 14 giorni». Un recupero lessicale non ha nessuna parola in comune da sfruttare. Un recupero per embedding può cavarsela, ma anche lui sbaglia quando il dominio è specialistico. Il generatore, senza il passo giusto, risponde comunque.
Il contesto è sporco. Con un top-k alto entrano chunk vicini per argomento ma inutili. Il modello deve ignorarli e non sempre ci riesce. Liu e colleghi hanno misurato che la qualità delle risposte cambia con la posizione dell'informazione: spesso è massima quando sta all'inizio o alla fine del contesto e cala quando sta nel mezzo, anche nei modelli pensati per contesti lunghi. Non è una ragione per tagliare il contesto a due chunk, è la ragione per misurare l'ordine in cui il retriever consegna i risultati.
L'allucinazione credibile. Il contesto è corretto ma incompleto, e il generatore completa a modo suo: dal chunk con il prezzo ricava anche un numero di utenti che nessun documento riporta. È il guasto più pericoloso perché supera la rilettura veloce di una persona. Un'indagine preregistrata su strumenti commerciali di ricerca legale basati su RAG ha trovato che gli strumenti di LexisNexis e Thomson Reuters allucinavano tra il 17% e il 33% delle volte: meno dei chatbot generici, quindi, ma ben lontano dallo zero promesso dai fornitori. Il dato riguarda quei prodotti chiusi e quel dominio, non il tuo sistema; serve a ricordare che «c'è un retriever» non equivale a «non inventa».
Il corpus invecchia o si duplica. Due versioni dello stesso documento convivono nell'indice, con numeri diversi, e il retriever non ha nessun motivo per preferire quella aggiornata. Il listato seguente isola questo meccanismo. Parte da dieci documenti e dieci domande, ciascuna con un solo documento giusto, e aggiunge per ogni documento k copie in cui le cifre sono cambiate. Il vettorizzatore tiene conto solo delle parole (token_pattern scarta i numeri), quindi una copia ha lo stesso punteggio dell'originale; l'ordine di inserimento viene mescolato con un seme fisso, così nessuna copia è favorita dalla posizione.
# b1.py - il retriever "funziona" sulla demo e perde terreno quando il corpus si riempie di quasi-duplicati
import random
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
BASE = {
"prezzi": "Il piano Pro ha un prezzo mensile di 49 euro e include 10 utenti.",
"rimborso": "Il rimborso e possibile entro 14 giorni dall'acquisto, con richiesta al supporto.",
"export": "L'esportazione dei dati produce un file CSV dalla pagina Impostazioni.",
"sso": "L'accesso con SSO e disponibile solo nel piano Enterprise tramite SAML.",
"backup": "I backup automatici girano ogni notte e si conservano per 30 giorni.",
"api": "Le API accettano al massimo 100 richieste al minuto per ogni chiave.",
"fatture": "Le fatture si scaricano in PDF dalla sezione Fatturazione dell'account.",
"password": "Per reimpostare la password usa il link inviato alla tua email di registrazione.",
"integraz": "Le integrazioni con Slack e Teams si attivano dal pannello Integrazioni.",
"limiti": "Ogni progetto accetta fino a 500 documenti caricati in formato PDF.",
}
# (domanda, documento che contiene la risposta)
QUERY = [
("quanto costa al mese il piano Pro", "prezzi"),
("posso farmi restituire i soldi", "rimborso"), # nessuna parola in comune: vocabolario diverso
("come scarico i miei dati in CSV", "export"),
("serve SAML per accedere", "sso"),
("ogni quanto si fanno i backup", "backup"),
("quante richieste al minuto accetta l'API", "api"),
("dove scarico le fatture in PDF", "fatture"),
("ho dimenticato la password", "password"),
("come collego Slack", "integraz"),
("quanti documenti posso caricare per progetto", "limiti"),
]
def con_quasi_duplicati(k: int, seme: int = 7) -> dict:
"""Aggiunge k varianti per documento: stesso testo, numeri cambiati (versioni vecchie)."""
rng = random.Random(seme)
corpus = dict(BASE)
for nome, testo in BASE.items():
for j in range(k):
variante = "".join(str(rng.randint(1, 99)) if c.isdigit() else c for c in testo)
corpus[f"{nome}#v{j}"] = variante
ordine = list(corpus)
rng.shuffle(ordine) # l'ordine di inserimento non deve favorire l'originale
return {n: corpus[n] for n in ordine}
def valuta(corpus: dict) -> tuple[float, float]:
ids = list(corpus)
vec = TfidfVectorizer(sublinear_tf=True, token_pattern=r"(?u)\b[^\W\d_]{2,}\b") # solo parole, i numeri non contano
M = vec.fit_transform(corpus.values())
hit1 = hit3 = 0
for domanda, giusto in QUERY:
punteggi = (M @ vec.transform([domanda]).T).toarray().ravel()
ordine = [i for i in np.argsort(-punteggi, kind="stable") if punteggi[i] > 0][:3] # punteggio 0 = nessun risultato
top = [ids[i] for i in ordine]
hit1 += bool(top) and top[0] == giusto
hit3 += giusto in top
return hit1 / len(QUERY), hit3 / len(QUERY)
for k in (0, 2, 10, 50):
c = con_quasi_duplicati(k)
h1, h3 = valuta(c)
print(f"documenti={len(c):4d} hit@1={h1:.0%} hit@3={h3:.0%}")
# Output:
# documenti= 10 hit@1=90% hit@3=90%
# documenti= 30 hit@1=10% hit@3=90%
# documenti= 110 hit@1=10% hit@3=20%
# documenti= 510 hit@1=0% hit@3=0%
Con dieci documenti il recupero sbaglia una domanda su dieci, e non per caso: è «posso farmi restituire i soldi», che non condivide nemmeno una parola con la frase sul rimborso, per cui hit@3 resta al 90% anche dopo. Il resto è un'altra cosa: il documento giusto ha lo stesso punteggio delle sue copie, e l'ordine tra pari merito decide chi sale. Con 30 documenti hit@1 scende al 10% (il giusto è fra i primi tre, raramente primo), con 110 anche hit@3 cala al 20%, e con 510 l'originale non compare mai nei primi tre posti. Nessuna soglia universale di dimensione del corpus compare qui: dipende da quanto i documenti si somigliano, ed è proprio per questo che va misurata sul tuo corpus. La contromisura non è un modello migliore, è un metadato di versione e un filtro prima del recupero.
Due controlli che non richiedono un modello
Prima di pagare un giudice per ogni risposta, conviene tirare fuori ciò che si può calcolare con una riga. Il recupero si controlla con gli identificativi: se per ogni domanda sai quali documenti contengono la risposta, recupero_ok verifica che almeno uno compaia nei primi k. La fedeltà ha un controllo grezzo ma utile per un caso preciso: i numeri. Se la risposta afferma «25 utenti» e nel contesto non c'è 25, qualcosa è stato inventato. numeri_senza_appoggio estrae le sequenze di cifre dalla risposta e toglie quelle presenti nel contesto.
# b3.py - un cruscotto minimo senza modello: due controlli che costano zero e prendono errori veri
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class Caso:
id: str
domanda: str
id_attesi: frozenset # documenti che contengono la risposta (etichetta umana)
id_recuperati: tuple # cosa ha restituito il retriever, in ordine
contesto: str # testo dei chunk passati al generatore
risposta: str # cosa ha detto il generatore
NUMERO = re.compile(r"\d+(?:[.,]\d+)?")
def recupero_ok(c: Caso, k: int = 3) -> bool:
"""Controllo 1: almeno un documento atteso e tra i primi k."""
return bool(c.id_attesi & set(c.id_recuperati[:k]))
def numeri_senza_appoggio(c: Caso) -> list:
"""Controllo 2: numeri che la risposta afferma e il contesto non contiene."""
visti = set(NUMERO.findall(c.contesto))
return sorted(set(NUMERO.findall(c.risposta)) - visti)
CASI = [
Caso("q1", "Quanto costa il piano Pro?", frozenset({"prezzi"}), ("prezzi", "api", "sso"),
"Il piano Pro ha un prezzo mensile di 49 euro.",
"Il piano Pro costa 49 euro al mese."),
Caso("q2", "Quanti utenti include il piano Pro?", frozenset({"utenti"}), ("prezzi", "api", "sso"),
"Il piano Pro ha un prezzo mensile di 49 euro.",
"Il piano Pro costa 49 euro al mese e include fino a 25 utenti."),
Caso("q3", "Quanti utenti include il piano Pro?", frozenset({"utenti"}), ("prezzi", "api", "sso"),
"Il piano Pro ha un prezzo mensile di 49 euro.",
"Il contesto non dice quanti utenti include il piano."),
]
for c in CASI:
print(f"{c.id}: recupero_ok={recupero_ok(c)} numeri_senza_appoggio={numeri_senza_appoggio(c)}")
# Output:
# q1: recupero_ok=True numeri_senza_appoggio=[]
# q2: recupero_ok=False numeri_senza_appoggio=['25']
# q3: recupero_ok=False numeri_senza_appoggio=[]
Il caso q2 è l'allucinazione credibile del paragrafo precedente: il retriever ha sbagliato (recupero falso) e il generatore ha riempito il vuoto con un 25 che il contesto non contiene, quindi il controllo lo segnala. Il caso q3 è più istruttivo: il recupero è fallito allo stesso modo, ma la risposta dichiara di non avere l'informazione, e nessun numero è inventato. I due controlli sono indipendenti e ti dicono due cose diverse: il sistema q3 è onesto e inutile, il sistema q2 è disonesto e inutile. Un punteggio unico li renderebbe indistinguibili.
Il limite è evidente: il controllo vede solo i numeri. Una data scritta a parole, un nome sbagliato o una negazione sfuggono. Per quelle serve un giudice che scomponga la risposta in affermazioni e le confronti con il contesto, che è ciò che fanno le metriche descritte qui sotto. Non cercare di estendere il controllo con espressioni regolari: ogni eccezione che aggiungi è un falso allarme in più.
Le quattro metriche e il guasto che ciascuna vede
Ragas, descritto nell'articolo di Es e colleghi, propone metriche che valutano le dimensioni di un RAG separatamente, e la sua documentazione ne dà le definizioni operative. Ciascuna ha un input minimo diverso, ed è questa la differenza che conta quando devi decidere quanto lavoro di annotazione richiedere.
| Metrica | Che cosa guarda | Che cosa ti serve | Guasto che segnala |
|---|---|---|---|
| context precision | i chunk rilevanti stanno in alto nella lista | domanda, contesti recuperati, risposta di riferimento | contesto sporco |
| context recall | il riferimento è coperto dai contesti recuperati | domanda, contesti recuperati, risposta di riferimento | documento non trovato |
| faithfulness | le affermazioni della risposta sono supportate dal contesto | domanda, risposta, contesti recuperati | allucinazione credibile |
| answer relevancy | la risposta tratta ciò che è stato chiesto | domanda, risposta | risposta fuori tema o evasiva |
Leggi la tabella per colonna «che cosa ti serve», non per nome. Precision e recall, nelle versioni che le chiedono (la libreria contiene anche una variante della precision senza riferimento, ContextPrecisionWithoutReference), usano una risposta di riferimento: costano lavoro umano. Faithfulness e relevancy no: bastano l'uscita del sistema e un giudice. Per questo si parte da loro quando non hai ancora un golden dataset, e per questo l'articolo su Ragas dedica spazio alla generazione di dati sintetici. Nota anche ciò che la tabella non dice: nessuna delle quattro controlla che la risposta sia vera. Faithfulness vale 1 anche per una risposta sbagliata, se il contesto recuperato era sbagliato nello stesso modo.
Le definizioni, dalla documentazione di Ragas. La context precision è la media delle precisioni a ogni posizione occupata da un chunk rilevante, divisa per il numero di chunk rilevanti: un chunk giusto in prima posizione pesa di più di uno giusto in ottava. La faithfulness è il rapporto fra le affermazioni della risposta supportate dal contesto e le affermazioni totali; un punteggio di 0,75 significa che un quarto delle affermazioni non trova appoggio. La answer relevancy fa un giro più strano: chiede al modello di generare alcune domande (tre per impostazione predefinita) a partire dalla risposta, calcola la similarità coseno di ciascuna con la domanda originale e ne fa la media. Una risposta evasiva produce domande generiche, lontane dall'originale. La documentazione precisa che il valore sta di norma fra 0 e 1 ma, per via del coseno, non è garantito.
Il listato che segue mostra le quattro chiamate nell'interfaccia attuale, ragas.metrics.collections. Non è stato eseguito: richiede un client verso un modello giudice e un modello di embedding, e qui nessun modello viene chiamato. Le firme sono state lette con inspect.signature sulla versione 0.4.3 installata in una cartella temporanea. Controlla la versione che usi: l'interfaccia è cambiata fra le versioni (le importazioni da ragas.metrics emettono un avviso di deprecazione che rimanda a ragas.metrics.collections), e un esempio copiato da un articolo del 2024 può non girare più.
# b4.py - schema da adattare (non eseguito): servono un client verso un modello giudice e una chiave tua
# Firme verificate con inspect.signature su ragas 0.4.3; nessuna metrica e stata calcolata.
import asyncio
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory
from ragas.metrics.collections import AnswerRelevancy, ContextPrecision, ContextRecall, Faithfulness
llm = llm_factory("<nome-del-modello-giudice>", client=CLIENT_ASINCRONO_DEL_TUO_PROVIDER)
emb = embedding_factory("openai", model="<nome-embedding>", client=CLIENT_ASINCRONO_DEL_TUO_PROVIDER)
domanda = "Quanti utenti include il piano Pro?"
contesti = ["Il piano Pro ha un prezzo mensile di 49 euro e include 10 utenti."]
risposta = "Il piano Pro include 10 utenti."
riferimento = "Il piano Pro include 10 utenti."
async def main():
fedelta = await Faithfulness(llm=llm).ascore(
user_input=domanda, response=risposta, retrieved_contexts=contesti) # niente riferimento
pertinenza = await AnswerRelevancy(llm=llm, embeddings=emb).ascore(
user_input=domanda, response=risposta) # niente contesti
precisione = await ContextPrecision(llm=llm).ascore(
user_input=domanda, reference=riferimento, retrieved_contexts=contesti) # serve il riferimento
richiamo = await ContextRecall(llm=llm).ascore(
user_input=domanda, retrieved_contexts=contesti, reference=riferimento) # serve il riferimento
for nome, r in (("faithfulness", fedelta), ("answer_relevancy", pertinenza),
("context_precision", precisione), ("context_recall", richiamo)):
print(nome, r.value)
asyncio.run(main())
Guarda gli argomenti, non i nomi dei modelli: Faithfulness non accetta reference, AnswerRelevancy non accetta i contesti, mentre ContextPrecision e ContextRecall li richiedono entrambi. Se il tuo dataset non ha un riferimento, le ultime due non sono calcolabili e non vanno sostituite con un riferimento inventato. Ogni chiamata invia al modello giudice prompt che contengono le tue domande e i tuoi documenti: scegli il modello sapendo dove finiscono quei dati, e confronta i costi sul tuo volume reale, perché per un numero di chiamate per campione fisso il costo cresce linearmente con il dataset.
Quanti esempi servono e quanto è largo l'intervallo
«Ho testato 100 domande e la fedeltà è 0,85» è una frase con una parte omessa: l'incertezza. Se l'esito di ogni domanda è sì o no (il documento giusto è nei primi tre, la risposta è supportata), il punteggio è una proporzione, e per una proporzione esistono due formule che si usano a coppie. La prima dice quante domande servono per ottenere un errore massimo e con una certa confidenza: n = z² · p(1−p) / e², con p = 0,5 nel caso peggiore perché massimizza n. La seconda, l'intervallo di Wilson, dice quanto è largo l'intervallo che hai di fatto ottenuto. Nel listato z viene calcolato dalla distribuzione normale della libreria standard, senza tabelle copiate.
# b2.py - quante domande servono, e quanto e largo l'intervallo di un punteggio
import math
from statistics import NormalDist
def n_minimo(errore: float, confidenza: float = 0.95, p: float = 0.5) -> int:
"""n = z^2 * p * (1-p) / e^2, con p=0.5 come caso peggiore."""
z = NormalDist().inv_cdf(1 - (1 - confidenza) / 2) # z bilaterale
return math.ceil(z * z * p * (1 - p) / (errore * errore))
def wilson(successi: int, n: int, confidenza: float = 0.95) -> tuple[float, float]:
"""Intervallo di Wilson per una proporzione: regge meglio di 'p +- z*sqrt(p(1-p)/n)' ai bordi."""
z = NormalDist().inv_cdf(1 - (1 - confidenza) / 2)
p = successi / n
centro = (p + z * z / (2 * n)) / (1 + z * z / n)
meta = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / (1 + z * z / n)
return centro - meta, centro + meta
print("errore n@90% n@95% n@99%")
for e in (0.10, 0.07, 0.05, 0.03):
print(f"±{e:.0%} {n_minimo(e, .90):6d} {n_minimo(e, .95):6d} {n_minimo(e, .99):6d}")
for successi, n in ((17, 20), (24, 30), (85, 100), (170, 200), (340, 400)):
lo, hi = wilson(successi, n)
print(f"{successi}/{n} = {successi/n:.2f} -> intervallo 95%: [{lo:.2f}, {hi:.2f}] (larghezza {hi-lo:.2f})")
# Output:
# errore n@90% n@95% n@99%
# ±10% 68 97 166
# ±7% 139 196 339
# ±5% 271 385 664
# ±3% 752 1068 1844
# 17/20 = 0.85 -> intervallo 95%: [0.64, 0.95] (larghezza 0.31)
# 24/30 = 0.80 -> intervallo 95%: [0.63, 0.90] (larghezza 0.28)
# 85/100 = 0.85 -> intervallo 95%: [0.77, 0.91] (larghezza 0.14)
# 170/200 = 0.85 -> intervallo 95%: [0.79, 0.89] (larghezza 0.10)
# 340/400 = 0.85 -> intervallo 95%: [0.81, 0.88] (larghezza 0.07)
La prima tabella conferma l'ordine di grandezza di una regola pratica: per ±7% al 95% servono 196 domande, per ±5% ne servono 385. Non scendere sotto il centinaio per ragionare su un confronto: con 100 domande 0,85 diventa un intervallo da 0,77 a 0,91, largo 14 punti. Con 20 domande l'intervallo va da 0,64 a 0,95, e con 30 domande a 0,80 (24 su 30) da 0,63 a 0,90: troppo largo per distinguere due versioni del sistema che differiscono di cinque punti. Raddoppiare le domande da 100 a 200 stringe l'intervallo da 14 a 10 punti, non lo dimezza: la larghezza cala con la radice di n.
Due avvertenze che spesso mancano. Le formule valgono per esiti binari e domande indipendenti: se le domande vengono dallo stesso documento in dieci varianti, il campione efficace è più piccolo di quanto dica il conteggio. E per un punteggio continuo, come una faithfulness media, la formula usa la deviazione standard dei punteggi al posto di p(1−p), che devi stimare da una prima esecuzione. In ogni caso decidi la dimensione del campione prima di guardare i risultati; scegliere l'insieme di domande dopo aver visto dove il sistema va bene è il modo più rapido per ottenere un numero che non predice nulla.
Il giudice è un modello e ha i suoi difetti
Faithfulness e relevancy dipendono da un modello che giudica. Zheng e colleghi, nell'articolo che ha introdotto MT-Bench e Chatbot Arena, hanno studiato proprio questo uso e hanno descritto tre distorsioni: il giudice tende a preferire la risposta presentata per prima (position bias), quella più lunga (verbosity bias) e quella prodotta da modelli simili a lui (self-enhancement bias). Gli stessi autori hanno trovato che un giudice forte concorda con le preferenze umane oltre l'80% delle volte, lo stesso livello di accordo fra persone: lo stesso studio che denuncia i difetti ne giustifica l'uso, a patto di conoscerli.
Da qui discendono tre abitudini. Non cambiare il giudice fra due esecuzioni che vuoi confrontare, perché una differenza di punteggio potrebbe essere una differenza di giudice. Usa un modello diverso da quello che genera le risposte quando puoi. Etichetta a mano un sottoinsieme e misura l'accordo: con 30 casi, come mostra il listato precedente, l'intervallo sull'accordo è largo 28 punti, quindi la verifica serve a scoprire un giudice grossolanamente sbagliato, non a certificare uno buono. Il punteggio assoluto del giudice è poco trasferibile; il confronto fra due versioni dello stesso sistema, con lo stesso giudice e le stesse domande, lo è molto di più.
Che cosa sai fare adesso
Sai distinguere i quattro guasti di un RAG e associare a ciascuno la metrica che lo vede: contesto sporco con la context precision, documento mancante con la context recall, invenzione con la faithfulness, risposta fuori tema con la answer relevancy. Sai che due di quelle metriche richiedono una risposta di riferimento e due no, e quindi da dove partire senza un golden dataset. Sai calcolare quante domande servono per l'errore che accetti e quanto è largo l'intervallo che hai ottenuto. E hai due controlli a costo zero, sul recupero e sui numeri, da mettere nel tuo codice questa settimana. Il passo successivo è lo strumento che calcola le metriche con un giudice, nell'articolo su Ragas, e poi il modo di portarle in un cancello di integrazione continua, in quello su DeepEval e Ragas; per la parte di monitoraggio in produzione, che qui non è affrontata, c'è l'articolo RAG in produzione: monitoring, evaluation, optimization, e per una versione compatta di un cancello di regressione c'è RAG Evaluation Pipeline: golden dataset.
Nella stessa serie trovi anche il monitoraggio in produzione.
Domande frequenti
Quante domande servono per partire con la valutazione di un RAG?
Con una cinquantina di domande scegli fra approcci molto diversi, non fra due versioni simili. Per ±7% di errore al 95% di confidenza, su esiti sì/no, ne servono 196; per ±5% ne servono 385. Sotto le cento domande l'intervallo su un punteggio come 0,85 supera i 14 punti.
Quali metriche richiedono una risposta di riferimento?
Nelle definizioni di Ragas la context precision e la context recall richiedono il riferimento; la faithfulness richiede domanda, risposta e contesti; la answer relevancy richiede solo domanda e risposta. Controlla le firme della versione che usi, perché l'interfaccia cambia fra una versione e l'altra.
Una faithfulness alta garantisce che la risposta sia giusta?
No. La faithfulness misura se le affermazioni della risposta trovano appoggio nel contesto recuperato, non se il contesto è corretto. Con un recupero sbagliato e un generatore fedele ottieni una risposta coerente con il contesto e sbagliata per l'utente. Serve guardare insieme recupero e fedeltà.
Un giudice basato su un modello è affidabile?
Entro dei limiti. Lo studio di Zheng e colleghi ha misurato un accordo con le preferenze umane oltre l'80% per un giudice forte, e ha descritto bias di posizione, di lunghezza e di auto-preferenza. Tieni fisso il giudice fra le esecuzioni che confronti e controlla l'accordo su un sottoinsieme etichettato a mano.
Perché il numero di documenti simili peggiora il recupero?
Perché i punteggi dei candidati si avvicinano, e quando due testi hanno lo stesso punteggio decide un criterio arbitrario. Nel modello didattico dell'articolo l'originale esce dai primi tre posti non appena il corpus contiene molte copie quasi identiche. Un campo di versione e un filtro prima del recupero risolvono il problema alla radice.
Fonti
- Es et al., Ragas: Automated Evaluation of Retrieval Augmented Generation (arXiv:2309.15217) - l'idea di una valutazione senza annotazioni umane e le dimensioni del RAG da misurare, consultata il 2026-11-12
- Ragas, Context Precision - la formula della precisione ai ranghi rilevanti, consultata il 2026-11-12
- Ragas, Faithfulness - la definizione come rapporto fra affermazioni supportate e affermazioni totali, consultata il 2026-11-12
- Ragas, Answer Relevancy - le domande generate (tre per impostazione predefinita) e il coseno con la domanda originale, consultata il 2026-11-12
- Zheng et al., Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena (arXiv:2306.05685) - i tre bias del giudice e l'accordo oltre l'80% con le preferenze umane, consultata il 2026-11-12
- Liu et al., Lost in the Middle: How Language Models Use Long Contexts (arXiv:2307.03172) - l'effetto della posizione dell'informazione nel contesto, consultata il 2026-11-12
- Magesh et al., Hallucination-Free? Assessing the Reliability of Leading AI Legal Research Tools (arXiv:2405.20362) - l'intervallo 17%-33% di allucinazioni negli strumenti legali basati su RAG, consultata il 2026-11-12
I listati con un'uscita riportata sono stati eseguiti due volte con Python 3.14, NumPy e scikit-learn, e le uscite coincidono. Il listato di Ragas non è stato eseguito: le firme sono quelle della versione 0.4.3, lette con inspect.signature. Il corpus è inventato e il recupero TF-IDF è un modello didattico.

Commenti
Caricamento commenti...
Accedi per commentare