Loops de raciocínio de AI agents: ReAct, ReWOO, Plan-and-Execute
Tradução automática Este artigo foi traduzido automaticamente a partir da versão original em inglês.
Atualização do artigo
Publicado originalmente em 31 de janeiro de 2026. Revisto e atualizado em 6 de setembro de 2026. A atualização centra-se nas capacidades mais recentes dos modelos e nas reasoning APIs, com exemplos e ligações para fontes revistos.
Um agent reasoning loop é o fluxo de controlo que decide quando um modelo planeia, faz um tool call, lê o resultado e termina. Para um engenheiro que está a construir um agent, essa escolha também é um orçamento: determina com que frequência o modelo é executado, quanta informação histórica cada chamada transporta e se um resultado inesperado pode alterar a ação seguinte.
Este artigo compara ReAct, ReWOO e Plan-and-Execute através de um LangGraph Market Analyst Agent que construí. No final, terá uma regra de routing e estruturas de implementação que poderá adaptar, em vez de apenas três nomes para acrescentar a um diagrama.
O loop é a camada mais interna da série. Memory, tools, security, runtime e as verificações feitas antes de declarar uma tarefa concluída envolvem-no; não o substituem.
Em resumo: escolha ReAct quando cada resultado de uma tool possa alterar o passo seguinte, ReWOO quando o dependency graph for conhecido antes da execução e Plan-and-Execute quando uma tarefa grande puder ser decomposta, mas os passos individuais continuarem a precisar de feedback. Estes são critérios de routing, não classificações universais de velocidade; faça medições com o seu modelo, as suas tools e a sua retry policy.
Para uma comparação breve de frameworks, consulte Best AI Agent Frameworks in 2026.
O reasoning loop decide o que fazer a seguir. Não armazena estado, executa tools nem autoriza side effects.
O harness é o programa de controlo entre o modelo e a máquina. Constrói prompts a partir do estado armazenado (Parte 2), define as ações que o modelo pode nomear (Parte 3), autoriza chamadas (Parte 4) e verifica evidências antes de declarar uma tarefa concluída (Parte 6). São problemas de engenharia distintos, mas um turno passa pelos quatro.
O runtime (Parte 5) fornece o session log, o sandbox, o checkpoint store e os traces que sobrevivem a um único processo worker.
Cada artigo é autónomo. Em conjunto, avançam do loop para o exterior.
Comece pela fronteira de falha
Um bom prompt não resolve a questão do fluxo de controlo. Os padrões diferem na quantidade de trabalho que é fixada antes do primeiro tool call. Isso determina o número de chamadas ao modelo, o momento em que um plano incorreto se torna visível e se um resultado inesperado de uma tool pode redirecionar a execução.
Três padrões de raciocínio para AI agents
ReAct: decidir depois de cada observação
ReAct (Yao et al., 2022), abreviatura de Reason + Act, mantém a decisão seguinte próxima da observação mais recente:
O artigo original usa texto explícito de Thought, Action e Observation. As APIs nativas de tools expõem proposed calls e tool results; não é necessário apresentar um thought visível. Interleaved thinking é uma capacidade distinta do modelo/API. No padrão histórico de prompting:
- Thought: o agent gera um “thought” para decompor o objetivo e planear o passo seguinte.
- Action: com base no thought, faz um tool call.
- Observation: o agent lê o resultado, atualizando a sua compreensão para o thought seguinte.
Isto confere ao ReAct propriedades úteis:
- Na amostra manual do artigo com PaLM-540B HotpotQA, as observações da Wikipédia produziram menos factos alucinados do que o prompting com chain-of-thought.
- O agent pode alterar a estratégia em tempo real com base no que acabou de observar.
- O histórico de tool calls e observações fornece um execution trace concreto.
O mesmo loop também tem custos:
- Numa implementação ingénua com histórico completo e sem caching, cada turno processa novamente o histórico crescente. Prompt caching altera o custo de input e o tempo de processamento, mas não a ocupação da context window nem as observações obsoletas. Meça separadamente os tokens cached e uncached; summarization e truncation eliminam contexto.
- É ineficiente quando os tool calls poderiam ter sido planeados antecipadamente — precisamente o nicho do ReWOO.
- Sem uma stop condition ou step limit, o loop pode executar indefinidamente.
Use-o em tarefas exploratórias, debugging e trabalho em que não seja possível prever a ação seguinte.
ReWOO: compilar primeiro o tool graph
ReWOO (Reasoning WithOut Observation) separa o planeamento da execução. O planner escreve toda a sequência de tools numa única passagem, usando placeholders para valores que só existirão depois da execução.
- Plan: uma chamada ao LLM escreve o plano completo de tool calls, usando variable placeholders (
#E1,#E2) para outputs que ainda não existem. - Worker: um executor que não é um LLM executa as tools planeadas e preenche os placeholders. O worker do artigo segue o plano; a implementação apresentada mais adiante acrescenta batches paralelos sensíveis a dependências para os passos prontos.
- Solver: uma chamada final ao LLM recebe as observações recolhidas e escreve a resposta.
Esta separação proporciona:
- Menos chamadas repetidas ao modelo do que ReAct quando o plano inicial continua válido.
- Menos histórico de prompt repetido do que num loop intercalado com histórico completo. A latência das tools continua a depender da forma como o worker agenda as chamadas.
- O planner pode ser fine-tuned isoladamente, sem um ambiente em tempo real.
Também cria uma fronteira rígida. No teste de stress HotpotQA do artigo, todas as tools
devolveram No evidence found; o ReWOO perdeu menos precisão do que o ReAct porque as
observações falhadas não fizeram o planner entrar noutro loop. Trata-se de robustez relativa,
não de uma policy de recovery durante a execução. Uma implementação continua a ter de decidir
se um erro de uma tool passa a ser evidência para o solver, desencadeia um retry ou aborta a
execução. O ReWOO adequa-se a workflows previsíveis; não re-planeia autonomamente em torno de
um graph inicial incorreto.
Use-o para snapshots rápidos, verificações de estado e dashboards cujo comportamento das tools seja previsível.
Plan-and-Execute: decompor e reagir localmente
Plan-and-Solve prompting descreve um método de prompting que cria primeiro um plano e resolve depois as subtarefas. Um padrão relacionado de tool orchestration é habitualmente chamado Plan-and-Execute. O guia Plan-and-Execute da LangChain documenta esse padrão:
- Planning phase: o agent gera primeiro um plano que divide a tarefa em subtarefas mais pequenas.
- Execution phase: o agent executa essas subtarefas uma de cada vez. Quando estão envolvidas tools, cada subtarefa costuma executar o seu próprio pequeno loop ReAct, permitindo ao executor reagir ao resultado de uma tool, embora o plano global seja fixo.
O artigo original centrou-se em zero-shot prompting. Numa implementação com tools, o padrão de orchestration pode executar sequencialmente os passos planeados e usar modelos diferentes para planeamento e execução. Essa separação de modelos é uma escolha de implementação, não um resultado estabelecido pelo artigo Plan-and-Solve.
A figura inclui um ramo opcional de replanning. O teaching graph apresentado mais adiante neste artigo não o utiliza: o feedback pode alterar o trabalho dentro de um passo, mas não o plano restante.
O padrão é útil porque oferece:
- Raciocínio hierárquico que espelha a forma como um especialista humano decompõe um projeto.
- Uma edge explícita de replanning pode pausar e reavaliar depois de um resultado inesperado de um passo.
- Especialização de modelos. O planner pode ser dispendioso e o executor pode ser barato.
- Com um checkpointer configurado, cada passo concluído pode tornar-se num resume boundary.
Os custos são:
- Mais round trips ao modelo do que ReWOO quando cada passo contém o seu próprio loop ReAct.
- Mais estado para gerir.
- Complexidade excessiva para queries one-shot.
Use-o para análises complexas e investigação que exijam uma síntese final.
Escolha com base no ponto onde o plano pode falhar
| Funcionalidade | ReAct (2022) | Plan-and-Execute (2023) | ReWOO (2023) |
|---|---|---|---|
| Filosofia central | Improvisador: decide o passo seguinte a partir do último resultado, uma chamada de cada vez. | Arquiteto: constrói um blueprint completo, executa-o e revê-o depois. | Optimizador: compila um dependency graph e agrupa as chamadas que estão prontas. |
| Workflow | Loop iterativo: Thought → Action → Observation. | Duas fases: Fase 1 (Planning), Fase 2 (Execution). | Desacoplado: o Planner escreve um graph de tool calls; o Worker executa-os; o Solver compõe a resposta. |
| Adaptabilidade | Máxima: pode mudar de direção depois de cada tool call. | Cada passo pode responder ao seu resultado. Um replanner opcional pode rever os passos seguintes. | Mínima: o script do planner executa até ao fim; nada faz replanning durante a execução. |
| Eficiência | Um loop ingénuo com histórico completo repete mais tokens de input; a gestão de contexto pode limitar esse crescimento. | O loop ReAct de cada passo pode começar com um contexto curto, em vez do histórico completo da execução; um replanner acrescenta chamadas de planeamento quando ativado. | Menos chamadas ao modelo; o worker deste artigo também agrupa tools prontas por dependências. |
| Mais indicado para | Exploração aberta ou tarefas cujos resultados são imprevisíveis. | Tarefas de longo horizonte que exigem um objetivo estável (por exemplo, escrever um artigo). | Workflows estruturados e repetíveis (por exemplo, verificar o tempo em 5 cidades). |
A tabela serve para orientar o routing, não é um benchmark. Use ReAct quando o resultado de uma tool puder alterar a ação seguinte. Use Plan-and-Execute quando a tarefa se dividir em passos, mas cada passo continuar a precisar de feedback. Acrescente um replanner apenas quando um resultado tiver de alterar passos posteriores; o teaching graph abaixo não tem um. Use ReWOO quando todas as dependências das tools forem conhecidas antes da execução. O seu dependency graph permite ao worker deste exemplo executar em paralelo as chamadas prontas e detetar um graph que não pode avançar. Com o helper execute_tool do companion, os erros capturados das tools tornam-se strings passadas ao solver. Uma exceção que escape ao helper aborta o worker. Nenhum dos dois caminhos fornece retries ou replanning. Meça os três com o seu modelo, a latência das tools, o conjunto de tarefas e a retry policy antes de otimizar o número de chamadas.
O que muda com os modelos e harnesses atuais
Estes padrões descrevem decisões fora do modelo. Um modelo que raciocina entre tool calls continua a precisar de um programa para executar essas chamadas, parar o loop e autorizar efeitos. No Claude Sonnet 5, o adaptive thinking está ativado por predefinição e o seu texto é omitido por predefinição. Um thinking block vazio não significa que o modelo tenha ignorado o raciocínio. Preserve os thinking blocks completos e assinados ao devolver tool results; reconstruir a conversação a partir do texto visível elimina essa continuidade. Compare o reasoning effort e os output tokens faturados, além do número de chamadas ao modelo.
Para uma nova implementação em LangChain, comece por create_agent. Este exemplo autónomo de wiring usa o identificador atual do Sonnet e uma fixture tool local. Defina ANTHROPIC_API_KEY e instale langchain mais langchain-anthropic antes de o invocar; essa invocação faz requests pagos ao modelo.
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)
Este é um tool loop orientado por observações. Não é necessário um planner separado para essa consulta única. Deixo de fora os sampling overrides no exemplo: uma atualização do modelo também exige verificar os parâmetros aceites, os response blocks e o comportamento de structured output. O exemplo foi verificado offline quanto a imports e construção, mas não foi avaliado com inference em tempo real.
Se o trabalho precisar de contextos isolados de subagents, ficheiros e gestão automática de contexto, os Deep Agents disponibilizam essas capacidades em torno do mesmo tool loop. Trata-se de uma escolha de harness, não de um quarto algoritmo de reasoning. Compare um agent único com delegation em tarefas que realmente se dividam em trabalho independente; inclua no resultado o custo de coordenação e o contexto perdido.
Um exemplo completo: o Market Analyst Agent
O Market Analyst Agent torna a distinção concreta. Uma base de código usa os três padrões para market research, e um router escolhe entre um percurso de deep research e um percurso de flash briefing. Os excertos abaixo são variantes pedagógicas abreviadas do commit b4e769a: o worker abaixo acrescenta batches paralelos de passos prontos por dependências e lança uma exceção quando o graph não pode avançar. O seu execute_tool é o helper companion, que captura exceções das tools e devolve strings de erro ao solver. Apenas as exceções que escapam a esse helper abortam um future. Esta adaptação pedagógica não acrescenta retries nem recovery.
Usa LangGraph para orchestration. Um node é uma função Python que devolve campos a atualizar no estado partilhado. Uma edge declara o node seguinte e pode chamar uma função de routing. O LangGraph combina as atualizações e faz checkpoints nas fronteiras de super-step: um node ou um batch de nodes paralelos. Os pending writes preservam os resultados dos siblings que tiveram sucesso quando outro node falha. Os três padrões partilham um único objeto de estado, pelo que o routing não exige três schemas separados:
O diagrama isola o routing e a criação do draft. Omite o evaluator partilhado e a aprovação humana antes da publicação, apresentados mais adiante, para que os dois reasoning loops permaneçam legíveis.
Definição do estado
Os blocos Python abaixo são excertos de integração, não scripts autónomos: partilham tipos de estado, node helpers e imports do framework com o companion. O runner isolado de exemplos do repositório ignora-os; os contract checks offline abrangem o estado, as proposed calls, os message IDs e o comportamento de resume.
O state schema transporta os campos necessários aos dois modos:
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)
Padrão 1: implementação de Plan-and-Execute
Plan-and-Execute adequa-se a sínteses com vários passos. Um planner escreve os passos de alto nível e, em seguida, um loop ReAct executa cada passo e reage aos resultados das tools.
Os seguintes excertos históricos do companion mantêm claude-sonnet-4-5-20250929 e a API create_react_agent mais antiga, para continuarem comparáveis com o commit associado. O ponto de partida atual é o exemplo create_agent acima. Migrar o graph completo exige testar em conjunto os schemas do planner, o tratamento de mensagens internas, o routing e o comportamento de resume; alterar apenas a string do modelo não constitui essa migração.
A implementação mantém visíveis quatro fronteiras:
- Uma única fase inicial de planeamento. Uma chamada ao LLM produz o plano completo sob a forma de uma lista de descrições de passos.
- Structured output guiado por schema, que valida a forma da resposta. A execução precisa de uma verificação separada do plano.
- Ainda não há execução de tools. O planner decide apenas o que fazer, não como fazê-lo.
- Passos legíveis por humanos. Cada passo é texto que um executor irá interpretar.
# 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
}
Essa linha llm.with_structured_output(PlanOutput) corresponde a Schema-Guided Reasoning (SGR), que abordei num artigo anterior. O schema rejeita campos malformados. Antes da execução, exija também uma lista de passos não vazia e limitada, com números de passo únicos e descrições utilizáveis; este schema abreviado não impõe essas condições. O companion pode ainda aceitar um plano vazio e falhar quando o seu executor tentar aceder ao primeiro passo.
Padrão 2: execução ReAct
Quando o plano existe, o executor executa cada passo como o seu próprio loop ReAct. Esta é a Fase 2: cada passo é suficientemente pequeno para manter focado um ciclo Thought-Action-Observation, e o agent pode reagir ao que quer que a tool devolva.
A parte ReAct organiza-se assim:
- Execução iterativa. Um passo de cada vez, com feedback das observações.
- O loop entre o modelo e o tool result é executado dentro de
create_react_agent; a factory não garante um reasoning transcript exposto. - Os resultados dos passos anteriores são fornecidos como contexto para o raciocínio atual.
- O agent escolhe as tools com base na descrição do passo.
- Pode alterar a abordagem a meio do passo com base no resultado de uma tool.
# 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,
}
Padrão 3: ReWOO para snapshots rápidos
Para um briefing rápido, o ReWOO elimina as chamadas ao modelo da fase de execução. As tools independentes são executadas em paralelo; as tools dependentes aguardam os seus pré-requisitos. O planner emite antecipadamente o graph de tools e o worker executa-o sem perguntar ao modelo o que deve fazer a seguir.
A estrutura é a seguinte:
- Três fases (Planner → Worker → Solver). O worker não pergunta ao modelo o que deve fazer a seguir nem faz replanning.
- Os tool calls referenciam
#E1,#E2placeholders para resultados que ainda não existem. - Não há LLM durante a execução. O worker limita-se a executar as tools.
- As tools independentes são executadas em paralelo.
- Uma única chamada de síntese no final, sobre todos os dados em conjunto.
Fase 1: planner ReWOO (escreve antecipadamente um plano tipado de proposed calls)
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}
Fase 2: worker ReWOO (executa as tools sem reasoning do 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])}
Fase 3: solver ReWOO (sintetiza todos os resultados numa única chamada ao 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())}
O planner escreve proposed calls, o worker preenche os seus placeholders e o solver recebe os resultados. O schema verifica apenas a forma da resposta. Antes do dispatch, rejeite planos vazios ou demasiado grandes, IDs duplicados, dependências em falta e cycles. IDs duplicados colapsam silenciosamente no dicionário pending acima. Verifique os nomes e argumentos das tools contra um registry, inspecione recursivamente os placeholders dentro de listas e objetos aninhados, faça o cross-check contra depends_on e rejeite referências não resolvidas antes da call afetada. Este teaching worker ignora essas verificações e envia os passos prontos para execute_tool do companion, incluindo o seu comportamento de error-as-evidence. O solver deve tratar uma string de erro como evidência em falta, não como uma consulta bem-sucedida. Se o graph não puder avançar, para antes do solver; não existe um caminho de retry ou replanning. Repetir o mesmo estado apenas repete o mesmo plano, pelo que o replanning exige um failure result, uma conditional edge e um limite para o número de tentativas.
Onde cada padrão chama o modelo
| Padrão | Chamadas ao LLM durante a execução | Atualizações de estado | Padrão de código principal |
|---|---|---|---|
| Plan-and-Execute | 1 para planeamento + um loop ReAct por passo (várias chamadas cada) + 1 para o relatório | Conclusão sequencial dos passos | planner_node() → loop: executor_node() → reporter_node() |
| ReAct (dentro de cada passo) | Várias por passo (ciclos thought-action) | Apenas o transcript interno; o graph exterior regista os resultados dos passos concluídos | O companion fixado usa create_react_agent(), que está deprecated |
| ReWOO | 1 para planeamento + 0 durante a execução + 1 para síntese | Batches de tools sensíveis a dependências | rewoo_planner_node() → rewoo_worker_node() → rewoo_solver_node() |
A diferença importante está no output do planner. Este determina quanta discricionariedade permanece com o executor:
-
Plan-and-Execute cria descrições de passos legíveis por humanos:
# 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 ]O executor lê cada descrição e decide que tools deve chamar. É flexível, mas cada passo é o seu próprio loop ReAct, pelo que um passo custa várias chamadas ao modelo, não uma.
-
O ReAct não tem um plano inicial. Usa raciocínio iterativo:
# 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 ]Num loop ReAct autónomo com histórico completo, cada chamada ao modelo transporta um histórico que cresce ao longo de toda a tarefa. Cache hits podem reduzir o processamento repetido e os custos de input; um loop de produção pode também fazer summarization ou truncation. O exemplo de Plan-and-Execute inicia cada invocação ReAct com o passo atual e um digest das conclusões anteriores. Mantém o resultado concluído em
plan, não o transcript interno de tool calls. -
O ReWOO cria proposed tool calls explícitos e tipados:
# 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 ]O worker executa às cegas, sem intervenção do LLM. Todas as chamadas ao modelo estão no planner e no solver, tornando previsível o número de chamadas.
Fluxo de memory e estado:
- Plan-and-Execute: o estado passa por
plan→current_step_index→research_data. - ReAct dentro deste graph Plan-and-Execute: o agent interno produz o transcript de um passo. O graph exterior mantém
plan,current_step_indexe os resultados dos passos concluídos. - ReWOO: o estado passa por
rewoo_plan, com os camposresultpreenchidos pelo worker.
Ligar ambos os percursos num único graph
O graph tem dois percursos expostos ao utilizador sobre um único AgentState: o deep research usa Plan-and-Execute com um loop ReAct dentro de cada passo, enquanto o flash briefing usa ReWOO. O ReAct é aqui uma primitiva de execução, não um terceiro percurso.
Esta implementação não tem replanner: executa o plano inicial até ao fim. Acrescentar replanning exigiria uma edge de executor de volta a planner e uma regra para determinar quando um resultado inesperado justifica outra chamada ao modelo.
O LangGraph mantém o wiring declarativo:
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"],
)
Seleção automática do padrão com um router
O router mapeia a forma do pedido para um percurso. O Schema-Guided Reasoning restringe o output do 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)
Com este router, “current price” segue para ReWOO e “investment thesis” para Plan-and-Execute. A predefinição é deep research quando o pedido é ambíguo. Antes de colocar o router à frente dos utilizadores, compare ambos os percursos com um workflow fixo, usando as mesmas tarefas, tools, evidências e orçamento. Repita trials estocásticos e conte todas as tentativas iniciadas. Registe o sucesso das tarefas, a aceitação incorreta, os tokens cached e uncached, o tempo de parede e o recovery depois de erros das tools ou observações enganadoras. Estes padrões são escolhas de fluxo de controlo, não uma classificação de adoção; o relato de engenharia da Anthropic recomenda igualmente começar com workflows simples e composable.
A implementação completa do companion, incluindo o router e o estado partilhado, está no commit fixado do Market Analyst Agent.
A camada seguinte é a memory
A Parte 2, AI Agent Memory Architecture, separa checkpoints resumíveis do conhecimento entre sessões e dos documentos do projeto. Sem essa camada de estado, o router e o executor acima só funcionam enquanto um processo e uma context window permanecerem ativos.
Referências
- 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