Reasoning Paradigms¶
Hands-on
Full runnable recipe: reasoning-paradigms/ in the companion cookbook — Reflexion, ReWOO vs. ReAct measured on real call count and token count, LLM Compiler's concurrent dispatch, and Plan-and-Solve vs. zero-shot CoT, each with a real captured run.
TL;DR — quick revision
- These are a different axis from workflow patterns. Workflow patterns fix the topology (which steps exist, in what order). Reasoning paradigms change how a single step or loop reasons, searches, or recovers from its own mistakes — they're about the shape of thinking, not the shape of the pipeline.
- Plan-and-Solve is a zero-shot prompting technique, not a planner/executor architecture — it's one prompt ("first devise a plan, then carry it out") compared against plain "let's think step by step." Don't confuse it with LangChain's separately-named Plan-and-Execute agent, which actually is an architecture.
- ReWOO's real, verified advantage is token and call efficiency, not latency — the paper never claims a latency win. A real measured run on this page needed 2.5x the calls and 6.8x the tokens for ReAct versus ReWOO on the identical question and model.
- ReWOO's plan-then-execute split has a real failure surface ReAct doesn't: a bad step in the plan is only caught once it's actually executed, not before. This page's real run hit exactly that — a generated plan referenced a function its tool didn't support, and one step genuinely failed.
- Reasoning models changed what needs to be built by hand. DeepSeek-R1's reported finding is that self-reflection and verification can emerge from pure reinforcement learning, without anyone hand-coding a Reflexion-style retry loop or a ToT-style external search — some of what these 2023 papers built as scaffolding, later training runs learned to do internally.
Why these are a different axis from workflow patterns¶
Workflow Patterns covered five ways to wire fixed pipelines: chaining, routing, parallelization, orchestrator-workers, evaluator-optimizer. Every one of those fixes the sequence of steps ahead of time — the LLM fills in judgment calls inside a shape the developer already committed to in code.
The paradigms on this page are a different axis entirely: they're about what happens inside a single reasoning step or loop, independent of pipeline shape. ReAct is about how one agent loop decides its next action. Reflexion is about what happens after that loop fails. Tree of Thoughts is about exploring more than one line of reasoning before committing to an answer. None of them describe a pipeline topology — they describe a reasoning strategy, and several of them (ReWOO, ReAct) could sit inside any of last page's workflow patterns as the mechanism one step uses internally.
ReAct: the loop you've already built¶
The Agent Loop From Scratch is a ReAct-style loop — the paper's own framing is generating reasoning traces and actions in an interleaved manner, where the reasoning helps the model track its plan and handle exceptions, and the actions let it reach outside the model into tools or an environment. Nothing new to build here; that page's real captured tool-calling loop is the working reference. The paper (Yao et al., ICLR 2023) reported a 34-percentage-point absolute success-rate improvement over imitation and reinforcement-learning baselines on ALFWorld, and a 10-point improvement on WebShop — evidence for interleaving reasoning and acting specifically, not for any particular provider's tool-calling API, which didn't exist yet in 2022 when this was written.
Reflexion: solve, verify, reflect, retry¶
Reflexion agents verbally reflect on a failure signal, then keep that reflection in an episodic memory buffer to inform the next attempt — no gradient update, no fine-tuning, just text carried forward into the next try. On HumanEval, the paper reports 91% pass@1, ahead of the GPT-4 baseline's reported 80% at the time (Shinn et al., 2023).
import re
def _call(model, prompt, max_tokens=500):
# client.messages.create(...) -- see agent_loop.py for the full call shape.
...
_PEOPLE = ["Nadia", "Omar", "Priya"]
_SWAPS = [
("Nadia", "Omar"), ("Omar", "Priya"), ("Nadia", "Priya"),
("Nadia", "Omar"), ("Omar", "Priya"), ("Nadia", "Priya"),
("Nadia", "Omar"), ("Omar", "Priya"), ("Nadia", "Priya"),
]
def simulate_swaps() -> dict:
"""The ground truth, computed independently of any model."""
state = {"Nadia": "red", "Omar": "blue", "Priya": "green"}
for a, b in _SWAPS:
state[a], state[b] = state[b], state[a]
return state
def _swap_question() -> str:
lines = [f"{a} and {b} swap whatever key they are currently holding." for a, b in _SWAPS]
return (
"Nadia starts holding the red key, Omar starts holding the blue key, "
"and Priya starts holding the green key. Then, in this exact order:\n"
+ "\n".join(f"{i+1}. {line}" for i, line in enumerate(lines))
+ "\n\nAfter all the swaps, who holds the red key? "
"End your answer with a final line in the exact form: Final answer: <name>"
)
def _extract_name(text: str) -> str:
match = re.search(r"Final answer:\s*(\w+)", text)
return match.group(1) if match else ""
def reflexion_demo(client, model) -> dict:
"""Solve, check against a local (non-LLM) verifier, and only if wrong,
reflect in words and retry with the reflection in context."""
ground_truth = simulate_swaps()
correct_holder = [name for name, key in ground_truth.items() if key == "red"][0]
question = _swap_question()
attempt_1_text = _call(model, question, max_tokens=600)
attempt_1_answer = _extract_name(attempt_1_text)
if attempt_1_answer == correct_holder:
return {"ground_truth": correct_holder, "attempt_1": {"answer": attempt_1_answer, "correct": True}, "reflection": None}
reflection_prompt = (
f"You were asked:\n\n{question}\n\nYou answered:\n\n{attempt_1_text}\n\n"
"That answer is INCORRECT. Without being told the right answer, reflect in "
"2-3 sentences on what likely went wrong in how you tracked the swaps, and "
"what you would do differently to avoid that mistake."
)
reflection_text = _call(model, reflection_prompt, max_tokens=300)
retry_prompt = (
f"{question}\n\nBefore answering, here is a reflection on a previous incorrect "
f"attempt at this exact problem: {reflection_text}\n\nNow answer carefully, applying that reflection."
)
attempt_2_text = _call(model, retry_prompt, max_tokens=600)
attempt_2_answer = _extract_name(attempt_2_text)
return {
"ground_truth": correct_holder,
"attempt_1": {"answer": attempt_1_answer, "correct": False},
"reflection": reflection_text,
"attempt_2": {"answer": attempt_2_answer, "correct": attempt_2_answer == correct_holder},
}
A real run — input and output shown in full
Input — the exact question sent to the model, a 9-swap state-tracking puzzle with a locally-computed (non-LLM) ground truth: "Nadia starts holding the red key, Omar starts holding the blue key, and Priya starts holding the green key. Then, in this exact order: 1. Nadia and Omar swap whatever key they are currently holding. 2. Omar and Priya swap whatever key they are currently holding. 3. Nadia and Priya swap whatever key they are currently holding." — repeated in that same 3-swap pattern three times (9 swaps total), then: "After all the swaps, who holds the red key? End your answer with a final line in the exact form: Final answer:
Output — attempt 1, Claude Haiku 4.5, correct on the first try:
I'll track which key each person holds after each swap.
Initial state: Nadia: Red, Omar: Blue, Priya: Green
After swap 1 (Nadia and Omar swap): Nadia: Blue, Omar: Red, Priya: Green
After swap 2 (Omar and Priya swap): Nadia: Blue, Omar: Green, Priya: Red
(...six more swaps, each stated explicitly...)
After swap 9 (Nadia and Priya swap): Nadia: Red, Omar: Green, Priya: Blue
Final answer: Nadia
Ground truth (computed independently, by literally simulating the swaps in Python, not by the model): Nadia. Correct — no reflection needed, so none fired.
An honest result, not a weaker one: the failure this mechanism exists to catch didn't occur on this run. What's genuinely interesting is why it didn't — the model wrote out the full intermediate state after every single swap instead of trying to hold nine sequential updates in its head, which is exactly the kind of externalized bookkeeping that avoids the state-tracking errors this task was designed to risk. That's a real, checkable finding about how it avoided the failure, not just that it did.
Plan-and-Solve: a prompt, not an architecture¶
The seed interview-prep material this blog started from described Plan-and-Solve as an early planner/executor split. That's not what the paper is: it's a zero-shot prompting technique — one instruction, "let's first understand the problem and devise a complete plan, then carry out the plan and solve the problem step by step," compared against the plainer "let's think step by step" (zero-shot chain-of-thought). The paper's target is zero-shot-CoT's three failure modes — calculation errors, missing-step errors, and semantic misunderstanding — and it reports that Plan-and-Solve "consistently outperforms Zero-shot-CoT across all datasets by a large margin" and reaches performance "comparable with 8-shot CoT prompting" on math reasoning (Wang et al., ACL 2023). Don't confuse this with LangChain's separately-named Plan-and-Execute agent, which genuinely is a two-role architecture — same words, different thing, published two months apart.
_WORD_PROBLEM = (
"A bakery bakes 320 loaves of bread each morning. It sells 3/8 of the loaves by noon, "
"then donates 20 loaves to a shelter, then sells half of what remains by evening. "
"The rest goes into the discount bin overnight. Of the loaves in the discount bin, "
"20% are stale and thrown out the next morning. How many loaves actually make it to "
"the discount bin's shelf for sale?"
)
def plan_and_solve_demo(client, model) -> dict:
"""The same word problem, two prompts: plain zero-shot chain-of-thought
versus an explicit plan-then-solve instruction. Plan-and-Solve is a
prompting technique, not a multi-agent architecture -- this is the
entire mechanism, one prompt string."""
zs_cot_text = _call(model, f"{_WORD_PROBLEM}\n\nLet's think step by step.", max_tokens=500)
ps_prompt = (
f"{_WORD_PROBLEM}\n\n"
"Let's first understand the problem and devise a complete plan. Then, let's carry "
"out the plan and solve the problem step by step."
)
ps_text = _call(model, ps_prompt, max_tokens=500)
return {"zero_shot_cot": zs_cot_text, "plan_and_solve": ps_text}
A real run — same problem, two prompts, both shown in full
Input — the exact word problem, identical for both prompts: "A bakery bakes 320 loaves of bread each morning. It sells 3/8 of the loaves by noon, then donates 20 loaves to a shelter, then sells half of what remains by evening. The rest goes into the discount bin overnight. Of the loaves in the discount bin, 20% are stale and thrown out the next morning. How many loaves actually make it to the discount bin's shelf for sale?" Ground truth, computed independently: 72 loaves.
Output — zero-shot-CoT ("Let's think step by step."), Claude Haiku 4.5:
Step 1: Calculate loaves sold by noon — 3/8 × 320 = 120 loaves. Step 2: Remaining = 320 − 120 = 200 loaves. Step 3: After donating 20 loaves = 200 − 20 = 180 loaves. Step 4: Sold by evening = 1/2 × 180 = 90 loaves. Step 5: Remaining = 180 − 90 = 90 loaves. Step 6: Stale loaves = 20% × 90 = 18 loaves. Step 7: Available for sale = 90 − 18 = 72 loaves
Output — Plan-and-Solve, same model:
Problem Analysis and Plan — Let me break down what happens to the 320 loaves throughout the day: 1. By noon: sell 3/8 of loaves 2. After donation: donate 20 loaves to shelter 3. By evening: sell half of what remains 4. Overnight: remaining loaves go to discount bin 5. Next morning: 20% of discount bin loaves are stale and thrown out.
Step-by-Step Solution — Step 1: Sold by noon: 3/8 × 320 = 120, remaining 200. Step 2: After donation: 200 − 20 = 180. Step 3: Sold by evening: 1/2 × 180 = 90, remaining 90. Step 4: Discount bin receives 90. Step 5: Stale: 20% × 90 = 18. Step 6: Available for sale: 90 − 18 = 72 loaves
Both correct: 72. An honest negative result for this specific comparison — the paper's claim is distributional (it wins "across all datasets," averaged over many problems), not a guarantee on any single one, and this one problem didn't distinguish the two prompts. The real difference in the transcripts isn't the answer, it's structure: Plan-and-Solve produced an explicit, separated planning section before any arithmetic began, while zero-shot-CoT interleaved planning and computing from the first line.
ReWOO: plan once, execute deterministically¶
ReWOO's own abstract frames the problem directly: in an interleaved loop, "an LLM reasons to call an external tool, gets halted to fetch the tool's response, and then decides the next action based on all preceding response tokens" — a pattern that "often leads to huge computation complexity from redundant prompts and repeated execution" (Xu et al., 2023). ReWOO's fix: one planning call produces the entire plan up front, using #E1, #E2-style variables so a later step can reference an earlier step's result before that result exists yet — then the plan executes deterministically, with real values substituted in, and a final call solves from the gathered evidence. Two LLM calls, no matter how many tool steps the plan has. The paper reports "5x token efficiency and 4% accuracy improvement on HotpotQA" against the interleaved baseline — a token and accuracy claim, notably not a latency one.
import ast
import json
import operator
_SAFE_OPS = {
ast.Add: operator.add, ast.Sub: operator.sub,
ast.Mult: operator.mul, ast.Div: operator.truediv,
ast.Pow: operator.pow, ast.USub: operator.neg,
}
def _safe_eval(node):
if isinstance(node, ast.Expression):
return _safe_eval(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in _SAFE_OPS:
return _SAFE_OPS[type(node.op)](_safe_eval(node.left), _safe_eval(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in _SAFE_OPS:
return _SAFE_OPS[type(node.op)](_safe_eval(node.operand))
raise ValueError(f"Disallowed expression: {ast.dump(node)}")
def calculator(expression: str) -> dict:
# Deliberately supports only +, -, *, /, ** and parentheses -- no function
# calls like round(). See this page's real run for what happens when a
# generated plan assumes more than the tool actually supports.
try:
return {"result": _safe_eval(ast.parse(expression, mode="eval"))}
except Exception as e:
return {"error": f"Could not evaluate '{expression}': {e}"}
_COLONY_RECORDS = {"Meridian Station": 48200, "Halcyon Outpost": 15750, "Kepler Reach": 92400}
def lookup_population(entity: str) -> dict:
if entity not in _COLONY_RECORDS:
return {"error": f"No record for '{entity}'"}
return {"result": _COLONY_RECORDS[entity]}
def _parse_json(raw: str):
import re
match = re.search(r"[\[{].*[\]}]", raw, re.S)
if not match:
return None
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
return None
QUESTION = (
"Using the colony records, what is the combined population of Meridian Station "
"and Halcyon Outpost, expressed as a percentage of Kepler Reach's population? "
"Round to 2 decimal places."
)
def rewoo_demo(client, model) -> dict:
"""Plan every tool call up front, with variable substitution across steps
(1 call) -- execute the plan deterministically (0 calls) -- solve from the
gathered evidence (1 call). Total: 2 LLM calls, however many tool steps."""
plan_prompt = (
f"Question: {QUESTION}\n\n"
"Produce a plan as a JSON list of steps needed to answer it, using these tools: "
"lookup_population(entity), calculator(expression). Each step is an object with "
'"var" (e.g. "E1"), "tool", and "args" (an object). A later step may reference an '
'earlier result by writing its var name (e.g. "E1") inside a string argument -- '
"it will be substituted with that step's actual result before execution. "
"Output ONLY the JSON list, no prose."
)
plan_text = _call(model, plan_prompt, max_tokens=500)
plan = _parse_json(plan_text) or []
evidence = {}
for step in plan:
args = dict(step["args"])
for key, value in args.items():
if isinstance(value, str):
for var, result in evidence.items():
value = value.replace(var, str(result))
args[key] = value
if step["tool"] == "lookup_population":
out = lookup_population(args["entity"])
elif step["tool"] == "calculator":
out = calculator(args["expression"])
else:
out = {"error": f"unknown tool {step['tool']}"}
evidence[step["var"]] = out.get("result", out.get("error"))
solve_prompt = (
f"Question: {QUESTION}\n\nEvidence gathered:\n{json.dumps(evidence, indent=2)}\n\n"
"Using only this evidence, give the final numeric answer."
)
answer_text = _call(model, solve_prompt, max_tokens=200)
return {"plan": plan, "evidence": evidence, "answer": answer_text}
A real run — the exact question, the real generated plan, and a real partial failure
Input — the question, sent once, to a planner: "Using the colony records, what is the combined population of Meridian Station and Halcyon Outpost, expressed as a percentage of Kepler Reach's population? Round to 2 decimal places." Fictional records the tools read from: Meridian Station 48,200; Halcyon Outpost 15,750; Kepler Reach 92,400.
Output — the plan, generated by Claude Sonnet 5 in one call, verbatim:
[
{"var": "E1", "tool": "lookup_population", "args": {"entity": "Meridian Station"}},
{"var": "E2", "tool": "lookup_population", "args": {"entity": "Halcyon Outpost"}},
{"var": "E3", "tool": "lookup_population", "args": {"entity": "Kepler Reach"}},
{"var": "E4", "tool": "calculator", "args": {"expression": "E1 + E2"}},
{"var": "E5", "tool": "calculator", "args": {"expression": "(E4 / E3) * 100"}},
{"var": "E6", "tool": "calculator", "args": {"expression": "round(E5, 2)"}}
]
Executing that plan deterministically, with real values substituted for each #E reference, produced this evidence: E1=48200, E2=15750, E3=92400, E4=63950, E5=69.20995670995671, and then E6 genuinely failed: "Could not evaluate 'round(69.20995670995671, 2)': Disallowed expression" — this recipe's calculator, like agent-loop-from-scratch's, is an AST walk that only allows + - * / **, deliberately not function calls. The model's plan assumed a capability the tool didn't have. This is exactly the failure surface the mechanism itself creates: a bad step is only caught once the plan actually runs, because nothing checks the plan against the tool's real capabilities before execution starts — unlike ReAct below, where the model sees each real result before deciding the next action.
Output — the final solve call, given that evidence (including E6's error) and nothing else: "69.21". Correct, and recovered despite one step failing — the solve step reasoned from E5's raw unrounded value once E6 came back unusable, rather than the whole run failing.
ReAct vs. ReWOO, measured¶
Same question, same model (Sonnet), same correct final answer — call count and token count measured, not estimated:
| Calls | Total tokens | |
|---|---|---|
| ReWOO | 2 | 756 |
| ReAct | 5 | 5,141 |
def react_demo(client, model, tools, max_steps=6) -> dict:
"""Classic ReAct: one decision at a time, based on the latest observation.
Deliberately executes only the FIRST tool call per turn even if the model
requests several, so native parallel tool-calling can't quietly collapse
this into fewer turns -- that would test the API, not ReAct's actual
interleaved-decision mechanism."""
messages = [{"role": "user", "content": QUESTION}]
trace = []
calls = 0
for step in range(1, max_steps + 1):
response = client.messages.create(model=model, max_tokens=500, tools=tools, messages=messages)
calls += 1
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
final_text = "".join(b.text for b in response.content if b.type == "text")
return {"answer": final_text, "trace": trace, "calls": calls}
blocks = [b for b in response.content if b.type == "tool_use"]
first, rest = blocks[0], blocks[1:]
result = lookup_population(**first.input) if first.name == "lookup_population" else calculator(**first.input)
trace.append({"step": step, "tool": first.name, "arguments": first.input, "result": result})
tool_results = [{"type": "tool_result", "tool_use_id": first.id, "content": json.dumps(result)}]
for extra in rest:
tool_results.append({"type": "tool_result", "tool_use_id": extra.id, "content": json.dumps({"error": "Not run this turn."})})
messages.append({"role": "user", "content": tool_results})
return {"answer": "(budget exhausted)", "trace": trace, "calls": calls}
A real run — ReAct's turn-by-turn trace
Methodology, disclosed plainly: this ReAct loop deliberately executes only the first requested tool call per turn, even on a turn where the model requests several — otherwise Claude's native parallel tool-calling could quietly collapse several ReAct "steps" into one API call, which would measure the API's own batching rather than ReAct's actual interleaved-decision mechanism.
Real trace, one call per line: turn 1 → lookup_population("Meridian Station") → 48200. Turn 2 → lookup_population("Halcyon Outpost") → 15750. Turn 3 → lookup_population("Kepler Reach") → 92400. Turn 4 → calculator("(48200 + 15750) / 92400 * 100") → 69.20995670995671. Turn 5 → final answer: "The combined population of Meridian Station (48,200) and Halcyon Outpost (15,750) is 63,950, which is 69.21% of Kepler Reach's population (92,400)."
5 calls, 5,141 total tokens against ReWOO's 2 calls, 756 total tokens — 2.5x the calls, 6.8x the tokens, for the identical question, model, and final answer. The gap between the call-count ratio (2.5x) and the token ratio (6.8x) is itself the finding: ReAct's transcript grows every turn and gets resent in full on the next call, so token cost compounds faster than call count does — precisely the "redundant prompts and repeated execution" ReWOO's own abstract names as the thing it's fixing.
LLM Compiler: parallel dispatch over the plan¶
ReWOO's plan above executes in written order regardless of whether a step actually depends on an earlier one — E1, E2, and E3 don't depend on each other at all, yet nothing in ReWOO dispatches them concurrently. LLM Compiler's contribution is exactly that gap: a planner that produces an execution graph, a dispatch unit that starts any task whose dependencies are already satisfied, and an executor running independent tasks in parallel (Kim et al., ICML 2024). The paper reports "latency speedup of up to 3.7x," "cost savings of up to 6.7x," and "accuracy improvement of up to ~9%" against a ReAct-style baseline across their benchmark suite.
import concurrent.futures
import time
def llm_compiler_demo(plan: list) -> dict:
"""The same plan ReWOO produced, executed differently: steps with no
dependency on each other's results dispatch concurrently instead of
strictly in written order."""
independent_steps = [s for s in plan if not any(re.search(r"\bE\d+\b", str(v)) for v in s["args"].values())]
dependent_steps = [s for s in plan if s not in independent_steps]
def _lookup_with_simulated_latency(entity):
# These are in-memory fictional records -- nothing to actually wait
# on. This stands in for the I/O latency a real lookup (an API call,
# a DB query) would have, which is what makes concurrency worth
# measuring at all.
time.sleep(0.4)
return lookup_population(entity)
t0 = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=len(independent_steps) or 1) as pool:
futures = {pool.submit(_lookup_with_simulated_latency, s["args"]["entity"]): s["var"] for s in independent_steps if s["tool"] == "lookup_population"}
evidence = {futures[f]: f.result().get("result") for f in concurrent.futures.as_completed(futures)}
concurrent_seconds = time.time() - t0
t1 = time.time()
for s in independent_steps:
if s["tool"] == "lookup_population":
_lookup_with_simulated_latency(s["args"]["entity"])
sequential_seconds = time.time() - t1
return {
"independent_steps": [s["var"] for s in independent_steps],
"dependent_steps": [s["var"] for s in dependent_steps],
"concurrent_seconds": round(concurrent_seconds, 2),
"sequential_seconds": round(sequential_seconds, 2),
}
A real run — reusing ReWOO's own plan, dispatched differently
Given the exact same 6-step plan ReWOO generated above, dependency analysis correctly split it into E1, E2, E3 (no reference to any other step's result — independent) and E4, E5, E6 (each references an earlier #E — dependent). No new LLM call needed for this: it's a regex check over the plan's own argument strings.
Dispatching the three independent lookups concurrently: 0.45s. Running the same three, one at a time: 1.22s — about a 2.7x speedup for 3 calls. Disclosed plainly: these are fictional, in-memory records with an artificial 0.4s delay added to stand in for real I/O latency (a real API call, a real database query), since there's nothing to actually wait on for a dict lookup — this measures the concurrency mechanism honestly, not a claim about real network behavior. For a real-network version of the same measurement, see Workflow Patterns's parallelization demo, which measured a 2.6x speedup on 3 genuine API calls (the more realistic number, since queueing and network variance eat into the clean theoretical multiple).
Tree of Thoughts and LATS: search over thoughts¶
Tree of Thoughts generalizes chain-of-thought into a search: instead of one linear reasoning path, the model proposes multiple candidate "thoughts" at each step, self-evaluates them, and explores the more promising branches — with the ability to backtrack, unlike a single forward pass (Yao et al., 2023). On the Game of 24 arithmetic puzzle, the paper reports GPT-4 with chain-of-thought prompting solved 4% of tasks, against a 74% success rate with ToT search. That's a genuinely large gap, and it comes with a genuinely large cost: exploring several branches at each of several steps multiplies the number of LLM calls per problem, which is exactly why this page didn't build a fresh demo for it — the tradeoff itself (breadth of search bought with a multiplicative call budget) doesn't need a bespoke run to establish; it's inherent to the algorithm, the same way parallelization's real speedup was worth measuring but its existence wasn't in question.
LATS (Language Agent Tree Search) goes further, combining ReAct-style acting, Reflexion-style self-reflection, and Monte Carlo Tree Search's exploration into one loop, using the language model itself as both the value function scoring a branch and the source of reflections that inform later branches (Zhou et al., 2023). The paper reports 92.7% pass@1 on HumanEval with GPT-4, and a WebShop score the authors describe as comparable to gradient-based fine-tuning — without any gradient update. It's the most capable of the paradigms on this page and the most expensive to run, for the same structural reason as ToT: real tree search over real API calls.
What reasoning models changed¶
Every paradigm above except ReAct was built as external scaffolding around a base model that didn't reliably reflect on, search over, or verify its own reasoning unprompted — Reflexion's episodic buffer, ToT's explicit branching, LATS's tree search all exist because the mechanism wasn't happening inside the model on its own. DeepSeek-R1's reported finding changes that premise for at least some of these: "reasoning abilities of LLMs can be incentivized through pure reinforcement learning (RL), obviating the need for human-labeled reasoning trajectories," and the paper reports that "self-reflection, verification, and dynamic strategy adaptation" emerged from that RL training rather than being explicitly programmed (DeepSeek-AI, 2025).
The practical shift: for tasks a modern reasoning model handles well, some of what Reflexion or ToT used to buy through external scaffolding now happens inside one long chain-of-thought, before the model ever emits a final answer — self-checking and backtracking as part of generation, not as a separate loop your code has to build and orchestrate. This doesn't make the paradigms on this page obsolete. A reasoning model's internal self-correction isn't inspectable or interruptible the way an explicit Reflexion loop is, it isn't available at all on models without that training, and it doesn't help with the structural problems ReWOO and LLM Compiler solve — those are about call and token economics across tool-using steps, a different axis entirely from whether the model second-guesses itself well. What changed is which problems are worth solving with hand-built scaffolding versus which ones a well-trained model increasingly handles on its own — worth checking per-task, not assuming either way.
Interview angle¶
Weak answer to "when would you reach for ReWOO instead of ReAct": "ReWOO is faster since it needs fewer calls." This states the direction of the real effect but skips the actual trade-off, and this page's own real run shows why that matters: ReWOO's E6 step failed, and ReWOO structurally could not have caught that until execution — there's no point where the model looks at a real result before committing to the next step, because the whole plan was already committed before any of it ran.
Strong answer: the real trade-off is speed-through-fewer-round-trips against a wider blast radius per undetected planning error. ReWOO wins decisively on tokens and calls (this page measured 6.8x and 2.5x on one real run) because the model reasons about the shape of the problem once, then never has to re-read a growing transcript. ReAct wins on catching a bad step early, because every decision is made with the real previous result already in hand. The corollary that actually answers "when": ReWOO is the better default when the tool calls are cheap to verify after the fact and errors are recoverable at the solve step (as this run's own E6 failure was) — ReAct is the better default when a wrong intermediate step could send the rest of the plan somewhere expensive or irreversible to unwind.
Follow-up to expect: "doesn't LLM Compiler just make ReWOO's redundant round-trips point moot, since you get parallelism either way?" No — LLM Compiler adds concurrency for independent steps, which is a wall-clock win, not a token-count win. ReWOO's own plan in this page's real run had three independent lookups; running them concurrently (LLM Compiler's contribution) sped up wall-clock time, but it didn't touch the token count at all, because the same evidence still gets assembled into the same final solve call either way. Token efficiency and call-latency are separate axes — a system can be strong on one and unremarkable on the other, and this page's own numbers show ReWOO winning specifically on tokens/calls while LLM Compiler wins specifically on wall-clock.
Build it yourself — 30 minutes¶
- Pick a multi-step question that genuinely needs 3+ tool calls, with a ground truth you can compute independently of any model (a calculation, a lookup you control) — like this page's population question.
- Build the ReAct version first: a loop that decides one action, executes it, observes the real result, and only then decides the next action. Restrict it to one tool call per turn even if your provider supports requesting several, so you're testing the mechanism, not the API's batching.
- Build ReWOO next, on the identical question: one call that plans every step with
#E-style variables, deterministic execution with substitution, one call that solves from the gathered evidence. Compare real call counts and real token counts (from your API response's usage field) against step 2 — don't estimate them. - Only then try LLM Compiler: take ReWOO's plan, write a simple dependency check (does a step's arguments reference another step's variable?), and dispatch the independent ones concurrently. Time it against running them one at a time.
Scenario Check¶
Sources & further reading¶
- Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models" (arXiv 2210.03629, submitted 2022-10-06, ICLR 2023) — the interleaved thought/action/observation mechanism every tool-calling agent loop uses today.
- Shinn et al., "Reflexion: Language Agents with Verbal Reinforcement Learning" (arXiv 2303.11366, submitted 2023-03-20) — verbal self-reflection stored in an episodic buffer across trials.
- Wang et al., "Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models" (arXiv 2305.04091, ACL 2023) — a zero-shot prompting technique, not a planner/executor architecture.
- Xu et al., "ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models" (arXiv 2305.18323, submitted 2023-05-23) — plan-then-execute with variable substitution; reports token efficiency, not latency.
- Kim et al., "An LLM Compiler for Parallel Function Calling" (arXiv 2312.04511, ICML 2024) — a DAG-based planner and parallel dispatch over independent tool calls.
- Yao et al., "Tree of Thoughts: Deliberate Problem Solving with Large Language Models" (arXiv 2305.10601, submitted 2023-05-17) — search over candidate reasoning branches with self-evaluation and backtracking.
- Zhou et al., "Language Agent Tree Search Unifies Reasoning, Acting, and Planning in Language Models" (arXiv 2310.04406, submitted 2023-10-06) — Monte Carlo Tree Search combining ReAct-style acting, Reflexion-style self-reflection, and ToT-style search.
- DeepSeek-AI, "DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning" (arXiv 2501.12948, submitted 2025-01-22, also published in Nature vol. 645) — self-reflection and verification reported to emerge from pure RL, without hand-coded scaffolding.