Changer le thème

Collaboration multi-agents en pratique : guide de choix entre 4 architectures

Easton editorial illustration: permission gate hub

Les recherches d’Anthropic montrent qu’un système multi-agents surpasse un mono-Agent de 90,2 %. Le mono-Agent a trois faiblesses fatales : limite de contexte (200k tokens saturés, puis sorties incohérentes), compétences dispersées (trop de skills = touche-à-tout), débogage difficile (un Agent en panne, impossible de localiser la source). Le système multi-agents, c’est l’« architecture microservices » de l’IA : chaque Agent ne fait qu’une chose, et collabore via un passage de messages clair.

Quatre architectures clés, chacune avec son cas d’usage : Subagents (orchestration centrale) pour plusieurs domaines indépendants en parallèle, Skills (chargement à la demande) pour un flux mono-thread multi-étapes, Handoffs (piloté par l’état) pour les dialogues multi-phases, Router (distribution parallèle) pour interroger plusieurs sources de données. Cet article couvre la logique de choix, les pièges en production et l’implémentation LangGraph — pour trancher entre Subagents et Skills, et gérer l’état entre Agents.

Pourquoi un système multi-agents

Franchement, le plus gros problème du mono-Agent n’est pas « est-ce utilisable », mais « combien de temps ça tient ».

Vous avez déjà vécu ça : un Agent écrivait du bon code, puis a commencé à produire n’importe quoi ? Ou vous demandiez A, et il partait sur B, puis C, jusqu’à Z ? Ce n’est pas qu’il soit « bête » — sa fenêtre de contexte est saturée.

Le mono-Agent a trois faiblesses fatales.

Limite de contexte. Même puissant, un Agent ne dispose que de 200k tokens de contexte. Demandez-lui revue de code, analyse de sécurité et optimisation des performances en même temps : retenir la moitié, c’est déjà optimiste. J’ai vu un Agent qui, après 20 tours sur Python, basculait en JavaScript au tour 21 — il avait oublié sa mission.

Compétences dispersées. Plus vous empilez de skills, plus il ressemble à un touche-à-tout — un peu de tout, expert en rien. Revue de code ? Il vous écrit des tests unitaires. Documentation ? Il refactorise le code au passage. Sens de l’orientation ? Inexistant.

Débogage difficile. Quand un Agent plante, impossible de savoir où ça coince. Prompt trop long ? Échec d’appel d’outil ? Pollution du contexte ? Chercher, c’est comme une aiguille dans une botte de foin.

Le système multi-agents, en clair, c’est l’architecture microservices de l’IA. Chaque Agent ne fait qu’une chose, et la fait bien. Ils collaborent via des messages clairs, au lieu de tout entasser dans un « super Agent ».

Google, LangChain et Anthropic poussent tous cette approche. Le rapport O’Reilly indique que les publications sur les Agents sont passées de 820 début 2025 à plus de 2 500 — triplées. Pourquoi ? Parce que l’ère du mono-Agent tout-puissant est révolue.

Quatre architectures clés

LangChain et Google ont formalisé les architectures multi-agents dominantes. Après plusieurs projets, chacune a son terrain — mal choisir, c’est tirer au canon sur une mouche ou manger de la soupe aux baguettes.

Subagents (sous-agents) — orchestration centrale

Le mode le plus intuitif : un « Agent principal » chef d’orchestre, entouré de « sous-agents ». Les sous-agents sont les outils du principal, qui décide quand en appeler lequel.

Requête utilisateur → Agent principal (coordinateur) → distribution aux sous-agents A/B/C → agrégation → retour utilisateur

Quand l’utiliser ? Tâches couvrant plusieurs domaines indépendants. Exemple service client : un sous-agent pour les commandes, un pour les remboursements, un pour les réclamations. Chaque domaine a sa base de connaissances et ses outils ; le principal ne fait que router.

Exemple de code (LangGraph) :

from langgraph.prebuilt import create_react_agent

# Définir les sous-agents
order_agent = create_react_agent(
    model="claude-3-5-sonnet-20241022",
    tools=[query_order, update_order],
    prompt="Vous êtes expert commandes, vous ne traitez que les questions liées aux commandes."
)

refund_agent = create_react_agent(
    model="claude-3-5-sonnet-20241022",
    tools=[check_refund_policy, process_refund],
    prompt="Vous êtes expert remboursements, vous ne traitez que les questions de remboursement."
)

