DEV Community

Cover image for Building Your First AI Agent with the ReAct Pattern
Gokulnath P
Gokulnath P

Posted on AI-assisted

Building Your First AI Agent with the ReAct Pattern

In Post #2 we gave the LLM tools and ran the loop manually — one tool call, get the result, done. That works, but it's still us doing the driving. A real agent drives itself.

That's what this post is about. We're going to build something that takes a task, figures out what it needs to do, calls the right tools, and keeps going until it has an answer — all without us stepping in between.

The difference between a chatbot and an agent

A chatbot responds to one message at a time. You ask, it answers, and that's the end of the interaction. An agent is different — it has a task to complete, and it keeps working until that task is done.

The simplest way to see this: give an agent a question that requires multiple steps to answer.

"What is the population of the capital of the country that hosted the 2020 Olympics, divided by 1000?"

A chatbot either guesses or gives up. An agent works through it:

Step 1: search("2020 Olympics host country")  → Japan
Step 2: search("capital of Japan")            → Tokyo
Step 3: search("population of Tokyo")         → ~13.96 million
Step 4: calculate("13960000 / 1000")          → 13960
Final Answer: 13960
Enter fullscreen mode Exit fullscreen mode

No human in the loop between steps. The agent chained four tool calls on its own.

The ReAct pattern

The loop that makes this possible is called ReAct — short for Reasoning and Acting. It was published by Google in 2022 and it's still the foundation that most agent systems are built on.

Thought: what do I need to do?
    │
    ▼
Action: call a tool
    │
    ▼
Observation: tool result
    │
    ▼
Thought: do I have the answer yet?
    ├── No  → loop back
    └── Yes → Final Answer
Enter fullscreen mode Exit fullscreen mode

Each iteration gets added to the conversation history, so the agent always has the full picture of what it's tried and what it's learned before deciding the next step.

Stopping conditions

An agent needs to know when to stop — without that, it just runs forever. There are two ways it stops.

The natural way is when the model decides it has enough information and produces a final answer instead of another tool call. You detect that in code and exit the loop.

The safety net is max_iterations — a hard cap on how many steps the agent can take. If it hasn't finished by then, you stop it and handle the incomplete state. Always add this. It's the single most important guardrail in agent code.

Setup

pip install ollama chromadb
Enter fullscreen mode Exit fullscreen mode

Use qwen2.5 for these exercises — it handles tool selection reliably:

ollama pull qwen2.5
Enter fullscreen mode Exit fullscreen mode

Exercise 1 — ReAct agent from scratch

Let's build the loop manually so every piece is visible:

import ollama

def calculate(expression: str) -> str:
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

def search(query: str) -> str:
    # Simulated search — swap with a real API in production
    knowledge = {
        "capital of france": "Paris",
        "capital of japan": "Tokyo",
        "capital of australia": "Canberra",
        "population of paris": "2.1 million in the city, 12 million in greater Paris",
        "population of tokyo": "approximately 13.96 million in the city",
        "population of canberra": "approximately 460,000",
        "2020 olympics host": "Japan hosted the 2020 Summer Olympics in Tokyo",
        "eiffel tower height": "330 metres including the antenna",
        "python creator": "Python was created by Guido van Rossum",
    }
    query_lower = query.lower().strip()
    for key, value in knowledge.items():
        if key in query_lower or query_lower in key:
            return value
    return "No information found for that query."

TOOLS = {"calculate": calculate, "search": search}

tools = [
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Perform arithmetic. Use for any math calculation.",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "A Python math expression e.g. '(4 * 7) + 3'"}
                },
                "required": ["expression"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search",
            "description": "Look up factual information: geography, people, events.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "The thing to look up"}
                },
                "required": ["query"]
            }
        }
    }
]

def run_agent(user_task: str, max_iterations: int = 10):
    print(f"\nTask: {user_task}")
    print("=" * 60)

    messages = [
        {
            "role": "system",
            "content": (
                "You are a helpful assistant with access to tools. "
                "Use tools to look up information and perform calculations. "
                "Keep using tools until you have enough to give a complete answer. "
                "When you have the final answer, state it clearly."
            )
        },
        {"role": "user", "content": user_task}
    ]

    for i in range(max_iterations):
        print(f"\n[Iteration {i + 1}]")
        response = ollama.chat(model="qwen2.5", messages=messages, tools=tools)

        if response.message.tool_calls:
            messages.append(response.message)
            for tool_call in response.message.tool_calls:
                name = tool_call.function.name
                args = tool_call.function.arguments
                print(f"  Action:      {name}({args})")
                result = TOOLS[name](**args)
                print(f"  Observation: {result}")
                messages.append({"role": "tool", "content": result})
        else:
            print(f"\nFinal Answer: {response.message.content}")
            return response.message.content

    print("\n[Max iterations reached]")
    return None


run_agent("What is the population of the capital of France?")
run_agent("Who created Python, and what is 2 to the power of 10?")
run_agent("What country hosted the 2020 Olympics, and what is its capital?")
Enter fullscreen mode Exit fullscreen mode

