← All Posts

Agent Fundamentals: ReAct, Plan-and-Execute

What Are LLM Agents?

An LLM agent is a system where a large language model acts as the central reasoning engine that decides which actions to take, observes the results, and iterates until a goal is met. Unlike a single-shot prompt → response pattern, an agent operates in a loop: it can call external tools (APIs, databases, code interpreters), read their outputs, reflect on progress, and decide what to do next — all autonomously.

The core insight is that LLMs are remarkably good at decomposing problems into steps and selecting the right tool for each step, provided they are given structured access to those tools. An agent combines three capabilities that a standalone LLM lacks:

Key distinction: A chain follows a fixed, pre-determined sequence of steps. An agent dynamically chooses its next action based on observations — the control flow is determined by the LLM at runtime.

The Observe → Think → Act Loop

Every agent architecture — regardless of framework — can be reduced to a three-phase loop that repeats until a termination condition is met:

  1. Observe: The agent receives new information. On the first iteration this is the user query; on subsequent iterations it is the output from the previous tool call or environment feedback.
  2. Think: The LLM processes all accumulated context (the original query, the action history, and the latest observation) and reasons about what to do next. This is where chain-of-thought prompting, self-reflection, and plan revision occur.
  3. Act: The agent either invokes a tool (e.g., search("latest GDP of France")) or produces a final answer to return to the user. If a tool is invoked, its output becomes the next observation and the loop continues.

The loop terminates when the LLM decides it has sufficient information to answer the user, or when a maximum iteration count is reached (a critical safety rail). In pseudo-code:

# The universal agent loop observation = user_query for step in range(max_iterations): thought = llm.think(history, observation) # Think action = llm.choose_action(thought) # Act if action.type == "final_answer": return action.content observation = execute_tool(action) # Observe raise AgentTimeoutError("Max iterations reached")
Safety rail: Always enforce a max_iterations limit. Without it, a confused agent can loop indefinitely, burning tokens and API credits. A typical default is 5–10 iterations.

ReAct: Reasoning + Acting

ReAct (Yao et al., 2022) is the foundational agent pattern that interleaves reasoning traces with actions in a single prompt. The key innovation is that the LLM explicitly writes out its chain-of-thought before selecting an action, making the decision process interpretable and debuggable. A ReAct trace looks like this:

# ReAct trace example Question: What is the population of the capital of France? Thought 1: I need to find the capital of France first. Action 1: search("capital of France") Observation 1: Paris is the capital of France. Thought 2: Now I need the population of Paris. Action 2: search("population of Paris 2024") Observation 2: The population of Paris is approximately 2.1 million. Thought 3: I have the answer. Action 3: finish("The population of Paris, the capital of France, is ~2.1 million.")

ReAct outperforms both pure chain-of-thought (no tool use) and pure action-only approaches (no explicit reasoning) because the reasoning trace helps the model stay on track across multiple steps, while tool use grounds the reasoning in real-world data.

In implementation, ReAct is typically achieved through a carefully structured system prompt that defines the Thought / Action / Observation format and lists available tools with their signatures. The LLM generates text until it emits an Action: line, at which point the framework parses the tool call, executes it, appends the result as Observation:, and feeds everything back to the LLM for the next iteration.

Tip: When implementing ReAct, use stop sequences (e.g., "Observation:") to prevent the LLM from hallucinating tool outputs. The framework — not the model — should provide observations.

Plan-and-Execute Pattern

While ReAct interleaves planning and execution at each step, the Plan-and-Execute pattern (Wang et al., 2023) separates them into two distinct phases. A planner LLM first generates a complete multi-step plan, and then an executor carries out each step sequentially, optionally re-planning if a step fails or produces unexpected results.

This decomposition offers several advantages for complex tasks:

# Plan-and-Execute pattern def plan_and_execute(query, tools, max_replans=3): plan = planner_llm.generate_plan(query, tools) for replan_attempt in range(max_replans): results = [] for step in plan.steps: result = executor.run(step, context=results) if result.failed: # Re-plan with failure context plan = planner_llm.replan( query, plan, results, step, result.error ) break results.append(result) else: # All steps succeeded return synthesizer.combine(query, results) raise ReplanLimitExceeded("Failed after max re-plans")