# L'Agent principal détient les sous-agents comme outils
main_agent = create_react_agent(
    model="claude-3-5-sonnet-20241022",
    tools=[order_agent, refund_agent],  # Les sous-agents sont des outils
    prompt="Vous êtes le responsable du service client, distribuez les questions aux experts adaptés."
)

Avantages : isolation de contexte propre, chaque sous-agent ne voit que son périmètre. Exécution parallèle efficace.

Inconvénients : chaque sous-agent = un appel LLM indépendant, consommation de tokens élevée. Partager l’état entre sous-agents demande un détour.

Skills (compétences) — chargement à la demande

Un Agent, plusieurs « personnalités ». Les Skills sont essentiellement des modèles de prompts chargés dynamiquement. L’Agent change d’« identité » selon la tâche, mais reste le même Agent.

Requête utilisateur → Agent unique → chargement Skill « revue de code » → exécution → chargement Skill « génération doc » → exécution

Quand l’utiliser ? Traitement mono-thread, mais phases nécessitant des expertises différentes. Exemple assistant de programmation : mode « développeur » pour coder, mode « rédacteur technique » pour la doc.

Exemple de code :

# Structure du répertoire Skills
skills/
├── code_review.md      # Prompt revue de code
├── doc_writer.md       # Prompt génération doc
└── security_audit.md   # Prompt audit sécurité

# Chargement dynamique d'un Skill
def load_skill(skill_name: str) -> str:
    with open(f"skills/{skill_name}.md") as f:
        return f.read()

# Exemple d'utilisation
agent = create_react_agent(
    model="claude-3-5-sonnet-20241022",
    tools=[...],
    prompt=load_skill("code_review")  # Bascule à l'exécution
)

Avantages : léger, pas de surcoût de coordination Agent. Consommation de tokens inférieure aux Subagents.

Inconvénients : le contexte s’accumule. Après 10 bascules de Skill, le contenu des 9 précédents reste dans la fenêtre — de plus en plus confus.

Handoffs (transferts) — piloté par l’état

Les Agents se passent la tâche comme un relais. L’Agent A termine, « jette » l’état à l’Agent B, qui continue. Comme un relais.

Requête utilisateur → Agent A (collecte) → transfert → Agent B (analyse) → transfert → Agent C (solution)

Quand l’utiliser ? Dialogues multi-phases. Exemple support technique : collecter le problème → diagnostiquer → proposer une solution → confirmer la résolution. Chaque phase demande une expertise différente.

Exemple de code :

from langchain_core.tools import tool

# Définir les outils de transfert
@tool
def handoff_to_diagnosis(issue_summary: str) -> str:
    """Transférer le problème à l'expert diagnostic."""
    return f"Problème reçu : {issue_summary}, début du diagnostic..."

@tool
def handoff_to_solution(diagnosis_result: str) -> str:
    """Transférer le diagnostic à l'expert solution."""
    return f"Sur la base du diagnostic : {diagnosis_result}, élaboration de la solution..."

# Chaîne d'Agents
triage_agent = create_react_agent(
    tools=[handoff_to_diagnosis],
    prompt="Vous êtes trieur, collectez le problème utilisateur et transférez à l'expert diagnostic."
)

diagnosis_agent = create_react_agent(
    tools=[handoff_to_solution],
    prompt="Vous êtes expert diagnostic, analysez la cause racine et transférez à l'expert solution."
)

Avantages : flux de dialogue naturel, proche de la collaboration humaine. Chaque Agent ne se concentre que sur la phase en cours.

Inconvénients : gestion d’état complexe. Le format des données passées de A à B doit être correct, sinon la chaîne casse.

Router (routage) — distribution parallèle

Un « Agent routeur » analyse la requête, appelle en parallèle plusieurs Agents spécialisés, puis synthétise les résultats.

Requête utilisateur → Router (classification) → appels parallèles Agents A/B/C → synthèse → retour utilisateur

Quand l’utiliser ? Une requête doit interroger plusieurs sources. Exemple Q&R base de connaissances d’entreprise : le Router classe la question, interroge en parallèle docs internes, API externe et base de données, puis fusionne la réponse.

Exemple de code :

from langgraph.graph import StateGraph

# Définir les nœuds d'exécution parallèle
async def query_internal_docs(state):
    # Interroger la documentation interne
    return {"internal_results": [...]}

async def query_external_api(state):
    # Interroger l'API externe
    return {"external_results": [...]}

async def query_database(state):
    # Interroger la base de données
    return {"db_results": [...]}

async def synthesize(state):
    # Synthétiser tous les résultats
    all_results = state["internal_results"] + state["external_results"] + state["db_results"]
    return {"final_answer": summarize(all_results)}

