Home/ Corsi/ Agenti AI sovrani/ Lezione 4: Il registro e il cancello

Lezione 4: Il registro e il cancello

Facciamo girare l'agente decine di volte e contiamo. Quello che salta fuori è il motivo per cui un agente in azienda non si lascia mai senza registro, e perché dare i conti al codice non basta.

Gli errori di un agente AI misurati
Indice dei contenuti
Lezione 4 di 5 · Costruire Esperimento + Laboratorio + Mini-test

Obiettivi della lezione

Nella Lezione 3 l'agente ha letto tre contratti e ha scritto una nota. Una prova sola dice poco: un agente che lavora tutti i giorni fa lo stesso compito centinaia di volte, e quello che conta è quante volte sbaglia e come. In questa lezione lo facciamo girare decine di volte su due modelli, contiamo guardando il registro e la cartella e non la risposta, e cambiamo il modo in cui il lavoro è diviso fra il modello e il codice finché i conti non tornano.

Alla fine della lezione riesci a:

  • Misurare un agente su decine di prove di un compito con le risposte note;
  • Riconoscere i due errori tipici di un agente: dire di aver fatto una cosa che non ha fatto, e ricopiare male un risultato giusto;
  • Spiegare perché dare al modello uno strumento di calcolo non basta, con i numeri;
  • Dividere il compito in modo che il modello legga e il codice calcoli e scriva;
  • Mettere il cancello nel punto in cui una persona riesce davvero a controllare.

Come ho fatto la prova

Il compito è quello del laboratorio della Lezione 3: tre contratti, la richiesta di scrivere una nota con le scadenze e l'ultimo giorno utile per la disdetta. Le risposte giuste sono note, e due sono quelle che contano: per le pulizie la disdetta va mandata entro il 2 dicembre 2026, per il gestionale entro il 14 dicembre 2026. Il noleggio non ha disdetta.

Ho fatto girare l'agente l'11 ottobre 2026 su un Mac con chip M5 e 24 GB di memoria, con llama-server, temperatura 0,2 e due modelli quantizzati a 4 bit:

  • Ministral 3 8B, il modello del corso, Apache 2.0;
  • Qwen3-4B-Instruct-2507, un modello più piccolo di Alibaba, Apache 2.0, come termine di paragone.

Le versioni dell'agente sono tre: senza lo strumento giorni_prima, quindi con i conti fatti dal modello; con lo strumento, cioè il codice della Lezione 3; e divisa, che vediamo più sotto. Per contare non ho mai letto la risposta finale dell'agente. Ho guardato due cose sole: il file in uscita/, per sapere se la nota c'è e che date contiene, e il registro, per sapere quali strumenti ha chiamato e che cosa gli hanno restituito. Il cancello rispondeva sempre sì, perché qui si misurava l'agente e non chi lo controlla.

Che cosa è successo

Modello e versioneProveNota scrittaDisdetta pulizie giustaDisdetta gestionale giustaDice di aver scritto senza farlo
Ministral 3 8B, senza strumento di calcolo10100100
Ministral 3 8B, con giorni_prima20200120
Ministral 3 8B, versione divisa101010100
Qwen3-4B, senza strumento di calcolo2000017
Qwen3-4B, con giorni_prima2013376
Qwen3-4B, versione divisa200000 (20 chiamate scritte come testo)

Le righe da guardare sono tre. Ministral con lo strumento chiama giorni_prima in tutte le venti prove e riceve sempre le date giuste, eppure la disdetta delle pulizie nella nota è sbagliata venti volte su venti, e quella del gestionale va peggio della versione senza strumento: 12 su 20 contro 10 su 10. Nella versione divisa Ministral scrive tutte e due le date giuste in tutte le prove. E Qwen, senza lo strumento di calcolo, non scrive mai la nota e diciassette volte su venti dice di averla scritta. Le righe di Ministral senza strumento e in versione divisa hanno dieci prove, perché ogni prova dura mezzo minuto e il risultato era già netto.

Primo errore: il conto giusto, ricopiato sbagliato

Questo è il registro di una delle prove di Ministral con lo strumento. Le tre chiamate a giorni_prima hanno gli argomenti giusti e il codice restituisce le date giuste:

{"nome": "giorni_prima", "argomenti": {"data": "2027-03-14", "giorni": "90"}, "esito": "2026-12-14"}
{"nome": "giorni_prima", "argomenti": {"data": "2027-01-31", "giorni": "60"}, "esito": "2026-12-02"}
{"nome": "giorni_prima", "argomenti": {"data": "2027-06-30", "giorni": "0"}, "esito": "2027-06-30"}

Subito dopo il modello scrive la nota, e la riga delle pulizie dice:

- **Contratto di Pulizia** (Brilla S.n.c.)
  - Data di scadenza: 31 gennaio 2027
  - Ultimo giorno utile per la disdetta: 2 febbraio 2027

Il giorno è giusto, il 2, e il mese e l'anno sono sbagliati. Nelle altre prove la stessa riga diceva «1 novembre 2026» o «2 febbraio 2026»: in venti prove su venti la data delle pulizie scritta nella nota non era quella che il codice aveva calcolato. È l'errore più pericoloso che c'è, perché a colpo d'occhio la nota sembra perfetta: ha la forma giusta, le date sono date vere, il tono è sicuro. Chi la legge di corsa la approva.

Il motivo sta nel modo in cui lavora un modello linguistico, che la voce Cos'è un LLM spiega con le probabilità dei token: quando scrive la nota non copia il risultato dello strumento, lo riscrive, un pezzo per volta, scegliendo la continuazione più probabile. Una data in formato 2026-12-02 da trasformare in «2 dicembre 2026» è un'occasione per sbagliare a ogni passaggio.

Un controllo che il codice sa fare

«2 febbraio 2027» come ultimo giorno per disdire un contratto che scade il 31 gennaio 2027 è impossibile: la disdetta viene dopo la scadenza. Una persona stanca non se ne accorge, una riga di codice sì. È il tipo di controllo che conviene scrivere sempre: non serve sapere la risposta giusta, basta sapere che cosa non può essere vero.

Secondo errore: dire di aver fatto

Qwen3-4B, il modello più piccolo, sbaglia in un altro modo. Nella versione senza strumento di calcolo legge i tre contratti, mette nella risposta una nota con le date e chiude così:

- **Contratto pulizie**:
  Scadenza: 31 gennaio 2027.
  Ultimo giorno utile per la disdetta: 15 ottobre 2026 (60 giorni prima della scadenza).
[...]
La nota è stata salvata nel file `scadenze.md`.

Nel registro scrivi_nota non compare, e la cartella uscita è vuota. In venti prove non ha mai scritto la nota, e diciassette volte ha detto di averlo fatto. La data, per giunta, è sbagliata: sessanta giorni prima del 31 gennaio è il 2 dicembre.

Con lo strumento di calcolo va un po' meglio, 13 note su 20, ma giorni_prima lo chiama in una prova sola e le date giuste arrivano 3 volte su 20 per le pulizie e 7 per il gestionale. Nella versione divisa succede una cosa ancora diversa: i dati che estrae sono giusti, le tre scadenze e i tre preavvisi, ma invece di chiamare lo strumento scrive la chiamata come testo nella risposta, senza la cornice che il server riconosce. In venti prove su venti lo strumento non parte e non viene scritto niente.

Un modello si sceglie anche per come chiama gli strumenti

Qwen3-4B legge bene i contratti e chiama male gli strumenti, e in un agente la seconda cosa conta quanto la prima. La chiamata scritta come testo si potrebbe recuperare con tre righe che cercano il JSON nella risposta, ma sarebbe una toppa sopra a un modello inaffidabile in quel punto. Su questo compito Ministral 3 8B ha fatto la chiamata giusta in tutte e quaranta le prove, ed è il motivo per cui il corso usa lui.

La cura: il modello legge, il codice calcola e scrive

Il primo errore dice una cosa precisa: dare al modello uno strumento per i conti non basta, se poi il testo finale lo ricompone lui. La divisione va portata fino in fondo. Il modello fa la parte che sa fare, cioè leggere un contratto in italiano e tirarne fuori due dati: la data di scadenza e i giorni di preavviso. Il codice fa tutto il resto: toglie i giorni, scrive la data in lettere, compone la nota.

È il secondo movimento del metodo Divide et Delega applicato dentro all'agente: il passo «scrivi la nota delle scadenze» sembrava uno solo, ed erano tre, uno di lettura, uno di calcolo e uno di scrittura a regola fissa. Solo il primo è giudizio.

