n4nAI

Building a ReAct agent with GPT-4o and function calling

Hands-on tutorial to build ReAct agent GPT-4o with function calling: define tools, implement the reasoning-action loop, and inspect expected output.

n4n Team2 min read537 words

Audio narration

Coming soon — every post will get a voice note here.

To build ReAct agent GPT-4o with function calling, you need a model that supports parallel tool calls and a client loop that pipes model-generated calls to local functions. This tutorial walks through a minimal but production-shaped implementation using the OpenAI Python SDK, showing exact message shapes and a runnable script.

Prerequisites

  • Python 3.10 or newer
  • openai Python package >= 1.0.0 (pip install openai)
  • An API key from OpenAI, or any OpenAI-compatible gateway
  • Basic familiarity with JSON Schema and chat completions

Set your key in the environment:

export OPENAI_API_KEY="sk-..."

If you route through n4n.ai, the same OpenAI-compatible endpoint works and adds automatic fallback when a provider is degraded.

ReAct in plain terms

ReAct (Reason + Act) is not a framework feature. It is a loop: the model emits a reasoning step and optionally one or more action requests (tool calls); your code executes those actions and returns observations; the model then reasons again. GPT-4o handles this natively through the tools parameter—no special prompt prefix required.

The simplest way to build ReAct agent GPT-4o is to treat the model as a state machine that either returns text (done) or returns tool_calls (continue).

Define your tools

Tools are local functions wrapped in JSON Schema. Keep descriptions precise; GPT-4o reads them literally.

import json

def get_weather(city: str) -> str:
    # Mock: in production call a real API
    return json.dumps({"city": city, "temp_c": 21, "condition": "clear"})

def calculate(expression: str) -> str:
    # Demo-only safe eval; use a real math parser in prod
    try:
        return str(eval(expression, {"__builtins__": {}}))
    except Exception as e:
        return f"error: {e}"

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a given city.",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Evaluate a basic arithmetic expression.",
            "parameters": {
                "type": "object",
                "properties": {"expression": {"type": "string"}},
                "required": ["expression"],
            },
        },
    },
]

Client and model configuration

Use the standard client. If you want fallback across providers, point the base URL at a gateway.

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
# Alternative: client = OpenAI(api_key=os.environ["N4N_API_KEY"], base_url="https://api.n4n.ai/v1")

Model name is gpt-4o. Tool choice left as "auto" so the model decides when to call.

The reasoning-action loop

The core loop sends messages, checks for tool_calls, executes them locally, and appends results with role: "tool". The tool_call_id must match the id emitted by the model.

def run_agent(user_msg: str, max_iter: int = 5) -> str:
    messages = [{"role": "user", "content": user_msg}]
    for _ in range(max_iter):
        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",
        )
        msg = resp.choices[0].message
        if not msg.tool_calls:
            return msg.content
        # Append the assistant message with tool calls
        messages.append(msg)
        for tc in msg.tool_calls:
            fn = tc.function
            args = json.loads(fn.arguments)
            if fn.name == "get_weather":
                result = get_weather(args["city"])
            elif fn.name == "calculate":
                result = calculate(args["expression"])
            else:
                result = "unknown tool"
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result,
            })
    return "max iterations reached"

When you build ReAct agent GPT-4o for production, extract the execution switch into a registry keyed by function name. The loop above is intentionally flat for clarity.

Full runnable script

import os
import json
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

def get_weather(city: str) -> str:
    return json.dumps({"city": city, "temp_c": 21, "condition": "clear"})

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

TOOLS = [ ... ]  # as defined above

def run_agent(user_msg: str, max_iter: int = 5) -> str:
    messages = [{"role": "user", "content": user_msg}]
    for _ in range(max_iter):
        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",
        )
        msg = resp.choices[0].message
        if not msg.tool_calls:
            return msg.content
        messages.append(msg)
        for tc in msg.tool_calls:
            args = json.loads(tc.function.arguments)
            if tc.function.name == "get_weather":
                result = get_weather(args["city"])
            elif tc.function.name == "calculate":
                result = calculate(args["expression"])
            else:
                result = "unknown tool"
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result,
            })
    return "max iterations reached"

if __name__ == "__main__":
    print(run_agent("What is the temperature in Tokyo and what is 23 * 7?"))

What the output looks like

First API response (abridged):

{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {"id": "call_1", "function": {"name": "get_weather", "arguments": "{\"city\":\"Tokyo\"}"}},
        {"id": "call_2", "function": {"name": "calculate", "arguments": "{\"expression\":\"23 * 7\"}"}}
      ]
    }
  }]
}

Your code executes both, appends two tool messages, and sends them back. Final response:

Tokyo is currently clear with a temperature of 21°C. 23 * 7 equals 161.

GPT-4o issued parallel calls in one turn—no sequential prompting needed. That is the key efficiency win when you build ReAct agent GPT-4o instead of hand-rolling chain-of-thought.

Failure modes and guardrails

  • Malformed arguments: json.loads can throw. Wrap in try/except and return an error string so the model can self-correct.
  • Infinite loops: Always cap iterations. Five is sane for simple tasks; raise it only with explicit termination cues.
  • Tool hallucination: If the model calls a tool not in your registry, return "unknown tool" and let it recover.
  • Token bloat: Tool results accumulate in context. Trim old observations or summarize if the agent runs long.

Routing and metering notes

If you serve this behind an inference gateway, honor client routing directives and forward provider cache-control hints to cut latency on repeated tool schemas. When using n4n.ai, per-token usage metering is automatic and the gateway forwards your tools definition unchanged, so the loop code above requires zero modifications.

For any ReAct deployment, log resp.usage each turn. GPT-4o input tokens grow linearly with tool results; watch that curve before scaling.

Closing implementation tip

Keep the system prompt empty or minimal. GPT-4o with tools already knows the ReAct contract. Adding “You are a ReAct agent” rarely improves output and wastes tokens. The loop is the agent—not the prompt.

Tagsreact-patterngpt-4ofunction-callingtutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All react & reasoning-action loops posts →