Engineering the Agentic Stack · Teil 1

Reasoning Loops für AI Agents: ReAct, ReWOO, Plan-and-Execute

Automatische Übersetzung Dieser Artikel wurde automatisch aus der englischen Originalversion übersetzt.

Ein Agent Reasoning Loop ist der Kontrollfluss, der entscheidet, wann ein Model plant, ein Tool aufruft, das Ergebnis liest und stoppt. Für Engineers, die einen Agent bauen, ist diese Wahl zugleich eine Budgetentscheidung: Sie bestimmt, wie oft das Model ausgeführt wird, wie viel Verlauf jeder Call mitführt und ob ein überraschendes Ergebnis die nächste Aktion ändern kann.

Dieser Artikel vergleicht ReAct, ReWOO und Plan-and-Execute anhand eines LangGraph Market Analyst Agent, den ich entwickelt habe. Am Ende hast du eine Routing-Regel und übertragbare Implementierungsmuster statt nur drei Namen für ein Diagramm.

Der Loop ist die innerste Schicht dieser Serie. Memory, Tools, Security, Runtime und Acceptance Checks umgeben ihn; sie ersetzen ihn nicht.

Den kurzen Framework-Vergleich findest du unter Best AI Agent Frameworks in 2026.

Der Reasoning Loop entscheidet, was als Nächstes geschieht. Er speichert keinen State, führt keine Tools aus und autorisiert keine Side Effects.

Das Harness ist das Control Program zwischen Model und Maschine. Es setzt Prompts aus gespeichertem State zusammen (Teil 2), definiert die Aktionen, die das Model benennen darf (Teil 3), autorisiert Calls (Teil 4) und prüft die Evidence, bevor es eine Aufgabe als abgeschlossen markiert (Teil 6). Das sind getrennte Engineering-Probleme, aber ein Turn durchläuft alle vier.

Die Runtime (Teil 5) stellt Session Log, Sandbox, Checkpoint Store und Traces bereit, die den Lebenszyklus eines einzelnen Worker-Prozesses überdauern.

Position der einzelnen Teile der Serie „Engineering the Agentic Stack“Position der einzelnen Teile der Serie „Engineering the Agentic Stack“

Jeder Beitrag steht für sich. Zusammen bewegen sie sich vom Loop nach außen.


Beginne mit der Failure Boundary

Ein guter Prompt beantwortet die Control-Flow-Frage nicht. Die Patterns unterscheiden sich darin, wie viel Arbeit vor dem ersten Tool Call feststeht. Das bestimmt die Anzahl der Model Calls, wann ein fehlerhafter Plan sichtbar wird und ob ein unerwartetes Tool Result den Lauf umleiten kann.

Drei Reasoning Patterns für AI Agents

Vergleich von ReAct, ReWOO und Plan-and-Execute danach, wann Evidence den Plan ändern kannVergleich von ReAct, ReWOO und Plan-and-Execute danach, wann Evidence den Plan ändern kann

ReAct: Nach jeder Observation entscheiden

ReAct (Yao et al., 2022), kurz für Reason + Act, hält die nächste Entscheidung nah an der letzten Observation:

  1. Thought: Der Agent erzeugt einen „Thought“, um das Ziel zu zerlegen und den nächsten Schritt zu planen.
  2. Action: Auf Basis des Thoughts ruft er ein Tool auf.
  3. Observation: Der Agent liest das Ergebnis, das sein Verständnis für den nächsten Thought aktualisiert.

ReAct liest jedes Tool Result, bevor die nächste Action gewählt wirdReAct liest jedes Tool Result, bevor die nächste Action gewählt wird

Dadurch erhält ReAct seine nützlichen Eigenschaften:

  • Im manuellen PaLM-540B-HotpotQA-Beispiel des Papers erzeugten Wikipedia-Observations weniger halluzinierte Fakten als Chain-of-Thought Prompting.
  • Der Agent kann seine Strategie auf Basis des gerade Gesehenen dynamisch ändern.
  • Die Historie von Tool Calls und Observations liefert einen konkreten Execution Trace.

