Alejandro Rioja.
AI Agents Operations

Pattern di Orchestrazione Multi-Agente: Code, Stato e Handoff

Alejandro Rioja
Alejandro Rioja
7 min di lettura
TL;DR

I sistemi multi-agente affidabili non si basano su prompt ingegnosi — si basano sulla noiosa disciplina dei sistemi distribuiti: code durevoli tra gli agenti, stato tenuto fuori dal modello e handoff idempotenti che sopravvivono ai retry. Il modello è il worker; la coda è la spina dorsale.

Newsletter gratuita

Ogni mercoledì. 28.400+ operatori. Zero riempitivo.

Indice

Aggiornato giugno 2026.

TL;DR: I sistemi multi-agente affidabili non si vincono con prompt ingegnosi — si vincono con la noiosa disciplina dei sistemi distribuiti. Metti una coda durevole tra gli agenti, tieni lo stato fuori dal modello e rendi ogni handoff idempotente così che un retry non possa agire due volte. Il modello è il worker; la coda è la spina dorsale. Azzecca questi tre punti e l’orchestrazione smette di fare paura.

Lettura dell’operatore: La maggior parte dei miei oltre 100 agenti è a singolo step. Quelli che non lo sono — le pipeline che classificano, poi arricchiscono, poi agiscono — sono diventati affidabili solo quando ho smesso di pensare a una “catena di prompt” e ho iniziato a pensare a una “coda di job con worker LLM”. Questa è architettura, non prompt engineering.

“Multi-agente” suona come se gli agenti si parlassero tra loro. In pratica, la versione affidabile è l’opposto: gli agenti non comunicano direttamente affatto. Lasciano messaggi su una coda e prelevano lavoro da una coda, e l’orchestrazione vive nelle tubature tra di loro. Ecco i pattern che reggono in produzione.

Pattern 1: Metti una coda durevole tra ogni agente

Il primo istinto è chiamare l’agente B direttamente da dentro l’agente A. Non farlo. Le chiamate dirette accoppiano i due: se B è lento, A si blocca; se B fallisce, il lavoro di A è perso; se devi scalare B, non puoi senza toccare A.

Invece, A termina il suo lavoro e accoda un messaggio per B. B è un worker separato che svuota la coda al proprio ritmo.

typescript
// L'agente A finisce e fa l'handoff via coda — nessuna chiamata diretta a B
await env.ENRICH_QUEUE.send({
  traceId,
  type: "enrich",
  payload: classifierResult,
});
// Il lavoro di A è fatto. B lo preleverà in modo indipendente.

Su Cloudflare uso Workers Queues esattamente per questo — le stesse primitive dietro lo stack di agenti che uso. La coda ti dà quattro cose gratis: buffering (B può essere down senza perdere lavoro), retry (i messaggi falliti vengono riconsegnati), backpressure (un picco si accoda invece di far crashare) e disaccoppiamento (scala o ridistribuisci B senza toccare A). Ognuna di queste è qualcosa che altrimenti dovresti costruire a mano e sbagliare.

Pattern 2: Tieni lo stato fuori dal modello, sempre

Il bug multi-agente più comune è assumere che il modello ricordi qualcosa tra uno step e l’altro. Non lo fa. Ogni chiamata al modello è stateless; l’unica memoria è ciò che metti nel prompt. Quindi la fonte di verità per “a che punto è questo job nella pipeline” deve vivere in un database, non in una conversazione.

Tengo un singolo record di job che ogni agente legge e aggiorna:

typescript
interface JobState {
  traceId: string;
  stage: "classified" | "enriched" | "acted" | "done" | "failed";
  data: Record<string, unknown>;
  attempts: number;
  updatedAt: number;
}

Ogni agente fa lo stesso ciclo: leggere lo stato del job, fare il proprio lavoro, scrivere il nuovo stato, accodare lo stage successivo. Il modello non tiene mai lo stato — riceve la porzione rilevante come input e restituisce un risultato. Questo è ciò che rende il sistema riavviabile: se un worker muore a metà job, il record di stato dice ancora esattamente a che punto erano le cose, e il messaggio di coda riconsegnato riprende da lì. Rende anche il debug gestibile, perché la tabella di stato è un record interrogabile del percorso di ogni job — la stessa mentalità di strumentazione di come misuro se un agente sta funzionando.

Pattern 3: Rendi ogni handoff idempotente

Le code garantiscono la consegna almeno una volta, non esattamente una volta. Questo significa che un messaggio può essere consegnato due volte — interruzioni di rete, retry, ridistribuzioni. Se l’azione del tuo agente non è idempotente, una doppia consegna agisce due volte: due email di conferma, due prenotazioni, due addebiti. Questa è la classe di bug di orchestrazione più insidiosa, ed è quella che i team scoprono in produzione.

La soluzione è rendere le azioni idempotenti con una chiave:

