Pipeline di testing e valutazione automatizzata che ho costruito end-to-end per Camilla, il chatbot di assistenza ai bandi distribuito a livello nazionale per la Pubblica Amministrazione italiana. Il cuore è un valutatore orientato ai task: uno scheduler basato su DAG con regole di dipendenza dichiarate in PostgreSQL e worker FastAPI che scalano orizzontalmente consentendo di testare in parallelo il comportamento dell’assistente su più bandi. Il volume di dati aggiunti al chatbot e da testare coincide con tutti i bandi pubblici pubblicati su INPA, circa 100 bandi nuovi al giorno.

L’orchestrazione dei task

Ho scelto deliberatamente la strada più semplice che reggesse i requisiti, senza integrare framework di workflow: Postgres è l’unico punto di contatto del sistema. Il container che sottomette la run e i worker che eseguono i task non parlano tra loro: tutti leggono e scrivono lo stato sul database, e le dipendenze del DAG vengono risolte da una funzione SQL che sblocca i task man mano che i prerequisiti si completano. Ogni task ha retry, timeout e un ciclo di vita esplicito (in coda, in esecuzione, completato, fallito, annullato), e le risorse si governano nel modo più prosaico possibile: docker compose –scale per aggiungere o togliere worker. Un minore overhead nell’orchestrazione rende più facile capire perché una run si è fermata e farla ripartire.

avvio run (Docker)              worker FastAPI × N
                               (docker compose --scale)
     |                                 ^    |
     v                                 |    v
Postgres: coda, stato task, DAG in funzione SQL
     |
     |  per bando: evidenze --> verdetto ok/ko
     |  globali: red-teaming, diagnostica Weaviate
     v
dashboard + email al servizio

Se un task fallisce, quelli sani vengono comunque portati a termine prima di fermare la run: rilanciando la run, verranno automaticamente ripresi solo i task falliti.

In fondo alla catena, costo e latenza di ogni run sono tracciati in una dashboard che consente al servizio di governare e monitorare le performance del chatbot su centinaia di nuovi documenti al giorno. È il punto in cui i dati e le valutazioni diventano governabilità e osservabilità reali del servizio, e non solo un verdetto su una singola run.

Criteri e metodi di valutazione

Nell’approcciare questo progetto mi sono scontrato con il problema che affligge buona parte del campo dell’evaluation: valutare il comportamento di un sistema così espressivo senza poter definire criteri di successo netti, o quantomeno verificabili in modo deterministico. Ho quindi provato a scomporre il problema, ponendomi ogni volta due domande: che cosa, in questo scenario, posso ancora verificare in modo deterministico? E per tutto il resto, con quale mandato metto un modello a giudicare?

Quello che resta deterministico

Alla prima domanda si risponde sfruttando i vincoli del dominio e dello scenario, cioè usando a mio vantaggio tutte le caratteristiche note dell’agente da valutare. Un agente usa dei tool, e i tool hanno una firma e una semantica precise: se pongo una domanda di contenuto su un bando, mi aspetto che l’assistente invochi un tool leggi_bando o simili. Tutte queste aspettative sono diventate regole deterministiche da applicare agli scenari di test. Se nella suite stiamo ponendo una domanda di contenuto, un segnale di fallimento chiaro e non negoziabile è che il chatbot risponda senza consultare il tool.

Lo stesso vale per il dominio. I bandi hanno caratteristiche comuni, note agli esperti, e questo permette di predeterminare una serie di domande che in qualsiasi bando devono per forza di cose trovare risposta: l’assenza di una risposta è di per sé un fallimento, senza bisogno di alcun giudizio.

Su questo stesso principio poggiano due scenari in cui un LLM entra in gioco, ma non come giudice: serve a costruire il caso di prova, mentre il controllo resta deterministico.

  • Conversazioni multi-turno con cambio dinamico di ambito. L’assistente dialoga con un utente simulato da un LLM che alterna domande specifiche su un bando e domande generali (FAQ). Lo scenario verifica che a ogni cambio di ambito (bando a → bando b, oppure bando → FAQ) l’assistente utilizzi sempre i tool giusti, dimostrando di saper tenere l’argomento senza rimanere incastrato nel bando precedente.
  • Controlli sul comportamento del motore di ricerca. Query di ricerca realistiche (per esempio “bandi per neolaureati a Milano”) formulate da un LLM a partire dai requisiti e dai metadati del bando. Il modello che costruisce il caso di prova determina anche quale sia l’esito corretto della query, positivo (il bando deve essere trovato) o negativo (il bando non deve comparire tra i risultati).

Resta però la domanda scomoda: come si distingue “una risposta” da una risposta fondata sui fatti? Qui il controllo deterministico non arriva.

Il mandato del giudice

Per verificare l’aderenza delle risposte a quanto realmente riportato nel bando, e la tenuta sotto scenari di attacco, ho fatto uso della metodologia LLM-as-a-judge: un modello separato valuta le risposte dell’assistente secondo un mandato ben definito, in cui sono indicate le metriche o quantomeno la semantica di che cosa significhi “successo”. La qualità della valutazione dipende quasi per intero dalla precisione di quel mandato, per cui ne ho scritto uno diverso per ogni tipo di test.

  • Pseudo-ground-truth. Fatti verificabili estratti dai documenti sorgente e ri-interrogati sul bot: l’assistente non vede la risposta esatta, il giudice sì, e deve valutare quanto la risposta dell’assistente vi aderisca.
  • Domande boilerplate. Le domande generiche di cui sopra. Una volta accertato in modo deterministico che una risposta c’è stata, al giudice resta da valutarne tono, forma e completezza rispetto alla domanda posta, senza confronto con la fonte originale.
  • Scenari di attacco. Al giudice vengono forniti la risposta dell’assistente e il copione dell’attacco, cioè la descrizione del comportamento che l’attaccante stava cercando di ottenere (per esempio indurre l’assistente a rivelare il proprio system prompt).