Derselbe Loop verursacht jedoch Kosten:

  • In einer naiven Full-History-Implementierung wird der Verlauf bei jedem Schritt erneut verarbeitet, sodass Latency und Kosten mit der Loop-Länge wachsen. Summarization oder Truncation können dieses Wachstum auf Kosten verworfenen Contexts begrenzen.
  • Ineffizient, wenn die Tool Calls im Voraus geplant werden könnten – genau diese Nische füllt ReWOO.
  • Ohne Stop Condition oder Step Limit kann der Loop unendlich laufen.

Verwende ReAct für explorative Aufgaben, Debugging und Arbeiten, bei denen sich die nächste Action nicht vorhersagen lässt.

ReWOO: Den Tool Graph zuerst kompilieren

ReWOO (Reasoning WithOut Observation) trennt Planning und Execution. Der Planner schreibt die vollständige Tool-Sequenz in einem Durchlauf und verwendet Platzhalter für Werte, die erst nach der Ausführung existieren.

  1. Plan: Ein LLM Call schreibt den vollständigen Plan der Tool Calls und verwendet Variablen-Platzhalter (#E1, #E2) für Outputs, die noch nicht existieren.
  2. Worker: Ein Non-LLM Executor führt die geplanten Tools aus und füllt die Platzhalter. Der Worker des Papers folgt dem Plan; die spätere Implementierung in diesem Artikel ergänzt dependency-aware parallele Batches für bereite Schritte.
  3. Solver: Ein abschließender LLM Call übernimmt die gesammelten Observations und schreibt die Antwort.

ReWOO plant den Dependency Graph, bevor die Tools ausgeführt werdenReWOO plant den Dependency Graph, bevor die Tools ausgeführt werden

Diese Trennung bietet:

  • Weniger wiederholte Model Calls als ReAct, wenn der ursprüngliche Plan gültig bleibt.
  • Weniger wiederholte Prompt History als bei einem interleaved Full-History Loop. Die Tool Latency hängt weiterhin davon ab, wie der Worker die Calls plant.
  • Der Planner kann unabhängig Fine-Tuned werden, ohne Live-Umgebung.

Sie erzeugt jedoch eine harte Grenze. Im HotpotQA-Stresstest des Papers lieferte jedes Tool No evidence found zurück; ReWOO verlor weniger Accuracy als ReAct, weil die fehlgeschlagenen Observations den Planner nicht in einen weiteren Loop schickten. Das ist relative Robustness, keine Execution-Recovery-Policy. Eine Implementierung muss weiterhin entscheiden, ob ein Tool Error zum Solver-Evidence wird, einen Retry auslöst oder den Lauf abbricht. ReWOO passt zu vorhersehbaren Workflows; einen fehlerhaften initialen Graph plant es nicht selbstständig neu.

Verwende es für schnelle Snapshots, Status Checks und Dashboards mit vorhersehbarem Tool-Verhalten.

Plan-and-Execute: Zerlegen und lokal reagieren

Plan-and-Solve Prompting beschreibt eine Prompting-Methode, die zunächst einen Plan erstellt und anschließend die Subtasks löst. Ein verwandtes Tool-Orchestration-Pattern wird üblicherweise Plan-and-Execute genannt. Der Plan-and-Execute Guide von LangChain dokumentiert dieses Pattern:

  1. Planning Phase: Der Agent erzeugt zunächst einen Plan, der die Aufgabe in kleinere Subtasks zerlegt.
  2. Execution Phase: Der Agent führt diese Subtasks anschließend nacheinander aus. Sobald Tools beteiligt sind, läuft jeder Subtask gewöhnlich als eigener kleiner ReAct Loop, sodass der Executor weiterhin auf Tool Results reagieren kann, obwohl der übergeordnete Plan feststeht.

Das ursprüngliche Paper konzentrierte sich auf Zero-Shot Prompting. In einer Tool-Using-Implementierung kann das Orchestration Pattern die geplanten Schritte sequenziell ausführen und unterschiedliche Models für Planning und Execution verwenden. Diese Model-Aufteilung ist eine Implementierungsentscheidung und kein Ergebnis, das durch das Plan-and-Solve-Paper belegt wurde.

Plan-and-Execute behält einen übergeordneten Plan bei, während jeder Schritt Feedback nutztPlan-and-Execute behält einen übergeordneten Plan bei, während jeder Schritt Feedback nutzt

Das Pattern ist nützlich, weil es Folgendes bietet:

  • Hierarchical Reasoning, das widerspiegelt, wie ein menschlicher Experte ein Projekt zerlegt.
  • Eine explizite Replanning Edge kann nach einem unerwarteten Step Result pausieren und neu bewerten.
  • Model Specialization: Der Planner kann teuer, der Executor günstig sein.
  • Mit konfiguriertem Checkpointer kann jeder abgeschlossene Schritt zu einer Resume Boundary werden.

Die Kosten sind:

  • Mehr Model Round Trips als bei ReWOO, wenn jeder Schritt seinen eigenen ReAct Loop enthält.
  • Mehr zu verwaltender State.
  • Overkill für One-Shot Queries.

Verwende es für komplexe Analysis und Research, die eine abschließende Synthesis benötigen.

Wähle danach, wo der Plan scheitern kann

FeatureReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Core PhilosophyImproviser: Die nächste Action wird aus dem letzten Result abgeleitet, ein Call nach dem anderen.Architect: Einen vollständigen Blueprint erstellen, ausführen und anschließend prüfen.Optimizer: Einen Dependency Graph kompilieren und anschließend die bereiten Calls batchen.
WorkflowIterativer Loop: Thought → Action → Observation.Zwei Stufen: Phase 1 (Planning), Phase 2 (Execution).Entkoppelt: Der Planner schreibt einen Graph aus Tool Calls; der Worker führt sie aus; der Solver setzt die Antwort zusammen.
AdaptabilityAm höchsten: Kann nach jedem einzelnen Tool Call die Richtung ändern.Mittel: Replant typischerweise erst nach Abschluss einer Gruppe von Schritten.Am niedrigsten: Das Script des Planners läuft bis zum Ende; während des Laufs wird nicht neu geplant.
EfficiencyEin naiver Full-History Loop wiederholt mehr Input Tokens; Context Management kann dieses Wachstum begrenzen.Replanning erfolgt gelegentlich statt pro Schritt, und jeder Step-ReAct-Loop kann mit kurzem Context statt mit der History des gesamten Laufs beginnen.Weniger Model Calls; der Worker dieses Artikels batcht außerdem dependency-ready Tools.
Best forOffen-ended Exploration oder Aufgaben mit unvorhersehbaren Results.Long-Horizon Tasks mit stabilem Ziel (z. B. das Schreiben eines Papers).Strukturierte, wiederholbare Workflows (z. B. das Prüfen des Wetters in fünf Städten).

Die Tabelle dient als Routing-Hilfe, nicht als Benchmark. Verwende ReAct, wenn eine Observation die nächste Action ändern kann. Verwende Plan-and-Execute, wenn sich die Aufgabe sauber zerlegen lässt, aber jeder Schritt weiterhin Feedback benötigt. Verwende ReWOO, wenn jede Tool Dependency vor der Ausführung bekannt ist. In der folgenden Teaching-Implementierung ermöglicht dieser Dependency Graph parallele Batches und lässt den Worker einen Graph erkennen, der nicht weiter vorankommt. Sein execute_tool Helper ist bewusst Fail-Fast; Production Code benötigt eine explizite Retry-, Fallback- oder Error-as-Evidence-Policy. Miss alle drei Patterns mit deinem Model, deiner Tool Latency, deinem Task Set und deiner Retry Policy, bevor du auf die Anzahl der Calls optimierst.

Ein durchgängiges Beispiel: der Market Analyst Agent

Der Market Analyst Agent macht den Unterschied greifbar. Eine Codebase verwendet alle drei Patterns für Market Research, und ein Router entscheidet zwischen einem Deep-Research-Pfad und einem Flash-Briefing-Pfad. Die folgenden Ausschnitte sind gekürzte Teaching-Varianten von Commit b4e769a: Der gepinnte Companion fängt eine Tool Exception ab und übergibt dem Solver einen Error String, während der hier gezeigte Worker diese Exception den Lauf abbrechen lässt und einen Fehler auslöst, wenn sein Dependency Graph keinen Fortschritt machen kann. Route und State Shape sind identisch; die Failure Policy ist hier bewusst explizit.

Er verwendet LangGraph für die Orchestration. Ein Node ist eine Python-Funktion, die Felder zurückgibt, die im gemeinsamen State aktualisiert werden sollen. Eine Edge definiert den nächsten Node und kann eine Routing-Funktion aufrufen. LangGraph führt Updates zusammen und kann nach jedem Node Checkpoints erstellen, wodurch ein Lauf resumable wird. Die drei Patterns teilen sich ein State Object, sodass das Routing keine drei separaten Schemas benötigt:

Der Market Analyst Agent routet eine Anfrage über einen gemeinsamen State in zwei Reasoning LoopsDer Market Analyst Agent routet eine Anfrage über einen gemeinsamen State in zwei Reasoning Loops

Das Diagramm isoliert Routing und Draft Creation. Der später gezeigte gemeinsame Evaluator und Publish Gate sind ausgeblendet, damit die beiden Reasoning Loops übersichtlich bleiben.

State Definition

Das State Schema enthält die Felder, die beide Modi benötigen:

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-Implementierung

Plan-and-Execute eignet sich für Multi-Step Synthesis. Ein Planner schreibt die übergeordneten Schritte, anschließend führt ein ReAct Loop jeden Schritt aus und reagiert auf Tool Results.

Der folgende Code pinnt claude-sonnet-4-5-20250929. Darauf habe ich diese Beispiele ausgeführt, als der Beitrag im Januar 2026 veröffentlicht wurde. Model-Generationen haben sich seitdem weiterentwickelt. Tausche es gegen ein aktuelles Model aus und prüfe das Routing-Verhalten mit deinen eigenen Tasks erneut – entscheidend ist das Pattern, nicht die Model ID.

Die Implementierung macht vier Boundaries sichtbar:

  1. Eine vorgelagerte Planning Phase. Ein einzelner LLM Call erzeugt den vollständigen Plan als Liste von Step Descriptions.
  2. Schema-Guided Output, der den Plan vor der Ausführung validiert.
  3. Noch keine Tool Execution. Der Planner entscheidet nur, was zu tun ist, nicht wie.
  4. Menschenlesbare Steps. Jeder Step ist Text, den ein Executor interpretieren wird.
# 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 Zeile llm.with_structured_output(PlanOutput) steht für Schema-Guided Reasoning (SGR), das ich in einem früheren Beitrag behandelt habe. Das Schema ermöglicht es der Anwendung, einen fehlerhaften Plan zurückzuweisen, bevor LangGraph die validierten Felder zur Steuerung Conditional Edges verwendet.

Pattern 2: ReAct Execution

Sobald der Plan existiert, führt der Executor jeden Step als eigenen ReAct Loop aus. Das ist Phase 2: Jeder Step ist klein genug, damit ein Thought-Action-Observation-Zyklus fokussiert bleibt, und der Agent kann auf beliebige Tool Results reagieren.

So ist der ReAct-Teil aufgebaut:

  1. Iterative Execution: ein Step nach dem anderen mit Observation Feedback.
  2. Der Thought-Action-Observation Loop läuft innerhalb von create_react_agent.
  3. Results vorheriger Steps werden als Context für das aktuelle Reasoning eingespeist.
  4. Der Agent wählt Tools anhand der Step Description.
  5. Er kann seine Vorgehensweise innerhalb eines Steps auf Basis eines Tool Results ändern.
# 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 für schnelle Snapshots

Für ein kurzes Briefing entfernt ReWOO die Model Calls aus der Execution Phase. Unabhängige Tools laufen parallel; abhängige Tools warten auf ihre Prerequisites. Der Planner gibt den Tool Graph im Voraus aus, und der Worker führt ihn aus, ohne das Model zu fragen, was als Nächstes zu tun ist.

Das Grundmuster:

  1. Drei Phasen (Planner → Worker → Solver), keine Loops.
  2. Tool Calls referenzieren #E1, #E2 als Platzhalter für Results, die noch nicht existieren.
  3. Kein LLM während der Execution. Der Worker führt lediglich Tools aus.
  4. Unabhängige Tools laufen parallel.
  5. Ein Synthesis Call am Ende über alle Daten auf einmal.

Phase 1: ReWOO Planner (erstellt den vollständigen Execution Graph im Voraus)

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 (führt Tools ohne LLM Reasoning aus)

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 (synthetisiert alle Results in einem 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())}

