Skip to main content
Cloud & AI Hub
Browse
Glossary AI Directory Playgrounds Models Prompts Explainers Strategy Matrix Benchmark Decoder
intermediate 9 mins Read

The ReAct Agent Execution Loop

How LLM agents reason and act in loops — the Reasoning + Acting (ReAct) pattern, tool call mechanics, observation processing, and how agents know when to stop.

Introduction

A standalone LLM is stateless — it takes text in, produces text out, and cannot interact with the world. An LLM agent breaks this limitation by equipping the model with tools (functions it can call) and running it in a loop until the task is complete. The ReAct pattern (Yao et al., 2022) is the dominant framework: the model alternates between Thought (internal reasoning), Action (tool invocation), and Observation (reading the tool’s output) until it reaches a Final Answer.


The ReAct Execution Loop

flowchart TD
    U["User input"] --> T["Thought: reason about what's needed next"]
    T --> A["Action: call a tool"]
    A --> O["Observation: read the tool's output"]
    O -->|"not enough info yet"| T
    O -->|"enough info"| F["Final Answer"]

A worked trace of that loop, resolving a two-step question:

User Input: "What's the current price of NVDA and how does it compare to its 52-week high?"


┌─────────────────────────────────────────────────────────────────────┐
│ ITERATION 1                                                          │
│                                                                     │
│ Thought: I need to get the current price of NVDA first.            │
│          I'll use the get_stock_price tool.                        │
│                                                                     │
│ Action: get_stock_price(ticker="NVDA")                              │
│    │                                                                │
│    └── [Tool Executes] → API call → returns JSON                   │
│                                                                     │
│ Observation: {"ticker": "NVDA", "price": 875.43, "currency": "USD"}│
└─────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│ ITERATION 2                                                          │
│                                                                     │
│ Thought: I have the current price ($875.43). Now I need the        │
│          52-week high to make the comparison.                      │
│                                                                     │
│ Action: get_stock_stats(ticker="NVDA", stat="52_week_high")        │
│    │                                                                │
│    └── [Tool Executes] → API call → returns JSON                   │
│                                                                     │
│ Observation: {"ticker": "NVDA", "52_week_high": 974.00}            │
└─────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│ ITERATION 3: FINAL ANSWER                                           │
│                                                                     │
│ Thought: I have both values. Current: $875.43, 52-week high:       │
│          $974.00. That's 10.2% below the 52-week high.             │
│          I have enough information to answer.                      │
│                                                                     │
│ Final Answer: NVDA is currently trading at $875.43, which is       │
│              $98.57 (10.2%) below its 52-week high of $974.00.     │
└─────────────────────────────────────────────────────────────────────┘

Tool Call Mechanics (OpenAI Function Calling)

Modern LLMs implement ReAct via structured tool calls rather than text parsing. The model outputs a JSON object declaring which function to call and with what arguments.

from openai import OpenAI
import json

client = OpenAI()

# Define tools as JSON Schema
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "Search the web for real-time information about a topic.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "The search query"},
                    "num_results": {"type": "integer", "description": "Number of results to return", "default": 5}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "run_python",
            "description": "Execute Python code and return the output. Use for calculations, data processing.",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {"type": "string", "description": "Python code to execute"}
                },
                "required": ["code"]
            }
        }
    }
]

# Tool executor registry
def execute_tool(tool_name: str, tool_args: dict) -> str:
    if tool_name == "search_web":
        return web_search(tool_args["query"], tool_args.get("num_results", 5))
    elif tool_name == "run_python":
        return run_sandboxed_python(tool_args["code"])
    raise ValueError(f"Unknown tool: {tool_name}")

