Testing dei Prompt: Come Valutare la Qualità delle Risposte AI

Testing dei Prompt: Come Valutare la Qualità delle Risposte AI

Perché il testing dei prompt è fondamentale nel 2026

Nel 2026 costruire applicazioni AI senza un sistema di valutazione dei prompt è come deployare codice senza test: prima o poi ti esplode in faccia. Il testing dei prompt — spesso chiamato prompt evaluation o semplicemente eval — è la disciplina che ti permette di misurare oggettivamente se un LLM sta rispondendo nel modo corretto, coerente e affidabile al tuo use case specifico.

Il problema è reale: un prompt che funziona perfettamente oggi può degradarsi dopo un aggiornamento del modello, una modifica al context window, o semplicemente su input leggermente diversi dagli esempi di sviluppo. Senza metriche concrete non sai quando un prompt “va bene” e quando no. E se hai già lavorato con la guida al prompt engineering per sviluppatori, sai quanto sia facile ottimizzare un prompt per un caso ma farlo regredire su un altro.

In questa guida vedremo come costruire un sistema di testing sistematico: dalla creazione di dataset di valutazione, alle metriche automatiche, fino all’uso di LLM-as-a-judge — il pattern che sta dominando l’industria nel 2026 per valutare output testuali complessi dove non esiste una risposta “giusta” univoca.

Tipologie di eval: automatico vs manuale vs LLM-as-judge

Prima di scrivere codice è importante distinguere le tre categorie principali di testing per prompt:

  • Eval deterministico: l’output atteso è noto e confrontabile con exact match, regex o parsing strutturato. Usato per classificazioni, estrazione dati, output JSON.
  • Eval basato su metriche NLP: BLEU, ROUGE, BERTScore per testi dove esiste una reference answer. Utile per riassunti e traduzioni.
  • LLM-as-a-judge: un secondo LLM valuta la risposta del primo secondo criteri definiti. Il pattern più potente per output aperti come spiegazioni, codice, risposte di supporto.

Per la maggior parte dei progetti reali userai una combinazione di tutti e tre. Se stai costruendo un sistema RAG, ad esempio, vorrai eval deterministici per verificare che i metadati vengano estratti correttamente, metriche di faithfulness per i riassunti, e LLM-as-judge per valutare la qualità complessiva della risposta.

Costruire il dataset di valutazione

Tutto parte dal dataset. Un buon eval dataset deve coprire i casi d’uso reali del tuo prodotto: non solo i casi felici, ma soprattutto i casi limite, gli input ambigui e le situazioni in cui il modello tende a sbagliare. Regola pratica: 50-100 esempi per task semplice, 200+ per task complesso.

Ecco la struttura minima di un dataset di eval in formato JSON:

[
  {
    "id": "eval_001",
    "input": "Riassumi questo articolo tecnico in 3 punti chiave: ...",
    "context": "Articolo originale...",
    "expected_output": null,
    "criteria": ["conciseness", "accuracy", "completeness"],
    "tags": ["summarization", "technical"],
    "difficulty": "medium"
  },
  {
    "id": "eval_002",
    \"input\": \"Estrai il nome e l'email da questo testo: 'Contatta Mario Rossi a mario@example.com'\",
    "context": null,
    "expected_output": {"name": "Mario Rossi", "email": "mario@example.com"},
    "criteria": ["exact_match"],
    "tags": ["extraction", "structured"],
    "difficulty": "easy"
  }
]

La chiave expected_output è null per i casi in cui userai LLM-as-judge, mentre è compilata per gli eval deterministici. I criteria guidano sia il judge che la reportistica finale.

🔧 Pro tip: Usa i tuoi log di produzione per costruire il dataset. Esporta 100 query reali degli utenti e annota manualmente quelle dove il modello ha sbagliato: sono oro puro per il testing.

Eval deterministico: exact match e parsing strutturato