ReAct

  • Interleaved thinking + acting
  • One step at a time — reactive
  • Simple to implement
  • Works well for ≤ 5 tool calls
  • Lower latency for short tasks

Plan-and-Execute

  • Separate planning and execution phases
  • Full plan generated upfront
  • More complex, but more robust
  • Better for 5–20+ step tasks
  • Supports re-planning on failure
Hybrid approach: Many production systems use Plan-and-Execute at the top level, with each execution step internally using a ReAct loop for individual tool interactions.

Building an Agent Loop in Python

Below is a complete, minimal ReAct agent implementation using the OpenAI function-calling API. This agent can search the web and perform calculations — extend it by adding more tool definitions.

import json import openai client = openai.OpenAI() # ── Define tools ── tools = [ { "type": "function", "function": { "name": "search", "description": "Search the web for current information", "parameters": { "type": "object", "properties": { "query": {"type": "string"} }, "required": ["query"] } } }, { "type": "function", "function": { "name": "calculator", "description": "Evaluate a math expression", "parameters": { "type": "object", "properties": { "expression": {"type": "string"} }, "required": ["expression"] } } } ] # ── Tool dispatch ── def dispatch_tool(name, args): if name == "search": return web_search(args["query"]) if name == "calculator": return str(eval(args["expression"])) raise ValueError(f"Unknown tool: {name}") # ── Agent loop ── def run_agent(user_query, max_steps=8): messages = [ {"role": "system", "content": "You are a helpful assistant. Use tools when needed."}, {"role": "user", "content": user_query} ] for _ in range(max_steps): resp = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, tool_choice="auto" ) msg = resp.choices[0].message messages.append(msg) # No tool calls → final answer if not msg.tool_calls: return msg.content # Execute each tool call for tc in msg.tool_calls: args = json.loads(tc.function.arguments) result = dispatch_tool(tc.function.name, args) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": result }) raise RuntimeError("Agent exceeded max steps")

The key design decisions in this implementation:

Warning: Never use eval() in production for the calculator tool. Use a sandboxed expression parser like asteval or numexpr to prevent code injection.

Interactive: Agent Execution Loop

Walk through how an agent processes the query "What is the GDP per capita of France?" step by step. Use the controls below to advance or rewind through the execution loop.

Step 1 / 5
Observe: Agent receives the user query.
Observe Receive input / result Think LLM reasoning Act Tool or answer Tool search / calc / API result feeds back

When to Use Agents vs Chains

Not every LLM application needs an agent. Agents introduce latency (multiple LLM calls), cost (more tokens), and non-determinism (different runs may take different paths). Use the simplest architecture that solves your problem:

Use a Chain when…

  • The steps are known at design time
  • No conditional branching is needed
  • Latency budget is tight (< 2 seconds)
  • The task is a simple transform pipeline
  • Predictability > flexibility

Use an Agent when…

  • The number of steps is unknown upfront
  • The LLM must choose between multiple tools
  • Intermediate results affect the next action
  • The task requires multi-hop reasoning
  • Flexibility > predictability

A practical decision framework: start with a chain. If you find yourself adding more and more if/else branches to handle edge cases, that's a sign you need an agent. Common scenarios that benefit from agents include customer support (unpredictable queries), data analysis (dynamic SQL generation), and research tasks (iterative web search).

Production tip: In high-throughput systems, use agents only for the complex tail of queries. Route simple, well-structured requests through deterministic chains to save cost and reduce latency. This hybrid routing approach is used by most production LLM platforms.

To summarize the spectrum of LLM application architectures from simplest to most complex:

  1. Single prompt: One LLM call, no tools. Good for summarization, translation, classification.
  2. Chain: Fixed sequence of LLM calls and/or tools. Good for RAG, structured extraction.
  3. ReAct agent: Dynamic loop with interleaved reasoning and tool use. Good for multi-hop QA, research.
  4. Plan-and-Execute agent: Upfront planning with iterative execution. Good for complex, multi-step workflows.
  5. Multi-agent systems: Multiple specialized agents collaborating. Good for software engineering, complex analysis.
Anti-pattern: Don't use an agent when a simple prompt chain will do. Over-engineering with agents adds cost, latency, and debugging complexity. Always benchmark the simpler approach first.