Engineering the Agentic Stack · Partie 1

Boucles de raisonnement des AI agents : ReAct, ReWOO, Plan-and-Execute

Traduction automatique Cet article a été traduit automatiquement depuis la version originale en anglais.

Une boucle de raisonnement d’agent est le flux de contrôle qui détermine quand un modèle planifie, appelle un outil, lit le résultat et s’arrête. Pour l’ingénieur qui construit un agent, ce choix représente aussi un budget : il détermine la fréquence d’exécution du modèle, la quantité d’historique transportée par chaque appel et la possibilité pour un résultat surprenant de modifier l’action suivante.

Cet article compare ReAct, ReWOO et Plan-and-Execute à travers un Market Analyst Agent basé sur LangGraph que j’ai développé. Vous repartirez avec une règle de routage et des structures d’implémentation à adapter, plutôt qu’avec trois noms à ajouter à un diagramme.

La boucle constitue la couche la plus interne de cette série. La mémoire, les outils, la sécurité, le runtime et les contrôles d’acceptation l’entourent ; ils ne la remplacent pas.

Pour une comparaison succincte des frameworks, consultez Best AI Agent Frameworks in 2026.

La boucle de raisonnement détermine quoi faire ensuite. Elle ne stocke pas l’état, n’exécute pas les outils et n’autorise pas les effets de bord.

Le harness est le programme de contrôle situé entre le modèle et la machine. Il assemble les prompts à partir de l’état stocké (Partie 2), définit les actions que le modèle peut nommer (Partie 3), autorise les appels (Partie 4) et vérifie les preuves avant de déclarer une tâche terminée (Partie 6). Ce sont des problèmes d’ingénierie distincts, mais un tour passe par ces quatre éléments.

Le runtime (Partie 5) fournit le journal de session, le sandbox, le checkpoint store et les traces qui survivent à un processus worker.

Position de chaque partie de la série Engineering the Agentic StackPosition de chaque partie de la série Engineering the Agentic Stack

Chaque article se suffit à lui-même. Ensemble, ils partent de la boucle pour aller vers les couches extérieures.


Commencer par la frontière d’échec

Un bon prompt ne suffit pas à trancher la question du flux de contrôle. Les patterns diffèrent par la quantité de travail fixée avant le premier tool call. Cela détermine le nombre d’appels au modèle, le moment où un mauvais plan devient visible et la possibilité pour un résultat d’outil inattendu de réorienter l’exécution.

Trois patterns de raisonnement pour les AI agents

Comparaison de ReAct, ReWOO et Plan-and-Execute selon le moment où les preuves peuvent modifier le planComparaison de ReAct, ReWOO et Plan-and-Execute selon le moment où les preuves peuvent modifier le plan

ReAct : décider après chaque observation

ReAct (Yao et al., 2022), abréviation de Reason + Act, garde la décision suivante au plus près de l’observation la plus récente :

  1. Thought : l’agent génère un « thought » pour décomposer l’objectif et planifier l’étape suivante.
  2. Action : à partir du thought, il appelle un outil.
  3. Observation : l’agent lit le résultat, ce qui met à jour sa compréhension pour le thought suivant.

ReAct lit chaque résultat d’outil avant de choisir l’action suivanteReAct lit chaque résultat d’outil avant de choisir l’action suivante

Cette boucle confère à ReAct ses propriétés utiles :

  • Dans l’exemple manuel PaLM-540B de l’article sur HotpotQA, les observations issues de Wikipédia ont produit moins de faits hallucinés qu’un prompting chain-of-thought.
  • L’agent peut modifier sa stratégie à la volée en fonction de ce qu’il vient d’observer.
  • L’historique des tool calls et des observations fournit une trace d’exécution concrète.

La même boucle a aussi des coûts :

  • Dans une implémentation naïve avec historique complet, l’historique est retraité à chaque étape ; la latence et le coût augmentent donc avec la longueur de la boucle. La summarization ou la troncature peuvent plafonner cette croissance, au prix d’une perte de contexte.
  • Elle est inefficace lorsque les tool calls auraient pu être planifiés à l’avance, ce qui constitue précisément le créneau de ReWOO.
  • Sans condition d’arrêt ni limite d’étapes, la boucle peut s’exécuter indéfiniment.

