Un agent ReAct est un pattern d’orchestration parmi d’autres — souvent pas le bon. Quand une tâche se décompose en étapes connues, un workflow déterministe est plus fiable, plus rapide et moins cher qu’un agent autonome. LangGraph excelle aux deux : c’est un langage d’orchestration où l’on dessine explicitement le flux.

Ce deep-dive est un catalogue. Pour chaque pattern : quand l’employer, le schéma, et le code LangGraph. On termine par Command, la primitive qui les unifie tous.

L’état partagé, socle commun

Tous les patterns reposent sur le même mécanisme : un état typé qui circule, avec des reducers pour les champs accumulés. On le pose une fois.

from typing import Annotated
from typing_extensions import TypedDict
import operator

class Etat(TypedDict):
    sujet: str
    plan: list[str]
    sections: Annotated[list[str], operator.add]   # accumulé en parallèle
    final: str

Pattern 1 — Prompt chaining (séquentiel)

Quand ? La tâche se décompose en étapes fixes où chaque étape s’appuie sur la précédente. Le cas le plus simple, et souvent suffisant.

Fig.01 · Chaînage
plan esquisse
rédige à partir du plan
relit corrige le style
Des étapes fixes, dans l'ordre. Aucune décision du modèle sur le flux : c'est un workflow, pas un agent.
from langgraph.graph import StateGraph, START, END

def plan(etat: Etat) -> dict:
    r = modele.invoke(f"Esquisse un plan en 3 points sur : {etat['sujet']}")
    return {"plan": r.content.splitlines()}

def rediger(etat: Etat) -> dict:
    r = modele.invoke(f"Rédige l'article à partir du plan : {etat['plan']}")
    return {"final": r.content}

def relire(etat: Etat) -> dict:
    r = modele.invoke(f"Corrige le style de ce texte :\n{etat['final']}")
    return {"final": r.content}

g = StateGraph(Etat)
g.add_node("plan", plan); g.add_node("rediger", rediger); g.add_node("relire", relire)
g.add_edge(START, "plan")
g.add_edge("plan", "rediger")
g.add_edge("rediger", "relire")
g.add_edge("relire", END)
chaine = g.compile()

Pattern 2 — Routing (aiguillage)

Quand ? L’entrée appartient à des catégories qui demandent des traitements distincts. On classe, puis on route — chaque branche est spécialisée et donc plus fiable qu’un prompt unique « fourre-tout ».

from typing import Literal

def classer(etat: Etat) -> dict:
    cat = modele.invoke(f"Classe en [facturation|technique|autre] : {etat['sujet']}")
    return {"categorie": cat.content.strip()}

def router(etat: Etat) -> Literal["facturation", "technique", "autre"]:
    return etat["categorie"]      # la cible est décidée à l'exécution

g.add_node("classer", classer)
g.add_conditional_edges("classer", router, {
    "facturation": "agent_factu",
    "technique": "agent_tech",
    "autre": "agent_generique",
})

La fonction de routage peut renvoyer le nom d’un nœud (comme ici) ou une liste de Send (pattern suivant). C’est le même mécanisme d’arête conditionnelle vu en initiation, porté à l’échelle d’un aiguillage métier.

Pattern 3 — Parallélisation & map-reduce (Send)

Quand ? Plusieurs sous-tâches indépendantes peuvent s’exécuter de front — soit un ensemble fixe de branches, soit un nombre dynamique déterminé à l’exécution (une section par point du plan). C’est le pattern map-reduce.

Fig.02 · Fan-out / fan-in
Send() disperse worker A worker B worker C
L'API Send crée dynamiquement une branche par élément ; le reducer du champ « sections » agrège les résultats à la jonction.

Le cœur est l’API Send : depuis une arête conditionnelle, on renvoie une liste de Send("nœud", état_partiel) — LangGraph instancie une exécution du nœud par Send, toutes en parallèle.

from langgraph.types import Send

# MAP : une branche par point du plan (nombre connu seulement à l'exécution)
def disperser(etat: Etat) -> list[Send]:
    return [Send("ecrire_section", {"titre": p}) for p in etat["plan"]]

def ecrire_section(etat: dict) -> dict:
    r = modele.invoke(f"Rédige la section : {etat['titre']}")
    return {"sections": [r.content]}      # reducer operator.add → accumulation

# REDUCE : agréger les sections produites en parallèle
def assembler(etat: Etat) -> dict:
    return {"final": "\n\n".join(etat["sections"])}

