How to Build a ReAct Agent with Claude and Tool Use in 2026
TL;DR
- Build a ReAct agent that breaks down complex questions using a thought-action-observation loop
- Implement three tools (web search, calculator, file reader) that Claude can call autonomously
- Create a reasoning loop that continues until the agent has enough information to answer
- End result: An agent that can answer questions like “What’s the population density of the city where the current US president was born?” by searching, calculating, and reasoning step-by-step
Prerequisites
Before starting, make sure you have:
Required:
- Python 3.11+ installed on your system
- An Anthropic API key (sign up at console.anthropic.com, $5 free credit for new accounts)
- pip package manager (comes with Python)
- A code editor (VS Code, PyCharm, or any text editor)
Knowledge:
- Basic Python syntax (functions, loops, dictionaries)
- Familiarity with API calls and JSON
- Understanding of command-line basics
Time:
- ~45 minutes to complete
- ~15 minutes to experiment with different queries
Cost:
- Claude API: ~$0.01-0.05 per agent run (using Claude 3.5 Sonnet)
- Tavily Search API: Free tier includes 1,000 searches/month
What We’re Building
We’re creating a ReAct (Reasoning + Acting) agent that thinks through problems iteratively. Unlike simple chatbots that respond once, our agent will:
- Think about what information it needs
- Act by calling tools (search, calculate, read files)
- Observe the results
- Repeat until it can answer confidently
Architecture flow:
User Question → Agent Reasoning (Claude)
↓
Need info?
↓
Call Tool (Action)
↓
Get Result (Observation)
↓
Update Context → Repeat
↓
Final Answer
Why this matters: ReAct agents bridge the gap between LLMs’ reasoning abilities and real-world data access. This pattern is the foundation for autonomous agents that can complete complex, multi-step tasks.
Step 1: Set Up Your Project Environment
Create a new directory and set up a virtual environment to keep dependencies isolated.
mkdir react-agent-tutorial
cd react-agent-tutorial
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
Install the required packages:
pip install anthropic tavily-python python-dotenv
Expected output:
Successfully installed anthropic-0.28.0 tavily-python-0.3.0 python-dotenv-1.0.0 ...
What we installed:
anthropic: Official Claude API clienttavily-python: Web search API wrapper (alternative to Google Search API)python-dotenv: Secure environment variable management
Create a .env file to store your API keys:
touch .env
Add your keys to .env:
ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
TAVILY_API_KEY=tvly-your-key-here
Get your Tavily API key at tavily.com (free tier, no credit card required).
Step 2: Define the Tool Schemas
Claude needs to know what tools are available and how to use them. We define tools using JSON schemas that describe their purpose, parameters, and types.
Create tools.py:
# tools.py
TOOLS = [
{
"name": "web_search",
"description": "Search the internet for current information. Use this when you need recent data, facts, news, or information not in your training data. Returns a summary of the top search results.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query. Be specific and include relevant keywords."
}
},
"required": ["query"]
}
},
{
"name": "calculator",
"description": "Perform mathematical calculations. Supports basic arithmetic, exponents, and Python math expressions. Use this for any numerical computation.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A mathematical expression to evaluate (e.g., '(45 + 32) * 1.5' or '2**8')"
}
},
"required": ["expression"]
}
},
{
"name": "read_file",
"description": "Read the contents of a text file from the local filesystem. Use this to access data stored in files.",
"input_schema": {
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "The path to the file to read (relative or absolute)"
}
},
"required": ["filepath"]
}
}
]
What this does:
- Each tool has a
name,description, andinput_schema - The
descriptiontells Claude when to use the tool - The
input_schemadefines what parameters Claude must provide - Claude will parse these and decide which tool to call based on the user’s question
Step 3: Implement the Tool Functions
Now we’ll write the actual Python functions that execute when Claude calls a tool.
Create tool_executor.py:
# tool_executor.py
import os
import math
from tavily import TavilyClient
from dotenv import load_dotenv
load_dotenv()
# Initialize Tavily client
tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
def web_search(query: str) -> str:
"""
Search the web using Tavily API.
Returns a formatted string with the top results.
"""
try:
response = tavily_client.search(
query=query,
search_depth="basic",
max_results=3
)
if not response.get('results'):
return "No results found."
# Format results into readable text
results = []
for idx, result in enumerate(response['results'], 1):
results.append(
f"{idx}. {result['title']}\n"
f" {result['content']}\n"
f" Source: {result['url']}"
)
return "\n\n".join(results)
except Exception as e:
return f"Search error: {str(e)}"
def calculator(expression: str) -> str:
"""
Safely evaluate a mathematical expression.
Returns the result or an error message.
"""
try:
# Create a safe namespace with math functions
safe_namespace = {
"__builtins__": {},
"math": math,
"abs": abs,
"round": round,
"min": min,
"max": max,
"sum": sum,
}
result = eval(expression, safe_namespace)
return str(result)
except Exception as e:
return f"Calculation error: {str(e)}"
def read_file(filepath: str) -> str:
"""
Read and return the contents of a text file.
Returns the file contents or an error message.
"""
try:
# Security: only allow reading from current directory or subdirectories
abs_path = os.path.abspath(filepath)
current_dir = os.path.abspath(".")
if not abs_path.startswith(current_dir):
return "Error: Can only read files from the current directory or subdirectories."
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
return content if content else "File is empty."
except FileNotFoundError:
return f"Error: File '{filepath}' not found."
except Exception as e:
return f"Error reading file: {str(e)}"
# Map tool names to functions
TOOL_FUNCTIONS = {
"web_search": web_search,
"calculator": calculator,
"read_file": read_file,
}
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""
Execute a tool by name with the provided input.
Returns the result as a string.
"""
if tool_name not in TOOL_FUNCTIONS:
return f"Error: Unknown tool '{tool_name}'"
func = TOOL_FUNCTIONS[tool_name]
# Call the function with unpacked kwargs
try:
result = func(**tool_input)
return result
except TypeError as e:
return f"Error: Invalid parameters for {tool_name}: {str(e)}"
Key implementation details:
- web_search: Uses Tavily to get real-time web results with titles, snippets, and URLs
- calculator: Uses
eval()with a restricted namespace to prevent code injection - read_file: Includes path validation to prevent reading sensitive system files
- execute_tool: Central dispatcher that routes tool calls to the correct function
Step 4: Build the ReAct Agent Loop
This is the core reasoning engine. The agent will loop, calling Claude repeatedly until it has enough information to answer.
Create react_agent.py:
# react_agent.py
import os
import anthropic
from dotenv import load_dotenv
from tools import TOOLS
from tool_executor import execute_tool
load_dotenv()
class ReActAgent:
def __init__(self, model="claude-3-5-sonnet-20241022"):
self.client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
self.model = model
self.max_iterations = 10 # Prevent infinite loops
def run(self, user_query: str, verbose: bool = True) -> str:
"""
Run the ReAct loop to answer a user query.
Args:
user_query: The question to answer
verbose: Whether to print reasoning steps
Returns:
The final answer as a string
"""
messages = [
{
"role": "user",
"content": user_query
}
]
iteration = 0
while iteration < self.max_iterations:
iteration += 1
if verbose:
print(f"\n{'='*60}")
print(f"ITERATION {iteration}")
print(f"{'='*60}")
# Call Claude with tool definitions
response = self.client.messages.create(
model=self.model,
max_tokens=4096,
tools=TOOLS,
messages=messages
)
if verbose:
print(f"\nStop Reason: {response.stop_reason}")
# Check if Claude wants to use a tool
if response.stop_reason == "tool_use":
# Process all tool uses in this turn
tool_results = []
for block in response.content:
if block.type == "text":
if verbose:
print(f"\n💭 Thought: {block.text}")
elif block.type == "tool_use":
tool_name = block.name
tool_input = block.input
tool_use_id = block.id
if verbose:
print(f"\n🔧 Action: {tool_name}")
print(f" Input: {tool_input}")
# Execute the tool
result = execute_tool(tool_name, tool_input)
if verbose:
print(f"\n📊 Observation: {result[:200]}...") if len(result) > 200 else print(f"\n📊 Observation: {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": result
})
# Add assistant's response to message history
messages.append({
"role": "assistant",
"content": response.content
})
# Add tool results to message history
messages.append({
"role": "user",
"content": tool_results
})
elif response.stop_reason == "end_turn":
# Claude has finished reasoning and has a final answer
final_answer = ""
for block in response.content:
if block.type == "text":
final_answer += block.text
if verbose:
print(f"\n✅ Final Answer:\n{final_answer}")
return final_answer
else:
# Unexpected stop reason
return f"Unexpected stop reason: {response.stop_reason}"
return "Max iterations reached without a final answer."
# Example usage
if __name__ == "__main__":
agent = ReActAgent()
query = "What is the population density of Scranton, Pennsylvania? First find the current population, then the area, then calculate the density."
answer = agent.run(query, verbose=True)
How the loop works:
- Send message to Claude with tool definitions and conversation history
- Check stop_reason:
tool_use: Claude wants to call a tool → execute it and continue loopend_turn: Claude has the final answer → return it
- Execute tools: Call our Python functions and collect results
- Add to history: Append both Claude’s response and tool results to messages
- Repeat until Claude says it’s done or max iterations reached
Important: We maintain the full conversation history in messages. This gives Claude context from all previous reasoning steps.
Step 5: Create Test Data and Example Queries
Let’s create a sample data file to test the read_file tool.
Create sample_data.txt:
Project Status Report - Q1 2026
Budget: $450,000 allocated, $387,500 spent (86%)
Team Size: 12 engineers, 3 designers, 2 product managers
Completion: 67% complete
Key Milestones:
- API v2 launch: Completed (Jan 15)
- Mobile app beta: In progress (on track for Feb 28)
- Enterprise features: Scheduled for March
Risks: Database migration delayed by 2 weeks due to schema complexity.
Now create a test script to run different query types:
Create test_agent.py:
# test_agent.py
from react_agent import ReActAgent
def test_agent():
agent = ReActAgent()
test_queries = [
# Test 1: Simple calculation
"What is 234 multiplied by 567, then divided by 12?",
# Test 2: Web search
"Who is the current CEO of Anthropic and when did they found the company?",
# Test 3: File reading
"Read the sample_data.txt file and tell me what percentage of the Q1 budget has been spent.",
# Test 4: Multi-step reasoning (search + calculation)
"Find the current population of Tokyo and calculate how many people that is per square kilometer if Tokyo is 2,194 square kilometers.",
# Test 5: Complex multi-tool query
"Read the sample_data.txt file, then search for the current stock price of any company mentioned, and calculate the total market cap if they have 1 million shares.",
]
for i, query in enumerate(test_queries, 1):
print(f"\n\n{'#'*80}")
print(f"TEST {i}: {query}")
print(f"{'#'*80}")
try:
answer = agent.run(query, verbose=True)
print(f"\n\n{'='*80}")
print("FINAL RESULT:")
print(answer)
print(f"{'='*80}")
except Exception as e:
print(f"\n❌ Error: {e}")
input("\nPress Enter to continue to next test...")
if __name__ == "__main__":
test_agent()
Step 6: Run Your ReAct Agent
Execute the test script:
python test_agent.py
Expected output for Test 1 (calculation):
============================================================
ITERATION 1
============================================================
Stop Reason: tool_use
💭 Thought: I need to calculate 234 * 567 / 12. Let me use the calculator tool.
🔧 Action: calculator
Input: {'expression': '234 * 567 / 12'}
📊 Observation: 11056.5
============================================================
ITERATION 2
============================================================
Stop Reason: end_turn
✅ Final Answer:
The result of 234 multiplied by 567, then divided by 12 is 11,056.5.
Expected output for Test 4 (multi-step):
============================================================
ITERATION 1
============================================================
Stop Reason: tool_use
💭 Thought: I need to find Tokyo's current population. Let me search for this information.
🔧 Action: web_search
Input: {'query': 'Tokyo current population 2026'}
📊 Observation: 1. Tokyo Population 2026
Tokyo's population is approximately 14.09 million as of 2026...
Source: https://worldpopulationreview.com/...
============================================================
ITERATION 2
============================================================
Stop Reason: tool_use
💭 Thought: Now I have the population (14.09 million) and the area (2,194 km²). I need to calculate the density.
🔧 Action: calculator
Input: {'expression': '14090000 / 2194'}
📊 Observation: 6422.151957238742
============================================================
ITERATION 3
============================================================
Stop Reason: end_turn
✅ Final Answer:
Tokyo has a population density of approximately 6,422 people per square kilometer. With a population of about 14.09 million people spread across 2,194 square kilometers, this makes Tokyo one of the most densely populated major cities in the world.
Step 7: Add Logging and Debugging
To better understand agent behavior, add structured logging.
Create agent_logger.py:
# agent_logger.py
import json
from datetime import datetime
from pathlib import Path
class AgentLogger:
def __init__(self, log_dir="logs"):
self.log_dir = Path(log_dir)
self.log_dir.mkdir(exist_ok=True)
self.current_session = None
def start_session(self, query: str):
"""Start a new agent session."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.current_session = {
"timestamp": timestamp,
"query": query,
"iterations": [],
"final_answer": None,
"total_iterations": 0
}
def log_iteration(self, iteration: int, thought: str, actions: list, observations: list):
"""Log a single iteration of the ReAct loop."""
if not self.current_session:
return
self.current_session["iterations"].append({
"iteration": iteration,
"thought": thought,
"actions": actions,
"observations": observations
})
def end_session(self, final_answer: str):
"""End the session and save to file."""
if not self.current_session:
return
self.current_session["final_answer"] = final_answer
self.current_session["total_iterations"] = len(self.current_session["iterations"])
# Save to JSON file
filename = f"session_{self.current_session['timestamp']}.json"
filepath = self.log_dir / filename
with open(filepath, 'w') as f:
json.dump(self.current_session, f, indent=2)
print(f"\n📝 Session logged to {filepath}")
Update react_agent.py to use the logger:
# Add to ReActAgent.__init__
from agent_logger import AgentLogger
self.logger = AgentLogger()
# Add to run() method at the start
self.logger.start_session(user_query)
# Add after each iteration
iteration_actions = []
iteration_observations = []
iteration_thought = ""
for block in response.content:
if block.type == "text":
iteration_thought = block.text
elif block.type == "tool_use":
iteration_actions.append({"tool": block.name, "input": block.input})
# After executing tool:
iteration_observations.append({"tool": block.name, "result": result})
self.logger.log_iteration(iteration, iteration_thought, iteration_actions, iteration_observations)
# Add before returning final answer
self.logger.end_session(final_answer)
Testing Your Implementation
Run a comprehensive test to verify everything works:
python -c "from react_agent import ReActAgent; agent = ReActAgent(); print(agent.run('What is 15 percent of 240?'))"
Expected output:
============================================================
ITERATION 1
============================================================
Stop Reason: tool_use
🔧 Action: calculator
Input: {'expression': '240 * 0.15'}
📊 Observation: 36.0
============================================================
ITERATION 2
============================================================
✅ Final Answer:
15 percent of 240 is 36.
📝 Session logged to logs/session_20260315_143022.json
Verify the logs:
ls logs/
cat logs/session_*.json
You should see structured JSON showing the complete reasoning chain.
Common Issues & Fixes
Problem: anthropic.APIConnectionError: Connection error
Cause: Invalid or missing API key
Fix: Verify your .env file has the correct key and run source .env or restart your terminal
echo $ANTHROPIC_API_KEY # Should print your key
Problem: Agent loops indefinitely without reaching an answer
Cause: Tool returns unhelpful results or Claude gets stuck in a reasoning loop
Fix: Reduce max_iterations to 5 and improve tool descriptions to be more specific about when to use each tool
self.max_iterations = 5 # Fail faster
# Make descriptions more directive
"description": "Search the web for CURRENT information about events, people, or facts after 2023. Do NOT use for calculations or file operations."
Problem: eval() security warning from code analysis tools
Cause: Using eval() can execute arbitrary code if input isn’t sanitized
Fix: Use ast.literal_eval() for safer evaluation or switch to a dedicated math parser:
import ast
import operator
# Safe operator whitelist
ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
}
def safe_eval(expression):
node = ast.parse(expression, mode='eval')
# Walk the AST and only allow whitelisted operations
# (Full implementation left as exercise)
Problem: Web search returns “No results found” for valid queries Cause: Tavily API key invalid or rate limit exceeded Fix: Check your Tavily dashboard for quota and verify the key:
# Add debug logging
print(f"Searching for: {query}")
print(f"API Key: {os.getenv('TAVILY_API_KEY')[:10]}...") # Print first 10 chars
Problem: File reading fails with “Permission denied” Cause: Trying to read a file outside the project directory or without read permissions Fix: Use absolute paths and check permissions:
ls -la sample_data.txt # Should show read permissions
chmod 644 sample_data.txt # Add read permissions if needed
Next Steps
Now that you have a working ReAct agent, here are ways to extend it:
1. Add More Tools
- Email sender: Integrate with SendGrid or Gmail API
- Database query: Connect to PostgreSQL/MySQL
- Image analysis: Use Claude’s vision API or OpenAI DALL-E
- Code executor: Safely run Python/JavaScript snippets in a sandbox
2. Implement Streaming Responses
Claude’s API supports streaming. Update the agent to show reasoning in real-time:
with self.client.messages.stream(
model=self.model,
max_tokens=4096,
tools=TOOLS,
messages=messages
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
3. Add Memory and Context Persistence
Store conversation history in a vector database (Pinecone, Weaviate) to give your agent long-term memory across sessions.
4. Build a Web Interface
Create a FastAPI backend and React frontend to let users interact with your agent via a chat UI.
Challenge: Extend this agent to handle multi-step planning. Before executing tools, have the agent first create a plan (a list of steps), then execute each step sequentially. This improves reliability for complex, multi-tool queries.
Related Resources:
- Anthropic Tool Use Documentation
- ReAct Paper (Yao et al., 2022)
- Building Autonomous Agents Tutorial (coming soon)
You now have a production-ready ReAct agent that can reason through complex problems by iteratively using tools. The pattern you’ve learned here scales to any tool you can wrap in a Python function—from APIs to databases to custom business logic.