Utilisez-le pour les tâches exploratoires, le debugging et les travaux dont vous ne pouvez pas prévoir l’action suivante.

ReWOO : compiler d’abord le graphe d’outils

ReWOO (Reasoning WithOut Observation) sépare la planification de l’exécution. Le planner écrit la séquence complète d’outils en un seul passage, en utilisant des placeholders pour les valeurs qui n’existent qu’après l’exécution.

  1. Plan : un appel LLM écrit le plan complet des tool calls, en utilisant des placeholders de variables (#E1, #E2) pour les sorties qui n’existent pas encore.
  2. Worker : un exécuteur sans LLM exécute les outils planifiés et renseigne les placeholders. Le worker de l’article suit le plan ; l’implémentation présentée plus loin ajoute des batches parallèles tenant compte des dépendances pour les étapes prêtes.
  3. Solver : un appel LLM final reçoit les observations collectées et rédige la réponse.

ReWOO planifie le graphe de dépendances avant l’exécution des outilsReWOO planifie le graphe de dépendances avant l’exécution des outils

Cette séparation apporte plusieurs avantages :

  • Moins d’appels répétés au modèle que ReAct lorsque le plan initial reste valide.
  • Moins de répétition de l’historique dans le prompt qu’avec une boucle entrelacée à historique complet. La latence des outils dépend toujours de la façon dont le worker planifie les appels.
  • Le planner peut être fine-tuné séparément, sans environnement live.

Elle crée aussi une frontière stricte. Dans le stress test HotpotQA de l’article, chaque outil a renvoyé No evidence found ; ReWOO a perdu moins de précision que ReAct, car les observations en échec n’ont pas entraîné son planner dans une nouvelle boucle. Il s’agit d’une robustesse relative, et non d’une politique de récupération à l’exécution. Une implémentation doit tout de même décider si une erreur d’outil devient une preuve pour le solver, déclenche un retry ou interrompt l’exécution. ReWOO convient aux workflows prévisibles ; il ne replanifie pas de lui-même autour d’un mauvais graphe initial.

Utilisez-le pour les instantanés rapides, les vérifications d’état et les dashboards dont le comportement des outils est prévisible.

Plan-and-Execute : décomposer, puis réagir localement

Plan-and-Solve prompting décrit une méthode de prompting qui crée d’abord un plan, puis résout les sous-tâches. Un pattern d’orchestration d’outils associé est couramment appelé Plan-and-Execute. Le guide Plan-and-Execute de LangChain documente ce pattern :

  1. Phase de planification : l’agent génère d’abord un plan qui décompose la tâche en sous-tâches plus petites.
  2. Phase d’exécution : l’agent réalise ensuite ces sous-tâches une par une. Dès que des outils interviennent, chaque sous-tâche s’exécute généralement comme une petite boucle ReAct autonome ; l’exécuteur peut donc toujours réagir au résultat renvoyé par un outil, même si le plan global est fixe.

L’article original se concentrait sur le prompting zero-shot. Dans une implémentation utilisant des outils, le pattern d’orchestration peut exécuter les étapes planifiées séquentiellement et utiliser des modèles différents pour la planification et l’exécution. Cette séparation des modèles relève d’un choix d’implémentation, et non d’un résultat établi par l’article Plan-and-Solve.

Plan-and-Execute conserve un plan global tandis que chaque étape utilise le feedbackPlan-and-Execute conserve un plan global tandis que chaque étape utilise le feedback

Ce pattern est utile parce qu’il fournit :

  • Un raisonnement hiérarchique qui reflète la manière dont un expert humain décompose un projet.
  • Une arête de replanification explicite permettant de mettre l’exécution en pause et de réévaluer la situation après le résultat inattendu d’une étape.
  • Une spécialisation des modèles. Le planner peut être coûteux, tandis que l’exécuteur peut être économique.
  • Avec un checkpointer configuré, chaque étape terminée peut devenir un point de reprise.

Ses coûts sont les suivants :

  • Davantage d’allers-retours avec le modèle que ReWOO lorsque chaque étape contient sa propre boucle ReAct.
  • Davantage d’état à gérer.
  • Une complexité excessive pour les requêtes one-shot.

Utilisez-le pour les analyses complexes et les travaux de recherche nécessitant une synthèse finale.

Choisir selon l’endroit où le plan peut échouer

FonctionnalitéReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Philosophie centraleImprovisateur : décider de l’action suivante à partir du dernier résultat, un appel à la fois.Architecte : construire un blueprint complet, l’exécuter, puis le réexaminer.Optimiseur : compiler un graphe de dépendances, puis regrouper les appels prêts.
WorkflowBoucle itérative : Thought → Action → Observation.En deux étapes : Phase 1 (Planning), Phase 2 (Execution).Découplé : le Planner écrit un graphe de tool calls ; le Worker les exécute ; le Solver compose la réponse.
AdaptabilitéMaximale : peut changer de direction après chaque tool call.Moyenne : replanifie généralement seulement après la réalisation d’un ensemble d’étapes.Minimale : le script du planner s’exécute jusqu’au bout ; rien ne replanifie en cours d’exécution.
EfficacitéUne boucle naïve à historique complet répète davantage de tokens d’entrée ; la gestion du contexte peut plafonner cette croissance.La replanification est occasionnelle plutôt qu’effectuée à chaque étape, et la boucle ReAct de chaque étape peut démarrer avec un contexte court au lieu de l’historique complet de l’exécution.Moins d’appels au modèle ; le worker de cet article regroupe également les outils dont les dépendances sont satisfaites.
Idéal pourL’exploration ouverte ou les tâches dont les résultats sont imprévisibles.Les tâches à long horizon nécessitant un objectif stable (par ex. rédiger un article).Les workflows structurés et répétables (par ex. vérifier la météo dans cinq villes).

Ce tableau sert d’aide au routage, pas de benchmark. Utilisez ReAct lorsqu’une observation peut modifier l’action suivante. Utilisez Plan-and-Execute lorsque la tâche se décompose proprement, mais que chaque étape nécessite encore un feedback. Utilisez ReWOO lorsque toutes les dépendances entre outils sont connues avant l’exécution. Dans l’implémentation pédagogique ci-dessous, ce graphe de dépendances permet des batches parallèles et permet au worker de détecter un graphe qui ne peut plus progresser. Son helper execute_tool est volontairement fail-fast ; le code de production doit définir explicitement une politique de retry, de fallback ou de traitement de l’erreur comme preuve. Mesurez les trois patterns avec votre modèle, la latence des outils, votre jeu de tâches et votre politique de retry avant d’optimiser le nombre d’appels.

Exemple concret : le Market Analyst Agent

Le Market Analyst Agent rend la distinction concrète. Une même codebase utilise les trois patterns pour la recherche de marché, et un router choisit entre un parcours de recherche approfondie et un parcours de briefing rapide. Les extraits ci-dessous sont des variantes pédagogiques abrégées de commit b4e769a : le companion pinned intercepte une exception d’outil et transmet une chaîne d’erreur à son solver, tandis que le worker présenté ici laisse cette exception interrompre l’exécution et lève une erreur lorsque son graphe de dépendances ne peut plus progresser. Le routage et la forme de l’état sont identiques ; la politique d’échec est volontairement explicite ici.

Il utilise LangGraph pour l’orchestration. Un node est une fonction Python qui renvoie les champs à mettre à jour dans l’état partagé. Une edge déclare le node suivant et peut appeler une fonction de routage. LangGraph fusionne les mises à jour et peut créer un checkpoint après chaque node, ce qui rend une exécution reprenable. Les trois patterns partagent un même objet d’état ; le routage ne nécessite donc pas trois schémas distincts :

Le Market Analyst Agent dirige une requête vers l’une de deux boucles de raisonnement avec un état partagéLe Market Analyst Agent dirige une requête vers l’une de deux boucles de raisonnement avec un état partagé

Le diagramme isole le routage et la création du draft. Il omet l’évaluateur partagé et la publish gate présentés plus loin afin que les deux boucles de raisonnement restent lisibles.

Définition de l’état

Le schéma d’état contient les champs nécessaires aux deux modes :

class PlanStep(BaseModel):
    """A single step in the research plan."""
    step_number: int
    description: str
    tool_hint: str | None = None
    completed: bool = False
    result: str | None = None

class UserProfile(BaseModel):
    """Structured user context loaded from long-term memory."""
    risk_tolerance: str | None = None
    investment_horizon: str | None = None

class AgentState(BaseModel):
    """Main state for the Market Analyst Agent graph."""

    # Identity and profile context for memory-backed personalization
    user_id: str
    user_profile: UserProfile = Field(default_factory=UserProfile)

    # Message history with LangGraph's add_messages reducer
    messages: Annotated[list, add_messages] = Field(default_factory=list)

    # Execution mode (set by router)
    execution_mode: ExecutionMode | None = None

    # Plan-and-Execute state
    plan: list[PlanStep] = Field(default_factory=list)
    current_step_index: int = 0

    # ReWOO state
    rewoo_plan: list[ReWOOPlanStep] = Field(default_factory=list)

    # Research results
    research_data: ResearchData | None = None

    # Final report. Both paths write this field, then the graph pauses before
    # publishing (see interrupt_before below), so a human signs off on a draft
    # a fresh-context evaluator has already voted on.
    draft_report: DraftReport | None = None

Pattern 1 : implémentation de Plan-and-Execute

Plan-and-Execute convient à la synthèse multi-étapes. Un planner rédige les étapes de haut niveau, puis une boucle ReAct exécute chaque étape et réagit aux résultats des outils.

Le code ci-dessous épingle claude-sonnet-4-5-20250929, le modèle utilisé pour ces exemples lors de la publication de cet article, en janvier 2026. Les générations de modèles ont évolué depuis. Remplacez-le par le modèle actuel et revérifiez le comportement du routage sur vos propres tâches — le pattern compte davantage que l’identifiant du modèle.

L’implémentation rend visibles quatre frontières :

  1. Une phase de planification initiale unique. Un seul appel LLM produit le plan complet sous forme de liste de descriptions d’étapes.
  2. Une sortie guidée par schéma, qui valide le plan avant l’exécution.
  3. Aucune exécution d’outil à ce stade. Le planner décide uniquement quoi faire, pas comment.
  4. Des étapes lisibles par un humain. Chaque étape est un texte qu’un exécuteur interprétera.
# System prompt guides the LLM to think like a research analyst
# creating a strategic plan, not immediate tool calls
PLANNER_SYSTEM_PROMPT = """You are a senior investment research analyst.
Break down stock analysis requests into 4-6 research steps covering:
1. Current price and basic metrics
2. Recent news and announcements
3. Competitor analysis (if relevant)
4. Financial health assessment
5. Risk factors
6. Investment thesis synthesis

Output as JSON with step_number, description, and tool_hint."""

# Schema-Guided Reasoning: Enforce structure with Pydantic
class PlanOutput(BaseModel):
    """Structured output for the planner."""

    steps: list[PlanStep] = Field(description="Research steps to execute")
    ticker: str = Field(description="The stock ticker being analyzed")

def planner_node(state: AgentState) -> dict:
    """Generate a research plan from the user's request.

    This is Phase 1 of Plan-and-Execute: creating the high-level strategy.
    """

    # Use a powerful model for strategic planning
    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)

    # Ask for a typed plan and validate it before execution.
    # The API can still fail, so production code also handles that exception.
    structured_llm = llm.with_structured_output(PlanOutput)

    # Pull the request out of the message history
    human = [m for m in state.messages if isinstance(m, HumanMessage)]
    last_user_message = human[-1].content if human else "Analyze the market"

    # Context from long-term memory personalizes the plan
    profile_context = f"""
User Profile:
- Risk Tolerance: {state.user_profile.risk_tolerance}
- Investment Horizon: {state.user_profile.investment_horizon}
"""

    # Single LLM call creates the complete plan
    result: PlanOutput = structured_llm.invoke([
        SystemMessage(content=PLANNER_SYSTEM_PROMPT + profile_context),
        HumanMessage(content=f"Create a research plan for: {last_user_message}"),
    ])

    # State update: Store the plan and initialize tracking
    return {
        "plan": result.steps,           # The sequential steps to execute
        "current_step_index": 0,        # Start at step 0
        "research_data": ResearchData(ticker=result.ticker),  # Initialize data container
    }

