Salta al contenuto
Torna alla home
2024-12-1012 min di lettura
LangChainAgentsMulti-AgentArchitecture

Perché lo stato condiviso fallisce oltre tre agenti

Le lavagne condivise falliscono sotto carico: limiti dello stato condiviso e vantaggi del passaggio di messaggi con supervisore.

Perché conta

Il primo sistema multi-agent che ho costruito usava un dict di stato condiviso. Tre agenti — un planner, un researcher e un writer — leggevano tutti da e scrivevano sullo stesso AgentState di LangGraph. Funzionava nelle demo. Due settimane dopo, con cinque agenti e l'esecuzione concorrente in produzione, ha fallito in modi difficili da sottoporre a debug.

Non crash. Peggio: output sottilmente errati. Il planner sovrascriveva un risultato di ricerca mentre il writer lo stava ancora utilizzando. 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 compito e stava aspettando il proprio output, che era stato sovrascritto da un agente parallelo.

Lo stato condiviso non è sbagliato. Smetto di fidarmi quando diversi agenti si coordinano attraverso scritture sovrapposte. La domanda importante è dove quel modello inizia a rompersi e con cosa lo sostituisco quando mi servono stato persistente, recovery deterministica, eval riproducibili e costi sotto controllo.

1. Le tre modalità di fallimento delle lavagne condivise

Una lavagna condivisa è qualsiasi progettazione in cui più agenti leggono da e scrivono nello stesso oggetto di stato senza ownership esplicita. Questo include TypedDict piatti di LangGraph senza annotazioni reducer, dict Python condivisi passati per riferimento e tabelle di database senza locking a livello di riga.

Modalità di fallimento 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 parallel super-step non vengono sottoposte a merge silenziosamente — LangGraph genera InvalidUpdateError, che in produzione si manifesta come un crash anziché come una risposta errata. Il fallimento più silenzioso avviene tra step: un nodo successivo sovrascrive un campo impostato da un nodo precedente e, a meno che non ispezioni lo stato a ogni checkpoint, quella scrittura persa rimane invisibile. In entrambi i casi, è il modello di coordinamento, non un bug, a imporre la scelta.

Modalità di fallimento 2: letture obsolete. L'agente B legge 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 del researcher sia attuale ma il planner ha nel frattempo cambiato la direzione della ricerca, l'agente B risponde a una domanda che non viene più posta.

Modalità di fallimento 3: stato fantasma. Fare checkpoint di un sistema multi-agent significa salvare lo stato condiviso in un punto nel tempo. Se l'agente C legge lo stato, svolge il lavoro e viene sottoposto a checkpoint, poi l'agente A sovrascrive gli stessi campi, il replay dal checkpoint dell'agente C ripristina una versione dello stato che l'agente A aveva già superato. Il replay è internamente coerente ma esternamente errato.

Non considero questi bug di LangGraph o del checkpointer. Derivano dal modello di coordinamento basato sullo stato condiviso. Se aggiungo lock, campi versionati e garanzie read-your-writes intorno allo stato condiviso, inizio a ricostruire parti di un database distribuito. Di solito è la primitiva sbagliata 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 attraverso 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 dei messaggi e instrada in base al tipo e al contenuto del messaggio anziché ai campi dello stato globale.

La differenza pratica è l'ownership. 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 campi "propri"; si limita ad aggiungere 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_]

Ciò che 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 sé ed elabora la propria inbox. Non esiste un campo condiviso su cui scrivono entrambi gli agenti; esiste soltanto un canale a cui entrambi aggiungono elementi.

3. Il pattern supervisor e quando giustifica il suo costo

Un agente supervisor è un router con memoria. Riceve messaggi, decide quale agente eseguire successivamente e mantiene lo stato globale del task. Aggiunge anche una chiamata al modello agli step di routing. In una pipeline di 10 step, questo può significare 10 chiamate LLM aggiuntive.

Il costo è reale. Uso un supervisor solo quando il controllo che aggiunge vale quel costo.

