+210 XP

Orchestration multi-agents : handoffs et agents parallèles

Un agent bourré de douze tools et d'un system prompt 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 guardrails 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.

python
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 :

python
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 tokens : 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 maî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é.

python
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 ?

CHOIX MULTIPLES

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.

CHOIX MULTIPLES

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 :

python
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_output

Dans 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

Watch on YouTube

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 brû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_description comme 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.gather ré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_type avec 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
Voir le plan d'action complet →