La ligne llm.with_structured_output(PlanOutput) correspond au Schema-Guided Reasoning (SGR), que j’ai présenté dans un article précédent. Le schéma permet à l’application de rejeter un plan mal formé avant que LangGraph utilise les champs validés pour piloter les arêtes conditionnelles.

Pattern 2 : exécution ReAct

Une fois le plan créé, l’exécuteur traite chaque étape comme sa propre boucle ReAct. Il s’agit de la Phase 2 : chaque étape est suffisamment petite pour qu’un cycle Thought-Action-Observation reste ciblé, et l’agent peut réagir à ce que renvoie l’outil.

Correspondance avec la partie ReAct :

  1. Exécution itérative. Une étape à la fois, avec le feedback des observations.
  2. La boucle Thought-Action-Observation s’exécute à l’intérieur de create_react_agent.
  3. Les résultats des étapes précédentes sont injectés dans le contexte du raisonnement courant.
  4. L’agent choisit les outils à partir de la description de l’étape.
  5. Il peut modifier son approche en cours d’étape selon le résultat renvoyé par un outil.
# The five market-data tools the ReAct agent chooses from here. The repo's
# TOOLS list carries four more — a skill loader, two CLI wrappers, and a
# restricted in-process Python evaluator — covering three of the five tool
# modalities Part 3 compares. MCP is the fourth, and it lives in a sidecar
# rather than in this list.
TOOLS = [
    get_stock_snapshot,
    get_price_history,
    search_news,
    search_competitors,
    get_financials,
]

