Machine Learning

AI Agents Explained: What Is the React Loop and How Does It Work?

In my last post,. Calling a Tool is a method that allows an AI model to decide which function to execute and which arguments, instead of producing text as output. By the end of that post, we had a setup that we could decide to do get_current_weather or convert_currencyor you can do both at the same time by calling it parallel, or some of it, and just generate the script. In other words, the model decides what to do next, we (all the code) make that decision, return the result to the model, and the model finally provides informative feedback to the user in text format.

A more advanced version of this loop does not stop after one cycle of determining – executing the code – returning the result – responding to the model. Instead of generating an answer at the end, the model can use the result of one tool call to determine which, and which, tool to call next. As mentioned at the end of the Calling Tool post, this is React loop (Reason + Law), and that's exactly what allows agents to handle tasks that can't be solved with a single call.

But what would such a task be? In the corresponding call example from the previous post, we asked What's the weather in Athens and how much is 100 USD in EUR?which are two different things that require the use of two different tools to find the answer, but are also independent of each other. In other words, we can answer those two questions independently, simultaneously, without needing information from the first question to answer the second.

But what if we ask such a thing I bet my friend 100 EUR that it would rain in Athens today. If I won, how many USD is that? Here, the model cannot decide if it needs to drive convert_currency until it starts calling get_current_weather and finds out if it is really raining. Simply put, the answer to the second question depends entirely on the result of the first. This is exactly the kind of dependency that a parallel tool call can solve in one round, and what the ReAct loop is designed for.

So, let's take a look!

🍨 DataCream a newsletter about AI, data, and technology. If you are interested in these topics, register here!

But what exactly is a ReAct loop?

A React loop it's just three steps repeated in sequence:

  1. The reason
  2. Action
  3. Be careful

At the beginning of the loop, the model reasons about what information is already known and what additional information is missing to provide the correct answer to the user's question. Then actions by calling the appropriate tool for the purpose of finding this missing information. Finally, once the appropriate tool call has been made and its result passed back to the model, the model you notice result (adds a tool result to its context). Then, it goes back to thinking again, except for this new realization that sits at its core. This loop is repeated until the model checks that the available information is sufficient to answer the user's question, and at this point, it abandons the typing tools and simply responds with text.

But isn't this the same as the tool calls we already know? Sort of, but not quite. The part that makes this different from what we covered in the Call Tool post is the loop itself. In one tool call, the model asks for something, gets it, and that's the end of the job for that call. In the ReAct loop, the conversation is always open, as each new observation becomes a new context for the next thinking step, and the model can change its plan based on what it just learned.

Same Tools, New Trick

To do this in concrete, let's go back to the betting example from the presentation and think about what exactly the model needs to do to give us a reliable answer. The question is: I bet my friend 100 EUR that it would rain in Athens today. If I won, how many USD is that? Note the conditional statement between you: if I won. Whether the model needs to change any currency at all depends on what the weather call comes back. If it rains, the model needs to be called convert_currency with 100 EUR as an input parameter and return the converted winnings. If it doesn't rain, the bet is lost, convert_currency is irrelevant, and the model should just return the appropriate text, without making a second call.

To put it differently, a model literally cannot schedule its full sequence of tool calls in advance. He must check the weather first, look at the result, think about what that result means in the betting situation, and then decide if a second call is necessary. Unlike the corresponding tool call which worked well in response What's the weather in Athens and how much is 100 USD in EUR?this question needs a loop.


The good thing about the ReAct loop is that it doesn't require any new tools. We can still use the same functions, just in a different way. So we will use get_current_weather again convert_currency just like we built last time using Open-Meteo for weather and Frankfurter for currency conversion (both still need an API key):

import requests
import json
from openai import OpenAI

client = OpenAI(api_key="your_api_key")

def get_current_weather(city: str, unit: str = "celsius") -> dict:
    # Step 1: geocode the city name to coordinates
    geo = requests.get(
        "
        params={"name": city, "count": 1}
    ).json()
    lat = geo["results"][0]["latitude"]
    lon = geo["results"][0]["longitude"]

    # Step 2: fetch current weather
    weather = requests.get(
        "
        params={
            "latitude": lat,
            "longitude": lon,
            "current": "temperature_2m,precipitation",
            "temperature_unit": unit
        }
    ).json()

    return {
        "city": city,
        "temperature": weather["current"]["temperature_2m"],
        "precipitation_mm": weather["current"]["precipitation"],
        "unit": unit
    }


def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
    response = requests.get(
        f"
    ).json()

    rate = response["rate"]
    converted = round(amount * rate, 2)
    return {
        "amount": amount,
        "from_currency": from_currency,
        "to_currency": to_currency,
        "converted_amount": converted,
        "rate": rate
    }

Note one small addition compared to last time: get_current_weather now he is coming back precipitation_mmas it is the platform that the model needs to check the betting situation. Everything else is the same. I tools The schema is also unchanged from our previous post:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "Get the current weather for a given city, including temperature and precipitation",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "The name of the city"},
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "convert_currency",
            "description": "Convert an amount from one currency to another",
            "parameters": {
                "type": "object",
                "properties": {
                    "amount": {"type": "number", "description": "The amount to convert"},
                    "from_currency": {"type": "string", "description": "The source currency code, e.g. EUR"},
                    "to_currency": {"type": "string", "description": "The target currency code, e.g. USD"}
                },
                "required": ["amount", "from_currency", "to_currency"]
            }
        }
    }
]

We also need to define a lookup dictionary for our code to use to send the model tool selection to the actual Python function:

available_functions = {
    "get_current_weather": get_current_weather,
    "convert_currency": convert_currency
}

