Archives du blog

LangGraph Expliqué : Pourquoi les Workflows Cycliques Changent la Donne pour les Agents Autonomes

La plupart des frameworks IA actuels construisent des pipelines linéaires : une entrée traverse une séquence d'étapes et produit une sortie. C'est simple, prévisible, et ça fonctionne — jusqu'à ce que vous ayez besoin d'un agent qui raisonne, agit, observe et adapte son comportement. Les agents autonomes ont besoin de boucles, pas de lignes droites. C'est exactement ce problème que LangGraph résout avec ses workflows cycliques. Cet article vous explique pourquoi c'est un changement de paradigme.

Le Problème des Chaînes Linéaires

Les DAGs : une illusion de contrôle

Les chaînes LangChain traditionnelles sont des DAGs (Directed Acyclic Graphs) — des graphes orientés acycliques. L'information y circule dans une seule direction, sans jamais revenir en arrière.

Entrée -> Retrieval -> LLM -> Formatage -> Sortie

Ce modèle fonctionne parfaitement pour les tâches de transformation :

  • RAG (question -> recherche -> synthèse -> réponse)
  • Extraction (document -> parsing -> structuration -> base de données)
  • Traduction (texte source -> traduction -> révision -> texte final)

Mais dès que vous introduisez de l'autonomie, le modèle se brise.

Pourquoi les agents autonomes ont besoin de boucles

Un agent autonome ne suit pas un chemin prédéfini. Il observe, raisonne, agit et réévalue en permanence. C'est le cycle OODA (Observe, Orient, Decide, Act) popularisé par le strategiste militaire John Boyd, appliqué à l'intelligence artificielle.

Observe -> Oriente -> Décide -> Agit -> Observe -> ...

Ce cycle est par nature cyclique. L'agent boucle jusqu'à ce qu'il atteigne son objectif ou décide d'abandonner. Les DAGs ne peuvent pas modéliser ce comportement nativement.

Les contournements en LangChain

En pratique, les développeurs LangChain contournent cette limitation avec des boucles manuelles :

# Pattern "while loop" — fragile et difficile à maintenir
max_iterations = 10
for i in range(max_iterations):
    thought = agent.think(task)
    action = agent.act(thought)
    observation = agent.observe(action)

    if observation.is_complete():
        break

    task = observation.feedback  # Réinjecte le feedback

Ce code fonctionne, mais il présente plusieurs problèmes :

  • L'état est géré manuellement : les variables thought, action, observation sont dispersées, difficiles à tracer.
  • Pas de persistance : si le programme crash à l'itération 7, tout est perdu.
  • Pas d'observabilité : difficile de comprendre pourquoi l'agent a pris une décision à l'itération 5.
  • Pas de human-in-the-loop : impossible d'interrompre pour demander une validation humaine.
  • Difficile à paralléliser : les boucles for sont séquentielles par nature.

Pour une comparaison détaillée entre les deux approches, consultez notre article LangChain vs LangGraph : Quelles Différences et Quand Passer à l'Autre ?.

Qu'est-ce qu'un Workflow Cyclique ?

Définition

Un workflow cyclique est un graphe d'exécution où les nœuds peuvent être visités plusieurs fois, dans un ordre déterminé dynamiquement par des conditions. Contrairement au DAG, le graphe peut contenir des cycles — des chemins qui ramènent à un nœud précédemment visité.

Analogie : la conversation vs le formulaire

  • Workflow linéaire (DAG) : un formulaire administratif. Vous remplissez les champs dans l'ordre, vous ne revenez pas en arrière. Simple mais rigide.
  • Workflow cyclique : une conversation avec un expert. Vous posez une question, il répond, vous reformulez, il affine, vous validez, il conclut. Flexible et adaptatif.

Représentation graphique

Un workflow cyclique typique pour un agent autonome :

[DÉBUT] -> [Raisonner] -> [Agir] -> [Observer] -> [Évaluer]
                ^                                   |
                |                                   v
                +-----------------------------------+
                            (si objectif non atteint)

