Engineering the Agentic Stack · Deel 1

Reasoning loops voor AI agents: ReAct, ReWOO en Plan-and-Execute

Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Een agent reasoning loop is de control flow die bepaalt wanneer een model plant, een tool aanroept, het resultaat leest en stopt. Voor een engineer die een agent bouwt, is die keuze ook een budgetbeslissing: ze bepaalt hoe vaak het model draait, hoeveel history elke call meedraagt en of een onverwacht resultaat de volgende actie kan veranderen.

In dit artikel vergelijk ik ReAct, ReWOO en Plan-and-Execute aan de hand van een LangGraph Market Analyst Agent die ik heb gebouwd. Je houdt er een routingregel en aanpasbare implementatievormen aan over, in plaats van drie namen om aan een diagram toe te voegen.

De loop is de binnenste laag van deze serie. Memory, tools, security, runtime en acceptance checks omringen de loop; ze vervangen haar niet.

Zie Best AI Agent Frameworks in 2026 voor de korte frameworkvergelijking.

De reasoning loop bepaalt wat er vervolgens moet gebeuren. Ze slaat geen state op, voert geen tools uit en autoriseert geen side effects.

De harness is het control-programma tussen het model en de machine. Ze bouwt prompts op basis van opgeslagen state (Part 2), definieert de acties die het model mag benoemen (Part 3), autoriseert calls (Part 4) en controleert het bewijs voordat ze een taak als voltooid markeert (Part 6). Het zijn afzonderlijke engineeringproblemen, maar één turn doorloopt ze alle vier.

De runtime (Part 5) levert de session log, sandbox, checkpoint store en traces die langer meegaan dan één worker process.

Waar elk onderdeel van de Engineering the Agentic Stack-serie zich bevindtWaar elk onderdeel van de Engineering the Agentic Stack-serie zich bevindt

Elk artikel staat op zichzelf. Samen bewegen ze van de loop naar buiten.


Begin bij de failure boundary

Een goede prompt beantwoordt de control-flowvraag niet. De patronen verschillen in hoeveel werk vóór de eerste tool call wordt vastgelegd. Dat bepaalt het aantal model calls, wanneer een slecht plan zichtbaar wordt en of een onverwacht tool result de run kan omleiden.

Drie reasoningpatronen voor AI agents

ReAct, ReWOO en Plan-and-Execute vergeleken op basis van het moment waarop evidence het plan kan veranderenReAct, ReWOO en Plan-and-Execute vergeleken op basis van het moment waarop evidence het plan kan veranderen

ReAct: beslis na elke observatie

ReAct (Yao et al., 2022), een afkorting van Reason + Act, houdt de volgende beslissing dicht bij de meest recente observatie:

  1. Thought: de agent genereert een “thought” om het doel op te splitsen en de volgende stap te plannen.
  2. Action: op basis van de thought roept de agent een tool aan.
  3. Observation: de agent leest het resultaat, waardoor zijn begrip voor de volgende thought wordt bijgewerkt.

ReAct leest elk tool result voordat de volgende actie wordt gekozenReAct leest elk tool result voordat de volgende actie wordt gekozen

Daaruit volgen de nuttige eigenschappen van ReAct:

  • In het handmatige PaLM-540B HotpotQA-voorbeeld uit het paper leverden Wikipedia-observaties minder gehallucineerde feiten op dan chain-of-thought prompting.
  • De agent kan zijn strategie on the fly aanpassen op basis van wat hij zojuist heeft gezien.
  • De history van tool calls en observations geeft je een concrete execution trace.

Dezelfde loop heeft ook kosten:

  • In een naïeve implementatie met volledige history wordt de history bij elke stap opnieuw verwerkt, waardoor latency en kosten groeien met de lengte van de loop. Summarization of truncation kan die groei begrenzen, maar kost discarded context.
  • Inefficiënt wanneer de tool calls vooraf hadden kunnen worden gepland; dat is precies het domein van ReWOO.
  • Zonder stop condition of step limit kan de loop oneindig blijven draaien.

Gebruik ReAct voor exploratory tasks, debugging en werk waarbij je de volgende actie niet kunt voorspellen.

ReWOO: compileer de tool graph eerst

