18. Sistemi multi-agente — Il pattern Supervisor
Ripensa all'agente di assistenza clienti che abbiamo costruito nel Capitolo 16. Aveva esattamente un solo strumento — la ricerca degli ordini — quindi quando un cliente chiedeva di un ordine, riportava lo stato di spedizione. Con un solo strumento, semplicemente non c'era modo che scegliesse quello sbagliato.
Ora supponiamo di far crescere quell'agente fino a farne un vero servizio in produzione. La sola ricerca degli ordini non basta. Avremmo bisogno di annullamento degli ordini, tracciamento delle spedizioni, modifica dell'indirizzo, richieste di cambio, richieste di reso, controlli di idoneità al rimborso, elaborazione dei rimborsi, controlli di inventario, emissione di coupon, ricerca dei punti, creazione di ticket di supporto e altro ancora. La meccanica è semplice: continuare ad aggiungere strumenti e appendere regole di business al prompt di sistema.
Ma man mano che strumenti e regole si accumulano, emergono tre problemi.
-
La selezione degli strumenti diventa meno accurata. A ogni turno il modello legge tutte le descrizioni degli strumenti e decide quale chiamare. Man mano che aggiungi strumenti che accettano gli stessi input e si sovrappongono nello scopo — come richiesta di cambio e richiesta di reso — le probabilità di sceglierne uno sbagliato aumentano.
-
Il contesto si riempie di informazioni che al momento non ti servono. Anche mentre elabora un rimborso, lo schema dello strumento di controllo inventario, le regole di emissione dei coupon, la procedura di richiesta di cambio e tutto il resto viaggiano insieme a ogni chiamata. Ogni volta invii l'intero insieme di schemi degli strumenti e tutte le regole di business di ciascun dominio. Quando il contesto è affollato di materiale irrilevante rispetto al compito attuale, le parti che contano finiscono sepolte e l'accuratezza ne risente.
-
Diventa difficile da modificare. Aggiustare una singola regola di rimborso significa modificare un prompt in cui le regole di ogni dominio sono aggrovigliate insieme, e non puoi essere certo che la modifica non si ripercuota su cambi o spedizioni. Se i diversi domini hanno proprietari diversi, il problema non fa che aggravarsi.
Questo capitolo insegna un modo per affrontare tutto ciò. Invece di accumulare più strumenti e regole su un singolo agente, suddividiamo il lavoro in un agente per ogni dominio. Continuando lo scenario di assistenza clienti del Capitolo 16, costruiremo un team composto da un agente che gestisce solo le ricerche degli ordini e un agente che gestisce solo i rimborsi. Ciascuno ha i propri strumenti e il proprio prompt. Poi aggiungiamo un ulteriore agente per dirigerli — questo direttore si chiama supervisor. Un'impostazione con diversi agenti come questa è un sistema multi-agente, e la disposizione in cui un supervisor comanda gli altri è il pattern supervisor.
I sistemi multi-agente hanno un costo. Poiché il supervisor deve decidere a quale worker delegare a ogni passo, ci sono più chiamate all'LLM, e questo significa più latenza e più spesa. Quindi, se non hai poi così tanti strumenti e regole, non c'è alcun bisogno di ricorrere a un sistema multi-agente.
Ecco il piano. In 18.1 vediamo cos'è un sistema multi-agente e come funziona. In 18.2 costruiamo gli agenti worker. In 18.3 costruiamo a mano un supervisor con l'oggetto Command di LangGraph. In 18.4 racchiudiamo i worker come strumenti e ricostruiamo lo stesso team con molto meno codice.
18.1) Comprendere i sistemi multi-agente
18.1.1) Cos'è un sistema multi-agente
Progettiamo un agente in grado di gestire la richiesta: "Il mio ordine non è mai arrivato — se c'è qualcosa che non va, rimborsalo." Potremmo costruirlo con tutto ciò che abbiamo imparato finora: collegare a un unico agente uno strumento di ricerca degli ordini e uno strumento di rimborso, e lasciare che l'agente esegua il ciclo finché il lavoro non è finito. Un singolo agente cerca l'ordine, conferma che la consegna è fallita, vede quel risultato, richiede il rimborso e scrive la risposta finale.
Un sistema multi-agente è una struttura in cui diversi agenti si suddividono questo lavoro. Suddividi gli agenti per dominio e collochi un supervisor sopra di loro, e ciascun agente possiede solo gli strumenti di cui ha bisogno. L'agente che abbiamo appena abbozzato, per esempio, si divide naturalmente in un agente di ricerca degli ordini e un agente di rimborso. Il supervisor chiama prima l'agente di ricerca degli ordini per verificare lo stato di spedizione; quando riceve la risposta che la consegna è fallita, chiama l'agente di rimborso per elaborare il rimborso; poi raccoglie entrambi i risultati per rispondere al cliente. Ciò che prima era un singolo agente che chiamava strumenti in sequenza è diventato un supervisor che chiama agenti in sequenza.
Se ci fossero solo due strumenti, non ci sarebbe motivo di suddividere in questo modo. Un unico agente basterebbe, e aggiungere un supervisor aggiungerebbe solo chiamate all'LLM. Ma come abbiamo visto nell'introduzione, quando gli strumenti sono molti, un agente può sceglierne uno sbagliato, il suo contesto si riempie di informazioni non correlate al compito da svolgere e il suo prompt diventa difficile da modificare.
La suddivisione risolve questi problemi. L'agente di ricerca degli ordini vede solo pochi strumenti legati agli ordini, quindi scegliere da un elenco di decine si riduce a scegliere tra una manciata. Il suo prompt contiene solo le regole di ricerca degli ordini, quindi la politica sui rimborsi, le condizioni dei coupon e altre questioni non correlate alla ricerca degli ordini non riempiono il suo contesto. E quando devi modificare una regola di rimborso, tocchi solo l'agente di rimborso, così la modifica non raggiunge gli altri.
L'agente di ricerca degli ordini e l'agente di rimborso qui non hanno nulla di speciale. Sono lo stesso tipo di agente che hai costruito nel Capitolo 16. Li crei passando un modello, degli strumenti e un prompt a create_agent, e li chiami con invoke. Coprono semplicemente meno terreno.
Quindi cosa fa il supervisor? L'agente di ricerca degli ordini e l'agente di rimborso non sanno l'uno dell'esistenza dell'altro. Ciascuno fa solo il proprio lavoro; nessuno dei due sa chi dovrebbe agire per primo. Nell'esempio sopra, chiamare prima la ricerca degli ordini e — solo dopo averne visto il risultato — chiamare il rimborso è stato il giudizio del supervisor.
Chi chiama chi, e quando. È questo che chiamiamo orchestrazione.
18.1.2) Il pattern Supervisor
Il pattern supervisor è una struttura in cui un unico supervisor centrale orchestra diversi agenti worker. Segue queste regole:
- Il supervisor non fa mai il lavoro da solo. Non cerca ordini né elabora rimborsi. Decide soltanto a chi affidare il compito, poi raccoglie i risultati restituiti in una risposta finale.
- I worker non si chiamano mai tra loro. L'agente di ricerca degli ordini non chiama mai direttamente l'agente di rimborso. Ogni percorso passa attraverso il supervisor.
- Solo il supervisor parla con il cliente. I worker riferiscono al supervisor, non al cliente.
Quindi, come fa il supervisor a decidere quale agente chiamare? Decide l'LLM. Il supervisor legge l'intera conversazione fino a quel punto e giudica. Se non conosce ancora lo stato dell'ordine, chiama l'agente di ricerca degli ordini; una volta confermato che la consegna è fallita ed è necessario un rimborso, chiama l'agente di rimborso.
Il supervisor ripete questo giudizio finché la richiesta dell'utente non è completa. Chiama un agente, riceve un resoconto, rilegge la conversazione ora che il resoconto è stato aggiunto e decide quale agente chiamare successivamente.
Questo ciclo ha la stessa struttura di quello che hai costruito nel Capitolo 14.
- Think (pensa) — leggi la conversazione fino a quel punto e decidi quale agente chiamare.
- Act (agisci) — esegui l'agente scelto.
- Observe (osserva) — prendi il resoconto dell'agente e aggiungilo alla conversazione.
Nel Capitolo 14 chiamavi strumenti; qui chiami agenti. È l'unica cosa che cambia.
Nota come le frecce tornino indietro al supervisor. Quando un worker termina, riferisce al supervisor, e il supervisor legge quel resoconto e decide la mossa successiva.
Il supervisor è ciò che termina il ciclo. Una volta che giudica che la richiesta del cliente è stata completamente gestita, produce la risposta finale e si ferma.
18.2) Costruire gli agenti worker
Costruiamo i due agenti worker che abbiamo progettato in 18.1: un worker di ricerca ordini che verifica lo stato dell'ordine, e un worker di rimborso che elabora i rimborsi. Costruiremo il supervisor in 18.3.
Un agente worker è semplicemente l'ordinario agente che hai costruito nel Capitolo 16. Li costruiremo rapidamente con create_agent.
Per prima cosa, impostiamo i dati degli ordini che i due worker condivideranno.
ORDERS = {
"12345": {"item": "Wireless Earbuds", "amount": 89,
"status": "in_transit", "status_text": "In transito (arrivo previsto domani)"},
"67890": {"item": "Mechanical Keyboard", "amount": 129,
"status": "delivered", "status_text": "Consegnato"},
"24680": {"item": "Noise-Cancelling Headphones", "amount": 249,
"status": "delivery_failed", "status_text": "Consegna fallita (restituito — destinatario non trovato)"},
}Il worker di ricerca ordini ha un solo strumento.
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""Cerca l'articolo, l'importo pagato e lo stato di spedizione per un numero d'ordine."""
order = ORDERS.get(order_id)
if order is None:
return f"Ordine {order_id} non trovato."
return (f"Ordine {order_id}: {order['item']}, "
f"${order['amount']:,}, stato: {order['status_text']}")
order_agent = create_agent(
name="order_expert",
model="openai:gpt-5.4-mini",
tools=[get_order_status],
system_prompt=(
"Sei uno specialista di ricerca ordini. Cerca lo stato dell'ordine e rispondi.\n"
"Includi nella tua risposta finale il numero d'ordine, l'articolo, l'importo pagato e lo stato di spedizione.\n"
"Non giudicare se un rimborso sia giustificato né menzionare i rimborsi in alcun modo. Il tuo ruolo è solo la ricerca degli ordini e la segnalazione dello stato."
),
)Il worker di rimborso ha due strumenti: uno che decide se un ordine è idoneo al rimborso, e uno che elabora effettivamente il rimborso.
@tool
def check_refund_eligibility(order_id: str) -> str:
"""Determina se un ordine è idoneo per un rimborso. Solo gli ordini con consegna fallita si qualificano."""
order = ORDERS.get(order_id)
if order is None:
return f"Ordine {order_id} non trovato."
if order["status"] == "delivery_failed":
return f"L'ordine {order_id} è idoneo per un rimborso (motivo: consegna fallita)."
return f"L'ordine {order_id} non è idoneo per un rimborso (stato attuale: {order['status_text']})."
@tool
def issue_refund(order_id: str) -> str:
"""Elabora un rimborso. Conferma sempre l'idoneità con check_refund_eligibility prima di chiamare questa funzione."""
order = ORDERS.get(order_id)
if order is None:
return f"Ordine {order_id} non trovato."
return (f"Rimborso completato: ${order['amount']:,} per l'ordine {order_id} "
f"verrà rimborsato entro 3–5 giorni lavorativi. (numero di approvazione: RF-{order_id})")
refund_agent = create_agent(
name="refund_expert",
model="openai:gpt-5.4-mini",
tools=[check_refund_eligibility, issue_refund],
system_prompt=(
"Sei uno specialista nell'elaborazione dei rimborsi.\n"
"Conferma sempre prima l'idoneità con check_refund_eligibility, e solo "
"dopo chiama issue_refund.\n"
"Se hai elaborato un rimborso, includi nella tua risposta finale l'importo e il numero di approvazione.\n"
"Se l'ordine non è idoneo, non elaborarlo — riporta invece il motivo."
),
)Abbiamo assegnato a entrambi i worker un name. Questo nome è quello che create_supervisor in 18.5 utilizza come nome del nodo e come nome dello strumento di handoff.
Quattro elementi di cui ha bisogno il prompt di sistema di un worker
Il prompt di sistema di un worker è costruito a partire da quattro elementi — principi che Anthropic ha distillato mentre costruiva il proprio sistema multi-agente di ricerca. Quando questi sono deboli, i worker duplicano il lavoro, lasciano compiti incompiuti o non riescono a trovare le informazioni di cui hanno bisogno.
| Elemento | order_agent | refund_agent |
|---|---|---|
| Ruolo | Sei uno specialista di ricerca ordini | Sei uno specialista nell'elaborazione dei rimborsi |
| Guida agli strumenti | Conferma sempre prima con check_refund_eligibility, e solo dopo chiama issue_refund | |
| Formato di output | Includi nella tua risposta finale il numero d'ordine, l'articolo, l'importo pagato e lo stato di spedizione | Se hai elaborato un rimborso, includi nella tua risposta finale l'importo e il numero di approvazione |
| Limiti del compito | Non giudicare se un rimborso sia giustificato né menzionare i rimborsi in alcun modo. Il tuo ruolo è solo la ricerca degli ordini e la segnalazione dello stato. | Se l'ordine non è idoneo, non elaborarlo — riporta invece il motivo |
Il ruolo definisce, in una sola frase, chi è questo worker. Fissare la sua identità con "Sei uno specialista di ricerca ordini" mantiene il modello concentrato sul proprio lavoro e meno incline a sconfinare in quello di qualcun altro.
La guida agli strumenti. Scrivila quando c'è qualcosa che il solo schema dello strumento non può trasmettere — l'ordine e le condizioni in cui gli strumenti dovrebbero essere usati, per esempio. Se non c'è nulla da aggiungere oltre lo schema, puoi ometterla.
Il formato di output e i limiti del compito contano moltissimo in un contesto multi-agente.
Il formato di output. La risposta finale di un worker non è una risposta al cliente — è un resoconto presentato al supervisor. Tutto ciò che non è scritto lì non raggiunge mai il supervisor. Se un worker cerca un importo con uno strumento ma lo omette dalla sua risposta finale, il supervisor non ha modo di saperlo.
I limiti del compito. Questo è l'ambito che definisce fino a dove può spingersi un worker e cosa non deve fare. Il worker di ricerca ordini dovrebbe solo cercare — mai rimborsare. Ecco perché non gli abbiamo dato uno strumento di rimborso. Ma trattenere lo strumento non basta di per sé, perché il modello può comunque dire "La consegna è fallita, quindi emetterò un rimborso per te" senza alcuno strumento. Se quella frase raggiunge il supervisor, il supervisor potrebbe presumere che un rimborso sia già in corso e non chiamare mai il worker di rimborso. Perciò il prompt dice anche: "Non giudicare se un rimborso sia giustificato né menzionare i rimborsi in alcun modo," impedendogli persino di tirare in ballo i rimborsi a parole.
Scegliere i modelli
Usiamo gpt-5.4-mini per i worker e gpt-5.4 per il supervisor. Un worker svolge il semplice compito di chiamare pochi strumenti in un ordine prestabilito, quindi un modello piccolo è più che sufficiente. Il supervisor deve leggere l'intera conversazione e giudicare chi chiamare successivamente, quindi ha bisogno di un modello più grande. Poter scegliere un modello per ogni agente, adattato alla difficoltà del suo lavoro, è un altro vantaggio della suddivisione.
Ora possiamo aggiungere il supervisor.
18.3) Costruire il supervisor a mano
Costruiamo un supervisor a mano. In pratica userai per lo più l'approccio in cui il framework se ne occupa per te (trattato nella prossima sezione), ma per capire cosa succede sotto il cofano, devi costruirlo tu stesso almeno una volta.
Come abbiamo visto in 18.1, ciò che fa il supervisor è un unico ciclo: chiama un worker, leggi il resoconto e decidi chi chiamare successivamente — o se fermarsi — ancora e ancora.
18.3.1) Handoff e Command
Perché questo ciclo giri, il controllo deve passare avanti e indietro tra il supervisor e i worker. Il supervisor cede il controllo — "questo worker va per primo" — e quando il worker termina, ricede il controllo al supervisor. Questo passaggio di controllo da un nodo all'altro si chiama handoff.
Un handoff necessita di due informazioni: dove andare (la destinazione) e cosa passare (il payload). La destinazione è sempre obbligatoria; il payload è incluso solo quando c'è qualcosa da passare. In LangGraph, un nodo specifica entrambi restituendo un Command.
from typing import Literal
from langgraph.graph import MessagesState
from langgraph.types import Command
def some_node(state: MessagesState) -> Command[Literal["refund_expert_proxy"]]:
return Command(
goto="refund_expert_proxy", # dove: il nodo da eseguire successivamente
update={"messages": [...]}, # cosa: il resoconto del worker (il payload aggiunto allo State)
)goto è la destinazione; update è il payload. Il type hint di ritorno Command[Literal["refund_expert_proxy"]] elenca in anticipo le destinazioni verso cui questo nodo può andare. Vedremo come questo Command viene effettivamente usato nella prossima sezione, dove costruiamo il nodo supervisor e i proxy dei worker.
18.3.2) Costruire il ciclo del supervisor
La struttura in sé è semplice. Creiamo un nodo supervisor e tanti proxy dei worker quanti ne servono. Un proxy di worker è un nodo che chiama, per conto del supervisor, l'agente worker che gli è stato assegnato. Il punto d'ingresso è il supervisor, e ogni proxy di worker, una volta terminato, torna al supervisor — formando il ciclo. Il Command restituito da ciascun nodo è ciò che decide dove va il controllo successivamente.
Le linee continue sono handoff tra nodi (goto); le linee tratteggiate sono un proxy di worker che chiama il proprio agente worker (invoke). Il supervisor cede il controllo a un proxy di worker, e il proxy di worker, una volta terminato, ritorna al supervisor. Quando il supervisor decide FINISH, esce verso END.
Ora trasformiamo questa immagine in codice. Per prima cosa, la classe Route. Route è lo schema per ricevere la risposta del supervisor come risposta strutturata quando chiediamo all'LLM quale proxy di worker chiamare successivamente. Se l'LLM rispondesse in linguaggio naturale libero, sarebbe difficile capire quale proxy di worker eseguire. Route contiene quale proxy di worker chiamare successivamente (next) e il motivo per cui ha deciso in quel modo (reason).
from typing import Literal
from pydantic import BaseModel, Field
class Route(BaseModel):
reason: str = Field(description="Il motivo di questa decisione.")
next: Literal["order_expert_proxy", "refund_expert_proxy", "FINISH"] = Field(
description="Il nodo worker da eseguire successivamente. FINISH se la richiesta è completamente gestita."
)C'è un motivo per cui reason è dichiarato prima di next. L'output strutturato viene generato nell'ordine in cui i campi appaiono nello schema, quindi con reason per primo l'LLM scrive il proprio ragionamento prima di scegliere un worker. Ragionare prima e decidere dopo produce una scelta migliore. Inverti l'ordine — metti next per primo — e l'LLM sceglie un worker prima ancora di aver ragionato, poi costruisce a posteriori una motivazione per adattarla a una scelta che potrebbe già aver sbagliato.
Poi, il nodo supervisor.
from typing import Literal
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
supervisor_llm = ChatOpenAI(model="gpt-5.4")
SUPERVISOR_PROMPT = (
"Sei il supervisor di un team di assistenza clienti. Gestisci due worker:\n"
"- order_expert_proxy: cerca lo stato dell'ordine.\n"
"- refund_expert_proxy: verifica l'idoneità al rimborso ed elabora i rimborsi.\n"
"Per decidere se è necessario un rimborso, devi prima verificare lo stato dell'ordine.\n"
"Assegna a un worker alla volta, e rispondi con FINISH una volta che la richiesta è completamente gestita."
)
def supervisor(
state: MessagesState,
) -> Command[Literal["order_expert_proxy", "refund_expert_proxy", "__end__"]]:
messages = [{"role": "system", "content": SUPERVISOR_PROMPT}, *state["messages"]]
decision = supervisor_llm.with_structured_output(Route).invoke(messages)
print(f"[supervisor] → {decision.next} ({decision.reason})")
if decision.next == "FINISH":
final = supervisor_llm.invoke(
[{"role": "system", "content": "Basandoti sulla conversazione fino a questo punto, scrivi una risposta al cliente."},
*state["messages"]]
)
return Command(goto=END, update={"messages": [final]}) # cede il controllo a END
return Command(goto=decision.next) # cede il controllo al proxy di workerFino ad ora, i nodi restituivano solo lo State modificato. Ma il nodo supervisor restituisce un Command. Quando un nodo restituisce un Command, LangGraph fa due cose: applica il contenuto di update allo State ed esegue successivamente il nodo nominato in goto. Nel codice sopra, mettiamo in goto il nome del proxy di worker estratto da decision.next, quindi il nodo scelto dall'LLM — decision.next — è ciò che viene eseguito.
Poi, i proxy dei worker. Un proxy di worker svolge il suo compito e ridà il controllo al supervisor — motivo per cui cede il controllo con goto="supervisor".
Il compito di un proxy di worker è semplice: chiamare il proprio agente worker con invoke.
def order_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
result = order_agent.invoke(state)
last = result["messages"][-1]
return Command(
goto="supervisor",
update={"messages": [HumanMessage(content=last.content, name="order_expert")]},
)
def refund_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
result = refund_agent.invoke(state)
last = result["messages"][-1]
return Command(
goto="supervisor",
update={"messages": [HumanMessage(content=last.content, name="refund_expert")]},
)Nota che estraiamo solo il messaggio finale del worker e lo passiamo al supervisor. Il supervisor ha bisogno solo della conclusione; non ha bisogno di sapere quante volte il worker ha chiamato strumenti internamente.
18.3.3) Collegare ed eseguire il grafo
Registriamo i tre nodi e colleghiamo solo il punto d'ingresso al supervisor. Ogni altra mossa è decisa dal Command di ciascun nodo, quindi non servono ulteriori archi.
builder = StateGraph(MessagesState)
builder.add_node("supervisor", supervisor)
builder.add_node("order_expert_proxy", order_expert_proxy)
builder.add_node("refund_expert_proxy", refund_expert_proxy)
builder.add_edge(START, "supervisor")
team = builder.compile()
result = team.invoke(
{"messages": [HumanMessage(
content="L'ordine 24680 non è ancora arrivato. Se c'è qualcosa che non va, rimborsatelo per favore."
)]},
config={"recursion_limit": 15},
)Output:
[supervisor] → order_expert_proxy (È necessario verificare lo stato dell'ordine prima di decidere su un rimborso.)
[supervisor] → refund_expert_proxy (La consegna fallita è confermata, quindi verifica l'idoneità al rimborso ed elaboralo.)
[supervisor] → FINISH (La verifica dell'ordine e l'elaborazione del rimborso sono entrambe completate.)Il supervisor ha instradato prima alla ricerca degli ordini. Quando è tornato il resoconto che la consegna era fallita, ha instradato al rimborso, e quando è arrivato il resoconto di rimborso completato, ha terminato. Ha scelto ogni destinazione successiva leggendo il resoconto dell'agente precedente.
Il proxy di worker passa l'intero State condiviso al proprio worker tramite invoke(state), quindi ciascun worker vede l'intera conversazione fino a quel punto. Con soli due worker questo va bene, ma man mano che worker e conversazione crescono, ciascun worker finisce per leggere messaggi che non hanno nulla a che fare con il proprio lavoro. Risolveremo questo in modo diverso nella prossima sezione.
18.4) Delegare ai worker tramite strumenti
Avendo costruito a mano le viscere del supervisor in 18.3, ricostruiamo ora lo stesso team nel modo raccomandato per progetti reali. Questo approccio non richiede alcuna nuova API. Trasformi ciascun worker in uno strumento con @tool, e dai quegli strumenti a un agente supervisor. Costruiremo l'agente supervisor semplicemente con create_agent.
L'idea chiave sta in una frase: il supervisor è esso stesso solo un agente, e ciascun worker diventa uno strumento che il supervisor chiama.
Visto così, il supervisor ha la stessa struttura dell'agente che chiama strumenti che hai costruito nel Capitolo 16. Semplicemente possiede strumenti di alto livello che chiamano agenti, invece di strumenti di basso livello come get_order_status.
18.4.1) Racchiudere i worker come strumenti
Usiamo order_agent e refund_agent di 18.2 senza modifiche. Tutto ciò che facciamo è racchiudere ciascuno in una funzione @tool.
from langchain.tools import tool
# order_agent e refund_agent sono i worker di 18.2
@tool
def lookup_order(request: str) -> str:
"""Cerca l'articolo, l'importo pagato e lo stato di spedizione di un ordine. Usa questo strumento quando devi conoscere lo stato di un ordine.
Input: una richiesta di ricerca in linguaggio naturale (es. 'Dimmi lo stato di spedizione dell'ordine 24680').
"""
print("[chiamata al tool] lookup_order")
print(f" request: {request}")
result = order_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].content
@tool
def handle_refund(request: str) -> str:
"""Verifica l'idoneità al rimborso ed elabora un rimborso. Usa questo strumento quando il cliente vuole un rimborso e hai già confermato lo stato dell'ordine.
Input: una richiesta di rimborso in linguaggio naturale. Includi il numero d'ordine e lo stato di spedizione confermato dalla ricerca.
(es. 'L'ordine 24680 è in stato di consegna fallita. Elabora un rimborso se idoneo.')
"""
print("[chiamata al tool] handle_refund")
print(f" request: {request}")
result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].contentSono cambiate tre cose.
-
Le descrizioni degli strumenti sostituiscono la logica di instradamento. In 18.3 abbiamo scritto a mano
SUPERVISOR_PROMPTe lo schemaRouteper dire al supervisor il suo elenco di worker e le sue scelte. Qui questo compito lo svolgono le docstring degli strumenti. L'LLM del supervisor legge le descrizioni degli strumenti e decide quando chiamare cosa. -
Ciascun worker parte da un contesto pulito. Messo fianco a fianco con 18.3, la differenza è chiara.
python# 18.3 (grafo manuale): passa l'intero State condiviso result = refund_agent.invoke(state) # 18.4 (delega tramite tool): passa solo la descrizione del compito scritta dal supervisor result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})In 18.4 il worker di rimborso riceve solo una frase che descrive il proprio compito. Non vede mai le parole originali del cliente, il ragionamento del supervisor o la cronologia delle chiamate agli strumenti di un altro worker. Anche con dieci worker e cento turni di conversazione, il contesto di ciascun worker è ancora solo quella singola descrizione del compito.
-
In cambio, il supervisor si assume il compito di passare le informazioni. Poiché un worker non può vedere la cronologia della conversazione, tutto ciò di cui ha bisogno deve essere impacchettato dal supervisor nella stringa
request. Ecco perché la docstring dihandle_refundspecifica: "Includi il numero d'ordine e lo stato di spedizione confermato dalla ricerca." Senza quell'istruzione, il supervisor potrebbe passare solo"Elabora un rimborso", lasciando il worker di rimborso incerto persino su quale ordine si tratti.
18.4.2) Assemblare ed eseguire il supervisor
In questo approccio anche il supervisor è solo un agente. Non servono Command né schema Route.
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
TOOL_SUPERVISOR_PROMPT = (
"Sei il supervisor di un team di assistenza clienti.\n"
"Per decidere se è necessario un rimborso, devi prima verificare lo stato dell'ordine.\n"
"Non fare il lavoro tu stesso — delega ai worker.\n"
"I worker non possono vedere questa conversazione. Quando deleghi, metti nella richiesta tutto ciò di cui hanno bisogno.\n"
"Quando tutto il lavoro è fatto, sintetizza i risultati dei worker in una risposta al cliente."
)
supervisor_agent = create_agent(
model="openai:gpt-5.4",
tools=[lookup_order, handle_refund],
system_prompt=TOOL_SUPERVISOR_PROMPT,
)
result = supervisor_agent.invoke(
{"messages": [HumanMessage(
content="L'ordine 24680 non è ancora arrivato. Se c'è qualcosa che non va, rimborsatelo per favore."
)]}
)
print("\n\n[risposta finale]")
print(result["messages"][-1].content)Il risultato finale è lo stesso di 18.3.
[chiamata al tool] lookup_order
request: Il cliente dice che l'ordine 24680 non è ancora arrivato. Per decidere se è
necessario un rimborso, forniscimi l'articolo, l'importo pagato e lo stato di spedizione attuale dell'ordine 24680.
[chiamata al tool] handle_refund
request: L'ordine 24680 è un paio di Noise-Cancelling Headphones, $249, e il suo stato di spedizione è
confermato come 'Consegna fallita (restituito — destinatario non trovato)'. Il cliente sta
richiedendo un rimborso, quindi verifica l'idoneità ed elaboralo se idoneo.
[risposta finale]
Ho verificato, e l'ordine 24680 era in stato di consegna fallita (restituito — destinatario non trovato).
Era idoneo per un rimborso, e ho completato il rimborso.
- Articolo: Noise-Cancelling Headphones
- Importo del rimborso: $249
- Numero di approvazione del rimborso: RF-24680
A seconda del tuo metodo di pagamento, di solito occorrono alcuni giorni lavorativi perché il rimborso venga accreditato.Ma guarda le chiamate agli strumenti che il supervisor ha fatto lungo il percorso — è lì che si vede la differenza rispetto a 18.3. Guarda la request di handle_refund. Il supervisor ha riassunto il precedente risultato della ricerca e ha scritto lui stesso la descrizione del compito. Il worker di rimborso riceve solo questa singola frase. Dove 18.3 consegnava al worker l'intera conversazione e lo lasciava rovistare, qui il supervisor seleziona solo ciò che serve e lo passa oltre.
18.4.3) Aggiungere memoria al supervisor con un checkpointer
Dire che il supervisor è un agente ordinario significa che il checkpointing che hai imparato nel Capitolo 17 funziona su di esso così com'è.
from langgraph.checkpoint.memory import InMemorySaver
supervisor_agent = create_agent(
model="openai:gpt-5.4",
tools=[lookup_order, handle_refund],
system_prompt=TOOL_SUPERVISOR_PROMPT,
checkpointer=InMemorySaver(), # il checkpointer va solo sull'agente di livello superiore
)
config = {"configurable": {"thread_id": "cs-1"}}
supervisor_agent.invoke(
{"messages": [HumanMessage(content="Dov'è il mio ordine 12345?")]},
config,
)
follow_up = supervisor_agent.invoke(
{"messages": [HumanMessage(content="Quanto era?")]},
config,
)
print(follow_up["messages"][-1].content)Output:
Il tuo ordine 12345, i Wireless Earbuds, era di $89.Il supervisor legge correttamente quello nel follow-up come l'ordine 12345 del turno precedente. Il checkpointer funziona sul supervisor esattamente allo stesso modo.
Non collegare un checkpointer agli agenti worker (
order_agent,refund_agent). Se lo fai, un worker porterà i risultati della sua chiamata precedente in quella attuale, cosa che può interferire con il compito da svolgere. Senza di esso, un worker gira su nient'altro che la richiesta del supervisor. Per i subagent, questo è il valore predefinito raccomandato.
18.5) create_supervisor: la forma che incontrerai nel codice legacy
Nelle codebase esistenti e nei tutorial più datati, ti imbatterai nell'helper create_supervisor del pacchetto langgraph-supervisor. Dagli un elenco di agenti e un prompt, e costruisce per te l'intero grafo del supervisor.
from langgraph_supervisor import create_supervisor
from langchain_openai import ChatOpenAI
workflow = create_supervisor(
agents=[order_agent, refund_agent], # ogni agente deve avere un name impostato
model=ChatOpenAI(model="gpt-5.4"),
prompt="Assegna i controlli degli ordini a order_expert e i rimborsi a refund_expert.",
)
app = workflow.compile()create_supervisor è un helper che assembla un team supervisor con una singola chiamata di funzione (l'approccio con handoff di 18.3). Non usarlo però in nuovi progetti. È codice legacy che LangChain non raccomanda più, e internamente dipende da create_react_agent, che è stato deprecato in v1 (la rimozione è pianificata per v2). LangChain raccomanda il supervisor basato su strumenti che hai imparato in 18.4.
In questo capitolo abbiamo preso un agente che era vissuto all'interno di un singolo grafo e lo abbiamo espanso in un team — suddiviso per dominio, orchestrato da un supervisor. Abbiamo costruito lo stesso team in tre modi: il grafo manuale con Command in 18.3, l'approccio di delega tramite strumenti in 18.4 e l'helper legacy create_supervisor in 18.5. Per il lavoro reale, rendi l'approccio di delega tramite strumenti la tua scelta predefinita.