Skip to content

Agent Loop and ReAct Pattern#

An agent loop ReAct pattern turns a stateless LLM into a goal-driven worker by alternating reasoning steps with tool calls. The model thinks, picks a tool, observes the tool's output, then thinks again until it returns a final answer.

flowchart LR
  U([User goal]) --> P[Perceive<br/>input + history]
  P --> T[Think<br/>LLM reasoning]
  T --> A{Tool needed?}
  A -- yes --> X[Act<br/>tool call]
  X --> O[Observe<br/>tool result]
  O --> P
  A -- no --> F([Final answer])

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    class U,F client;
    class P,T,A service;
    class X,O compute;

ReAct (Reason + Act) interleaves chain-of-thought reasoning with discrete tool actions. Each iteration appends the tool call and its result to the conversation, so the next thought step sees grounded evidence rather than hallucinated facts.

The loop terminates on three conditions: the model emits a final answer with no tool call, the step budget is exhausted, or a no-progress detector spots repetition. Without these guards, an agent will happily spin forever calling search on slight rewordings of the same query.

Variants worth knowing: Plan-Act drafts a static plan upfront and executes it, ReAct decides one step at a time, and Reflexion adds a self-critique pass after failures. ReAct dominates in practice because it adapts to surprising tool output.

Problem statement

You need an LLM to answer questions that require fresh data, do math, or call internal APIs. A single prompt cannot. Design a control loop that lets the model iteratively reason, call tools, observe results, and converge on a grounded answer without infinite loops or runaway cost.

The agent loop ReAct pattern is the canonical way to wire an LLM into an agent. Each iteration runs four phases: perceive the current state, think about the next step, act by invoking a tool, observe the result. The loop continues until the model emits a final answer or a guard trips.

The four-phase cycle#

sequenceDiagram
  autonumber
  participant U as User
  participant L as LLM
  participant R as Runtime
  participant T as Tool API
  U->>R: goal
  loop until final or budget
    R->>L: messages so far
    L-->>R: thought + tool_call(name, args)
    R->>T: invoke(args)
    T-->>R: result or error
    R->>R: append tool result to messages
  end
  L-->>R: final answer
  R-->>U: response

The runtime owns the loop, not the LLM. The model sees a conversation that grows by two turns each iteration: its own assistant turn with a tool call, then a tool turn with the result. Claude, GPT, and Gemini all use this shape with minor schema differences.

Plan-Act vs ReAct vs Reflexion#

flowchart TB
  subgraph PA[Plan-Act]
    P1[Draft full plan] --> P2[Execute step 1] --> P3[Execute step 2] --> P4[Execute step N]
  end
  subgraph RA[ReAct]
    R1[Think] --> R2[Act] --> R3[Observe] --> R1
  end
  subgraph RX[Reflexion]
    X1[Attempt] --> X2[Self-critique] --> X3[Retry with notes] --> X1
  end

    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    class P1,P2,P3,P4 service;
    class R1,R2,R3 compute;
    class X1,X2,X3 service;
Pattern When it wins When it loses
Plan-Act Predictable workflows, parallelizable steps Tool output changes the plan
ReAct Open-ended research, dynamic recovery Many steps, high token cost
Reflexion Tasks with verifiable success, e.g. code that compiles One-shot answers, vague success criteria

In production, hybrid is common: a thin plan sets the high-level goal, ReAct fills in each step, and a Reflexion pass kicks in on detected failure.

A minimal ReAct loop in Python#

import json
from anthropic import Anthropic

client = Anthropic()

TOOLS = [
    {
        "name": "search_docs",
        "description": "Search internal documentation by keyword.",
        "input_schema": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
    },
    {
        "name": "calculator",
        "description": "Evaluate an arithmetic expression.",
        "input_schema": {
            "type": "object",
            "properties": {"expr": {"type": "string"}},
            "required": ["expr"],
        },
    },
]

def run_tool(name: str, args: dict) -> str:
    if name == "search_docs":
        return json.dumps(search_docs(args["query"]))
    if name == "calculator":
        return str(eval(args["expr"], {"__builtins__": {}}))
    return f"unknown tool: {name}"

def agent(goal: str, max_steps: int = 8) -> str:
    messages = [{"role": "user", "content": goal}]
    for step in range(max_steps):
        resp = client.messages.create(
            model="claude-opus-4-7",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": resp.content})
        if resp.stop_reason == "end_turn":
            return "".join(b.text for b in resp.content if b.type == "text")
        tool_uses = [b for b in resp.content if b.type == "tool_use"]
        tool_results = []
        for tu in tool_uses:
            try:
                out = run_tool(tu.name, tu.input)
            except Exception as e:
                out = f"ERROR: {e}"
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": tu.id,
                "content": out,
            })
        messages.append({"role": "user", "content": tool_results})
    raise RuntimeError("step budget exhausted")