ReWOO (Reasoning WithOut Observation) scheidt planning van execution. De planner schrijft de volledige tool sequence in één pass, met placeholders voor waarden die pas na execution bestaan.

  1. Plan: één LLM call schrijft het volledige plan van tool calls, met variable placeholders (#E1, #E2) voor outputs die nog niet bestaan.
  2. Worker: een non-LLM executor voert de geplande tools uit en vult de placeholders in. De worker in het paper volgt het plan; de implementatie verderop in dit artikel voegt dependency-aware parallel batches toe voor ready steps.
  3. Solver: een laatste LLM call ontvangt de verzamelde observations en schrijft het antwoord.

ReWOO plant de dependency graph voordat de tools worden uitgevoerdReWOO plant de dependency graph voordat de tools worden uitgevoerd

Deze scheiding biedt:

  • Minder herhaalde model calls dan ReAct wanneer het initiële plan geldig blijft.
  • Minder herhaalde prompt history dan een interleaved loop met volledige history. Tool latency hangt nog steeds af van hoe de worker calls plant.
  • De planner kan afzonderlijk worden fine-tuned, zonder live environment.

Er ontstaat ook een harde grens. In de HotpotQA-stresstest uit het paper gaf elke tool No evidence found terug; ReWOO verloor minder accuracy dan ReAct omdat de mislukte observations zijn planner niet in een nieuwe loop stuurden. Dat is relatieve robustness, geen execution recovery policy. Een implementatie moet nog steeds bepalen of een tool error solver evidence wordt, een retry triggert of de run afbreekt. ReWOO past bij predictable workflows; het re-plant niet zelfstandig rond een slechte initiële graph.

Gebruik het voor quick snapshots, status checks en dashboards waarvan het toolgedrag voorspelbaar is.

Plan-and-Execute: decompositieer en reageer lokaal

Plan-and-Solve prompting beschrijft een promptingmethode die eerst een plan maakt en daarna de subtasks oplost. Een verwant tool-orchestrationpatroon wordt meestal Plan-and-Execute genoemd. De Plan-and-Execute guide van LangChain documenteert dat patroon:

  1. Planning phase: de agent genereert eerst een plan dat de taak opsplitst in kleinere subtasks.
  2. Execution phase: de agent voert die subtasks vervolgens één voor één uit. Zodra tools betrokken zijn, draait elke subtask meestal als een eigen kleine ReAct-loop, zodat de executor nog steeds kan reageren op wat een tool teruggeeft, ook al ligt het overkoepelende plan vast.

Het oorspronkelijke paper richtte zich op zero-shot prompting. In een tool-using implementatie kan het orchestrationpatroon de geplande stappen sequentieel uitvoeren en verschillende models gebruiken voor planning en execution. Die model split is een implementatiekeuze, geen resultaat dat door het Plan-and-Solve-paper is vastgesteld.

Plan-and-Execute behoudt een overkoepelend plan terwijl elke stap feedback gebruiktPlan-and-Execute behoudt een overkoepelend plan terwijl elke stap feedback gebruikt

Het patroon is nuttig omdat het zorgt voor:

  • Hiërarchical reasoning die aansluit bij de manier waarop een menselijke expert een project opsplitst.
  • Een expliciete replanning edge die na een onverwacht step result kan pauzeren en opnieuw kan beoordelen.
  • Model specialisatie. De planner kan duur zijn, de executor goedkoop.
  • Met een geconfigureerde checkpointer kan elke voltooide stap een resume boundary worden.

De kosten zijn:

  • Meer model round trips dan ReWOO wanneer elke stap zijn eigen ReAct-loop bevat.
  • Meer state om te beheren.
  • Overkill voor one-shot queries.

Gebruik het voor complexe analysis en research die een final synthesis nodig hebben.

Kies op basis van waar het plan kan falen

FeatureReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Core philosophyImproviser: bepaal de volgende stap op basis van het laatste resultaat, één call per keer.Architect: bouw een volledig blueprint, voer die uit en review het daarna.Optimizer: compileer een dependency graph en batch vervolgens de calls die ready zijn.
WorkflowIteratieve loop: Thought → Action → Observation.Two-stage: Phase 1 (Planning), Phase 2 (Execution).Decoupled: de Planner schrijft een graph van tool calls; de Worker voert ze uit; de Solver stelt het antwoord samen.
AdaptabilityHighest: kan na elke afzonderlijke tool call van richting veranderen.Medium: re-plant doorgaans pas nadat een set stappen is voltooid.Lowest: het script van de planner draait tot voltooiing; tijdens de run wordt niet opnieuw gepland.
EfficiencyEen naïeve loop met volledige history herhaalt meer input tokens; context management kan die groei begrenzen.Re-planning gebeurt af en toe in plaats van per stap, en de ReAct-loop van elke stap kan starten met een korte context in plaats van de volledige history van de run.Minder model calls; de worker in dit artikel batcht bovendien tools waarvan de dependencies ready zijn.
Best forOpen-ended exploration of taken waarbij resultaten onvoorspelbaar zijn.Long-horizon tasks die een stabiel doel vereisen (bijv. het schrijven van een paper).Structured, repeatable workflows (bijv. het controleren van het weer in 5 steden).

De tabel is een routinghulp, geen benchmark. Gebruik ReAct wanneer een observation de volgende action kan veranderen. Gebruik Plan-and-Execute wanneer de taak netjes kan worden gedecomposeerd, maar elke stap nog feedback nodig heeft. Gebruik ReWOO wanneer elke tool dependency vóór execution bekend is. In de teaching implementation hieronder maakt die dependency graph parallel batches mogelijk en kan de worker detecteren dat een graph geen voortgang meer kan boeken. De execute_tool-helper is bewust fail-fast; production code heeft een expliciete retry-, fallback- of error-as-evidence-policy nodig. Meet alle drie met jouw model, tool latency, task set en retry policy voordat je optimaliseert voor het aantal calls.

Een uitgewerkt voorbeeld: de Market Analyst Agent

De Market Analyst Agent maakt het onderscheid concreet. Eén codebase gebruikt alle drie patronen voor market research, en een router kiest tussen een deep-researchpad en een flash-briefingpad. De fragmenten hieronder zijn ingekorte teaching variants van commit b4e769a: de pinned companion vangt een tool exception af en geeft een error string door aan zijn solver, terwijl de worker hier die exception laat afbreken en een exception gooit wanneer zijn dependency graph geen voortgang kan boeken. De route en state shape zijn hetzelfde; de failure policy wordt hier bewust expliciet gemaakt.

De implementatie gebruikt LangGraph voor orchestration. Een node is een Python function die fields retourneert om in shared state bij te werken. Een edge declareert de volgende node en kan een routing function aanroepen. LangGraph voegt updates samen en kan na elke node checkpointen, waardoor een run hervatbaar wordt. De drie patronen delen één state object, zodat routing geen drie afzonderlijke schemas vereist:

De Market Analyst Agent routeert één request naar twee reasoning loops met gedeelde stateDe Market Analyst Agent routeert één request naar twee reasoning loops met gedeelde state

Het diagram isoleert routing en draft creation. De gedeelde evaluator en publish gate die verderop worden getoond, zijn weggelaten zodat de twee reasoning loops leesbaar blijven.

State definition

Het state schema bevat de fields die beide modes nodig hebben:

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: Plan-and-Execute-implementatie

Plan-and-Execute past bij multi-step synthesis. Een planner schrijft de high-level steps, waarna een ReAct-loop elke stap uitvoert en op tool results reageert.

De code hieronder pint claude-sonnet-4-5-20250929 vast; daartegen heb ik deze voorbeelden uitgevoerd toen dit artikel in januari 2026 verscheen. Modelgeneraties zijn sindsdien vervangen. Gebruik wat momenteel beschikbaar is en controleer het routinggedrag opnieuw met je eigen tasks — het patroon is belangrijk, niet het model-ID.

De implementatie houdt vier boundaries zichtbaar:

  1. Eén voorafgaande planning phase. Eén LLM call produceert het volledige plan als een lijst met step descriptions.
  2. Schema-guided output, die het plan valideert vóór execution.
  3. Nog geen tool execution. De planner beslist alleen wat er moet gebeuren, niet hoe.
  4. Human-readable steps. Elke stap is tekst die een executor interpreteert.
# 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
    }

