← Back to DevBytes

How to Implement ReAct Prompting for Tool-Calling Agents

How to Implement ReAct Prompting for Tool-Calling Agents

Large language models are powerful reasoners, but they cannot natively fetch live data, run calculations, or interact with external systems. ReAct prompting bridges this gap by interleaving reasoning and acting in a single loop, allowing an LLM to decide which tool to call, observe the result, and continue reasoning until it can answer the user's question. This tutorial walks through what ReAct is, why it matters, and how to implement a working tool-calling agent from scratch.

What Is ReAct Prompting?

ReAct — short for Reasoning + Acting — is a prompting paradigm introduced in 2022 that asks a language model to produce a structured stream of thoughts, actions, and observations rather than a single free-form answer. At each step the model emits a Thought describing its reasoning, an Action naming a tool and its arguments, and then the runtime executes that tool and feeds back an Observation. The loop continues until the model emits a Final Answer.

A typical ReAct trace looks like this:

Question: What is the square root of the population of Tokyo?
Thought: I need to find the population of Tokyo first, then take its square root.
Action: search["population of Tokyo"]
Observation: Approximately 13,960,000
Thought: Now I need the square root of 13960000.
Action: calculator["sqrt(13960000)"]
Observation: 3736.30
Thought: I have the answer.
Final Answer: The square root of Tokyo's population (~13.96 million) is approximately 3736.3.

This structure is what makes ReAct different from plain chain-of-thought: the model is not just thinking aloud, it is actively invoking tools and grounding its reasoning in real observations.

Why ReAct Matters for Tool-Calling Agents

ReAct solves three core problems that plague naive LLM applications:

Modern APIs like OpenAI's function calling and Anthropic's tool use are essentially productionized versions of the ReAct loop. Understanding the underlying pattern lets you build agents that work across providers, frameworks, and custom runtimes.

The Anatomy of a ReAct Loop

Every ReAct implementation has four components:

Implementing a Minimal ReAct Agent

Let's build a self-contained ReAct agent in Python using only the OpenAI client. We will define two tools — a calculator and a fake web search — and wire them into the loop.

Step 1: Define the Tools

Each tool is a plain Python function with a docstring that the model can read to understand its purpose.

import json
import math
import re

def calculator(expression: str) -> str:
    """Evaluate a mathematical expression and return the result as a string.
    Supports standard Python math syntax, e.g. 'sqrt(13960000)' or '12 * 7 + 3'.
    """
    try:
        safe_env = {"sqrt": math.sqrt, "abs": abs, "pow": pow, "pi": math.pi}
        result = eval(expression, {"__builtins__": {}}, safe_env)
        return str(result)
    except Exception as e:
        return f"Error: {e}"

def search(query: str) -> str:
    """Pretend to search the web for a factual query and return a short snippet."""
    fake_db = {
        "population of tokyo": "Approximately 13,960,000 as of 2023.",
        "capital of france": "Paris.",
        "height of mount everest": "8,848.86 meters.",
    }
    key = query.lower().strip()
    return fake_db.get(key, f"No results found for '{query}'.")

TOOL_REGISTRY = {
    "calculator": calculator,
    "search": search,
}

Step 2: Write the System Prompt

The system prompt is the most important piece. It must tell the model exactly which tools exist, how to call them, and when to stop.

SYSTEM_PROMPT = """You are a ReAct agent. Answer the user's question by reasoning step by step.

Available tools:
- search[query]: Look up a factual question. Example: search["population of Tokyo"]
- calculator[expression]: Evaluate a math expression. Example: calculator["sqrt(144)"]

You MUST respond using EXACTLY one of these formats on each turn:

Thought: <your reasoning>
Action: <tool_name>[<argument>]

Or, when you have enough information:

Thought: <your reasoning>
Final Answer: <your answer to the user>

Do not include any other text. Use double quotes inside the action brackets.
"""

Step 3: Build the Parser

We need a parser that can detect whether the model emitted an Action or a Final Answer, and extract the relevant pieces.