Three things to notice: the runtime appends both sides of each turn, multiple tool calls in one assistant message are handled (parallel tool use), and errors flow back as content rather than exceptions so the model can recover.

Termination criteria#

flowchart LR
  S[Step end] --> A{Stop reason}
  A -- end_turn --> F([Final answer])
  A -- tool_use --> B{Step budget left?}
  B -- no --> X([Abort, budget])
  B -- yes --> C{Progress made?}
  C -- no for N steps --> Y([Abort, stuck])
  C -- yes --> N[Next step]

    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    class A,B,C service;
    class X,Y datastore;

Healthy stop conditions:

  1. Final answer: the model returns text with no tool call. This is the natural exit.
  2. Step budget: hard cap on iterations, typically 8 to 20. Prevents runaway loops.
  3. Token budget: total input tokens crossing a threshold (e.g. 100K). Cheaper to abort and summarize than to keep growing context.
  4. No-progress detector: same tool with near-identical arguments called twice in a row, or repeated tool errors. Either bail or inject a "you appear stuck, summarize what you have" prompt.
  5. External cancel: user-initiated stop or timeout from the parent service.

Production failure modes#

Failure Symptom Mitigation
Infinite tool loop Same search query every step Hash recent calls, refuse duplicates
Tool error cascade One 500 leads to model panic and 6 retries Surface errors as structured content; not exceptions
Context overflow Step 12 exceeds 200K tokens Summarize older tool results into a scratchpad
Wrong tool picked Model calls search when it should use calculator Tighten tool descriptions, add few-shot examples
Hallucinated tool name Tool not in registry Validate name; return "no such tool" as tool_result
Premature final answer Stops without checking Add system prompt: "Verify with at least one tool"

A common Claude vs GPT difference: Claude tends to emit multiple parallel tool calls when the goal warrants it, while GPT historically defaults to one per turn. The loop must support both shapes. Gemini exposes tool calling but is more eager to free-form prose between calls; strip non-tool prose before continuing.

Observability#

Log each iteration with: step index, model output tokens, tool name, tool latency, tool result size, and stop reason. A flame view (step on X, latency on Y, color by tool) reveals which tool dominates the wall clock. Most agent bugs are visible in a well-instrumented trace before they are visible in code.

Quick reference#

Mental model#

Stateless LLM + runtime loop = agent. Each step: perceive, think, act, observe.

Cycle phases#

  1. Perceive: feed conversation so far
  2. Think: LLM produces thought + optional tool call
  3. Act: runtime executes tool(s)
  4. Observe: append tool result to conversation
  5. Repeat until final answer or guard fires

Variants#

Pattern Idea Best for
Plan-Act Plan upfront, execute Predictable pipelines
ReAct Decide per step Open-ended, dynamic
Reflexion Critique + retry Verifiable tasks (code, math)

Termination guards#

  • Stop reason = end_turn
  • Step budget (8 to 20 typical)
  • Token budget (e.g. 100K input)
  • No-progress detector (duplicate tool calls)
  • External cancel / timeout

Failure modes#

  • Infinite loops on near-duplicate tool args
  • Tool errors cascading into panic retries
  • Context overflow after many tool turns
  • Wrong tool selected by ambiguous descriptions
  • Premature final answer without verifying

Model behavior quirks#

  • Claude: emits parallel tool calls when independent
  • GPT: defaults to one tool per turn historically
  • Gemini: more prose between tool calls, strip before next step

Observability essentials#

Log step index, model tokens, tool name, tool latency, result size, stop reason.

Refs#

  • Yao et al., ReAct: Synergizing Reasoning and Acting (2022)
  • Shinn et al., Reflexion (2023)
  • Anthropic, Claude tool use docs

FAQ#

How does the ReAct agent loop work?#

ReAct interleaves reasoning and tool actions. The LLM thinks, emits a tool call, the runtime executes it, the result is appended to the conversation, and the loop repeats until the model returns a final answer.

What is the difference between ReAct and Plan-and-Execute agents?#

Plan-and-Execute drafts a full plan upfront and then runs each step. ReAct decides one step at a time based on the latest observation, which is more adaptive but uses more tokens.

How do you stop an agent loop from running forever?#

Use a step budget of 8 to 20 iterations, a token budget cap, and a no-progress detector that flags duplicate tool calls. Also honour an external cancel signal from the parent service.

Why does my agent keep calling the same tool in a loop?#

Usually the tool result is not helpful and the model retries with slightly different arguments. Hash recent calls to refuse near-duplicates and surface tool errors as structured content.

What is the difference between Reflexion and ReAct?#

ReAct alternates reasoning and acting in one attempt. Reflexion adds a self-critique pass after a failed attempt and retries with the critique as additional context, which suits verifiable tasks like code.