[DÉBUT] -> [Raisonner] -> [Agir] -> [Observer] -> [Évaluer] -> [FIN]
                                                  (si objectif atteint)

Ce cycle raisonner-agir-observer-évaluer est le pattern fondamental des agents autonomes. LangGraph le modélise nativement.

L'Approche de LangGraph : Graphes d'État

Le StateGraph

LangGraph modélise les workflows cycliques via des graphes d'état. Un StateGraph est un graphe où :

  1. L'état est explicite et typé : un dictionnaire (TypedDict ou Pydantic) qui circule entre les nœuds et est mis à jour à chaque étape.
  2. Les nœuds sont des fonctions pures : chaque nœud reçoit l'état, le transformme et retourne les mises à jour.
  3. Les arêtes sont conditionnelles : les transitions entre nœuds dépendent de l'état courant.
  4. Les checkpoints assurent la persistance : l'état est sauvegardé à chaque étape, permettant la reprise et le time-travel debugging.

Exemple : Agent ReAct avec LangGraph

from typing import TypedDict, Annotated, List
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

# Définition de l'état
class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    iteration: int
    task_complete: bool

# Définition d'un outil
@tool
def search_web(query: str) -> str:
    """Recherche sur le web."""
    # Implémentation réelle avec Tavily, SerpAPI, etc.
    return f"Résultats de recherche pour : {query}"

@tool
def calculate(expression: str) -> str:
    """Évalue une expression mathématique."""
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Erreur : {e}"

# Outils disponibles
tools = [search_web, calculate]
llm = ChatOpenAI(model="gpt-4o").bind_tools(tools)

# Nœud 1 : L'agent raisonne et décide d'une action
def reason(state: AgentState) -> dict:
    response = llm.invoke(state["messages"])
    return {
        "messages": [response],
        "iteration": state["iteration"] + 1
    }

# Nœud 2 : Exécuter les outils demandés
def execute_tools(state: AgentState) -> dict:
    last_message = state["messages"][-1]
    tool_calls = last_message.tool_calls

    results = []
    for tool_call in tool_calls:
        tool_name = tool_call["name"]
        tool_args = tool_call["args"]

        for t in tools:
            if t.name == tool_name:
                result = t.invoke(tool_args)
                results.append({
                    "role": "tool",
                    "content": str(result),
                    "tool_call_id": tool_call["id"]
                })

    return {"messages": results}

# Arête conditionnelle : continuer ou terminer
def should_continue(state: AgentState) -> str:
    last_message = state["messages"][-1]

    # Si l'agent n'appelle plus d'outils, il a terminé
    if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
        return END

    # Limite de sécurité
    if state["iteration"] >= 15:
        return END

    return "execute_tools"

# Construction du graphe
graph = StateGraph(AgentState)
graph.add_node("reason", reason)
graph.add_node("execute_tools", execute_tools)

graph.add_edge(START, "reason")
graph.add_conditional_edges("reason", should_continue, {
    "execute_tools": "execute_tools",
    END: END
})
graph.add_edge("execute_tools", "reason")

# Compilation avec persistance
checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

# Exécution
config = {"configurable": {"thread_id": "agent-1"}}
result = app.invoke(
    {"messages": [{"role": "user", "content": "Combien font les ventes de Q1 multipliées par 1.15 ?"}]},
    config
)

print(result["messages"][-1].content)

Ce qui se passe sous le capot

  1. L'utilisateur envoie une question.
  2. Le nœud reason appelle le LLM, qui décide d'appeler un outil (par exemple, search_web pour trouver les ventes Q1).
  3. L'arête conditionnelle should_continue détecte qu'il y a des appels d'outils et redirige vers execute_tools.
  4. Le nœud execute_tools exécute les outils et renvoie les résultats.
  5. L'arête inconditionnelle renvoie vers reason (cycle !).
  6. Le nœud reason reçoit les résultats des outils et décide soit d'appeler un autre outil, soit de répondre.
  7. Quand le LLM ne demande plus d'outils, should_continue renvoie END.