Die regel llm.with_structured_output(PlanOutput) is Schema-Guided Reasoning (SGR), dat ik in een eerder artikel heb behandeld. Dankzij het schema kan de applicatie een malformed plan afwijzen voordat LangGraph de gevalideerde fields gebruikt om conditional edges aan te sturen.

Pattern 2: ReAct execution

Zodra het plan bestaat, voert de executor elke stap uit als een eigen ReAct-loop. Dit is Phase 2: elke stap is klein genoeg om een Thought-Action-Observation-cycle gefocust te houden, en de agent kan reageren op wat de tool teruggeeft.

Zo sluit het ReAct-gedeelte aan:

  1. Iterative execution. Eén stap per keer, met observation feedback.
  2. De Thought-Action-Observation-loop draait binnen create_react_agent.
  3. Results van eerdere stappen worden als context aan de huidige reasoning meegegeven.
  4. De agent kiest tools op basis van de step description.
  5. Hij kan zijn aanpak midden in een stap wijzigen op basis van wat een tool teruggeeft.
# 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 voor snelle snapshots

Voor een snelle briefing verwijdert ReWOO model calls uit de execution phase. Independent tools draaien parallel; dependent tools wachten op hun prerequisites. De planner emit de tool graph vooraf en de worker voert die uit zonder het model te vragen wat de volgende stap is.