def executor_node(state: AgentState) -> dict:
    """Execute the current step using a ReAct agent.

    This is Phase 2 of Plan-and-Execute: adaptive execution of each planned step.
    Each step runs as a mini ReAct loop until completion.
    """

    # Get the current step from the plan
    current_step = state.plan[state.current_step_index]

    # Build context from what we've learned so far
    # This matters: each step builds on previous observations
    previous_context = ""
    for step in state.plan[:state.current_step_index]:
        if step.result:
            previous_context += f"\nStep {step.step_number}: {step.result}\n"

    # Create a ReAct agent for this step
    # This companion example pins LangGraph's deprecated create_react_agent API.
    # Current LangChain guidance recommends create_agent instead:
    # https://reference.langchain.com/python/langgraph.prebuilt/chat_agent_executor/create_react_agent
    # The factory name does not change the Thought-Action-Observation loop:
    # 1. Agent generates a "thought" about what tool to call
    # 2. Agent calls the tool ("action")
    # 3. Tool returns result ("observation")
    # 4. Agent decides: call another tool or finish
    react_agent = create_react_agent(
        model=ChatAnthropic(model="claude-sonnet-4-5-20250929"),
        tools=TOOLS,
    )

    # Invoke the ReAct loop for this single step
    # The agent will loop internally until it completes the step
    result = react_agent.invoke({
        "messages": [
            SystemMessage(content=EXECUTOR_SYSTEM_PROMPT),
            HumanMessage(content=f"""Execute Step {current_step.step_number}:
{current_step.description}

Ticker: {state.research_data.ticker}
Previous findings: {previous_context}"""),
        ]
    })

    # Extract the final answer from the ReAct agent's message history
    # The last message contains the synthesis after all tool calls
    updated_plan = list(state.plan)
    updated_plan[state.current_step_index] = PlanStep(
        step_number=current_step.step_number,
        description=current_step.description,
        completed=True,
        result=result["messages"][-1].content,  # Final synthesized answer
    )

    # State update: Mark step complete and advance to next
    return {
        "plan": updated_plan,
        "current_step_index": state.current_step_index + 1,
    }

