Understanding the ReAct Pattern
The ReAct (Reasoning + Acting) pattern is one of the most influential agent architectures in modern AI. Introduced by Yao et al. in their 2022 paper "ReAct: Synergizing Reasoning and Acting in Language Models," this pattern creates agents that generate verbal reasoning traces alongside actions, producing a powerful synergy between thinking and doing.
Unlike chain-of-thought prompting (which only reasons) or action-only agents (which act without explicit reasoning), ReAct interleaves both. The agent explicitly states what it's thinking, decides on an action, observes the result, and updates its reasoning — creating a transparent, debuggable decision-making process.
The ReAct Cycle
- Thought: The agent reasons about the current situation, what information it has, and what it needs
- Action: Based on reasoning, the agent selects a tool and provides input parameters
- Observation: The agent receives the tool's output and incorporates it into its context
- Repeat: The cycle continues until the agent has enough information to provide a final answer
Why ReAct Works
The power of ReAct comes from the explicit reasoning traces. When an agent thinks out loud before acting, several benefits emerge:
- Grounded Decisions: Actions are tied to explicit reasoning, reducing hallucination and random tool calls
- Error Recovery: When an action fails, the agent can reason about why and adjust its approach
- Transparency: Developers can read the thought traces to understand and debug agent behavior
- Context Maintenance: The reasoning trace acts as working memory, tracking what the agent knows and still needs
- Self-Correction: The agent can notice mistakes in its own reasoning and correct course
Implementing ReAct from Scratch
Let's build a complete ReAct agent from scratch to understand every component. This implementation shows the raw mechanics without relying on a framework.
// Complete ReAct agent implementation from scratch
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// Define tool interfaces
interface ToolDefinition {
name: string;
description: string;
parameters: Record<string, { type: string; description: string }>;
required: string[];
}
interface ToolResult {
success: boolean;
data: string;
}
// Implement tools
const tools: Record<string, {
definition: ToolDefinition;
execute: (params: Record<string, string>) => Promise<ToolResult>;
}> = {
search: {
definition: {
name: "search",
description: "Search the web for current information",
parameters: {
query: { type: "string", description: "Search query" },
},
required: ["query"],
},
execute: async (params) => {
const res = await fetch(
`https://api.tavily.com/search`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: process.env.TAVILY_API_KEY,
query: params.query,
max_results: 3,
}),
}
);
const data = await res.json();
return {
success: true,
data: data.results
.map((r: any) => `${r.title}: ${r.content}`)
.join("\n\n"),
};
},
},
calculate: {
definition: {
name: "calculate",
description: "Evaluate a mathematical expression",
parameters: {
expression: { type: "string", description: "Math expression to evaluate" },
},
required: ["expression"],
},
execute: async (params) => {
try {
const result = Function(`"use strict"; return (${params.expression})`)();
return { success: true, data: String(result) };
} catch (e) {
return { success: false, data: `Error: ${e}` };
}
},
},
};
// The ReAct agent loop
async function reactAgent(question: string, maxSteps = 8): Promise<string> {
const toolDescriptions = Object.values(tools)
.map(t => `- ${t.definition.name}: ${t.definition.description}`)
.join("\n");
const systemPrompt = `You are a ReAct agent. For each step, you MUST follow this format exactly:
Thought: [Your reasoning about what to do next]
Action: [tool_name]
Action Input: [JSON parameters for the tool]
When you have enough information to answer, use:
Thought: [Final reasoning]
Final Answer: [Your complete answer]
Available tools:
${toolDescriptions}
Important rules:
1. Always think before acting
2. Use observations to refine your approach
3. Never make up information - only use tool results
4. Provide a Final Answer when you have sufficient information`;
let scratchpad = `Question: ${question}\n`;
let steps = 0;
while (steps < maxSteps) {
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: systemPrompt,
messages: [{ role: "user", content: scratchpad }],
});
const text = response.content[0].type === "text"
? response.content[0].text
: "";
scratchpad += text + "\n";
// Check for final answer
if (text.includes("Final Answer:")) {
const answer = text.split("Final Answer:")[1].trim();
return answer;
}
// Parse action and execute
const actionMatch = text.match(/Action:\s*(.+)/);
const inputMatch = text.match(/Action Input:\s*(.+)/);
if (actionMatch && inputMatch) {
const toolName = actionMatch[1].trim();
const toolInput = JSON.parse(inputMatch[1].trim());
const tool = tools[toolName];
if (tool) {
const result = await tool.execute(toolInput);
scratchpad += `Observation: ${result.data}\n`;
} else {
scratchpad += `Observation: Tool "${toolName}" not found.\n`;
}
}
steps++;
}
return "I was unable to find a complete answer within the step limit.";
}
// Usage
const answer = await reactAgent(
"What is the current population of Japan and what percentage of the world population is that?"
);
console.log(answer);
# Complete ReAct agent in Python
import anthropic
import json
import re
client = anthropic.Anthropic()
# Define tools
def search_web(query: str) -> str:
"""Search the web for information."""
import requests
response = requests.post(
"https://api.tavily.com/search",
json={"api_key": os.environ["TAVILY_API_KEY"], "query": query, "max_results": 3}
)
results = response.json().get("results", [])
return "\n\n".join(f"{r['title']}: {r['content']}" for r in results)
def calculate(expression: str) -> str:
"""Evaluate a math expression safely."""
try:
# Use a safe evaluator
allowed_names = {"__builtins__": {}}
result = eval(expression, allowed_names)
return str(result)
except Exception as e:
return f"Error: {e}"
TOOLS = {
"search": {"fn": search_web, "desc": "Search the web for current information"},
"calculate": {"fn": calculate, "desc": "Evaluate a mathematical expression"},
}
def react_agent(question: str, max_steps: int = 8) -> str:
tool_desc = "\n".join(f"- {name}: {t['desc']}" for name, t in TOOLS.items())
system = f"""You are a ReAct agent. Follow this format:
Thought: [reasoning]
Action: [tool_name]
Action Input: {{"param": "value"}}
When ready to answer:
Thought: [final reasoning]
Final Answer: [answer]
Tools:
{tool_desc}"""
scratchpad = f"Question: {question}\n"
for step in range(max_steps):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": scratchpad}],
)
text = response.content[0].text
scratchpad += text + "\n"
# Check for final answer
if "Final Answer:" in text:
return text.split("Final Answer:")[1].strip()
# Parse and execute action
action_match = re.search(r"Action:\s*(.+)", text)
input_match = re.search(r"Action Input:\s*(.+)", text)
if action_match and input_match:
tool_name = action_match.group(1).strip()
tool_input = json.loads(input_match.group(1).strip())
if tool_name in TOOLS:
result = TOOLS[tool_name]["fn"](**tool_input)
scratchpad += f"Observation: {result}\n"
else:
scratchpad += f"Observation: Tool '{tool_name}' not found\n"
return "Max steps reached without finding an answer."
# Usage
answer = react_agent("What are the top 3 AI frameworks in 2026 by GitHub stars?")
print(answer)
ReAct with LangGraph
While building from scratch helps understanding, production systems typically use frameworks like LangGraph that provide built-in ReAct implementations with streaming, persistence, and error handling.
// Production ReAct agent with LangGraph
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { ChatAnthropic } from "@langchain/anthropic";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { MemorySaver } from "@langchain/langgraph";
const model = new ChatAnthropic({
model: "claude-sonnet-4-20250514",
temperature: 0,
});
// Define tools with Zod schemas
const searchTool = tool(
async ({ query }) => {
// Implementation here
return `Results for: ${query}`;
},
{
name: "web_search",
description: "Search the web for current information",
schema: z.object({
query: z.string().describe("The search query"),
}),
}
);
const dbTool = tool(
async ({ sql }) => {
// Execute SQL query
return `Query results for: ${sql}`;
},
{
name: "query_database",
description: "Execute a read-only SQL query",
schema: z.object({
sql: z.string().describe("SQL SELECT query to execute"),
}),
}
);
// Create agent with memory for conversation persistence
const checkpointer = new MemorySaver();
const agent = createReactAgent({
llm: model,
tools: [searchTool, dbTool],
checkpointSaver: checkpointer,
});
// Invoke with thread ID for persistence
const config = { configurable: { thread_id: "user-session-123" } };
const result = await agent.invoke(
{ messages: [{ role: "user", content: "Find recent AI news and summarize" }] },
config
);
// Continue the conversation
const followUp = await agent.invoke(
{ messages: [{ role: "user", content: "Tell me more about the first result" }] },
config
);
ReAct Trace Example
Example ReAct Execution Trace
Question: "What is the market cap of NVIDIA and how does it compare to Apple?"
Common ReAct Pitfalls
- Infinite Loops: The agent keeps searching without converging. Always set a max step limit.
- Hallucinated Tools: The agent tries to use tools that don't exist. Validate tool names before execution.
- Reasoning Drift: The agent's thoughts diverge from the original question. Include the question in every prompt.
- Over-thinking: The agent reasons for too many steps when the answer is straightforward. Use temperature=0.
- Context Window Overflow: Long scratchpads exceed the model's context window. Summarize periodically.
Summary
The ReAct pattern is the foundation of modern agent development. Its strength lies in making agent reasoning explicit and transparent. Whether you implement it from scratch or use a framework like LangGraph, the core loop — think, act, observe — remains the same. Mastering ReAct gives you the building blocks to understand all other agent architectures.