← AI Agents Architecture & Patterns
🤖 AI Agents

ReAct Pattern: Reasoning and Acting in Agents

Source: mortalapps.com
TL;DR
  • The ReAct pattern interleaves reasoning traces (thoughts) with execution steps (actions and observations) to solve complex, multi-hop tasks.
  • It resolves the core limitation of pure Chain-of-Thought (CoT) by grounding reasoning in real-world, dynamic external feedback.
  • This pattern is the foundational cognitive architecture for tool-using agents, enabling self-correction and iterative discovery.
  • Implementing ReAct in production requires strict execution budgets, robust error catching, and structured parsing to prevent infinite loops.
  • Tradeoffs include high token consumption and latency overhead due to sequential, multi-turn LLM invocations.

Why This Matters

The react agent pattern (Reasoning and Acting) represents a fundamental paradigm shift in how LLMs interact with external environments. Early agent designs relied on pure Chain-of-Thought (CoT) reasoning, which is highly effective for closed-book mathematical or logical tasks but suffers from severe hallucination and stale knowledge when applied to real-world systems. Conversely, pure action-oriented systems execute API calls without explicit planning, leading to fragile execution paths and an inability to recover from unexpected payloads.

By interleaving reasoning traces ("thoughts") with execution steps ("actions" and "observations"), the ReAct pattern creates a closed-loop feedback system. The model generates a thought explaining *why* it needs to take a specific action, executes that action via an external tool, and then ingests the observation to formulate its next thought. This grounding mechanism drastically reduces hallucinations, as the agent's next cognitive step is anchored in the empirical output of the previous tool execution rather than the model's static parametric weights.

In production, ignoring this pattern or failing to implement it correctly leads to catastrophic failures: agents getting stuck in infinite execution loops, runaway API costs, and unhandled tool exceptions that crash the entire run. When building complex, multi-hop workflows where the next step cannot be determined statically at compile-time, the ReAct pattern is the industry-standard baseline. However, for deterministic, linear tasks, engineers should bypass ReAct in favor of state machines or static DAGs to avoid unnecessary latency and token overhead.

Core Concepts

To build and debug ReAct agents, you must understand the core components of the cognitive loop:

  • Thought: The reasoning step where the LLM analyzes its current state, historical context, and the previous observation to decide the next logical move.
  • Action: The execution step where the LLM selects a specific tool and constructs the exact parameters required to invoke it.
  • Observation: The grounding step where the external tool's execution result is returned to the LLM's context window, serving as the raw data for the next Thought.
  • The Loop: The iterative cycle of Thought -> Action -> Observation that continues until the agent determines it has sufficient information to formulate a final Answer.
  • Stop Condition: The criteria (e.g., reaching the final answer, hitting max iterations, or exhausting token budgets) that halts the execution loop.
  • Tool Registry: A structured catalog of available functions, schemas, and descriptions that the agent can choose to invoke during the Action phase.
Pattern Interleaves Reasoning? Grounded in Tools? Best For
Chain-of-Thought (CoT) Yes No Closed-book reasoning, math, logic
Act-Only (MRKL/APIs) No Yes Simple, single-step API calls
ReAct Yes Yes Dynamic, multi-hop, uncertain tasks

How It Works

The ReAct pattern operates as a stateful, iterative execution loop managed by an orchestrator application. The lifecycle of a single query proceeds through the following phases:

Phase 1: Initialization

The orchestrator receives the user query and initializes the conversation memory. It compiles the system prompt, which contains the core ReAct instructions (forcing the model to output "Thought:", "Action:", and "Observation:" prefixes), the list of available tools with their JSON schemas, and few-shot examples demonstrating the loop.

Phase 2: Reasoning (Thought)

The orchestrator sends the compiled prompt and history to the LLM. The LLM generates a "Thought" block. In this block, the model reasons about what information is missing and decides which tool is best suited to retrieve it.

Phase 3: Tool Selection & Execution (Action)

Following the Thought, the LLM outputs an "Action" block specifying the tool name and arguments. The orchestrator parses this output, pauses the LLM generation, retrieves the corresponding tool from the Tool Registry, and executes it with the parsed arguments.

Phase 4: Grounding (Observation)

The orchestrator captures the output of the tool execution. It formats this output as an "Observation" block and appends it directly to the conversation history. This grounds the agent's state in empirical reality.

Phase 5: Evaluation & Termination

The orchestrator sends the updated history (now containing the new Observation) back to the LLM. The LLM evaluates the state. If the goal has been achieved, it outputs a "Final Answer" block, and the orchestrator terminates the loop. If the goal is not met, the loop returns to Phase 2.

