Ragionare e agire: il ciclo dell’agente#
Chiedete a un modello di linguaggio che ore sono. Non lo sa. Chiedetegli di moltiplicare \(4831\) per \(7092\): sputerà un numero dall’aria plausibile e spesso sbagliato. Il risultato vero è \(34\,261\,452\); un modello che sbaglia tende a scriverne uno lungo uguale e che comincia allo stesso modo, tipo \(34\,281\,452\), con l’errore nascosto in mezzo, dove nessuno lo cerca. Chiedetegli cosa è successo ieri, infine, e vi parlerà con sicurezza di un mondo che si è fermato alla fine del suo addestramento.
Un LLM, per quanto grande, è un cervello murato in una stanza senza finestre e senza orologio: sa moltissimo di ciò che ha letto, nulla del resto, e i conti lunghi li sbaglia come chiunque li faccia a mente.
E però può fare una cosa preziosa: decidere di chiedere aiuto. Invece di inventare la risposta, può emettere una richiesta («esegui questa moltiplicazione», «apri questa pagina», «che ore sono?») lasciare che qualcos’altro la esegua, e usare il risultato. È il tool use, l’uso degli strumenti: dare le mani a un cervello. Ed è il primo mattone di ciò che chiamiamo agente: un modello che non si limita a rispondere, ma osserva, decide e agisce, in un ciclo, finché il compito non è chiuso.
Costruiamo su terreno noto. Nel capitolo sui Transformer abbiamo visto tre cose. La prima è un modello che, letta una montagna di testo, impara a completarlo. La seconda è una fase di addestramento successiva, fatta di esempi di consegne già svolte, che lo rende capace di eseguire una richiesta invece di limitarsi a proseguirla. La terza è il prompt, cioè il foglietto di istruzioni che si scrive al modello prima di lasciarlo rispondere: è diventato il modo in cui gli si dice cosa fare, potente e fragile insieme, perché una parola diversa cambia il risultato.
Un agente è il passo successivo: quello stesso modello, messo dentro il ciclo osserva-ragiona-agisci, con strumenti a portata di mano e il permesso di usarli.
Dare le mani al modello: il tool use#
Il meccanismo è più semplice di quanto sembri, e si regge su un catalogo.
Insieme al prompt diamo al modello l’elenco degli attrezzi che ha a disposizione. Per ognuno tre cose: come si chiama, che cosa fa (scritto a parole, in italiano) e che cosa bisogna infilarci dentro perché funzioni. Quel terzo pezzo, in gergo, sono gli argomenti: per una calcolatrice il conto da fare, per una ricerca le parole da cercare, e niente a che vedere con gli argomenti di cui si discute. Dal lato del programma ogni attrezzo è una funzione, nel senso informatico che abbiamo dato all’inizio del capitolo, e da lì il nome inglese di tutto il meccanismo: function calling, «chiamata di funzione».
Quando il modello ritiene che serva uno strumento, non risponde con del testo
per l’utente: emette una richiesta strutturata. Non una frase rivolta a una
persona, cioè, ma una riga in un formato fisso, sempre lo stesso, che un
programma sa leggere alla lettera senza doverci capire dentro come si fa con
l’italiano («chiama calcola con argomento "4831 * 7092"»). Il sistema che
ospita il modello intercetta la richiesta, esegue davvero la funzione, e
restituisce il risultato al modello come nuovo pezzo di contesto, cioè del
testo che il modello si ritrova davanti agli occhi al giro dopo. Solo allora il
modello continua.
Fig. 17.3 Il giro del function calling. Il modello non esegue mai niente: chiede, e l’esecuzione resta nel codice di chi lo ospita. I passi 1 e 2 possono ripetersi più volte prima che arrivi la risposta finale.#
Le due etichette sulle frecce di Fig. 17.3 sono i nomi che
si usano in gergo per i due messaggi: tool_use è «chiedo di usare questo
attrezzo, con dentro queste cose», tool_result è «ecco cosa ha risposto
l’attrezzo». La divisione dei compiti che si vede nel disegno è la
ragione per cui il tool use è insieme potente e governabile: il modello
propone, il codice dispone. Chi ospita il modello decide quali funzioni
esistono, le valida prima di eseguirle e può rifiutarsi; il modello non ha mai
in mano l’esecuzione, solo la richiesta.
Immagina un dirigente competente ma con una regola personale: non fa mai i conti a mano e non si fida della propria memoria per i dettagli. Sulla scrivania ha una calcolatrice, un telefono e uno schedario. Quando gli chiedi «quanto fa il totale della commessa?» non azzarda una cifra: prende la calcolatrice. Quando gli chiedi «qual è l’indirizzo del cliente?» apre lo schedario. La sua bravura non sta nel sapere tutto, ma nel capire quale attrezzo serve e nell’usarlo bene.
Il tool use è esattamente questo. Al modello diamo un elenco di attrezzi, ognuno con un’etichetta che dice a cosa serve: «calcolatrice: fa i conti esatti», «motore di ricerca: trova pagine aggiornate», «archivio: cerca un dato». Quando arriva una domanda, il modello non prova a rispondere di pancia: sceglie l’attrezzo giusto, scrive cosa infilarci dentro, aspetta il risultato e lo usa. Un modello che sa dire «questo non lo so a memoria, ma so chi lo sa» è più affidabile di uno che indovina sempre.
Uno strumento è descritto da uno schema: un nome, una descrizione in linguaggio naturale e una firma tipata degli argomenti, tipicamente in JSON Schema. Per una calcolatrice:
{
"name": "calcola",
"description": "Valuta un'espressione aritmetica e ne restituisce il valore.",
"parameters": {
"type": "object",
"properties": {
"espressione": {"type": "string", "description": "es. '4831 * 7092'"}
},
"required": ["espressione"]
}
}
Le tre righe che avvolgono il parametro non sono cerimoniale: type dice che
gli argomenti arrivano raccolti in un oggetto, properties elenca i campi di
quell’oggetto e required dichiara quali non si possono omettere. Nessuna
delle tre è necessaria perché lo schema sia valido (uno schema senza
required è legittimo e vuol dire «sono tutti facoltativi»), ma è su quelle
righe che il modello decide cosa scrivere, e uno schema che non dice cosa è
obbligatorio glielo lascia indovinare. Il nome della
chiave che lo contiene invece cambia da un fornitore all’altro
(parameters, input_schema, inputSchema); la forma dello schema no, ed è
quella che vale la pena ricordare.
La descrizione non è decorazione: è il testo su cui il modello ragiona per
decidere se e quando invocare lo strumento, ed è quindi parte del prompt a
tutti gli effetti. Il modello, invece di campionare token destinati
all’utente, emette una struttura {"name": "calcola", "arguments": {"espressione": "4831 * 7092"}}; il runtime la valida contro lo schema, esegue
la funzione, e re-inietta il risultato nel contesto come messaggio di ruolo
tool. Anche qui i nomi esatti cambiano da un fornitore all’altro
(tool_use/tool_result per gli uni, come nella figura, function_call e
ruolo tool per gli altri, che per giunta passano gli arguments come
stringa JSON invece che come oggetto); il giro è lo stesso. La capacità di
scegliere lo strumento e compilarne gli argomenti
nel formato giusto non è innata: emerge dall’instruction tuning visto nel
capitolo sui Transformer, cioè dall’addestramento su esempi in cui a una
consegna corrisponde l’azione corretta invece della sua prosecuzione. Il
modello resta un generatore di testo: «chiamare uno strumento» è, sotto il
cofano, generare una particolare sequenza di token che il sistema ha imparato a
interpretare come una chiamata.
Quel catalogo, però, va scritto a mano, e va riscritto per ogni sistema esterno a cui si vuole attaccare l’agente: l’archivio dell’azienda, il calendario, il gestore dei file. Finché i sistemi sono due o tre va benissimo. Quando diventano venti conviene mettersi d’accordo su una lingua unica con cui chiedere a chiunque «che strumenti hai?» e «esegui questo».
Un accordo del genere si chiama protocollo, ed è la stessa idea per cui due computer che non si sono mai visti riescono a scambiarsi una pagina web. Ce n’è uno pensato apposta per questo, MCP («protocollo per il contesto del modello», dall’inglese Model Context Protocol), proposto nel 2024 dall’azienda Anthropic. La sua architettura è in Fig. 17.4, e conviene guardarla per la forma più che per il nome: di protocolli così ne nascono e ne muoiono parecchi, e quale finirà per imporsi è il tipo di cosa che si legge sui giornali fra un anno; la forma invece è la stessa per tutti.
Fig. 17.4 Lo stesso catalogo, ma standardizzato. A parlare il protocollo non è il modello: è l’applicazione che lo ospita, la quale apre un canale per ogni sistema esterno (nel disegno sono due, uno che governa dei file e uno che governa un archivio di dati) e li interroga tutti allo stesso modo. Al modello arrivano poi strumenti come gli altri, senza che debba sapere da dove vengono.#
Il salto di Fig. 17.4 non è nel meccanismo, che resta quello di prima (il modello scrive la richiesta, il programma la esegue), ma nel numero di sistemi che si riescono a collegare senza scrivere codice nuovo ogni volta. Cambia anche chi scrive le etichette degli attrezzi: non più chi costruisce l’agente, ma chi mette a disposizione il sistema dall’altra parte.
Resta però una domanda che finora abbiamo scavalcato: chi ha insegnato al modello quando fermarsi e chiamare un attrezzo? Non basta avere il catalogo: bisogna anche riconoscere il momento in cui serve. Glielo si insegna addestrandolo su tanti esempi di chiamate fatte al punto giusto, esempi che finora ha dovuto scrivere qualcuno, uno per uno, a mano.
Nel 2023, però, un gruppo di Meta AI (il laboratorio di ricerca dell’azienda a cui appartiene Facebook) ha mostrato che quegli esempi il modello se li può fabbricare da solo: è Toolformer [SDYDessi+23]. E si fabbricano in un punto inatteso, non prima o dopo una frase ma dentro, in mezzo a una parola e l’altra.
Fig. 17.5 Toolformer decide dentro la frase. Al punto di decisione il modello può proseguire normalmente oppure inserire una chiamata: il risultato torna nel testo e la generazione riparte da lì, come se il numero l’avesse scritto lui.#
Il dettaglio da guardare in Fig. 17.5 è appunto quello: la chiamata sta dentro la frase, e il suo risultato (\(0{,}29\)) rientra nel testo giusto prima della parola che lo commenta (\(29\%\)). Ed è questo a rendere possibile il trucco con cui Toolformer impara. Se la chiamata sta lì in mezzo, il modello può misurare una cosa che sa misurare benissimo: quanto gli riesce facile scrivere le parole che vengono subito dopo. Con il numero vero sotto gli occhi, «29%» diventa quasi obbligato; senza, è un tiro a indovinare. La differenza fra le due difficoltà è il voto che Toolformer dà alla chiamata.
Come impara un bambino a usare la calcolatrice? Provando. Fa un conto a mente, controlla con la calcolatrice, e nota che nei conti lunghi la calcolatrice ci azzecca dove lui sbaglia: così, la volta dopo, per i conti lunghi la prende subito. Toolformer fa qualcosa di simile con se stesso, ma con un’unica prova a disposizione, perché non ha un foglio delle soluzioni da confrontare.
Prende una montagna di testo già scritto e, qua e là, prova a infilare una chiamata a uno strumento. Poi si chiede: con il risultato dell’attrezzo davanti, il resto della frase mi viene più facile? Prendi «quattrocento su millequattrocento, cioè il 29%»: se in mezzo qualcuno ha messo il risultato della divisione, «0,29», scrivere «29%» subito dopo diventa quasi obbligato; se non c’è, bisogna tirare a indovinare. Quel salto di facilità è la prova che l’attrezzo serviva lì.
Le chiamate che superano la prova le tiene, le altre le butta. Alla fine si è fabbricato da solo un quaderno di esercizi («qui conveniva la calcolatrice», «qui conveniva la ricerca») e ci studia sopra. Nessun insegnante gli ha detto dove mettere gli attrezzi: l’ha scoperto misurando quanto lo aiutavano.
Toolformer usa un’auto-supervisione elegante. Partendo da poche dimostrazioni per ciascuna API (calcolatrice, sistema di domanda-risposta, motore di ricerca, traduttore, calendario), il modello campiona in molte posizioni di un corpus delle candidate chiamate ad API con i relativi argomenti. Ogni candidata viene eseguita, e si tiene solo se il suo risultato, inserito nel contesto, riduce la cross-entropia pesata sui token immediatamente successivi, rispetto al non chiamare o a un risultato inutile:
dove \(L_i^{\text{con}}\) e \(L_i^{\text{senza}}\) sono la perdita futura con e senza la chiamata inserita in posizione \(i\) (\(L_i^{\text{senza}}\) è il minimo fra il non chiamare affatto e il chiamare senza ottenere nulla di utile), \(\tau\) è una soglia di utilità, e i pesi \(w_t\) calano linearmente fino ad annullarsi dopo cinque token. Il dettaglio dei pesi non è pignoleria: dice che ciò che si misura è se la chiamata aiuta a scrivere la frase in corso, non il resto del documento, ed è la ragione per cui il filtro non annega nel rumore. Le chiamate che superano il filtro diventano un dataset aumentato, e il modello ci viene messo a punto sopra con il consueto obiettivo auto-supervisionato. Il risultato è un modello che, a inferenza, decide da sé quando emettere una chiamata, perché ha imparato che in quei punti la chiamata paga in termini di predizione. Il criterio è puramente interno («l’attrezzo mi aiuta a continuare il testo?») e non richiede alcuna etichetta umana su dove usarlo.
Vale la pena chiudere con il limite che gli autori dichiarano, perché è esattamente il confine di questa sezione. Toolformer decide dove chiamare, non come comporre: non sa usare gli strumenti in catena (l’uscita di uno come ingresso di un altro) né in modo interattivo (raffinare la richiesta guardando il risultato), ed è ciò che serve a un agente. È il salto che affronta il pattern delle prossime pagine, e conviene dire subito che non gli è succeduto: quel pattern è dell’ottobre 2022, Toolformer del febbraio successivo. Non sono due tappe di una scala, sono due risposte a due domande diverse.
Ragionare e agire insieme: ReAct#
Uno strumento, da solo, non basta a fare un agente. Serve una procedura: quando pensare, quando agire, come usare ciò che l’azione ha restituito. Lo schema di lavoro che ha dato il nome a questo modo di procedere si chiama ReAct, dall’inglese reasoning e acting, ragionare e agire, ed è stato proposto nel 2022 da Shunyu Yao e colleghi [YZY+23]. L’idea è intrecciare, in un unico flusso, tre tipi di passi: un pensiero (Thought, il ragionamento ad alta voce), un’azione (Action, la chiamata a uno strumento) e un’osservazione (Observation, il risultato che torna indietro). Il modello genera un pensiero, poi un’azione; il sistema esegue e restituisce l’osservazione; il modello legge l’osservazione, genera il pensiero successivo, e così via, fino a produrre la risposta finale.
Una nota per non confondersi con l’ordine. Un cerchio non ha un inizio, e infatti a volte lo si racconta partendo dall’osservazione (osserva, ragiona, agisci) e a volte dal pensiero (pensa, agisci, osserva): sono lo stesso giro guardato da due punti diversi. Nelle tracce scritte si parte dal pensiero, perché la prima osservazione è la domanda dell’utente, che è già lì.
Fig. 17.6 Due giri di ReAct su una domanda che nessuna singola ricerca risolve. Ogni osservazione non chiude il problema: lo restringe, e il pensiero successivo riparte da lì.#
La domanda dell’esempio in Fig. 17.6 è di quelle che sembrano banali: «in che anno è uscito il film d’esordio di quel regista?». Per rispondere servono due fatti, e il secondo si può cercare solo dopo aver ottenuto il primo: finché non so quale sia il film d’esordio, non ho niente da cercare. Un sistema che agisse una volta sola resterebbe fermo al primo giro, perché non saprebbe ancora cosa chiedere.
Perché conviene far ragionare il modello ad alta voce tra un’azione e l’altra? La risposta che viene per prima, la catena di ragionamento [WWS+22], qui vale poco: i suoi guadagni misurati stanno sui conti e sulla logica [SYR+25], mentre il ciclo di un agente è fatto in buona parte di altro, cioè scegliere uno strumento, leggere un risultato e decidere se ripetere.
La ragione per cui il pensiero esplicito serve qui è un’altra, e più prosaica: dà al modello un posto dove scrivere a che punto è del compito prima di scegliere l’azione. È la stessa idea che ritroveremo, chiamata foglio di brutta, parlando di come si riempie la finestra di contesto.
Ma il guadagno più grosso non è il pensiero: è l’osservazione, e per apprezzarlo serve un nome. Quando un modello inventa un fatto e lo dice con la faccia di chi lo sa, si parla di allucinazione: non è che menta, è che genera la continuazione più plausibile e nessuno gli ha mai chiesto di controllare. Un’osservazione che arriva da fuori, invece, non se l’è inventata lui: è testo che gli è stato messo davanti dal programma. Il pensiero decide quale strumento usare e come leggere ciò che è tornato, ma è l’osservazione a tenerlo attaccato a qualcosa di vero.
Pensa a un detective che indaga a voce alta. Non spara subito il colpevole: alterna ragionamenti e verifiche. «Penso: la vittima è stata vista l’ultima volta al porto, quindi conviene controllare i registri delle navi. Controllo i registri… Scopro che quella notte è salpato un solo mercantile. Penso: allora mi interessa chi era a bordo. Controllo la lista dell’equipaggio…». Ogni «penso» decide la prossima mossa; ogni «controllo» è un’azione nel mondo; ogni «scopro» è ciò che la mossa ha rivelato, e riparte il giro.
La forza del metodo sta nell’alternanza. Un detective che ragionasse soltanto, senza mai controllare, costruirebbe teorie eleganti e magari sbagliate. Uno che controllasse a caso, senza ragionare, si perderebbe tra mille indizi inutili. ReAct fa fare al modello tutti e due i mestieri: pensa per decidere dove guardare, guarda per correggere ciò che pensa.
Non è però un guadagno gratis, e conviene saperlo subito. Un detective che si è imposto di controllare ogni intuizione prima di proseguire fa meno voli di fantasia, ma finisce anche per pensare di meno: la mossa dopo gliela suggerisce sempre l’ultimo documento che ha letto, e le catene di ragionamento lunghe, che faceva quando ragionava e basta, smette di farle. Si aggiunge poi un modo di fallire che prima non c’era: il registro che va a consultare può non dirgli niente di utile, e a quel punto è fermo, mentre chi ragionava per conto proprio almeno un’ipotesi la produceva.
Il contesto dell’agente cresce come una sequenza strutturata di terne:
Thought: per rispondere mi serve l'anno del paper, non lo so a memoria.
Action: cerca[attention is all you need]
Observation: 2017
Thought: ora calcolo la differenza con il 2026.
Action: calcola[2026 - 2017]
Observation: 9
Thought: ho tutto.
Action: Answer[9 anni, dal 2017]
Ogni Observation è testo prodotto dall’esterno (non campionato dal modello) e questo è il punto cruciale: àncora il ragionamento a fatti recuperati, invece di lasciarlo derivare. Sui compiti interattivi come ALFWorld (eseguire istruzioni in un ambiente simulato) e WebShop (navigare un sito per acquistare), Yao e colleghi misurano che il ragionamento intercalato all’azione batte nettamente le politiche che agiscono senza pensare.
Sui compiti a forte intensità di conoscenza, invece, l’ancoraggio va letto per quello che è: uno scambio, non un guadagno secco. Sulla verifica di fatti (FEVER) ReAct supera la sola chain-of-thought; sulla domanda-risposta multi-hop (HotpotQA) le resta appena sotto. Le allucinazioni crollano (nei fallimenti passano da oltre metà a zero) ma il ragionamento si irrigidisce sulla forma pensiero-azione-osservazione, e gli errori di ragionamento quasi triplicano, dal 16% al 47% delle traiettorie fallite esaminate; per giunta nasce un modo di fallire che prima non esisteva, la ricerca che torna a mani vuote. Il risultato migliore del lavoro non è ReAct da solo: è la combinazione dei due, che si alternano quando l’uno si arena.
Il costo è in token e latenza (ogni pensiero è testo generato in più). In cambio la traccia è ispezionabile, ed è un vantaggio operativo vero. Ma qui va evitata una confusione che costa cara: leggibile non vuol dire fedele. La catena di pensieri è testo generato come tutto il resto, e può razionalizzare a posteriori una scelta compiuta per motivi che non scrive [TMPB23]; peggio, la fedeltà del ragionamento esplicito tende a calare al crescere della scala del modello [LCR+23]. La parte della traccia su cui si può contare sono le azioni e le osservazioni, perché quelle le esegue e le registra il runtime; i pensieri sono un indizio, non una spiegazione.
Imparare dai propri errori: la riflessione#
Un ciclo ReAct finisce in due modi: con la risposta, oppure a mani vuote, quando i passi concessi si esauriscono o la strada imboccata non porta da nessuna parte. In quel secondo caso la cosa ovvia da fare è riprovare da capo, e qui salta fuori il problema: l’agente riparte esattamente com’era partito la prima volta, senza sapere niente di com’è andata, e ha ottime probabilità di rifare lo stesso errore.
Nel 2023 Noah Shinn e colleghi propongono un rimedio semplice e umano, Reflexion [SCB+23]: dopo un fallimento, l’agente si ferma e scrive a parole cosa è andato storto, poi riprova tenendo quella critica sotto gli occhi.
È ciò che fa un buon studente dopo un compito andato male. Non si limita a riprovare identico: rilegge l’errore e se lo dice a parole, «ho sbagliato perché ho applicato la formula prima di convertire le unità; la prossima volta converto per prima cosa». Quella frase, appuntata a margine, alla prova successiva vale più di mille esercizi ripetuti a testa bassa, perché indirizza il tentativo nuovo lontano dallo stesso scoglio.
Reflexion dà all’agente questo quaderno di margine. Quando un tentativo fallisce, il modello genera una piccola auto-critica in linguaggio naturale (la sua «memoria verbale» degli errori) e la aggiunge al contesto del tentativo seguente. Non cambia un solo peso della rete: cambia solo ciò che il modello legge prima di riprovare. Eppure spesso basta, perché l’errore che prima era invisibile ora è scritto nero su bianco all’inizio della pagina.
Shinn e colleghi chiamano il metodo verbal reinforcement learning: al posto di aggiornare i parametri con un gradiente, il segnale di rinforzo è testo. Il ciclo ha tre ruoli: un attore (il modello ReAct) che tenta il compito; un valutatore che assegna un esito al tentativo (una ricompensa, il superamento o meno di test, il raggiungimento dell’obiettivo); e un modulo di auto-riflessione che, letta la traccia fallita e il suo esito, produce una critica verbale; «l’azione X non ha dato il risultato atteso, conviene provare Y». Questa critica finisce in una memoria episodica che viene anteposta al contesto del tentativo successivo. Sui compiti di programmazione gli autori misurano un guadagno netto di pass@1: iterare sull’auto-critica, senza toccare i pesi, recupera una fetta consistente dei casi inizialmente falliti.
Su quel guadagno conviene però leggere la lettera piccola, perché riguarda il distinguo che chiude questa sezione. Il valutatore che dice «hai sbagliato» non è, in quegli esperimenti di programmazione, un giudice esterno: è una batteria di test generata dal modello stesso, e gli autori dichiarano che può promuovere una soluzione sbagliata (tutti i test passano su un programma errato) o bocciarne una giusta. È un segnale d’esito, ma auto-prodotto: sta dalla parte scivolosa della riga che stiamo per tracciare, non da quella solida.
Detta così, la riflessione sembra magica, e qui il libro deve al lettore un distinguo netto. L’auto-critica non è auto-correzione garantita, e tutto dipende da chi dice all’agente che ha sbagliato. La riflessione funziona bene quando esiste un segnale d’esito affidabile ed esterno: dei test scritti da qualcun altro che passano o falliscono (i test di progetto di SWE-bench sono l’esempio buono, perché nessuno li ha scritti per far contento l’agente), un risultato numerico verificabile, un obiettivo raggiunto o no nell’ambiente. Lì la critica ha un appiglio solido su cui costruire. Quando invece l’unico giudice è il modello stesso, senza alcun riscontro dal mondo, la faccenda si fa scivolosa: un modello convinto di una risposta sbagliata tende a produrre auto-critiche che confermano l’errore, e può perfino peggiorare una risposta che era corretta, «correggendola» verso il falso. Riflettere aiuta a patto di avere qualcosa contro cui verificarsi; la sola introspezione, da sé, non crea competenza che il modello non aveva.
Un agente giocattolo, in Python#
Mettiamo insieme i pezzi nel modo più spoglio possibile: un mini-agente ReAct che gira davvero, in puro Python, senza collegarsi a internet e senza installare niente. Chiamiamo traccia l’elenco di quello che è successo finora, un blocco per giro (che cosa ho chiesto, che cosa mi è tornato): è la memoria del nostro agente, e la vedremo allungarsi sotto i nostri occhi.
Il trucco per concentrarci sul ciclo è sostituire il modello vero con un
LLM finto che, a parità di traccia, risponde sempre la stessa cosa (si
dice deterministico): non un modello, ma poche righe di regole scritte a
mano che, guardando la traccia, decidono il prossimo Thought e la prossima
Action. Gli strumenti, invece, sono veri: una calcolatrice che valuta
un’espressione aritmetica in modo sicuro, e un cerca che va a prendere una
voce da un archivio, come si cerca una parola sul vocabolario.
Il blocco che segue costruisce quei due strumenti, e la parte più lunga è la
calcolatrice. La via facile in Python sarebbe eval, la funzione che esegue
una stringa (cioè un pezzo di testo) come se fosse codice: comodissima e
pericolosa, perché eseguirebbe qualunque cosa il modello scriva, non solo un
conto. Al suo posto leggiamo l’espressione, la spezziamo nei suoi pezzi e la
calcoliamo noi, accettando soltanto gli operatori che abbiamo messo in elenco;
tutto il resto viene respinto con un messaggio che dice cosa non andava. Se
questa parte non interessa si può scorrere: il cuore del capitolo è il blocco
dopo.
import ast
import operator
# --- due strumenti reali ---
# operatori ammessi: un mini-interprete sicuro, niente eval()
_OP = {
ast.Add: operator.add, ast.Sub: operator.sub,
ast.Mult: operator.mul, ast.Div: operator.truediv,
ast.USub: operator.neg,
}
def _operatore(op):
"""Un operatore fuori elenco esce di qui con un errore leggibile."""
if type(op) not in _OP:
raise ValueError(f"operatore non ammesso: {type(op).__name__}")
return _OP[type(op)]
def _valuta(nodo):
if isinstance(nodo, ast.Constant): # un numero, e solo un numero
if not isinstance(nodo.value, (int, float)):
raise ValueError("ammessi solo numeri")
return nodo.value
if isinstance(nodo, ast.BinOp): # a operatore b
return _operatore(nodo.op)(_valuta(nodo.left), _valuta(nodo.right))
if isinstance(nodo, ast.UnaryOp): # -a
return _operatore(nodo.op)(_valuta(nodo.operand))
raise ValueError("espressione non ammessa")
def calcola(espressione):
"""Valuta un'espressione aritmetica in modo sicuro (senza eval)."""
return _valuta(ast.parse(espressione, mode="eval").body)
# un piccolo archivio: la memoria esterna che il modello non ha nei pesi
ARCHIVIO = {
"attention is all you need": "2017",
"gpt-3": "2020",
"react": "2022",
}
def cerca(chiave):
"""Cerca un fatto nell'archivio; restituisce sempre una stringa."""
return ARCHIVIO.get(chiave.lower().strip(), "non trovato")
STRUMENTI = {"calcola": calcola, "cerca": cerca}
Nell’archivio in fondo al blocco ci sono tre voci, e la prima è quella su cui verterà la domanda: Attention Is All You Need è il titolo dell’articolo scientifico (in gergo, un paper) che nel 2017 ha presentato i Transformer, l’architettura studiata nel capitolo sui Transformer.
Il cuore dell’agente sono le altre due funzioni. La prima è llm_finto: legge
la traccia e restituisce il pensiero e l’azione da fare (l’azione è due cose,
il nome dell’attrezzo e quello che ci va infilato dentro). La seconda è il
ciclo esegui_agente, che alterna decisione ed esecuzione. È la stessa
struttura di un agente vero, con l’unica differenza che qui il «modello» è una
regola scritta a mano.
# --- l'LLM finto: deterministico, a regole ---
def llm_finto(traccia):
"""Data la traccia finora, emette (pensiero, azione, argomento).
Un vero LLM genererebbe questo testo; qui lo decide una regola."""
ultima = traccia[-1]["osservazione"] if traccia else None
if ultima is None:
return ("Non conosco a memoria l'anno del paper: lo cerco.",
"cerca", "attention is all you need")
if ultima == "2017":
return ("Il paper è del 2017. Calcolo quanti anni fa, dal 2026.",
"calcola", "2026 - 2017")
return (f"Il calcolo dice {ultima}: ho tutto per rispondere.",
"Answer", "'Attention Is All You Need' è del 2017: 9 anni fa nel 2026.")
# --- il ciclo dell'agente ---
def esegui_agente(domanda, max_passi=5):
print(f"Domanda: {domanda}\n")
traccia = []
for _ in range(max_passi):
pensiero, azione, argomento = llm_finto(traccia) # il modello "ragiona"
print(f"Thought: {pensiero}")
if azione == "Answer": # fine del loop
print(f"Answer: {argomento}")
return argomento
print(f"Action: {azione}[{argomento}]")
osservazione = str(STRUMENTI[azione](argomento)) # il sistema agisce
print(f"Observation: {osservazione}\n")
traccia.append({"azione": azione, "argomento": argomento,
"osservazione": osservazione}) # torna nel contesto
print("(limite di passi raggiunto)")
esegui_agente("In che anno è uscito 'Attention Is All You Need' "
"e quanti anni fa è, nel 2026?")
L’esecuzione stampa la traccia completa: si vedono i tre Thought, le due
chiamate agli strumenti con le rispettive Observation, e la risposta finale.
Domanda: In che anno è uscito 'Attention Is All You Need' e quanti anni fa è, nel 2026?
Thought: Non conosco a memoria l'anno del paper: lo cerco.
Action: cerca[attention is all you need]
Observation: 2017
Thought: Il paper è del 2017. Calcolo quanti anni fa, dal 2026.
Action: calcola[2026 - 2017]
Observation: 9
Thought: Il calcolo dice 9: ho tutto per rispondere.
Answer: 'Attention Is All You Need' è del 2017: 9 anni fa nel 2026.
Quella traccia, guardata dall’alto, è un cerchio che gira tre volte (Fig. 17.7). I tre passi sono sempre gli stessi; quello che cambia a ogni giro è il contesto, cioè ciò che il modello si ritrova davanti prima di scegliere la mossa successiva, e che si allunga di un blocco ogni volta: la chiamata fatta e quello che ha risposto.
Fig. 17.7 Lo stesso giro, tre volte. A sinistra i passi che si ripetono; a destra quello che il modello si rilegge prima di decidere, più lungo di un blocco a ogni giro. Al terzo giro non serve nessuno strumento e l’agente esce dal ciclo con la risposta: è uno dei due modi in cui un ciclo finisce, l’altro è il limite di passi.#
Il modello finto non sapeva l’anno (l’ha cercato) e non ha fatto la
sottrazione a mente (l’ha delegata): esattamente il comportamento che vogliamo
da un agente. Sostituite llm_finto con un vero LLM a cui passate, a ogni
giro, la traccia accumulata e il catalogo degli strumenti, e avete (nella sua
ossatura essenziale) lo stesso ciclo che muove gli assistenti capaci di
navigare il web, eseguire codice e interrogare un archivio di dati.
Tutto il resto, nei sistemi reali, è il lavoro di reggere quando qualcosa va storto. Sono tre mestieri. Bisogna sapere cosa fare quando il modello scrive una chiamata malformata, cioè che non rispetta il formato concordato. Bisogna accorgersi che l’agente si è impantanato e ripete la stessa mossa all’infinito, e fermarlo. E bisogna decidere quali strumenti è prudente mettergli in mano, visto che li userà davvero.
(Sul secondo, una nota per non confondersi. In inglese quell’impantanarsi si dice «entrare in loop», e loop è la stessa parola che indica il ciclo che fa funzionare l’agente. Stessa parola, due significati opposti: uno è il motore, l’altro è il guasto.)
Le sei cose da portarsi via da questa sezione, prima di passare al recupero dei documenti.
Da ricordare
Un modello da solo è murato: non sa l’ora, sbaglia i conti lunghi, ignora quello che è successo dopo il suo addestramento. Il tool use gli dà le mani: invece di rispondere di pancia, scrive per uno strumento quello che nella sezione di apertura era il bigliettino d’ordine del cuoco; il programma che gli sta attorno lo esegue e gli riporta il risultato, che il modello ritrova davanti al giro dopo.
Ogni strumento si presenta con una scheda: come si chiama, a cosa serve, e cosa bisogna infilarci dentro perché funzioni. Il modello impara a scegliere l’attrezzo giusto e a riempirlo bene. Toolformer [SDYDessi+23] lo impara perfino da solo, come il bambino che scopre quando gli conviene la calcolatrice: prova a infilare una chiamata qua e là e tiene quelle che lo aiutano a indovinare meglio le parole successive.
ReAct [YZY+23] è il metodo del detective che ragiona a voce alta: penso → controllo → scopro, e si ricomincia (è lo stesso giro di prima, raccontato partendo dal pensiero). Le allucinazioni, cioè i fatti che il modello si inventa dicendoli con sicurezza, crollano, perché ogni passo si appoggia a qualcosa che è stato davvero trovato; in cambio il ragionamento si irrigidisce e nasce un modo nuovo di sbagliare, la ricerca che non trova niente di utile.
Reflexion [SCB+23] è il quaderno di margine: dopo un fallimento l’agente si scrive a parole cosa è andato storto e riprova leggendo quell’appunto. Non cambia niente dentro la rete: cambia solo quello che legge prima di ricominciare.
Onestà sui limiti: rileggersi non è correggersi. Aiuta quando c’è qualcuno o qualcosa fuori che dice «giusto» o «sbagliato»; se l’unico giudice è il modello stesso, può convincersi di avere ragione avendo torto, e perfino rovinare una risposta che era buona.
Il ciclo dell’agente (guarda, pensa, agisci, ripeti fino alla risposta) è lo stesso del mini-agente di poche decine di righe e degli assistenti che navigano il web ed eseguono codice. Quello che cambia, nei sistemi veri, è tutto il lavoro di rendere il ciclo robusto quando qualcosa va storto.
Da ricordare
Un LLM da solo è murato: non sa l’ora, sbaglia i conti lunghi, ignora ciò che è successo dopo l’addestramento. Il tool use gli dà le mani: invece di rispondere, emette una chiamata strutturata a uno strumento, che il sistema esegue e il cui risultato rientra nel contesto.
Ogni strumento è uno schema (nome, descrizione, argomenti tipati, in JSON Schema:
type,properties,required); la capacità di sceglierlo e compilarne gli argomenti emerge dall’instruction tuning. Toolformer [SDYDessi+23] impara da solo, con auto-supervisione, dove conviene chiamare un’API: tiene le chiamate che riducono la cross-entropia pesata sui cinque token successivi. Non sa però comporre gli strumenti in catena: è il salto che ReAct affronta.ReAct [YZY+23] intreccia in un loop Thought → Action → Observation: le osservazioni àncorano il ragionamento a fatti reali e le allucinazioni crollano, ma è uno scambio, non un guadagno secco (gli errori di ragionamento quasi triplicano, dal 16% al 47%, e si aggiunge il fallimento della ricerca a vuoto). La traccia è ispezionabile, non fedele [LCR+23, TMPB23]: contano le azioni, non i pensieri.
Reflexion [SCB+23] aggiunge una memoria verbale degli errori: dopo un fallimento l’agente si auto-critica a parole e riprova leggendo la critica, senza toccare i pesi.
Onestà sui limiti: l’auto-critica non è auto-correzione garantita. Aiuta quando c’è un esito esterno affidabile (test scritti da altri, risultato verificabile); con un giudice auto-prodotto, o con il solo giudizio del modello, può confermare l’errore o peggiorare una risposta giusta.
Il ciclo dell’agente (osserva, pensa, agisci, ripeti fino alla risposta) è la stessa ossatura del mini-agente di poche decine di righe e degli assistenti che navigano il web ed eseguono codice; la differenza è la robustezza attorno.