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.
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.
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.
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.
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
| Situation | Pattern | Famille |
|---|---|---|
| Étapes fixes et ordonnées | Prompt chaining | Workflow |
| Catégories à traiter différemment | Routing | Workflow |
| Sous-tâches indépendantes (n fixe ou dynamique) | Parallélisation / Send | Workflow |
| Plan généré par un LLM puis exécuté | Orchestrateur-ouvriers | Hybride |
| Qualité critique, critère d’éval disponible | Évaluateur-optimiseur | Hybride |
| Sous-système réutilisable / isolé | Sous-graphe | Composition |
| Domaines d’expertise disjoints | Multi-agents superviseur | Agent |
| Chemin totalement imprévisible | ReAct | Agent |
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 :
- 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).
- 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.
Sendré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. Commandunifie 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é.