Failure Paths and Recovery

  • Parsing Failures: If the LLM outputs an invalid action format, the orchestrator catches the parsing error, formats it as an Observation (e.g., "Observation: Invalid action format. Please use the exact schema..."), and feeds it back to the model to trigger self-correction.
  • Tool Failures: If a tool throws an exception, the orchestrator catches the error and returns the stack trace or error message as the Observation, allowing the agent to reason about the failure and try an alternative tool.

Architecture

The ReAct architecture consists of five primary components. The Orchestrator acts as the central controller and entry point. It manages the execution state and coordinates data flow between all other components. The LLM Client handles communication with the foundation model, sending prompts and receiving text streams. The Parser is a streaming text processor that inspects the LLM output in real-time, extracting the Thought, Action, and Final Answer blocks. The Tool Registry is a secure repository of executable functions, mapping tool names to their Python implementations and JSON schemas. The Memory Buffer is a stateful store that maintains the chronological sequence of thoughts, actions, and observations.

Execution starts when a User Query enters the Orchestrator. The Orchestrator initializes the Memory Buffer with the query and system instructions. It then enters a loop: it sends the Memory Buffer contents to the LLM Client, which streams the response to the Parser. The Parser stops the stream as soon as an Action block is fully generated. The Orchestrator extracts the action parameters, invokes the corresponding function in the Tool Registry, and appends the returned result to the Memory Buffer as an Observation. This loop repeats until the Parser detects a Final Answer block, at which point the Orchestrator stops execution and returns the final payload to the user.

The Cognitive Advantage: Interleaving vs. Pure CoT

Pure Chain-of-Thought (CoT) reasoning operates entirely within the model's parametric memory. While CoT improves performance on logical tasks, it is a feed-forward process; if the model makes an incorrect assumption in step one, that error compounds throughout the remaining steps, leading to a highly confident but incorrect final answer. There is no mechanism for the model to verify its assumptions against external reality.

In contrast, the ReAct pattern acts as an online decision-making algorithm, structurally similar to a Partially Observable Markov Decision Process (POMDP). The agent's "Thought" represents its internal state estimation, the "Action" is the decision policy, and the "Observation" is the state transition feedback from the environment. Interleaving these steps allows the model to perform dynamic hypothesis testing: it proposes an action based on a hypothesis, observes the empirical result, and updates its internal state representation accordingly. This feedback loop is what enables agents to self-correct, recover from failed API calls, and discover alternative paths to a goal.

Multi-Hop Execution: A Worked Example

To understand the power of the ReAct pattern, consider the multi-hop query: *"What is the current stock price of the parent company of YouTube, and what was their Q3 revenue?"*

  1. Thought 1: I need to find the parent company of YouTube first. I will use the web search tool to find this information.
  2. Action 1: web_search(query="parent company of YouTube")
  3. Observation 1: "YouTube is a global online video sharing platform headquartered in San Bruno, California. It was bought by Google in November 2006 and is now a subsidiary of Alphabet Inc."
  4. Thought 2: The parent company of YouTube is Alphabet Inc. Now I need to find the current stock price of Alphabet Inc. (GOOGL) and their Q3 revenue.
  5. Action 2: get_stock_price(ticker="GOOGL")
  6. Observation 2: "GOOGL: $182.50 (+1.2%)"
  7. Thought 3: I have the stock price ($182.50). Now I need to find Alphabet Inc.'s Q3 revenue. I will search for their latest Q3 earnings report.
  8. Action 3: web_search(query="Alphabet Q3 revenue")
  9. Observation 3: "Alphabet Inc. announced financial results for the quarter ended September 30, reporting Q3 revenue of $88.27 billion, up 15% year-over-year."
  10. Thought 4: I now have both pieces of information: the parent company is Alphabet Inc., the stock price is $182.50, and the Q3 revenue is $88.27 billion. I can formulate the final answer.
  11. Final Answer: The parent company of YouTube is Alphabet Inc. Their current stock price is $182.50, and their Q3 revenue was $88.27 billion.

Parsing Strategies and Schema Enforcement

In production, parsing the LLM's output is the most fragile part of the ReAct pattern. Traditional regex-based parsing of raw text blocks (e.g., searching for Thought:, Action:, Action Input:) is highly susceptible to formatting drift, especially with smaller or less capable models.

To mitigate this, modern architectures leverage native JSON tool calling (or function calling) provided by LLM APIs. However, to maintain the cognitive benefits of ReAct, you must force the model to output its reasoning *before* the tool call. This is achieved by defining a schema where the tool call itself requires a thought or reasoning parameter, or by configuring the system prompt to require a structured JSON output containing both a thought field and an action object. Enforcing this schema via Pydantic or JSON Schema validation ensures that the model cannot bypass the reasoning step, preserving the integrity of the ReAct loop.

