>-
>-
Perché è importante
Il primo sistema multi-agent che ho costruito usava un dict di stato condiviso. Tre agenti — un planner, un researcher e un writer — leggevano e scrivevano tutti nello stesso AgentState di LangGraph. In demo funzionava bene. Due settimane dopo, in produzione, con cinque agenti ed esecuzione concorrente, ha iniziato a fallire in modi difficili da debuggare.
Non crash. Peggio: output sottilmente sbagliati. Il planner sovrascriveva un risultato di ricerca mentre il writer lo stava ancora consumando. Un agente leggeva stato obsoleto da una sessione precedente perché non avevo isolato il checkpoint. Il supervisor instradava verso un agente che aveva già completato il proprio task e stava aspettando il proprio output, che nel frattempo era stato sovrascritto da un agente parallelo.
Lo stato condiviso non è sbagliato in assoluto. Diventa fragile oltre una certa scala di coordinamento tra agenti. Capire dove quella scala si rompe — e con cosa sostituirla — è la differenza tra un sistema che funziona in test e uno che resta controllabile in produzione.
1. Le tre modalità di fallimento delle blackboard condivise
Una blackboard condivisa è qualunque design in cui più agenti leggono e scrivono nello stesso oggetto di stato senza ownership esplicita. Questo include il TypedDict piatto di LangGraph senza annotazioni di reducer, dict Python condivise passate per riferimento e tabelle di database senza row-level locking.
Modalità di fallimento 1: conflitti di scrittura. Due agenti producono output nello stesso step; vince l’ultima scrittura. Se entrambi gli agenti scrivono in messages, perdi un messaggio. Se entrambi scrivono in current_plan, un piano viene scartato silenziosamente. Questo può essere deterministico — a parità di input, perde sempre lo stesso agente — ma la perdita resta invisibile se non ispezioni lo stato a ogni checkpoint.
Modalità di fallimento 2: letture obsolete. L’Agent B legge stato che l’Agent A ha scritto due step prima. Questo può essere valido nelle pipeline sequenziali; diventa un bug di correttezza in quelle parallele. Quando l’Agent B assume che l’output del researcher sia aggiornato ma il planner ha nel frattempo rivisto la direzione della ricerca, l’Agent B produce una risposta a una domanda che non è più quella richiesta.
Modalità di fallimento 3: stato fantasma. Fare checkpointing di un sistema multi-agent significa salvare lo stato condiviso in un punto nel tempo. Se l’Agent C legge lo stato, lavora e viene checkpointato, poi l’Agent A sovrascrive gli stessi campi, un replay dal checkpoint dell’Agent C può ripristinare una versione dello stato che l’Agent A aveva già superato. Il replay è coerente con il checkpoint, ma non con lo stato reale del sistema.
Questi fallimenti non sono necessariamente bug di LangGraph o del tuo checkpointer. Sono proprietà del modello di coordinamento basato su stato condiviso. Aggiungere infrastruttura attorno allo stato condiviso — lock, campi versionati, garanzie read-your-writes — ricostruisce di fatto una parte di un database distribuito, che spesso non è la primitiva più semplice per un sistema agentico.
2. L’alternativa del message bus
Un message bus, cioè un bus di messaggi, inverte il modello di coordinamento: invece di far leggere agli agenti un oggetto condiviso, gli agenti si inviano messaggi tipizzati attraverso un canale esplicito. Nessun agente legge mai il “current output” di un altro agente — riceve un messaggio inviato deliberatamente a lui.
In LangGraph, questo significa usare Annotated[list[Message], operator.add] come canale primario di coordinamento, combinato con un nodo supervisor che legge la coda dei messaggi e instrada in base a tipo e contenuto del messaggio, invece che in base a campi di stato globali.
La differenza pratica: 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 l’ownership è esplicita. 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], lambda a, b: a | b]
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 indirizzati a lui ed elabora la propria inbox. Non esiste un campo condiviso su cui entrambi gli agenti scrivono; esiste solo un canale a cui entrambi fanno append.
3. Il pattern supervisor e quando giustifica il costo
Un agent supervisor, o supervisor agentico, è un router con memoria. Riceve tutti i messaggi, decide quale agente eseguire dopo e mantiene lo stato globale del task. Può aggiungere una chiamata al modello a ogni step di routing; in una pipeline da 10 step, se ogni step passa dal supervisor, può quindi aggiungere fino a 10 chiamate LLM.
Il costo è reale. La domanda è cosa compra.
Senza supervisor: aggiungere un nuovo agente richiede aggiornare ogni agente che potrebbe fare handoff verso di lui. Cambiare la logica di routing richiede aggiornare le edge function. Fare debug del motivo per cui un task si è bloccato significa leggere manualmente la coda dei messaggi.
Con supervisor: la logica di routing è centralizzata. Aggiungere un nuovo agente significa aggiungerlo alla tool list del supervisor. La decisione di routing del supervisor su “chi gestisce questo dopo” è ispezionabile. Fare debug di un task bloccato significa guardare l’ultima decisione del supervisor.
Il punto di pareggio operativo, nella mia esperienza, è spesso intorno a tre agenti. Sotto i tre, handoff peer-to-peer con edge condizionali sono più economici e più semplici. In molti casi, sopra i tre agenti il supervisor inizia a ripagarsi — soprattutto se l’insieme degli agenti sta evolvendo.
L’anti-pattern del supervisor: usare il supervisor come un router if-else glorificato, con regole esplicite come “se l’utente menziona Python, instrada al coder”. Un supervisor aggiunge valore quando deve valutare lo stato del task su più step, non quando fa pattern matching su un singolo campo. Se la logica di routing del tuo supervisor entra in uno switch statement, rimuoverlo e usare edge condizionali è spesso più semplice.
4. Governance dei tool nei sistemi multi-agent
Quando passi da un agente a molti, l’accesso ai tool diventa un problema di correttezza, non solo di sicurezza. Un agente con accesso a tool che non dovrebbe avere può usarli — soprattutto se il suo contesto include istruzioni provenienti da un altro agente che glielo suggeriscono.
Il principio: l’accesso ai tool deve corrispondere al ruolo dell’agente, non alla capacità dell’agente. Il researcher agent non dovrebbe avere accesso in scrittura al file system anche se il modello sottostante potrebbe generare comandi validi di scrittura file. Il writer non dovrebbe avere tool di ricerca anche se potrebbe usarli per “verificare” il proprio output — quello è compito del researcher.
# Researcher: read-only tools
researcher_agent = create_react_agent(
llm,
tools=[search_web, fetch_url, read_document],
system_message="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],
system_message="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],
system_message="You coordinate the research and writing workflow."
)
Non si tratta principalmente di sicurezza — si tratta di ridurre l’action space, cioè lo spazio delle azioni disponibili, a ciò che è appropriato per il ruolo dell’agente. Un action space più piccolo riduce le opzioni non pertinenti e rende più semplice verificare le decisioni dell’agente. Il researcher non può scrivere accidentalmente un file; il writer non può fare accidentalmente ricerca invece di scrivere; il supervisor non può eseguire accidentalmente azioni di dominio che dovrebbero passare da un worker.
5. Rilevamento dei fallimenti e recovery
I sistemi a stato condiviso tendono a fallire silenziosamente. I sistemi basati su message bus rendono il fallimento più esplicito — un messaggio arriva oppure no, e puoi ispezionare la coda per vedere quale delle due cose è successa.
La primitiva di recovery: se un agente non ha risposto entro un timeout, il supervisor può reinstradare la request o fare escalation a HITL. Questo richiede che ogni messaggio di request abbia un id e che il supervisor mantenga un registry delle request in sospeso.
import time
class SupervisorState(TypedDict):
messages: Annotated[list[Message], operator.add]
outstanding: dict[str, float] # message_id -> sent_at timestamp
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
return {
"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 ...
Questo pattern — registry delle request in sospeso con timeout ed escalation — è l’equivalente agentico di un circuit breaker. Sostituisce “attendere indefinitamente un agente bloccato” con “rilevare l’agente bloccato e instradare verso la recovery”. Senza questo, un singolo agente lento o in errore può bloccare l’intera pipeline senza un segnale chiaro.
6. Pattern di coordinamento per dimensione del sistema
Il modello di coordinamento corretto dipende dal numero di agenti e da come interagiscono:
2 agenti: Handoff diretto. L’Agent A viene eseguito, scrive l’output in un campo nominato (con un reducer chiaro), l’Agent B legge quel campo. Nessun supervisor necessario. Un edge condizionale instrada da A a B in base allo stato dell’output di A. Questo è spesso il design più semplice e sufficiente; non aggiungere complessità che non ti serve.
3–5 agenti: Supervisor con message bus. Il supervisor instrada tra agenti; gli agenti comunicano tramite il canale di messaggi append-only, non tramite campi nominati. Ogni agente ha un filtro inbox; il supervisor garantisce che il messaggio giusto raggiunga l’agente giusto. In molti casi, questa è la fascia in cui si collocano i sistemi multi-agent che devono restare controllabili in produzione.
5+ agenti o insiemi dinamici: Supervisor gerarchico. Un supervisor di livello superiore delega a sub-supervisor, ognuno dei quali gestisce un team di agenti specializzati. Il supervisor di livello superiore non parla mai direttamente con gli agenti di dominio — solo con i sub-supervisor, che instradano verso i propri agenti. Questo aggiunge overhead di coordinamento ma rende il sistema più modulare: ogni sub-supervisor può essere sviluppato e testato indipendentemente.
La transizione tra questi livelli non è arbitraria — è guidata dal momento in cui compaiono le modalità di fallimento. Con 2 agenti, i conflitti di scrittura sono più gestibili perché ci sono solo due writer. A 5+, diventano più difficili da evitare senza canali append-only. A 10+, un singolo supervisor può diventare un collo di bottiglia di routing; la delega gerarchica diventa spesso utile.
Il frame di categoria
Lo stato condiviso è il modello corretto per un singolo agente con uno schema chiaro. Diventa rischioso per un team di agenti con accesso in scrittura sovrapposto. Le modalità di fallimento — conflitti di scrittura, letture obsolete, checkpoint fantasma — non sono edge case. Sono proprietà del modello di coordinamento che diventano visibili quando aggiungi parallelismo.
Il message-passing con supervisor non è architettonicamente puro — è un trade-off pragmatico. Il debugging è esplicito (ispeziona la coda dei messaggi). La recovery è trattabile (rimetti in coda i messaggi in timeout). La crescita è additiva (aggiungi un agente aggiungendolo alla tool list del supervisor). Le chiamate LLM aggiuntive possono valere il costo quando comprano routing centralizzato, stato persistente più controllabile e recovery deterministica.
Se stai scalando un sistema multi-agent e incontri corruzione dello stato, instabilità di routing o bug di checkpoint replay, scrivimi. Sono problemi che si possono mitigare con pattern di coordinamento noti.