Skip to content

ReWOO: Plan Tool Calls, Then Observe In Batch

ReAct asks the model after every observation: what next?

That is flexible, but slow. If the travel assistant needs weather, exchange rate, opening hours, and transit times, and those calls do not depend on each other, step-by-step ReAct wastes round trips.

ReWOO plans tool calls first, executes them in batch, then lets the model read all observations and answer.

One Sentence

ReWOO turns observe-then-decide into plan tool calls, execute observations, then solve, reducing model round trips when tool calls are mostly independent.

What Breaks Without It

Problem What it looks like Risk
ReAct asks after every tool Flexible Slow with many tools
Independent calls run serially Simple Latency grows
Observations lack purpose Model can answer Hard to know why each call exists

What This Pattern Changes

Who Owns
Planner model Outputs tool call list
Python Executes tools in batch and handles failures
Solver model Reads observations and writes answer

It fits independent tool calls. It does not fit tasks where each observation changes the next action.

Walk Through One Trace

Stage Content Output
Plan Need add(2,2) Tool call list
Observe Python runs add 4
Solve Model reads observation Answer: 4

For travel, weather, opening hours, and transit times can often be queried in one batch if they do not depend on each other.

Flow

flowchart TD
  T["Task"] --> P["Plan tool call list"]
  P --> E["Python executes tools in batch"]
  E --> O["Collect observations"]
  O --> S["Model writes answer"]

Code Walk

The example tool is small:

def add(args: dict) -> str:
    return str(int(args["a"]) + int(args["b"]))

The model first returns a tool plan:

model = MockLLM(
    [
        '{"tool_calls":[{"tool":"add","args":{"a":2,"b":2},"purpose":"compute 2+2"}]}',
        "Answer: 4",
    ]
)

Full example:

from __future__ import annotations

from pathlib import Path

from agent_patterns_lab.patterns.rewoo import rewoo
from agent_patterns_lab.runtime import MockLLM, Tool, ToolRegistry, Tracer


def main() -> None:
    tracer = Tracer()

    def add(args: dict) -> str:
        return str(int(args["a"]) + int(args["b"]))

    tools = ToolRegistry([Tool(name="add", description="Add two integers", handler=add)])

    model = MockLLM(
        [
            '{"tool_calls":[{"tool":"add","args":{"a":2,"b":2},"purpose":"compute 2+2"}]}',
            "Answer: 4",
        ]
    )

    result = rewoo(model, task="Compute 2+2.", tools=tools, tracer=tracer)
    print(result.answer)

    trace_path = tracer.export_jsonl(Path(".traces") / "52_rewoo.jsonl")
    print(f"[trace] {trace_path}")


if __name__ == "__main__":
    main()

Run:

UV_CACHE_DIR=.uv_cache PYTHONPATH=src uv run --no-sync python examples/52_rewoo.py

Nearby Patterns

Pattern Who decides next Use when
ReAct Decide after each observation Strong dependencies
ReWOO Plan calls before observations Calls can be planned early
LLM Compiler Build dependency task graph Dependencies and parallelism matter
Workflow Python fixes steps Steps are fully known

When To Use It

  • Many tool calls are independent.
  • Model round trips dominate latency.
  • Planning before observing is acceptable.
  • Partial failure handling is defined.

When Not To Use It

  • Each next step depends on previous observation.
  • Tool results may change the whole task.
  • Tool plans are often wrong.
  • Partial failures have no handling path.

Costs And Common Failures

Failure Symptom Fix
Bad plan Irrelevant tools run Keep plans short, allow second batch
No mid-course correction Missing info but still answers Fallback to ReAct
Tool failure One failure spoils batch Per-tool retry and partial summaries
Too many observations Solver prompt bloats Summarize and preserve purpose

ReWOO fits many tools with few dependencies.

If calls form a dependency graph, read LLM Compiler. If each step needs observation, read ReAct.

References