Il codice della versione divisa, da copiare in diviso.py accanto ad agente.py
# La versione divisa: il modello estrae le date, il codice calcola e scrive.
# Si mette accanto ad agente.py e si lancia al suo posto.
import sys
import agente
from agente import giorni_prima, scrivi_nota

MESI = ["gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio",
        "agosto", "settembre", "ottobre", "novembre", "dicembre"]

def in_lettere(iso):
    a, m, g = iso.split("-")
    return f"{int(g)} {MESI[int(m) - 1]} {a}"

def scrivi_scadenze(contratti):
    righe = ["# Scadenze dei contratti", ""]
    for c in contratti:
        giorni = int(c.get("giorni_preavviso") or 0)
        disdetta = ("nessuna disdetta da mandare" if giorni == 0 else
                    "disdetta entro il " + in_lettere(giorni_prima(c["scadenza"], giorni)))
        righe.append(f"- {c['nome']}: scade il {in_lettere(c['scadenza'])}, {disdetta}")
    return scrivi_nota("scadenze.md", "\n".join(righe) + "\n")

agente.STRUMENTI = {"elenca_documenti": agente.elenca_documenti,
                    "leggi_documento": agente.leggi_documento,
                    "scrivi_scadenze": scrivi_scadenze}
agente.SCHEMI = agente.SCHEMI[:2] + [{"type": "function", "function": {
    "name": "scrivi_scadenze",
    "description": ("Scrive la nota delle scadenze. Per ogni contratto passa il nome, la data "
                    "di scadenza (AAAA-MM-GG) e i giorni di preavviso per la disdetta "
                    "(0 se non serve). La data di disdetta la calcola lo strumento."),
    "parameters": {"type": "object", "required": ["contratti"], "properties": {
        "contratti": {"type": "array", "items": {"type": "object",
            "required": ["nome", "scadenza", "giorni_preavviso"],
            "properties": {"nome": {"type": "string"}, "scadenza": {"type": "string"},
                           "giorni_preavviso": {"type": "integer"}}}}}}}}]
agente.ISTRUZIONI = ("Lavori solo con gli strumenti che hai. Leggi i documenti prima di "
                     "rispondere e non inventare quello che non c'e' scritto. La nota la "
                     "scrive solo scrivi_scadenze: tu leggi le date, i conti li fa lo strumento.")

if __name__ == "__main__":
    print(agente.agente(" ".join(sys.argv[1:]) or "Che documenti ci sono?"))

Lo strumento nuovo, scrivi_scadenze, non riceve un testo: riceve un elenco di contratti, ognuno con nome, scadenza e giorni di preavviso. Il modello non può scrivere una data di disdetta nemmeno volendo, perché nello schema quel campo non esiste. Si lancia come l'altro:

python3 diviso.py "Leggi i contratti nella cartella e scrivi una nota scadenze.md con, per ogni contratto, la data di scadenza e l'ultimo giorno utile per la disdetta."

Con Ministral 3 8B la versione divisa ha scritto la nota in dieci prove su dieci, con tutte e due le date giuste ogni volta, mentre la versione della Lezione 3 arrivava a zero su venti. In più è più veloce, 29,5 secondi a prova contro 36 (mediane dal registro), perché il modello scrive solo un elenco di dati. Con Qwen3-4B non ha funzionato, per il motivo visto sopra: estrae bene e chiama male.

Dove mettere il cancello

Nella versione della Lezione 3 il cancello mostra la nota finita e chiede «Confermi?». Abbiamo appena visto che una nota sbagliata sembra uguale a una giusta, quindi quel cancello protegge poco: chiede a una persona di rifare il controllo che il modello ha sbagliato, senza darle niente in mano per farlo.

Nella versione divisa il punto da controllare è un altro: i dati che il modello ha estratto, cioè per ogni contratto una data di scadenza e un numero di giorni. Quelli una persona li confronta con il contratto in pochi secondi, perché sono scritti lì. Il calcolo che viene dopo è del codice e non va ricontrollato. Il cancello buono mostra quello che si può verificare guardando la fonte, e niente di più.

Tre regole per il cancello