Die drei Phasen haben einen einfachen Contract: Der Planner erstellt ausführbare Calls, der Worker füllt deren Platzhalter, und der Solver erhält die vervollständigten Results. Ein nicht auflösbarer Graph löst einen Fehler aus, bevor der Solver läuft; eine LangGraph Retry Policy oder Failure Edge kann dann entscheiden, ob neu geplant wird. Dieser Contract ist nur dann effizient, wenn die Annahmen des Planners den Kontakt mit den Tools überstehen.

Wo jedes Pattern das Model aufruft

PatternLLM Calls während der ExecutionState UpdatesKey Code Pattern
Plan-and-Execute1 für Planning + ein ReAct Loop pro Step (jeweils mehrere Calls) + 1 für den ReportSequenzielle Step Completionplanner_node() → Loop: executor_node()reporter_node()
ReAct (innerhalb jedes Steps)Mehrere pro Step (Thought-Action-Zyklen)Akkumulierte Message HistoryDer gepinnte Companion verwendet das deprecated create_react_agent()
ReWOO1 für Planning + 0 während der Execution + 1 für die SynthesisDependency-aware Tool Batchesrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

Der entscheidende Unterschied liegt im Output des Planners. Er bestimmt, wie viel Discretion der Executor behält:

  1. Plan-and-Execute erstellt menschenlesbare 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
    ]

    Der Executor liest jede Description und entscheidet, welche Tools aufzurufen sind. Flexibel, aber jeder Step ist ein eigener ReAct Loop und kostet daher mehrere Model Calls, nicht nur einen.

  2. ReAct besitzt keinen vorgelagerten Plan. Es verwendet iteratives 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 dieser Full-History-Implementierung liest jeder Model Call eine History erneut, die während der gesamten Aufgabe wächst. Ein Production Loop kann sie zusammenfassen oder kürzen. Plan-and-Execute verwendet ebenfalls ReAct Loops, aber jeder startet mit der Description des jeweiligen Steps plus einem Digest früherer Findings und nicht mit der vollständigen Tool-Call-History.

  3. ReWOO erstellt explizite, ausführbare Tool-Call-Spezifikationen:

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

    Der Worker arbeitet blind, ohne Beteiligung eines LLM. Alle Model Calls liegen im Planner und Solver, wodurch die Anzahl der Model Calls vorhersehbar wird.

