DEV Community

Gabriel Melendez
Gabriel Melendez

Posted on

AI Agents: Mastering 3 Essential Patterns (ReAct). Part 2 of 3

The code for these patterns is available on Github. [Repo]

The ReAct Pattern (Reason + Act) – When the Agent Starts "Talking to Itself"

With the "Tool Using" pattern (Article 1), we gave the AI "hands" to interact with the outside world. With ReAct, we are installing a "functional brain."

Let's recap: in Article 1, everything was very linear. Stimulus → Response. You asked for a piece of data, the agent fired a tool, and that was it. No doubts, no plans. But let's be realistic, the real world is messy. What happens when the question isn't solved with a single click? What happens when you need to investigate, compare data, and then perform a calculation?

That is where the linear model crashes and ReAct (proposed by some very smart folks at Princeton and Google in 2022) comes into play, which is basically the industry standard today.

What exactly is ReAct?

The name comes from Reasoning + Acting.

The breakthrough idea here isn't just using tools (we already had that), but that we force the LLM to have an Internal Monologue ("Thought Trace"). Instead of rushing to answer blindly, the agent enters a cognitive loop:

Image 1

The agent literally "talks to itself" in the logs. It plans its next step based on what it just discovered a second ago. This allows it to course-correct on the fly if things don't go as expected.

Breaking Down the Loop: The Case of the "Historical Detective"

Let's see what happens under the hood with the example we just built. The user asks: "What is the square root of the age of the current President of France?"

If you ask this to a raw model, it will hallucinate or tell you it doesn't know today's date. But watch how a ReAct agent handles it step-by-step:

  • Iteration 1:
    Thought: "Okay, I have no idea how old the president is. In fact, I don't even know who the president is right now. Let's find that first."
    Action: search_tool("Current President of France")
    Observation: "Emmanuel Macron."

  • Iteration 2:
    Thought: "Ok, we have the name: Macron. Now I need his age."
    Action: search_tool("Emmanuel Macron age")
    Observation:** "47 years old."

  • Iteration 3:
    Thought: "I have the number 47. The user wanted the square root of this."
    Action: calculator_tool("sqrt(47)")
    Observation: "6.8556..."

  • Final Answer: "The square root of Emmanuel Macron's age (47) is approximately 6.85."

See the magic? Step 3 was impossible without having discovered Step 2 first. The agent has been "connecting the dots."

Now let's get to the code

We will use Agno to create our example of the ReAct pattern, implementing 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):
    """Captures unhandled exceptions and records 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:
    """
    Solves a simple mathematical expression (addition, subtraction, multiplication, division).
    Useful for calculating year or date differences.

    Args:
        expression (str): The mathematical expression to evaluate (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()

Enter fullscreen mode Exit fullscreen mode

Where is ReAct in the code?

  • THINK: It is defined in the agent's instructions.
    Code: instructions=["You are a researcher using the ReAct method...", "Think step-by-step...", "Show your reasoning..."].
    This forces the LLM to generate a "Thought Trace" (internal monologue) before doing anything.

  • ACT: The agent has external capabilities defined in tools.
    Code: tools=[TavilyTools(), calculate].
    If it needs data, it uses Tavily; if it needs math, it uses calculate.

  • OBSERVE: The framework (Agno) executes the tool and returns the result to the agent.
    Code: Occurs automatically within the Agent class. The agent "reads" the return from calculate (e.g., "Result: 6.85").

  • REPEAT: The loop continues until the agent has enough information.
    Code: agent.print_response(..., show_tool_calls=True).
    The show_tool_calls=True parameter is vital: it allows you to 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: It's the ability to answer questions where A leads you to B, and B leads you to C.
  • Self-Healing Capability: This is vital. Imagine it searches for "Macron's Age" and Google fails. A normal script would crash. A ReAct agent thinks: "Wow, the search failed. Well, I'll search for his date of birth and calculate the age myself."
  • Goodbye Black Box: As a developer, you can read the Thoughts. You know exactly why the agent decided to use the calculator and not something else.
  • Fewer Hallucinations: By forcing it to base every step on a real observation ("Observation"), it is much harder for it to invent data.

It's not all roses (Cons)

  • It's Slow (Latency): ReAct is sequential. Thinking 3 times means calling the LLM 3 times + executing tools. Be prepared for waits of 10 to 30 seconds.
  • The API Bill (Cost): That "internal monologue" consumes tokens like there's no tomorrow. Your context history fills up very quickly.
  • The Risk of Loops: Sometimes they get obsessed. "I search for X → It's not there → I think I should search for X again → I search for X..."
  • Delicate Prompting: You need a very well-tuned System Prompt so the model knows when to stop thinking and give you the answer.

Tips from the Trenches (Engineering)

If you are going to put this into production (using Agno, LangChain, or whatever), burn this into your brain:

  1. Put a Brake on it (Max Iterations): Always configure an iteration limit (e.g., 10 steps). If it hasn't solved the problem in 10 steps, it won't solve it in 100 and it's just burning your money.
  2. Teach it when to stop (Stop Sequences): Technically, the LLM must stop writing right after launching the Action. If you don't cut it off there, it will start inventing the Observation itself (hallucinating the tool result). Modern frameworks usually handle this, but keep an eye on it.
  3. 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.

Where is this used in the real world?

  • Coding Agents (like Devin or Copilot): The flow is: "I write code → Execute → See error → Think how to fix it → 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! 🤖

Top comments (0)