The ReAct Loop: Closing the Circle Between Reasoning and Acting
From the ReAct paper to a working agent skeleton: the Thought → Action → Observation loop, why it reduces hallucinations, and the engineering caveats that matter. Written in Chinese and English.
Summary: ReAct (Reasoning + Acting) interleaves reasoning traces with tool calls into a loop: Thought → Action → Observation, repeated until the model produces a final answer. This post covers the motivation, the loop structure, a production-ready pseudocode skeleton, and the trade-offs you should know before adopting it.
Two failing extremes
Before ReAct, there were two main ways to make language models complete complex tasks — each with obvious flaws:
- Pure reasoning (Chain-of-Thought): the model can only “think”. It has no access to external information or tools, so answers depend entirely on what’s memorized in training data. Ask for real-time data or precise calculation and it fails — confidently.
- Pure acting (Act-only, e.g. early WebGPT): the model can call tools like search or a calculator, but without a reasoning trace to guide it. It reacts step-by-step, and a single failed action pollutes every following step. Errors snowball.
The ReAct paper (Yao et al., 2022, ReAct: Synergizing Reasoning and Acting in Language Models) makes a simple claim: interleave the two. A reasoning trace (Thought) guides the next action, and the observation (Observation) fed back from the tool corrects the reasoning. The two calibrate each other and form a closed loop.
The loop: Thought → Action → Observation
A typical ReAct trajectory looks like this:
Thought: I need to check the weather API's response format before deciding how to answer.
Action: search["weather api docs"]
Observation: {"city": "Shanghai", "temp_c": 28, "forecast": {"tomorrow": {"high": 32}}}
Thought: The user asked whether it will be hot tomorrow — I need tomorrow's forecast.
Action: search["Shanghai tomorrow weather"]
Observation: {"date": "tomorrow", "high": 32, "condition": "sunny"}
Thought: 32°C and sunny tomorrow. I can answer now.
Action: finish["Tomorrow in Shanghai: 32°C, sunny — quite hot, short sleeves recommended."]
The key property: every step has a basis. The model states why it acts (Thought), executes (Action), then decides what’s next from the real result (Observation). Two guards protect the loop — a failed tool call is not fatal (the error comes back as an Observation and the model can correct itself), and drifting reasoning gets pulled back by the observed result.
A production-ready skeleton
Translated into code, the core is under 20 lines:
async function runReAct(goal: string, tools: Tool[], maxIterations = 25): Promise<string> {
const messages: Message[] = [{ role: "user", content: goal }];
for (let i = 0; i < maxIterations; i++) {
// 1. Model output: either Thought + Action, or a final answer
const output = await llm.complete(messages);
if (output.type === "final") {
return output.answer; // the model considers the task done
}
// 2. Parse the structured tool call
const action = parseToolCall(output.text);
// 3. Execute the tool and get an Observation
const result = await executeTool(action.name, action.args);
// 4. Append this turn to the context and loop
messages.push(
{ role: "assistant", content: output.text },
{ role: "tool", content: result, toolCallId: action.id },
);
}
throw new Error(`Reached max iterations ${maxIterations}, task incomplete`);
}
That’s a minimal working agent skeleton. Most coding agents you use (including the tooling the author of this blog uses daily) are engineered versions of this loop: the model reads files, runs commands, observes results, and iterates until the task is done or the cap is hit.
Why ReAct works
| Benefit | Mechanism |
|---|---|
| Fewer hallucinations | Key facts come from real tool observations, not model memory |
| Recoverable errors | A failed tool call is just one Observation; the model can switch strategy |
| Observable & debuggable | Thought / Action / Observation leave a full trail; failures localize to a specific turn |
| Complex tasks decompose | The reasoning trace naturally splits a big goal into verifiable steps |
Five things that matter in production
- Make the tool protocol structured: declare tool parameters with JSON schema — far more reliable than letting the model write free-form shell commands. Have a retry/error path for parse failures.
- Always cap the loop:
maxIterationsis both a cost ceiling and a dead-loop fuse. Without it, one hallucination can blow up the token bill. - Return errors as Observations: when a tool throws, hand the error message back as a normal Observation so the model can self-correct, instead of aborting the loop.
- Stream the trace: push each Thought to the UI in real time — the user can “watch it think”, which dramatically improves perceived quality and trust.
- Persist the messages: the loop state is the message array. Persisting it gives you resume, replay, and audit for free.
Limitations and trade-offs
ReAct is not a silver bullet. Three costs to be honest about:
- Cost grows linearly: each iteration is a full LLM call; tokens scale with the number of steps.
- Errors accumulate: once an Observation is polluted (say, the tool returned misleading data), all subsequent reasoning builds on it.
- Don’t use it for simple tasks: five iterations to answer a one-liner is pure waste. That’s why many agent systems add a router — trivial questions get a single-shot answer, hard ones enter the loop.
Wrap-up
ReAct became the mainstream agent skeleton because it pairs the language model’s strongest ability (reasoning) with its weakest areas (precise execution, real-time information) into a loop: thinking guides acting, acting feeds back into thinking. Understand this one loop and you understand the “heart” of an agent — multi-agent collaboration, planner-executor patterns, and memory systems all grow on top of it.
Next time: when a single loop isn’t enough, how to stack a “planning” layer on top of ReAct for longer-horizon tasks.