Boucles de raisonnement des AI agents : ReAct, ReWOO et Plan-and-Execute
Traduction automatique Cet article a été traduit automatiquement depuis la version originale en anglais.
Mise à jour de l’article
Publié à l’origine le 31 janvier 2026. Relu et mis à jour le 6 septembre 2026. Cette mise à jour porte sur les nouvelles capacités des modèles et les reasoning APIs, avec des exemples et des liens vers les sources révisés.
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 constitue 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 inattendu de modifier l’action suivante.
Cet article compare ReAct, ReWOO et Plan-and-Execute à travers un LangGraph Market Analyst Agent que j’ai construit. Vous repartirez avec une règle de routage et des formes d’implémentation adaptables, plutôt qu’avec trois noms à ajouter à un diagramme.
La boucle est la couche la plus interne de cette série. La mémoire, les outils, la sécurité, le runtime et les vérifications effectuées avant de déclarer une tâche terminée l’entourent ; ils ne la remplacent pas.
En bref : choisissez ReAct lorsque chaque tool result peut modifier l’action suivante, ReWOO lorsque le graphe de dépendances est connu avant l’exécution, et Plan-and-Execute lorsqu’une tâche volumineuse peut être décomposée alors que chacune de ses étapes nécessite encore un feedback. Il s’agit de critères de routage, pas d’un classement universel des performances ; mesurez-les avec votre modèle, vos outils et votre politique de retry.
Pour une comparaison succincte des frameworks, consultez Best AI Agent Frameworks in 2026.
La boucle de raisonnement décide de l’action suivante. 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 éléments de preuve avant de déclarer une tâche terminée (Partie 6). Ce sont des problèmes d’ingénierie distincts, mais un tour les traverse tous les quatre.
Le runtime (Partie 5) fournit le journal de session, le sandbox, le checkpoint store et les traces qui persistent au-delà d’un processus worker.
Chaque article se suffit à lui-même. Ensemble, ils vont de la boucle vers les couches externes.
Commencez par la frontière d’échec
Un bon prompt ne tranche pas 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 tool result inattendu de rediriger l’exécution.
Trois patterns de raisonnement pour les AI agents
ReAct : décider après chaque observation
ReAct (Yao et al., 2022), abréviation de Reason + Act, maintient la décision suivante au plus près de l’observation la plus récente :
L’article original utilise explicitement les textes Thought, Action et Observation. Les APIs natives d’outils exposent aujourd’hui les appels proposés et les tool results ; une pensée visible n’est pas nécessaire. La pensée intercalée est une capacité distincte du modèle/de l’API. Dans le prompting historique :
- Thought : l’agent génère une « pensée » pour décomposer l’objectif et planifier l’étape suivante.
- Action : à partir de cette pensée, il appelle un outil.
- Observation : l’agent lit le résultat, ce qui met à jour sa compréhension pour la pensée suivante.
ReAct possède ainsi plusieurs propriétés utiles :
- Dans l’exemple manuel HotpotQA de l’article, exécuté avec PaLM-540B, les observations issues de Wikipedia produisaient moins de faits hallucinés que le 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.
Cette même boucle a aussi des coûts :
- Dans une implémentation naïve à historique complet sans caching, chaque tour retraite l’historique qui s’allonge. Le prompt caching modifie le coût des entrées et le temps de traitement, mais pas l’occupation de la context window ni les observations obsolètes. Mesurez séparément les tokens mis en cache et hors cache ; la summarization et la troncature suppriment du 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-la 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 des outils en un seul passage, en utilisant des placeholders pour les valeurs qui n’existent qu’après l’exécution.
- Plan : un appel au LLM écrit le plan complet des tool calls, en utilisant des placeholders de variables (
#E1,#E2) pour les sorties qui n’existent pas encore. - Worker : un executor sans LLM exécute les outils planifiés et remplit 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.
- Solver : un dernier appel au LLM reçoit les observations collectées et rédige la réponse.
Cette séparation apporte :
- Moins d’appels répétés au modèle que ReAct lorsque le plan initial reste valide.
- Moins d’historique de prompt répété qu’une boucle intercalée à historique complet. La latence des outils dépend néanmoins de la manière dont le worker planifie les appels.
- La possibilité de fine-tuner séparément le planner, sans environnement réel.
Elle crée aussi une frontière stricte. Dans le test de robustesse HotpotQA de l’article, chaque outil renvoyait No evidence found ; ReWOO perdait moins en précision que ReAct, car les observations en échec n’entraînaient pas son planner dans une nouvelle boucle. Il s’agit d’une robustesse relative, pas d’une politique de récupération lors de l’exécution. Une implémentation doit toujours décider si une erreur d’outil devient un élément de 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 graphe initial incorrect.
Utilisez-le pour les instantanés rapides, les vérifications de statut 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ésigne 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 généralement appelé Plan-and-Execute. Le guide Plan-and-Execute de LangChain documente ce pattern :
- Phase de planification : l’agent génère d’abord un plan qui décompose la tâche en sous-tâches plus petites.
- Phase d’exécution : l’agent exécute ensuite ces sous-tâches une par une. Dès que des outils sont impliqués, chaque sous-tâche s’exécute généralement comme sa propre petite boucle ReAct ; l’executor peut donc toujours réagir à ce que renvoie 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 est un choix d’implémentation, pas un résultat établi par l’article Plan-and-Solve.
La figure inclut une branche facultative de replanning. Le graphe pédagogique présenté plus loin dans cet article ne l’utilise pas : le feedback peut modifier le travail à l’intérieur d’une étape, mais pas le plan restant.
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 explicite de replanning peut interrompre l’exécution et réévaluer la situation après un résultat inattendu.
- Une spécialisation des modèles. Le planner peut être coûteux, tandis que l’executor peut être peu coûteux.
- 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 qui nécessitent une synthèse finale.
Choisissez selon l’endroit où le plan peut échouer
| Fonctionnalité | ReAct (2022) | Plan-and-Execute (2023) | ReWOO (2023) |
|---|---|---|---|
| Philosophie centrale | Improvisateur : décider de l’action suivante à partir du dernier résultat, un appel à la fois. | Architecte : construire un plan complet, l’exécuter, puis le réviser. | Optimiseur : compiler un graphe de dépendances, puis regrouper les appels prêts. |
| Workflow | Boucle 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. | Chaque étape peut réagir à son résultat. Un replanner facultatif peut réviser les étapes suivantes. | 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. | Chaque boucle ReAct d’une étape peut démarrer avec un contexte court plutôt qu’avec l’historique complet de l’exécution ; un replanner ajoute des appels de planification lorsqu’il est activé. | Moins d’appels au modèle ; le worker de cet article regroupe également les outils dont les dépendances sont prêtes. |
| Idéal pour | L’exploration ouverte ou les tâches dont les résultats sont imprévisibles. | Les tâches à long horizon qui nécessitent un objectif stable (par exemple, rédiger un article). | Les workflows structurés et répétables (par exemple, vérifier la météo dans 5 villes). |
Le tableau est une aide au routage, pas un benchmark. Utilisez ReAct lorsqu’un tool result peut modifier l’action suivante. Utilisez Plan-and-Execute lorsque la tâche se divise en étapes, mais que chaque étape nécessite encore du feedback. N’ajoutez un replanner que lorsqu’un résultat doit modifier les étapes ultérieures ; le graphe pédagogique ci-dessous n’en possède pas. Utilisez ReWOO lorsque toutes les dépendances entre outils sont connues avant l’exécution. Son graphe de dépendances permet au worker pédagogique d’exécuter en parallèle les appels prêts et de détecter un graphe qui ne peut pas progresser. Avec le helper execute_tool du companion, les erreurs d’outils interceptées deviennent des chaînes transmises au solver. Une exception qui échappe au helper interrompt le worker. Aucun de ces deux chemins ne fournit de retries ni de replanning. 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.
Ce qui change avec les modèles et les harnesses actuels
Ces patterns décrivent des décisions situées en dehors du modèle. Un modèle qui raisonne entre les tool calls a toujours besoin d’un programme pour exécuter ces appels, arrêter la boucle et autoriser les effets. Avec Claude Sonnet 5, l’adaptive thinking est activé par défaut et son texte est omis par défaut. Un bloc de réflexion vide ne signifie pas que le modèle a ignoré le raisonnement. Préservez les blocs de réflexion signés complets lors du renvoi des tool results ; reconstruire la conversation à partir du seul texte visible rompt cette continuité. Comparez l’effort de raisonnement et les tokens de sortie facturés, ainsi que le nombre d’appels au modèle.
Pour une nouvelle implémentation LangChain, commencez par create_agent. Cet exemple autonome utilise l’identifiant actuel de Sonnet et un outil fixture local. Définissez ANTHROPIC_API_KEY et installez langchain ainsi que langchain-anthropic avant de l’invoquer ; cette invocation effectue des requêtes payantes au modèle.
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
def lookup_fixture_company(ticker: str) -> str:
"""Look up a company name in a tiny local fixture, not live market data."""
return {"NVDA": "NVIDIA", "AMD": "AMD"}.get(
ticker.strip().upper(), "No company in the fixture"
)
agent = create_agent(
model=ChatAnthropic(model="claude-sonnet-5", max_tokens=4096),
tools=[lookup_fixture_company],
system_prompt="Use the fixture for company names. Do not invent market data.",
)
result = agent.invoke({
"messages": [{"role": "user", "content": "Which company is NVDA in the fixture?"}]
})
print(result["messages"][-1].content)
Il s’agit d’une boucle d’outils pilotée par les observations. Aucun planner distinct n’est nécessaire pour cette recherche unique. Je laisse de côté les overrides d’échantillonnage dans l’exemple : une mise à niveau du modèle nécessite également de vérifier les paramètres acceptés, les blocs de réponse et le comportement des structured outputs. L’exemple a été vérifié hors ligne pour les imports et la construction, mais n’a pas été évalué avec une inférence en direct.
Si le travail nécessite des contextes de subagents isolés, des fichiers et une gestion automatique du contexte, Deep Agents regroupe ces capacités autour de la même boucle d’outils. Il s’agit d’un choix de harness, pas d’un quatrième algorithme de raisonnement. Comparez un agent unique avec la délégation sur des tâches qui se divisent réellement en travaux indépendants ; incluez le coût de coordination et le contexte perdu dans le résultat.
Exemple détaillé : le Market Analyst Agent
Le Market Analyst Agent rend cette distinction concrète. Une même codebase utilise les trois patterns pour l’étude de marché, et un routeur choisit entre un chemin de recherche approfondie et un chemin de briefing rapide. Les extraits ci-dessous sont des variantes pédagogiques abrégées de commit b4e769a : le worker ci-dessous ajoute des batches parallèles pour les étapes dont les dépendances sont prêtes et lève une exception lorsque le graphe ne peut pas progresser. Son execute_tool est le helper companion, qui intercepte les exceptions des outils et renvoie des chaînes d’erreur au solver. Seules les exceptions qui échappent à ce helper interrompent un future. Cette adaptation pédagogique n’ajoute ni retries ni récupération.
Il utilise LangGraph pour l’orchestration. Un node est une fonction Python qui renvoie des 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 crée des checkpoints aux frontières des super-steps : un node ou un batch de nodes parallèles. Les écritures en attente préservent les résultats réussis des nodes frères lorsqu’un autre node échoue. Les trois patterns partagent un même objet d’état ; le routage ne nécessite donc pas trois schemas distincts :
Le diagramme isole le routage et la création du brouillon. Il omet l’évaluateur partagé et l’approbation humaine avant publication, présentés plus loin, afin que les deux boucles de raisonnement restent lisibles.
Définition de l’état
Les blocs Python ci-dessous sont des extraits d’intégration, pas des scripts autonomes : ils partagent les types d’état, les helpers de node et les imports de framework du companion. Le runner d’exemple isolé du dépôt les ignore ; les vérifications de contrat hors ligne couvrent l’état, les appels proposés, les IDs de messages et le comportement de reprise.
Le schema d’état contient les champs nécessaires aux deux modes :
from typing import Literal
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
report_approved: bool = False
evaluator_verdict: Literal["pass", "fail", "needs_human"] | None = None
evaluator_reasons: list[str] = Field(default_factory=list)
Pattern 1 : implémentation Plan-and-Execute
Plan-and-Execute convient à la synthèse en plusieurs étapes. Un planner écrit les étapes de haut niveau, puis une boucle ReAct exécute chaque étape et réagit aux tool results.
Les extraits historiques du companion conservent claude-sonnet-4-5-20250929 et l’ancienne API create_react_agent afin de rester comparables avec le commit lié. Le point de départ actuel est l’exemple create_agent ci-dessus. Migrer le graphe complet nécessite de tester ensemble ses schemas de planner, la gestion des messages internes, le routage et le comportement de reprise ; modifier uniquement la chaîne du modèle ne constitue pas cette migration.
L’implémentation maintient visibles quatre frontières :
- Une seule phase de planification initiale. Un appel unique au LLM produit le plan complet sous forme de liste de descriptions d’étapes.
- Une sortie guidée par un schema qui valide la structure de la réponse. L’exécution nécessite une vérification séparée du plan.
- Aucune exécution d’outil à ce stade. Le planner décide uniquement quoi faire, pas comment.
- Des étapes lisibles par un humain. Chaque étape est un texte qu’un executor 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
}
Cette ligne llm.with_structured_output(PlanOutput) correspond au Schema-Guided Reasoning (SGR), que j’ai présenté dans un article précédent. Le schema rejette les champs mal formés. Avant l’exécution, exigez également une liste d’étapes non vide et bornée, des numéros d’étape uniques et des descriptions exploitables ; ce schema abrégé n’impose pas ces conditions. Le companion peut donc encore accepter un plan vide et échouer lorsque son executor indexe la première étape.
Pattern 2 : exécution ReAct
Une fois le plan créé, l’executor exécute 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 :
- Exécution itérative. Une étape à la fois, avec le feedback des observations.
- La boucle modèle/tool-result s’exécute à l’intérieur de
create_react_agent; la factory ne promet pas de transcript de raisonnement exposé. - Les résultats des étapes précédentes sont injectés comme contexte pour le raisonnement courant.
- L’agent choisit les outils en fonction de la description de l’étape.
- 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 observable loop is a proposed call, its result, and the next decision.
# A visible reasoning block depends on the model and API configuration.
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 de 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 est la suivante :
- Trois phases (Planner → Worker → Solver). Le worker ne demande pas au modèle quoi faire ensuite et ne replanifie pas.
- Les tool calls référencent les placeholders
#E1,#E2correspondant à des résultats qui n’existent pas encore. - Aucun LLM pendant l’exécution. Le worker se contente d’exécuter les outils.
- Les outils indépendants s’exécutent en parallèle.
- Un seul appel de synthèse à la fin, portant sur toutes les données à la fois.
Phase 1 : planner ReWOO (écrit en amont un plan typé d’appels proposés)
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 # Requested tool name; validate it against a registry in production
tool_args: dict # Proposed arguments; may contain refs like {"price": "#E1"}
depends_on: list[str] = [] # Declared ordering; cross-check it against #E placeholders
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, it creates typed proposed tool calls.
Production code validates them before execution; this teaching worker does not.
"""
llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
# Schema-Guided Reasoning validates the planner response shape.
# It does not validate a tool name, its arguments, or its dependencies.
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"""),
])
# Store the typed proposed-call plan.
# This teaching worker sends ready steps directly to execute_tool.
return {"rewoo_plan": result.steps}
Phase 2 : worker ReWOO (exécute les outils sans raisonnement du 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 au 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())}
Le planner écrit les appels proposés, le worker remplit leurs placeholders et le solver reçoit les résultats. Le schema vérifie uniquement la structure de la réponse. Avant le dispatch, rejetez les plans vides ou trop volumineux, les IDs dupliqués, les dépendances manquantes et les cycles. Les IDs dupliqués s’écrasent silencieusement dans le dictionnaire pending ci-dessus. Vérifiez les noms et arguments des outils par rapport à un registre, inspectez récursivement les placeholders à l’intérieur des listes et objets imbriqués, confrontez-les à depends_on et rejetez les références non résolues avant l’appel concerné. Ce worker pédagogique ignore ces vérifications et transmet les étapes prêtes au execute_tool du companion, y compris son comportement de transformation des erreurs en éléments de preuve. Le solver doit traiter une chaîne d’erreur comme une preuve manquante, et non comme une recherche réussie. Si son graphe ne peut pas progresser, il s’arrête avant le solver ; aucun chemin de retry ou de replanning n’existe. Réessayer avec le même état ne fait que répéter le même plan ; le replanning nécessite donc un résultat d’échec, une arête conditionnelle et une limite de tentatives.
Où chaque pattern appelle le modèle
| Pattern | Appels au LLM pendant l’exécution | Mises à jour de l’état | Pattern de code clé |
|---|---|---|---|
| Plan-and-Execute | 1 pour la planification + une boucle ReAct par étape (plusieurs appels chacune) + 1 pour le rapport | Achèvement séquentiel des étapes | planner_node() → boucle : executor_node() → reporter_node() |
| ReAct (dans chaque étape) | Plusieurs par étape (cycles thought-action) | Transcript interne uniquement ; le graphe externe enregistre les résultats des étapes terminées | Le companion épinglé utilise create_react_agent(), désormais obsolète |
| ReWOO | 1 pour la planification + 0 pendant l’exécution + 1 pour la synthèse | Batches d’outils tenant compte des dépendances | rewoo_planner_node() → rewoo_worker_node() → rewoo_solver_node() |
La différence importante réside dans la sortie du planner. Elle détermine le niveau de discrétion conservé par l’executor :
-
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’executor lit chaque description et décide quels outils appeler. C’est flexible, mais chaque étape constitue sa propre boucle ReAct ; une étape coûte donc plusieurs appels au modèle, et non un seul.
-
ReAct ne possède pas de plan initial. Il utilise un raisonnement itératif :
# A standalone full-history ReAct loop carries messages from earlier turns. messages = [ HumanMessage(content="Execute Step 1: Get current price"), AIMessage(content="", tool_calls=[{ "id": "1", "name": "get_stock_snapshot", "args": {"ticker": "NVDA"}, "type": "tool_call", }]), ToolMessage(tool_call_id="1", content="$132.45"), AIMessage(content="Now I need metrics..."), # ... agent continues until step complete ]Dans une boucle ReAct autonome à historique complet, chaque appel au modèle transporte un historique qui s’allonge pendant toute la tâche. Les cache hits peuvent réduire le calcul répété et les frais d’entrée ; une boucle de production peut aussi le résumer ou le tronquer. L’exemple Plan-and-Execute démarre chaque invocation ReAct avec l’étape courante et un condensé des résultats précédents. Il conserve le résultat terminé dans
plan, et non le transcript interne des tool calls. -
ReWOO crée des tool calls proposés, explicites et typés :
# Planner output (list of ReWOOPlanStep objects) rewoo_plan = [ ReWOOPlanStep( step_id="#E1", description="Read the current price and valuation snapshot", tool_name="get_stock_snapshot", tool_args={"ticker": "NVDA"} ), ReWOOPlanStep( step_id="#E2", description="Find recent NVDA earnings news", tool_name="search_news", tool_args={"query": "NVDA earnings", "max_results": 5} ), # ... all tool calls planned upfront ]Le worker s’exécute en aveugle, sans intervention du 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 passe par
plan→current_step_index→research_data. - ReAct dans ce graphe Plan-and-Execute : l’agent interne produit le transcript d’une étape. Le graphe externe conserve
plan,current_step_indexet les résultats des étapes terminées. - ReWOO : l’état passe par
rewoo_plan, dont les champsresultsont remplis par le worker.
Relier les deux routes dans un même graphe
Le graphe expose deux routes utilisateur sur un seul 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, pas une troisième route.
Cette implémentation ne possède pas de replanner : elle exécute le plan initial jusqu’au bout. Ajouter du replanning nécessiterait une arête de executor vers planner et une règle déterminant à quel moment 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 run the same evaluator before the human approval step.
builder.add_edge("evaluator", "publish")
builder.add_edge("publish", END)
return builder.compile(
checkpointer=checkpointer,
# Human-in-the-loop pause: the reporter (or ReWOO solver) writes a
# draft. A separate model session reads that draft and records its
# assessment. The graph then stops before publishing, whichever
# assessment it produced, so a human can review both the draft and
# assessment. Part 6 explains how the program decides a run is complete.
interrupt_before=["publish"],
)
Sélection automatique du pattern avec un routeur
Le routeur associe la forme de la requête à une route. Le Schema-Guided Reasoning contraint la sortie du classifier :
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 routeur, « current price » est dirigé vers ReWOO et « investment thesis » vers Plan-and-Execute. Par défaut, les requêtes ambiguës sont envoyées vers la recherche approfondie. Avant de placer le routeur devant les utilisateurs, comparez les deux routes avec un workflow fixe sur les mêmes tâches, outils, éléments de preuve et budget. Répétez les essais stochastiques et comptez chaque tentative démarrée. Suivez la réussite des tâches, les acceptations incorrectes, les tokens mis en cache et hors cache, le temps écoulé et la récupération après des erreurs d’outils ou des observations trompeuses. Ces patterns sont des choix de flux de contrôle, pas un classement d’adoption ; le retour d’expérience d’ingénierie d’Anthropic recommande lui aussi de commencer par des workflows simples et composables.
L’implémentation complète du companion, avec le routeur et l’état partagé, se trouve dans le commit épinglé du Market Analyst Agent.
La prochaine couche 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 routeur et l’executor ci-dessus ne fonctionnent que tant qu’un seul processus et une seule context window restent actifs.
Références
- ReAct: Synergizing Reasoning and Acting in Language Models (Yao et al., 2022)
- ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models (Xu et al., 2023)
- Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models (Wang et al., 2023)
- Market Analyst Agent Repository