Eval self-hosted per agenti AI: guida pratica
Crea eval nel tuo repository con tracce reali, controlli deterministici e un giudice LLM. Misura il tasso di superamento per commit, senza vendor e con SQLite.
Cosa sono le eval self-hosted per gli agenti AI
Le eval self-hosted per gli agenti AI sono quattro elementi che conservi nel tuo repository: un file con i casi salvati, uno script che esegue l’agente su questi casi, un insieme di controlli che assegna un punteggio a ogni risposta e una tabella dei risultati che puoi interrogare. Nessuno di questi elementi richiede un vendor. L’intero ciclo richiede poche centinaia di righe di Python e un unico file SQLite.
L’agente ha funzionato nella demo perché hai scelto personalmente i cinque input. Nella seconda settimana ha iniziato a fallire perché è cambiata una riga del prompt, è cambiato un modello oppure è cambiata la descrizione di uno strumento, senza che alcuna misurazione rilevasse il problema. Un ciclo di eval trasforma «ora sembra peggiore» in «il tasso di superamento è passato da 58 su 60 a 51 su 60 nel commit 4f1c9ab».
Il ciclo comprende quattro passaggi, e questa guida dedica una sezione a ciascuno: raccogliere tracce reali, trasformare quelle interessanti in casi, valutare ogni caso a ogni modifica e memorizzare il tasso di superamento accanto al commit che lo ha prodotto. Lo stesso ciclo funziona indipendentemente da ciò su cui esegui l’agente, e i framework self-hosted per agenti che vale la pena eseguire si distinguono soprattutto per la quantità di traccia che rendono disponibile senza configurazione aggiuntiva.
Perché l'agente si rompe nella seconda settimana
Un agente è composto da un prompt, un modello, un insieme di definizioni degli strumenti e il contesto recuperato al momento dell'esecuzione. Tutti e quattro gli elementi possono cambiare senza modificare il codice dell'applicazione, quindi una normale revisione del codice non rileva alcun problema.
La causa più comune è una modifica al prompt. Aggiungi una frase per impedire una risposta scortese. Quella frase cambia il comportamento per input che nessuno ha sottoposto nuovamente a test. Le tracce lo mostrano chiaramente: la traccia della settimana scorsa per la stessa domanda contiene una chiamata allo strumento create_refund, quella di questa settimana non ne contiene alcuna e la risposta è invece una scusa cortese. Non si è verificato alcun errore, quindi non è stato generato alcun alert.
La seconda causa è il modello. Registra la stringa esatta del modello inviata con ogni esecuzione, claude-haiku-4-5-20251001 invece di usare una forma abbreviata che ricordi a memoria, perché un tasso di superamento dei test che diminuisce il giorno in cui cambi modello è diagnosticabile soltanto se il modello è riportato nella riga.
La terza causa sono gli strumenti. Riscrivere la descrizione di uno strumento modifica il momento in cui il modello decide di chiamarlo. Se gli strumenti arrivano da server MCP eseguiti su un VPS, lo schema risiede in un altro processo. Di conseguenza può cambiare senza che nel repository compaia alcuna differenza. La quarta causa è il recupero dei contenuti: la stessa domanda raggiunge un indice ricostruito durante la notte e la risposta si basa sul nuovo documento.
Crea il set di riferimento a partire dalle tracce che raccogli già
Non inventare i casi di valutazione. Ricavali dal traffico. Se utilizzi già il tracing self-hosted di Langfuse per il tuo agente, ogni richiesta viene salvata con l'input, le chiamate agli strumenti e l'output: è esattamente il materiale grezzo necessario per un caso.
Esporta tramite l'API pubblica una finestra di osservazioni root. L'API utilizza l'autenticazione di base: la chiave pubblica è il nome utente e la chiave segreta è la password.
export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
"$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
| jq '.data[0]'Leggi un record prima di scrivere il parser. Le righe vengono restituite in data, ma i nomi dei campi che contengono la domanda e la risposta dipendono dal modo in cui l'agente strumenta i propri span. Mappa quindi i dati effettivamente presenti, invece di usare quelli previsti. Scrivi quindi manualmente i casi, con un oggetto JSON per riga, in evals/cases.jsonl:
{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}Cinque regole mantengono il set adatto all'esecuzione:
- Per iniziare sono sufficienti da 40 a 80 casi. Sotto 20 casi, un singolo caso instabile modifica il tasso di superamento di 5 punti e un numero che varia senza motivo viene ignorato.
- Ogni bug di produzione corretto diventa un caso il giorno stesso della correzione. Questa abitudine fa crescere il set nella direzione giusta.
- Un solo comportamento per caso. Un caso che controlla insieme l'importo del rimborso e il tono non fornisce informazioni quando fallisce.
- Il
idnon cambia mai, perché l'ID consente di confrontare l'esecuzione odierna con quella del mese scorso. - Rimuovi i dati sensibili prima del commit. Questo file viene inserito in git, quindi elimina i nomi dei clienti e i numeri d'ordine che non possiedi.
Esegui prima le verifiche deterministiche, perché non hanno costi
Tutto ciò che ha una risposta univoca deve essere verificato con una semplice asserzione. Non serve una chiamata al modello, non ci sono costi né ambiguità. Le verifiche deterministiche rilevano le regressioni strutturali, cioè quelle che compromettono i sistemi che circondano il tuo agent: il JSON non viene analizzato, lo strumento non è mai stato chiamato, ricompare la frase vietata oppure la risposta non cita alcuna fonte.
Una sola funzione deve conoscere il tuo agent. Tutto il resto dell'harness deve essere generico.
import json, os, urllib.request
def run_agent(case):
req = urllib.request.Request(
os.environ["AGENT_URL"],
data=json.dumps({"input": case["input"]}).encode(),
headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as resp:
return json.load(resp)
def deterministic(case, result):
text = result.get("output", "")
called = [c["name"] for c in result.get("tool_calls", [])]
failures = []
for tool in case.get("must_call", []):
if tool not in called:
failures.append(f"tool not called: {tool}")
for phrase in case.get("must_not_include", []):
if phrase.lower() in text.lower():
failures.append(f"forbidden phrase: {phrase}")
if len(called) > case.get("max_tool_calls", 12):
failures.append(f"too many tool calls: {len(called)}")
return failuresMantieni il budget degli strumenti in quell'elenco. Un agent che oggi risolve un caso con 3 chiamate e domani ne usa 11 ha subito una regressione anche se la risposta finale è corretta, perché ogni chiamata comporta un costo.
LLM come giudice e i quattro modi in cui può fallire
Ciò che supera le asserzioni richiede un valutatore in grado di leggere. Un giudice LLM è una seconda chiamata al modello: riceve la domanda, la risposta dell'agente e un criterio, quindi restituisce un verdetto. È l'unico metodo pratico per valutare se la risposta soddisfa effettivamente la richiesta dell'utente.
Quattro regole rendono utilizzabile un giudice:
- Verdetto binario, mai un punteggio da 1 a 10. Una scala restituisce 7 e 8 per quasi tutto, quindi il numero non cambia mai e non fornisce informazioni utili.
- Un criterio per chiamata. Chiedi di valutare l'importo del rimborso oppure il tono, non entrambi contemporaneamente.
- Fornisci al giudice la risposta attesa quando il caso ne prevede una. Valutare rispetto a un riferimento è molto più semplice che valutare in astratto.
- Imposta una struttura di output obbligatoria e analizzala in modo rigoroso.
from anthropic import Anthropic
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
def judge_prompt(case, output):
return (
"You grade one answer against one criterion.\n"
"Reply with JSON only, in this exact shape:\n"
'{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
f"Criterion: {case['rubric']}\n"
f"Question: {case['input']}\n"
f"Answer: {output}\n"
"Length is not a criterion. Judge only the criterion above."
)
def judge(case, output, model):
msg = client.messages.create(
model=model,
max_tokens=200,
messages=[{"role": "user", "content": judge_prompt(case, output)}],
)
return json.loads(msg.content[0].text)Ora esaminiamo le modalità di errore. Ognuna può essere verificata con un test eseguibile oggi stesso. Questi test sono importanti, perché un giudice non verificato produce numeri apparentemente precisi ma privi di significato.
Distorsione a favore delle risposte lunghe. Le risposte più lunghe superano il test più spesso. Verifica questa ipotesi: prendi dieci risposte che il giudice ha valutato negativamente, aggiungi a ciascuna due paragrafi di testo sicuro di sé ma privo di nuovi dati, quindi sottoponile di nuovo alla valutazione. Se un verdetto cambia in positivo, si tratta di una distorsione a favore della lunghezza e devi correggere il rubric.
Preferenza per il proprio modello. Spesso un giudice valuta più bene l'output della propria famiglia di modelli rispetto a quello di un'altra famiglia. Verifica questa ipotesi: valuta le stesse 30 risposte con giudici appartenenti a due famiglie diverse e confronta i verdetti caso per caso. Nei casi di disaccordo, esamina personalmente il caso.
Distorsione dovuta alla posizione. Se usi il giudice per confrontare due risposte, A e B, invertine l'ordine ed esegui di nuovo la valutazione. Se il verdetto cambia dopo lo scambio, il confronto a coppie non è ancora affidabile per quel rubric.
Deriva del rubric. I criteri vaghi producono giudici troppo permissivi. "La risposta è utile?" porta quasi tutto a un esito positivo. "La risposta indica l'importo del rimborso in dollari?" porta a un esito positivo solo quando è presente il dato richiesto. Riscrivi ogni criterio finché non identifica il fatto da verificare.
Un'unica protezione copre tutti e quattro i casi. Conserva 30 casi etichettati manualmente e confronta il giudice con le tue etichette ogni volta che modifichi il modello o il prompt del giudice. Se il giudice non concorda con te in più di un caso su dieci, correggi il rubric prima di fidarti dei tassi di superamento che produce. Il giudice è codice: deve essere sottoposto a versionamento e revisione come qualsiasi altro codice.
Dare il primo giudizio con un modello economico e passare a un modello frontier
Valutare ogni caso con il modello più costoso a ogni commit è il modo più rapido per far crescere il costo delle eval oltre il costo dell'agent che si sta verificando. Ordina i valutatori in base al prezzo e interrompi il processo non appena la risposta è chiara.
The data behind this chart
[
{
"label": "Haiku 4.5, Batch API",
"usd_per_1000_judge_calls": "0.90"
},
{
"label": "Haiku 4.5",
"usd_per_1000_judge_calls": "1.80"
},
{
"label": "Sonnet 5",
"usd_per_1000_judge_calls": "3.60"
},
{
"label": "Opus 5",
"usd_per_1000_judge_calls": "9.00"
}
]Queste cifre presuppongono circa 1,200 token di input e 120 token di output per chiamata del valutatore, una dimensione realistica per una domanda, una risposta e un criterio. Valutare 1,000 casi costa 1.80 dollari statunitensi con Claude Haiku 4.5 e 9.00 con Claude Opus 5. La differenza sembra trascurabile finché non la si moltiplica. Un set di 60 casi, valutato a ogni commit con 40 commit alla settimana, genera 2,400 chiamate del valutatore alla settimana prima ancora che qualcuno esegua il job notturno.
Due sconti si applicano bene alle eval e sono cumulabili. Le esecuzioni delle eval non sono interattive, quindi la Batch API dimezza i prezzi di input e output in cambio di una consegna asincrona; questa è la prima riga del grafico. La rubric e le istruzioni sono identiche byte per byte in ogni chiamata, quindi è possibile usare il prompt caching: una lettura dalla cache costa un decimo del prezzo base dell'input, mentre una scrittura nella cache con durata di cinque minuti costa 1.25 volte il prezzo base dell'input. Di conseguenza, la cache si ripaga dopo un solo riutilizzo. Questi sono i prezzi di listino Anthropic ad agosto 2026 e Sonnet 5 applica prezzi promozionali fino al 31 agosto 2026; per questo la terza barra aumenta dopo tale data.
La sequenza, nell'ordine:
- Controlli deterministici su ogni caso. Nessun costo API.
- Un modello di piccole dimensioni valuta i casi che hanno superato quei controlli.
- Un modello frontier valuta soltanto i casi per i quali il modello piccolo indica un fallimento oppure un superamento con bassa confidenza.
- Revisione umana su un campione ridotto, una volta alla settimana.
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"
def grade(case, result):
hard = deterministic(case, result)
if hard:
return False, "deterministic", "; ".join(hard)
first = judge(case, result["output"], CHEAP)
if first["verdict"] == "pass" and first["confidence"] == "high":
return True, CHEAP, first["reason"]
second = judge(case, result["output"], STRICT)
return second["verdict"] == "pass", STRICT, second["reason"]Questa strategia riduce in parte l'accuratezza della valutazione per contenere i costi. Misura quindi questo compromesso invece di darlo per scontato. Una volta al mese, valuta l'intero set anche con il valutatore più rigoroso e confronta le due colonne. Se non concordano su più di una manciata di casi, la rubric è troppo poco vincolante per il modello piccolo. È la rubric che devi correggere. Il controllo della spesa dell'agent è un'attività separata, descritta in controllo dei costi per un agent AI su un VPS.
Monitora nel tempo la percentuale di superamento in un sistema di tua proprietà
Una percentuale di superamento che non puoi associare a un commit è solo un’impressione. Archivia una riga per ogni caso e per ogni esecuzione, includendo nella riga il commit e il modello.
CREATE TABLE IF NOT EXISTS results (
run_id TEXT NOT NULL,
ran_at TEXT NOT NULL,
git_sha TEXT NOT NULL,
agent_model TEXT NOT NULL,
case_id TEXT NOT NULL,
passed INTEGER NOT NULL,
graded_by TEXT NOT NULL,
reason TEXT
);SELECT run_id, git_sha, agent_model,
count(*) AS cases,
round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;Carica lo schema con sqlite3 evals/results.db < evals/schema.sql, quindi leggi l’andamento con sqlite3 -box evals/results.db < evals/passrate.sql. Un anno di esecuzioni giornaliere su 60 casi produce circa 22,000 righe, quindi l’archivio non diventa mai un progetto autonomo. Eseguire SQLite in produzione su un VPS illustra le impostazioni che diventano importanti se questo file viene condiviso tra più macchine.
Il runner stampa le stesse informazioni per una persona:
run 2026-08-05T09:14:22Z sha 4f1c9ab model claude-sonnet-5 58/60 pass (96.7%)
FAIL refund-double-charge deterministic: tool not called: create_refund
FAIL pto-policy-question judge(opus): reply gives no dollar amountEsegui la suite sulle modifiche che possono compromettere un agente, cioè modifiche ai prompt, al modello e agli strumenti, invece che a ogni commit presente nel repository. Un hook pre-push copre il sottoinsieme rapido:
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-pushLe esecuzioni complete sono più lente e devono essere pianificate. Un servizio e timer systemd sul VPS esegue ogni notte l’intero insieme di test sul prompt distribuito. In questo modo rileva anche le modifiche provenienti dall’esterno del repository, ad esempio quando cambia il comportamento di uno strumento ospitato.
Revisione umana a campione, non esaustiva
Il giudice viene calibrato rispetto alle valutazioni umane, che quindi devono essere prodotte da qualcuno. Ogni settimana esamina un campione: tutti i casi in cui il giudice ha sbagliato, più dieci casi superati scelti casualmente. I casi superati scelti casualmente sono la metà più importante, perché un giudice che ha iniziato a convalidare in modo discreto risposte errate sembra perfetto in qualsiasi dashboard costruita sui suoi stessi verdetti.
Quindici casi da tre minuti ciascuno richiedono 45 minuti alla settimana. In cambio, ottieni correzioni al rubric nei casi in cui tu e il giudice non siete d'accordo, oltre a nuovi casi per tipi di errore che nessuno aveva previsto. Scrivi il verdetto umano nella stessa tabella con graded_by impostato su human, in modo che l'accordo tra giudice e valutazione umana diventi una query invece di restare affidato alla memoria.
Cosa può rompersi nell'harness di valutazione
anthropic.RateLimitError alla prima esecuzione completa. Sessanta casi avviati contemporaneamente superano il limite di richieste o di token previsto dal tuo tier. Limita la concorrenza a quattro worker e sposta l'esecuzione notturna sulla Batch API.
json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) del valutatore. Il modello ha risposto in prosa oppure ha racchiuso il JSON in un code fence. Esegui un nuovo tentativo, quindi registra il caso come errore. Un errore di parsing non deve mai essere conteggiato come superamento, perché una suite che converte gli errori in superamenti può arrivare al 100% mentre l'agent peggiora.
Casi instabili. Lo stesso input supera un'esecuzione e fallisce quella successiva perché l'agent campiona l'output. Esegui il caso instabile tre volte e registra la frazione, invece di eliminarlo. Un caso che supera due esecuzioni su tre indica un problema reale di robustezza e un cliente lo rileverà.
Deterioramento del golden set. Qualcuno modifica una risposta attesa per far risultare la suite corretta. Esamina con la stessa attenzione i diff di evals/cases.jsonl e quelli dell'agent, perché quel file è la definizione scritta di ciò che consideri corretto.
Una suite che non fallisce mai. Un tasso di superamento fermo al 100% per un mese indica che il set non segue più il prodotto. Recupera dieci trace recenti, individua quelli gestiti male dall'agent e aggiungili. Poi introduci volutamente un errore e verifica che l'esecuzione fallisca: è il controllo mutation testing si applica a una test suite e l'unico modo per sapere che il set continua a rilevare i problemi.
FAQ
Di quanti casi ha bisogno un set di valutazione per un agente AI?
Inizia con 40-80 casi e amplia il set a partire dagli errori reali. Al di sotto di circa 20 casi, un singolo risultato instabile modifica il tasso di superamento di 5 punti, quindi il numero non è più informativo. Oltre qualche centinaio di casi, ogni esecuzione richiede tempo e costi reali, mentre ogni caso aggiuntivo aumenta poco la copertura. La misura importante non è il numero di casi, ma la percentuale dei tipi di errore noti in produzione che compaiono nel set almeno una volta.
Posso affidarmi a un giudice LLM per valutare il mio agente?
Solo dopo averne misurato l'accuratezza rispetto alle tue etichette. Conserva 30 casi valutati manualmente e confronta il giudice con questi casi ogni volta che cambi il modello o il prompt del giudice. I giudici mostrano un bias verso la lunghezza: le risposte prolisse superano più spesso la valutazione. Mostrano anche una preferenza per il proprio modello: valutano in modo più favorevole gli output della stessa famiglia di modelli. Entrambi i bias sono verificabili: aggiungi testo a una risposta che non ha superato la valutazione e sottoponila di nuovo al giudice, oppure valuta le stesse risposte con un giudice di un'altra famiglia. Se il giudice non concorda con le tue etichette in più di 1 caso su 10, i criteri di valutazione sono troppo vaghi per essere utilizzati.
Quale modello deve valutare le verifiche?
Usa prima i modelli economici e passa a modelli più avanzati solo quando serve. Le asserzioni deterministiche non hanno costi, quindi vengono eseguite per prime su ogni caso. Un modello piccolo gestisce i casi chiaramente superati. Solo i casi non superati e i verdetti con bassa confidenza vengono inviati a un modello frontier. Ai prezzi di listino di August 2026, la valutazione di 1.000 casi costa circa 1.80 dollari statunitensi con Claude Haiku 4.5 e circa 9.00 con Claude Opus 5. Poiché le esecuzioni delle verifiche sono asincrone, la Batch API dimezza entrambi gli importi.
Le verifiche sostituiscono il monitoraggio in produzione?
No, perché rispondono a domande diverse. Una suite di valutazione indica se una modifica che stai per rilasciare migliora o peggiora un set fisso di casi. Tracing e monitoraggio mostrano quali richieste reali stanno inviando gli utenti in quel momento, inclusi gli input non coperti da alcun caso. I due strumenti si alimentano a vicenda: le tracce forniscono nuovi casi e la suite di valutazione determina se la correzione ha effettivamente risolto il problema.