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 |
What To Read Next
ReWOO fits many tools with few dependencies.
If calls form a dependency graph, read LLM Compiler. If each step needs observation, read ReAct.