g.add_node("ecrire_section", ecrire_section)
g.add_node("assembler", assembler)
g.add_conditional_edges("plan", disperser, ["ecrire_section"])
g.add_edge("ecrire_section", "assembler")

Pattern 4 — Orchestrateur-ouvriers

Quand ? Les sous-tâches ne sont pas connues d’avance : c’est un LLM orchestrateur qui établit le plan, puis délègue chaque élément à des ouvriers. C’est la version « agentique » du map-reduce : le plan est généré, pas codé en dur.

class PlanStructure(BaseModel):
    sections: list[str] = Field(description="Liste ordonnée des sections à rédiger")

def orchestrateur(etat: Etat) -> dict:
    plan = modele.with_structured_output(PlanStructure).invoke(
        f"Découpe en sections un article sur : {etat['sujet']}"
    )
    return {"plan": plan.sections}

# l'orchestrateur décide dynamiquement combien d'ouvriers lancer
def deleguer(etat: Etat) -> list[Send]:
    return [Send("ouvrier", {"titre": s}) for s in etat["plan"]]

g.add_node("orchestrateur", orchestrateur)
g.add_conditional_edges("orchestrateur", deleguer, ["ouvrier"])

La différence avec le pattern 3 est subtile mais cruciale : ici le plan lui-même est produit par un modèle. On gagne en flexibilité (s’adapte à n’importe quel sujet) ce qu’on perd en prévisibilité — d’où l’importance de tracer.

Pattern 5 — Évaluateur-optimiseur (boucle de qualité)

Quand ? La qualité compte plus que la latence, et on dispose d’un critère d’évaluation. Un générateur produit, un évaluateur juge ; on reboucle tant que ce n’est pas bon. C’est la mise en graphe de l’auto-réflexion.

Fig.03 · Évaluateur-optimiseur
produit critique · rejeté accepté générateur évaluateur LLM-juge END
Le générateur produit, l'évaluateur (souvent un LLM-juge) accepte ou renvoie une critique qui relance la génération. On borne toujours le nombre d'itérations.
class Verdict(BaseModel):
    accepte: bool
    critique: str = Field(description="Ce qu'il faut corriger si refusé")

def generer(etat: Etat) -> dict:
    consigne = etat.get("critique", "")
    r = modele.invoke(f"Rédige (consigne de révision : {consigne}) : {etat['sujet']}")
    return {"final": r.content, "iterations": etat.get("iterations", 0) + 1}

def evaluer(etat: Etat) -> dict:
    v = modele.with_structured_output(Verdict).invoke(f"Évalue ce texte :\n{etat['final']}")
    return {"accepte": v.accepte, "critique": v.critique}

def boucler(etat: Etat) -> Literal["generer", "__end__"]:
    if etat["accepte"] or etat["iterations"] >= 3:    # garde-fou anti-boucle
        return END
    return "generer"

g.add_edge("generer", "evaluer")
g.add_conditional_edges("evaluer", boucler, {"generer": "generer", END: END})

Pattern 6 — Sous-graphes (composition)

Quand ? Un sous-système (un agent complet, un pipeline RAG) doit être réutilisé ou isolé. On compile un graphe et on l’ajoute comme nœud d’un graphe parent. La modularité de LangGraph tient là.

# un agent RAG complet, compilé indépendamment
rag = construire_rag().compile()

# réutilisé tel quel comme un simple nœud du graphe parent
parent = StateGraph(Etat)
parent.add_node("recherche", rag)       # le sous-graphe EST un nœud
parent.add_node("synthese", synthese)
parent.add_edge(START, "recherche")
parent.add_edge("recherche", "synthese")

Si parent et sous-graphe partagent des clés d’état, l’état circule de façon transparente. Sinon, on adapte l’entrée/sortie dans une fonction d’enveloppe. Les sous-graphes ont leur propre checkpointing et apparaissent imbriqués dans les traces LangFuse — un atout majeur pour déboguer les systèmes complexes.

Pattern 7 — Multi-agents en superviseur

Quand ? Le problème se découpe en domaines d’expertise distincts (recherche, rédaction, code), chacun avec ses propres outils. Un agent superviseur route vers l’agent spécialisé adéquat et reprend la main entre chaque.

Fig.04 · Superviseur
délègue ↩ retour superviseur route recherche agent + outils rédaction agent code agent + REPL
Le superviseur est un routeur : il délègue à un agent spécialisé, récupère le résultat, et décide de la suite — déléguer encore ou terminer.

Chaque agent est un sous-graphe ; le superviseur les orchestre via Command, qui permet en une seule valeur de mettre à jour l’état et de router :