Per task come l’estrazione di dati o la classificazione, l’eval deterministico è il più preciso e veloce. Con la Claude API e tool use puoi forzare output strutturati e verificarli programmaticamente:

import anthropic
import json
from dataclasses import dataclass
from typing import Any

client = anthropic.Anthropic()

@dataclass
class EvalResult:
    id: str
    passed: bool
    score: float
    actual_output: Any
    expected_output: Any
    error: str | None = None

def run_deterministic_eval(case: dict) -> EvalResult:
    # Esegue un eval deterministico con exact match su output strutturato
    try:
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=512,
            tools=[{
                "name": "extract_data",
                "description": "Estrai i dati richiesti dal testo",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "email": {"type": "string"}
                    },
                    "required": ["name", "email"]
                }
            }],
            tool_choice={"type": "tool", "name": "extract_data"},
            messages=[{"role": "user", "content": case["input"]}]
        )

        tool_use = next(b for b in response.content if b.type == "tool_use")
        actual = tool_use.input
        expected = case["expected_output"]

        passed = (
            actual.get("name", "").lower() == expected["name"].lower() and
            actual.get("email", "").lower() == expected["email"].lower()
        )

        return EvalResult(
            id=case["id"],
            passed=passed,
            score=1.0 if passed else 0.0,
            actual_output=actual,
            expected_output=expected
        )

    except Exception as e:
        return EvalResult(
            id=case["id"],
            passed=False,
            score=0.0,
            actual_output=None,
            expected_output=case["expected_output"],
            error=str(e)
        )

def run_eval_suite(dataset: list[dict]) -> dict:
    results = [run_deterministic_eval(c) for c in dataset if "exact_match" in c["criteria"]]
    passed = sum(1 for r in results if r.passed)
    return {
        "total": len(results),
        "passed": passed,
        "failed": len(results) - passed,
        "pass_rate": passed / len(results) if results else 0,
        "results": results
    }

Il pass_rate finale è la tua metrica principale. Per task di estrazione semplici dovresti mirare a superare il 95%. Sotto il 90% significa che il prompt ha problemi strutturali.

LLM-as-a-judge: valutare output complessi

Quando l’output è testuale aperto — una risposta di supporto, una spiegazione tecnica, un piano di progetto — non esiste una risposta “giusta” da confrontare. In questi casi il pattern LLM-as-a-judge è lo standard de facto: usi un modello (spesso lo stesso o uno più capace) per valutare la risposta secondo una rubrica definita.

Questo approccio è usato internamente da Anthropic, OpenAI e Google per valutare i propri modelli, e puoi replicarlo facilmente con la Claude API nel tuo workflow di sviluppo:

import anthropic
import json

client = anthropic.Anthropic()

JUDGE_PROMPT = (
    "Sei un valutatore esperto di qualita delle risposte AI.\n"
    "Valuta la seguente risposta secondo i criteri forniti.\n\n"
    "DOMANDA ORIGINALE:\n{question}\n\n"
    "RISPOSTA DA VALUTARE:\n{response}\n\n"
    "CRITERI DI VALUTAZIONE:\n{criteria}\n\n"
    "Fornisci una valutazione strutturata con:\n"
    "1. Un punteggio da 1 a 5 per ogni criterio\n"
    "2. Una spiegazione breve per ogni punteggio\n"
    "3. Un punteggio complessivo (media pesata)\n"
    "4. Un giudizio finale: PASS (>= 3.5) o FAIL (< 3.5)\n\n"
    "Rispondi SOLO con JSON valido nel formato specificato."
)

def llm_judge(question: str, response: str, criteria: list[str]) -> dict:
    # Valuta una risposta usando un LLM come giudice
    prompt = JUDGE_PROMPT.format(
        question=question,
        response=response,
        criteria="\n".join(f"- {c}" for c in criteria)
    )

    result = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        system="Rispondi sempre e solo con JSON valido, senza testo aggiuntivo.",
        messages=[{"role": "user", "content": prompt}]
    )

    judge_output = json.loads(result.content[0].text)
    return judge_output

def batch_judge_eval(dataset: list[dict], model_responses: dict) -> dict:
    # Esegue LLM-as-judge su un batch di risposte
    results = []

    for case in dataset:
        if "exact_match" in case.get("criteria", []):
            continue

        case_id = case["id"]
        response_text = model_responses.get(case_id, "")

        judgment = llm_judge(
            question=case["input"],
            response=response_text,
            criteria=case["criteria"]
        )

        results.append({
            "id": case_id,
            "judgment": judgment,
            "passed": judgment.get("final_verdict") == "PASS",
            "overall_score": judgment.get("overall_score", 0)
        })

    pass_rate = sum(1 for r in results if r["passed"]) / len(results) if results else 0
    avg_score = sum(r["overall_score"] for r in results) / len(results) if results else 0

    return {
        "total": len(results),
        "pass_rate": pass_rate,
        "avg_score": avg_score,
        "results": results
    }

Un aspetto critico dell’LLM-as-judge è il position bias: i modelli tendono a preferire la prima risposta quando valutano due alternative. Per mitigarlo, esegui ogni valutazione due volte invertendo l’ordine delle risposte e fai la media dei punteggi.

💡 Pro tip: Definisci la rubrica di valutazione PRIMA di raccogliere le risposte. Una rubrica vaga produce valutazioni inconsistenti. Specifica esempi concreti di risposta da 1, 3 e 5 per ogni criterio — il judge capisc molto meglio cosa cercare.

Regression testing: proteggere i prompt dalle regressioni

Il testing dei prompt diventa davvero utile quando lo integri nel tuo CI/CD. Ogni volta che modifichi un prompt — anche solo una virgola — dovresti rieseguire l’intera suite di eval per verificare che non ci siano regressioni. Questo è esattamente il principio del code review automatico con GitHub Actions, applicato ai prompt.

Ecco come strutturare un runner di eval completo con report:

import json
import time
from datetime import datetime
from pathlib import Path

def run_full_eval_suite(
    dataset_path: str,
    prompt_version: str,
    output_dir: str = "./eval_reports"
) -> dict:
    # Runner completo per una suite di eval con report e soglia minima
    dataset = json.loads(Path(dataset_path).read_text())
    results = {
        "prompt_version": prompt_version,
        "timestamp": datetime.now().isoformat(),
        "dataset_size": len(dataset),
        "deterministic": {},
        "llm_judge": {},
        "overall_pass_rate": 0.0,
        "duration_seconds": 0
    }

    start_time = time.time()

    # Step 1: genera risposte del modello per tutti i casi
    model_responses = {}
    for case in dataset:
        response = generate_model_response(case["input"], case.get("context"))
        model_responses[case["id"]] = response

    # Step 2: eval deterministico
    det_cases = [c for c in dataset if "exact_match" in c.get("criteria", [])]
    if det_cases:
        results["deterministic"] = run_eval_suite(det_cases)

    # Step 3: LLM-as-judge per gli altri
    judge_cases = [c for c in dataset if "exact_match" not in c.get("criteria", [])]
    if judge_cases:
        results["llm_judge"] = batch_judge_eval(judge_cases, model_responses)

    # Calcolo pass rate complessivo
    total_tests = results["deterministic"].get("total", 0) + results["llm_judge"].get("total", 0)
    total_passed = (
        results["deterministic"].get("passed", 0) +
        sum(1 for r in results["llm_judge"].get("results", []) if r["passed"])
    )
    results["overall_pass_rate"] = total_passed / total_tests if total_tests > 0 else 0
    results["duration_seconds"] = round(time.time() - start_time, 2)

    # Salva il report
    Path(output_dir).mkdir(exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    report_path = f"{output_dir}/eval_{prompt_version}_{ts}.json"
    Path(report_path).write_text(json.dumps(results, indent=2, default=str))

    # Fail il CI se pass_rate scende sotto 0.85
    if results["overall_pass_rate"] < 0.85:
        raise ValueError(
            f"EVAL FAILED: pass_rate={results['overall_pass_rate']:.1%} < 85% threshold"
        )

    return results

def generate_model_response(input_text: str, context: str | None = None) -> str:
    import anthropic
    client = anthropic.Anthropic()
    messages = [{"role": "user", "content": input_text}]
    if context:
        messages.insert(0, {"role": "user", "content": f"Contesto: {context}"})
    response = client.messages.create(
        model="claude-opus-4-5", max_tokens=1024, messages=messages
    )
    return response.content[0].text

La soglia dell’85% nel run_full_eval_suite non è casuale: è un valore ragionevole per la maggior parte dei task NLP. Per sistemi critici (compliance, medicina, finanza) alzala al 90-95%. Per prototipi early-stage, puoi scendere al 75%.

Strumenti e framework per il testing dei prompt nel 2026

Oltre al codice custom, nel 2026 esistono strumenti maturi per gestire eval a livello enterprise. Ecco un confronto rapido:

  • PromptFoo: CLI open source, perfetto per progetti individuali. Configura gli eval in YAML, supporta tutti i modelli principali, genera report HTML.
  • Braintrust: SaaS con UI grafica, ottimo per team. Tracing integrato, confronto tra versioni di prompt, score tracking nel tempo.
  • LangSmith: ecosistema LangChain, potente se usi già LangChain/LangGraph. Valutatori predefiniti per use case comuni.
  • Weave (W&B): ideale se fai anche fine-tuning. Integrazione nativa con Weights & Biases per tracking esperimenti.
  • Codice custom: massima flessibilità, zero vendor lock-in. La scelta giusta se hai use case specifici o vuoi full control.

PromptFoo merita una menzione speciale per la sua semplicità. Eccone un esempio di configurazione base:

# Installa PromptFoo
npm install -g promptfoo

# Inizializza un progetto di eval
promptfoo init

# Esegui gli eval
promptfoo eval

# Visualizza report nel browser
promptfoo view

Il file di configurazione promptfooconfig.yaml ti permette di definire provider multipli (Claude, GPT, Gemini) e confrontarli sulla stessa suite di test: essenziale quando devi scegliere quale modello usare per un task specifico, come descritto nell’articolo sul confronto tra LLM nel 2026.

Metriche chiave e come interpretarle

Raccogliere dati senza saperli interpretare non serve a nulla. Queste sono le metriche che dovresti monitorare in ogni suite di eval:

  • Pass Rate: percentuale di casi che superano la soglia definita. La metrica primaria. Monitorala nel tempo per individuare regressioni.
  • Score Distribution: non basta sapere quanti casi passano, ma come sono distribuiti i punteggi. Un pass rate del 90% con tutti i punteggi a 3.6 è molto meno robusto di uno con punteggi a 4.8.
  • Failure Modes: categorizza i fallimenti per tag (“extraction”, “summarization”, “edge_cases”). Se il 90% dei fallimenti cade su “edge_cases” sai dove concentrare il lavoro.
  • Latency P95: il 95° percentile della latenza. I prompt più lunghi e complessi possono avere latenze molto diverse dalla media.
  • Token Cost per Eval: critico per il budget. Con il prompt caching di Claude API puoi ridurre i costi degli eval fino al 90% su prompt con context fisso.

🎯 Pro tip: Implementa un sistema di “golden dataset” separato dal dataset di sviluppo. Il golden dataset non si tocca mai e serve solo per valutazioni finali prima del deploy in produzione. Sul dataset di sviluppo puoi iterare liberamente.

FAQ e Domande Frequenti

Quanti esempi servono nel dataset di eval per avere risultati affidabili?

Non esiste un numero magico, ma una regola pratica: almeno 50 esempi per task semplice con output deterministico, almeno 100-150 per task con LLM-as-judge. Sotto questi numeri la varianza statistica è troppo alta per trarre conclusioni affidabili — una differenza del 5% tra due versioni di prompt potrebbe essere rumore statistico piuttosto che un miglioramento reale. Per task critici o use case con alta variabilità degli input, punta a 300+ esempi. La qualità degli esempi conta quanto la quantità: 50 esempi ben scelti che coprono tutti i casi limite valgono più di 200 esempi banali e ripetitivi.

Come evito che il judge LLM sia biased verso risposte più lunghe?

Il verbosity bias (preferire risposte più lunghe) è uno dei problemi più noti dell’LLM-as-judge. Per mitigarlo: specifica esplicitamente nella rubrica che la lunghezza non è un criterio di qualità; includi esempi di risposte brevi e concise che ricevono punteggio massimo; valuta separatamente “completeness” e “conciseness” come criteri distinti; usa temperature=0 per il judge per ridurre la variabilità; esegui ogni valutazione più volte (3-5x) e fai la media. Alcune aziende usano anche un meta-eval: valutano il judge stesso su un piccolo dataset annotato manualmente per misurarne l’allineamento con i giudizi umani.

Come gestisco gli eval quando il modello viene aggiornato dal provider?

Gli aggiornamenti dei modelli — anche quelli “minori” — possono causare regressioni inattese. La strategia corretta è versioning aggressivo: fissa sempre la versione esatta del modello (es. claude-opus-4-5 invece di claude-latest), mantieni gli ultimi 3-5 report di eval come baseline di riferimento, e riesegui l’intera suite prima di aggiornare la versione del modello in produzione. Se un aggiornamento causa regressioni, hai tre opzioni: adattare il prompt per il nuovo modello, restare sulla versione precedente, o accettare la regressione se i miglioramenti in altri ambiti la compensano. Documenta sempre questa decisione.

Qual è il costo tipico di una suite di eval completa?

Dipende fortemente dal numero di casi e dall’uso di LLM-as-judge. Per una suite di 100 esempi con LLM-as-judge su Claude Opus, aspettati circa 0.50-2.00 USD per run completo (ogni caso richiede due chiamate API: una per la risposta e una per il giudizio). Per ridurre i costi: usa modelli più economici per la generazione delle risposte e uno più capace solo per il giudizio; sfrutta il prompt caching per i prompt di sistema fissi; esegui eval completi solo su commit significativi, non su ogni push. Con un buon setup, puoi mantenere i costi di eval sotto i 50 USD al mese anche per team con decine di iterazioni giornaliere.

Conclusione

Il testing dei prompt non è un lusso riservato alle big tech: nel 2026 è una pratica fondamentale per chiunque costruisca applicazioni AI in produzione. Senza un sistema di eval non sai se i tuoi prompt stanno migliorando o degradando, non puoi fare deploy con confidenza, e non hai dati per giustificare le scelte tecniche al cliente o al team.

Inizia in piccolo: un dataset di 50 esempi, un semplice script Python per l’eval deterministico, e un LLM-as-judge per i casi complessi. Una volta che il framework è in piedi, ampliarlo costa pochissimo. L’investimento iniziale di qualche ora ti risparmierà settimane di debugging su comportamenti inattesi in produzione.

Il passo successivo naturale è integrare questi eval nel tuo CI/CD e automatizzare il reporting — ma questa è una storia per il prossimo articolo.

Suggerimenti e Risorse

🔧 PromptFoo: Il tool open source più usato per il testing dei prompt nel 2026. promptfoo.dev — supporta Claude, GPT, Gemini e modelli locali con Ollama.

💡 Anthropic Evals: Il repository ufficiale di Anthropic con centinaia di eval benchmark open source. Ottimo punto di partenza per capire come strutturare dataset di qualità. github.com/anthropics/evals

🎯 RAGAS: Framework specifico per valutare sistemi RAG. Metriche predefinite per faithfulness, answer relevancy e context precision. Perfetto in combinazione con RAG con Claude API.

Condividi

Articoli Recenti

Categorie popolari