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:
- Final answer: the model returns text with no tool call. This is the natural exit.
- Step budget: hard cap on iterations, typically 8 to 20. Prevents runaway loops.
- Token budget: total input tokens crossing a threshold (e.g. 100K). Cheaper to abort and summarize than to keep growing context.
- 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.
- 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#
- Perceive: feed conversation so far
- Think: LLM produces thought + optional tool call
- Act: runtime executes tool(s)
- Observe: append tool result to conversation
- 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.
Related Topics#
- LLM Tool Use and Function Calling: how the agent's
actstep is wired - Model Context Protocol (MCP): standard transport for the tool registry
- RAG Patterns: retrieval is the most common first tool in an agent loop