from langgraph.types import Command
from typing import Literal

def superviseur(etat: Etat) -> Command[Literal["recherche", "redaction", "code", "__end__"]]:
    decision = modele.with_structured_output(Routage).invoke(etat["messages"])
    if decision.fini:
        return Command(goto=END)
    # route vers l'agent choisi ET journalise la décision dans l'état
    return Command(goto=decision.agent, update={"derniere_delegation": decision.agent})

# chaque agent, après son travail, rend la main au superviseur
def agent_recherche(etat: Etat) -> Command[Literal["superviseur"]]:
    resultat = sous_agent_recherche.invoke(etat)
    return Command(goto="superviseur", update={"messages": resultat["messages"]})

g.add_node("superviseur", superviseur)
g.add_node("recherche", agent_recherche)
# … redaction, code …
g.add_edge(START, "superviseur")

Command : la primitive qui unifie tout

Vous l’avez vu apparaître au fil des patterns. Command est la clé de voûte de l’orchestration avancée : un nœud peut renvoyer à la fois une mise à jour d’état et une cible de routage.

return Command(
    update={"messages": [reponse], "etape": "validee"},   # ce qui change dans l'état
    goto="noeud_suivant",                                  # où aller ensuite
    graph=Command.PARENT,    # (optionnel) router dans le graphe parent — handoffs
)

Là où l’arête conditionnelle ne fait que router, Command route et écrit. C’est ce qui rend possibles les handoffs entre agents (graph=Command.PARENT), les superviseurs, et tout flux où la décision et la donnée sont liées.

Choisir le bon pattern

SituationPatternFamille
Étapes fixes et ordonnéesPrompt chainingWorkflow
Catégories à traiter différemmentRoutingWorkflow
Sous-tâches indépendantes (n fixe ou dynamique)Parallélisation / SendWorkflow
Plan généré par un LLM puis exécutéOrchestrateur-ouvriersHybride
Qualité critique, critère d’éval disponibleÉvaluateur-optimiseurHybride
Sous-système réutilisable / isoléSous-grapheComposition
Domaines d’expertise disjointsMulti-agents superviseurAgent
Chemin totalement imprévisibleReActAgent

Bonnes pratiques & pièges

Choisir un pattern est facile ; le faire tenir en production l’est moins. Les règles qui distinguent une orchestration robuste d’un assemblage fragile :

Bonnes pratiques
  • Choisissez le pattern le plus simple qui marche : un workflow bat souvent un agent.
  • Mettez un reducer (operator.add) sur tout champ agrégé en parallèle — sinon le fan-in écrase.
  • Bornez toujours les boucles : compteur d'itérations ou recursion_limit à l'invocation.
  • Isolez les sous-systèmes en sous-graphes : modularité et traces imbriquées lisibles.
  • Utilisez Command(goto=..., update=...) quand la décision et la donnée sont liées.
  • Tracez systématiquement les flux générés par un LLM (orchestrateur, superviseur).
Pièges à éviter
  • Dégainer un système multi-agents là où un agent bien outillé suffirait : contexte fragmenté.
  • Fan-out sans reducer : les branches parallèles s'écrasent, il ne reste qu'un seul résultat.
  • Boucle évaluateur-optimiseur non bornée : un juge trop strict ne termine jamais.
  • Confondre routing (renvoie un nom de nœud) et Send (parallélise) : besoins distincts.
  • Multiplier les agents avant de mesurer : complexité et coût explosent sans gain prouvé.
  • Oublier que chaque tour d'orchestrateur ou de superviseur est un appel LLM de plus.

Ce qu’il faut retenir

  • LangGraph orchestre aussi bien des workflows déterministes que des agents ; préférez le pattern le plus simple qui marche.
  • Send réalise la parallélisation et le map-reduce dynamiques ; un reducer sur le champ agrégé est indispensable au fan-in.
  • L’évaluateur-optimiseur met la qualité en boucle — toujours bornée.
  • Les sous-graphes rendent les systèmes modulaires et lisibles dans les traces.
  • Le multi-agents superviseur se câble avec Command(goto=…, update=…) — et ne s’emploie qu’en dernier recours.
  • Command unifie routage + mise à jour d’état, et autorise les handoffs.

Vous avez le catalogue. La pièce manquante pour qu’un système composite reste maîtrisable en production, c’est l’observabilité — voir, à travers toutes ces couches, ce que fait réellement l’agent. C’est le rôle de LangFuse, illustré dans le cas pratique de support tracé.

#langgraph#orchestration#multi-agent#send#command#patterns