Senza un supervisor, aggiungere un nuovo agente richiede l'aggiornamento di ogni agente che potrebbe effettuare un handoff verso di esso. Cambiare la logica di routing richiede l'aggiornamento delle edge function. Sottoporre a debug il motivo per cui un task si è bloccato significa leggere manualmente l'intera coda dei messaggi.

Con un supervisor, la logica di routing è centralizzata. Aggiungere un nuovo agente significa aggiungerlo alla tool list del supervisor. La decisione di routing del supervisor è visibile nella sua decisione registrata nei log o nel messaggio emesso. Il debug di un task bloccato parte dall'ultima decisione osservabile del supervisor.

Come regola empirica, inizio a considerare un supervisor intorno a tre agenti. Al di sotto di questa soglia, gli handoff peer-to-peer con conditional edge sono spesso meno costosi e più facili da ispezionare. Con tre o più agenti, il routing centralizzato può valere le chiamate aggiuntive, specialmente quando il set di agenti è ancora in evoluzione.

L'anti-pattern del supervisor consiste nell'usarlo come un router if-else glorificato, 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 effettua soltanto pattern matching su un campo. Se la logica di routing entra in uno statement switch, rimuovo il supervisor e uso conditional edge.

4. Tool governance nei sistemi multi-agent

Quando passo da un agente a molti, l'accesso ai tool diventa un problema di correttezza, non soltanto di sicurezza. Un agente con accesso a tool che non dovrebbe avere può usarli, specialmente 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 per la scrittura di file. Il writer non dovrebbe avere tool di ricerca anche se potrebbe usarli per "verificare" il proprio output — questo è 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. Il researcher non può scrivere accidentalmente un file; il writer non può effettuare accidentalmente una ricerca anziché scrivere; il supervisor non può eseguire accidentalmente azioni di dominio che dovrebbero passare attraverso un worker.

5. Rilevamento dei fallimenti e recovery

I sistemi a stato condiviso spesso falliscono con poche evidenze locali. I sistemi basati sul message-passing rendono i fallimenti più facili da osservare quando consegna e logging sono espliciti: posso ispezionare la coda, confrontare i messaggi inviati con quelli ricevuti e identificare richieste senza risposta.

La primitiva di recovery è diretta: se un agente non ha risposto entro un timeout, il supervisor può re-instradare la richiesta o effettuare escalation a HITL. Questo richiede che ogni messaggio di richiesta abbia un id e che il supervisor mantenga un registry delle richieste 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 ...

Tratto questo pattern — registry delle richieste in sospeso con timeout ed escalation — come l'equivalente per gli agenti di un circuit breaker. Sostituisce "attendere indefinitamente un agente bloccato" con "rilevare l'agente bloccato e instradare verso il recovery". Senza di esso, un singolo agente lento o guasto può bloccare la pipeline senza un segnale chiaro.

6. Pattern di coordinamento per dimensione del sistema

Scelgo il modello di coordinamento in base al numero di agenti e al modo in cui interagiscono:

2 agenti: Handoff diretto. L'agente A viene eseguito, scrive l'output in un campo nominato con un reducer chiaro, e l'agente B legge quel campo. Non è necessario alcun supervisor. Una conditional edge instrada da A a B in base allo stato dell'output di A. È la progettazione corretta più semplice che uso per questa forma di problema. Evito di aggiungere meccanismi di coordinamento prima che mi servano.

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 nominati. Ogni agente filtra la propria inbox. Il supervisor decide quale messaggio dovrebbe attivare quale agente. Questo è l'intervallo in cui di solito inizio a preferire routing esplicito e cronologia dei messaggi persistente.

5+ agenti o set dinamici: Supervisor gerarchico. Un supervisor di livello superiore delega a sub-supervisor. Ogni sub-supervisor gestisce un team di agenti specializzati. Il supervisor di livello superiore comunica con i sub-supervisor, non direttamente con gli agenti di dominio. Questo aggiunge overhead di coordinamento, ma mantiene più chiari i confini. Ogni sub-supervisor può essere sviluppato e testato in modo indipendente.

