Orchestration multi-agents : handoffs et agents parallèles
Un agent bourré de douze tools et d'un system promptsystem promptLes instructions cachées qui définissent le comportement d'un assistant IA avant toute question : son rôle, son ton, ses limites et ses règles.Voir la définition complète → de 2 000 mots est un cauchemar de maintenance, et il raisonne généralement moins bien que trois agents ciblés qui font chacun une chose correctement. Les workflows réels se découpent en spécialistes qui soit se passent la main, soit tournent côte à côte. Cette leçon entre dans le détail de la construction de tout cela avec le OpenAI Agents SDK : handoffs, fan-out parallèle, et les guardrailsguardrailsRègles et contrôles qui maintiennent un système d'IA dans des limites sûres, légales et conformes à la marque, en bloquant les sorties et actions hors cadre.Voir la définition complète → et le tracing qui empêchent un système multi-agents de devenir un magma intraçable.
Pourquoi découper un agent en plusieurs
Un agent unique se dégrade de façon prévisible à mesure qu'on empile les responsabilités. Le system prompt grossit jusqu'à ce que les instructions se contredisent. La liste de tools devient assez longue pour que le modèle choisisse le mauvais tool. L'évaluation devient impossible parce que chaque retouche de prompt affecte toutes les tâches à la fois.
Le découpage corrige cela en donnant à chaque agent un jeu d'instructions restreint et une petite liste de tools. Un agent facturation ne connaît que les factures et les remboursements. Un agent support technique ne connaît que les diagnostics et les procédures de réinitialisation. Chacun est plus simple à prompter, à tester et à remplacer.
Les deux patterns de base :
- Handoff : un agent décide qu'un autre agent est mieux placé et lui délègue toute la conversation. Séquentiel, comme un transfert d'appel.
- Parallèle : vous lancez plusieurs agents en concurrence sur des sous-tâches indépendantes, puis vous fusionnez les résultats. Fan-out, comme envoyer trois chercheurs dans trois bibliothèques en même temps.
Handoffs : la délégation comme primitive de premier rang
Dans l'Agents SDK, un handoff est un tool particulier que le modèle peut appeler pour transférer le contrôle à un autre agent. Vous n'écrivez pas la logique de routage à la main. Vous donnez à un agent une liste de cibles de handoff possibles, et le modèle choisit quand transférer en fonction de la conversation.
La forme canonique est l'agent de triage : un routeur léger dont le seul rôle est de lire la demande de l'utilisateur et de la passer au bon spécialiste. Il ne porte aucun tool métier propre.
from agents import Agent, Runner
billing_agent = Agent(
name="Billing Agent",
handoff_description="Handles invoices, charges, and refunds.",
instructions=(
"You resolve billing issues. Look up the customer's latest "
"invoice and explain charges or process a refund. If the "
"problem is technical, say so clearly."
),
tools=[lookup_invoice, issue_refund],
)
tech_agent = Agent(
name="Tech Support Agent",
handoff_description="Handles login, errors, and device issues.",
instructions=(
"You diagnose technical problems. Walk the user through "
"resets and known fixes. If the issue is about money, "
"say so clearly."
),
tools=[run_diagnostic, send_reset_link],
)
triage_agent = Agent(
name="Triage Agent",
instructions=(
"Route the user to the right specialist. Do not answer "
"billing or technical questions yourself. Hand off."
),
handoffs=[billing_agent, tech_agent],
)
result = Runner.run_sync(
triage_agent,
"I was charged twice for my subscription this month.",
)
print(result.final_output)Deux éléments font fonctionner ce montage. Le handoff_description est le texte que lit le modèle de routage pour décider où envoyer la demande : écrivez-le comme une offre d'emploi, pas comme un commentaire. Et la liste handoffs est ce qui transforme chaque spécialiste en cible appelable par l'agent de triage.
Ce qui franchit réellement la frontière
Par défaut, tout l'historique de conversation voyage avec le handoff, si bien que l'agent facturation voit la réclamation d'origine sans que vous ayez à la repasser. C'est généralement ce que vous voulez. Quand ce n'est pas le cas (par exemple si le spécialiste ne doit pas voir un message sensible antérieur), vous pouvez personnaliser le handoff pour filtrer l'input ou injecter du contexte supplémentaire. La documentation sur les handoffs couvre les filtres d'input et les callbacks on_handoff pour logger ou pré-charger des données au moment du transfert.
Un point subtil : après un handoff, le contrôle ne revient pas automatiquement à l'agent de triage. Le spécialiste est désormais propriétaire de la conversation. Si vous voulez un pattern en étoile où les spécialistes renvoient vers le triage, donnez à chaque spécialiste un handoff de retour vers l'agent de triage. Concevez la topologie volontairement.
Agents parallèles : fan out, puis regroupement
Les handoffs sont séquentiels. Parfois vous voulez un travail concurrent parce que les sous-tâches ne dépendent pas les unes des autres. Cas classiques : rédiger trois réponses candidates et retenir la meilleure, ou récupérer l'historique de facturation et les logs techniques en même temps avant qu'un spécialiste raisonne sur les deux.
Le SDK tourne sur asyncio, donc le parallélisme se résume à lancer plusieurs agents avec asyncio.gather et à combiner les sorties. Voici un fan-out qui génère deux brouillons indépendants puis utilise un troisième agent pour synthétiser :
import asyncio
from agents import Agent, Runner
drafter = Agent(
name="Drafter",
instructions="Write one concise support reply to the user.",
)
synthesizer = Agent(
name="Synthesizer",
instructions=(
"You are given two candidate replies. Merge their best "
"parts into one final reply. Remove redundancy."
),
)
async def main(user_msg: str) -> str:
draft_a, draft_b = await asyncio.gather(
Runner.run(drafter, user_msg),
Runner.run(drafter, user_msg),
)
merged = await Runner.run(
synthesizer,
f"Reply A:\n{draft_a.final_output}\n\nReply B:\n{draft_b.final_output}",
)
return merged.final_output
print(asyncio.run(main("How do I export my data?")))Le travail parallèle vous achète de la latence (deux choses se passent en même temps) ou de la qualité (échantillonner plusieurs fois et sélectionner). Il vous coûte des tokenstokensUn token est l'unité de base de texte que traitent les modèles de langage : le plus souvent un fragment de mot, un mot entier ou un signe de ponctuation, plutôt qu'un simple caractère.Voir la définition complète → : lancer le drafter deux fois double à peu près la dépense de cette étape. Ne faites du fan-out que là où le parallélisme se rentabilise.
Un pattern voisin est celui des agents comme tools. Au lieu de passer la main (transférer le contrôle), un agent parent peut appeler un autre agent comme s'il s'agissait d'une fonction via agent.as_tool(...), récupérer le résultat et garder le contrôle. C'est ainsi que vous construisez un orchestrateur de haut niveau qui dispatche vers des sous-agents en parallèle et reste mamaUtiliser un logiciel pour automatiser les tâches et campagnes marketing répétitives, afin de personnaliser à grande échelle sur des canaux comme l'email, le web et le social.Voir la définition complète →ître de la réponse finale. Les handoffs transfèrent la propriété ; les agents-as-tools la conservent.
Guardrails : détecter tôt les mauvais inputs et outputs
Plus d'agents signifie plus de surface d'exposition aux problèmes. Les guardrails sont des contrôles qui tournent en parallèle de vos agents pour valider l'input ou l'output, et ils peuvent déclencher un tripwire qui stoppe l'exécution avant que vous ne gaspilliez des tokens ou ne laissiez fuiter quelque chose.
Un input guardrail s'exécute sur le message entrant de l'agent de triage. Un modèle rapide et peu coûteux peut filtrer les demandes hors sujet ou abusives avant qu'un spécialiste onéreux ne soit lancé.
from agents import (
Agent, Runner, GuardrailFunctionOutput, input_guardrail,
)
from pydantic import BaseModel
class Relevance(BaseModel):
is_support_request: bool
screener = Agent(
name="Screener",
instructions="Is this a customer support request? Answer strictly.",
output_type=Relevance,
)
@input_guardrail
async def on_topic(ctx, agent, user_input):
result = await Runner.run(screener, user_input, context=ctx.context)
flagged = not result.final_output.is_support_request
return GuardrailFunctionOutput(
output_info=result.final_output,
tripwire_triggered=flagged,
)
triage_agent = Agent(
name="Triage Agent",
instructions="Route to the right specialist.",
handoffs=[billing_agent, tech_agent],
input_guardrails=[on_topic],
)Les output guardrails fonctionnent de la même façon sur la réponse finale, utiles pour vérifier qu'un agent facturation ne promet jamais un remboursement qu'il n'est pas autorisé à accorder. Notez l'usage de output_type avec un modèle Pydantic ci-dessus : ce sont les structured outputs qui assurent le contrôle, de sorte que le screener renvoie un booléen typé plutôt que de la prose qu'il faudrait parser. Voir le guide des guardrails pour le flux d'exception du tripwire.
Vérification des acquis
1. D'après la leçon, pourquoi le découpage d'un gros agent en plusieurs agents ciblés améliore-t-il généralement les résultats ?
2. Qu'est-ce qui distingue le mieux un handoff d'un pattern parallèle dans l'orchestration multi-agents ?
3. Dans l'Agents SDK, comment un agent réalise-t-il concrètement un handoff vers un autre agent ?
4. Sélectionnez TOUTES les façons dont un agent unique surchargé se dégrade à mesure que les responsabilités s'empilent, selon la leçon.
Sélectionnez toutes les réponses correctes.
5. Sélectionnez TOUS les énoncés qui décrivent correctement un agent de triage tel que présenté dans la leçon.
Sélectionnez toutes les réponses correctes.
Tracing : voir ce que vos agents ont réellement fait
Dès que vous avez trois agents, des handoffs, des branches parallèles et des guardrails, « il a donné une réponse bizarre » n'est plus débogable en lisant une seule transcription. Vous devez voir l'exécution entière sous forme d'arbre.
L'Agents SDK intègre le tracing, activé par défaut. Chaque run produit une trace contenant des spans pour chaque invocation d'agent, chaque appel de tool, chaque handoff et chaque guardrail. Vous les consultez dans le dashboard Traces de la plateforme OpenAI. C'est la principale raison d'utiliser le SDK plutôt que de bricoler votre propre boucle d'orchestration : vous obtenez l'observabilité gratuitement.
Regroupez les runs liés sous une même trace pour qu'une session client complète apparaisse comme un seul arbre :
from agents import Runner, trace
async def handle_session(msg: str):
with trace("support-session"):
result = await Runner.run(triage_agent, msg)
return result.final_outputDans le dashboard, vous voyez exactement où le triage a envoyé la demande, combien de temps a pris la recherche de facture, et si un guardrail s'est déclenché. Quand un handoff part vers le mauvais spécialiste, la trace vous montre la décision de routage, ce qui vous permet de corriger le handoff_description au lieu de deviner.
Building Multi-Agent Systems with the OpenAI Agents SDK
Quand le multi-agents l'emporte sur l'agent unique, et ce qu'il coûte
Optez pour plusieurs agents quand :
- Les responsabilités sont réellement distinctes et chacune nécessite des tools ou des instructions différentes (triage vs facturation vs technique).
- Vous voulez une évaluation indépendante : vous pouvez tester l'agent facturation isolément.
- Les sous-tâches sont indépendantes et le parallélisme réduit la latence ou améliore la qualité par échantillonnage.
Restez sur un seul agent quand :
- La tâche est un flux cohérent unique où chaque étape a besoin du contexte complet.
- La latence et le coût comptent plus que la modularité. Chaque handoff et chaque guardrail est un appel de modèle supplémentaire, et le fan-out parallèle multiplie la dépense en tokens.
- Vous êtes encore en prototypage. Commencez avec un agent, ne découpez que lorsqu'une ligne de séparation nette apparaît.
Le vrai coût du design multi-agents est la complexité de coordination : le routage peut se tromper, le contexte peut se perdre dans un handoff mal configuré, et une topologie bavarde peut faire rebondir la conversation entre agents en brbrLe pourcentage de visiteurs qui repartent après avoir vu une seule page, souvent le signe d'une pertinence insuffisante, d'un décalage d'intention ou d'une expérience utilisateur faible.Voir la définition complète →ûlant des tokens. Le tracing et les guardrails existent précisément pour rendre cette complexité gérable, pas pour l'éliminer. Ajoutez des agents quand la ligne de séparation est évidente, et laissez les traces vous dire si un découpage aide ou nuit.
Points clés
- Découpez par responsabilité, pas par coquetterie. Utilisez un agent de triage léger pour router, et donnez à chaque spécialiste un jeu d'instructions restreint et une petite liste de tools. Écrivez le
handoff_descriptioncomme une offre d'emploi ; le routeur le lit pour décider. - Les handoffs transfèrent le contrôle ; les agents-as-tools le conservent. Utilisez les handoffs pour la délégation (transfert d'appel), et
agent.as_tool()quand un orchestrateur a besoin de récupérer le sous-résultat et reste aux commandes. - Ne faites du fan-out que là où cela paie. Les agents parallèles via
asyncio.gatherréduisent la latence ou élèvent la qualité par échantillonnage, mais ils multiplient le coût en tokens. Réservez-les aux sous-tâches indépendantes. - Les guardrails se placent aux extrémités. Filtrez l'input avec un modèle peu coûteux avant que les spécialistes onéreux ne tournent, et validez l'output avant qu'il n'atteigne l'utilisateur. Utilisez
output_typeavec Pydantic pour que les contrôles renvoient des valeurs typées. - Le tracing n'est pas optionnel à l'échelle. Encapsulez les sessions dans
trace(...)et lisez le dashboard Traces pour déboguer le routage et mesurer si chaque découpage aide vraiment.
À faire, tiré de cette leçon
Ces actions sont compilées dans le plan d'action du rôle.
- Modéliser les systèmes comme de petits agents spécialistes avec handoffs et guardrails sur modèles low cost