M1 · Agent Architecture and DesignM1-0323 min read
Lesson 3 of 58 · Module 2 of 10 · Week 1
Threads:The memory and grounding threadThe oversight thread
ReAct: Interleaving Reasoning and Acting, with a Worked Implementation
ReAct (Yao et al., 2022) interleaves a Thought -> Action -> Observation loop so that each reasoning step is grounded in a real tool result before the next Thought runs, which is what curbs hallucination that chain-of-thought alone cannot catch — and a production ReAct loop is only as trustworthy as its handling of the Observation step when the Action it depends on actually fails.
By the end you can
- 01Trace one full Thought -> Action -> Observation -> Thought cycle and explain what specifically grounds the second Thought in something chain-of-thought reasoning never provides.
- 02Explain why ReAct is neither "chain-of-thought with extra steps" nor "just tool calling," and name the property that distinguishes it from both.
- 03Implement a ReAct loop's control flow, including the branch that runs when a tool call fails rather than succeeds.
- 04Recognize the standing exam trap: assuming more reasoning tokens, on their own, reduce hallucination the way a real Observation does.
The ReAct loop: Thought, Action, Observation
The three-part cycle
[GROUND TRUTH] (Sources/ncp-aai/domain-1-agent-architecture-design.md): the loop's shape is Thought → Action (call a tool or query an environment) → Observation → Thought → …, repeating until the model decides it has enough information to produce a final answer. Each of the three parts does a distinct job. A Thought is the model reasoning, in natural language, about what it currently knows and what it should do next — functionally similar to a chain-of-thought step, but written with the explicit purpose of deciding on an action rather than working toward a final answer directly. An Action is a concrete, executable step the surrounding system can actually carry out: call a search API with a specific query, look up a specific record, run a specific calculation. An Observation is the real result that action produced, fed back into the model's context before the next Thought begins.
Why the interleaving is the whole point
The word "interleaves" in the ground-truth framing above is doing real work and is worth dwelling on, because it is the single fact this domain's own trap list treats as most commonly missed. Interleaving does not mean "reason a bit, then act a bit, in some order" — it means the loop alternates strictly between the two, with each new Thought conditioned on the Observation that just came back, not on the model's own prior expectation of what that Observation would say. That structural alternation is what gives ReAct its error-correcting property: if a Thought predicts "the search will show X" and the Observation instead shows Y, the very next Thought is reasoning from Y, the actual result, not from X, the prediction — which means a wrong guess gets corrected by real information within a single loop iteration, rather than compounding silently the way an uncorrected chain-of-thought step does.
What "grounding" concretely buys you
[GROUND TRUTH] (Sources/ncp-aai/domain-1-agent-architecture-design.md): the source material states plainly that grounding each step in a real observation is what reduces the hallucination and error propagation seen in reasoning-only chain-of-thought, and reports that in the original work, ReAct outperformed imitation- and reinforcement-learning baselines on interactive benchmarks (ALFWorld, WebShop) using only one or two in-context examples, and improved factual QA and fact-verification tasks by interacting with a simple Wikipedia API. The mechanism behind that result is exactly the alternation described above: a model that has to check its own intermediate claims against a real tool result, every single step, has far fewer opportunities to build an entire wrong answer on top of one early, uncorrected mistake, because each step is a fresh chance for reality to intervene before the reasoning goes any further.
Why ReAct is not chain-of-thought with extra steps
The comparison the exam builds its trap items around
[GROUND TRUTH] (Sources/ncp-aai/domain-1-agent-architecture-design.md): the domain's own trap list states this distinction directly — CoT reasons internally with no external actions, while ReAct adds an act-and-observe step that grounds reasoning in real tool output. The distinction is not about how many reasoning steps exist, or how sophisticated any individual step's reasoning is; a five-step chain-of-thought and a five-iteration ReAct loop can both look, on the page, like a sequence of "Thought"-labeled paragraphs. The difference is whether any of those paragraphs is followed by an actual, executable action whose real-world result then constrains the next paragraph. Remove the Action and Observation steps from a ReAct trace and what remains is functionally chain-of-thought — reasoning with nothing to check it against — which is exactly why "ReAct is CoT with extra formatting" is the specific misconception this lesson's opening example (the confident five-step wrong answer) was built to make concrete.
Why ReAct is also not "just tool calling"
[GROUND TRUTH] (Sources/ncp-aai/domain-1-agent-architecture-design.md): the same trap list names a second, related misconception — ReAct is not "just tool calling" either, because the interleaving of explicit reasoning with actions is the point, not the presence of tool calls by themselves. A system that calls a search tool, a calculator, and a code-execution tool one after another, purely because a fixed pipeline says to call them in that order, is doing tool calling without doing ReAct's actual work, if none of those calls is preceded by an explicit Thought reasoning about why this specific action, right now, given what the loop has learned so far. The visible artifact — an Action being executed — looks identical in both cases. What differs is whether a reasoning step chose that action based on the accumulated Observations, or whether the action ran on a fixed schedule regardless of what earlier Observations actually said. A ReAct loop that always calls the same three tools in the same order, no matter what each Observation reveals, has kept the interleaving's visual form while losing its substance — the Thought steps exist on paper, but they are not actually steering which Action comes next.
More reasoning tokens are not a substitute for a real Observation
The third trap this domain names, and the one most directly relevant to a production system's design choices, is this: [GROUND TRUTH] (Sources/ncp-aai/domain-1-agent-architecture-design.md) more reasoning tokens don't guarantee correctness; the Observation step is what curbs hallucination. A team tempted to "just make the Thought steps longer and more careful" as a cheaper substitute for wiring up a real tool is optimizing the part of the loop that was never the source of the correction in the first place. A longer, more elaborate Thought reasoning from a wrong or missing Observation is still reasoning from wrong or missing information — more words spent on a flawed premise do not fix the premise. The fix is always upstream, in whether a real Action ran and a real Observation came back, not downstream, in how much reasoning gets applied to whatever information happened to be available.
Worked implementation: a ReAct loop with tool-failure handling
Why the failure path deserves its own worked example
Nearly every conceptual description of ReAct — including the one above — describes the happy path: a Thought decides on an Action, the Action succeeds, an Observation reflects that success, and the next Thought reasons from accurate information. Production systems do not get to assume the happy path. A search API times out. A database lookup returns an error instead of a record. A calculator tool receives malformed input and raises an exception. What a ReAct implementation does at exactly that moment — the moment an Action fails rather than succeeds — determines whether the loop degrades gracefully or silently reasons its way to a confident, wrong answer built on an Observation that was actually an error message the model misread as data.
The control flow, worked step by step
Consider an agent answering the question "What was the closing stock price of the company that acquired Company X, on the day after the acquisition closed?" — a question that genuinely requires two sequential lookups, where the second lookup's query depends on the first lookup's result.
Iteration 1
Thought: "I need to find which company acquired Company X before I can
look up any stock price. I will search for the acquirer."
Action: search_tool(query="company that acquired Company X")
[tool executes]
Observation: "Company Y acquired Company X in a deal that closed on
March 14."
Iteration 2
Thought: "Now I know the acquirer is Company Y and the deal closed
March 14. I need Company Y's closing stock price on
March 15, the day after close."
Action: stock_price_tool(ticker="Company Y", date="2024-03-15")
[tool executes -- and this time it fails]
Observation: "ERROR: ticker lookup failed - 'Company Y' is not a valid
ticker symbol. Did you mean to provide an exchange
ticker (e.g. 'CY') rather than a company name?"
Iteration 3
Thought: "The stock_price_tool needs an exchange ticker symbol, not a
company name, and my last call used the company name. I do
not yet know Company Y's ticker symbol. I need to look that
up before retrying the price lookup, not retry the same
failing call unchanged."
Action: ticker_lookup_tool(company_name="Company Y")
[tool executes]
Observation: "Company Y's ticker symbol is CY."
Iteration 4
Thought: "Now I have the correct ticker, CY. I can retry the stock
price lookup with the corrected input."
Action: stock_price_tool(ticker="CY", date="2024-03-15")
[tool executes]
Observation: "CY closed at $47.82 on 2024-03-15."
Iteration 5
Thought: "I now have both pieces of information the question requires:
the acquirer (Company Y, ticker CY) and its closing price
the day after close ($47.82 on 2024-03-15). I can answer."
Final Answer: "$47.82."
Constructed scenario, illustrative only — company names, dates, and figures are invented for the walkthrough. The load-bearing moment in that trace is Iteration 3. A naive implementation that treats every Observation as reliable data, rather than checking whether it represents a failure, would feed the literal string "ERROR: ticker lookup failed..." into the next Thought's context as if it were a stock price, and the model — reasoning from what looks like retrieved information — could easily hallucinate a plausible-sounding dollar figure rather than recognizing that no real price was ever retrieved. What Iteration 3 shows instead is a Thought that correctly interprets the Observation as a failure, diagnoses why it failed (wrong input shape, not a transient glitch), and chooses a different next Action — looking up the ticker — rather than blindly repeating the failed call or fabricating an answer from the error text.
What the surrounding harness has to guarantee for that Thought to be possible
The model producing Iteration 3's Thought can only reason correctly about the failure if the surrounding system feeds it an Observation that clearly distinguishes success from failure, rather than a bare exception trace or a silently truncated empty string. A minimal but load-bearing implementation pattern looks like this:
def run_react_step(thought_fn, action_registry, state):
"""One Thought -> Action -> Observation cycle.
Constructed scaffold, illustrative only -- names and
signatures are simplified for the walkthrough."""
thought, chosen_action, action_args = thought_fn(state)
state.log_thought(thought)
tool = action_registry.get(chosen_action)
try:
result = tool.call(**action_args)
observation = f"Result: {result}"
state.record_success(chosen_action)
except ToolInputError as e:
# The action ran, but the input shape was wrong -- this is
# exactly Iteration 3's case above. The Observation must say
# THIS explicitly, or the next Thought cannot distinguish a
# bad-input failure from a "no data exists" result.
observation = f"ERROR: invalid input for {chosen_action} - {e}"
except ToolUnavailableError as e:
# A transient dependency failure -- distinct from a bad-input
# failure, and the thing M2-05's circuit breaker exists for.
observation = f"ERROR: {chosen_action} is currently unavailable - {e}"
state.record_observation(observation)
return observation
The comment inside the except ToolInputError branch is the crux of the whole worked example: an Observation string has to carry enough information for the next Thought to tell "the input I sent was wrong" apart from "the thing I searched for genuinely does not exist," because those two failures call for completely different next Actions — retry with corrected input, versus give up on this line of inquiry and try a different approach entirely. A harness that collapses every failure into a single generic "Something went wrong" Observation removes the information the next Thought would need to recover the way Iteration 3 recovered, and pushes the loop toward exactly the hallucinated-answer failure mode this section opened with.
Distinguishing a bad-input failure from a persistently unavailable tool
The two except branches above are not decorative — they map directly onto the resilience-pattern distinction this cert treats as its own separately numbered material. A ToolInputError, like Iteration 3's wrong-ticker-format case, is not a transient fault at all in the sense M2-04's Retry pattern uses that term; retrying the identical call with the identical wrong input will fail identically every time, and the correct move is what Iteration 3 did — change the input, via a different Action, not repeat the same call. A ToolUnavailableError — the tool's underlying API is timing out or returning 500s — is the shape of failure M2-05's Retry-versus-Circuit-Breaker material addresses directly: a brief version of this fault is worth retrying, a persistent version of it is exactly when a circuit breaker should trip and stop the loop from hammering a dependency that is not coming back on its own. A ReAct loop's Thought step benefits from knowing which of these two failure shapes it is looking at, because "should I retry, retry differently, or give up on this tool for now" is itself a reasoning question the Thought has to answer correctly for the loop to recover gracefully rather than either looping forever on a doomed retry or abandoning a fixable problem too early.
When does a ReAct loop stop?
A ReAct loop needs an explicit termination condition, because nothing about the Thought → Action → Observation cycle inherently ends on its own — left unconstrained, a model could keep generating "one more search, just to be sure" indefinitely. Three termination conditions cover the practical cases. The clean case is the model's own Thought concluding that it now has sufficient information to answer, exactly as Iteration 5 in the worked trace above does explicitly, stating what it now knows and moving to a final answer rather than another Action. The bounded case is a hard iteration cap set by the surrounding harness — after N cycles with no clean conclusion, the loop is stopped regardless of the model's own assessment, which protects against a model that keeps deciding "one more lookup" past the point of diminishing returns. The failure case is the loop recognizing, from a pattern of repeated failed Observations against the same class of problem, that continuing is unlikely to help — a harness-level check that, after some number of consecutive tool failures on attempts to resolve the same sub-question, surfaces a partial answer or an explicit "unable to determine" response rather than continuing to retry indefinitely. All three conditions matter for a production loop; relying only on the model's own judgment to stop, with no hard cap and no failure-pattern check, means a single stubborn or confused Thought sequence can consume unbounded tool calls and latency with no external backstop.
What happens when the second failure hits the same sub-question
The worked implementation in §3 resolved cleanly after one corrective Action — the ticker lookup fixed the input, and the retried price lookup succeeded. Production tool failures are not always that cooperative, and a thorough treatment of tool-failure handling has to account for what a Thought does when the same sub-question fails twice in a row, through two different Actions, rather than assuming one correction always works.
Extend the worked trace: suppose Iteration 4's retried stock_price_tool(ticker="CY", date="2024-03-15") call also fails, this time with an Observation reading "ERROR: no trading data available for CY on 2024-03-15 - market holiday." A Thought reasoning correctly from that Observation has to recognize this as a third, distinct failure shape beyond the two the code in §3 distinguishes: not a bad-input failure (the ticker and date format were both correct this time) and not an unavailable-dependency failure (the tool itself is working fine and returned a clear, specific answer) but a genuinely absent-data case — the requested day simply has no trading data to return, because markets do not trade on holidays. The correct next Action is a third kind of recovery entirely: adjust the date to the next actual trading day, rather than retrying the same date again or giving up on the whole question. A Thought that fails to recognize this third failure shape, and instead treats "no trading data available" the same way it treated the earlier bad-ticker error, might retry the identical failing call, burning an iteration for no benefit; a Thought that treats it the same way it would treat a dependency outage might invoke a circuit-breaker-style "give up on this tool for now" response to a tool that was never actually broken.
This is the concrete reason a production Observation schema benefits from carrying more than a binary success/failure signal: bad input, unavailable dependency, and no-data-for-this-query are three failure shapes calling for three different corrective Actions, and collapsing any two of them into the same generic error string removes information the next Thought needs to choose correctly among retry, correct-and-retry, or move to a different approach. A harness that tags each failure Observation with which of these shapes it represents — even something as simple as a structured failure_kind field alongside the human-readable message — gives the model's next Thought a much more reliable basis for the same kind of correct reasoning Iteration 3 demonstrated, rather than relying on the model to infer the failure's true shape purely from free-text error wording that may vary across tools and providers.
⭐ THE EARNED INSIGHT The grounding property that makes ReAct reduce hallucination and the resilience property that makes a production ReAct loop survive real tool failures are the same mechanism viewed from two angles, not two separate features bolted together. Grounding means the next Thought reasons from what an Observation actually says rather than from what the model expected it to say; correct failure handling is exactly that same discipline applied to the specific case where what the Observation actually says is "this failed, and here is why." A Thought that hallucinates a plausible answer from a garbled error string has failed at grounding in precisely the same way a Thought that hallucinates a fact from an ambiguous search result has — the only difference is which kind of real-world signal it chose to ignore in favor of a more comfortable guess.
ReAct vs. chain-of-thought vs. plain tool calling
| Chain-of-thought | Plain tool calling | ReAct | |
|---|---|---|---|
| External actions | None — all reasoning is internal | Present, but not necessarily reasoning-driven step to step | Present, and each one is chosen by the immediately preceding Thought |
| Grounding in real results | None — nothing checks an intermediate claim against reality | Only incidental, if a fixed pipeline happens to use the results at all | Central — the next Thought is conditioned directly on the Observation that just returned |
| Error propagation | An early wrong step compounds silently through later steps | Depends entirely on whether downstream code checks tool results, independent of any reasoning about them | An Observation showing an unexpected result is exactly what the next Thought is reasoning from, so a wrong assumption is caught within one cycle |
| Order of actions | N/A — no actions exist | Often fixed in advance by a pipeline, run regardless of intermediate results | Dynamic — chosen by each Thought based on the accumulated Observations so far |
| Best-fit task | Reasoning-only problems with no need for external, verifiable information | Deterministic pipelines where the same fixed sequence of calls is always correct | Multi-step tasks where a later step's correct action depends on an earlier step's actual, unpredictable result |
| Failure handling | Not applicable — no external calls to fail | Whatever the pipeline's error handling happens to do, independent of the model's reasoning | The next Thought reasons explicitly about a failed Observation, choosing a different Action, a retry, or a graceful stop |
Why ReAct is on the NCP-AAI exam
ReAct sits inside Agent Architecture and Design, tied for the heaviest weight of any domain in the NCP-AAI blueprint at 15%, and is named directly as objective 1.2 rather than folded into a generic "reasoning frameworks" item. [GROUND TRUTH] (Sources/ncp-aai/domain-1-agent-architecture-design.md): the domain's scope note states candidates are expected to "implement" ReAct, not merely define it — a verb choice that matches this lesson's decision to go deep on the worked implementation and its tool-failure branch rather than stopping at the conceptual Thought/Action/Observation description alone.
Expect three recurring question shapes. The first is direct identification: a described loop's behavior (interleaving reasoning with tool calls, grounding claims in real results) mapped to "ReAct" by name, distractors typically naming chain-of-thought or a generic "agentic workflow" instead. The second is the CoT-versus-ReAct-versus-tool-calling distinction worked through in the comparison table above, usually phrased as a scenario ("a model reasons through five steps with no external calls" versus "a model calls three tools in a fixed sequence with no reasoning between them") asking which pattern, if any, the description actually matches. The third, and the one this lesson's worked example was built specifically to prepare for, is a scenario involving a failed tool call, asking what the correct next Action or Thought should be — testing whether a candidate understands that a ReAct loop's resilience lives in how its Thought steps interpret and respond to failed Observations, not merely in whether the loop has tools wired up at all.
Common mistakes about ReAct
| Mistake | What it gets wrong | Fix |
|---|---|---|
| Calling any multi-step reasoning trace "ReAct" | Ignores whether any real external action and observation is actually present | Confirm the trace alternates real Actions with Observations that feed the next Thought — reasoning alone, however long, is chain-of-thought |
| Treating a fixed sequence of tool calls as ReAct | Confuses the presence of tool calls with reasoning-driven selection of them | Check whether each Action is chosen by the Thought immediately preceding it, based on prior Observations, rather than by a fixed schedule |
| Assuming longer, more careful Thoughts substitute for a missing or wrong Observation | Optimizes the reasoning layer while leaving the actual grounding problem unfixed | Fix the Action/Observation pipeline itself — no amount of reasoning repairs a Thought built on bad or absent information |
| Feeding a raw exception or an empty string back as an Observation on tool failure | Gives the next Thought no way to distinguish "no data exists" from "the call itself was malformed" | Return an explicit, typed failure Observation (bad input vs. unavailable dependency) so the next Thought can choose the right recovery Action |
| Retrying a failed Action with identical input, unchanged | Treats every failure as transient when some failures are permanent given that input | A Thought reasoning from a bad-input Observation should choose a different Action (correct the input, or use a different tool), not repeat the same call |
| Letting a loop run with no hard iteration cap | Assumes the model's own judgment to stop is a sufficient safeguard | Enforce a harness-level cap and a repeated-failure check independent of the model's own stated confidence that "one more step" will help |
Does ReAct require a specific framework to implement?
No — ReAct describes a control-flow pattern (interleave Thought, Action, Observation; let each Thought choose the next Action based on the most recent Observation), not a specific product. The pattern can be implemented with a hand-rolled loop like the worked example in §3, or with any general-purpose agent-orchestration library that exposes a tool-calling interface and lets the model's output drive which tool runs next. What makes an implementation genuinely ReAct, regardless of the underlying library, is that Thoughts and Observations alternate and each Thought is conditioned on the Observation immediately before it — the specific code structure achieving that alternation is an implementation detail, not the defining property.
How many in-context examples does ReAct need to work well?
[GROUND TRUTH] (Sources/ncp-aai/domain-1-agent-architecture-design.md): the original ReAct work reports strong results on interactive benchmarks using only one or two in-context examples, which is a notably small number compared to what many few-shot prompting techniques require to reach comparable performance. This does not mean one or two examples is a universal requirement or ceiling for every task — the source material states this as the reported result for the specific benchmarks studied (ALFWorld, WebShop, and the factual-QA and fact-verification tasks using a Wikipedia API), not as a general law governing every possible ReAct deployment. ⚠️ UNVERIFIED: the exact number of examples a specific production task needs to reach acceptable performance is task-dependent and is not something this source material states as a fixed rule, so treat "one or two examples" as the reported benchmark result rather than a guaranteed number for any new task.
Glossary recap
| Term | One-line definition |
|---|---|
| ReAct | A framework interleaving Thought, Action, and Observation steps so each reasoning step is grounded in a real tool result |
| Thought | A natural-language reasoning step deciding what to do next, based on everything observed so far |
| Action | A concrete, executable step — a tool call or environment query — chosen by the immediately preceding Thought |
| Observation | The real result an Action produced, fed back into context before the next Thought runs |
| Chain-of-thought (CoT) | Step-by-step internal reasoning with no external actions or observations to ground it |
| Grounding | Checking an intermediate reasoning claim against a real, external result rather than the model's own expectation |
| Bad-input failure | A tool call that fails because the input it received was malformed or wrongly shaped, not because the dependency is unavailable |
| Unavailable-dependency failure | A tool call that fails because the underlying service is down, slow, or erroring — the shape of failure Retry and Circuit Breaker patterns address |
| Termination condition | The explicit rule (model concludes it has enough information, a hard iteration cap, or a repeated-failure check) that ends a ReAct loop |
Key takeaways
- ReAct interleaves Thought, Action, and Observation in a strict alternation, and each new Thought is conditioned on the real Observation that just returned, not on the model's prior expectation of what it would say.
- ReAct is neither chain-of-thought with extra formatting (no real external actions ground the reasoning) nor plain tool calling (tool calls with no reasoning-driven selection between them) — the interleaving itself, reasoning choosing action based on observation, is the defining property.
- More reasoning tokens do not substitute for a real Observation; the grounding, not the volume of reasoning, is what curbs hallucination.
- A production ReAct loop's Observation must distinguish a bad-input failure (retry with different input, or a different Action entirely) from an unavailable-dependency failure (a Retry/Circuit-Breaker concern), or the next Thought cannot recover correctly.
- A loop needs an explicit termination condition — clean completion, a hard iteration cap, and a repeated-failure check — because nothing about the Thought/Action/Observation cycle stops it on its own.
- The original ReAct work reported strong results on interactive benchmarks with only one or two in-context examples, a specific reported result rather than a universal number for every task.
Interleaving reasoning with action inside one agent is the foundation, but many real tasks are too large or too specialized for one agent to handle every role itself — a research step, a drafting step, and a compliance-review step may genuinely need three different specialized agents rather than one generalist running a longer ReAct loop alone. M1-04, already authored in this module, covers exactly that shift: the four orchestration topologies — centralized, decentralized, federated, and hierarchical — that govern how a team of agents, each potentially running its own ReAct-style loop, actually coordinates.
Next: M1-04 — multi-agent orchestration topologies, and why the topology choice, not the agent count, determines a multi-agent system's coordination cost and failure mode.