Pattern 3 : ReWOO pour les instantanés rapides

Pour un briefing rapide, ReWOO supprime les appels au modèle pendant la phase d’exécution. Les outils indépendants s’exécutent en parallèle ; les outils dépendants attendent leurs prérequis. Le planner émet le graphe d’outils en amont, et le worker l’exécute sans demander au modèle quoi faire ensuite.

Sa structure :

  1. Trois phases (Planner → Worker → Solver), sans boucles.
  2. Les tool calls référencent #E1, #E2, des placeholders pour des résultats qui n’existent pas encore.
  3. Aucun LLM pendant l’exécution. Le worker se contente d’exécuter les outils.
  4. Les outils indépendants s’exécutent en parallèle.
  5. Un seul appel de synthèse à la fin, portant sur l’ensemble des données.

Phase 1 : planner ReWOO (crée le graphe d’exécution complet en amont)

class ReWOOPlanStep(BaseModel):
    """A step in the ReWOO plan with variable placeholders.

    Key difference from Plan-and-Execute's PlanStep:
    - Contains actual tool_name and tool_args (not just description)
    - Uses variable references (#E1) for dependencies
    """
    step_id: str  # e.g., "#E1" - becomes a variable
    description: str
    tool_name: str     # Exact tool to call
    tool_args: dict    # May contain variable refs like {"price": "#E1"}
    depends_on: list[str] = []  # For dependency ordering
    result: str | None = None

