Il chatbot legale che si credeva un negozio di dischi

13/09/20266 min read
Il chatbot legale che si credeva un negozio di dischi

Risposta diretta: un guardrail anti-allucinazione è codice specifico di dominio, non una misura di sicurezza universale. Applicato al dominio sbagliato produce un bug che sembra un problema di prompt ma è un problema di instradamento: nel mio caso, una regola pensata per impedire a un negozio online di inventare prezzi ha fatto rifiutare a un assistente giuridico una domanda legittima su un progetto pubblico, con una frase da commesso. La lezione: ogni guardrail va scopato al verticale per cui è stato scritto, sempre.

TL;DR

  • Il motore multi-tenant nasce per e-commerce. Un suo guardrail intercetta parole come «costo», «prezzo», «disponibilità», «edizione» e inietta l'istruzione «PRIMA chiama lo strumento che verifica il magazzino».
  • Quella regola era cablata per tutti i tenant, non solo per chi vende.
  • Su un tenant giuridico lo strumento di verifica magazzino non esiste.
  • Il modello riceveva un'istruzione impossibile da eseguire e la restituiva all'utente come un rifiuto, parola per parola.
  • Il sintomo (un rifiuto commerciale a una domanda legale) puntava nella direzione sbagliata: sembrava il prompt, era il codice.
  • Correzione strutturale: i guardrail si attivano per verticale, e ogni guardrail che nomina uno strumento deve verificare che quello strumento esista per quel tenant.

Cosa è successo

Il contesto: un motore conversazionale multi-tenant nato per l'e-commerce — negozi di dischi, hi-fi, articoli da collezione — dentro cui ho migrato un prodotto completamente diverso: un assistente che risponde su leggi, progetti pubblici e dati parlamentari.

A un certo punto, alla domanda su un progetto PNRR da 864.000 euro, l'assistente giuridico ha risposto più o meno così:

«Non posso fornire informazioni su prezzi, disponibilità o edizioni senza consultare il magazzino.»

Su una domanda che riguardava una delibera pubblica. Nessun magazzino da nessuna parte.

Perché sembrava un problema di prompt (e non lo era)

La prima ipotesi di chiunque, davanti a una risposta così, è: il modello ha allucinato un ruolo sbagliato. Il tono è da commesso, il vocabolario è da negozio, la conclusione è un rifiuto — sembra il classico caso di system prompt sporco o contaminato.

Non lo era. Il modello stava eseguendo alla lettera un'istruzione che il motore gli aveva iniettato dinamicamente, un attimo prima, senza che nessuno l'avesse scritta nel prompt della persona.

Il motore aveva un guardrail anti-allucinazione perfettamente sensato nel suo contesto d'origine: una regex che intercetta nel messaggio dell'utente le parole che segnalano una domanda commerciale — costo, prezzo, disponibilità, edizione — e, quando le trova, aggiunge al contesto un'istruzione perentoria: prima di rispondere, chiama lo strumento che verifica la disponibilità reale.

È esattamente ciò che serve a un negozio di dischi, dove un modello che si inventa il prezzo di una prima stampa è un danno commerciale concreto.

Il problema è che la parola «progetto da 864.000 euro» contiene il concetto di costo. La regex ha fatto il suo lavoro. Ha iniettato l'istruzione. E lo strumento che quell'istruzione ordinava di chiamare non esiste per un tenant giuridico.

Il modello si è trovato con un ordine impossibile da eseguire. Ha fatto la cosa più prevedibile: ha comunicato all'utente il motivo per cui non poteva procedere, usando le parole che aveva a disposizione — quelle dell'istruzione stessa.

L'errore di progettazione, in una riga

Il guardrail era giusto. Era nel posto sbagliato.

Livello Dove stava Dove doveva stare
Rilevamento intento commerciale Globale, tutti i tenant Solo tenant con catalogo
Istruzione «verifica disponibilità» Globale, tutti i tenant Solo tenant con lo strumento
Verifica esistenza dello strumento Assente Precondizione dell'iniezione

Tre righe, e la terza è quella che mancava del tutto: nessuno controllava che lo strumento nominato dall'istruzione esistesse davvero per quel tenant. Il guardrail dava per scontato un mondo — «se sei qui, vendi qualcosa» — che era vero quando è stato scritto e ha smesso di esserlo appena il motore ha ospitato un prodotto diverso.

La lezione: i guardrail sono codice di dominio

È la parte che vale oltre questo caso specifico.

Tendiamo a pensare ai guardrail come a uno strato di sicurezza generale, tipo la validazione degli input o il rate limiting: qualcosa che si mette una volta e vale per tutti. Non lo sono. Un guardrail codifica un'assunzione su cosa sia vero nel dominio: «qui si vendono cose», «qui esiste un inventario», «qui una risposta sbagliata sul prezzo è un danno».

Quelle assunzioni sono precisamente ciò che cambia quando un motore diventa multi-tenant e ospita verticali diversi. E cambiano in silenzio, perché nessuno rilegge i guardrail quando aggiunge un cliente nuovo.

Tre regole pratiche che ne ho ricavato:

  • Ogni guardrail dichiara il suo dominio. Se non sai dire per quale verticale è stato scritto, non sai dove smette di valere.
  • Un'istruzione che nomina uno strumento deve verificare che lo strumento esista per quel contesto. Altrimenti stai chiedendo al modello di fare l'impossibile, e il modello lo dirà all'utente al posto tuo.
  • Il sintomo mente sistematicamente. Un rifiuto in linguaggio naturale sembra sempre un problema di prompt, perché il prompt è l'unica cosa fatta di linguaggio. Quasi sempre è l'instradamento del codice.

Questo è anche il motivo per cui, quando valuto un motore conversazionale da riusare su un dominio nuovo, la prima cosa che chiedo non è «quali modelli supporta» ma «quali assunzioni sul dominio ci sono cablate dentro». È una domanda che quasi nessun fornitore si aspetta, e le risposte sono istruttive.

FAQ

Cos'è un guardrail anti-allucinazione in un chatbot?

È una regola che intercetta certe richieste — tipicamente quelle in cui una risposta inventata farebbe danno, come prezzi o disponibilità — e forza il modello a verificare il dato con uno strumento reale prima di rispondere, invece di generarlo dal proprio addestramento.

Perché un guardrail può diventare un bug?

Perché codifica un'assunzione sul dominio. Quando lo stesso motore ospita clienti di settori diversi, l'assunzione smette di essere vera per alcuni di loro: la regola continua a scattare, ma l'azione che impone non ha più senso — o non è nemmeno eseguibile.

Come si riconosce che il problema è nel codice e non nel prompt?

Controllando se l'istruzione che il modello sta seguendo compare davvero nel prompt configurato. Se il testo del rifiuto assomiglia a un'istruzione di sistema che nessuno ha scritto nella configurazione, qualcosa la sta iniettando dinamicamente: cerca lì.

Vale solo per i motori multi-tenant?

È lì che esplode, ma il principio vale sempre: ogni guardrail ha un dominio di validità. Anche in un prodotto singolo, quando aggiungi una funzionalità che esce dal perimetro originale, le regole scritte per il perimetro vecchio ti seguono.


Se stai valutando di riusare un motore conversazionale per un dominio diverso da quello per cui è nato, parliamone: le assunzioni cablate sono il costo nascosto di quella scelta, ed è meglio scoprirle prima. Vedi anche cosa ho imparato mettendo MCP in produzione.

AIChatbotBehind the scenes

Scritto da Giulio Garofalo