Managing the Context Window and State Decay

As a ReAct loop executes, the context window grows rapidly. Every iteration appends the system prompt, the tool definitions, the history of thoughts, the exact tool payloads, and the raw observations. This accumulation leads to two major issues: exponential token cost growth and "lost in the middle" attention degradation.

To manage this, production orchestrators implement dynamic context eviction and summarization. When the context window reaches a predefined threshold (e.g., 80% of the model's limit), the orchestrator should compress the history. A highly effective strategy is to retain only the last two iterations of raw Thought-Action-Observation blocks, while summarizing all preceding iterations into a single "Historical Summary" block. This keeps the immediate context clean and focused on the current step while preserving the overall trajectory of the run.

Code Example

A complete, production-grade implementation of a ReAct agent loop in Python. It features robust error handling, an execution loop budget to prevent infinite runs, structured tool execution, and detailed logging.
Python
import os
import re
import logging
from typing import Dict, Any, Callable

# Configure structured logging for production visibility
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("ReActOrchestrator")

# Mock Tool Registry
class ToolRegistry:
    def __init__(self):
        self._tools: Dict[str, Callable] = {}

    def register(self, name: str, func: Callable):
        self._tools[name] = func

    def execute(self, name: str, argument: str) -> str:
        if name not in self._tools:
            raise ValueError(f"Tool '{name}' is not registered.")
        try:
            logger.info(f"Executing tool '{name}' with argument: '{argument}'")
            return self._tools[name](argument)
        except Exception as e:
            logger.error(f"Error executing tool '{name}': {str(e)}")
            return f"Error: Tool execution failed. {str(e)}"

# Define mock tools
def web_search(query: str) -> str:
    query_lower = query.lower()
    if "parent company of youtube" in query_lower:
        return "YouTube is owned by Alphabet Inc."
    elif "alphabet q3 revenue" in query_lower:
        return "Alphabet Inc. reported Q3 revenue of $88.27 billion."
    return "No relevant search results found."

def get_stock_price(ticker: str) -> str:
    if ticker.upper() == "GOOGL":
        return "GOOGL: $182.50"
    return f"Ticker '{ticker}' not found."

# Initialize registry and register tools
registry = ToolRegistry()
registry.register("web_search", web_search)
registry.register("get_stock_price", get_stock_price)

# Mock LLM Client simulating the ReAct responses for a multi-hop query
class MockLLMClient:
    def __init__(self):
        self.step = 0

    def generate(self, prompt: str) -> str:
        # Simulate sequential LLM outputs based on the current step of the loop
        self.step += 1
        if self.step == 1:
            return (
                "Thought: I need to find the parent company of YouTube first.
"
                "Action: web_search(parent company of YouTube)"
            )
        elif self.step == 2:
            return (
                "Thought: The parent company of YouTube is Alphabet Inc. Now I need to find their Q3 revenue.
"
                "Action: web_search(Alphabet Q3 revenue)"
            )
        elif self.step == 3:
            return (
                "Thought: I have the parent company (Alphabet Inc.) and their Q3 revenue ($88.27 billion). I can now answer the query.
"
                "Final Answer: The parent company of YouTube is Alphabet Inc., and their Q3 revenue was $88.27 billion."
            )
        return "Final Answer: Loop completed."

class ReActAgent:
    def __init__(self, tool_registry: ToolRegistry, max_iterations: int = 5):
        self.registry = tool_registry
        self.max_iterations = max_iterations
        # Ensure API keys are present in a real environment
        self.api_key = os.environ.get("LLM_API_KEY", "mock-key-for-local-run")
        self.llm = MockLLMClient()

    def run(self, query: str) -> str:
        logger.info(f"Starting ReAct agent for query: '{query}'")
        
        # Initialize conversation history with system instructions
        history = (
            "System: You are a helpful assistant. Solve the query using the ReAct pattern.
"
            "Use the format: Thought -> Action -> Observation -> Final Answer.
"
            f"Query: {query}
"
        )

        for iteration in range(1, self.max_iterations + 1):
            logger.info(f"Starting iteration {iteration}/{self.max_iterations}")
            
            # Call the LLM with the accumulated history
            response = self.llm.generate(history)
            history += response + "
"
            
            # Parse the response for Action or Final Answer
            if "Final Answer:" in response:
                final_answer = response.split("Final Answer:")[-1].strip()
                logger.info("Final Answer found. Terminating loop.")
                return final_answer

            # Extract Action using regex
            action_match = re.search(r"Action:\s*(\w+)\(([^)]+)\)", response)
            if action_match:
                tool_name = action_match.group(1)
                tool_arg = action_match.group(2).strip('"\' ')
                
                # Execute the tool and capture the observation
                observation = self.registry.execute(tool_name, tool_arg)
                logger.info(f"Observation captured: {observation}")
                
                # Append observation to history to ground the next iteration
                history += f"Observation: {observation}
"
            else:
                # Handle parsing failure gracefully
                error_msg = "Observation: Error parsing action. Ensure you output exactly 'Action: tool_name(argument)'."
                logger.warning("Failed to parse action from LLM response.")
                history += f"
{error_msg}
"

        logger.error("Max iterations reached without a final answer.")
        return "Error: Execution budget exceeded without reaching a final answer."

# Execution block
if __name__ == "__main__":
    agent = ReActAgent(registry, max_iterations=5)
    result = agent.run("What is the parent company of YouTube and what was their Q3 revenue?")
    print(f"
Result: {result}")
Expected Output
2026-03-30 10:00:00 [INFO] Starting ReAct agent for query: 'What is the parent company of YouTube and what was their Q3 revenue?'
2026-03-30 10:00:00 [INFO] Starting iteration 1/5
2026-03-30 10:00:00 [INFO] Executing tool 'web_search' with argument: 'parent company of YouTube'
2026-03-30 10:00:00 [INFO] Observation captured: YouTube is owned by Alphabet Inc.
2026-03-30 10:00:00 [INFO] Starting iteration 2/5
2026-03-30 10:00:00 [INFO] Executing tool 'web_search' with argument: 'Alphabet Q3 revenue'
2026-03-30 10:00:00 [INFO] Observation captured: Alphabet Inc. reported Q3 revenue of $88.27 billion.
2026-03-30 10:00:00 [INFO] Starting iteration 3/5
2026-03-30 10:00:00 [INFO] Final Answer found. Terminating loop.

Result: The parent company of YouTube is Alphabet Inc., and their Q3 revenue was $88.27 billion.

Key Takeaways

The ReAct pattern combines reasoning (thoughts) and acting (actions) to ground LLM decisions in real-world observations.
Interleaving thoughts with actions dramatically reduces hallucinations compared to pure Chain-of-Thought or Act-Only approaches.
Production implementations require strict iteration budgets and robust error handling to prevent infinite execution loops.
Security must be enforced at the tool execution layer using RBAC, rather than relying on the LLM to police its own actions.
Context window management and prompt caching are essential to control latency and token costs in multi-turn loops.
Comprehensive tracing of every Thought, Action, and Observation is critical for debugging and regulatory compliance.

Frequently Asked Questions

What is the difference between the ReAct pattern and Chain-of-Thought (CoT)? +
Chain-of-Thought is a static reasoning process where the model generates a sequence of thoughts before outputting a final answer. ReAct interleaves reasoning with external tool execution, allowing the model to update its thoughts based on real-world observations.
When should I avoid using the ReAct pattern? +
Avoid ReAct for deterministic, linear workflows where the sequence of steps is known beforehand. In those cases, use state machines or static DAGs to save on latency, token costs, and complexity.
How does ReAct handle tool execution failures? +
A robust ReAct orchestrator catches tool exceptions, formats them as structured error messages, and injects them back into the loop as an Observation. This allows the agent to reason about the failure and attempt an alternative approach.
What are the primary causes of infinite loops in ReAct agents? +
Infinite loops typically occur when the LLM repeatedly generates the same malformed action, when a tool returns an unexpected or unhelpful observation, or when the model fails to recognize that it has already gathered enough information.
Can I use lightweight models like GPT-4o-mini or Claude 3.5 Haiku for ReAct? +
Yes, but lightweight models are more prone to parsing errors and tool selection confusion. For complex, multi-hop reasoning, frontier models (like Claude Sonnet 4.6 or GPT-4o) are highly recommended, while smaller models can be used for simpler, single-step tool routing.
How does the Model Context Protocol (MCP) integrate with the ReAct pattern? +
MCP provides a standardized protocol for the agent orchestrator to discover, describe, and execute tools. The ReAct loop acts as the cognitive driver, while MCP serves as the secure, structured transport layer for the Actions and Observations.
How do I prevent prompt injection attacks in a ReAct agent? +
Never trust the LLM to sanitize inputs. Run all tool executions in sandboxed environments, enforce strict RBAC at the API gateway level, and use LLM-as-a-judge guardrails to inspect inputs and outputs for malicious instructions.
How does context window exhaustion affect ReAct agents? +
As the loop runs, the context window grows with each iteration. If it exceeds the model's limit, the run will fail. To prevent this, implement dynamic context eviction, summarize older observations, or truncate intermediate reasoning steps.