Con tre agenti, comincio a valutare lo scambio di messaggi
Le blackboard condivise cedono sotto il carico di coordinamento: i failure mode dello stato condiviso e quando serve un message bus con supervisor.
Ho tradotto questo articolo dall’inglese con un modello linguistico. Leggi l’originale in inglese
Aggiornamento, ottobre 2026. Ho corretto la descrizione di condivisione di questo articolo e reso più preciso parte del testo. Il motivo è nella sezione di aggiornamento verso la fine.
Perché conta
Il primo sistema multi-agent che ho costruito usava un dict di stato condiviso, una blackboard: diversi agenti leggevano e scrivevano sullo stesso oggetto, senza un unico proprietario. Tre agenti — uno dedicato alla pianificazione, uno alla ricerca e uno alla scrittura — leggevano e scrivevano tutti nello stesso AgentState di LangGraph. Funzionava nelle demo. Due settimane dopo, con cinque agenti e l'esecuzione concorrente in produzione, ha prodotto errori difficili da diagnosticare.
Non crash. Peggio: output errati in modo poco evidente. L'agente di pianificazione sovrascriveva un risultato della ricerca mentre quello di scrittura lo stava ancora usando. Un agente leggeva uno stato obsoleto da una sessione precedente perché non avevo isolato il checkpoint. Il supervisor instradava il workflow verso un agente che aveva già completato il proprio compito e attendeva il suo stesso output, sovrascritto nel frattempo da un agente eseguito in parallelo.
Lo stato condiviso non è sbagliato. Smetto di fidarmi quando diversi agenti si coordinano tramite scritture sovrapposte. Intorno a tre agenti, comincio a valutare lo scambio di messaggi con un supervisor: gli agenti si inviano messaggi tipizzati e un router decide chi eseguire dopo. La domanda importante è dove questo modello comincia a rompersi e con cosa lo sostituisco quando mi servono stato persistente, ripristino deterministico, eval riproducibili e costi sotto controllo.
1. I tre failure mode delle blackboard condivise
Una blackboard condivisa è qualsiasi design in cui più agenti leggono da e scrivono sullo stesso oggetto di stato senza proprietà esplicita. Include TypedDict piatti di LangGraph senza annotazioni di reducer, dict Python condivisi passati per riferimento e tabelle di database senza locking a livello di riga.
Failure mode 1: conflitti di scrittura. Quando due agenti scrivono lo stesso campo, la regola di merge decide il risultato e quella predefinita raramente corrisponde a ciò che voglio. Senza un reducer, due scritture sulla stessa chiave all'interno di un singolo super-step parallelo non vengono unite silenziosamente — LangGraph genera InvalidUpdateError, che in produzione si manifesta come un crash. Il fallimento meno evidente avviene tra step: un nodo successivo sovrascrive un campo impostato da un nodo precedente e la scrittura persa potrebbe non essere visibile, a meno che non ispezioni lo stato a ogni checkpoint. In entrambi i casi, il modello di coordinamento impone una scelta esplicita.
Failure mode 2: letture obsolete. L'agente B legge uno stato che l'agente A ha scritto due step prima. Questo può essere valido in una pipeline sequenziale. In una pipeline parallela, è un bug di correttezza. Se l'agente B presume che l'output della ricerca sia aggiornato, ma l'agente A ne ha nel frattempo cambiato la direzione, l'agente B può rispondere a una domanda che non viene più posta.
Failure mode 3: replay di un checkpoint obsoleto. Salvare un checkpoint di un sistema multi-agent significa conservare lo stato condiviso in un determinato momento. Riprodurre un checkpoint precedente è previsto: ripristina lo stato storico acquisito in quel punto del workflow. Il problema si presenta quando il sistema tratta quello stato storico come stato corrente del workflow, oppure non riesce a preservare la lineage prevista dei checkpoint durante branching e recovery. Se l'agente C legge lo stato, esegue il lavoro e viene salvato in un checkpoint, poi l'agente A sovrascrive gli stessi campi, il replay dal checkpoint dell'agente C può ripristinare correttamente una versione priva degli aggiornamenti successivi dell'agente A. Il replay può essere coerente al proprio interno, ma inadatto al recovery se il workflow richiede che quegli aggiornamenti restino nella lineage attiva.
Non considero questi problemi bug di LangGraph o del checkpointer. Derivano dal modello di coordinamento basato su stato condiviso. Se aggiungo lock, campi versionati e garanzie read-your-writes intorno allo stato condiviso, comincio a ricostruire parti di un database distribuito. Nella mia esperienza, spesso non è il meccanismo di base che voglio per un sistema di agenti.
2. L'alternativa del message bus
Un message bus cambia il modello di coordinamento. Invece di leggere da un oggetto condiviso, gli agenti si inviano messaggi tipizzati tramite un canale esplicito. Nessun agente legge direttamente l'"output corrente" di un altro agente. Riceve un messaggio che gli è stato inviato deliberatamente.
In LangGraph, di solito modello questo con Annotated[list[Message], operator.add] come canale di coordinamento principale, combinato con un nodo supervisor che legge la coda di messaggi e instrada in base al tipo e al contenuto del messaggio anziché ai campi di stato globali.
La differenza pratica è la proprietà. Ogni agente produce un Message con to, from_, type e payload espliciti. Il supervisor legge la coda, invia i messaggi all'agente appropriato e l'agente elabora la propria inbox. Lo stato è ancora condiviso — la lista messages è globale — ma le scritture sono vincolate. Nessun agente scrive nei "propri" campi; aggiunge solo elementi al canale condiviso.
from typing import Annotated
import operator
class Message (TypedDict ):
id : str
from_: str
to: str
type : Literal ["request" , "result" , "error" , "status" ]
payload: dict
class AgentState (TypedDict ):
messages: Annotated[list [Message], operator.add] # append-only
session_id: str
completed_agents: Annotated[set [str ], operator.or_]
Cosa cambia: il researcher non scrive in research_results. Invia Message(from_="researcher", to="writer", type="result", payload={"findings": ...}). Il nodo writer filtra state["messages"] per i messaggi a esso indirizzati ed elabora la propria inbox. Il solo filtraggio non definisce un'inbox: ogni agente registra anche gli ID dei messaggi consumati o un offset dell'inbox in modo persistente, così replay o re-entry non elaborano nuovamente lo stesso messaggio. Rendo la gestione dei messaggi idempotente affinché una consegna ripetuta con lo stesso ID non produca un secondo effetto collaterale. Non c'è un campo condiviso su cui entrambi gli agenti scrivono; c'è solo un canale a cui entrambi aggiungono elementi.
3. Il pattern supervisor e quando ne vale il costo
Un agente supervisor è un router con memoria. Riceve messaggi, decide quale agente eseguire dopo e mantiene lo stato globale del task. Aggiunge inoltre una chiamata al modello agli step di routing. Nell'ipotesi illustrativa di una chiamata di routing per step, una pipeline di 10 step può aggiungere 10 chiamate LLM.
Il costo è reale. Uso un supervisor solo quando il controllo che aggiunge vale quel costo per il workflow.
Senza un supervisor, aggiungere un nuovo agente può richiedere di aggiornare ogni agente che potrebbe passargli il controllo. Modificare la logica di routing può richiedere di aggiornare le funzioni degli edge. Capire perché un task si è bloccato può richiedere di leggere manualmente l'intera coda di messaggi.
Con un supervisor, la logica di routing è centralizzata. Aggiungere un nuovo agente può significare registrarlo nella configurazione di routing o nell'interfaccia dei tool del supervisor. La decisione di routing può essere rappresentata nel log del supervisor o nel messaggio emesso. L'analisi di un task bloccato può iniziare dall'ultima decisione osservabile del supervisor.
Come regola pratica personale, comincio a considerare un supervisor intorno a tre agenti. Al di sotto di questa soglia, i passaggi diretti tra agenti con edge condizionali sono spesso più economici e più facili da ispezionare. Con tre o più agenti, il routing centralizzato può valere le chiamate aggiuntive, specialmente quando l'insieme di agenti sta ancora cambiando.
L'anti-pattern del supervisor consiste nell'usarlo come un router if-else più elaborato, con regole esplicite come "se l'utente menziona Python, instrada al coder". Un supervisor aggiunge valore quando deve tracciare lo stato del task attraverso più step. Aggiunge poco quando si limita al pattern matching su un campo. Se la logica di routing entra in una switch statement, rimuovo il supervisor e uso edge condizionali.
4. Tool governance nei sistemi multi-agent
Quando passo da un agente a molti, l'accesso ai tool diventa un problema di correttezza, non solo un problema di sicurezza. Un agente con accesso a tool al di fuori del proprio ruolo previsto potrebbe usarli, soprattutto se il suo contesto include istruzioni di un altro agente che suggeriscono di farlo.
Uso un principio semplice: l'accesso ai tool deve corrispondere al ruolo dell'agente, non alla capacità dell'agente. L'agente researcher non dovrebbe avere accesso in scrittura al file system anche se il modello sottostante può generare comandi validi di scrittura su file. Il writer non dovrebbe avere tool di ricerca anche se potrebbe usarli per "verificare" il proprio output — è il compito del researcher.
# Researcher: read-only tools
researcher_agent = create_react_agent(
llm,
tools=[search_web, fetch_url, read_document],
prompt="You retrieve and synthesize information. Do not write or modify files."
)
# Writer: write tools only, no search
writer_agent = create_react_agent(
llm,
tools=[write_draft, format_output],
prompt="You write and format content based on researcher findings. Do not search."
)
# Supervisor: routing tools only, no domain tools
supervisor_agent = create_react_agent(
llm,
tools=[route_to_researcher, route_to_writer, mark_complete],
prompt="You coordinate the research and writing workflow."
)
Non tratto questo principalmente come una misura di sicurezza. Lo uso per ridurre lo spazio d'azione dell'agente a ciò che è appropriato per il suo ruolo. In questa configurazione, il researcher non ha tool per scrivere file, il writer non ha tool di ricerca e il supervisor non ha tool per agire sul dominio.
5. Rilevamento degli errori e recovery
I sistemi a stato condiviso possono lasciare poche evidenze locali. I sistemi basati sullo scambio di messaggi rendono gli errori più facili da osservare quando consegna e logging sono espliciti: posso ispezionare la coda, confrontare i messaggi inviati con quelli ricevuti e identificare le richieste senza una risposta registrata.
Il meccanismo di recovery è semplice: se un agente non ha risposto entro un timeout, il supervisor può instradare di nuovo la richiesta o inoltrarla a human-in-the-loop (HITL). Questo richiede che ogni messaggio di richiesta abbia un id e che il supervisor mantenga un registro delle richieste in sospeso.
import time
class SupervisorState (TypedDict ):
messages: Annotated[list [Message], operator.add]
outstanding: dict [str , float ] # message_id -> sent_at timestamp
escalated: Annotated[set [str ], operator.or_]
def supervisor_node (state: SupervisorState) -> dict :
now = time.time()
for msg_id, sent_at in list (state["outstanding" ].items()):
if now - sent_at > 30 : # 30-second timeout
outstanding = dict (state["outstanding" ])
outstanding.pop(msg_id) # remove before emitting recovery
return {
"outstanding" : outstanding,
"escalated" : {msg_id},
"messages" : [Message(
id =new_id(),
from_="supervisor" ,
to="hitl" ,
type ="request" ,
payload={"reason" : f"Agent timeout on message {msg_id}" }
)]
}
pending = [m for m in state["messages" ] if m["to" ] == "supervisor" and m["type" ] == "result" ]
# route based on pending results ...
Prima di emettere il messaggio di recovery, rimuovo la richiesta da outstanding e ne contrassegno l'ID come già inoltrato. Il registro deve conservare questa transizione affinché un replay o un'altra invocazione del supervisor possano distinguere una richiesta già inoltrata da una nuova.
Tratto questo pattern — un registro delle richieste in sospeso con timeout ed escalation — come un pattern di monitoraggio dei timeout e gestione delle dead letter. È simile nello spirito a un circuit breaker, ma il suo scopo è rilevare lavoro bloccato e instradare il recovery anziché impedire chiamate a una dipendenza in errore. Rende esplicito lo stato di recovery invece di lasciare il workflow in attesa indefinita senza una transizione registrata.
6. Pattern di coordinamento per dimensione del sistema
Scelgo il modello di coordinamento in base al numero di agenti e a come interagiscono. Questi intervalli sono euristiche personali:
2 agenti: Passaggio diretto. L'agente A viene eseguito, scrive l'output in un campo denominato con un reducer chiaro e l'agente B legge quel campo. Non serve un supervisor. Un edge condizionale instrada da A a B in base allo stato dell'output di A. Questo è il design più semplice che uso per questo tipo di problema. Evito di aggiungere meccanismi di coordinamento prima di averne bisogno.
3–5 agenti: Supervisor con message bus. Il supervisor instrada tra gli agenti. Gli agenti comunicano attraverso il canale di messaggi append-only, non attraverso campi denominati. Ogni agente filtra la propria inbox e traccia in modo persistente gli ID dei messaggi consumati o un offset dell'inbox. Il supervisor decide quale messaggio deve attivare quale agente. Questo è l'intervallo in cui spesso comincio a preferire routing esplicito e cronologia persistente dei messaggi.
5+ agenti o insiemi dinamici: Supervisor gerarchico. Un supervisor di livello superiore delega a supervisor subordinati. Ognuno gestisce un team di agenti specializzati. Il supervisor di livello superiore comunica con quelli subordinati, non direttamente con gli agenti di dominio. Questo aggiunge overhead di coordinamento, ma rende più espliciti i confini. Ogni supervisor subordinato può essere sviluppato e testato indipendentemente.
Non tratto la transizione tra questi livelli come arbitraria, ma non considero neppure i conteggi come regole fisse. Cerco i failure mode. Con 2 agenti, può essere più facile ragionare sui conflitti di scrittura perché ci sono solo due entità che scrivono. Man mano che il numero di agenti cresce e aumenta il parallelismo, i canali append-only possono rendere più facile ispezionare la cronologia dei messaggi. Quando un singolo supervisor diventa un collo di bottiglia nel routing, considero la delega gerarchica.
Il quadro generale
Lo stato condiviso è il modello giusto per un singolo agente con uno schema chiaro. Non lo uso come modello di coordinamento principale per un team di agenti con accesso in scrittura sovrapposto. I failure mode — conflitti di scrittura, letture obsolete e replay di checkpoint obsoleti — sono proprietà del modello di coordinamento che diventano visibili man mano che aggiungo parallelismo.
Lo scambio di messaggi con un supervisor non è una soluzione architetturalmente pura; è un compromesso pragmatico. L'analisi degli errori diventa più diretta quando posso ispezionare la coda dei messaggi. Il recovery diventa gestibile quando posso rimettere in coda i messaggi scaduti. La crescita diventa più controllata quando registro un agente nella configurazione di routing o nell'interfaccia dei tool del supervisor. In un sistema di 10 step, le chiamate di routing aggiuntive possono essere giustificate quando analisi e recovery contano più della latenza.
Se stai analizzando corruzione dello stato, instabilità del routing o bug nel replay dei checkpoint in un sistema multi-agent, puoi scrivermi indicando il failure mode e la forma della trace. Questi failure mode hanno pattern di mitigazione consolidati, ma la scelta giusta dipende dal workflow e dai requisiti di recovery.
Aggiornamento — ottobre 2026
La descrizione di condivisione di questo articolo affermava che lo scambio di messaggi con un supervisor "rimane deterministico". Il corpo dell'articolo non supporta questa affermazione. La sezione 3 dice che il supervisor aggiunge una chiamata al modello agli step di routing, e una chiamata al modello non è deterministica. Ciò che il corpo supporta è più circoscritto: lo scambio di messaggi rende esplicita la proprietà, e la decisione di routing del supervisor può essere registrata in modo che io possa ispezionarla. Ora la descrizione di condivisione riflette questa distinzione.
FAQ
Quando lo stato condiviso comincia a rompersi nei sistemi multi-agent?
Smetto di fidarmi dello stato condiviso quando diversi agenti si coordinano tramite scritture sovrapposte. Come euristica personale, comincio a considerare un supervisor intorno a tre agenti; con 3–5 agenti, spesso preferisco un supervisor con un message bus append-only e cronologia persistente dei messaggi.
Quali sono i principali failure mode di una blackboard condivisa?
Vedo tre failure mode ricorrenti: conflitti di scrittura, letture obsolete e replay di checkpoint obsoleti. Non sono necessariamente bug di LangGraph o del checkpointer; possono derivare da un modello di coordinamento in cui più agenti leggono da e scrivono sullo stesso oggetto di stato senza proprietà esplicita.
In che modo un message bus riduce i bug di coordinamento dello stato condiviso?
Un message bus cambia la proprietà. Gli agenti non scrivono nei campi degli altri né leggono direttamente l'output corrente di un altro agente. Ogni agente aggiunge messaggi tipizzati con to, from_, type e payload espliciti, quindi elabora quelli indirizzati alla propria inbox. Traccio in modo persistente gli ID dei messaggi consumati o gli offset dell'inbox e rendo l'elaborazione idempotente per evitare lavoro ripetuto dopo replay o re-entry.
Quando un supervisor vale le chiamate LLM aggiuntive?
Uso un supervisor solo quando routing centralizzato, tracciamento dello stato del task e decisioni osservabili valgono le chiamate aggiuntive per il workflow. Giustifica il suo costo quando la logica di routing cambia spesso, l'insieme di agenti sta ancora cambiando o l'analisi dovrebbe iniziare dall'ultima decisione registrata del supervisor.
Come dovrebbe essere assegnato l'accesso ai tool tra gli agenti?
Assegno l'accesso ai tool in base al ruolo dell'agente, non alla capacità del modello. Il researcher riceve tool in sola lettura, il writer riceve tool di scrittura e formattazione e il supervisor riceve tool di routing. Questo vincola le azioni disponibili per ogni agente a quelle appropriate per il suo ruolo.
Vuoi qualcosa di simile? Vedi servizi e prezzi
Articoli correlati
Stato come API: LangGraph dopo tre riscritture
Lo schema dello stato è la decisione più cruciale in LangGraph: tre iterazioni e perché i canali con reducer sono la primitiva giusta.
8 gen 202511 min di lettura#LangGraph#LLM#Multi-agente#OrchestrazioneDove CrewAI ha ceduto in due sistemi che ho migrato, e cosa uso al suo posto
Quattro punti in cui CrewAI ha ceduto quando l’ho usato oltre le demo, e i pattern LangGraph verso cui ho migrato.
15 gen 202512 min di lettura#CrewAI#Multi-agente#LangGraph#ProduzioneMCP in produzione: checklist
Prima che un server MCP tocchi dati veri controllo sette cose: hint, destinatario del token, scope, discovery OAuth, server locali, handle e log.
24 ott 202610 min di lettura#MCP#Sicurezza#Agenti#Produzione