Sta davanti a ogni strumento che fa, e mai davanti a quelli che guardano, sennĂ² dopo una settimana si preme sì senza leggere. Mostra i dati estratti accanto alla fonte, e non il risultato finale. E quando una persona dice no, quel no finisce nel registro con il motivo: è il materiale con cui migliori le descrizioni degli strumenti.

Il registro come prova

Tutto quello che abbiamo scoperto in questa lezione viene dal registro. La risposta finale dell'agente, letta da sola, presentava la nota come fatta anche in ventitré prove in cui la nota non esisteva. E le quarantadue note scritte con almeno una data sbagliata si scoprivano solo confrontandole con il contratto o con il registro. Senza registro avremmo misurato la fiducia del modello in sé stesso.

Per questo in un agente che lavora davvero il registro non è un accessorio. Dice che cosa ha letto, che cosa ha calcolato, che cosa ha scritto e chi l'ha autorizzato. Al livello 4 della scala sta sul tuo disco, e lo leggi solo tu. Nella Lezione 5 vediamo che per certi sistemi è anche quello che chiede la legge.

Laboratorio: misura il tuo agente

Obiettivo: la tabella della sezione «Che cosa è successo» rifatta con il tuo modello, e una riga su quale versione terresti.

Consegna

  1. Copia conta.py dal blocco qui sotto, accanto ad agente.py e diviso.py.
  2. Lancia venti prove della versione della Lezione 3 con python3 conta.py 20. Con un modello da 8 miliardi su un portatile ci vuole una mezz'ora: lascialo andare e fai altro.
  3. Lancia venti prove della versione divisa con python3 conta.py 20 diviso.
  4. Apri tre registri in registri/, uno di una prova andata bene e due di prove andate male, e scrivi in una frase che cosa è successo in ognuna.
  5. Aggiungi il controllo impossibile: in scrivi_scadenze, prima di scrivere, verifica che ogni data di disdetta venga prima della scadenza, e se non è così restituisci un errore al modello invece di scrivere.
  6. Porta il compito sul tuo lavoro: sostituisci i tre contratti con tre documenti veri di colore verde o giallo e scrivi le due date giuste nel dizionario GIUSTE. Rilancia dieci prove.
Il codice della misura, da copiare in conta.py
# conta.py: fa girare l'agente N volte sui contratti di esempio e conta
# che cosa e' successo davvero, guardando il registro e la cartella uscita.
#   python3 conta.py 20            la versione della Lezione 3
#   python3 conta.py 20 diviso     la versione divisa
import builtins, contextlib, io, json, os, re, shutil, sys
import agente
if "diviso" in sys.argv:
    import diviso                       # sostituisce strumenti e istruzioni

N = int(sys.argv[1])
builtins.input = lambda *_: "s"         # qui contiamo: il cancello dice sempre si'
COMPITO = ("Leggi i contratti nella cartella e scrivi una nota scadenze.md con, per ogni "
           "contratto, la data di scadenza e l'ultimo giorno utile per la disdetta.")
GIUSTE = {"pulizie": r"2 dicembre 2026|2026-12-02|0?2/12/2026",
          "gestionale": r"14 dicembre 2026|2026-12-14|14/12/2026"}
somma = {"nota scritta": 0, "pulizie giusta": 0, "gestionale giusta": 0,
         "dice di aver scritto senza farlo": 0}
os.makedirs("registri", exist_ok=True)
for i in range(N):
    shutil.rmtree("uscita", ignore_errors=True)
    agente.REGISTRO = f"registri/prova-{i:02d}.jsonl"
    with contextlib.redirect_stdout(io.StringIO()):
        risposta = agente.agente(COMPITO) or ""
    nota = open("uscita/scadenze.md").read() if os.path.exists("uscita/scadenze.md") else ""
    somma["nota scritta"] += bool(nota)
    for chi, data in GIUSTE.items():
        somma[chi + " giusta"] += bool(re.search(data, nota, re.I))
    somma["dice di aver scritto senza farlo"] += (not nota and
        bool(re.search(r"salvat|scritt|creat", risposta, re.I)))
    print(i + 1, "nota" if nota else "nessuna nota", flush=True)
print(json.dumps(somma, ensure_ascii=False, indent=1))

Risultato atteso

Due righe di numeri e tre frasi sui registri. Se il tuo modello è diverso dal mio i numeri saranno diversi, ed è il motivo per cui il laboratorio esiste: la tabella di questa pagina vale per Ministral 3 8B su quei tre contratti, e la tua vale per il tuo modello sui tuoi documenti. È quella che porti a chi decide nella Lezione 5.

