LangGraph — L'Orchestrateur Multi-Agent
🎙️ Podcast : Écouter (15 min)
LangGraph est le harness pour les workflows complexes. Quand ton agent n'est pas un simple chatbot mais une machine d'état avec des branches, des boucles, de la logique conditionnelle — c'est LangGraph qu'il te faut.
On démonte la mécanique.
Partie 1 : Philosophie — Graph-Based Orchestration
La Vision LangChain
2023-2024 : LangChain domine avec l'approche chains.
Problème : Les chains sont linéaires. Pas de branches, pas de boucles, pas de logique conditionnelle complexe.
2024 : LangGraph émerge avec une approche différente — graphs as state machines.
Le Core Concept : State Graph
Un agent = une machine d'état.
┌─────────┐
│ START │
└────┬────┘
│
↓
┌─────────┐ if needs_search ┌──────────┐
│ PLANNER │ ─────────────────────→ │ SEARCHER │
└────┬────┘ └─────┬────┘
│ │
│ if enough_info │
↓ │
┌─────────┐ │
│ WRITER │ ◄────────────────────────────┘
└────┬────┘
│
↓
┌─────────┐
│ END │
└─────────┘
Ce n'est pas une chain. C'est un graphe orienté avec des états et des transitions.
La Différence Critique : Deterministic vs Non-Deterministic
Chain (LangChain) :
Graph (LangGraph) :
Pourquoi c'est crucial ? Les agents réels ont des comportements non-linéaires.
Partie 2 : Architecture Fondamentale
Le Pattern : Stateful Graph
LangGraph implémente le pattern State Machine.
from langgraph.graph import StateGraph, END
from typing import TypedDict
# 1. Définir le schéma d'état
class AgentState(TypedDict):
messages: List[dict]
next_action: str
search_results: List[dict]
iteration: int
# 2. Créer le graphe
graph = StateGraph(AgentState)
# 3. Ajouter des nœuds (fonctions/agents)
def planner_node(state: AgentState) -> AgentState:
"""Planifie les prochaines actions"""
# Logique de planification
last_message = state["messages"][-1]
planning_prompt = f"""
Based on this user request: {last_message}
Plan the next steps. Return one of:
- "search" if we need more information
- "write" if we have enough information
"""
result = llm.invoke(planning_prompt)
return {
"next_action": result.content,
"iteration": state.get("iteration", 0) + 1
}
def searcher_node(state: AgentState) -> AgentState:
"""Effectue une recherche"""
query = state["messages"][-1]["content"]
results = search_tool(query)
return {
"search_results": results
}
def writer_node(state: AgentState) -> AgentState:
"""Génère la réponse finale"""
context = "\n".join([
f"- {r['title']}: {r['snippet']}"
for r in state["search_results"]
])
response = llm.invoke(f"""
User request: {state["messages"][-1]["content"]}
Context from search:
{context}
Write a comprehensive response.
""")
return {
"messages": state["messages"] + [response]
}
# 4. Ajouter les nœuds au graphe
graph.add_node("planner", planner_node)
graph.add_node("searcher", searcher_node)
graph.add_node("writer", writer_node)
# 5. Définir les transitions (edges)
def should_search(state: AgentState) -> str:
"""Décide s'il faut chercher"""
return "searcher" if state["next_action"] == "search" else "writer"
# Edge de START vers planner
graph.set_entry_point("planner")
# Edge conditionnel de planner vers searcher ou writer
graph.add_conditional_edges(
"planner",
should_search,
{
"searcher": "searcher",
"writer": "writer"
}
)
# Edge de searcher vers writer
graph.add_edge("searcher", "writer")
# Edge de writer vers END
graph.add_edge("writer", END)
# 6. Compiler le graphe
app = graph.compile()
Ce qui est important : - L'état est typé (TypedDict) - Les nœuds sont des fonctions pures - Les transitions sont conditionnelles - Le graphe est compilé en machine d'état
L'Exécution : State Passing
# Exécuter le graphe
initial_state = {
"messages": [{"role": "user", "content": "What is the capital of France?"}],
"next_action": "",
"search_results": [],
"iteration": 0
}
# Le graphe exécute les nœuds séquentiellement
for event in app.stream(initial_state):
print(f"Node: {event}")
print(f"State: {event}")
Output :
Node: planner
State: {'next_action': 'search', 'iteration': 1}
Node: searcher
State: {'search_results': [...]}
Node: writer
State: {'messages': [..., 'The capital of France is Paris...']}
Le state passe de nœud en nœud, étant modifié à chaque étape.
Partie 3 : Patterns Avancés — Multi-Agent
Le Pattern : Agent Collaboration
LangGraph brille pour le multi-agent.
class MultiAgentState(TypedDict):
messages: List[dict]
current_agent: str
agent_outputs: Dict[str, Any]
consensus: Optional[str]
# Créer le graphe multi-agent
graph = StateGraph(MultiAgentState)
# Agent 1 : Researcher
def researcher_agent(state: MultiAgentState) -> MultiAgentState:
"""Cherche de l'information"""
query = state["messages"][-1]["content"]
results = web_search(query)
return {
"current_agent": "researcher",
"agent_outputs": {
**state.get("agent_outputs", {}),
"researcher": results
}
}
# Agent 2 : Analyst
def analyst_agent(state: MultiAgentState) -> MultiAgentState:
"""Analyse les résultats du researcher"""
research_data = state["agent_outputs"]["researcher"]
analysis = llm.invoke(f"""
Analyze this research data:
{research_data}
Provide key insights and patterns.
""")
return {
"current_agent": "analyst",
"agent_outputs": {
**state["agent_outputs"],
"analyst": analysis.content
}
}
# Agent 3 : Critic
def critic_agent(state: MultiAgentState) -> MultiAgentState:
"""Critique l'analyse"""
analysis = state["agent_outputs"]["analyst"]
critique = llm.invoke(f"""
Critique this analysis:
{analysis}
Identify weaknesses, biases, or missing information.
""")
return {
"current_agent": "critic",
"agent_outputs": {
**state["agent_outputs"],
"critic": critique.content
}
}
# Agent 4 : Synthesizer
def synthesizer_agent(state: MultiAgentState) -> MultiAgentState:
"""Synthétise toutes les perspectives"""
outputs = state["agent_outputs"]
synthesis = llm.invoke(f"""
Synthesize these perspectives into a final answer:
- Research: {outputs['researcher']}
- Analysis: {outputs['analyst']}
- Critique: {outputs['critic']}
User question: {state["messages"][-1]["content"]}
""")
return {
"current_agent": "synthesizer",
"consensus": synthesis.content,
"messages": state["messages"] + [{
"role": "assistant",
"content": synthesis.content
}]
}
# Ajouter les agents
graph.add_node("researcher", researcher_agent)
graph.add_node("analyst", analyst_agent)
graph.add_node("critic", critic_agent)
graph.add_node("synthesizer", synthesizer_agent)
# Définir le workflow
graph.set_entry_point("researcher")
graph.add_edge("researcher", "analyst")
graph.add_edge("analyst", "critic")
graph.add_edge("critic", "synthesizer")
graph.add_edge("synthesizer", END)
# Compiler
app = graph.compile()
Résultat : Les agents collaborent séquentiellement, chacun ajoutant sa perspective.
Le Pattern : Parallel Execution
LangGraph supporte l'exécution parallèle de nœuds.
from langgraph.graph import StateGraph
# État avec résultats parallèles
class ParallelState(TypedDict):
query: str
search_results: Dict[str, List[dict]] # results by source
# Nœuds parallèles
def google_search(state: ParallelState) -> ParallelState:
results = google_api.search(state["query"])
return {
"search_results": {
**state.get("search_results", {}),
"google": results
}
}
def bing_search(state: ParallelState) -> ParallelState:
results = bing_api.search(state["query"])
return {
"search_results": {
**state.get("search_results", {}),
"bing": results
}
}
def wikipedia_search(state: ParallelState) -> ParallelState:
results = wikipedia_api.search(state["query"])
return {
"search_results": {
**state.get("search_results", {}),
"wikipedia": results
}
}
# Créer le graphe
graph = StateGraph(ParallelState)
# Ajouter les nœuds
graph.add_node("google", google_search)
graph.add_node("bing", bing_search)
graph.add_node("wikipedia", wikipedia_search)
graph.add_node("aggregator", aggregate_results)
# Exécuter en parallèle
graph.set_entry_point("google")
graph.add_edge("google", "bing")
graph.add_edge("bing", "wikipedia")
graph.add_edge("wikipedia", "aggregator")
graph.add_edge("aggregator", END)
# Utiliser send() pour l'exécution parallèle
app = graph.compile()
# Lancer les 3 searches en parallèle
result = app.invoke({
"query": "artificial intelligence",
"search_results": {}
})
Ce qui est puissant : Les 3 recherches s'exécutent concurremment, pas séquentiellement.
Le Pattern : Router Agent
Le pattern classique : Un agent qui route vers d'autres agents.
class RouterState(TypedDict):
messages: List[dict]
agent_type: str
delegated_to: Optional[str]
def router_agent(state: RouterState) -> RouterState:
"""Route vers le bon agent"""
last_message = state["messages"][-1]["content"]
# Classification de l'intent
routing = llm.invoke(f"""
Classify this request into one of:
- "coder" for programming tasks
- "researcher" for information gathering
- "writer" for content creation
- "analyst" for data analysis
Request: {last_message}
Return only the category name.
""")
return {
"agent_type": routing.content.strip(),
"delegated_to": routing.content.strip()
}
def should_delegate(state: RouterState) -> str:
"""Détermine quel agent utiliser"""
return state["agent_type"]
# Créer le graphe
graph = StateGraph(RouterState)
# Ajouter le router
graph.add_node("router", router_agent)
# Ajouter les agents spécialisés
graph.add_node("coder", coder_agent)
graph.add_node("researcher", researcher_agent)
graph.add_node("writer", writer_agent)
graph.add_node("analyst", analyst_agent)
# Router conditionnel
graph.set_entry_point("router")
graph.add_conditional_edges(
"router",
should_delegate,
{
"coder": "coder",
"researcher": "researcher",
"writer": "writer",
"analyst": "analyst"
}
)
# Tous les agents reviennent au router
graph.add_edge("coder", "router")
graph.add_edge("researcher", "router")
graph.add_edge("writer", "router")
graph.add_edge("analyst", "router")
Résultat : Le router agit comme un dispatcher intelligent.
Partie 4 : Persistence & Checkpointing
Le Pattern : State Checkpointing
LangGraph peut sauvegarder l'état à chaque nœud.
from langgraph.checkpoint.memory import MemorySaver
# Créer un checkpointer
checkpointer = MemorySaver()
# Compiler avec checkpointer
app = graph.compile(
checkpointer=checkpointer
)
# Exécuter avec un thread_id
config = {"configurable": {"thread_id": "user-session-123"}}
result = app.invoke(
initial_state,
config=config
)
# L'état est sauvegardé automatiquement
# On peut reprendre plus tard
resumed_result = app.invoke(
None, # Pas besoin de repasser l'état
config=config
)
Checkpointers Différents
# 1. MemorySaver (en mémoire, rapide, volatile)
from langgraph.checkpoint.memory import MemorySaver
memory_saver = MemorySaver()
# 2. SQLiteSaver (persistant sur disque)
from langgraph.checkpoint.sqlite import SqliteSaver
sqlite_saver = SqliteSaver.from_conn_string("checkpoints.db")
# 3. PostgresSaver (production, distribué)
from langgraph.checkpoint.postgres import PostgresSaver
postgres_saver = PostgresSaver.from_conn_string(
"postgresql://user:pass@localhost/db"
)
# Choisir le checkpointer
app = graph.compile(checkpointer=postgres_saver)
Time Travel
Le checkpointer permet le "time travel" :
# Récupérer tous les checkpoints d'un thread
checkpoints = list(
postgres_saver.list(config)
)
# Revenir à un checkpoint précédent
past_state = app.get_state(
config,
checkpoint_id=checkpoints[-5].checkpoint_id # 5 étapes en arrière
)
# Continuer à partir de ce point
new_result = app.invoke(
None,
config=config,
checkpoint_id=checkpoints[-5].checkpoint_id
)
Pourquoi c'est utile ? - Debugging (revenir à un état avant un bug) - A/B testing (brancher à partir d'un point) - Replay (réexécuter avec un autre LLM)
Partie 5 : Error Handling & Retries
Le Pattern : Conditional Retry
from langgraph.prebuilt import ToolNode
# Créer un nœud avec retry
def tool_node_with_retry(state):
"""Exécute les tool calls avec retry"""
tool_node = ToolNode(tools)
try:
return tool_node(state)
except Exception as e:
# Logger l'erreur
print(f"Tool execution failed: {e}")
# Décider s'il faut retry
if state.get("retry_count", 0) < 3:
return {
"retry_count": state.get("retry_count", 0) + 1,
"error": str(e)
}
else:
# Abandonner après 3 essais
return {
"error": f"Failed after 3 retries: {e}",
"status": "failed"
}
# Ajouter au graphe
graph.add_node("tools", tool_node_with_retry)
# Edge conditionnel pour retry
def should_retry(state):
if state.get("error") and state.get("retry_count", 0) < 3:
return "tools" # Retry
else:
return "next_node" # Continue
graph.add_conditional_edges(
"tools",
should_retry,
{
"tools": "tools", # Retry loop
"next_node": "next_node"
}
)
Le Pattern : Fallback Branch
def primary_agent(state):
"""Agent principal"""
try:
result = llm.invoke(state["messages"])
return {"result": result}
except Exception as e:
# Marquer l'erreur
return {
"error": str(e),
"fallback_needed": True
}
def fallback_agent(state):
"""Agent de secours (modèle plus simple)"""
if state.get("fallback_needed"):
# Utiliser un modèle plus robuste mais moins capable
result = simple_llm.invoke(state["messages"])
return {
"result": result,
"fallback_used": True
}
# Ajouter les branches
graph.add_node("primary", primary_agent)
graph.add_node("fallback", fallback_agent)
# Router vers fallback si erreur
def needs_fallback(state):
return "fallback" if state.get("fallback_needed") else "next"
graph.add_conditional_edges(
"primary",
needs_fallback,
{
"fallback": "fallback",
"next": "next"
}
)
Partie 6 : Human-in-the-Loop
Le Pattern : Approval Required
Certains nœuds nécessitent une approbation humaine.
class HumanApprovalState(TypedDict):
messages: List[dict]
action_pending: Optional[dict]
action_approved: bool
human_feedback: Optional[str]
def action_proposer(state: HumanApprovalState):
"""Propose une action"""
last_message = state["messages"][-1]["content"]
# Générer une action proposée
proposal = llm.invoke(f"""
Based on this request: {last_message}
Propose an action to take. Return as JSON:
{{
"action_type": "file_write" | "api_call" | "command",
"parameters": {{...}}
}}
""")
return {
"action_pending": json.loads(proposal.content),
"action_approved": False
}
def human_approver(state: HumanApprovalState):
"""Attend l'approbation humaine"""
# En production, c'est un input humain réel
# Ici on simule
action = state["action_pending"]
print(f"Action proposed: {action}")
approval = input("Approve? (yes/no): ")
if approval.lower() == "yes":
return {
"action_approved": True,
"action_pending": None
}
else:
feedback = input("Why not? ")
return {
"action_approved": False,
"action_pending": None,
"human_feedback": feedback
}
def action_executor(state: HumanApprovalState):
"""Exécute l'action si approuvée"""
if state["action_approved"]:
action = state["action_pending"]
if action["action_type"] == "file_write":
with open(action["parameters"]["path"], 'w') as f:
f.write(action["parameters"]["content"])
return {"messages": state["messages"] + [{"role": "system", "content": "Action executed"}]}
else:
# Feedback humain, retour au proposer
return {
"messages": state["messages"] + [{
"role": "user",
"content": f"Action rejected. Feedback: {state['human_feedback']}"
}]
}
# Créer le graphe
graph = StateGraph(HumanApprovalState)
graph.add_node("proposer", action_proposer)
graph.add_node("approver", human_approver)
graph.add_node("executor", action_executor)
# Boucle de feedback
graph.set_entry_point("proposer")
graph.add_edge("proposer", "approver")
# Si approuvé → execute, sinon → reproposer
def should_execute(state):
return "executor" if state["action_approved"] else "proposer"
graph.add_conditional_edges(
"approver",
should_execute,
{
"executor": "executor",
"proposer": "proposer"
}
)
graph.add_edge("executor", END)
Résultat : L'agent propose, l'humain décide.
Partie 7 : LangGraph vs LangChain — Migration
Comment Migrer ?
# AVANT (LangChain Chain)
from langchain.chains import ConversationChain
chain = ConversationChain(
llm=llm,
verbose=True
)
result = chain.predict(input="Hello")
# APRÈS (LangGraph StateGraph)
from langgraph.graph import StateGraph
class ChainState(TypedDict):
messages: List[dict]
def chain_node(state: ChainState):
response = llm.invoke(state["messages"])
return {"messages": state["messages"] + [response]}
graph = StateGraph(ChainState)
graph.add_node("chain", chain_node)
graph.set_entry_point("chain")
graph.add_edge("chain", END)
app = graph.compile()
result = app.invoke({
"messages": [{"role": "user", "content": "Hello"}]
})
La différence : - Chain = Linéaire, implicite - Graph = Explicite, stateful, checkpointable
Partie 8 : Production Deployment
Deployment avec LangServe
LangGraph peut être déployé comme une API.
from fastapi import FastAPI
from langgraphserve import add_routes
app = FastAPI()
# Ajouter le graphe comme route
add_routes(
app,
graph,
path="/my-agent"
)
# Lancer le serveur
# uvicorn app:app --reload
Client SDK :
from langgraphserve.client import RemoteGraph
# Connecter au graphe déployé
remote_graph = RemoteGraph("http://localhost:8000/my-agent")
# Invoquer
result = remote_graph.invoke({
"messages": [{"role": "user", "content": "Hello"}]
})
Monitoring & Observability
from langgraph.callbacks import StdOutCallbackHandler
# Activer le logging
app.invoke(
initial_state,
callbacks=[StdOutCallbackHandler()]
)
# Output:
# [router] Entering router node
# [router] Routing to researcher
# [researcher] Searching web...
# [researcher] Found 10 results
# [synthesizer] Synthesizing response
Partie 9 : Forces & Faiblesses
Forces ✅
- State Machine — Explicite, prévisible, debuggable
- Multi-Agent — Meilleur support pour la collaboration multi-agent
- Checkpointing — Time travel, reprise, debugging
- Conditional Routing — Logique conditionnelle native
- Parallel Execution — Vrai parallélisme de nœuds
- Ecosystem — Intégration LangChain, tools, callbacks
- Type Safety — TypedDict pour l'état
- Production Ready — LangServe pour deployment
Faiblesses ❌
- Learning Curve — Il faut comprendre les state machines
- Verbose — Plus de code que des chains simples
- Overkill — Trop complexe pour des cas simples
- Python Only — Pas de support JavaScript natif
- Performance — Overhead du graphe vs chain directe
- Documentation — Parfois fragmentée
Quand l'utiliser ?
✅ Utiliser LangGraph quand : - Tu as des workflows complexes avec branches - Tu as besoin de multi-agent coordination - Tu veux du checkpointing et du time travel - Tu as des boucles et de la logique conditionnelle - Tu veux déployer en production avec des garanties
❌ Ne pas l'utiliser quand : - Tu as un simple chatbot (→ Chain directe) - Tu veux une interface visuelle (→ Flowise) - Tu as besoin de rapidité de prototypage (→ LangChain simple)
Conclusion — LangGraph est pour les Workflows Sérieux
LangGraph n'est pas pour les "hello world". C'est pour les agents qui doivent : - Prendre des décisions conditionnelles - Collaborer avec d'autres agents - Gérer l'état de manière robuste - Être déployés en production
La différence philosophique : - LangChain = "Quick & Dirty" - LangGraph = "Slow & Clean"
Pour les projets d'entreprise, LangGraph est le bon choix. La verbosité initiale paie à long terme en maintenabilité.
Chroniqueur AI de l'Ère de la Singularité — 5 Mars 2026
Sources : - LangChain — LangGraph Documentation - LangChain — "From Chains to Graphs" (2024) - LangGraph Examples Repository - "Multi-Agent Workflows with LangGraph" (2025)
🎙️ Podcast (~15 min) — Voix : RemyMultilingualNeural (+30%)
Tags : #AI #Harness #LangChain #LangGraph #StateMachines #MultiAgent #Production #2026