class ReWOOPlanOutput(BaseModel):
    """Structured output for ReWOO planner."""
    steps: list[ReWOOPlanStep] = Field(description="Planned tool calls with variables")

def rewoo_planner_node(state: AgentState) -> dict:
    """Generate a complete plan of tool calls upfront.

    This is the key difference from Plan-and-Execute: instead of creating
    human-readable step descriptions, we create EXACT tool calls that
    the worker will execute blindly.
    """

    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)

    # Schema-Guided Reasoning ensures valid tool call specifications
    structured_llm = llm.with_structured_output(ReWOOPlanOutput)

    ticker = state.research_data.ticker if state.research_data else "UNKNOWN"
    human = [m for m in state.messages if isinstance(m, HumanMessage)]
    query = human[-1].content if human else f"Analyze {ticker}"

    # Single LLM call to plan ALL tool executions
    result: ReWOOPlanOutput = structured_llm.invoke([
        SystemMessage(content=REWOO_PLANNER_PROMPT),
        HumanMessage(content=f"""Create a ReWOO plan for: {query}

Ticker: {ticker}

Output tool calls with:
- step_id: Variable name (#E1, #E2, etc.)
- description: What this accomplishes
- tool_name: Exact tool from the list
- tool_args: Dictionary of arguments
- depends_on: List of step_ids this depends on"""),
    ])

    # State update: Store the complete execution plan
    # Worker will execute this without any LLM involvement
    return {"rewoo_plan": result.steps}

Phase 2 : worker ReWOO (exécute les outils sans raisonnement LLM)

def rewoo_worker_node(state: AgentState) -> dict:
    """Execute dependency-ready tools in parallel batches (no LLM calls).

    Independent tools share a batch. Dependent tools wait until their
    prerequisites complete. The worker follows the dependency graph and
    does not add LLM calls.
    """

    results = {}        # Results keyed by step_id (e.g., "#E1": "$150.23")
    updated_steps = []  # Plan steps with their result field filled in
    pending = {step.step_id: step for step in state.rewoo_plan}

    # Keep scheduling dependency-ready batches until the graph is complete.
    # This handles chains even when the planner does not list them topologically.
    with ThreadPoolExecutor(max_workers=5) as executor:
        while pending:
            ready = [
                step for step in pending.values()
                if all(dep in results for dep in step.depends_on)
            ]
            if not ready:
                unresolved = ", ".join(pending)
                raise ValueError(f"Unresolvable ReWOO dependencies: {unresolved}")

            futures = {
                executor.submit(execute_tool, step, results): step
                for step in ready
            }
            for future in as_completed(futures):
                step = futures[future]
                results[step.step_id] = future.result()
                updated_steps.append(step.model_copy(update={"result": results[step.step_id]}))
                del pending[step.step_id]

    # State update: restore the planner's order (sorting on step_id would put
    # "#E10" before "#E2") and hand the filled-in plan to the Solver
    plan_order = {s.step_id: i for i, s in enumerate(state.rewoo_plan)}
    return {"rewoo_plan": sorted(updated_steps, key=lambda s: plan_order[s.step_id])}

Phase 3 : solver ReWOO (synthétise tous les résultats en un seul appel LLM)

def rewoo_solver_node(state: AgentState) -> dict:
    """Synthesize all tool results into a flash briefing.

    This is the second efficiency gain: Instead of interleaving
    LLM calls with tool execution (like ReAct), we make ONE
    final synthesis call with all gathered data.
    """

    # Build context from ALL tool results at once
    tool_results = []
    for step in state.rewoo_plan:
        if step.result:
            tool_results.append(f"### {step.description}\n{step.result}")

    context = "\n\n".join(tool_results)

    # Single LLM call to synthesize everything
    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
    structured_llm = llm.with_structured_output(FlashBriefingOutput)
    result = structured_llm.invoke([
        SystemMessage(content=REWOO_SOLVER_PROMPT),
        HumanMessage(content=f"Create a flash briefing from this data:\n\n{context}"),
    ])

    # FlashBriefingOutput and DraftReport carry the same fields; the state
    # schema expects DraftReport, so convert before returning.
    return {"draft_report": DraftReport(**result.model_dump())}