À chaque étape, le checkpoint sauvegarde l'état complet. Vous pouvez inspecter, reprendre ou annuler n'importe quelle étape.

Pourquoi les Workflows Cycliques Changent la Donne

1. L'autonomie devient réelle

Avec les chaînes linéaires, l'autonomie d'un agent est simulée — elle repose sur des boucles manuelles. Avec LangGraph, l'autonomie est native. L'agent boucle naturellement jusqu'à ce qu'il atteigne son objectif, sans intervention du développeur.

2. Le debugging devient possible

Les checkpoints de LangGraph permettent le "time-travel debugging" — la capacité de rejouer l'exécution à partir de n'importe quel point. Vous pouvez voir exactement quel était l'état à l'itération 3, pourquoi l'agent a pris une décision, et ce qui se serait passé si une condition avait été différente.

# Récupérer l'état à chaque étape
states = app.get_state_history(config)
for state in states:
    print(f"Étape {state.values['iteration']} : {state.values['messages'][-1].content[:80]}...")

3. Le human-in-the-loop devient trivial

Les workflows cycliques permettent d'interrompre l'exécution à n'importe quel point pour demander une validation humaine, puis de reprendre automatiquement.

from langgraph.checkpoint import MemorySaver

def human_review(state: AgentState) -> dict:
    """Nœud qui demande une validation humaine."""
    last_action = state["messages"][-1].content
    approval = input(f"Approuver cette action ? (oui/non) : {last_action[:100]}...")

    if approval.lower() == "oui":
        return {"task_complete": True}
    return {"task_complete": False, "messages": [
        {"role": "user", "content": "Action non approuvée. Réessaie avec une approche différente."}
    ]}

# Ajout du nœud de validation humaine
graph.add_node("human_review", human_review)
graph.add_edge("execute_tools", "human_review")
graph.add_conditional_edges("human_review", lambda s: "reason" if not s["task_complete"] else END)

4. La persistance garantit la robustesse

En production, un agent peut tourner pendant des minutes ou des heures. Sans persistance, un crash signifie une perte totale de progression. Avec les checkpoints LangGraph, l'état est sauvegardé à chaque étape et peut être repris après n'importe quel incident.

5. La composition permet la complexité

Les graphes LangGraph peuvent être composés — un nœud peut être lui-même un graphe. Cela permet de construire des systèmes hiérarchiques où un superviseur orchestre des sous-graphes spécialisés.

# Un sous-graphe de recherche
research_graph = StateGraph(ResearchState)
research_graph.add_node("search", search_node)
research_graph.add_node("synthesize", synthesize_node)
research_graph.add_edge(START, "search")
research_graph.add_edge("search", "synthesize")
research_graph.add_edge("synthesize", END)

research_app = research_graph.compile()

# Le graphe principal utilise le sous-graphe comme nœud
main_graph = StateGraph(MainState)
main_graph.add_node("supervisor", supervisor_node)
main_graph.add_node("research", research_app)  # Sous-graphe comme nœud
main_graph.add_node("write", write_node)

Cas d'Usage Concrets

1. Agent de Recherche Autonome

Un agent qui cherche, synthétise et vérifie des informations en bouclant jusqu'à obtenir un résultat fiable.

class ResearchState(TypedDict):
    query: str
    findings: List[str]
    confidence: float
    messages: Annotated[list, add_messages]

def search(state: ResearchState) -> dict:
    # Recherche web
    new_findings = search_engine(state["query"])
    return {"findings": state["findings"] + new_findings}

def synthesize(state: ResearchState) -> dict:
    # Synthèse des résultats
    summary = llm.invoke(f"Synthèse de : {state['findings']}")
    return {"messages": [summary]}

def verify(state: ResearchState) -> dict:
    # Vérification des faits
    confidence = fact_checker(state["messages"][-1].content)
    return {"confidence": confidence}

def needs_more_research(state: ResearchState) -> str:
    if state["confidence"] < 0.8:
        return "search"  # Boucle : chercher plus
    return END