typescript
async function handleEnrich(msg: QueueMessage, env: Env) {
  const job = await getJob(env, msg.traceId);
  if (job.stage !== "classified") {
    // Già elaborato oltre questo stage — è una consegna duplicata. Salta.
    return;
  }
  const result = await enrich(job.data);
  await advanceJob(env, msg.traceId, "enriched", result);
  await env.ACT_QUEUE.send({ traceId: msg.traceId, type: "act" });
}

Il controllo dello stage rende l’operazione sicura da eseguire due volte: la seconda consegna vede che il job è già avanzato e non fa nulla. Per gli effetti collaterali esterni (inviare un’email, addebitare una carta), passa una chiave di idempotenza all’API a valle così che anch’essa deduplichi. Assumi che ogni messaggio verrà consegnato due volte e progetta in modo che sia innocuo — perché prima o poi accadrà.

Pattern 4: Orchestratore vs coreografia — scegli deliberatamente

Ci sono due modi di cablare il flusso, e la scelta giusta dipende dalla complessità.

Coreografia (la mia scelta di default): ogni agente conosce solo lo step successivo e lo accoda. Il flusso emerge dalla catena. Semplice, decentralizzata, facile da estendere — aggiungi uno stage inserendo una coda. Lo svantaggio è che nessun singolo posto descrive l’intero flusso, quindi una pipeline complessa può diventare difficile da ragionare.

Orchestrazione (un coordinatore centrale): un orchestratore possiede il flusso, chiama ogni agente a turno e decide cosa segue in base ai risultati. L’intero flusso vive in un unico posto leggibile e la logica di branching è esplicita. Il costo è un componente centrale che deve essere esso stesso durevole — se lo stato proprio dell’orchestratore non è esternalizzato (Pattern 2), diventa il single point of failure.

La mia regola: coreografia finché il branching non diventa complesso, poi un orchestratore durevole. Una pipeline lineare a tre stage è coreografia. Un flusso con routing condizionale, fan-out parallelo e join vuole un orchestratore il cui stato vive nel database così da poter riprendere dopo un crash.

Pattern 5: Fan-out, fan-in senza perdere pezzi

Quando un job genera N sotto-task paralleli (arricchire 50 record, riassumere 20 documenti) e devi aspettarli tutti prima di continuare, ti serve un join. Il trucco è un contatore nello stato del job:

  1. Il padre accoda N messaggi figli e scrive expected: N, completed: 0 nel record del job.
  2. Ogni figlio fa il suo lavoro e incrementa atomicamente completed.
  3. Il figlio che porta completed a eguagliare expected accoda lo stage successivo.

L’incremento atomico è cruciale — senza di esso, due figli che finiscono simultaneamente possono entrambi credere di non essere l’ultimo, e il join non scatta mai. Usa un contatore che il datastore può incrementare atomicamente, o una transazione. Questo pattern ti permette di parallelizzare il costoso centro di una pipeline (spesso lavoro economico per Haiku — vedi la matematica dei costi Haiku vs Sonnet) mantenendo un join pulito alla fine.

Cosa eviterei

Non ti serve un framework di agenti pesante per fare niente di tutto questo. Code, una tabella di stato e chiavi di idempotenza sono primitive che ogni piattaforma ha già. Ho visto team ricorrere a elaborati framework multi-agente per ottenere funzionalità che una coda dà gratis, ed ereditare una scatola nera più difficile da debuggare delle tubature che sostituiva. Inizia con le noiose primitive. Ricorri a un framework solo quando hai sentito un dolore specifico che esso risolve.

Il riassunto: gli agenti sono worker stateless, le code sono la spina dorsale durevole, lo stato vive in un database e ogni handoff è sicuro da eseguire due volte. Questo è tutto il gioco.

FAQ

Gli agenti dovrebbero chiamarsi direttamente o passare per una coda?

Per una coda. Le chiamate dirette accoppiano gli agenti — il fallimento o la lentezza di uno si propaga all’altro, e non puoi scalare o ridistribuire in modo indipendente. Una coda durevole ti dà buffering, retry, backpressure e disaccoppiamento gratis.

Dove dovrebbe vivere lo stato multi-agente?

Fuori dal modello, in un database, come record di job che ogni agente legge e aggiorna. Le chiamate al modello sono stateless, quindi la fonte di verità per il progresso della pipeline deve essere esterna — è questo che rende il sistema riavviabile dopo un crash.

Come impedisco a un agente di agire due volte sullo stesso job?

Rendi gli handoff idempotenti. Controlla lo stage del job prima di agire e non fare nulla se è già avanzato, e passa chiavi di idempotenza alle API esterne. Le code consegnano almeno una volta, quindi assumi che ogni messaggio possa arrivare due volte e progetta in modo che i duplicati siano innocui.

Mi serve un framework multi-agente?

Di solito no. Code durevoli, una tabella di stato e chiavi di idempotenza coprono la maggior parte delle esigenze di produzione con primitive che la tua piattaforma fornisce già. Adotta un framework solo quando incontri un problema concreto che esso risolve in modo unico, non di default.

Continua a leggere

Ricevi il manuale dell'IA nella tua casella di posta

Ogni mercoledì. 28.400+ operatori. Zero riempitivo.

↵ per tutti i risultati esc esc per chiudere