Eval self-hosted per agenti AI: guida pratica
Crea un ciclo di eval nel tuo repository con casi reali, controlli deterministici, giudice LLM e tasso di superamento tracciato a ogni commit.
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 interrogabile. 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. Ha iniziato a fallire nella seconda settimana perché è cambiata una riga del prompt, oppure è cambiato il modello o la descrizione di uno strumento, senza che alcuna misurazione rilevasse il cambiamento. 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 prevede quattro passaggi, e questa guida dedica una sezione a ciascuno: raccogliere trace reali, trasformare quelli 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 per agenti self-hosted che vale la pena eseguire differiscono soprattutto per la quantità di trace che rendono disponibili 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 durante l’esecuzione. Tutti e quattro questi 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 su input che nessuno ha sottoposto di nuovo a test, e le tracce lo mostrano chiaramente: la traccia della settimana scorsa per la stessa domanda contiene una create_refund chiamata allo strumento, mentre quella di questa settimana non ne contiene alcuna e la risposta è invece una scusa cortese. Non è stato generato alcun errore, quindi non è scattato nessun alert.
La seconda causa è il modello. Registra la stringa esatta del modello inviata con ogni esecuzione, claude-haiku-4-5-20251001 non una forma abbreviata che ricordi a memoria, perché una riduzione del tasso di successo il giorno in cui hai cambiato modello è diagnosticabile soltanto se il modello è presente nella riga.
La terza causa riguarda gli strumenti. Riscrivere la descrizione di uno strumento cambia il momento in cui il modello decide di chiamarlo. Se gli strumenti arrivano tramite server MCP eseguiti su un VPS, lo schema risiede in un altro processo, quindi può cambiare senza che nel repository compaia alcuna differenza. La quarta causa è il recupero: la stessa domanda interroga un indice ricostruito durante la notte e la risposta segue il nuovo documento.
Costruisci 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 agent, ogni richiesta viene salvata con input, chiamate agli strumenti e output. Questo è esattamente il materiale grezzo necessario per un caso.
Esporta tramite API pubblica un intervallo di osservazioni root. L'API usa 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 da come il tuo agent strumenta i propri span. Mappa quindi i campi effettivamente presenti, senza basarti su quelli attesi. Scrivi poi 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 utile per le esecuzioni:
- Per iniziare sono sufficienti da 40 a 80 casi. Sotto 20, un singolo caso instabile modifica il tasso di superamento di 5 punti e un valore che cambia senza motivo viene ignorato.
- Ogni bug rilevato in produzione che correggi diventa un caso il giorno stesso della correzione. Questa abitudine fa crescere il set nella direzione corretta.
- Un solo comportamento per caso. Un caso che verifica contemporaneamente l'importo del rimborso e il tono della risposta non fornisce informazioni quando fallisce.
- Il
idnon cambia mai, perché l'id consente di confrontare l'esecuzione di oggi con quella del mese scorso. - Oscura i dati prima del commit. Questo file viene archiviato in git: rimuovi quindi i nomi dei clienti e tutti i numeri d'ordine che non ti appartengono.
Valuta prima con controlli deterministici, perché non hanno costi
Per tutto ciò che ha una risposta univoca, usa un'asserzione semplice. Non serve alcuna chiamata al modello, non ci sono costi né ambiguità. I controlli deterministici rilevano le regressioni strutturali, cioè quelle che compromettono i sistemi che circondano l'agente: il JSON non viene analizzato, lo strumento non è mai stato chiamato, ricompare la frase vietata oppure la risposta non cita alcuna fonte.
Nel test harness, una sola funzione deve conoscere l'agente. Tutto il resto 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 in quell'elenco anche il budget delle chiamate agli strumenti. Un agente 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 genera un costo.
LLM come giudice e i quattro modi in cui può sbagliare
Tutto 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 modo 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 il formato dell'output e analizzalo 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 vediamo le modalità di errore. Per ciascuna puoi eseguire un test già oggi. È importante farlo, perché un giudice non verificato produce numeri dall'aspetto preciso ma privi di significato.
Bias di lunghezza. Le risposte più lunghe superano il test più spesso. Esegui questo test: prendi dieci risposte che il giudice ha valutato negativamente, aggiungi a ciascuna due paragrafi di testo assertivo che non introducano nuovi fatti e sottoponile di nuovo alla valutazione. Se un verdetto passa da negativo a positivo, si tratta di un bias di lunghezza e devi correggere il criterio di valutazione.
Preferenza per il proprio modello. Spesso un giudice valuta più bene l'output della propria famiglia di modelli rispetto a quello di un'altra. Esegui questo test: valuta le stesse 30 risposte con giudici appartenenti a due famiglie diverse e confronta i verdetti caso per caso. Nei casi di disaccordo, esamina tu stesso il caso.
Bias di posizione. Se usi il giudice per confrontare due risposte, A e B, inverti l'ordine ed esegui di nuovo la valutazione. Se il verdetto cambia dopo lo scambio, il confronto a coppie non è ancora affidabile per quel criterio.
Deriva del criterio. I criteri vaghi producono giudici accomodanti. "La risposta è utile?" produce un esito positivo quasi sempre. "La risposta indica l'importo del rimborso in dollari?" produce un esito positivo solo quando è presente il contenuto previsto. Riscrivi ogni criterio finché non identifica il fatto da verificare.
Un'unica misura di controllo copre tutti e quattro i problemi. Conserva 30 casi che hai etichettato 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 criterio prima di fidarti di qualsiasi percentuale di esiti positivi che produce. Il giudice è codice: deve quindi essere versionato e sottoposto a revisione come il codice.
Dai un punteggio con un modello economico e passa a un modello frontier
Valutare ogni caso con il modello più costoso a ogni commit è il modo migliore per far crescere il costo delle valutazioni oltre il costo dell’agente che si sta testando. Ordina i valutatori in base al prezzo e interrompi la procedura 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 ogni chiamata al 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, ma cambia quando la si moltiplica. Un set di 60 casi, valutato a ogni commit con 40 commit alla settimana, genera 2,400 chiamate al valutatore alla settimana prima ancora di eseguire il job notturno.
Due sconti si applicano direttamente alle attività di valutazione e sono cumulabili. Le esecuzioni delle valutazioni non sono interattive, quindi la Batch API dimezza i prezzi di input e output in cambio della consegna asincrona. Questa è la prima riga del grafico. Il rubric e le istruzioni sono identici byte per byte in ogni chiamata, quindi il prompt caching è adatto: una lettura dalla cache costa un decimo del prezzo base dell’input e 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 di Anthropic ad agosto 2026. Sonnet 5 ha un prezzo promozionale fino al 31 agosto 2026, quindi la terza barra aumenta dopo quella data.
La sequenza, in ordine:
- Controlli deterministici su ogni caso. Nessun costo API.
- Un valutatore basato su un modello di piccole dimensioni sui casi che superano i controlli.
- Un valutatore frontier solo quando il modello di piccole dimensioni indica un fallimento o 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 il compromesso invece di darlo per scontato. Una volta al mese, valuta l’intero set anche con il valutatore rigoroso e confronta le due colonne. Se i risultati differiscono in più di pochi casi, il rubric è troppo permissivo per il modello di piccole dimensioni. È quindi il rubric che devi correggere. Il controllo della spesa dell’agente è un’attività distinta, descritta in controllo dei costi per un agente AI su un VPS.
Monitorare nel tempo il tasso di superamento dei test in un sistema di cui si dispone
Un tasso di superamento dei test che non può essere associato a un commit è solo un’impressione. Salvare 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;Caricare lo schema con sqlite3 evals/results.db < evals/schema.sql, quindi leggere 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 descrive le impostazioni da considerare 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 amountEseguire la suite sulle modifiche che possono compromettere un agent, cioè modifiche ai prompt, al modello e agli strumenti, anziché a ogni commit del 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 vanno pianificate. Un servizio e timer systemd sul VPS eseguito ogni notte avvia l’intero insieme di test sul prompt distribuito. In questo modo rileva le modifiche che arrivano dall’esterno del repository, ad esempio quando cambia il comportamento di uno strumento ospitato.
Revisione umana, su un campione e non esaustiva
Il giudice viene calibrato rispetto alle valutazioni umane, quindi qualcuno deve produrle. Esaminate ogni settimana un campione composto da tutti i casi in cui il giudice ha fallito e da dieci valutazioni positive selezionate casualmente. Le valutazioni positive casuali sono la metà più importante, perché un giudice che ha iniziato a convalidare in modo impercettibile risposte errate sembra perfetto in qualsiasi dashboard basata sui propri verdetti.
Quindici casi da tre minuti ciascuno richiedono 45 minuti alla settimana e permettono di correggere i criteri nei casi in cui voi e il giudice non siete d'accordo, oltre ad aggiungere nuovi casi per tipi di errore che nessuno aveva previsto. Scrivete il verdetto umano nella stessa tabella con graded_by impostato su human, in modo che l'accordo tra giudice e valutazione umana sia rilevabile tramite una query invece di restare affidato alla memoria.
Cosa si rompe nel test harness stesso
anthropic.RateLimitError alla prima esecuzione completa. L'esecuzione simultanea di sessanta casi supera il limite di richieste o token del 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 judge. Il modello ha risposto in prosa oppure ha racchiuso il JSON in un code fence. Riprova una volta, quindi registra il caso come errore. Un errore di parsing non deve mai essere considerato un pass, perché una suite che trasforma gli errori in pass arriva verso il 100% mentre l'agente peggiora.
Casi intermittenti. Lo stesso input passa in un'esecuzione e fallisce in quella successiva perché l'agente campiona l'output. Esegui il caso intermittente tre volte e registra la frazione invece di eliminarlo. Un caso che passa due volte su tre indica un problema reale di robustezza, che un cliente prima o poi troverà.
Degrado del golden set. Qualcuno modifica una risposta attesa per far risultare verde la suite. Esamina le differenze rispetto a evals/cases.jsonl con la stessa attenzione riservata alle differenze dell'agente, perché quel file è la definizione scritta di ciò che è corretto.
Una suite che non fallisce mai. Un tasso di pass fermo al 100% per un mese significa che il set non tiene più traccia del prodotto. Recupera dieci trace recenti, individua quelli gestiti male dall'agente e aggiungili.
FAQ
Quanti casi servono in 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 dato non è più informativo. Oltre qualche centinaio di casi, ogni esecuzione richiede tempo e costi concreti, 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 almeno una volta nel set.
Posso fidarmi di un giudice LLM per valutare il mio agente?
Solo dopo averlo confrontato con le tue valutazioni. 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 a favore delle risposte lunghe: le risposte con contenuto aggiunto superano più spesso la valutazione. Mostrano anche una preferenza per il proprio modello: valutano con maggiore benevolenza gli output della propria famiglia di modelli. Entrambi i fenomeni sono verificabili: aggiungi contenuto a una risposta che aveva fallito e sottoponila nuovamente alla valutazione, oppure valuta le stesse risposte con un giudice di un'altra famiglia. Se il giudice non concorda con le tue valutazioni in più di un caso su dieci, la rubrica è troppo vaga per essere utilizzata.
Quale modello deve valutare le prove?
Inizia con il modello meno costoso e passa a uno più potente quando serve. Le asserzioni deterministiche non hanno costi di inferenza, quindi vengono eseguite per prime su ogni caso. Un modello di dimensioni ridotte gestisce i superamenti chiari. Solo i fallimenti e i verdetti con bassa confidenza passano a un modello frontier. Ai prezzi di listino di agosto 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 valutazioni sono asincrone, la Batch API dimezza entrambe le cifre.
Le valutazioni 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, incluse quelle per cui il set non contiene alcun caso. I due strumenti si alimentano a vicenda: i trace forniscono nuovi casi e la suite di valutazione stabilisce se la correzione ha funzionato davvero.