← All posts

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

  1. 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.
  2. Always cap the loop: maxIterations is both a cost ceiling and a dead-loop fuse. Without it, one hallucination can blow up the token bill.
  3. 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.
  4. 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.
  5. 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.