Les trois phases ont un contrat simple : le planner crée des appels exécutables, le worker renseigne leurs placeholders et le solver reçoit les résultats complétés. Un graphe impossible à résoudre lève une erreur avant l’exécution du solver ; une politique de retry ou une failure edge de LangGraph peut alors décider de replanifier. Ce contrat n’est efficace que lorsque les hypothèses du planner résistent au contact des outils.

Où chaque pattern appelle le modèle

PatternAppels LLM pendant l’exécutionMises à jour de l’étatPattern de code clé
Plan-and-Execute1 pour la planification + une boucle ReAct par étape (plusieurs appels chacune) + 1 pour le rapportAchèvement séquentiel des étapesplanner_node() → boucle : executor_node()reporter_node()
ReAct (dans chaque étape)Plusieurs par étape (cycles thought-action)Historique cumulé des messagesLe companion pinned utilise create_react_agent(), désormais deprecated
ReWOO1 pour la planification + 0 pendant l’exécution + 1 pour la synthèseBatches d’outils tenant compte des dépendancesrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

La différence importante réside dans la sortie du planner. Elle détermine la marge de manœuvre conservée par l’exécuteur :

  1. Plan-and-Execute crée des descriptions d’étapes lisibles par un humain :

    # Planner output (list of PlanStep objects)
    plan = [
        PlanStep(
            step_number=1,
            description="Get current price and key financial metrics",
            tool_hint="get_stock_snapshot"
        ),
        PlanStep(
            step_number=2,
            description="Search for recent news and earnings",
            tool_hint="search_news"
        ),
        # ... more steps
    ]

    L’exécuteur lit chaque description et décide quels outils appeler. C’est flexible, mais chaque étape possède sa propre boucle ReAct ; une étape coûte donc plusieurs appels au modèle, et non un seul.

  2. ReAct ne dispose pas de plan initial. Il utilise un raisonnement itératif :

    # No planning phase - ReAct works step-by-step with accumulated messages
    messages = [
        HumanMessage(content="Execute Step 1: Get current price"),
        AIMessage(content="I'll call get_stock_snapshot"),
        ToolMessage(tool_call_id="1", content="$132.45"),
        AIMessage(content="Now I need metrics..."),
        # ... agent continues until step complete
    ]

    Dans cette implémentation à historique complet, chaque appel au modèle relit un historique qui s’allonge pendant toute la tâche. Une boucle de production peut le résumer ou le tronquer. Plan-and-Execute utilise également des boucles ReAct, mais chacune démarre avec la description de l’étape et un digest des résultats précédents, et non avec l’historique complet des tool calls.

  3. ReWOO crée des spécifications explicites et exécutables de tool calls :

    # Planner output (list of ReWOOPlanStep objects)
    rewoo_plan = [
        ReWOOPlanStep(
            step_id="#E1",
            tool_name="get_stock_snapshot",
            tool_args={"ticker": "NVDA"}
        ),
        ReWOOPlanStep(
            step_id="#E2",
            tool_name="search_news",
            tool_args={"query": "NVDA earnings", "limit": 5}
        ),
        # ... all tool calls planned upfront
    ]

    Le worker s’exécute en aveugle, sans intervention d’un LLM. Tous les appels au modèle se trouvent dans le planner et le solver, ce qui rend leur nombre prévisible.

Flux de la mémoire et de l’état :

  • Plan-and-Execute : l’état transite par plancurrent_step_indexresearch_data.
  • ReAct : l’état s’accumule dans le tableau messages (l’historique complet de la conversation).
  • ReWOO : l’état transite par rewoo_plan, dont les champs result sont remplis par le worker.

Relier les deux routes dans un même graphe

Le graphe expose deux routes destinées à l’utilisateur au-dessus d’un même AgentState : la recherche approfondie utilise Plan-and-Execute avec une boucle ReAct à l’intérieur de chaque étape, tandis que le briefing rapide utilise ReWOO. ReAct est ici une primitive d’exécution, et non une troisième route.

