ReAct Pattern: Reasoning and Acting in Agents
Source: mortalapps.com- 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?"*
- Thought 1: I need to find the parent company of YouTube first. I will use the web search tool to find this information.
- Action 1:
web_search(query="parent company of YouTube") - 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."
- 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.
- Action 2:
get_stock_price(ticker="GOOGL") - Observation 2: "GOOGL: $182.50 (+1.2%)"
- 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.
- Action 3:
web_search(query="Alphabet Q3 revenue") - 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."
- 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.
- 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
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}")
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.