Watch the iteration log as it runs. You'll see the agent chain tool calls naturally — it searches, gets a result, decides what to search next, and keeps going until it has everything it needs.

Exercise 2 — Adding RAG as a tool

In Post #3 we built retrieval. Now let's give it to the agent as a tool — so the agent can search a knowledge base on its own:

import ollama
import chromadb

client = chromadb.Client()
collection = client.create_collection("agent_kb")

docs = [
    "Python was created by Guido van Rossum and first released in 1991.",
    "PostgreSQL is a powerful open-source relational database system.",
    "Docker is a platform for containerising applications and their dependencies.",
    "Kafka is a distributed event streaming platform for high-throughput messaging.",
    "FastAPI is a modern Python web framework for building APIs quickly.",
    "Redis is an in-memory data store commonly used for caching and queues.",
    "Integration tests typically require a running database and external services.",
    "Static analysis tools check code for style issues and potential bugs without running it.",
]

for i, doc in enumerate(docs):
    emb = ollama.embeddings(model="nomic-embed-text", prompt=doc).embedding
    collection.add(ids=[str(i)], embeddings=[emb], documents=[doc])

def calculate(expression: str) -> str:
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

def search_knowledge_base(query: str) -> str:
    emb = ollama.embeddings(model="nomic-embed-text", prompt=query).embedding
    results = collection.query(query_embeddings=[emb], n_results=3)
    docs = results["documents"][0]
    return "\n".join(f"- {d}" for d in docs) if docs else "No relevant information found."

TOOLS = {"calculate": calculate, "search_knowledge_base": search_knowledge_base}

tools = [
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Perform arithmetic or math calculations.",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "Python math expression"}
                },
                "required": ["expression"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "Search the internal knowledge base for information about "
                           "the knowledge base — tech topics, tools, and concepts.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "What to look up"}
                },
                "required": ["query"]
            }
        }
    }
]

def run_agent(user_task: str, max_iterations: int = 8):
    print(f"\nTask: {user_task}")
    print("=" * 60)
    messages = [
        {
            "role": "system",
            "content": "You are a helpful assistant. Use the knowledge base to answer questions. "
                       "Use calculate for math. Answer clearly when you have enough information."
        },
        {"role": "user", "content": user_task}
    ]
    for i in range(max_iterations):
        response = ollama.chat(model="qwen2.5", messages=messages, tools=tools)
        if response.message.tool_calls:
            messages.append(response.message)
            for tool_call in response.message.tool_calls:
                name = tool_call.function.name
                args = tool_call.function.arguments
                print(f"{name}({args})")
                result = TOOLS[name](**args)
                print(f"{result[:120]}")
                messages.append({"role": "tool", "content": result})
        else:
            print(f"\nAnswer: {response.message.content}")
            return
    print("[Max iterations reached]")


run_agent("What database is commonly used in Python web services?")
run_agent("What do I need to run integration tests?")
run_agent("What is Redis used for, and what is 2024 - 2016?")
Enter fullscreen mode Exit fullscreen mode

This is where Posts #2, #3, and #4 all come together — tool use, retrieval, and the agent loop working as one system.

Exercise 3 — Reading the reasoning trace

This exercise adds visibility into what the model is actually thinking between tool calls:

def run_agent_verbose(user_task: str, max_iterations: int = 8):
    print(f"\nTask: {user_task}")
    print("=" * 60)

    messages = [
        {
            "role": "system",
            "content": "You are a helpful assistant with tools. "
                       "Think step by step. Use tools when needed. "
                       "State your final answer clearly."
        },
        {"role": "user", "content": user_task}
    ]

    for i in range(max_iterations):
        print(f"\n--- Step {i + 1} | {len(messages)} messages in context ---")
        response = ollama.chat(model="qwen2.5", messages=messages, tools=tools)

        if response.message.content:
            print(f"Thought: {response.message.content}")

        if response.message.tool_calls:
            messages.append(response.message)
            for tool_call in response.message.tool_calls:
                name = tool_call.function.name
                args = tool_call.function.arguments
                print(f"Action:      {name}({args})")
                result = TOOLS[name](**args)
                print(f"Observation: {result}")
                messages.append({"role": "tool", "content": result})
        else:
            print(f"\n✓ Final Answer: {response.message.content}")
            return

    print("\n[Stopped: max iterations reached]")
Enter fullscreen mode Exit fullscreen mode

Notice two things as it runs. First, the context size grows with every step — every action and observation gets added to the message history. Second, try setting max_iterations=2 on a task that needs four steps and watch what happens when it gets cut off. That's why the cap matters, and why you'd want to handle that incomplete state gracefully in a real application.

Wrapping up

An agent is not magic. It's the same tool loop from Post #2, just running autonomously inside a loop with a stopping condition. The ReAct pattern gives it structure — reason, act, observe, repeat — and the system prompt tells it how to behave.

The two things that matter most: always have a max_iterations cap, and write good tool descriptions (as we covered in Post #2). Everything else follows from those.

In Post #5, we tackle the one thing this agent still can't do — remember anything between runs. See you there. 🚀

Top comments (0)