Sijin T V
Sijin T V A passionate Software Engineer who contributes to the wonders happenning on the internet

Agent Orchestration: Comparing ReAct, Plan-and-Solve, and Reflection Patterns

Almost every agent starts as a naive while True: call_model, run_tool, call_model loop, and almost every production agent ends up as something else. The reason is that the three canonical loop patterns — ReAct, Plan-and-Solve, and Reflection — have opposite failure modes, and the naive loop inherits all of them at once. After running agents against internal ticket pipelines and code-migration work for a year, I’d rather argue about which loop shape to use than which model, because the model’s quality is bounded by the loop’s structure either way.

The three loops and their actual costs

ReAct (Thought → Action → Observation) interleaves reasoning with tool calls. Its strength is that it adapts mid-flight: it can change course the moment an observation contradicts an assumption. Its weakness is that it spends tokens discovering structure it could have been given upfront, and it will happily loop — tool call, error, same tool call, error — until a step budget stops it. In our 5-step code-migration benchmark, pure ReAct averaged 18 tool calls and 22k tokens per task with 81% success.

Plan-and-Solve writes a plan first, then executes it linearly. One planning pass, then a deterministic walk. It costs roughly half of ReAct — 9 tool calls, 9k tokens — but its success rate dropped to 63%, because plans are always wrong in the details: a table that doesn’t exist, an API that moved, an assumption invalidated by the first tool result. A plan is a hypothesis, and executing a falsified hypothesis linearly is just expensive wrongness.

Reflection (Reflexion-style) adds a critic pass over a draft before delivery. It is the most expensive pattern per task — 2-3x token overhead — and in our benchmarks it only paid for itself on tasks with no deterministic verifier. On anything with a compiler, a test suite, or a schema check, the LLM critic is redundant: the verifier already told you it’s broken.

The numbers, one internal migration task (repoint a service from HTTP to gRPC across 40 files):

pattern tool calls tokens success
ReAct 18 22k 81%
Plan-and-Solve 9 9k 63%
ReAct + re-plan gate 11 12k 87%

The last row is the one we shipped. Not a new pattern, but a discipline: plan once cheaply, then run ReAct, then detect plan invalidation deterministically and re-plan rather than blindly continuing.

A ReAct loop that doesn’t leak

The loop itself is unglamorous; the sharp edges are the budget, the tool schema, and the termination signal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def react(question, tools, model, max_steps=12):
    messages = [{"role": "user", "content": question}]
    for _ in range(max_steps):
        reply = model.chat(messages, tools=[t.schema for t in tools])
        messages.append({"role": "assistant", "content": reply})
        if not reply.tool_calls:
            return reply.content
        for call in reply.tool_calls:
            result = tools[call.name].invoke(**call.arguments)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result, default=str),
            })
    raise LoopBudgetExceeded()

Three things make this survivable in production. First, max_steps is the contract, not a hope: every budget-constrained loop turns an unbounded latency tail into a bounded one, and the step budget is a product decision (what is an acceptable worst-case round trip for this request?) more than an engineering one. Second, tool calls are structured JSON, not free text — a schema on the tool contract cuts hallucinated arguments the same way constrained decoding cuts malformed JSON. Third, the loop terminates only on reply.tool_calls being empty, which means the model must be able to refuse; a loop with no explicit stop token just burns the budget and fails anyway, but slower.

Where planning and reflection still earn their keep

We do use a planner, but as a re-planner. Instead of a single upfront plan, we check after each tool observation whether the assumptions that motivated the next step still hold — if a file moved, if an API signature changed — and only then pay for a re-plan. That’s the difference between the 63% and 87% rows above: the plan is a working hypothesis, invalidated by evidence, not a load-bearing structure. Cheap plans, frequent cheap verification, occasional re-planning.

Reflection, similarly, we scope to where no verifier exists: summarizing a negotiation, drafting a stakeholder message, editing prose. If the output is checkable by a program, the program is the critic, and the reflection pass is wasted spend. The rule we enforce: an LLM critic is allowed exactly where a deterministic verifier cannot exist.

Production lessons

  • Log the full trajectory. We ship every step (prompt, action, observation, latency, token count) to Langfuse; the routing bottlenecks show up in the step-latency breakdown — a tool that takes 4s dominates everything else and makes the loop’s model calls irrelevant.
  • Parallelize independent tool calls. ReAct is serial by default; when a step obviously fans out (read these 10 files), the model should issue 10 calls in one message and get them executed concurrently. This alone cut our median migration task wall-time by 40%.
  • Budget tokens, not just steps. A step that dumps 200k tokens of file contents into context is a step that derails every subsequent decision. Truncate, diff, or summarize observations before they re-enter the context window.
  • Verification is a first-class tool. The loop’s most valuable “tool” is often the checker (compile, lint, schema validate, run the test). Prefer adding a verifier to adding reflection.

The loop is the agent’s architecture. Choose the shape deliberately, budget it, log it — and let the deterministic verifiers do the judging, not another model call.

comments powered by Disqus