# Graphe cyclique : recherche -> synthèse -> vérification -> (recherche ou fin)
graph = StateGraph(ResearchState)
graph.add_node("search", search)
graph.add_node("synthesize", synthesize)
graph.add_node("verify", verify)

graph.add_edge(START, "search")
graph.add_edge("search", "synthesize")
graph.add_edge("synthesize", "verify")
graph.add_conditional_edges("verify", needs_more_research)

2. Agent de Code Autonome

Un agent qui écrit, exécute, débogue et améliore du code en bouclant jusqu'à ce que les tests passent.

class CodingState(TypedDict):
    requirements: str
    code: str
    test_results: str
    iteration: int
    messages: Annotated[list, add_messages]

def write_code(state: CodingState) -> dict:
    code = coder_llm.invoke(
        f"Écris du code Python pour : {state['requirements']}\n"
        f"Code précédent (si correction) : {state.get('code', 'aucun')}\n"
        f"Résultat des tests : {state.get('test_results', 'aucun')}"
    )
    return {"code": code.content, "iteration": state["iteration"] + 1}

def run_tests(state: CodingState) -> dict:
    # Exécution des tests en sandbox
    result = sandbox.execute(state["code"])
    return {"test_results": result.output}

def should_fix(state: CodingState) -> str:
    if "PASS" in state["test_results"]:
        return END
    if state["iteration"] >= 5:
        return END
    return "write_code"  # Boucle : corriger le code

graph = StateGraph(CodingState)
graph.add_node("write_code", write_code)
graph.add_node("run_tests", run_tests)
graph.add_edge(START, "write_code")
graph.add_edge("write_code", "run_tests")
graph.add_conditional_edges("run_tests", should_fix)

3. Agent de Service Client avec Escalade

Un agent qui répond aux questions, et si la confiance est faible, boucle vers un agent spécialisé ou un humain.

class SupportState(TypedDict):
    customer_query: str
    response: str
    confidence: float
    escalated: bool
    messages: Annotated[list, add_messages]

def answer(state: SupportState) -> dict:
    response = support_llm.invoke(state["messages"])
    return {"response": response.content}

def evaluate_confidence(state: SupportState) -> dict:
    score = confidence_evaluator(state["response"])
    return {"confidence": score, "escalated": score < 0.6}

def route(state: SupportState) -> str:
    if state["escalated"]:
        return "specialist"  # Transfert à un agent spécialisé
    return END

graph = StateGraph(SupportState)
graph.add_node("answer", answer)
graph.add_node("evaluate", evaluate_confidence)
graph.add_node("specialist", specialist_node)

graph.add_edge(START, "answer")
graph.add_edge("answer", "evaluate")
graph.add_conditional_edges("evaluate", route)

Comparaison : Chaînes Linéaires vs Workflows Cycliques

CritèreChaînes linéaires (LangChain)Workflows cycliques (LangGraph)
Flux d'exécutionSéquentiel, prévisibleCyclique, adaptatif
Autonomie de l'agentLimitée, simuléeRéelle, native
Raisonnement itératifVia boucles manuellesVia arêtes conditionnelles
PersistanceNon nativeCheckpoints intégrés
Human-in-the-loopComplexe à implémenterNatif (interruptions)
DebuggingLogging manuelTime-travel debugging
Reprise après crashImpossible sans code customAutomatique via checkpoints
CompositionChaînes de chaînesGraphes de graphes
ParallélismeRunnableParallelNœuds parallèles natifs
Complexité initialeFaibleModérée
Adapté aux agentsPartiellementTotalement

Patterns de Workflows Cycliques

Pattern ReAct (Reason-Act)

Le pattern le plus courant : l'agent raisonne, agit, observe et boucle.

[Raisonner] -> [Agir] -> [Observer] -> (objectif atteint ?)
     ^                                      |
     |                                      v
     +---------- (non) ---------------------+

Pattern Rédacteur-Critique

Un agent rédige, un autre critique, et le cycle continue jusqu'à validation.

[Rédiger] -> [Critiquer] -> (approuvé ?)
     ^                           |
     |                           v
     +-------- (non) ------------+

