AI Agents: Mastering 3 Essential Patterns (ReAct). Part 2 of 3
Source: Dev.to
Article [Part 1]
The code for these patterns is available on GitHub. [Repo]
“Tool‑Using” Pattern (Article 1)
We gave the AI hands to interact with the outside world.
With ReAct, we are installing a functional brain.
Recap
-
In Article 1 the flow was completely linear:
Stimulus → ResponseYou asked for a piece of data, the agent fired a tool, and that was it.
No doubts, no plans. -
The real world is messy.
What happens when a single click isn’t enough?
What if you need to investigate, compare data, and then perform a calculation?
That is where the linear model crashes and ReAct (proposed by researchers at Princeton and Google in 2022) comes into play. It is basically the industry standard today.
The name comes from Reasoning + Acting.
The breakthrough idea isn’t just using tools (we already had that); we force the LLM to have an internal monologue – a Thought Trace.
Instead of rushing to answer blindly, the agent enters a cognitive loop:
The agent literally talks to itself in the logs.
It plans its next step based on what it just discovered a second ago, allowing it to course‑correct on the fly if things don’t go as expected.
Under the Hood – Example
User query: “What is the square root of the age of the current President of France?”
| Iteration | Action | Observation |
|---|---|---|
| 1 | search_tool("Current President of France") | Returns “Emmanuel Macron”. |
| 2 | search_tool("Emmanuel Macron age") | Returns “47”. |
| 3 | calculator_tool("sqrt(47)") | Returns “6.85”. |
| Final Answer | — | “The square root of Emmanuel Macron’s age (47) is approximately 6.85.” |
See the magic? Step 3 would be impossible without having discovered Step 2 first. The agent has been connecting the dots.
Implementing the ReAct Pattern with Agno
We will use Agno to create the example above, implementing the loop Think → Act → Observe → Repeat.
import os
import sys
import logging
import traceback
from typing import List, Optional
from dotenv import load_dotenv, find_dotenv
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.tavily import TavilyTools
# ----------------------------------------------------------------------
# 1. Global Logging and Error Handling Configuration
# ----------------------------------------------------------------------
LOG_DIR = os.path.join(os.path.dirname(__file__), "log")
LOG_FILE = os.path.join(LOG_DIR, "logs.txt")
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_FILE, encoding="utf-8"),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
def global_exception_handler(exctype, value, tb):
"""Capture unhandled exceptions and record them in the log."""
error_msg = "".join(traceback.format_exception(exctype, value, tb))
logger.error(f"Unhandled exception:\n{error_msg}")
sys.__excepthook__(exctype, value, tb)
sys.excepthook = global_exception_handler
# ----------------------------------------------------------------------
# 2. Environment Variables Loading
# ----------------------------------------------------------------------
env_path = find_dotenv()
if env_path:
load_dotenv(env_path)
logger.info(f".env file loaded from: {env_path}")
else:
logger.warning(".env file not found")
# ----------------------------------------------------------------------
# 3. Tool Definitions
# ----------------------------------------------------------------------
def calculate(expression: str) -> str:
"""
Solve a simple mathematical expression (addition, subtraction,
multiplication, division). Useful for calculating year or date differences.
Args:
expression (str): e.g. "2024 - 1789"
"""
try:
# Allow only safe characters for basic eval
allowed_chars = "0123456789+-*/(). "
if all(c in allowed_chars for c in expression):
result = eval(expression)
return f"Result: {result}"
else:
return "Error: Disallowed characters in expression."
except Exception as e:
return f"Error while calculating: {str(e)}"
# ----------------------------------------------------------------------
# 4. Agno Agent Configuration (ReAct Pattern)
# ----------------------------------------------------------------------
model_id = os.getenv("BASE_MODEL", "gpt-4o")
agent = Agent(
model=OpenAIChat(id=model_id),
tools=[TavilyTools(), calculate],
instructions=[
"You are a researcher using the ReAct (Reason + Act) method.",
"1. **Think** step‑by‑step about what information you need to answer the user's question.",
"2. Use the search tool (Tavily) to find specific dates, facts, or data.",
"3. Use the calculator ('calculate') for any mathematical operation or time calculation.",
"4. Do not guess historical information. If you don't have a piece of data, look it up.",
"5. Show your reasoning clearly: 'Thought:', 'Action:', 'Observation:'.",
"6. Continue investigating until you have a complete and verified answer."
],
)
# ----------------------------------------------------------------------
# 5. User Interface
# ----------------------------------------------------------------------
def main():
logger.info("Starting Historical Detective Agent (ReAct)...")
print("--- Historical Detective - ReAct Pattern ---")
print("Type 'exit' to quit.\n")
while True:
try:
user_input = input("Researcher, what is your question?: ")
if user_input.lower() == "exit":
logger.info("The user has ended the session.")
break
if not user_input.strip():
continue
logger.info(f"User query: {user_input}")
print("\nInvestigating...\n")
agent.print_response(user_input, stream=True, show_tool_calls=True)
print("\n")
except KeyboardInterrupt:
logger.info("Keyboard interrupt detected.")
break
except Exception as e:
logger.error(f"Error in main loop: {str(e)}")
print(f"\nAn error occurred: {e}")
if __name__ == "__main__":
main()
Where is ReAct in the code?
| Component | How it implements ReAct |
|---|---|
| THINK | Defined in the agent’s instructions – the first bullet tells the model to think step‑by‑step and to show its reasoning (Thought:). |
| ACT | The tools list (TavilyTools() and calculate) gives the agent actions it can take. The instructions explicitly say “Use the search tool …” and “Use the calculator …”. |
| OBSERVE | After each tool call the agent receives an observation (the tool’s output) which it can reference in the next iteration. |
| REPEAT | The loop is implicit: the model continues to generate Thought → Action → Observation cycles until the final instruction (“Continue investigating until you have a complete and verified answer”) is satisfied. |
External Capabilities Defined in Tools
tools = [TavilyTools(), calculate]
OBSERVE: The framework (Agno) executes the tool and returns the result to the agent.
Agent class: The agent “reads” the return from calculate (e.g., Result: 6.85).
REPEAT: The loop continues until the agent has enough information.
agent.print_response(..., show_tool_calls=True)
show_tool_calls=Trueis vital: it lets you see in the console how the agent “fires” actions in real‑time.
Why Do We Like This Pattern So Much? (Pros)
- Solving Multi‑hop Problems – The agent can answer questions where A leads to B, and B leads to C.
- Self‑Healing Capability – If a search fails (e.g., “Macron’s age”), a normal script would crash. A ReAct agent thinks: “The search failed. I’ll look up his date of birth and calculate the age myself.”
- Goodbye Black Box – As a developer you can read the Thoughts and know exactly why the agent chose the calculator (or any other tool).
- Fewer Hallucinations – By forcing every step to be based on a real observation (
Observation), it’s much harder for the model to invent data.
Cons & Gotchas
- It’s Slow (Latency) – ReAct is sequential. Thinking three times means calling the LLM three times plus executing tools. Expect waits of 10–30 seconds.
- The API Bill (Cost) – The “internal monologue” consumes many tokens, so your context history fills up quickly.
- Risk of Loops – The agent can become obsessed:
I search for X → Not there → I should search for X again → I search for X… - Delicate Prompting – You need a well‑tuned system prompt so the model knows when to stop thinking and give the final answer.
Production Tips (Agno, LangChain, etc.)
-
Put a Brake on It – Max Iterations
Always set an iteration limit (e.g., 10 steps). If the problem isn’t solved in 10 steps, it won’t magically solve it in 100; you’ll just waste money. -
Teach It When to Stop – Stop Sequences
The LLM must stop writing right after launching the Action. If you don’t cut it off, it will start inventing the Observation itself (hallucinating the tool result). Modern frameworks usually handle this, but keep an eye on it. -
Clean Up the Trash – Context Management
In long chats, delete old thought traces and keep only the final answer. Otherwise the agent will run out of “RAM” (context window) immediately.
Example Use‑Cases
- Coding Agents (e.g., Devin, Copilot) –
Write code → Execute → See error → Think how to fix → Rewrite - Level‑2 Tech Support –
Read ticket → Check server status → Check user logs → Cross‑reference data → Answer - Financial Analysts –
Search current price → Search breaking news → Compare with history → Generate recommendation
Happy Coding! 🤖