AI Agents with Tool Use: ReAct in Practice
Build autonomous AI agents that can use external tools and APIs to accomplish complex tasks.
Overview
A chatbot talks; an agent acts. Give an LLM a set of tools — search, a database, an API — and it stops being a text generator and starts being a worker that searches the web, queries your tables, runs code, and chains the results into something a plain chatbot can’t do alone. For when the loop itself breaks, the LLM Fallback Pattern is the safety net worth wiring in first.
When to Use
Reach for this when the job needs more than one model call:
- Assistants that must fetch live data — search, databases, internal APIs
- Workflows where several API calls have to chain in the right order
- Tasks where the model reasons over external knowledge, not just training data
- Systems that should retry a failed step instead of dying on the first error
Solution
ReAct Pattern Agent (Python)
import openai
import json
from typing import Callable
def agent_react(query: str, tools: dict[str, Callable]) -> str:
messages = [
{"role": "system", "content": "You are a helpful assistant. Use tools when needed."},
{"role": "user", "content": query}
]
for _ in range(5): # Max iterations
response = openai.chat.completions.create(
model="gpt-4",
messages=messages,
tools=[
{"type": "function", "function": {"name": n, "parameters": {}}}
for n in tools.keys()
]
)
msg = response.choices[0].message
if not msg.tool_calls:
return msg.content
messages.append(msg)
for call in msg.tool_calls:
result = tools[call.function.name](**json.loads(call.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": str(result)
})
return "Max iterations reached"
Tool Definition Example
def search_wikipedia(query: str) -> str:
"""Search Wikipedia for a topic."""
# Implementation omitted
return f"Results for {query}"
tools = {"search_wikipedia": search_wikipedia}
result = agent_react("Who won the 2022 FIFA World Cup?", tools)
Explanation
The ReAct loop is deceptively simple: the model thinks about what to do next (Thought), calls a tool with structured arguments (Action), reads the result back into context (Observation), and repeats until it produces a final answer — or hits the step ceiling you remembered to set.
Two decisions carry most of the weight: tool schemas in OpenAI’s function-calling format keep arguments typed instead of hallucinated, and an iteration limit turns “agent that can’t decide” from an infinite bill into a clean failure.
- Error handling: Tools should return errors gracefully, not crash
- Context window: Summarize long tool outputs to fit token limits
Variants
| Framework | Pattern | Best For |
|---|---|---|
| LangChain Agents | ReAct, Plan-and-Execute | Rapid prototyping |
| AutoGen | Multi-agent conversation | Collaborative tasks |
| CrewAI | Role-based agents | Business workflows |
| Custom | ReAct with tool registry | Production systems |
What Works
The rules that separate a working tool-call setup from a flaky one: every tool gets a name, a description a model can reason about, and a JSON schema — vague definitions produce vague calls. Keep the count low; three to five sharp tools beat twenty mushy ones every time. Validate arguments before executing — the model will invent them eventually. Log every step, because the reasoning is otherwise a black box. And time out your tools — external calls hang, and a hung tool hangs the whole loop.
Common Mistakes
The failures rhyme across every agent we’ve seen. Tool sprawl — twenty tools confuse the model into wrong calls; three to five sharp ones win. No error path: a failed tool call without recovery crashes the loop or worse, gets silently hallucinated around. Context exhaustion: long observation histories eat the window until the model forgets why it’s here — cap and summarize. Trusting outputs: hallucinated arguments are a when, not an if — validate every call. And no kill switch: autonomous agents need a human stop button, especially when the tools can write, send, or spend.
Plan-and-Execute Pattern
import openai
import json
def plan_and_execute(query: str, tools: dict) -> str:
# Step 1: Generate a plan
plan_response = openai.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "Break the task into steps. Return a JSON array of steps."},
{"role": "user", "content": query}
],
response_format={"type": "json_object"}
)
steps = json.loads(plan_response.choices[0].message.content)["steps"]
# Step 2: Execute each step
results = []
for step in steps:
result = agent_react(step, tools)
results.append(result)
# Step 3: Synthesize
final = openai.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "Synthesize the results into a final answer."},
{"role": "user", "content": f"Query: {query}\nResults: {json.dumps(results)}"}
]
)
return final.choices[0].message.content
Plan-and-Execute separates planning from execution. The planner breaks the task into steps, each step runs through a ReAct loop, and a final call synthesizes results. This reduces token consumption because each step has a smaller context window — and independent steps can run in parallel.
Tool Schemas with JSON Schema
tool_schemas = [
{
"type": "function",
"function": {
"name": "search_database",
"description": "Search the product database by name or category",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search term"
},
"category": {
"type": "string",
"enum": ["electronics", "books", "clothing"],
"description": "Product category filter"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 10
}
},
"required": ["query"]
}
}
}
]
Detailed tool schemas improve agent accuracy. Include description for every parameter — the LLM reads these to decide when and how to call the tool. Use enum for constrained values, minimum/maximum for numeric ranges, and required for mandatory fields. The more precise the schema, the fewer hallucinated arguments.
Context Window Management
def summarize_observations(messages: list, max_tokens: int = 4000) -> list:
"""Summarize tool outputs when context grows too large."""
total = sum(len(str(m.get("content", ""))) for m in messages)
if total < max_tokens:
return messages
# Keep system + first user message, summarize the rest
system = messages[0]
user_query = messages[1]
history = messages[2:]
summary = openai.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "Summarize the tool calls and results concisely."},
{"role": "user", "content": json.dumps([{"role": m["role"], "content": m["content"][:500]} for m in history])}
]
).choices[0].message.content
return [system, user_query, {"role": "system", "content": f"Previous context: {summary}"}]
Long agent runs accumulate large observation histories that exhaust the context window. Summarize periodically by keeping the system prompt and original query, then compressing tool outputs into a summary. Alternatively, use sliding-window truncation or retrieval-augmented context selection to keep only relevant observations.
Multi-Agent Orchestration
def multi_agent_orchestrate(query: str) -> str:
agents = {
'researcher': {
'system': 'You are a research agent. Find information using search tools.',
'tools': ['web_search', 'read_file']
},
'coder': {
'system': 'You are a coding agent. Write and execute code.',
'tools': ['run_python', 'write_file']
},
'reviewer': {
'system': 'You are a review agent. Check code quality and correctness.',
'tools': ['read_file', 'run_tests']
}
}
# Route to researcher first
research = run_agent(agents['researcher'], query)
# Pass research to coder
code = run_agent(agents['coder'], f"Based on: {research}\nImplement the solution.")
# Review the code
review = run_agent(agents['reviewer'], f"Review this code:\n{code}")
if 'APPROVE' in review:
return code
else:
# Re-route to coder with feedback
return run_agent(agents['coder'], f"Fix issues:\n{review}\nOriginal:\n{code}")
Multi-agent orchestration assigns specialized roles to different agents. Each agent has its own system prompt and tool set. A coordinator routes tasks between agents based on the current state. This pattern works well for complex workflows: research → implement → review → deploy. Keep agent communication structured — pass context as formatted strings or JSON, not raw message histories.
When to Avoid
An agent loop isn’t free — skip it where it buys nothing. Single-call lookups — one LLM call answers it — don’t deserve the latency and cost of a loop. High-stakes calls (medical, legal, trading) can’t afford hallucinated arguments, period. Real-time systems can’t afford the loop either: five LLM calls is five to thirty seconds. And when the meter matters, each iteration is a full call — a ten-step agent is ten times the bill for the same answer a chain would give.
Troubleshooting
- Model outputs are inconsistent: set temperature to 0 for deterministic tasks, use seed where supported, and version the prompt.
- Prompt injection leaks context: separate user input from system instructions.
- High token costs: cache embeddings, summarize long context, and choose smaller models for simple tasks.
- Retrieval returns irrelevant chunks: tune chunk size, overlap, and metadata filters. Evaluate retrieval metrics separately from generation.
- Evaluation scores don’t match human judgment: define clear rubrics, use several judges, and track disagreement. Human review is still the ground truth.
Further Reading
- OpenAI — Function Calling Guide — tool use mechanics
- Anthropic — Tool Use Documentation — the same pattern on Claude
- LangChain Agents — the framework implementation
- ReAct: Synergizing Reasoning and Acting — the pattern’s foundational paper
Frequently Asked Questions
What is the difference between RAG and an agent?
RAG retrieves documents and answers once. Agents can take several actions, use tools, and iterate until a goal is met.
How many tools should an agent have?
Start with two or three. Past five to seven, the model starts picking the wrong tool often enough to notice — accuracy drops fast with every description you add.
Can agents run without OpenAI?
Sure — local models like Llama and Mistral do tool calling through JSON mode or similar structured output. The loop is the same; only the API changes.
How do I handle tool call failures?
Catch them in the tool wrapper and feed the error back as a tool result — the agent usually retries with fixed arguments or picks a different tool. Cap the retries so a stuck tool can't loop forever, and log the full trace while giving the agent a short error it can actually use.
What is the cost of running an agent loop?
One LLM call per iteration. A 5-step loop at ~2,000 tokens per call runs about $0.60 on GPT-4 — the same loop on GPT-4o-mini is under a cent. Route with the cheap model and save the expensive one for steps that actually need reasoning. Caching tool results skips calls you already paid for.
How do I test agent behavior?
Mock LLM responses and tool calls for unit tests; for integration tests, stub the tools so they return predictable results. Recording production traces and replaying them catches regressions. Skip live APIs in tests — flaky services make flaky tests.
When should I use multi-agent orchestration?
When the task splits cleanly into roles with different prompts or tool sets — research, implement, review. If one agent with all the tools handles it fine, stay there; orchestration adds coordination cost you don't need yet.
Related Resources
Build Autonomous AI Agents with Tool Use and Reasoning
How to design AI agents that autonomously plan, execute tools, and iterate toward goals using ReAct, function calling, and memory architectures.
RecipeBuild Semantic Search with Embeddings in Python, JS, Java
Build a semantic search engine with text embeddings and vector similarity. Includes Python, JavaScript, and Java examples with FAISS, pgvector, and OpenAI.
RecipeBuild a Slack Bot with OpenAI: Mentions, DMs, Memory
A conversational Slack bot powered by OpenAI that answers mentions and direct messages, keeps per-user conversation history, and stays under budget
RecipeOpenAI Assistants API Chatbot: Build, Cost & Deploy
How to create an AI chatbot using the OpenAI Assistants API with function calling and file search.
RecipeGenerate Images Programmatically with AI Models
How to create, edit, and optimize images using DALL-E, Stable Diffusion, and Midjourney APIs with prompt engineering, batch processing, and content moderation.
RecipeSentiment Analysis with Python and NLTK
Score text sentiment using NLTK VADER and custom lexicons in Python. Classify reviews, process CSVs, and track sentiment trends with copy-paste examples.