Memory und State Flow:

  • Plan-and-Execute: State bewegt sich durch plancurrent_step_indexresearch_data.
  • ReAct: State akkumuliert im Array messages (der vollständigen Conversation History).
  • ReWOO: State bewegt sich durch rewoo_plan, wobei der Worker die Felder result ausfüllt.

Beide Routes in einen Graph integrieren

Der Graph stellt zwei User-facing Routes über ein gemeinsames AgentState bereit: Deep Research verwendet Plan-and-Execute mit einem ReAct Loop innerhalb jedes Steps, während Flash Briefing ReWOO verwendet. ReAct ist hier ein Execution Primitive und keine dritte Route.

Diese Implementierung besitzt keinen Replanner: Sie führt den initialen Plan bis zum Ende aus. Replanning würde eine Edge von executor zurück zu planner und eine Regel erfordern, wann ein überraschendes Result einen weiteren Model Call rechtfertigt.

LangGraph hält die Verdrahtung deklarativ:

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 Pattern-Auswahl mit einem Router

Der Router ordnet die Request-Struktur einer Route zu. Schema-Guided Reasoning begrenzt den Output des Classifiers:

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)

Mit diesem Router wird „current price“ an ReWOO und „investment thesis“ an Plan-and-Execute geroutet. Bei mehrdeutigen Requests ist Deep Research der Default. Bevor du den Router vor User schaltest, spiele ein Task Set durch beide Routes und vergleiche Model Calls, Wall Time, Failures und Recovery-Verhalten.

Die vollständige Companion-Implementierung einschließlich Router und gemeinsamem State findest du im gepinnten Market Analyst Agent Commit.

Die nächste Schicht ist Memory

Teil 2, AI Agent Memory Architecture, trennt resumable Checkpoints von Wissen über mehrere Sessions hinweg und von Project Documents. Ohne diese State-Schicht funktionieren Router und Executor oben nur so lange, wie ein Prozess und ein Context Window aktiv bleiben.

References