18. Systèmes multi-agents — Le pattern superviseur
Repensez à l'agent de service client que nous avons construit au chapitre 16. Il possédait exactement un seul outil — la recherche de commande — donc lorsqu'un client posait une question sur une commande, il rapportait le statut d'expédition. Avec un seul outil, il n'y avait tout simplement aucun moyen pour lui de choisir le mauvais.
Supposons maintenant que nous fassions évoluer cet agent vers un véritable service de production. La recherche de commande à elle seule ne suffit pas. Nous aurions besoin de l'annulation de commande, du suivi d'expédition, du changement d'adresse, des demandes d'échange, des demandes de retour, des vérifications d'éligibilité au remboursement, du traitement des remboursements, des vérifications de stock, de l'émission de coupons, de la consultation de points, de la création de tickets de support, et bien plus encore. Le mécanisme est simple : continuez à ajouter des outils et à ajouter des règles métier au prompt système.
Mais à mesure que les outils et les règles s'accumulent, trois problèmes émergent.
-
La sélection des outils devient moins précise. À chaque tour, le modèle lit toutes les descriptions d'outils et décide lequel appeler. À mesure que vous ajoutez des outils qui prennent les mêmes entrées et se chevauchent dans leur objectif — comme demande d'échange et demande de retour — les chances de choisir le mauvais augmentent.
-
Le contexte se remplit d'informations dont vous n'avez pas besoin sur le moment. Même pendant le traitement d'un remboursement, le schéma de l'outil de vérification de stock, les règles d'émission de coupons, la procédure de demande d'échange et tout le reste voyagent à chaque appel. Vous transmettez l'ensemble complet des schémas d'outils et les règles métier de chaque domaine à chaque fois. Lorsque le contexte est encombré de matériel non pertinent pour la tâche en cours, les parties qui comptent se retrouvent enfouies et la précision en pâtit.
-
Cela devient difficile à modifier. Ajuster une seule règle de remboursement signifie éditer un prompt où les règles de chaque domaine sont enchevêtrées, et vous ne pouvez pas être sûr que le changement ne se répercutera pas sur les échanges ou l'expédition. Si différents domaines ont différents propriétaires, le problème ne fait que grandir.
Ce chapitre enseigne une façon de gérer cela. Au lieu d'entasser davantage d'outils et de règles sur un seul agent, nous répartissons le travail en un agent par domaine. En poursuivant le scénario de service client du chapitre 16, nous allons construire une équipe composée d'un agent qui ne gère que les recherches de commande et d'un agent qui ne gère que les remboursements. Chacun a ses propres outils et son propre prompt. Puis nous ajoutons un agent de plus pour les diriger — ce directeur est appelé le superviseur. Une configuration avec plusieurs agents comme celle-ci est un système multi-agents, et l'arrangement où un superviseur commande le reste est le pattern superviseur.
Les systèmes multi-agents ont bien un coût. Comme le superviseur doit décider à quel travailleur déléguer à chaque étape, il y a davantage d'appels LLM, ce qui signifie plus de latence et plus de dépenses. Donc si vous n'avez pas tant d'outils et de règles que cela, il n'y a aucune raison de recourir à un système multi-agents.
Voici le plan. En 18.1, nous examinons ce qu'est un système multi-agents et comment il fonctionne. En 18.2, nous construisons les agents travailleurs. En 18.3, nous construisons un superviseur à la main avec l'objet Command de LangGraph. En 18.4, nous enveloppons les travailleurs sous forme d'outils et reconstruisons la même équipe avec beaucoup moins de code.
18.1) Comprendre les systèmes multi-agents
18.1.1) Ce qu'est un système multi-agents
Concevons un agent capable de gérer la demande « Ma commande n'est jamais arrivée — si quelque chose ne va pas, remboursez-la. » Nous pourrions le construire avec tout ce que nous avons appris jusqu'à présent : attacher un outil de recherche de commande et un outil de remboursement à un seul agent, et laisser l'agent boucler jusqu'à ce que le travail soit terminé. Un seul agent recherche la commande, confirme que la livraison a échoué, voit ce résultat, demande le remboursement et rédige la réponse finale.
Un système multi-agents est une structure où plusieurs agents se répartissent ce travail. Vous répartissez les agents par domaine et placez un superviseur au-dessus d'eux, et chaque agent ne détient que les outils dont il a besoin. L'agent que nous venons d'esquisser, par exemple, se divise naturellement en un agent de recherche de commande et un agent de remboursement. Le superviseur appelle d'abord l'agent de recherche de commande pour vérifier le statut d'expédition ; lorsqu'il apprend que la livraison a échoué, il appelle l'agent de remboursement pour traiter le remboursement ; puis il rassemble les deux résultats pour répondre au client. Ce qui était autrefois un seul agent appelant des outils en séquence est devenu un superviseur appelant des agents en séquence.
S'il n'y avait que deux outils, il n'y aurait aucune raison de répartir ainsi. Un seul agent suffirait, et ajouter un superviseur ne ferait qu'ajouter des appels LLM. Mais comme nous l'avons vu en introduction, dès qu'il y a beaucoup d'outils, un agent peut en choisir le mauvais, son contexte se remplit d'informations sans rapport avec la tâche en cours, et son prompt devient difficile à modifier.
La répartition résout ces problèmes. L'agent de recherche de commande ne voit que quelques outils liés aux commandes, donc choisir dans une liste de dizaines se réduit à choisir parmi une poignée. Son prompt ne contient que les règles de recherche de commande, donc la politique de remboursement, les conditions des coupons et d'autres sujets sans rapport avec la recherche de commande ne remplissent pas son contexte. Et lorsque vous devez modifier une règle de remboursement, vous ne touchez que l'agent de remboursement, donc le changement n'atteint pas les autres.
L'agent de recherche de commande et l'agent de remboursement ici n'ont rien de spécial. Ce sont le même type d'agent que vous avez construit au chapitre 16. Vous les créez en passant un modèle, des outils et un prompt à create_agent, et vous les appelez avec invoke. Ils couvrent simplement moins de terrain.
Alors que fait le superviseur ? L'agent de recherche de commande et l'agent de remboursement ne savent pas que l'autre existe. Chacun fait juste son propre travail ; aucun ne sait qui devrait passer en premier. Dans l'exemple ci-dessus, appeler la recherche de commande en premier et — seulement après avoir vu son résultat — appeler le remboursement était le jugement du superviseur.
Qui appelle qui, et quand. C'est ce que nous appelons l'orchestration.
18.1.2) Le pattern superviseur
Le pattern superviseur est une structure dans laquelle un unique superviseur central orchestre plusieurs agents travailleurs. Il suit ces règles :
- Le superviseur ne fait jamais le travail lui-même. Il ne recherche pas les commandes ni ne traite les remboursements. Il décide seulement à qui passer la main, puis rassemble les résultats renvoyés en une réponse finale.
- Les travailleurs ne s'appellent jamais entre eux. L'agent de recherche de commande n'appelle jamais directement l'agent de remboursement. Chaque chemin passe par le superviseur.
- Seul le superviseur parle au client. Les travailleurs font rapport au superviseur, pas au client.
Alors comment le superviseur décide-t-il quel agent appeler ? Le LLM décide. Le superviseur lit toute la conversation jusqu'à présent et juge. S'il ne connaît pas encore le statut de la commande, il appelle l'agent de recherche de commande ; une fois qu'il a confirmé que la livraison a échoué et qu'un remboursement est nécessaire, il appelle l'agent de remboursement.
Le superviseur répète ce jugement jusqu'à ce que la demande de l'utilisateur soit accomplie. Il appelle un agent, reçoit un rapport, relit la conversation maintenant que le rapport y a été ajouté, et décide quel agent appeler ensuite.
Cette boucle a la même structure que celle que vous avez construite au chapitre 14.
- Penser — lire la conversation jusqu'à présent et décider quel agent appeler.
- Agir — exécuter l'agent choisi.
- Observer — prendre le rapport de l'agent et l'ajouter à la conversation.
Au chapitre 14, vous appeliez des outils ; ici vous appelez des agents. C'est la seule chose qui change.
Remarquez comment les flèches reviennent en boucle vers le superviseur. Lorsqu'un travailleur termine, il fait rapport au superviseur, et le superviseur lit ce rapport et décide du prochain mouvement.
Le superviseur est ce qui met fin à la boucle. Une fois qu'il juge que la demande du client est entièrement traitée, il produit la réponse finale et s'arrête.
18.2) Construire les agents travailleurs
Construisons les deux agents travailleurs que nous avons conçus en 18.1 : un travailleur de recherche de commande qui vérifie le statut de la commande, et un travailleur de remboursement qui traite les remboursements. Nous construirons le superviseur en 18.3.
Un agent travailleur n'est que l'agent ordinaire que vous avez construit au chapitre 16. Nous les construirons rapidement avec create_agent.
D'abord, mettons en place les données de commande que les deux travailleurs partageront.
ORDERS = {
"12345": {"item": "Wireless Earbuds", "amount": 89,
"status": "in_transit", "status_text": "En transit (arrivée demain)"},
"67890": {"item": "Mechanical Keyboard", "amount": 129,
"status": "delivered", "status_text": "Livré"},
"24680": {"item": "Noise-Cancelling Headphones", "amount": 249,
"status": "delivery_failed", "status_text": "Échec de livraison (retourné — destinataire introuvable)"},
}Le travailleur de recherche de commande n'a qu'un seul outil.
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""Recherche l'article, le montant payé et le statut d'expédition pour un numéro de commande."""
order = ORDERS.get(order_id)
if order is None:
return f"Commande {order_id} introuvable."
return (f"Commande {order_id} : {order['item']}, "
f"{order['amount']:,} $, statut : {order['status_text']}")
order_agent = create_agent(
name="order_expert",
model="openai:gpt-5.4-mini",
tools=[get_order_status],
system_prompt=(
"Vous êtes un spécialiste de la recherche de commande. Recherchez le statut de la commande et répondez.\n"
"Incluez le numéro de commande, l'article, le montant payé et le statut d'expédition dans votre réponse finale.\n"
"Ne jugez pas si un remboursement est justifié ni ne mentionnez les remboursements de quelque manière que ce soit. Votre rôle se limite à la recherche de commande et au rapport de statut."
),
)Le travailleur de remboursement a deux outils : un qui décide si une commande est éligible au remboursement, et un qui traite effectivement le remboursement.
@tool
def check_refund_eligibility(order_id: str) -> str:
"""Détermine si une commande est éligible à un remboursement. Seules les commandes en échec de livraison sont éligibles."""
order = ORDERS.get(order_id)
if order is None:
return f"Commande {order_id} introuvable."
if order["status"] == "delivery_failed":
return f"La commande {order_id} est éligible à un remboursement (raison : échec de livraison)."
return f"La commande {order_id} n'est pas éligible à un remboursement (statut actuel : {order['status_text']})."
@tool
def issue_refund(order_id: str) -> str:
"""Traite un remboursement. Confirmez toujours l'éligibilité avec check_refund_eligibility avant d'appeler ceci."""
order = ORDERS.get(order_id)
if order is None:
return f"Commande {order_id} introuvable."
return (f"Remboursement effectué : {order['amount']:,} $ pour la commande {order_id} "
f"seront remboursés sous 3 à 5 jours ouvrés. (numéro d'approbation : RF-{order_id})")
refund_agent = create_agent(
name="refund_expert",
model="openai:gpt-5.4-mini",
tools=[check_refund_eligibility, issue_refund],
system_prompt=(
"Vous êtes un spécialiste du traitement des remboursements.\n"
"Confirmez toujours l'éligibilité avec check_refund_eligibility d'abord, et "
"seulement ensuite appelez issue_refund.\n"
"Si vous avez traité un remboursement, incluez le montant et le numéro d'approbation dans votre réponse finale.\n"
"Si la commande n'est pas éligible, ne la traitez pas — rapportez plutôt la raison."
),
)Nous avons donné à chaque travailleur un name. Ce nom est ce que create_supervisor en 18.5 utilise comme nom de nœud et nom de l'outil de handoff.
Quatre éléments dont un prompt système de travailleur a besoin
Le prompt système d'un travailleur est construit à partir de quatre éléments — des principes qu'Anthropic a distillés en construisant son propre système de recherche multi-agents. Lorsqu'ils sont faibles, les travailleurs dupliquent le travail, laissent des tâches inachevées ou ne parviennent pas à trouver les informations dont ils ont besoin.
| Élément | order_agent | refund_agent |
|---|---|---|
| Rôle | Vous êtes un spécialiste de la recherche de commande | Vous êtes un spécialiste du traitement des remboursements |
| Guidage des outils | Confirmez toujours avec check_refund_eligibility d'abord, et seulement ensuite appelez issue_refund | |
| Format de sortie | Incluez le numéro de commande, l'article, le montant payé et le statut d'expédition dans votre réponse finale | Si vous avez traité un remboursement, incluez le montant et le numéro d'approbation dans votre réponse finale |
| Limite de tâche | Ne jugez pas si un remboursement est justifié ni ne mentionnez les remboursements de quelque manière que ce soit. Votre rôle se limite à la recherche de commande et au rapport de statut. | Si la commande n'est pas éligible, ne la traitez pas — rapportez la raison |
Rôle fixe, en une phrase, qui est ce travailleur. Épingler son identité avec « Vous êtes un spécialiste de la recherche de commande » garde le modèle concentré sur son propre travail et moins susceptible de s'égarer dans celui de quelqu'un d'autre.
Guidage des outils. Écrivez ceci lorsqu'il y a quelque chose que le schéma de l'outil seul ne peut pas transmettre — l'ordre et les conditions dans lesquels les outils doivent être utilisés, par exemple. S'il n'y a rien à ajouter au-delà du schéma, vous pouvez le laisser de côté.
Le format de sortie et la limite de tâche comptent énormément dans un contexte multi-agents.
Format de sortie. La réponse finale d'un travailleur n'est pas une réponse au client — c'est un rapport soumis au superviseur. Tout ce qui n'y est pas écrit n'atteint jamais le superviseur. Si un travailleur recherche un montant avec un outil mais l'omet de sa réponse finale, le superviseur n'a aucun moyen de le savoir.
Limite de tâche. C'est la portée qui définit jusqu'où un travailleur peut aller et ce qu'il ne doit pas faire. Le travailleur de recherche de commande ne devrait que rechercher — jamais rembourser. C'est pourquoi nous ne lui avons pas donné d'outil de remboursement. Mais retenir l'outil ne suffit pas à lui seul, car le modèle peut toujours dire « La livraison a échoué, donc je vais vous émettre un remboursement » sans aucun outil. Si cette phrase atteint le superviseur, le superviseur peut supposer qu'un remboursement est déjà en cours et ne jamais appeler le travailleur de remboursement. C'est pourquoi le prompt dit aussi « Ne jugez pas si un remboursement est justifié ni ne mentionnez les remboursements de quelque manière que ce soit », l'empêchant même d'évoquer les remboursements avec des mots.
Choisir les modèles
Nous utilisons gpt-5.4-mini pour les travailleurs et gpt-5.4 pour le superviseur. Un travailleur fait le travail simple d'appeler quelques outils dans un ordre défini, donc un petit modèle suffit largement. Le superviseur doit lire toute la conversation et juger qui appeler ensuite, donc il a besoin d'un modèle plus grand. Pouvoir choisir un modèle par agent, adapté à la difficulté de sa tâche, est un autre avantage de la répartition.
Nous pouvons maintenant ajouter le superviseur.
18.3) Construire le superviseur à la main
Construisons un superviseur à la main. En pratique, vous utiliserez surtout l'approche où le framework s'en charge pour vous (couverte dans la section suivante), mais pour comprendre ce qui se passe sous le capot, vous devez le construire vous-même une fois.
Comme nous l'avons vu en 18.1, ce que fait le superviseur est une seule boucle : appeler un travailleur, lire le rapport, et décider qui appeler ensuite — ou s'il faut s'arrêter — encore et encore.
18.3.1) Les handoffs et Command
Pour que cette boucle tourne, le contrôle doit passer d'avant en arrière entre le superviseur et les travailleurs. Le superviseur passe la main au contrôle — « ce travailleur passe ensuite » — et lorsque le travailleur termine, il rend le contrôle au superviseur. Ce passage de contrôle d'un nœud à un autre est appelé un handoff.
Un handoff a besoin de deux informations : où aller (la destination) et quoi transmettre (la charge utile). La destination est toujours requise ; la charge utile n'est incluse que lorsqu'il y a quelque chose à transmettre. Dans LangGraph, un nœud spécifie les deux en renvoyant 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", # où : le nœud à exécuter ensuite
update={"messages": [...]}, # quoi : le rapport du travailleur (la charge utile ajoutée à State)
)goto est la destination ; update est la charge utile. L'indication de type de retour Command[Literal["refund_expert_proxy"]] liste, en amont, les destinations vers lesquelles ce nœud peut aller. Nous verrons comment ce Command est réellement utilisé dans la section suivante, où nous construisons le nœud superviseur et les proxys de travailleurs.
18.3.2) Construire la boucle du superviseur
La structure elle-même est simple. Nous créons un nœud superviseur et autant de proxys de travailleurs que nécessaire. Un proxy de travailleur est un nœud qui appelle l'agent travailleur qui lui est assigné, au nom du superviseur. Le point d'entrée est le superviseur, et chaque proxy de travailleur, une fois terminé, revient au superviseur — formant la boucle. Le Command que chaque nœud renvoie est ce qui décide où va le contrôle ensuite.
Les lignes pleines sont des handoffs entre nœuds (goto) ; les lignes pointillées sont un proxy de travailleur appelant son agent travailleur (invoke). Le superviseur passe la main à un proxy de travailleur, et le proxy de travailleur, une fois terminé, revient au superviseur. Lorsque le superviseur décide FINISH, il sort vers END.
Maintenant, transformons cette image en code. D'abord, la classe Route. Route est le schéma pour recevoir la réponse du superviseur sous forme de réponse structurée lorsque nous demandons au LLM quel proxy de travailleur appeler ensuite. Si le LLM répondait en langage naturel libre, il serait difficile de savoir quel proxy de travailleur exécuter. Route contient quel proxy de travailleur appeler ensuite (next) et la raison pour laquelle il a décidé ainsi (reason).
from typing import Literal
from pydantic import BaseModel, Field
class Route(BaseModel):
reason: str = Field(description="La raison de cette décision.")
next: Literal["order_expert_proxy", "refund_expert_proxy", "FINISH"] = Field(
description="Le nœud travailleur à exécuter ensuite. FINISH si la demande est entièrement traitée."
)Il y a une raison pour laquelle reason est déclaré avant next. La sortie structurée est générée dans l'ordre où les champs apparaissent dans le schéma, donc avec reason en premier, le LLM écrit son raisonnement avant de choisir un travailleur. Raisonner d'abord et décider ensuite produit un meilleur choix. Inversez l'ordre — mettez next en premier — et le LLM choisit un travailleur avant d'avoir raisonné du tout, puis remplit après coup une justification pour l'adapter à un choix qu'il peut déjà avoir mal fait.
Ensuite, le nœud superviseur.
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 = (
"Vous êtes le superviseur d'une équipe de service client. Vous gérez deux travailleurs :\n"
"- order_expert_proxy : recherche le statut de la commande.\n"
"- refund_expert_proxy : vérifie l'éligibilité au remboursement et traite les remboursements.\n"
"Pour décider si un remboursement est nécessaire, vous devez d'abord vérifier le statut de la commande.\n"
"Assignez à un travailleur à la fois, et répondez FINISH une fois que la demande est entièrement traitée."
)
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": "En vous basant sur la conversation jusqu'à présent, rédigez une réponse au client."},
*state["messages"]]
)
return Command(goto=END, update={"messages": [final]}) # passe la main à END
return Command(goto=decision.next) # passe la main au proxy de travailleurJusqu'à présent, les nœuds ne renvoyaient que le State modifié. Mais le nœud superviseur renvoie un Command. Lorsqu'un nœud renvoie un Command, LangGraph fait deux choses : il applique le contenu de update au State, et il exécute ensuite le nœud nommé dans goto. Dans le code ci-dessus, nous plaçons le nom du proxy de travailleur extrait de decision.next dans goto, donc le nœud que le LLM a choisi — decision.next — est celui qui s'exécute.
Ensuite, les proxys de travailleurs. Un proxy de travailleur fait son travail et rend le contrôle au superviseur — c'est pourquoi il passe la main avec goto="supervisor".
Le travail d'un proxy de travailleur est simple : appeler son agent travailleur avec 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")]},
)Remarquez que nous extrayons uniquement le message final du travailleur et le transmettons au superviseur. Le superviseur n'a besoin que de la conclusion ; il n'a pas besoin de savoir combien de fois le travailleur a appelé des outils en interne.
18.3.3) Câbler et exécuter le graphe
Nous enregistrons les trois nœuds et connectons uniquement le point d'entrée au superviseur. Tout autre mouvement est décidé par le Command de chaque nœud, donc aucune autre arête n'est nécessaire.
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="La commande 24680 n'est toujours pas arrivée. Si quelque chose ne va pas, veuillez la rembourser."
)]},
config={"recursion_limit": 15},
)Sortie :
[supervisor] → order_expert_proxy (Besoin de vérifier le statut de la commande avant de décider d'un remboursement.)
[supervisor] → refund_expert_proxy (L'échec de livraison est confirmé, donc vérifier l'éligibilité au remboursement et le traiter.)
[supervisor] → FINISH (La vérification de la commande et le traitement du remboursement sont tous deux terminés.)Le superviseur a d'abord routé vers la recherche de commande. Lorsque le rapport est revenu indiquant que la livraison avait échoué, il a routé vers le remboursement, et lorsque le rapport de remboursement effectué est arrivé, il a terminé. Il a choisi chaque destination suivante en lisant le rapport de l'agent précédent.
Le proxy de travailleur transmet l'intégralité du State partagé à son travailleur via invoke(state), donc chaque travailleur voit toute la conversation jusqu'à présent. Avec seulement deux travailleurs, cela va, mais à mesure que les travailleurs et la conversation grandissent, chaque travailleur finit par lire des messages qui n'ont rien à voir avec son propre travail. Nous résoudrons cela différemment dans la section suivante.
18.4) Déléguer aux travailleurs via des outils
Ayant construit les rouages internes du superviseur à la main en 18.3, reconstruisons maintenant la même équipe de la manière recommandée pour les projets réels. Cette approche ne nécessite aucune nouvelle API. Vous transformez chaque travailleur en un outil avec @tool, et donnez ces outils à un agent superviseur. Nous construirons l'agent superviseur simplement avec create_agent.
L'idée clé tient en une phrase : le superviseur est lui-même juste un agent, et chaque travailleur devient un outil que le superviseur appelle.
Vu ainsi, le superviseur a la même structure que l'agent d'appel d'outils que vous avez construit au chapitre 16. Il détient simplement des outils de haut niveau qui appellent des agents, au lieu d'outils de bas niveau comme get_order_status.
18.4.1) Envelopper les travailleurs sous forme d'outils
Nous utilisons order_agent et refund_agent de 18.2 sans les modifier. Tout ce que nous faisons est d'envelopper chacun dans une fonction @tool.
from langchain.tools import tool
# order_agent et refund_agent sont les travailleurs de 18.2
@tool
def lookup_order(request: str) -> str:
"""Recherche l'article, le montant payé et le statut d'expédition d'une commande. Utilisez ceci lorsque vous avez besoin de connaître le statut d'une commande.
Entrée : une demande de recherche en langage naturel (par ex. « Donnez-moi le statut d'expédition de la commande 24680 »).
"""
print("[tool call] 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:
"""Vérifie l'éligibilité au remboursement et traite un remboursement. Utilisez ceci lorsque le client veut un remboursement et que vous avez déjà confirmé le statut de la commande.
Entrée : une demande de remboursement en langage naturel. Incluez le numéro de commande et le statut d'expédition confirmé par la recherche.
(par ex. « La commande 24680 est en statut d'échec de livraison. Traitez un remboursement si éligible. »)
"""
print("[tool call] handle_refund")
print(f" request: {request}")
result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].contentTrois choses ont changé.
-
Les descriptions d'outils remplacent la logique de routage. En 18.3, nous avons écrit
SUPERVISOR_PROMPTet le schémaRouteà la main pour indiquer au superviseur sa liste de travailleurs et ses choix. Ici, les docstrings des outils font ce travail. Le LLM du superviseur lit les descriptions des outils et décide quand appeler quoi. -
Chaque travailleur part d'un contexte vierge. Placé côte à côte avec 18.3, la différence est claire.
python# 18.3 (graphe manuel) : transmet l'intégralité du State partagé result = refund_agent.invoke(state) # 18.4 (délégation par outil) : transmet uniquement la description de tâche que le superviseur a rédigée result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})En 18.4, le travailleur de remboursement reçoit juste une phrase décrivant sa tâche. Il ne voit jamais le libellé original du client, le raisonnement du superviseur, ni l'historique d'appels d'outils d'un autre travailleur. Même avec dix travailleurs et cent tours de conversation, le contexte de chaque travailleur reste juste cette description de tâche unique.
-
En échange, le superviseur assume la tâche de transmettre l'information. Comme un travailleur ne peut pas voir l'historique de la conversation, tout ce dont il a besoin doit être empaqueté dans la chaîne
requestpar le superviseur. C'est pourquoi le docstring dehandle_refundprécise « Incluez le numéro de commande et le statut d'expédition confirmé par la recherche. » Sans cette instruction, le superviseur pourrait ne transmettre que"Traitez un remboursement", laissant le travailleur de remboursement incertain de la commande dont il s'agit.
18.4.2) Assembler et exécuter le superviseur
Dans cette approche, le superviseur aussi n'est qu'un agent. Pas de Command, pas de schéma Route nécessaire.
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
TOOL_SUPERVISOR_PROMPT = (
"Vous êtes le superviseur d'une équipe de service client.\n"
"Pour décider si un remboursement est nécessaire, vous devez d'abord vérifier le statut de la commande.\n"
"Ne faites pas le travail vous-même — déléguez aux travailleurs.\n"
"Les travailleurs ne peuvent pas voir cette conversation. Lorsque vous déléguez, mettez tout ce dont ils ont besoin dans la requête.\n"
"Lorsque tout le travail est terminé, synthétisez les résultats des travailleurs en une réponse au client."
)
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="La commande 24680 n'est toujours pas arrivée. Si quelque chose ne va pas, veuillez la rembourser."
)]}
)
print("\n\n[final response]")
print(result["messages"][-1].content)Le résultat final est le même qu'en 18.3.
[tool call] lookup_order
request: Le client dit que la commande 24680 n'est pas encore arrivée. Pour décider si un remboursement est
nécessaire, veuillez me donner l'article, le montant payé et le statut d'expédition actuel de la commande 24680.
[tool call] handle_refund
request: La commande 24680 est un casque à réduction de bruit, 249 $, et son statut d'expédition est
confirmé comme « Échec de livraison (retourné — destinataire introuvable) ». Le client
demande un remboursement, donc vérifiez l'éligibilité et traitez-le si éligible.
[final response]
J'ai vérifié, et la commande 24680 était en statut d'échec de livraison (retourné — destinataire introuvable).
Elle était éligible à un remboursement, et j'ai effectué le remboursement.
- Article : Casque à réduction de bruit
- Montant du remboursement : 249 $
- Numéro d'approbation du remboursement : RF-24680
Selon votre mode de paiement, il faut généralement quelques jours ouvrés pour que le remboursement soit crédité.Mais regardez les appels d'outils que le superviseur a effectués en cours de route — c'est là que se voit la différence avec 18.3. Regardez le request de handle_refund. Le superviseur a résumé le résultat de la recherche précédente et a rédigé lui-même la description de la tâche. Le travailleur de remboursement ne reçoit que cette phrase unique. Là où 18.3 remettait au travailleur toute la conversation et le laissait fouiller, ici le superviseur sélectionne juste ce qui est nécessaire et le transmet.
18.4.3) Ajouter de la mémoire au superviseur avec un checkpointer
Dire que le superviseur est un agent ordinaire signifie que le checkpointing que vous avez appris au chapitre 17 fonctionne sur lui tel quel.
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(), # le checkpointer ne va que sur l'agent de plus haut niveau
)
config = {"configurable": {"thread_id": "cs-1"}}
supervisor_agent.invoke(
{"messages": [HumanMessage(content="Où est ma commande 12345 ?")]},
config,
)
follow_up = supervisor_agent.invoke(
{"messages": [HumanMessage(content="Combien coûtait-elle ?")]},
config,
)
print(follow_up["messages"][-1].content)Sortie :
Votre commande 12345, les écouteurs sans fil, coûtait 89 $.Le superviseur lit correctement elle dans la question de suivi comme étant la commande 12345 du tour précédent. Le checkpointer fonctionne sur le superviseur exactement de la même manière.
N'attachez pas de checkpointer aux agents travailleurs (
order_agent,refund_agent). Si vous le faites, un travailleur reportera les résultats de son appel précédent dans l'appel courant, ce qui peut interférer avec la tâche en cours. Sans checkpointer, un travailleur s'exécute uniquement sur la base de la requête du superviseur. Pour les sous-agents, c'est le comportement par défaut recommandé.
18.5) create_supervisor : la forme que vous rencontrerez dans le code hérité
Dans les bases de code existantes et les tutoriels plus anciens, vous rencontrerez l'assistant create_supervisor du package langgraph-supervisor. Donnez-lui une liste d'agents et un prompt, et il construit tout le graphe superviseur pour vous.
from langgraph_supervisor import create_supervisor
from langchain_openai import ChatOpenAI
workflow = create_supervisor(
agents=[order_agent, refund_agent], # chaque agent doit avoir un name défini
model=ChatOpenAI(model="gpt-5.4"),
prompt="Assignez les vérifications de commande à order_expert et les remboursements à refund_expert.",
)
app = workflow.compile()create_supervisor est un assistant qui assemble une équipe de superviseur en un seul appel de fonction (l'approche par handoff de 18.3). Ne l'utilisez cependant pas dans de nouveaux projets. C'est de l'héritage que LangChain ne recommande plus, et en interne il dépend de create_react_agent, qui a été déprécié en v1 (la suppression est prévue pour la v2). LangChain recommande le superviseur basé sur les outils que vous avez appris en 18.4.
Dans ce chapitre, nous avons pris un agent qui vivait à l'intérieur d'un seul graphe et l'avons étendu en une équipe — répartie par domaine, orchestrée par un superviseur. Nous avons construit la même équipe de trois manières : le graphe Command manuel en 18.3, l'approche de délégation par outil en 18.4, et l'assistant hérité create_supervisor en 18.5. Pour le travail réel, faites de l'approche de délégation par outil votre choix par défaut.