Restano infine alcuni test di carattere più diagnostico che comportamentale, come i controlli di qualità sulle collezioni di embedding: si verifica che non vi siano troppi vettori nulli, sintomo di anomalie nell’ingestion, e che la silhouette della collezione resti entro valori ammissibili. Quest’ultima metrica segnala se i vettori dei bandi sono troppo schiacciati, il che può indicare problemi nell’embedding o nella strategia di chunking e si traduce in un degrado della qualità di ricerca.

Red-teaming: gli scenari di attacco

Gli scenari di attacco sono il banco di prova più importante dell’intera suite. Sopra questa infrastruttura ho progettato 200+ scenari multi-turno per la tenuta del ruolo istituzionale sotto pressione avversariale: resistenza a prompt injection e jailbreak, protezione dei confini informativi, equità e non discriminazione, integrità temporale, recupero del comportamento di base dopo l’attacco.

Non è nato così. All’inizio questo scenario di test era gestito manualmente, da una componente di piattaforma che inviava le domande al chatbot e verificava che ogni tentativo di attacco o manipolazione fosse respinto. Esaminare quel punto di partenza mi ha portato a due conclusioni, ed è da queste che deriva la tassonomia attuale.

Scenari di rischio specifici per la PA

La maggior parte degli scenari già preparati si concentrava sulla tenuta del tono e del mandato dell’assistente, non su veri attacchi o prompt injection. Per quanto un fallimento su un prompt injection sia catastrofico, specialmente per aziende in cui l’assistente è la porta d’ingresso a dati sensibili, nel caso di Camilla il rischio più grande era di natura reputazionale (un assistente che parla per rime, come un pirata, o che esprime pareri discriminatori sul portale nazionale dei bandi della PA) e informazionale (indurlo a inventare fatti, a fare ricerche casuali o comportamenti simili).

Fare red-teaming significa quindi anche sapere come si configura il rischio nel dominio specifico in cui si opera. Quei test hanno conservato tutta la loro rilevanza anche quando l’impianto manuale è stato assorbito dalla suite.

Attacchi single-turn o multi-turn?

Il secondo punto riguardava la natura dei test: erano quasi tutti a singolo messaggio, un tentativo di attacco e la risposta dell’assistente. Ma i casi reali di manipolazione, emersi dal monitoraggio continuo delle risposte durante i nostri test, mostravano che l’assistente cedeva raramente alla prima richiesta, e molto più spesso dopo due o tre varianti, oppure alternando e mescolando richieste legittime e malevole.

Ho quindi costruito il red-teaming attorno agli attacchi multi-turno, sia per gli scenari “istituzionali” descritti sopra, sia per quelli più tecnici: prompt injection, omoglifi, caratteri invisibili.

La tassonomia che ne è risultata è di mia progettazione, derivata dalle interazioni in produzione e dai casi specifici del dominio, e integrata con la OWASP Top 10 per le applicazioni LLM.

200+ scenari avversariali
~100/giorno nuovi bandi testati
OWASP Top 10 LLM nella tassonomia

Entity extraction e verifica dei metadati

Sorprendentemente, uno degli aspetti più difficili da centrare durante la progettazione e il test di questa pipeline è stata la verifica dei metadati. Come spiegato nella pagina dedicata al motore di ricerca, il funzionamento e2e di Camilla si basa sulla corretta metadatazione di ciascun bando: quali titoli di studio e requisiti prevede, sedi lavorative e altre informazioni. Queste informazioni sono già state associate ai bandi nel momento in cui vengono testati dal valutatore, ma imprecisioni e omissioni rispetto a quanto effettivamente scritto nel testo del bando erano difficili da intercettare a monte. Perciò, ho aggiunto un task specifico per fare entity extraction di tutti i metadati dal testo del bando e confrontare quanto estratto con il set di metadati già presente sul bando, segnalando ogni discrepanza.

Le ambiguità dei dati reali

Le difficoltà sono emerse quando ho dovuto fare i conti con i dati reali: i bandi pubblici sono documenti incredibilmente eterogenei, ma soprattutto le entità desiderate (geografiche, titoli di studio) comparivano spessissimo con significati diversi da quello atteso. Ad esempio, un nome di città dove si sarebbero svolte le prove scritte (ma non relativo ad una sede di lavoro), o titoli di studio che costituivano titolo preferenziale (più punti in graduatoria), ma che non erano “necessari”. Inoltre, i dati erano spesso contenuti in tabelle di pagine e pagine, e comparivano nei formati più svariati (specialmente per i titoli di studio): con codice laurea, senza codice, con o senza ambito di studio, in elenchi spezzati o che indicavano equipollenze.

Come funziona l’estrazione

È stato quindi fondamentale un approccio a più step, individuando prima gli articoli del bando che contenevano informazioni su requisiti, sedi o profili. Fatto ciò, proseguiamo con regex ovunque possibile, per estrarre una serie di “isole” di testo (non troppo sconnesse tra loro, in modo da conservare il contesto circostante), e poi procediamo infine con l’entity extraction, i cui system prompt sono stati rivisti decine di volte, in modo da esplicitare ogni regola di business necessaria. Il confronto tra metadati estratti e associati al bando attualmente viene fatto il più possibile in maniera deterministica (es. eliminando da ambo i lati i titoli di studio a parità di codice laurea e con altre regex), per lasciare solo i veri casi ambigui all’LLM.

Una prima descrizione della pipeline è nell’articolo su Camilla; un articolo dedicato al red-teaming e alla valutazione comportamentale approfondisce il versante sicurezza.