Pattern Superviseur

Un agent superviseur délègue des tâches à des agents spécialisés et boucle jusqu'à complétion.

[Superviseur] -> [Agent A] -> [Superviseur] -> [Agent B] -> [Superviseur] -> (complété ?)

Pour d'autres approches d'orchestration, consultez notre comparatif CrewAI vs AutoGen.

FAQ

Pourquoi ne pas simplement utiliser une boucle while en Python ?

Vous pouvez, mais vous perdez tous les avantages de LangGraph : persistance, observabilité, human-in-the-loop, composition et reprise après crash. Une boucle while gère l'état manuellement, sans sauvegarde automatique, sans traçabilité et sans capacité de reprendre après un incident. LangGraph industrialise ce pattern.

Les workflows cycliques ne risquent-ils pas de boucler indéfiniment ?

C'est un risque réel, mais LangGraph offre plusieurs garde-fous : le paramètre max_round, les arêtes conditionnelles avec END, et les limites d'itération dans l'état. En production, imposez toujours un maximum d'itérations et un timeout.

LangGraph peut-il fonctionner sans LangChain ?

Techniquement oui, mais en pratique, les deux sont conçus pour fonctionner ensemble. LangGraph s'appuie sur les primitives LangChain (modèles, outils, parsers) pour les briques individuelles, et ajoute l'orchestration cyclique par-dessus. Utiliser LangGraph sans LangChain nécessite de réimplémenter les intégrations.

Quels sont les coûts d'un workflow cyclique par rapport à une chaîne linéaire ?

Un workflow cyclique consomme potentiellement plus de tokens car l'agent peut boucler plusieurs fois. Cependant, il produit généralement de meilleurs résultats car il itère jusqu'à la qualité voulue. Pour contrôler les coûts, fixez des limites d'itération et utilisez des modèles légers pour les itérations intermédiaires.

Les workflows cycliques sont-ils adaptés à toutes les tâches ?

Non. Pour les tâches purement séquentielles et prévisibles (ex. : RAG simple, extraction, traduction), les chaînes linéaires sont plus efficaces et plus simples. Les workflows cycliques sont justifiés dès que l'agent doit itérer, s'adapter ou valider ses résultats. Ne surdimensionnez pas votre architecture.

Mise à jour — Avril 2026

Les workflows cycliques avec LangGraph continuent de mûrir :

  • LangGraph Platform : le déploiement managé de graphes LangGraph est disponible en version stable. Il offre un serveur d'API, un dashboard de monitoring et des capacités de scaling pour les workflows cycliques en production.
  • LangGraph Studio : l'IDE visuel permet de concevoir des graphes cycliques en glisser-déposer, de les exécuter pas à pas et d'inspecter l'état à chaque nœud — y compris pendant les boucles.
  • Optimisation des performances : les checkpoints sont désormais incrémentaux (seuls les deltas sont sauvegardés), réduisant la surcharge mémoire et disque de 60 à 80%.
  • Support des sub-graphs : la composition de graphes (un graphe comme nœud d'un autre graphe) est pleinement supportée, permettant des architectures hiérarchiques complexes.
  • Intégration avec d'autres frameworks : LangGraph s'intègre désormais plus facilement avec CrewAI (pour les rôles structurés) et Semantic Kernel (pour l'écosystème .NET). Pour les développeurs .NET, consultez notre guide Semantic Kernel pour .NET.
  • Standards émergents : le protocole A2A (Agent-to-Agent) de Google et la spécification OpenTelemetry pour les agents normalisent l'observabilité des workflows cycliques.

Les workflows cycliques ne sont pas un gadget technique — ils répondent à un besoin fondamental des agents autonomes : la capacité d'itérer, d'adapter et de persister. En modélisant explicitement les boucles, l'état et les conditions de sortie, LangGraph transforme les agents IA de scripts séquentiels en systèmes véritablement autonomes. Si vous construisez des agents qui doivent raisonner et agir de manière itérative, les workflows cycliques ne sont pas une option — c'est le bon modèle.