def run_agent(user_input: str, max_iterations: int = 10) -> str:
    messages = [
        {"role": "system", "content": "You are a helpful assistant. Think step by step and use tools when needed."},
        {"role": "user", "content": user_input}
    ]
    
    for iteration in range(max_iterations):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto"  # model decides when to use tools
        )
        
        msg = response.choices[0].message
        messages.append(msg)  # add assistant turn to history
        
        # If model chose to call tools
        if msg.tool_calls:
            for tool_call in msg.tool_calls:
                tool_name = tool_call.function.name
                tool_args = json.loads(tool_call.function.arguments)
                
                print(f"→ Calling {tool_name}({tool_args})")
                result = execute_tool(tool_name, tool_args)
                print(f"← Observation: {result[:200]}...")
                
                # Inject tool result back into conversation
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": str(result)
                })
        else:
            # No tool calls = final answer
            return msg.content
    
    return "Max iterations reached without completing the task."

Multi-Agent Coordination Patterns

For complex tasks, a single agent loop is insufficient. The two primary multi-agent patterns:

1. Supervisor → Subagents (Hierarchical)

flowchart TD
    Q["User query"] --> S["Supervisor agent: plans, delegates, synthesizes"]
    S --> R["Research agent (search)"]
    S --> AN["Analyst agent (python)"]
    S --> W["Writer agent (prose)"]
    R --> S
    AN --> S
    W --> S
# LangGraph implementation of supervisor pattern
from langgraph.graph import StateGraph, END

def supervisor_node(state):
    """Routes tasks to appropriate subagents."""
    response = supervisor_llm.invoke(state)
    return {"next_agent": response.next, "task": response.task}

def research_agent_node(state):
    """Performs web searches and document retrieval."""
    results = search_tools.invoke(state["task"])
    return {"research_results": results}

# Build the graph
graph = StateGraph(AgentState)
graph.add_node("supervisor", supervisor_node)
graph.add_node("researcher", research_agent_node)
graph.add_node("analyst", analyst_node)

graph.add_conditional_edges(
    "supervisor",
    lambda state: state["next_agent"],
    {"researcher": "researcher", "analyst": "analyst", "FINISH": END}
)

2. Sequential Pipeline (Chain)

Agent A                Agent B                Agent C
(Research)    →        (Analyze)     →        (Write Report)
Outputs: docs          Outputs: insights       Outputs: final text

Stopping Conditions & Guard Rails

Without proper stopping logic, agents loop indefinitely or hallucinate tool calls. Implement:

class AgentGuardRails:
    def __init__(self, max_iterations=10, max_tokens=100_000, max_tool_errors=3):
        self.iteration = 0
        self.total_tokens = 0
        self.tool_errors = 0
        self.max_iterations = max_iterations
        self.max_tokens = max_tokens
        self.max_tool_errors = max_tool_errors

    def should_stop(self, response) -> tuple[bool, str]:
        self.iteration += 1
        self.total_tokens += response.usage.total_tokens

        if self.iteration >= self.max_iterations:
            return True, f"Stopped: max_iterations ({self.max_iterations}) reached"
        if self.total_tokens >= self.max_tokens:
            return True, f"Stopped: token budget ({self.max_tokens}) exhausted"
        if self.tool_errors >= self.max_tool_errors:
            return True, "Stopped: too many tool execution errors"
        return False, ""

Key Takeaways

  • ReAct = Thought + Action + Observation in a loop. The model can call multiple tools across multiple iterations before producing a Final Answer. This is how agents reason about complex tasks.
  • Tool calls are structured JSON, not free-form text — the model outputs {"name": "...", "arguments": {...}} which your code executes and returns as an observation.
  • Message history is the agent’s memory — every tool result is appended to the conversation. Long tasks accumulate context; implement sliding window or summarization to stay within context limits.
  • Add stopping conditions: max iterations, token budget, and error count guards prevent runaway loops that waste money and degrade user experience.
  • Multi-agent coordination (supervisor + specialists) is more reliable than a single monolithic agent for complex, multi-domain tasks.

Historical figures, architectures, and capabilities are for informational purposes only. Not technical, professional, legal, or financial advice. Sources: Research papers, developer documentation.