Gli agenti AI in produzione si rompono di continuo. Ne ho debuggati abbastanza da riconoscere lo schema, e il colpevole non è quasi mai il modello.
I problemi veri hanno un'altra faccia. Un rate limit alle tre di notte, senza nessun fallback. Una tool call che dopo un crash viene eseguita due volte, e il cliente riceve due email. Un'informazione in memoria che era vera a marzo e che a giugno l'agente usa ancora. Qualcuno "migliora" un prompt, sistema un caso e ne rompe altri undici, e nessuno se ne accorge finché non si lamenta un utente. Un agente con permessi di scrittura su un sistema che doveva solo leggere.
In tutti questi casi il modello ha fatto il suo lavoro. È il sistema intorno che ha ceduto.
Per questo, nel 2026, la domanda utile per chi sviluppa sistemi agentici non è tanto "come rendo l'agente più intelligente?", quanto "in che ambiente il modello che ho già diventa affidabile, osservabile e sicuro?". Il lavoro pubblicato dai grandi laboratori va nella stessa direzione.
L'esempio più chiaro è il resoconto di OpenAI sull'harness engineering. In cinque mesi un team passato da tre a sette ingegneri ha consegnato un prodotto di circa un milione di righe, con circa 1.500 pull request approvate. Ogni riga l'ha scritta Codex: logica applicativa, test, CI, documentazione, osservabilità e tool interni. Gli ingegneri si sono occupati di "progettare gli ambienti, specificare l'intento e costruire i cicli di feedback". La loro sintesi: gli umani guidano, gli agenti eseguono.
L'agente non è il sistema
L'immagine classica di un agente è un LLM in un ciclo, con qualche tool:
User → LLM → Tool → Observation → LLM → AnswerQuel ciclo resta il nucleo giusto. In produzione però sta dentro diversi livelli, e ognuno è responsabile di un problema diverso:
WORK issues, jobs, events, schedules
↓
ORCHESTRATION what runs, when, how many, retries
↓
HARNESS the agent loop: state, context, tool calls, interrupts
↓
CAPABILITIES tools (MCP), skills, memory — scoped and permissioned
↓
SANDBOX where actions actually execute
↓
EVALUATION did the work succeed? → done, retry, or replanLo disegno così per rendere evidente chi è responsabile di cosa. L'orchestratore non deve ragionare sul codice, e l'agente non deve gestire una coda di mille job. La sandbox non ha bisogno di conoscere l'obiettivo di business. Quando qualcosa si rompe, devi sapere quale livello ne risponde. Se tutto vive dentro un unico agent.run(task), non ne risponde nessuno.
1. Parti dal ciclo più piccolo che funziona
Prima di aggiungere qualsiasi livello, conviene interiorizzare la lezione opposta: quasi tutti i problemi richiedono meno agente di quanto si pensi.
mini-SWE-agent lo dimostra meglio di qualsiasi ragionamento. La classe dell'agente è di circa 100 righe di Python, il modello ha a disposizione soltanto una shell bash, e il risultato supera comunque il 74% su SWE-bench Verified, con le esecuzioni migliori della classifica ufficiale bash-only al 76,8%. Spesso lo scaffolding in più ti compra soltanto un sistema più difficile da debuggare.
La mia regola è disegnare la macchina a stati prima di scrivere un solo prompt, e poi guardare con attenzione ogni nodo. Un numero sorprendente di nodi non sono passi di ragionamento. Una ricerca è una query SQL. Una decisione di routing con tre esiti fissi è un if. Ogni nodo che trasformi in codice normale non può allucinare, non costa nulla per chiamata e si testa in pochi minuti.
L'agente si guadagna il posto dove l'input è disordinato: testo libero, documenti, decisioni che richiedono giudizio. Per tutto il resto, scrivi codice.
2. L'affidabilità sta nell'harness
L'harness è il runtime intorno al modello. Costruisce prompt e contesto, esegue i tool, tiene lo stato, gestisce interruzioni e retry, e salva tutto. Non entusiasma nessuno, eppure decide se il tuo agente arriva vivo alla fine di un martedì qualunque.
La scelta che mi ha risparmiato più problemi è la durable execution fin dal primo giorno. Un'esecuzione di un agente dura da pochi secondi a qualche ora, e in quel tempo i processi crashano, i deploy riavviano i pod e le API vanno in timeout. Se lo stato dell'agente vive solo in memoria, ognuno di questi eventi fa perdere lavoro. A volte succede di peggio: il lavoro viene ripetuto.
In LangGraph significa un checkpointer su ogni grafo e gli effetti collaterali dentro @task, così in caso di replay viene restituito il risultato già registrato invece di rieseguire l'azione:
from langgraph.func import task
@task
def send_follow_up(deal_id: str, draft: str) -> str:
# Runs once. On replay after a crash, the cached result is returned
# instead of sending the email a second time.
return email_client.send(deal_id, draft)Senza questo, arriva un bug che conosco bene: il workflow invia un'email, crasha prima di salvare lo stato, riparte dall'ultimo checkpoint e invia l'email di nuovo. Sono gli effetti collaterali idempotenti a trasformare quel crash in un retry innocuo.
C'è anche uno schema più ampio. Sia Codex di OpenAI (un runtime in Rust dietro un "App Server" JSON-RPC bidirezionale) sia l'Agent Server di OpenHands (REST e WebSocket, con ogni azione e osservazione rappresentata come evento serializzabile) mettono un confine di servizio intorno al runtime dell'agente. L'harness gira come processo a sé, con un protocollo davanti, separato dalla tua applicazione. Impacchettato così, un orchestratore può avviare, mettere in pausa, ispezionare e terminare un agente come farebbe con qualsiasi altro servizio.
3. Metti il determinismo intorno al modello
Le API degli LLM vanno giù. I rate limit scattano, e la latenza esplode senza preavviso. Un agente in produzione ha bisogno di un piano per ciascuno di questi casi che non dipenda dalla disponibilità del modello.
Ogni agente che rilascio ha un fallback deterministico, con un circuit breaker perché un provider in difficoltà non venga bombardato di retry:
@task
async def score_risk(item: Item) -> RiskScore:
if breaker.is_open(): # 3 consecutive failures → 60s cooldown
return rule_based_score(item)
try:
result = await risk_agent.ainvoke(item)
breaker.record_success()
return result
except (RateLimitError, TimeoutError):
breaker.record_failure()
return rule_based_score(item)Il punteggio basato su regole è più grezzo di quello del modello. Per me va bene così. Una risposta un po' peggiore è meglio di una pagina di errore, e il sistema continua a funzionare mentre il provider si riprende.
In generale rendo il sistema rigido dove la rigidità costa poco, e lascio al modello solo i punti che richiedono giudizio:
| Deterministico | Modello |
|---|---|
| Permessi, timeout, budget | Classificare input disordinati |
| Stato del workflow e retry | Pianificare e scomporre il lavoro |
| Esecuzione dei test, validazione, schemi | Scrivere bozze di testo o codice |
| Policy di sicurezza, gate di approvazione | Diagnosi e sintesi |
Lo structured output sta nella colonna di sinistra. Se il passo successivo deve leggere la risposta del modello, fatti restituire uno schema e validalo. Le regex sul testo libero prima o poi ti tradiscono.
4. L'approvazione umana è una scelta di architettura
Spesso lo "human in the loop" viene aggiunto alla fine, come funzionalità dell'interfaccia. È troppo tardi. Deve stare nella macchina a stati, perché quello che stai davvero decidendo è dove l'agente si ferma.
Seguo una regola sola: l'agente prepara, una persona conferma. Vale per tutto ciò che ha conseguenze fuori dal sistema, come pagamenti, messaggi ai clienti, ordini o cancellazioni. L'agente fa tutto il lavoro fino alla decisione, poi il grafo si interrompe e salva il proprio stato finché qualcuno non approva:
from langgraph.types import interrupt
def place_order(state: State) -> State:
decision = interrupt({"proposed_order": state["draft_order"]})
if decision["approved"]:
return {"order_id": erp.create_order(state["draft_order"])}
return {"status": "rejected", "reason": decision.get("reason")}Dato che l'interruzione è persistente, la risposta può arrivare dopo cinque secondi o dopo cinque giorni. Anche i rifiuti servono. Un "no" con una motivazione è un esempio etichettato di un errore dell'agente, e finisce dritto nel dataset di valutazione (ne parlo nella sezione 7).
5. Dai all'agente i permessi che daresti a un junior
Tutto ciò che un agente può toccare (tool, dati, rete) è la sua superficie d'attacco. Nella mia esperienza il guasto tipico raramente è malevolo. È un agente con credenziali troppo ampie che fa qualcosa di apparentemente sensato nel posto sbagliato.
Ecco cosa applico a ogni sistema.
Ogni tool riceve il privilegio minimo necessario. Un tool che legge gli ordini ha una credenziale in sola lettura, e la scrittura sta in un tool separato, dietro un gate di approvazione.
Le azioni su codice e shell girano in una sandbox con una network policy esplicita e un audit trail di ogni comando. Gestirlo a mano smette di funzionare già con un paio di agenti, ed è per questo che ho costruito nemoclaw-hub, per gestire più agenti in sandbox da un unico punto.
MCP è il confine verso i tool, quindi è anche il posto giusto per le policy. Il Model Context Protocol (articolo in inglese) standardizza il modo in cui gli agenti raggiungono i tool, e ha un modello di sicurezza che vale la pena leggere. Due errori ricorrono spesso: passare il token dell'utente direttamente alle API a valle, e il confused deputy, cioè un server con privilegi ampi che agisce per conto di un chiamante che quei privilegi non dovrebbe averli.
Quando più clienti condividono lo stesso agente, vanno isolati a livello di storage. Checkpoint, memorie e store devono essere separati davvero, perché un WHERE tenant_id = ? dimenticato è un data leak. langgraph-tenancy si occupa di questo isolamento, e la misurazione dei consumi per tenant sta nello stesso punto.
6. Il contesto va progettato
L'istinto è dare al modello di più: system prompt più lunghi, cronologie complete, tutti i documenti disponibili. Questo approccio si rompe in tre modi. Troppo contesto costa e distrae il modello. Troppo poco, e l'agente dimentica cosa stava facendo. Il caso peggiore è il contesto sbagliato, perché un'informazione obsoleta inserita con sicurezza è qualcosa su cui l'agente agirà.
Tre pratiche hanno retto alla prova dei fatti.
La prima è dividere la conoscenza in moduli. Le Agent Skills, introdotte da Anthropic a ottobre 2025 e poi pubblicate come standard aperto supportato da Codex e da altri strumenti, sono cartelle con un file SKILL.md, script e riferimenti che l'agente carica solo quando servono. La specializzazione diventa un insieme di file che puoi versionare, revisionare e testare. Lo stesso vale per il contesto del repository: un AGENTS.md e documenti di architettura che l'agente può leggere, con i vincoli importanti fatti rispettare da linter e test. Se una regola conta, deve poterla verificare una macchina.
La seconda è trattare la memoria come dati con un ciclo di vita. I prezzi cambiano, i responsabili cambiano, gli stati cambiano, e una memoria che si limita ad accodare finirà per contraddirsi. Il design su cui mi sono assestato funziona così. Un nuovo valore sostituisce il precedente. Una rimozione ritira il fatto invece di cancellarlo. La storia resta interrogabile, così puoi ancora rispondere a "cosa sapevamo a marzo?". È l'idea alla base di vayl. Verificare che i fatti salvati siano ancora veri è un lavoro diverso, e lo fa MemGuard. Livelli, compattazione e decadimento sono spiegati nel mio articolo sull'architettura della memoria in produzione (in inglese).
La terza è un budget per la finestra di contesto. Assegna a istruzioni, task, documenti recuperati e memorie un proprio numero di token, e decidi in anticipo cosa viene scartato quando si sfora. Se non lo decidi tu, viene scartato quello che è arrivato per ultimo.
7. La valutazione definisce cosa vuol dire "fatto"
Il ciclo che vedo più spesso è questo: cambi il prompt, provi tre esempi, decidi che "sembra meglio", rilasci. È testare in produzione con qualche passaggio in più.
Quello che funziona è trattare il comportamento dell'agente come codice sotto test:
production traces → failures → dataset → evaluator → fix → re-run all → deployI casi di test migliori vengono dai fallimenti reali. Su un agente di matching, ogni raccomandazione rifiutata da un utente finiva nel dataset di valutazione. Ho scritto un evaluator mirato per ogni tipo di errore e rieseguito l'intero set prima di ogni modifica. In tre mesi l'accuratezza del matching è salita dal 72% al 91%. Il modello è rimasto lo stesso per tutto il tempo: semplicemente non abbiamo mai rilasciato due volte lo stesso errore. (Gli strumenti per farlo sono nella mia guida a LangSmith, in inglese.)
I prompt meritano la stessa disciplina, perché modificare un prompt è modificare codice, e serve una suite di regressione davanti. Ho costruito prompt-diff proprio per questo: un prompt riscritto deve superare una suite di test in YAML prima di poter sostituire quello vecchio. Superpowers applica lo stesso ragionamento alle skill degli agenti. Osservi l'agente fallire senza la skill, scrivi la skill, poi verifichi che il comportamento sia davvero cambiato. È TDD applicato al comportamento degli agenti, e credo sia il modello mentale giusto.
Altre due cose che ho imparato.
Controlla come l'agente ci è arrivato, oltre a cosa ha prodotto. Per un agente che scrive codice, un diff finale che passa i test dice poco da solo. Ha toccato i file giusti? Ha preservato il comportamento esistente? Si è fermato quando serviva un'approvazione? Alla maggior parte di queste domande risponde la trace.
Un modello piccolo ben calibrato spesso vale quanto uno grande. Su un agente di scoring sono passato da un modello grande a uno piccolo con tre esempi few-shot di calibrazione, e ho ottenuto la stessa qualità sul set di valutazione spendendo circa l'85% in meno. Sapevo che il cambio era sicuro solo perché il set di valutazione esisteva.
8. Orchestrazione: quando un agente diventa tanti
Fin qui si è parlato di rendere affidabile un singolo agente. L'orchestrazione comincia quando hai molte esecuzioni in parallelo su un flusso continuo di lavoro, e le domande smettono di riguardare il ragionamento e diventano operative:
Which work items are eligible? How many run at once?
What happens when one fails? What if the work item changes mid-run?
Which workspace belongs to which run? How do we recover after a crash?Le strade sono due.
La prima è un orchestratore costruito apposta per il lavoro. Symphony di OpenAI è l'esempio pubblico più chiaro per il lavoro sul codice. Legge le issue da un tracker (Linear, nel setup di OpenAI), assegna a ciascuna un workspace isolato e una propria esecuzione dell'agente, e gestisce concorrenza, retry e riconciliazione. La specifica dice esplicitamente che non vuole essere un workflow engine generico, e OpenAI l'ha rilasciato come engineering preview. Mi piace molto come inquadra il problema: gestire il lavoro invece di supervisionare gli agenti.
La seconda è un motore di durable execution generico. Temporal e Conductor registrano ogni passo in una event history, così un workflow che crasha riparte esattamente da dove si era fermato. Nessuno dei due è un framework per agenti. Sono il livello deterministico dentro cui fai girare gli agenti. Conductor può persino chiamare agenti costruiti con LangGraph, OpenAI Agents o Google ADK come task nativo AGENT, e registra la scoperta e le chiamate ai tool MCP come passi del workflow che puoi ispezionare.
Questo chiarisce anche il ruolo di LangGraph. È ottimo dentro il livello dell'agente, quando un agente ha bisogno di un controllo con stato e a forma di grafo. Non deve per forza essere l'intera piattaforma.
Agenti costruiti con framework diversi possono parlarsi tramite il protocollo A2A, che gestisce la scoperta con le Agent Card e gli aggiornamenti dei task tramite polling, streaming o notifiche push. MCP copre invece il lato agente-tool. A2A diventa utile quando hai davvero agenti di tipo diverso che devono comunicare, il che succede molto più tardi di quanto suggeriscano i diagrammi di architettura.
Cosa adottare, e quando
Adottare tutto questo insieme è il modo più rapido per costruire qualcosa di fragile. Ecco cosa metterei in piedi a ogni fase:
| Fase | Adotta | Rimanda |
|---|---|---|
| Un agente, primi utenti | Nodi deterministici dove possibile, stato persistente, effetti collaterali idempotenti, un gate di approvazione, tracing | Orchestratori, A2A, multi-agente |
| Agente in uso quotidiano | Dataset di valutazione dai fallimenti reali, test di regressione sui prompt, fallback e circuit breaker, credenziali con permessi minimi | Orchestrazione su misura |
| Più agenti o più clienti | Isolamento per tenant, sandbox con network policy, memoria con sostituzione dei fatti, budget di costo | A2A, a meno che gli agenti siano davvero eterogenei |
| Molte esecuzioni parallele su una coda di lavoro | Un orchestratore durable (Temporal, Conductor o un gestore di lavoro alla Symphony) | Niente, a questo punto |
In sintesi
I modelli contano, e migliorano di continuo. Eppure i miglioramenti più grandi che ho visto in produzione raramente sono arrivati dal cambiare modello. Sono arrivati dal trasformare in codice i finti passi di ragionamento, dal rendere ogni effetto collaterale persistente e idempotente, e dal fermare l'agente prima di qualsiasi azione con conseguenze. Sono arrivati dal dare a ogni agente esattamente i permessi che gli servono, dal trattare contesto e memoria come dati da progettare, e dal rifiutarsi di rilasciare una modifica che il set di valutazione non ha visto.
Costruire l'agente ormai è la parte facile. La competenza più difficile è decidere dove finisce la sua responsabilità e dove deve subentrare il sistema intorno.
Fonti
- OpenAI, Harness engineering: leveraging Codex in an agent-first world, febbraio 2026
- OpenAI, Unlocking the Codex harness e openai/codex
- OpenAI, Symphony e annuncio, aprile 2026
- OpenHands Software Agent SDK
- mini-SWE-agent e la classifica di SWE-bench
- Specifica delle Agent Skills
- obra/superpowers
- Temporal, Event History
- Conductor OSS e le sue ricette per framework di agenti
- Specifica del protocollo A2A