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.

The ReAct Loop
┌─────────────────────────────────────────────────────────┐
│                      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

ReAct was introduced in "ReAct: Synergizing Reasoning and Acting in Language Models" (Yao et al., 2022). It showed that combining reasoning traces with actions outperforms either approach alone.

Basic ReAct Implementation

The core ReAct pattern combines three elements in a loop: thinking about what to do, taking action, and observing the results.

ReAct Agent Loop
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
# 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

Research shows that explicit Chain-of-Thought prompting can degrade performance by 3-5% on reasoning models like o1 and DeepSeek-R1. These models have internalized the reasoning process.
Modern Approach: Implicit Reasoning
# 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

Without proper termination conditions, agents can loop indefinitely. Always set maximum step limits and detect repetitive behavior.

Reasoning-Action Mismatch

The model might state an intention in its thought but take a different action. Validate that actions align with stated reasoning.

Over-thinking

Some tasks don't need multi-step reasoning. Don't force the ReAct pattern on simple queries that could be answered directly.

Lost Context

In long traces, the model may forget earlier observations. Consider summarizing history or using memory systems.

Trajectory Analysis & Debugging

One of ReAct's key benefits is interpretability. You can analyze agent trajectories to understand failures:

Trajectory Analysis
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

Best Practice

Log full trajectories in production. When failures occur, the trace shows exactly where and why the agent went wrong, making debugging straightforward.

Related Topics