# Construire le graphe parallèle
graph = StateGraph(State)
graph.add_node("internal", query_internal_docs)
graph.add_node("external", query_external_api)
graph.add_node("database", query_database)
graph.add_node("synthesize", synthesize)

# Exécution parallèle
graph.add_edge("router", ["internal", "external", "database"])
graph.add_edge(["internal", "external", "database"], "synthesize")

Avantages : exécution parallèle, vitesse maximale. Sans état, chaque requête est indépendante.

Inconvénients : peu adapté au dialogue multi-tours. Chaque requête repart de zéro — l’Agent n’a pas mémoire du tour précédent.

Cadre de décision pour choisir l’architecture

Bon, lequel choisir ? Voici un flux de décision simplifié :

Quel est votre besoin ?

         ├─→ Plusieurs domaines indépendants en parallèle ?
         │         │
         │         └─→ Subagents (orchestration centrale)

         ├─→ Mono-Agent, bascule de skills multi-étapes ?
         │         │
         │         └─→ Skills (chargement à la demande)

         ├─→ Workflow séquentiel, relais étape par étape ?
         │         │
         │         └─→ Handoffs (piloté par l'état)

         └─→ Requête multi-sources à synthétiser ?

                   └─→ Router (distribution parallèle)

Le flux seul peut manquer de clarté — voici un tableau comparatif :

ModeDev distribuéParallélisationDialogue multi-sautsInteraction directe utilisateurConsommation tokens
SubagentsÉlevéÉlevéÉlevéFaibleÉlevé
SkillsÉlevéMoyenÉlevéÉlevéFaible
HandoffsNonNonÉlevéÉlevéMoyen
RouterMoyenÉlevéNonMoyenÉlevé

Comment lire ce tableau ?

  • Dev distribué : l’équipe développe-t-elle des modules séparés ? Subagents et Skills conviennent — chacun prend un sous-Agent ou un Skill.
  • Parallélisation : la vitesse compte ? Router et Subagents exécutent plusieurs Agents en parallèle, le plus efficace.
  • Dialogue multi-sauts : l’utilisateur a-t-il besoin de plusieurs tours ? Handoffs et Skills supportent naturellement le flux conversationnel.
  • Interaction directe utilisateur : l’utilisateur parle-t-il directement aux sous-Agents ? Skills et Handoffs oui, Router non.
  • Consommation tokens : sensible au coût ? Skills est le plus économique, Router et Subagents les plus coûteux.

Mon expérience : partir simple. Validez d’abord un MVP avec Skills ou Handoffs ; si vous touchez un plafond, passez à Subagents ou Router. Ne démarrez pas en architecture distribuée — la sur-ingénierie, je connais.

Points clés pour la production

Entre la démo et la production, il y a un océan. Voici les pièges que j’ai tous frôlés.

Gestion d’état

Quand plusieurs Agents partagent un état, le problème classique est la condition de course — deux Agents écrivent la même variable, qui écrase qui ?

La solution LangGraph : output_key — chaque Agent n’écrit que dans sa clé dédiée.

from langgraph.graph import StateGraph, MessagesState

class GraphState(MessagesState):
    security_result: str = ""   # Réservé à l'Agent sécurité
    style_result: str = ""      # Réservé à l'Agent style
    perf_result: str = ""       # Réservé à l'Agent performance

# L'Agent sécurité n'écrit que security_result
async def security_agent(state: GraphState):
    result = await analyze_security(state["messages"])
    return {"security_result": result}  # Une seule clé

# L'Agent style n'écrit que style_result
async def style_agent(state: GraphState):
    result = await analyze_style(state["messages"])
    return {"style_result": result}

Ainsi, en parallèle ou en série, chaque Agent ne touche qu’à son périmètre — sans interférence.

Autre problème fréquent : pollution du contexte. La sortie de l’Agent A est lue par B, qui n’en a pas besoin. Ma pratique : ajouter un champ relevant_keys dans l’état, chaque Agent ne lit que les clés utiles.

Optimisation des performances

La consommation de tokens en multi-agents, c’est un gouffre. Quelques astuces :

1. Subagents économise 67 % de tokens vs Skills (scénario multi-domaines)

67%
Proportion de tokens économisés par Subagents vs Skills en scénario multi-domaines
Source: LangChain

Données de test LangChain : pour une tâche couvrant 3 domaines indépendants, Subagents consomme un tiers des tokens de Skills. Pourquoi ? Isolation de contexte — chaque sous-agent ne voit que son domaine. Avec Skills, tout le contexte des Skills s’accumule.

2. Mode avec état : 40-50 % d’appels en moins sur les requêtes répétées

Si la tâche comporte beaucoup de requêtes identiques (même question 10 fois), Handoffs avec état permet à l’Agent de mémoriser. Données LangChain : avec état vs sans état, près de la moitié d’appels LLM en moins.

3. Limiter les itérations en mode réflexion

Beaucoup ajoutent une « réflexion » — l’Agent vérifie sa sortie, corrige, regénère. Utile, mais risque de boucle infinie. Je limite généralement à max_iterations=2 ou 3, puis sortie forcée.

from langgraph.checkpoint.memory import MemorySaver

# Plafond d'itérations
graph = create_react_agent(
    model="claude-3-5-sonnet-20241022",
    tools=[...],
    checkpointer=MemorySaver(),
    config={"configurable": {"max_iterations": 3}}  # Max 3 réflexions
)

Pièges courants

Boucle infinie : l’Agent s’appelle lui-même, encore et encore. Solution : max_iterations et conditions de sortie explicites.

def should_continue(state):
    if state["iteration_count"] >= 3:
        return "end"
    if "done" in state["messages"][-1].content:
        return "end"
    return "continue"

Gonflement du contexte : l’Agent devient « bête », sorties de plus en plus courtes — contexte surchargé. Solution : mode Blackboard (tableau partagé), ne garder que le contexte nécessaire, nettoyage périodique.

Taxe de coordination : plus d’Agents, surcoût de communication exponentiel. Test perso : de 3 à 10 Agents, le temps de réponse passe de 2 s à 15 s. Solution : fusionner les Agents proches, rester sous 5 Agents.

Exemple d’implémentation complet

Assez de théorie — voici du concret. J’ai monté un système multi-agents de revue de code en mode Router + ParallelAgent.

Architecture : Router détecte langage et type → appels parallèles audit sécurité, style et performance → synthèse en rapport.

from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic

# Définir l'état
class CodeReviewState(TypedDict):
    code: str
    language: str
    security_issues: list
    style_issues: list
    perf_issues: list
    final_report: str

# Initialiser le LLM
llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")

# Router : détecter le langage
async def route_code(state: CodeReviewState) -> dict:
    code = state["code"]
    # Détection simple ; en prod, classification LLM
    if "def " in code or "import " in code:
        language = "python"
    elif "function" in code or "const " in code:
        language = "javascript"
    else:
        language = "unknown"
    return {"language": language}

# Agent audit sécurité
async def security_audit(state: CodeReviewState) -> dict:
    code = state["code"]
    prompt = f"""Vous êtes expert audit sécurité. Vérifiez le code suivant :
- Risque injection SQL
- Vulnérabilités XSS
- Fuite d'informations sensibles
- Dépendances non sécurisées

Code :
{code}

Sortie en liste JSON, chaque item : line (numéro), severity (gravité), description.
"""
    response = await llm.ainvoke(prompt)
    # Parser le résultat...
    return {"security_issues": []}

# Agent contrôle de style
async def style_check(state: CodeReviewState) -> dict:
    code = state["code"]
    language = state["language"]
    prompt = f"""Vous êtes expert style de code. Vérifiez ce code {language} :
- Conventions de nommage
- Formatage
- Complétude des commentaires

Code :
{code}

Sortie en liste JSON.
"""
    response = await llm.ainvoke(prompt)
    return {"style_issues": []}

# Agent analyse performance
async def perf_analysis(state: CodeReviewState) -> dict:
    code = state["code"]
    prompt = f"""Vous êtes expert performance. Vérifiez le code :
- Complexité temporelle excessive
- Boucles inutiles
- Risque fuite mémoire

Code :
{code}

Sortie en liste JSON.
"""
    response = await llm.ainvoke(prompt)
    return {"perf_issues": []}

# Rapport synthétique
async def generate_report(state: CodeReviewState) -> dict:
    security = state.get("security_issues", [])
    style = state.get("style_issues", [])
    perf = state.get("perf_issues", [])

    total_issues = len(security) + len(style) + len(perf)

    report = f"""# Rapport de revue de code

## Vue d'ensemble
- Langage : {state['language']}
- Total problèmes : {total_issues}

## Sécurité ({len(security)})
{format_issues(security)}

## Style ({len(style)})
{format_issues(style)}

## Performance ({len(perf)})
{format_issues(perf)}

## Recommandations
Prioriser la correction des problèmes de sécurité...
"""
    return {"final_report": report}

# Construire le graphe
graph = StateGraph(CodeReviewState)
graph.add_node("router", route_code)
graph.add_node("security", security_audit)
graph.add_node("style", style_check)
graph.add_node("perf", perf_analysis)
graph.add_node("report", generate_report)

# Flux : Router → trois contrôles en parallèle → rapport
graph.set_entry_point("router")
graph.add_edge("router", "security")
graph.add_edge("router", "style")
graph.add_edge("router", "perf")
graph.add_edge("security", "report")
graph.add_edge("style", "report")
graph.add_edge("perf", "report")
graph.add_edge("report", END)

# Compiler
app = graph.compile()

# Utilisation
async def review_code(code: str):
    result = await app.ainvoke({"code": code})
    return result["final_report"]

En pratique, ~100 lignes de code, trois Agents en parallèle : résultat en 3-5 secondes. En série, au moins 10 secondes.

C’est une version de base. En production, ajoutez : cache (même code non ré-analysé), revue incrémentale (seulement les changements), retour humain (marquer les faux positifs). Toutes ces extensions s’appuient sur cette architecture.

Conclusion

En résumé, trois idées :

Logique de base : le choix du mode compte plus que le framework. LangGraph, AutoGen, CrewAI sont de bons outils — mais si vous appliquez Router à un problème qui demande Handoffs, aucun framework ne vous sauvera.

Stratégie intermédiaire : partir simple, monter en puissance. Validez d’abord un MVP Skills ou Handoffs ; si vous touchez un plafond, passez à Subagents ou Router. La sur-ingénierie est le plus gros piège — je l’ai vécu, évitez-le.

Mise en production : état, performance et coût. Tokens, boucles infinies, pollution du contexte — maîtrisez ces trois points, et votre système multi-agents tournera de façon stable.

Prochaine étape : ouvrez la doc LangGraph, choisissez un mode, implémentez le système multi-agents le plus simple en 50 lignes. Ne réfléchissez pas trop — lancez-vous.

Construire un système multi-agents

Monter un système multi-agents de revue de code from scratch

  1. 1

    Step 1: Choisir l'architecture

    Sélectionner le mode adapté aux caractéristiques de la tâche
  2. 2

    Step 2: Définir la structure d'état

    Utiliser TypedDict pour l'état partagé entre Agents
  3. 3

    Step 3: Créer les nœuds Agent

    Une fonction nœud indépendante par Agent
  4. 4

    Step 4: Construire le graphe d'exécution

    Assembler le flux avec LangGraph StateGraph
  5. 5

    Step 5: Ajouter la gestion d'état

    Utiliser output_key pour éviter les conditions de course

FAQ

Quelle différence entre Subagents et Skills ?
Subagents : plusieurs Agents indépendants, contexte séparé, adapté aux tâches multi-domaines en parallèle. Skills : un seul Agent charge dynamiquement différents prompts, adapté au mono-thread multi-étapes. Subagents consomme plus de tokens mais isole mieux le contexte ; Skills est léger mais le contexte s'accumule.
Quand utiliser le mode Handoffs ?
Quand la tâche a un flux multi-phases clair et que chaque phase demande une expertise différente. Exemple support technique (collecte → diagnostic → solution → confirmation). Chaque Agent ne se concentre que sur la phase en cours, proche de la collaboration humaine.
Comment éviter les conditions de course en multi-agents ?
Utiliser output_key : chaque Agent n'écrit que dans sa clé dédiée. Exemple : Agent sécurité → security_result, Agent style → style_result, sans interférence. Ou mode Blackboard avec relevant_keys pour que chaque Agent ne lise que les champs utiles.
Comment maîtriser la consommation de tokens en multi-agents ?
Trois stratégies : 1) en multi-domaines, Subagents économise 67 % vs Skills ; 2) requêtes répétées, mode avec état économise 40-50 % d'appels ; 3) limiter les itérations de réflexion (max_iterations=2 ou 3). Limiter à 5 Agents évite aussi la taxe de coordination.
Quels cas d'usage pour le mode Router ?
Router convient aux requêtes parallèles multi-sources. Exemple Q&R base de connaissances : le Router classe la question, interroge en parallèle docs internes, API externe et BDD, puis synthétise. Avantage : vitesse maximale. Inconvénient : peu adapté au dialogue multi-tours.
Comment empêcher une boucle infinie Agent ?
Définir max_iterations (généralement 2-3) et des conditions de sortie explicites. Exemple : vérifier state['iteration_count'] ou la présence d'un marqueur de fin dans le message. MemorySaver de LangGraph aide à tracer l'état des itérations.

13 min de lecture · Publié le: 25 mars 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog