Da una distinta sbagliata a un eval set: misurare invece di sperare
Un chatbot tecnico interno restituisce cinque codici inventati su sei. Il fix funziona, ma il vero salto è quando smetti di sperare che i fix reggano e inizi a misurare con un eval set.
Sto lavorando a un chatbot tecnico interno per la PMI dove sono dipendente: assiste i nostri tecnici nella scelta di componenti di catalogo del fornitore principale. Funziona — finché non funziona.
Settimana scorsa una distinta articoli è uscita con cinque codici inventati su sei. Solo il primo era reale; gli altri avevano i pattern giusti della nomenclatura del fornitore ma erano puro frutto di hallucination del modello.
La diagnosi non era ovvia
Ispezionando la sessione: zero blocchi tool_use nella risposta finale. Il bot, pur avendo a disposizione un tool dedicato che restituisce i codici verificati dal database interno, ha generato la distinta a memoria. Il system prompt aveva una direttiva testuale ("DEVI chiamare il tool"), ma il modello fast (Haiku 4.5) l'ha ignorata.
In più, due bug concorrenti nel "gate" che decide quando iniettare il template canonico nel contesto. Quando l'utente forniva tutti i dati chiave, il sistema saltava l'iniezione del template. Il modello si trovava senza i pattern dei codici e senza un vincolo hard sul tool. Il classico "sbaglia perché l'abbiamo lasciato senza armi".
Le pezze
Quattro fix in sequenza, tutti deployati:
Iniezione del template anche con dati chiave completi. tool_choice forzato API-side (da invito testuale a costrizione). Forzatura del modello smart quando un template è in contesto. Alias del normalizer per coprire varianti che sfuggivano al regex.
Test manuale: i codici sono giusti. Tutto bene.
Il vero salto
A questo punto la persona con cui lavoravo ha detto la frase giusta: "ma stiamo procedendo per tentativi, mettendo pezze caso per caso". Aveva ragione. Senza una rete di sicurezza, ogni modifica futura è un atto di fede.
Cambio di marcia: scriviamo un eval set. Ventitre casi di test ripetibili, distribuiti su sette categorie (lookup, distinte per varie famiglie, regole tecniche, gate logic, edge case). Runner Python, ~5 minuti per il run completo, ~$0.30 di API.
Il primo run ha sorpreso: espandendo da 5 a 15 casi sono emersi quattro bug reali mai visti prima:
- una regex hardcoded su due valori specifici che bloccava silenziosamente tre famiglie prodotto su quattro
- un fallback del normalizer che mappava una variante alla famiglia sbagliata
- un termine letterale nel router invece della radice (matchava "calcolo" ma non "calcola")
- un'assertion mia troppo stretta
Fix: tre one-liner. Workflow: test fail → fix → test pass → deploy.
Cosa ho imparato
Tre cose che voglio ricordare:
- Il vero context engineering non è scrivere prompt lunghi, è decidere quali risorse iniettare quando: push vs pull, gate logic, model routing. Il modello sbagliato per il task sbagliato è una causa concomitante con un contesto incompleto.
- La pezza non è una parolaccia se è blindata da un test. Diventa tossica quando aggiunge dati senza una single source of truth — esempio: ho ancora le altezze degli stessi componenti scritte a mano in tre file diversi, e prima o poi divergeranno. Quello sì che è debt.
- Senza eval set, ogni modifica è speranza. Un'ora investita per costruire 23 casi ha trovato bug che mesi di sessioni manuali non avevano rilevato.
Il prossimo passo è il refactor "single source of truth". Ma adesso ho la rete di sicurezza per farlo senza paura.