Non considero arbitraria la transizione tra questi livelli. Cerco le modalità di fallimento. Con 2 agenti, è più facile ragionare sui conflitti di scrittura perché ci sono soltanto due writer. Con la crescita del numero di agenti e l'aumento del parallelismo, i canali append-only diventano più importanti. Quando un singolo supervisor diventa un collo di bottiglia nel routing, considero la delega gerarchica.

Il quadro della categoria

Lo stato condiviso è il modello corretto per un singolo agente con uno schema chiaro. Non lo uso come modello di coordinamento primario per un team di agenti con accesso in scrittura sovrapposto. Le modalità di fallimento — conflitti di scrittura, letture obsolete, checkpoint fantasma — non sono casi limite. Sono proprietà del modello di coordinamento che diventano visibili quando aggiungo parallelismo.

Il message-passing con un supervisor non è architetturalmente puro; è un compromesso pragmatico. Il debug diventa più diretto quando posso ispezionare la coda dei messaggi. Il recovery diventa trattabile quando posso reinserire in coda i messaggi in timeout. La crescita diventa più controllata quando aggiungo un agente aggiungendolo alla tool list del supervisor. In un sistema di 10 step, le chiamate di routing aggiuntive possono essere giustificate quando debug e recovery contano più della latenza.

Se stai sottoponendo a debug corruzione dello stato, instabilità del routing o bug di checkpoint replay in un sistema multi-agent, puoi scrivermi indicando la modalità di fallimento e la forma della trace. Tratto questi come problemi risolvibili con pattern noti.

FAQ

Quando lo stato condiviso inizia a rompersi nei sistemi multi-agent?

Smetto di fidarmi dello stato condiviso quando diversi agenti si coordinano attraverso scritture sovrapposte. Intorno a tre agenti, inizio a considerare un supervisor; con 3–5 agenti, di solito preferisco un supervisor con un message bus append-only e cronologia dei messaggi persistente.

Quali sono le principali modalità di fallimento di una lavagna condivisa?

Vedo tre modalità di fallimento ricorrenti: conflitti di scrittura, letture obsolete e stato fantasma. Non sono bug di LangGraph o del checkpointer; derivano da un modello di coordinamento in cui più agenti leggono da e scrivono nello stesso oggetto di stato senza ownership esplicita.

In che modo un message bus riduce i bug di coordinamento dello stato condiviso?

Un message bus cambia l'ownership. 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 i messaggi indirizzati alla propria inbox.

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. Giustifica il proprio costo quando la logica di routing cambia spesso, il set di agenti è ancora in evoluzione o il debug dovrebbe iniziare dall'ultima decisione 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 di sola lettura, il writer riceve tool di scrittura e formattazione e il supervisor riceve tool di routing. Questo riduce lo spazio d'azione di ciascun agente a ciò che è appropriato per il suo ruolo.

Condividi questo articolo

Articoli correlati

  • Lo stato come API: LangGraph dopo tre riscritture

    Lo schema di stato è la scelta chiave in LangGraph: tre iterazioni sul modello e perché i canali con reducer sono la primitiva giusta.

    8 gen 202512 min di lettura
    #LangGraph#LLM#Multi-Agent#Orchestration
  • Dove CrewAI fallisce in produzione e alternative

    L’astrazione dei ruoli in CrewAI regge nelle demo ma cede in produzione: quattro failure mode e i pattern LangGraph che li sostituiscono.

    15 gen 202512 min di lettura
    #CrewAI#Multi-Agent#LangGraph#Production
  • Lezioni dalla gestione chiavi bancaria per eval agenti

    Cinque discipline della sicurezza bancaria: stato persistente, recovery deterministica, doppio controllo e audit trail per valutare agenti LLM.

    18 apr 20267 min di lettura
    #Agent Evals#Verifiable Systems#LLM Production#Banking#MCP