Il problema: sapere che cosa fa il modello a ogni giro
Nel primo articolo abbiamo visto che un agente è un ciclo in cui un modello sceglie il passo successivo. Qui apriamo quel ciclo con tre strumenti di lettura che ti servono per costruirlo e per capire perché a volte sbaglia: il ciclo OODA, che descrive la struttura di una decisione ripetuta; il pattern ReAct, che dice in che forma il modello alterna ragionamento e azione; il tool calling, che è il formato tecnico con cui l'azione arriva al tuo codice. Sono tre livelli diversi dello stesso ciclo, e il listato che costruiamo li mette insieme in meno di quaranta righe.
Il ciclo OODA: osserva, orientati, decidi, agisci
OODA (Observe, Orient, Decide, Act) è un modello decisionale attribuito al colonnello dell'aeronautica statunitense John Boyd, nato per descrivere il combattimento aereo. Il suo punto è che il vantaggio sta nel completare il ciclo osservazione-decisione-azione più in fretta e con meno errori dell'avversario, non nel prendere la singola decisione perfetta. Applicarlo agli agenti è un'analogia nostra, utile come griglia di lettura: Boyd non parlava di modelli linguistici, e il resto di questa sezione è un'interpretazione, non una citazione.
Osservare, per un agente, vuol dire assemblare il contesto: la richiesta dell'utente, i risultati degli strumenti, ciò che la memoria restituisce, gli errori arrivati dall'ultima azione (un codice HTTP 429, per esempio, che dice di rallentare). Orientarsi è dare un senso a quei dati e coincide con il ragionamento del modello sul contesto; è anche la fase in cui il prompt di sistema pesa di più, perché orienta cosa conta. Decidere è scegliere tra chiamare uno strumento, rispondere, chiedere un chiarimento, cambiare piano o fermarsi. Agire è eseguire la scelta, e il risultato diventa la prossima osservazione.
| Fase OODA | Che cosa fa l'agente | Dove sta nel codice |
|---|---|---|
| Observe | Raccoglie richiesta, osservazioni, memoria | Costruzione del contesto da passare al modello |
| Orient | Interpreta il contesto | Dentro la chiamata al modello (ragionamento) |
| Decide | Sceglie azione o risposta | L'analisi (parsing) dell'uscita del modello |
| Act | Esegue lo strumento | Il dispatch verso la funzione, con gestione dell'errore |
La tabella dice una cosa utile: due delle quattro fasi (Orient e Decide) stanno dentro il modello, e quindi non puoi controllarle riga per riga; le altre due (Observe e Act) sono codice tuo, e lì si decide quanto contesto dare e quali azioni consentire. Il tempo di ciclo di Boyd diventa, per un agente, il numero di giri e la durata di ciascuno: strumenti piccoli e veloci permettono più giri correttivi di un unico strumento lento che aggrega dieci fonti prima di rispondere.
ReAct: pensiero e azione intrecciati
ReAct è descritto nell'articolo di Yao e coautori, pubblicato su arXiv nell'ottobre 2022 (ReAct: Synergizing Reasoning and Acting in Language Models). Il punto di partenza è che il ragionamento passo-passo (chain-of-thought) e la generazione di azioni erano stati studiati separatamente. ReAct fa produrre al modello tracce di ragionamento e azioni in modo intrecciato: il ragionamento aiuta a costruire, seguire e aggiornare il piano e a gestire le eccezioni, mentre le azioni permettono di interrogare fonti esterne. Sugli ambienti interattivi ALFWorld e WebShop, secondo l'abstract, ReAct supera i metodi di imitazione e apprendimento per rinforzo di 34 e 10 punti percentuali assoluti di successo, con uno o due esempi nel prompt; su HotpotQA e Fever riduce, interrogando una semplice API di Wikipedia, le allucinazioni e la propagazione degli errori tipiche della sola chain-of-thought. Sono risultati su quei benchmark: non dicono quanto ReAct renda il tuo agente.
Il ciclo ha tre componenti che si ripetono: un Thought in cui il modello dichiara cosa sa e cosa gli manca, un'Action con lo strumento e gli argomenti, e un'Observation con il
risultato che torna nel contesto. Il listato lo realizza con un modello finto che emette testo nel formato ReAct: l'espressione regolare FORMATO separa pensiero, nome dell'azione e argomenti JSON.
Tre scelte meritano attenzione. L'eccezione dello strumento non interrompe il programma ma diventa un'osservazione («Errore: ...»), così il modello può correggersi. Il tetto max_iterations evita
che il ciclo giri senza fine. La guardia sulle ultime tre azioni identiche interrompe chi ripete lo stesso tentativo fallito.
import json
import re
# Modello finto: restituisce testi nel formato ReAct, uno per giro.
COPIONE_OK = [
'Thought: Mi serve lo stato dell\'ordine, lo cerco.\nAction: cerca_ordine({"id": "A-1042"})',
'Thought: Ho lo stato, posso rispondere.\nAction: final_answer({"answer": "Ordine A-1042: in consegna."})',
]
COPIONE_CICLO = ['Thought: Riprovo.\nAction: cerca_ordine({"id": "A-9999"})'] * 6
ORDINI = {"A-1042": "in consegna"}
def cerca_ordine(id):
if id not in ORDINI:
raise ValueError(f"ordine {id} inesistente")
return ORDINI[id]
STRUMENTI = {"cerca_ordine": cerca_ordine}
FORMATO = re.compile(r"Thought:\s*(.*?)\nAction:\s*(\w+)\((.*)\)\s*$", re.S)
def react_loop(copione, max_iterations=5):
contesto, ultime = [], []
for i in range(max_iterations):
testo = copione[i] # qui andrebbe llm.generate(contesto)
m = FORMATO.match(testo)
if not m:
contesto.append("Observation: formato non valido, usa Thought/Action")
continue
thought, nome, args = m.group(1), m.group(2), json.loads(m.group(3))
if nome == "final_answer":
return args["answer"], i + 1, contesto
ultime.append((nome, json.dumps(args, sort_keys=True)))
if len(ultime) >= 3 and len(set(ultime[-3:])) == 1:
return "Interrotto: stessa azione per 3 volte", i + 1, contesto
try:
osservazione = STRUMENTI[nome](**args)
except Exception as e: # l'errore diventa un'osservazione
osservazione = f"Errore: {e}"
contesto += [f"Thought: {thought}", f"Action: {nome}({args})",
f"Observation: {osservazione}"]
return "Impossibile completare nel limite di iterazioni", max_iterations, contesto
for etichetta, copione in (("ok", COPIONE_OK), ("ciclo", COPIONE_CICLO)):
risposta, giri, contesto = react_loop(copione)
print(f"[{etichetta}] giri={giri} risposta={risposta!r}")
for riga in contesto:
print(" ", riga)
# Output:
# [ok] giri=2 risposta='Ordine A-1042: in consegna.'
# Thought: Mi serve lo stato dell'ordine, lo cerco.
# Action: cerca_ordine({'id': 'A-1042'})
# Observation: in consegna
# [ciclo] giri=3 risposta='Interrotto: stessa azione per 3 volte'
# Thought: Riprovo.
# Action: cerca_ordine({'id': 'A-9999'})
# Observation: Errore: ordine A-9999 inesistente
# Thought: Riprovo.
# Action: cerca_ordine({'id': 'A-9999'})
# Observation: Errore: ordine A-9999 inesistente
Il programma è stato eseguito due volte con Python 3.14.4 (python3 -I), con uscita identica. Nel primo scenario il modello cerca l'ordine, vede «in consegna» e chiude con final_answer al secondo giro.
Nel secondo, il copione ripete la stessa chiamata a un ordine inesistente: dopo due osservazioni di errore la guardia scatta al terzo giro, prima di eseguire l'azione, ed è per questo che il contesto contiene
solo due scambi. Se togli la guardia, il ciclo finisce comunque al limite di cinque iterazioni, ma spende cinque chiamate al modello per niente. Il modello è finto: il listato prova la struttura del ciclo, non la
capacità di un modello vero di rispettare il formato, e un modello reale a volte produce testo che FORMATO non riconosce (il ramo continue con l'avviso è lì per questo).
Che cosa dà e che cosa costa ReAct
- Tracce leggibili: ogni Thought, Action e Observation si registra e si rilegge, il che rende il debug molto più semplice che con una sola risposta opaca.
- Ancoraggio ai fatti: le osservazioni portano nel contesto dati esterni, il che può ridurre le allucinazioni, ma non le elimina: il modello può ignorare o fraintendere l'osservazione.
- Costo: ogni giro è una chiamata, e il contesto cresce a ogni giro; il costo cresce più che linearmente se rimandi ogni volta l'intera cronologia.
- Il Thought è testo: il Thought è testo generato, non una finestra sul calcolo interno; trattalo come indizio per il debug, non come prova.
Le prime due voci sono i motivi per cui il pattern funziona, le ultime due sono i motivi per cui non basta. Una regola prudente: registra le tracce per diagnosticare, ma non fidarti di un Thought come garanzia del comportamento; la garanzia sta nei controlli sugli argomenti e sui permessi, che mettiamo nella sezione successiva.
Tool calling: il formato dell'azione
Nel listato precedente l'azione era testo da analizzare con un'espressione regolare, fragile per natura. Il tool calling (o function calling) risolve il problema spostando il formato dentro il
protocollo del modello: tu dichiari gli strumenti con uno schema, e il modello risponde con una richiesta strutturata invece che con testo libero. La documentazione di Ollama mostra il formato: nella richiesta una lista
tools di oggetti con type: "function" e, dentro function, name, description e parameters (uno schema JSON); nella risposta un messaggio
dell'assistente con tool_calls, ciascuna con name e arguments; il risultato torna al modello come messaggio con ruolo tool e il campo tool_name. Il
modello non esegue nulla: propone una chiamata, e sei tu a eseguirla.
Per questo il lavoro importante sta nella validazione. Il listato seguente definisce lo schema di uno strumento search_orders e una funzione valida che controlla una chiamata
proposta contro lo schema: campi obbligatori, campi sconosciuti, tipo, valori dell'enum, e applicazione del valore predefinito di limit. Il valore predefinito lo applica il tuo codice, non il modello:
lo schema lo dichiara, ma nulla garantisce che il modello lo rispetti. In produzione useresti una libreria di validazione di schemi JSON; qui è scritta a mano per mostrare cosa controlla.
import json
# Schema nel formato usato da Ollama e dalle API compatibili OpenAI.
TOOL = {
"type": "function",
"function": {
"name": "search_orders",
"description": ("Cerca ordini nel gestionale. Usalo quando l'utente chiede "
"di un ordine; non per domande sui prezzi."),
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"filter_type": {"type": "string", "enum": ["name", "email", "order_id"]},
"limit": {"type": "integer", "default": 10},
},
"required": ["query", "filter_type"],
},
},
}
TIPI = {"string": str, "integer": int}
def valida(call):
"""Controlla gli argomenti contro lo schema; restituisce (argomenti, errore)."""
schema = TOOL["function"]["parameters"]
args = dict(call["function"]["arguments"])
for campo in schema["required"]:
if campo not in args:
return None, f"manca il campo obbligatorio '{campo}'"
for campo, valore in args.items():
prop = schema["properties"].get(campo)
if prop is None:
return None, f"campo sconosciuto '{campo}'"
if not isinstance(valore, TIPI[prop["type"]]):
return None, f"'{campo}' deve essere {prop['type']}"
if "enum" in prop and valore not in prop["enum"]:
return None, f"'{campo}' deve essere uno di {prop['enum']}"
for campo, prop in schema["properties"].items():
if campo not in args and "default" in prop:
args[campo] = prop["default"] # il default lo applica il runtime
return args, None
def esegui(call):
args, errore = valida(call)
if errore:
return {"role": "tool", "tool_name": call["function"]["name"], "content": f"Errore: {errore}"}
return {"role": "tool", "tool_name": call["function"]["name"],
"content": json.dumps({"results": [], "total_count": 0, "args": args}, sort_keys=True)}
chiamate = [
{"function": {"name": "search_orders", "arguments": {"query": "Mario Rossi", "filter_type": "name"}}},
{"function": {"name": "search_orders", "arguments": {"query": "Mario Rossi", "filter_type": "nome"}}},
{"function": {"name": "search_orders", "arguments": {"filter_type": "name"}}},
]
for c in chiamate:
print(esegui(c))
# Output:
# {'role': 'tool', 'tool_name': 'search_orders', 'content': '{"args": {"filter_type": "name", "limit": 10, "query": "Mario Rossi"}, "results": [], "total_count": 0}'}
# {'role': 'tool', 'tool_name': 'search_orders', 'content': "Errore: 'filter_type' deve essere uno di ['name', 'email', 'order_id']"}
# {'role': 'tool', 'tool_name': 'search_orders', 'content': "Errore: manca il campo obbligatorio 'query'"}
Eseguito due volte, con uscita identica. La prima chiamata è valida e il runtime completa gli argomenti con limit 10. La seconda propone "nome" al posto di "name": viene
rifiutata con un messaggio che elenca i valori ammessi, ed è quel messaggio, restituito come risultato dello strumento, che permette al modello di correggersi al giro dopo. La terza non ha il campo query. L'errore
restituito deve dire che cosa correggere: un generico «errore» non basta al modello per riprovare meglio. Il listato non esegue ricerche vere (results è vuoto): prova la validazione, non il gestionale.
La descrizione dello strumento conta più del codice
Il modello non vede il tuo codice: vede nome, descrizione e schema. Una descrizione come «Cerca nel database» non gli dice quale database, che dati contenga, né quando preferirla ad altri strumenti. Una descrizione utile risponde a tre domande: cosa fa lo strumento, quando usarlo (e implicitamente quando no), e che formato hanno gli argomenti, per esempio «ID ordine nel formato ORD-AAAA-NNNN». Nel listato la descrizione dice anche che non va usato per domande sui prezzi: indicare il confine riduce le chiamate sbagliate. Come provarla: scrivi richieste ambigue e guarda quale strumento sceglie il modello, ma questo si può fare solo con un modello vero; qui non è stato eseguito. Il disegno degli strumenti è approfondito in Tool Calling Avanzato: API, Web e Custom Tools.
Come si scelgono i pattern
| Caratteristica | Chain-of-Thought | Tool calling da solo | ReAct |
|---|---|---|---|
| Ragionamento visibile | Sì | Non necessariamente | Sì (Thought) |
| Azioni esterne | No | Sì | Sì |
| Dati aggiornati | Solo la conoscenza del modello | Sì, via strumenti | Sì, via strumenti |
| Giri al modello | Uno | Uno o pochi | Molti, non noti in anticipo |
| Quando conviene | Ragionamento puro: logica, aritmetica | Compiti con un'azione sola | Compiti dove il passo dipende dall'esito del precedente |
I tre pattern non si escludono: ReAct è, in pratica, tool calling dentro un ciclo con ragionamento esplicito, e i modelli moderni spesso ragionano internamente senza che tu chieda un «Thought». Il criterio di scelta è lo stesso del primo articolo: usa il più semplice che risolve il caso. Una sola azione e nessuna dipendenza dai risultati? Una chiamata con strumento. Un compito dove il passo successivo dipende da ciò che hai scoperto? Il ciclo.
Tree of Thoughts e pianificazione anticipata
Tree of Thoughts (Yao e coautori, maggio 2023) generalizza la chain-of-thought: il modello esplora più percorsi di ragionamento, valuta se stesso e torna indietro quando serve. Nell'abstract, sul problema Game of 24, GPT-4 con la sola chain-of-thought risolve il 4% dei compiti e con ToT il 74%. È un risultato su un compito costruito per richiedere esplorazione; il prezzo è che servono molte generazioni per ogni nodo, quindi molte più chiamate di un singolo ciclo.
La pianificazione anticipata (plan-first) è un'altra strategia: l'agente scrive prima un piano e poi lo esegue, riplanificando se un passo fallisce. È utile quando il compito è lungo e vuoi una visione d'insieme; il rischio è che il piano poggi su ipotesi che l'esecuzione smentisce. Lo schema sotto è un esempio di formato, non un listato eseguito.
[Piano]
1. Recuperare gli sprint chiusi nel trimestre
2. Calcolare velocity media e tendenza
3. Elencare i blocchi più frequenti
4. Scrivere il rapporto
[Esecuzione]
- Se il passo 1 non restituisce sprint: riplanificare (chiedere il periodo corretto)
- Se emerge un dato nuovo: aggiungere un passo
- Se un passo non serve più: saltarlo, e dirlo nel registro
Lo schema mostra la differenza di fondo con ReAct: qui il piano è un oggetto che puoi mostrare all'utente per approvarlo prima di spendere chiamate, mentre in ReAct il piano resta implicito nelle tracce. Le implementazioni pratiche combinano le due cose: piano iniziale e possibilità di correggerlo a ogni giro. Non è un esempio eseguito, ed è una schematizzazione: ogni framework lo realizza a modo suo.
Errori comuni nella progettazione
- Cicli senza fine: manca un tetto ai giri o un criterio di arresto. Rimedio:
max_iterationse rilevazione di azioni ripetute, come nel primo listato. - Descrizioni vaghe: lo strumento sbagliato viene scelto o quello giusto ignorato. Rimedio: descrizione con cosa, quando e formato.
- Errori inghiottiti: l'eccezione fa cadere il programma, oppure sparisce senza che il modello la veda. Rimedio: restituire l'errore come osservazione.
- Contesto che esplode: ogni giro aggiunge token. Rimedio: riassumere o troncare, tema di Memory Systems.
- Strumenti troppo piccoli o troppo grandi: troppi giri da un lato, nessun controllo dall'altro. Rimedio: uno strumento, una responsabilità.
- Nessun limite alle azioni: un agente può cancellare dati o spendere denaro. Rimedio: elenco di azioni consentite e conferma umana per quelle irreversibili, come in Security e Safety.
Metà di questi errori si risolve con codice di poche righe già visto sopra (tetto, guardia sulle ripetizioni, errore come osservazione, validazione degli argomenti), l'altra metà con progettazione (descrizioni, granularità, permessi). La prima metà si può coprire con test; la seconda si scopre solo osservando l'agente su casi reali, ed è per questo che nel pezzo su Testing e Evaluation si parte dalle tracce.
Domande frequenti
Che differenza c'è tra ReAct e tool calling?
ReAct è il modo di strutturare il ciclo, alternando ragionamento, azione e osservazione; il tool calling è il formato con cui il modello esprime l'azione, come richiesta strutturata invece che testo da analizzare. Si usano insieme.
Il modello esegue davvero le funzioni?
No. Il modello propone nome e argomenti di una chiamata; il tuo codice la valida, la esegue e restituisce il risultato come messaggio. Questa separazione è il punto in cui metti controlli sugli argomenti e sui permessi.
Come evito che l'agente giri all'infinito?
Imponi sempre un numero massimo di iterazioni e, in più, interrompi quando le ultime azioni sono identiche. Il primo listato di questo articolo fa entrambe le cose e mostra che la guardia sulle ripetizioni scatta prima del limite.
Che cosa sai fare adesso
Sai leggere un agente come un ciclo OODA e dire quali fasi sono nel modello e quali nel tuo codice. Sai scrivere un ciclo ReAct con tetto ai giri, guardia sulle ripetizioni ed errori trasformati in osservazioni, e sai validare una chiamata di strumento contro il suo schema prima di eseguirla. Nel prossimo articolo, LangChain e LangGraph: Agenti con AgentExecutor e Tools, vediamo come un framework offre questo stesso ciclo già pronto e che cosa devi comunque controllare tu. Quando i ruoli diventano più d'uno, lo stesso ciclo si ripete dentro ciascuno e il problema diventa coordinarli: se ne parla in Orchestrazione Multi-Agente: Architetture Production.
Fonti
- ReAct: Synergizing Reasoning and Acting in Language Models (Yao et al., arXiv:2210.03629) - idea del ragionamento intrecciato alle azioni, risultati su ALFWorld e WebShop (+34 e +10 punti assoluti); abstract letto il 2026-11-12
- Tree of Thoughts: Deliberate Problem Solving with Large Language Models (Yao et al., arXiv:2305.10601) - Game of 24: 4% con chain-of-thought contro 74% con ToT; abstract letto il 2026-11-12
- Chain-of-Thought Prompting Elicits Reasoning in Large Language Models (Wei et al., arXiv:2201.11903) - definizione di chain-of-thought; abstract letto il 2026-11-12
- Ollama - Tool calling - formato di
tools,tool_callse del messaggiotoolcontool_name; consultata il 2026-11-12

Commenti
Caricamento commenti...
Accedi per commentare