ReAct Pattern
Reasoning + Acting: The foundational loop that enables AI agents to think through problems and take action in the world.
What is ReAct?
ReAct (Reasoning + Acting) is a prompting paradigm introduced by Yao et al. in 2022 that interleaves reasoning traces with actions. The key insight: by making the model explicitly reason about its actions, we get more reliable and interpretable agent behavior.
┌─────────────────────────────────────────────────────────┐
│ ReAct Loop │
└─────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ THOUGHT │
│ "I need to find..." │
│ "The result shows..." │
│ "Now I should..." │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ ACTION │
│ search("query") │
│ calculate("2+2") │
│ lookup("term") │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ OBSERVATION │
│ Result from tool │
│ or environment │
└──────────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Continue │ │ Complete │
│ (loop back) │ │ (return) │
└───────────────┘ └───────────────┘ Original Paper
Basic ReAct Implementation
The core ReAct pattern combines three elements in a loop: thinking about what to do, taking action, and observing the results.
function reactAgent(task, tools, maxSteps = 10):
observations = []
for step in range(maxSteps):
# REASON: Generate thought about current state
thought = llm.think(
task: task,
history: observations,
prompt: "What should I do next to accomplish this task?"
)
# Check if task is complete
if thought.indicatesCompletion:
return extractFinalAnswer(thought, observations)
# ACT: Select and execute action
action = llm.selectAction(
thought: thought,
availableTools: tools
)
# OBSERVE: Get result from environment
observation = execute(action)
# Store for next iteration
observations.append({
thought: thought,
action: action,
observation: observation
})
return "Max steps reached without completion" from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
# Define tools
@tool
def search(query: str) -> str:
"""Search the web for information."""
return web_search_api(query)
@tool
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression."""
return str(eval(expression)) # Use safe eval in production
# Create ReAct agent
llm = ChatOpenAI(model="gpt-4")
tools = [search, calculator]
agent = create_react_agent(llm, tools)
# Run the agent
result = agent.invoke({
"messages": [
("user", "What's the population of France times 2?")
]
})
# The agent will:
# 1. Think: I need to find France's population
# 2. Act: search("population of France")
# 3. Observe: "67 million"
# 4. Think: Now I need to multiply by 2
# 5. Act: calculator("67000000 * 2")
# 6. Observe: "134000000"
# 7. Return: "France has ~67M people, doubled is 134M" using Microsoft.Extensions.AI;
using System.ComponentModel;
public class ReActAgent
{
private readonly IChatClient _client;
private readonly List<AITool> _tools;
private readonly int _maxIterations;
public ReActAgent(
IChatClient client,
List<AITool> tools,
int maxIterations = 10)
{
_client = client;
_tools = tools;
_maxIterations = maxIterations;
}
public async Task<string> RunAsync(string task)
{
var messages = new List<ChatMessage>
{
new(ChatRole.System, GetSystemPrompt()),
new(ChatRole.User, task)
};
for (int i = 0; i < _maxIterations; i++)
{
var response = await _client.GetResponseAsync(
messages,
new ChatOptions { Tools = _tools }
);
messages.Add(response.Message);
// Check if agent is done (no tool calls)
if (!response.Message.Contents
.OfType<FunctionCallContent>().Any())
{
return response.Message.Text ?? "";
}
// Execute tool calls and add results
foreach (var toolCall in response.Message.Contents
.OfType<FunctionCallContent>())
{
var result = await ExecuteToolAsync(toolCall);
messages.Add(new ChatMessage(
ChatRole.Tool,
result
));
}
}
return "Max iterations reached";
}
private string GetSystemPrompt() => """
You are a ReAct agent. For each step:
1. THOUGHT: Reason about what to do next
2. ACTION: Use a tool if needed
3. OBSERVATION: Analyze the result
Repeat until you can answer the user's question.
""";
} Explicit Reasoning Traces
The original ReAct approach uses explicit text formatting to structure thoughts and actions. This makes the agent's reasoning visible and debuggable:
# Explicit ReAct format with structured output
SYSTEM_PROMPT = """
You are a ReAct agent. Always respond in this exact format:
Thought: [Your reasoning about the current situation]
Action: [tool_name(arg1, arg2)] OR Answer: [final response]
"""
function parseReactResponse(response):
thought = extractBetween(response, "Thought:", "Action:")
if contains(response, "Answer:"):
answer = extractAfter(response, "Answer:")
return { type: "complete", answer: answer }
actionStr = extractAfter(response, "Action:")
action = parseToolCall(actionStr)
return { type: "action", thought: thought, action: action }
function reactLoop(task):
messages = [systemPrompt, userMessage(task)]
while true:
response = llm.generate(messages)
parsed = parseReactResponse(response)
if parsed.type == "complete":
return parsed.answer
# Execute action and format observation
result = execute(parsed.action)
observation = f"Observation: {result}"
messages.append(assistantMessage(response))
messages.append(userMessage(observation)) from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.tools import tool
from langchain import hub
# Define tools
@tool
def search(query: str) -> str:
"""Search the web for information."""
return search_api.search(query)
@tool
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression."""
return str(eval(expression))
@tool
def lookup(term: str) -> str:
"""Look up a definition or fact."""
return knowledge_base.lookup(term)
# Use LangChain's ReAct prompt (or customize your own)
prompt = hub.pull("hwchase17/react")
# Or create a custom ReAct prompt
custom_prompt = ChatPromptTemplate.from_messages([
("system", """You are a ReAct agent. Respond in this format:
Thought: [Your reasoning about what to do next]
Action: tool_name[input]
Observation: [Result from tool]
... (repeat until done)
Thought: I have the answer
Final Answer: [Your response]
Tools available: {tool_names}
{tools}"""),
("human", "{input}"),
("placeholder", "{agent_scratchpad}")
])
# Create the ReAct agent
llm = ChatOpenAI(model="gpt-4", temperature=0)
tools = [search, calculator, lookup]
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # Shows Thought/Action/Observation trace
max_iterations=10,
handle_parsing_errors=True
)
# Run with visible reasoning trace
result = agent_executor.invoke({
"input": "What's the population of Tokyo multiplied by 3?"
})
print(result["output"]) using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.ComponentModel;
// Define tools
[Description("Search the web for information")]
static string Search(string query) => searchApi.Search(query);
[Description("Evaluate a mathematical expression")]
static string Calculator(string expression) => Evaluate(expression);
[Description("Look up a definition or fact")]
static string Lookup(string term) => knowledgeBase.Lookup(term);
// Create ReAct-style agent with explicit reasoning prompt
AIAgent agent = new AzureOpenAIClient(endpoint, credentials)
.GetChatClient("gpt-4o")
.AsAIAgent(
instructions: """
You are a ReAct agent. For each step:
1. THOUGHT: Reason about what to do next
2. ACTION: Use a tool if needed
3. OBSERVATION: Analyze the result
Repeat until you can answer the user's question.
""",
tools: [
AIFunctionFactory.Create(Search),
AIFunctionFactory.Create(Calculator),
AIFunctionFactory.Create(Lookup)
]
);
// Run the agent - it follows ReAct pattern internally
var answer = await agent.RunAsync(
"What's the population of Tokyo multiplied by 3?"
);
Console.WriteLine(answer); Example Trace
User: What's the population of Tokyo multiplied by 3? Thought: I need to find the current population of Tokyo first. Action: search(query="Tokyo population 2024") Observation: Tokyo has a population of approximately 14 million people. Thought: Now I have the population (14 million). I need to multiply by 3. Action: calculator(expression="14000000 * 3") Observation: 42000000 Thought: I have the answer. 14 million * 3 = 42 million. Answer: The population of Tokyo (approximately 14 million) multiplied by 3 is 42 million.
Evolution: From Explicit to Implicit Reasoning
| Era | Approach | Characteristics |
|---|---|---|
| 2022-2023 | Explicit ReAct | Structured "Thought/Action/Observation" prompts |
| 2023-2024 | Tool-augmented LLMs | Native function calling, implicit reasoning |
| 2024-2025 | Reasoning Models | Internal chain-of-thought (o1, DeepSeek-R1, Claude) |
ReAct has evolved as models have become more capable
Important Finding
# Modern approach: Let reasoning models handle thinking internally
# No explicit "Thought:" prompting needed
function modernAgentLoop(task, tools):
messages = [userMessage(task)]
while true:
response = reasoningModel.generate(
messages: messages,
tools: tools,
# Reasoning model internally does CoT
# No need to prompt for explicit thoughts
)
if response.hasToolCalls:
for call in response.toolCalls:
result = execute(call)
messages.append(toolResult(call.id, result))
else:
# Model provides final answer directly
return response.content
# Key insight: Models like o1, DeepSeek-R1, Claude 3.5
# have internalized reasoning - explicit CoT prompts
# can actually degrade performance by 3-5% from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
# Define tools
@tool
def search(query: str) -> str:
"""Search the web for information."""
return search_api.search(query)
@tool
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression."""
return str(eval(expression))
# Modern LLMs have internalized reasoning
# LangGraph's create_react_agent handles the loop
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
tools = [search, calculator]
# Create agent - framework handles ReAct internally
agent = create_react_agent(llm, tools)
def modern_agent(task: str) -> str:
"""
Modern approach: LangGraph handles the ReAct loop.
The model reasons internally - no explicit prompting needed.
"""
result = agent.invoke({
"messages": [("user", task)]
})
# Get the final response
return result["messages"][-1].content
# For streaming with visible intermediate steps
async def modern_agent_stream(task: str):
async for event in agent.astream_events(
{"messages": [("user", task)]},
version="v2"
):
if event["event"] == "on_chat_model_stream":
print(event["data"]["chunk"].content, end="")
elif event["event"] == "on_tool_end":
print(f"\nTool result: {event['data']['output']}") using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.ComponentModel;
// Define tools
[Description("Search the web for information")]
static string Search(string query) => searchApi.Search(query);
[Description("Evaluate a mathematical expression")]
static string Calculator(string expression) => Evaluate(expression);
// Modern approach: Agent Framework handles ReAct loop internally
// No explicit reasoning prompts needed - model reasons internally
AIAgent agent = new AzureOpenAIClient(endpoint, credentials)
.GetChatClient("gpt-4o")
.AsAIAgent(
instructions: "You are a helpful assistant",
tools: [
AIFunctionFactory.Create(Search),
AIFunctionFactory.Create(Calculator)
]
);
// The agent automatically:
// 1. Sends the prompt to the model
// 2. Detects tool calls and executes them
// 3. Feeds results back to the model
// 4. Repeats until model gives final answer
var answer = await agent.RunAsync(
"What's the population of Tokyo multiplied by 3?"
);
// For streaming with visible intermediate steps
await foreach (var update in agent.RunStreamingAsync(
"What's the population of Tokyo multiplied by 3?"))
{
Console.WriteLine(update);
}
// Key insight: Modern frameworks abstract the ReAct loop
// The pattern is built into the infrastructure When to Use Each Approach
| Scenario | Recommended Approach | Reason |
|---|---|---|
| Debugging/Development | Explicit ReAct | Visible reasoning traces aid debugging |
| Production with GPT-4 | Either | Model supports both well |
| Production with o1/R1 | Implicit (native tools) | Explicit prompting hurts performance |
| Open-source models | Explicit ReAct | More predictable behavior |
| Compliance/Audit needs | Explicit ReAct | Full reasoning trail required |
Evaluation Approach
Evaluating ReAct agents requires measuring both the reasoning quality and task completion:
| Metric | What it Measures | Target |
|---|---|---|
| Task Completion | Did the agent achieve the goal? | Binary or partial credit |
| Step Efficiency | Steps taken vs optimal path | Lower is better |
| Reasoning Quality | Are thoughts logical and relevant? | LLM-as-judge or human eval |
| Error Recovery | Can agent recover from mistakes? | % successful recoveries |
| Hallucination Rate | Made-up facts in reasoning | Lower is better |
Key metrics for evaluating ReAct agents
Benchmarks
- HotpotQA - Multi-hop reasoning questions
- FEVER - Fact verification requiring evidence
- ALFWorld - Embodied agent tasks
- WebShop - Web navigation and shopping
- SWE-bench - Software engineering tasks
Common Pitfalls
Infinite Loops
Reasoning-Action Mismatch
Over-thinking
Lost Context
Trajectory Analysis & Debugging
One of ReAct's key benefits is interpretability. You can analyze agent trajectories to understand failures:
Trajectory: Weather Query
─────────────────────────────────────────────────────
Step 1 │ Thought: Need weather for NYC
│ Action: search("NYC weather")
│ Result: ✓ Got weather data
─────────────────────────────────────────────────────
Step 2 │ Thought: Need to convert to Celsius
│ Action: calculator("75 - 32 * 5/9") ← BUG!
│ Result: ✗ Wrong formula (missing parens)
─────────────────────────────────────────────────────
Step 3 │ Thought: Result seems wrong, retry
│ Action: calculator("(75 - 32) * 5/9")
│ Result: ✓ Correct conversion
─────────────────────────────────────────────────────
Analysis:
- Model caught its own error (good recovery)
- Root cause: Math formatting issue
- Fix: Add examples to calculator tool description