Cette implémentation ne possède pas de replanner : elle exécute le plan initial jusqu’au bout. Ajouter une replanification nécessiterait une edge de executor vers planner, ainsi qu’une règle déterminant dans quels cas un résultat surprenant justifie un nouvel appel au modèle.

LangGraph conserve un câblage déclaratif :

def create_graph(checkpointer=None):
    builder = StateGraph(AgentState)

    # Add nodes
    builder.add_node("router", router_node)
    builder.add_node("planner", planner_node)
    builder.add_node("executor", executor_node)
    builder.add_node("reporter", reporter_node)
    builder.add_node("rewoo_planner", rewoo_planner_node)
    builder.add_node("rewoo_worker", rewoo_worker_node)
    builder.add_node("rewoo_solver", rewoo_solver_node)
    builder.add_node("evaluator", evaluator_node)
    builder.add_node("publish", publish_node)

    # Define edges
    builder.add_edge(START, "router")
    builder.add_conditional_edges("router", route_after_router, {
        "planner": "planner",
        "rewoo_planner": "rewoo_planner",
    })

    # Deep Research path
    builder.add_edge("planner", "executor")
    builder.add_conditional_edges("executor", route_after_executor, {
        "executor": "executor",  # Loop back for more steps
        "reporter": "reporter",  # Done with plan
    })
    builder.add_edge("reporter", "evaluator")

    # Flash Briefing path (ReWOO)
    builder.add_edge("rewoo_planner", "rewoo_worker")
    builder.add_edge("rewoo_worker", "rewoo_solver")
    builder.add_edge("rewoo_solver", "evaluator")

    # Both paths converge on the same acceptance check
    builder.add_edge("evaluator", "publish")
    builder.add_edge("publish", END)

    return builder.compile(
        checkpointer=checkpointer,
        # Human-in-the-loop pause: the reporter (or the ReWOO solver) writes a
        # draft, a fresh-context evaluator — a second model session with no
        # history of the run — votes on it, and the graph stops before publish.
        # The human approves a report that already carries an evaluator's
        # verdict rather than adjudicating raw research. The verdict is
        # advisory here — the graph routes to the interrupt either way.
        # Part 6 takes up how an acceptance check decides that a run is done.
        interrupt_before=["publish"],
    )

Sélection automatique du pattern avec un router

Le router associe la forme de la requête à une route. Le Schema-Guided Reasoning contraint la sortie du classifieur :

class ExecutionMode(str, Enum):
    """Execution mode for the agent."""

    DEEP_RESEARCH = "deep_research"  # Plan-and-Execute + ReAct (thorough)
    FLASH_BRIEFING = "flash_briefing"  # ReWOO (fast, token-efficient)

class RouterOutput(BaseModel):
    """Structured output for the router."""

    mode: ExecutionMode  # DEEP_RESEARCH or FLASH_BRIEFING
    ticker: str
    reasoning: str

ROUTER_SYSTEM_PROMPT = """Classify the user's request:

1. **deep_research**: Complex analysis requiring synthesis
   - Examples: "Analyze strategic risks", "investment thesis"

2. **flash_briefing**: Quick snapshots, simple data retrieval
   - Examples: "quick snapshot", "current price"

Default to deep_research if unclear."""

llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
structured_llm = llm.with_structured_output(RouterOutput)

Avec ce router, « current price » est dirigé vers ReWOO et « investment thesis » vers Plan-and-Execute. Par défaut, une requête ambiguë est envoyée vers la recherche approfondie. Avant de placer le router devant les utilisateurs, rejouez un même jeu de tâches sur les deux routes et comparez les appels au modèle, le temps écoulé, les échecs et le comportement de récupération.

L’implémentation complète du companion, avec le router et l’état partagé, se trouve dans le commit épinglé du Market Analyst Agent.

La couche suivante est la mémoire

La Partie 2, AI Agent Memory Architecture, sépare les checkpoints reprenables des connaissances intersessions et des documents de projet. Sans cette couche d’état, le router et l’exécuteur ci-dessus ne fonctionnent que tant qu’un même processus et une même fenêtre de contexte restent actifs.

Références