def parse_response(text: str):
    """Return ('action', name, arg) or ('final', answer) or ('unknown', text)."""
    action_match = re.search(r"Action:\s*(\w+)\[(.*?)\]", text, re.DOTALL)
    if action_match:
        tool_name = action_match.group(1).strip()
        raw_arg = action_match.group(2).strip().strip('"').strip("'")
        return ("action", tool_name, raw_arg)

    final_match = re.search(r"Final Answer:\s*(.*)", text, re.DOTALL)
    if final_match:
        return ("final", final_match.group(1).strip())

    return ("unknown", text)

Step 4: Implement the Loop

The loop sends the conversation to the model, parses the response, executes the tool if needed, appends the observation, and repeats.

from openai import OpenAI

client = OpenAI()

def run_react_agent(question: str, max_steps: int = 6) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"Question: {question}"},
    ]

    for step in range(max_steps):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            temperature=0,
        )
        assistant_text = response.choices[0].message.content
        print(f"\n--- Step {step + 1} ---")
        print(assistant_text)

        parsed = parse_response(assistant_text)

        if parsed[0] == "final":
            return parsed[1]

        if parsed[0] == "action":
            _, tool_name, arg = parsed
            tool_fn = TOOL_REGISTRY.get(tool_name)
            if tool_fn is None:
                observation = f"Error: unknown tool '{tool_name}'"
            else:
                observation = tool_fn(arg)
            print(f"Observation: {observation}")
            messages.append({"role": "assistant", "content": assistant_text})
            messages.append({
                "role": "user",
                "content": f"Observation: {observation}",
            })
        else:
            messages.append({
                "role": "user",
                "content": "Please respond using the required Thought/Action or Thought/Final Answer format.",
            })

    return "Agent exceeded maximum steps without a final answer."

Step 5: Run It

if __name__ == "__main__":
    answer = run_react_agent(
        "What is the square root of the population of Tokyo?"
    )
    print("\n=== RESULT ===")
    print(answer)

When you run this script, you should see output similar to:

--- Step 1 ---
Thought: I need the population of Tokyo first.
Action: search["population of Tokyo"]
Observation: Approximately 13,960,000 as of 2023.

--- Step 2 ---
Thought: Now I compute the square root of 13960000.
Action: calculator["sqrt(13960000)"]
Observation: 3736.306393762955

--- Step 3 ---
Thought: I have the final value.
Final Answer: The square root of Tokyo's population (~13.96 million) is approximately 3736.3.

=== RESULT ===
The square root of Tokyo's population (~13.96 million) is approximately 3736.3.

Using Native Function Calling Instead of Text Parsing

Text-based ReAct is great for learning and for providers without native tool support, but most modern APIs now support structured function calling. The same loop applies — the only difference is that the action is returned as a JSON object instead of parsed from text.

tools = [
    {
        "type": "function",
        "function": {
            "name": "search",
            "description": "Look up a factual question.",
            "parameters": {
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "Evaluate a math expression.",
            "parameters": {
                "type": "object",
                "properties": {"expression": {"type": "string"}},
                "required": ["expression"],
            },
        },
    },
]

def run_native_react_agent(question: str, max_steps: int = 6) -> str:
    messages = [
        {"role": "system", "content": "Answer step by step using the provided tools."},
        {"role": "user", "content": question},
    ]

    for _ in range(max_steps):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=tools,
            temperature=0,
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:
            name = call.function.name
            args = json.loads(call.function.arguments)
            result = TOOL_REGISTRY[name](**args)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": str(result),
            })

    return "Agent exceeded maximum steps."

This version is more robust because the model returns structured arguments directly, eliminating the need for regex parsing and reducing format-related failures.

Best Practices

Common Pitfalls

Conclusion

ReAct prompting is the foundational pattern behind virtually every tool-calling agent in production today. By interleaving structured reasoning with concrete actions, it turns a language model from a static text generator into an interactive system that can query databases, run calculations, and compose multi-step workflows. Whether you implement the loop yourself with text parsing or lean on native function-calling APIs, the same principles apply: define clear tools, parse actions reliably, cap the step count, and log everything. Start with the minimal agent above, add the tools your domain requires, and iterate on the system prompt until the agent reliably chooses the right tool at the right time.

— Ad —

Google AdSense will appear here after approval

← Back to all articles