This allows us to go from the name of the tool that the model gives us, such as a string, to the actual Python function that we use. We'll need that mapping soon, since at this point we don't know in advance how many tool calls we'll have to resolve, or whether there will be more than one.

Watching the loop and thinking

Here's the really new part. Instead of making a single application and learning without a tool call, we wrap every exchange in a loop. On each pass, we send the model the full conversation so far, check if it has requested a tool, use that tool if so, enter the result, and loop again. We only stop if the model responds with an empty text and there are no tool calls left to make.

messages = [
    {
        "role": "user",
        "content": "I bet my friend 100 EUR that it would rain in Athens today. If I won, how many USD is that?"
    }
]

max_iterations = 5

for i in range(max_iterations):
    print(f"--- Step {i + 1}: Reason ---")

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        tools=tools
    )

    message = response.choices[0].message
    messages.append(message)

    # If there's no tool call, the model is ready to answer
    if not message.tool_calls:
        print("Final answer:")
        print(message.content)
        break

    # Otherwise, act on every tool call the model requested
    for tool_call in message.tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)

        print(f"--- Step {i + 1}: Act ({function_name}) ---")
        print(f"Calling {function_name} with {function_args}")

        function_response = available_functions[function_name](**function_args)

        print(f"--- Step {i + 1}: Observe ---")
        print(function_response)

        # Feed the observation back in so the next Reason step can use it
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(function_response)
        })

Also, notice the max_iterations a cap that prevents the model from deciding it needs to “Just some information” from loping forever. This is very important because we pay for all the calls in the model within those loops.

Finally, the resulting observation of the loop is entered as a role: "tool" a message tied to something tool_call_id. This allows the model to match each result back to the call that generated it.

And now that we have everything set up, we can finally see the ReAct loop in action.


So, our betting question can play out in two ways depending on the weather. Let's look at both.

1. If it is raining in Athens, our code would print to the terminal something like the following:

--- Step 1: Reason ---
--- Step 1: Act (get_current_weather) ---
Calling get_current_weather with {'city': 'Athens'}
--- Step 1: Observe ---
{'city': 'Athens', 'temperature': 17.4, 'precipitation_mm': 3.2, 'unit': 'celsius'}

--- Step 2: Reason ---
--- Step 2: Act (convert_currency) ---
Calling convert_currency with {'amount': 100, 'from_currency': 'EUR', 'to_currency': 'USD'}
--- Step 2: Observe ---
{'amount': 100, 'from_currency': 'EUR', 'to_currency': 'USD', 'converted_amount': 108.5, 'rate': 1.085}

--- Step 3: Reason ---
Final answer:
It did rain in Athens today (3.2mm of precipitation), so you won the bet!
Your 100 EUR comes out to 108.50 USD at today's exchange rate.

2. And if it had not rained in Athens, we would have had the following printed:

--- Step 1: Reason ---
--- Step 1: Act (get_current_weather) ---
Calling get_current_weather with {'city': 'Athens'}
--- Step 1: Observe ---
{'city': 'Athens', 'temperature': 34.1, 'precipitation_mm': 0.0, 'unit': 'celsius'}

--- Step 2: Reason ---
Final answer:
Unfortunately, it did not rain in Athens today, so it looks like you lost the bet.
No currency conversion needed!

Look what happened in the second case: the loop ran exactly once. The model saw that precipitation_mm it was 0.0he thought the condition of the bet was not met, and stopped without ever calling convert_currency. No one told it to skip the second tool call, but it decided that on its own, based on what it saw at the beginning of the loop.

This is the main difference (at least in this simple case) between the corresponding tool call and the ReAct loop. In the same tool call, we cannot exit early from the whole process, and not pick up the call. convert_currency. Instead, in the same setup, both tools would be called first, and the model would write the final answer later. This is very important because remember! we pay all calls in the model. Therefore, being able to reduce by building an AI model costs to what we need, without making additional unnecessary calls, is huge.

In my mind

So, when does a ReAct loop beat a parallel tool call?

The answer is: whenever the number of tool calls, or the arguments of those calls, can only be determined after seeing the previous result.

In our betting example, the model cannot decide whether to hit convert_currency at all until get_current_weather it means it's raining. No amount of forward thinking solves that, because the information doesn't exist yet in the world of the model. We have to go outside the model world, take the external information from the weather API, and add it to the model context. In contrast, parallel tool calls assume that the model already knows what it needs before it initiates any tool calls. The ReAct loop doesn't need that consideration: it lets the model figure out what it needs as it goes.

In particular, the ReAct loop wins a parallel tool that calls the following conditions:

  1. Where one outcome is the criterion for whether another call is necessary at all, as in the betting example.
  2. If the arguments in the latest call depend on the value returned in the previous one. For example, if the model had to first look at how much money a city spends before making a call convert_currency with the correct code.
  3. If the previous result returns unexpectedly, for example, the user might provide a city name that doesn't have a geocode, or the API returns an error, and the model needs to adjust its program rather than just reporting whatever it has.

However, in a specific situation where all the necessary tools and their arguments are visible only in the user message, the call of the corresponding tool is actually a better choice, since in this way we get fewer round trips, less delay, and the same result.

For me, the most interesting part of moving away from the parallel tool to call ReAct loop is how little code it took πŸ˜…: a for loop, a if statement, and dictionary observation. Still, that little bit of code works wonders. This ReAct loop is, in a way, the real mechanism behind what people call an “agent”.

✨ Thanks for reading! ✨


If you've made it this far, you may find pialgorithms useful β€” a platform we've been building that helps teams securely manage organizational information in one place.


Did you like this post? Join me πŸ’ŒA small stake and πŸ’ΌLinkedIn


All photos by the author, unless otherwise noted otherwise

Source link

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button