The ReAct Loop: How Most AI Agents Actually Work
Thought, action, observation: the loop that turned one-shot text generators into agents that can check their work.
Most AI agents that check their work, use external tools, or recover from errors run a variant of the same core loop, first described as ReAct. The model interleaves reasoning steps (Thought) with tool calls (Action) and reads their results (Observation) until it can give a final answer grounded in real data. This loop is what turns a text generator into a system that can act on the world.
First, a definition, because the word gets used loosely. An agent is a language model placed inside a loop that ordinary code runs. That code is called the runtime. The runtime gives the model a list of tools, runs the tool calls the model asks for, feeds the results back, and stops when the model gives a final answer or a limit is hit. The model decides what to do next. The runtime does it.
Without that loop, asking an assistant "does my dentist appointment clash with anything next week?" may return a confident guess, because the model has never seen your inbox. With the loop, the same model searches your actual inbox, reads the calendar, and compares real data. The model did not change. The loop around it made the difference.
Why does a plain language model guess at facts it could look up?
A plain language model call is a one-shot function: text in, text out. It has no access to your inbox or your calendar, so it can only answer from patterns it learned in training, or say that it does not know. When you ask about your dentist, it cannot pause, fetch a real email, and reconsider. A careful model will tell you it cannot see your mail. A careless setup will produce something plausible.
The fix has a name and a paper. ReAct, a name made from reasoning and acting, was introduced by Yao and colleagues at Princeton and Google (Yao et al. 2022, ReAct: Synergizing Reasoning and Acting in Language Models), a paper first posted in October 2022 and presented at ICLR 2023. On question answering (HotpotQA) and fact checking (FEVER), letting the model call a simple Wikipedia API reduced the hallucination and error propagation seen with chain-of-thought reasoning alone. On two interactive tasks, a text-based household game (ALFWorld) and a simulated online shop (WebShop), ReAct beat imitation learning and reinforcement learning methods by 34 and 10 absolute points of success rate, with only one or two worked examples in the prompt.
ReAct borrows a habit from how a detective works a case. A detective does not announce a conclusion the moment she hears about the crime. She thinks about what she knows, interviews a witness (action), reads the statement (observation), rethinks her theory, checks the records, and repeats. The loop ends only when the evidence supports a conclusion worth saying out loud.
Mailmind, our fictional inbox assistant, faces exactly that detective problem. A user asks, "When is my dentist appointment, and does it clash with anything?" Mailmind thinks: I need the appointment confirmation. It calls search_inbox("dentist"), reads the returned email, extracts the date and time, then thinks: Now I need the calendar for that day. It calls get_calendar("2026-07-08"), observes the results, compares entries, and finally answers with the real appointment time and any conflicts. At every step the model is the same. The loop provides the missing real-world signal.
What does each step in the loop actually do?
The loop cycles through four phases. They are not jargon. They are concrete positions that every agent fills with text and tool output.
Thought. Reasoning the model writes about what it knows so far and what it needs next. The user usually never sees it, but it is where the agent can reflect and replan. For Mailmind, a thought might be: The user asked about a dentist. I found a confirmation from SmileCare for 8 July at 10 AM. Now I need to see what else is on the calendar that morning. Many current models also reason internally before they answer, so a separate written thought is optional in modern setups. The loop works the same either way.
Action. A structured request to run a tool. It names a function and gives its arguments, such as search_inbox(query="dentist"), get_calendar(date="2026-07-08") or draft_reply(to="dentist@smilecare.com", body="..."). The model does not execute anything. It asks, and the runtime runs the function. With today's model APIs this request arrives as a structured tool call block, not as free text the runtime has to parse.
Observation. The raw output of the tool, added to the conversation before the next model call. If search_inbox returns a JSON (JavaScript Object Notation) list of email snippets, that list is the observation. The model sees it exactly as the runtime provides it. A good observation carries enough signal for the next thought. A bad one, like an empty string after a failed search, forces the model to guess what went wrong.
Final answer. Once the model judges it has enough evidence, it replies to the user without asking for another tool, and the loop stops. That reply is the only output the user sees.
What does the loop look like in code?
The whole loop fits on one screen. Here it is as pseudo-code: llm, run_tool and tool_result stand in for your provider's SDK (software development kit) and your own tool functions.
Notice two things. The runtime, not the model, runs every tool. And the whole messages list goes back to the model on every call, which matters for cost later in this article.
Here is the dentist question as the messages would look with native tool calling. The field names follow Anthropic's Messages API, as its tool use documentation describes. Other providers carry the same three ideas (a tool call with an ID, a result that quotes the ID, a final text) under slightly different names.
The short text before the first tool call is the Thought, the tool_use blocks are the Actions, and the tool_result blocks are the Observations. The last turn has no tool call, so the runtime knows it is the final answer.
How many iterations does a real agent need?
As a rough rule of thumb, not a measurement, simple tasks finish in two or three iterations. Complex tasks, like "summarize all my receipts from last year for tax season", can take ten to fifteen or more. An uncapped agent will sometimes wander: it re-searches the inbox with slightly different keywords, over and over, convinced a better result is out there. Without a stop signal, it can loop forever.
So a production agent needs a hard limit on iterations, like the MAX_ITERATIONS above. Mailmind's runtime might cap at 20. When the cap is hit, the agent returns whatever partial answer it has or hands the task to a human. An agent that loops without end blocks the work queued behind it and burns money for nothing. Set the cap from what you observe on your own tasks, plus a safety margin.
What happens when a tool call fails?
Tool failure becomes just another Observation. If search_inbox gets an HTTP (HyperText Transfer Protocol) 500 server error or a connection timeout, the runtime turns that error into a message and puts it in the conversation. The model sees: Error: search_inbox timed out after 5 seconds. Native tool calling has a place for this. In Anthropic's API, for example, the runtime returns the error as a tool result marked is_error: true, as described in its guide to handling tool calls.
Now the model has a reasoning problem. It can retry the same call. It can fall back to a different tool, like search_inbox_by_date. It can work from earlier observations. Or it can conclude the task cannot be done and say "I couldn't find your appointment." What matters is that the error carries enough context for the model to choose. A bare "ERROR" string is useless. A message like "Tool unavailable: DNS (Domain Name System) lookup failed for mailhost.internal" gives the model a fighting chance.
Mailmind's runtime wraps every tool in a thin error handler, the try block in the code above. If the tool raises an exception, the handler catches it and returns it as an observation: Tool search_inbox failed: [the exception's message]. This keeps the loop alive and gives the model the same chance a developer would have to debug.
How does the loop stay within the context window?
You met the context window in Context Windows: The Model's Working Memory. Here is why it matters for agents. Every iteration adds the model's turn and a tool result to the conversation, and the runtime sends the whole conversation again on the next call. So three things grow with every step. Cost grows, because you pay for all those input tokens again. Latency grows, because the model has to read them again. And quality can slip: Anthropic's documentation on context windows notes that as the token count grows, accuracy and recall degrade, a problem it calls context rot.
Today's context windows are large, so a short loop rarely hits the hard limit. If a request does go over, it fails rather than quietly forgetting your earliest steps: Anthropic's API, for example, rejects a prompt that is too long with an error. Keeping the conversation small is your runtime's job.
The common defences:
- Trim or summarize old turns. Keep recent turns word for word and replace older ones with a short summary.
- Shrink observations. Instead of passing back a full 10-page PDF, the runtime cuts or summarizes it before it enters the conversation.
- Fetch only what each step needs. Design tools and tasks so one iteration does not pull in the whole world.
- Split big jobs. For Mailmind's receipt summary, one giant loop over 12 months of email is the wrong shape. Chunk by month, process each chunk in its own short loop, store the results outside the conversation, and build the final answer from those stored summaries.
- Cache the stable start. Prompt caching lets the provider resume from a prefix it has already processed, such as the system prompt and tool list, which cuts processing time and cost for the part of the request that has not changed.
Context Window Management for Big Jobs goes deeper on long jobs later in this series.
- Every call re-sends the whole history
- Cost and latency grow with each step
- Long contexts lose accuracy, and an over-long request fails
- Recent turns kept, older turns summarized
- Large tool outputs cut down before they enter
- A cached prefix makes the repeated part cheaper
Quick Reference
| Property | Typical value |
|---|---|
| Default format | Native tool calling (structured tool call and tool result messages); the text Thought, Action, Observation format only for models without tool support |
| Simple task iterations | 2 to 3 (rule of thumb) |
| Complex task iterations | 10 to 15 or more (rule of thumb) |
| Required guard | A hard max_iterations cap, set from your own task data |
| Tool failure representation | The error returned as a tool result, flagged as an error |
| Context defence | Trim or summarize old turns, shrink tool output, split big jobs, cache the stable prefix |
Frequently Asked Questions
Q: Can I run ReAct on any LLM (large language model), or does it require fine-tuning?
No fine-tuning is needed. Most current models support native tool calling: you send a list of tools with the request (each with a name, a description and a JSON Schema for its inputs), the model replies with structured tool call blocks, and you send back tool results. That is the ReAct loop with the parsing done for you. For a model without tool support, you can still prompt the original text format (Thought, Action and Observation lines) with a few examples and parse the Action lines yourself, but it breaks more easily. Either way the loop is an orchestration pattern, not a change to the model.
Q: What if the agent gets stuck in an infinite loop of repeated actions?
The iteration cap stops it. You can also detect loops by hashing each action (the tool name plus its arguments) and breaking when the same action repeats. Many production agents add a "stuck detector" that inserts a special observation: "You have repeated the same action twice. Try a different approach or answer with what you have."
Q: Does ReAct guarantee a correct answer?
No. The tools can return wrong data, the model can misread an observation, or the reasoning can lead to a wrong conclusion. ReAct reduces hallucination by grounding answers in retrieved data; it does not eliminate it. The answer is only as good as the tools and the logic.
Q: How is ReAct different from earlier chain-of-thought prompting?
Chain-of-thought writes reasoning steps and then a final answer in a single model call, with no outside information added along the way. ReAct interleaves reasoning with tool calls that bring new information in between calls. Chain-of-thought can only reason about what the model already knows. ReAct can reason about the world outside the model.
Q: What is the typical latency overhead of ReAct compared to a single LLM call?
Each iteration is a full model call plus the tool's own time, and each call reads a longer history than the last. A three-iteration loop takes roughly three times the model time of a single call, plus the tool latencies. For latency-sensitive features, keep iterations low, use fast tools (an in-memory lookup rather than a slow network call where you can), cache the stable prefix, and stream progress to the user while the loop runs.
Test yourself
Mailmind is summarizing a year of receipts for tax season. The agent is at iteration 25 of a 30-iteration cap and has not yet produced a final answer. It keeps fetching pages of emails and adding them to the conversation. The previous 24 iterations are all still in context. What is the root cause, and what would you change?
Answer: The root cause is too much data per iteration and a naive loop that keeps growing the conversation. Every call re-sends all 24 earlier steps, so each one is slower, costlier and less accurate than the last. Restructure the task. First, filter the inbox search so it returns only receipt emails, not every message. Then split the year into 12 monthly chunks. For each chunk, run a short loop of two or three iterations that extracts merchants, dates and totals and stores a structured summary outside the conversation. Run chunks in parallel where you can. Only after all chunks are done, run a final step that combines the monthly summaries into one tax report. Keep a hard cap on each monthly loop: if one runs out, save a partial summary flagged "incomplete: 8 of 12 months processed" and track which chunks succeeded. The user gets something useful, not a silent timeout.
Next: Multi-Agent Orchestration: Managers, Workers, and Pipelines. It covers when one agent loop is not enough and how several agents can split the work without retrying forever.
If you want this kind of breakdown every week, how real systems actually work under the hood, subscribe to Internals Decoded at internalsdecoded.com.
Sources
- ReAct: Synergizing Reasoning and Acting in Language Models
- Tool use with Claude
- Handle tool calls
- Context windows
- Prompt caching