De vorm:

  1. Drie phases (Planner → Worker → Solver), zonder loops.
  2. Tool calls verwijzen naar #E1 en #E2, placeholders voor results die nog niet bestaan.
  3. Geen LLM tijdens execution. De worker voert alleen tools uit.
  4. Independent tools draaien parallel.
  5. Eén synthesis call aan het einde, over alle data tegelijk.

Phase 1: ReWOO planner (maakt vooraf de volledige execution graph)

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: ReWOO worker (voert tools uit zonder LLM reasoning)

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: ReWOO solver (synthesiseert alle results in één LLM call)

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())}

De drie phases hebben een eenvoudig contract: de planner maakt executable calls, de worker vult hun placeholders in en de solver ontvangt de ingevulde results. Een unresolved graph gooit een exception voordat de solver draait; een LangGraph retry policy of failure edge kan vervolgens beslissen of er opnieuw moet worden gepland. Dat contract is alleen efficiënt wanneer de aannames van de planner standhouden zodra ze met de tools in aanraking komen.

Waar elk patroon het model aanroept

PatternLLM calls tijdens executionState updatesKey code pattern
Plan-and-Execute1 voor planning + een ReAct-loop per stap (meerdere calls per loop) + 1 voor het reportSequential step completionplanner_node() → loop: executor_node()reporter_node()
ReAct (binnen elke stap)Meerdere per stap (thought-action-cycles)Accumulated message historyPinned companion gebruikt deprecated create_react_agent()
ReWOO1 voor planning + 0 tijdens execution + 1 voor synthesisDependency-aware tool batchesrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

Het belangrijke verschil zit in de output van de planner. Die bepaalt hoeveel discretion de executor behoudt:

  1. Plan-and-Execute maakt human-readable step descriptions:

    # 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
    ]

    De executor leest elke description en beslist welke tools moeten worden aangeroepen. Flexibel, maar elke stap is een eigen ReAct-loop, dus een stap kost meerdere model calls, niet één.

  2. ReAct heeft geen upfront plan. Het gebruikt iterative reasoning:

    # 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
    ]

    In deze implementatie met volledige history leest elke model call opnieuw een history die gedurende de hele taak groeit. Een production loop kan die history samenvatten of trunceren. Plan-and-Execute gebruikt ook ReAct-loops, maar elke loop start met de description van die stap plus een digest van eerdere findings, niet met de volledige tool-call history.

  3. ReWOO maakt expliciete, executable tool call specifications:

    # 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
    ]

    De worker draait blind, zonder LLM-betrokkenheid. Alle model calls zitten in de planner en solver, waardoor het aantal model calls voorspelbaar is.

Memory- en stateflow:

  • Plan-and-Execute: state beweegt via plancurrent_step_indexresearch_data.
  • ReAct: state accumuleert in de messages-array (de volledige conversation history).
  • ReWOO: state beweegt via rewoo_plan, waarbij de worker de fields result invult.

Beide routes in één graph bedraden

De graph heeft twee user-facing routes over één AgentState: deep research gebruikt Plan-and-Execute met binnen elke stap een ReAct-loop, terwijl flash briefing ReWOO gebruikt. ReAct is hier een execution primitive, geen derde route.

Deze implementatie heeft geen replanner: ze voert het initiële plan volledig uit. Replanning toevoegen vereist een edge van executor terug naar planner en een regel voor wanneer een onverwacht result een nieuwe model call rechtvaardigt.

LangGraph houdt de wiring declaratief:

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"],
    )

Automatische patroonselectie met een router

De router koppelt de vorm van de request aan een route. Schema-Guided Reasoning beperkt de output van de 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)

Met deze router gaat “current price” naar ReWOO en “investment thesis” naar Plan-and-Execute. De default is deep research wanneer de request ambigu is. Voordat je de router voor gebruikers inzet, replay je één task set via beide routes en vergelijk je model calls, wall time, failures en recovery behavior.

De volledige companion implementation, inclusief de router en shared state, staat in de pinned Market Analyst Agent-commit.

De volgende laag is memory

Part 2, AI Agent Memory Architecture, scheidt resumable checkpoints van cross-session knowledge en projectdocuments. Zonder die state layer werken de router en executor hierboven alleen zolang één process en één context window actief blijven.

References