ReAct Agent
Build an agent that reasons step-by-step before taking actions
ReAct Agent
TL;DR
ReAct = Reasoning + Acting. Instead of jumping straight to tool calls, the agent explicitly thinks aloud: "Thought: I need to search for X" → "Action: search" → "Observation: result" → repeat. This makes agents more interpretable, debuggable, and reliable.
| Difficulty | Intermediate |
| Time | ~4 hours |
| Code Size | ~350 LOC |
| Prerequisites | Tool Calling Agent |
Why ReAct?
In the Tool Calling Agent project, you built an agent that calls tools when the LLM decides it needs one. That works, but the LLM's reasoning is hidden -- you only see which tool was called, not why. If something goes wrong, you are left guessing.
ReAct fixes this by forcing the LLM to think out loud before every action. The reasoning is right there in the output, so you can trace exactly where the agent went off track.
Basic Tool Calling vs ReAct
Basic Tool Calling
LLM receives a question and directly emits a tool call. You see the tool name and input, but not the reasoning. If the agent picks the wrong tool, you have no way to tell why.
ReAct Agent
RecommendedLLM first writes a Thought explaining its plan, then chooses an Action. After each Observation, it reflects before continuing. Every decision is traceable and debuggable.
When to use ReAct over basic tool calling:
- You need to debug or audit agent behavior (the thought trace is a built-in log)
- The task requires multi-step reasoning where each step depends on the last
- You want the agent to self-correct after seeing unexpected results
- You are building for a domain where transparency matters (finance, healthcare, legal)
What You'll Learn
- The ReAct (Reasoning + Acting) pattern
- How to structure thought-action-observation loops
- Implementing self-reflection in agents
- Building more interpretable AI systems
Tech Stack
| Component | Technology | Why |
|---|---|---|
| LLM | OpenAI GPT-4o-mini / Claude | Fast, cheap model that follows the ReAct format reliably |
| Framework | LangChain / LangGraph | Provides tool abstractions and agent orchestration out of the box |
| Tracing | LangSmith | Visualizes each Thought/Action/Observation step for debugging |
| API | FastAPI | Async-ready Python API with automatic OpenAPI docs |
The ReAct Pattern
The ReAct Loop
Question
User asks: 'What is X...?'
Thought
'I need to search for X...'
Action
Execute tool: search('X')
Observation
Tool returns: 'X is defined as...'
Final Thought
'I now have enough information'
Final Answer
Synthesized answer returned to user
ReAct agents explicitly reason about what to do next, making their decision process transparent and debuggable.
Project Structure
react-agent/
├── src/
│ ├── __init__.py
│ ├── agent.py # ReAct agent implementation
│ ├── prompts.py # ReAct prompt templates
│ ├── tools.py # Available tools
│ ├── parser.py # Output parsing
│ └── api.py # FastAPI application
├── tests/
│ └── test_agent.py
├── requirements.txt
└── README.mdImplementation
Step 1: Project Setup
mkdir react-agent && cd react-agent
python -m venv venv
source venv/bin/activateopenai>=1.0.0
langchain>=0.1.0
langchain-openai>=0.0.5
langgraph>=0.0.20
langsmith>=0.0.60
fastapi>=0.100.0
uvicorn>=0.23.0
httpx>=0.25.0Step 2: ReAct Prompt Template
The key to ReAct is the prompt structure that enforces reasoning.
"""
ReAct prompt templates.
The ReAct pattern structures LLM output as:
Thought: reasoning about what to do
Action: the action to take
Action Input: input to the action
Observation: result of the action (added by system)
... (repeat)
Thought: I now know the final answer
Final Answer: the answer to the original question
"""
REACT_SYSTEM_PROMPT = """You are a helpful assistant that thinks step-by-step.
You have access to the following tools:
{tools}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original question
Important rules:
1. Always start with a Thought
2. If you need information, use a tool
3. After each Observation, reflect on what you learned
4. When you have enough information, provide Final Answer
5. Be concise but thorough in your reasoning
Begin!
Question: {input}
{agent_scratchpad}"""
TOOL_DESCRIPTION_TEMPLATE = """
{name}: {description}
Input: {input_schema}
"""
def format_tools(tools: list) -> tuple[str, str]:
"""Format tools for the prompt."""
tool_strings = []
tool_names = []
for tool in tools:
tool_strings.append(
f"- {tool['name']}: {tool['description']}"
)
tool_names.append(tool['name'])
return "\n".join(tool_strings), ", ".join(tool_names)Understanding the ReAct Prompt:
The prompt has three jobs, and each section targets one of them:
| Prompt Section | Purpose |
|---|---|
Tool listing ({tools}, {tool_names}) | Tells the LLM which tools exist so it only picks valid actions |
| Format block (Thought/Action/Observation template) | Forces the LLM to output in a parseable structure -- without this, it would produce free-form text |
| Rules 1-5 | Guard rails: always think first, use tools for facts, reflect after each observation, stop when done |
{agent_scratchpad} | The running log of previous Thought/Action/Observation rounds -- this is how the agent "remembers" what it already tried |
The stop=["Observation:"] parameter (used later in the LLM call) is critical: it prevents the LLM from hallucinating tool results. The agent generates up to the Observation: line, then your code fills in the real tool output.
Step 3: Output Parser
Parse the LLM output to extract thoughts, actions, and final answers.
"""
Parser for ReAct agent output.
Extracts structured data from the LLM's text response.
"""
import re
from dataclasses import dataclass
from typing import Optional, Union
@dataclass
class AgentAction:
"""An action the agent wants to take."""
thought: str
action: str
action_input: str
@dataclass
class AgentFinish:
"""The agent's final answer."""
thought: str
output: str
class ReActOutputParser:
"""Parse ReAct-formatted LLM output."""
def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
"""
Parse LLM output into action or final answer.
Args:
text: Raw LLM output
Returns:
AgentAction if more work needed, AgentFinish if done
"""
# Check for final answer
if "Final Answer:" in text:
return self._parse_final_answer(text)
# Otherwise, parse as action
return self._parse_action(text)
def _parse_final_answer(self, text: str) -> AgentFinish:
"""Extract final answer from text."""
# Get thought before final answer
thought_match = re.search(
r"Thought:\s*(.+?)(?=Final Answer:|$)",
text,
re.DOTALL
)
thought = thought_match.group(1).strip() if thought_match else ""
# Get final answer
answer_match = re.search(r"Final Answer:\s*(.+?)$", text, re.DOTALL)
answer = answer_match.group(1).strip() if answer_match else text
return AgentFinish(thought=thought, output=answer)
def _parse_action(self, text: str) -> AgentAction:
"""Extract action from text."""
# Extract thought
thought_match = re.search(
r"Thought:\s*(.+?)(?=Action:|$)",
text,
re.DOTALL
)
thought = thought_match.group(1).strip() if thought_match else ""
# Extract action
action_match = re.search(
r"Action:\s*(.+?)(?=Action Input:|$)",
text,
re.DOTALL
)
action = action_match.group(1).strip() if action_match else ""
# Extract action input
input_match = re.search(
r"Action Input:\s*(.+?)(?=Observation:|$)",
text,
re.DOTALL
)
action_input = input_match.group(1).strip() if input_match else ""
return AgentAction(
thought=thought,
action=action,
action_input=action_input
)How the parser works:
The parser uses regex to slice the LLM's text output into named fields. The key decision is the very first check: does the text contain Final Answer:?
- Yes -- the agent is done reasoning. Extract the final thought and answer into an
AgentFinishobject. - No -- the agent wants to use a tool. Extract the thought, action name, and action input into an
AgentActionobject.
Each regex follows the same pattern: match a label like Thought:, capture everything after it, and stop at the next label (or end of string). The re.DOTALL flag lets . match newlines, so multi-line thoughts are captured correctly.
LLM output text
│
├── Contains "Final Answer:"?
│ ├── YES → AgentFinish(thought, output)
│ └── NO → AgentAction(thought, action, action_input)Understanding the ReAct Output Format:
Parsing ReAct Output
Why ReAct is Better Than Direct Tool Calling:
| Aspect | Direct Tool Calling | ReAct |
|---|---|---|
| Debuggability | Black box decision | See reasoning in "Thought" |
| Course correction | Hard to intervene | Can stop after bad thought |
| Multi-step reasoning | Implicit | Explicit and traceable |
| Error recovery | Must retry whole request | Can reflect on observation |
Step 4: Tools
"""
Tools available to the ReAct agent.
"""
import httpx
import json
from datetime import datetime
def search(query: str) -> str:
"""
Search the web for information.
Args:
query: Search query string
"""
# Simulated search - replace with real API (Serper, Brave Search, SearXNG, etc.)
try:
# Use Wikipedia as a simple search backend
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{query.replace(' ', '_')}"
response = httpx.get(url, timeout=10.0)
if response.status_code == 200:
data = response.json()
return data.get("extract", "No results found.")[:500]
return f"No results found for: {query}"
except Exception as e:
return f"Search error: {str(e)}"
def calculate(expression: str) -> str:
"""
Calculate a mathematical expression.
Args:
expression: Math expression like "2 + 2" or "sqrt(16)"
"""
import math
allowed = {
"abs": abs, "round": round, "min": min, "max": max,
"sqrt": math.sqrt, "pow": pow, "log": math.log,
"sin": math.sin, "cos": math.cos, "pi": math.pi
}
try:
result = eval(expression, {"__builtins__": {}}, allowed)
return str(result)
except Exception as e:
return f"Calculation error: {str(e)}"
def get_date() -> str:
"""Get the current date and time."""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def lookup(term: str) -> str:
"""
Look up a specific term or fact.
Args:
term: The term to look up
"""
# Simulated knowledge base
knowledge = {
"python": "Python is a high-level programming language created by Guido van Rossum in 1991.",
"react": "React is a JavaScript library for building user interfaces, developed by Facebook.",
"langchain": "LangChain is a framework for developing applications powered by language models.",
"openai": "OpenAI is an AI research company that created GPT-4 and ChatGPT.",
}
term_lower = term.lower()
for key, value in knowledge.items():
if key in term_lower:
return value
return f"No information found for: {term}"
# Tool registry with metadata
TOOLS = [
{
"name": "search",
"description": "Search the web for current information about a topic",
"function": search
},
{
"name": "calculate",
"description": "Perform mathematical calculations",
"function": calculate
},
{
"name": "get_date",
"description": "Get the current date and time",
"function": get_date
},
{
"name": "lookup",
"description": "Look up facts about programming and technology",
"function": lookup
}
]
def get_tool(name: str):
"""Get a tool function by name."""
for tool in TOOLS:
if tool["name"] == name:
return tool["function"]
return NoneStep 5: ReAct Agent
"""
ReAct Agent implementation.
The agent follows the Thought -> Action -> Observation loop
until it reaches a final answer.
"""
from dataclasses import dataclass, field
from typing import Optional
from openai import OpenAI
from .prompts import REACT_SYSTEM_PROMPT, format_tools
from .parser import ReActOutputParser, AgentAction, AgentFinish
from .tools import TOOLS, get_tool
@dataclass
class ReActStep:
"""A single step in the ReAct loop."""
thought: str
action: Optional[str] = None
action_input: Optional[str] = None
observation: Optional[str] = None
@dataclass
class ReActResponse:
"""Complete response from the ReAct agent."""
answer: str
steps: list[ReActStep] = field(default_factory=list)
total_tokens: int = 0
class ReActAgent:
"""
An agent that reasons step-by-step using the ReAct pattern.
ReAct = Reasoning + Acting
The agent:
1. Thinks about what to do (Thought)
2. Decides on an action (Action)
3. Executes the action and observes result (Observation)
4. Repeats until it has enough information
5. Provides a final answer
"""
def __init__(
self,
model: str = "gpt-4o-mini",
max_iterations: int = 10,
verbose: bool = True
):
self.client = OpenAI()
self.model = model
self.max_iterations = max_iterations
self.verbose = verbose
self.parser = ReActOutputParser()
self.tools = TOOLS
def _build_prompt(self, question: str, scratchpad: str) -> str:
"""Build the full prompt with tools and scratchpad."""
tool_str, tool_names = format_tools(self.tools)
return REACT_SYSTEM_PROMPT.format(
tools=tool_str,
tool_names=tool_names,
input=question,
agent_scratchpad=scratchpad
)
def _call_llm(self, prompt: str) -> tuple[str, int]:
"""Call the LLM and return response + tokens."""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
stop=["Observation:"] # Stop before observation
)
return (
response.choices[0].message.content,
response.usage.total_tokens
)
def _execute_action(self, action: str, action_input: str) -> str:
"""Execute a tool and return the observation."""
tool_fn = get_tool(action)
if tool_fn is None:
return f"Error: Unknown tool '{action}'"
try:
# Handle tools with no input
if action == "get_date":
return tool_fn()
return tool_fn(action_input)
except Exception as e:
return f"Error executing {action}: {str(e)}"
def run(self, question: str) -> ReActResponse:
"""
Run the ReAct agent on a question.
Args:
question: The user's question
Returns:
ReActResponse with answer and reasoning steps
"""
scratchpad = ""
steps = []
total_tokens = 0
for i in range(self.max_iterations):
# Build prompt and call LLM
prompt = self._build_prompt(question, scratchpad)
llm_output, tokens = self._call_llm(prompt)
total_tokens += tokens
if self.verbose:
print(f"\n--- Iteration {i+1} ---")
print(llm_output)
# Parse the output
parsed = self.parser.parse(llm_output)
# Check if we're done
if isinstance(parsed, AgentFinish):
steps.append(ReActStep(thought=parsed.thought))
return ReActResponse(
answer=parsed.output,
steps=steps,
total_tokens=total_tokens
)
# Execute the action
observation = self._execute_action(
parsed.action,
parsed.action_input
)
if self.verbose:
print(f"Observation: {observation}")
# Record the step
steps.append(ReActStep(
thought=parsed.thought,
action=parsed.action,
action_input=parsed.action_input,
observation=observation
))
# Update scratchpad for next iteration
scratchpad += f"""Thought: {parsed.thought}
Action: {parsed.action}
Action Input: {parsed.action_input}
Observation: {observation}
"""
# Max iterations reached
return ReActResponse(
answer="I couldn't find a complete answer within the iteration limit.",
steps=steps,
total_tokens=total_tokens
)The agent loop, step by step:
┌─────────────────────────────────────────────────────────────┐
│ agent.run("What is the population of France?") │
├─────────────────────────────────────────────────────────────┤
│ │
│ for i in range(max_iterations): │
│ │ │
│ ├─ 1. Build prompt (question + scratchpad) │
│ │ Scratchpad = "" on first pass, grows each loop │
│ │ │
│ ├─ 2. Call LLM ──► returns text + token count │
│ │ stop=["Observation:"] prevents hallucination │
│ │ │
│ ├─ 3. Parse output ──► AgentAction or AgentFinish? │
│ │ │ │
│ │ ├─ AgentFinish? ──► return final answer │
│ │ │ │
│ │ └─ AgentAction? ──► continue to step 4 │
│ │ │
│ ├─ 4. Execute tool (search, calculate, etc.) │
│ │ Returns observation string │
│ │ │
│ └─ 5. Append Thought/Action/Observation to scratchpad │
│ Loop back to step 1 with updated context │
│ │
│ If max_iterations reached ──► return fallback message │
└─────────────────────────────────────────────────────────────┘Key design decisions:
| Decision | Why |
|---|---|
stop=["Observation:"] | The LLM must not generate fake observations -- only real tool output goes there |
max_iterations=10 | Safety net to prevent infinite loops if the agent keeps finding new things to investigate |
| Scratchpad as a growing string | Each iteration sees all prior reasoning, so the agent builds on its own work |
temperature=0.7 | Slight creativity helps with varied phrasing, but keeps reasoning mostly deterministic |
Step 6: FastAPI Application
"""FastAPI application for ReAct agent."""
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Optional
from .agent import ReActAgent, ReActResponse
app = FastAPI(
title="ReAct Agent API",
description="An AI agent that reasons step-by-step",
version="1.0.0"
)
agent = ReActAgent(verbose=False)
class QueryRequest(BaseModel):
question: str = Field(..., min_length=1, max_length=1000)
verbose: bool = False
class StepResponse(BaseModel):
thought: str
action: Optional[str] = None
action_input: Optional[str] = None
observation: Optional[str] = None
class QueryResponse(BaseModel):
answer: str
reasoning_steps: list[StepResponse]
total_tokens: int
@app.post("/query", response_model=QueryResponse)
async def query(request: QueryRequest):
"""Ask the ReAct agent a question."""
try:
agent.verbose = request.verbose
result = agent.run(request.question)
return QueryResponse(
answer=result.answer,
reasoning_steps=[
StepResponse(
thought=s.thought,
action=s.action,
action_input=s.action_input,
observation=s.observation
)
for s in result.steps
],
total_tokens=result.total_tokens
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/")
async def health():
return {"status": "healthy", "agent": "react"}Running the Agent
export OPENAI_API_KEY="your-key"
uvicorn src.api:app --reload --port 8000# Test the agent
curl -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"question": "What is the population of France and what is that number divided by 1000?"}'Example: Watching ReAct Think
Here is a real trace for the question "What is the population of France and what is its square root?" -- a question that requires two tools (search, then calculate) and three iterations:
Question: What is the population of France and what is its square root?
--- Iteration 1 ---
Thought: I need to find the current population of France. Let me search for it.
Action: search
Action Input: population of France
Observation: France has a population of approximately 68.4 million people
as of 2025.
--- Iteration 2 ---
Thought: The population of France is approximately 68,400,000. Now I need to
calculate the square root of that number.
Action: calculate
Action Input: sqrt(68400000)
Observation: 8272.47
--- Iteration 3 ---
Thought: I now know the final answer. The population is ~68.4 million and
its square root is ~8,272.
Final Answer: The population of France is approximately 68.4 million
(68,400,000). The square root of 68,400,000 is approximately 8,272.47.Notice how each iteration builds on the last. The agent did not try to calculate the square root until it had a concrete number from the search. That is the core value of ReAct: the explicit Thought step forces the agent to plan before it acts.
Key Benefits of ReAct
| Benefit | Description |
|---|---|
| Interpretability | See exactly why the agent made each decision |
| Debugging | Easy to identify where reasoning went wrong |
| Reliability | Step-by-step thinking reduces errors |
| Control | Can intervene or guide the reasoning process |
Key Concepts Recap
| Concept | What It Is | Why It Matters |
|---|---|---|
| ReAct | Reasoning + Acting pattern | Makes agent thinking explicit and auditable |
| Thought | LLM's reasoning about next step | Shows why the agent makes decisions |
| Action | Tool to execute | What the agent decides to do |
| Observation | Tool result fed back to LLM | Gives agent information to continue reasoning |
| Scratchpad | Accumulated thoughts/actions/observations | Maintains context across iterations |
| Agent Finish | Final answer with reasoning complete | Signals the loop should end |
| Stop Tokens | Tokens that halt LLM generation | Prevents LLM from generating fake observations |
Next Steps
- Conversational Agent - Add memory to your agent
- Planning Agent - Plan complex tasks before executing