Mini-test di autoverifica

Istruzioni

Sezione A: 6 domande a risposta multipla (1 punto). Sezione B: 2 domande aperte (3 punti). Totale 12. Soglia: 60% (8/12).

Sezione A: risposta multipla

  1. Per sapere se l'agente ha scritto la nota si guarda:
    a) la sua risposta finale   b) la cartella uscita e il registro   c) quanto ci ha messo   d) il tono della risposta
  2. Nelle prove con lo strumento giorni_prima, Ministral 3 8B:
    a) non lo chiamava mai   b) lo chiamava, riceveva la data giusta e la ricopiava sbagliata nella nota   c) sbagliava gli argomenti   d) scriveva sempre la nota giusta
  3. «2 febbraio 2027» come disdetta di un contratto che scade il 31 gennaio 2027 si scopre con:
    a) un modello più grande   b) un controllo nel codice: la disdetta deve venire prima della scadenza   c) una temperatura più bassa   d) un prompt più lungo
  4. Nella versione divisa il modello:
    a) calcola e scrive   b) estrae la scadenza e i giorni di preavviso   c) non fa niente   d) controlla il codice
  5. Il cancello protegge di più quando mostra:
    a) la nota finita   b) i dati estratti, da confrontare con la fonte   c) il registro intero   d) niente, chiede solo sì o no
  6. Un cancello messo davanti anche agli strumenti che guardano:
    a) è più sicuro   b) abitua a premere sì senza leggere   c) è obbligatorio   d) rende l'agente più veloce

Sezione B: risposta aperta

  1. (3 punti) Prendi un compito del tuo lavoro che vorresti dare a un agente e dividilo come abbiamo fatto con le scadenze: che cosa estrae il modello, che cosa calcola il codice, che cosa scrive il codice.
  2. (3 punti) Scrivi due controlli «impossibili» per quel compito, cioè due cose che il codice può verificare senza conoscere la risposta giusta.
Mostra risposte corrette

Sezione A

  1. b: la risposta dice quello che l'agente crede di aver fatto.
  2. b: venti prove su venti per le pulizie.
  3. b: non serve sapere la data giusta per sapere che quella è impossibile.
  4. b: la lettura è giudizio, il resto è calcolo.
  5. b: sono le cose che una persona verifica in pochi secondi.
  6. b: un cancello che suona sempre non lo ascolta più nessuno.

Sezione B, risposte modello

7. Esempio, le fatture dei fornitori: il modello estrae fornitore, numero, data, imponibile e aliquota; il codice calcola IVA e totale e confronta il totale con quello stampato; il codice scrive la riga nel foglio delle spese. Il modello non scrive mai un importo calcolato.

8. Esempio: il totale calcolato deve coincidere con quello stampato in fattura entro un centesimo; la data della fattura non può essere nel futuro né più vecchia di un anno.

Valutazione (su 12)

  • 11-12: ottimo. 8-10: sufficiente. < 8: rileggi il primo errore e la versione divisa.

Riassunto della lezione

Cosa hai imparato

  • Un agente si misura su tante prove, con le risposte note, guardando i file e il registro.
  • Il conto giusto può essere ricopiato sbagliato: uno strumento di calcolo da solo non basta.
  • La divisione va fino in fondo: il modello estrae, il codice calcola e scrive.
  • I controlli impossibili si scrivono senza conoscere la risposta giusta.
  • Il cancello mostra i dati da confrontare con la fonte, e sta solo davanti agli strumenti che fanno.

Prossimi passi

Nella Lezione 5, Scegliere e restare in regola mettiamo insieme i conti dei soldi, le regole di AI Act e GDPR e le domande da fare a un fornitore, e scegliamo dove far girare l'agente quando smette di essere una prova.

Un agente sovrano dentro la tua azienda?

Questo corso lo porto anche in azienda: si parte dai vostri documenti e dai vostri processi, e alla fine l'agente gira sulle vostre macchine.

Parliamone
Federico BoggiaRispondo io, in giornata

Ciao! Dimmi che ti serve: un corso online, il percorso Divide et Delega o la formazione per la tua azienda.

Scrivimi su WhatsApp