Agentic AI · Module 9 of 41 · 11 concepts · about 18 min

ReAct Agents

The thought, action, observation cycle in full: why it works, how tools fit into it, how it knows when to stop, how it fails, and how to write it yourself.

At work: The loop inside almost every tool-using agent. Being able to write it from memory, and name its failure modes, is the most common practical test in agent interviews.

The ReAct cycle

What it is

ReAct stands for reason and act. The agent repeats three steps:

  • Thought: the model states what it knows and what it needs next.
  • Action: it requests one tool with specific inputs.
  • Observation: your code runs the tool and gives back the result.

Then the next thought, building on that observation. It ends with a thought that concludes, followed by a final answer instead of an action.

Why it matters

The cycle joins two things that are weak apart.

  • Reasoning alone works from memory and makes things up.
  • Acting alone, with no stated reasoning, calls tools without direction.
  • Together, each action has a stated purpose and each next thought is anchored to a real result.

In the real world: A support lead reads a ReAct trace line by line and finds the exact thought where the agent misread 'promised by 6' as 6 a.m.; the fix is a clearer tool result, made in ten minutes.

Visual example

ReAct

The observation is the only part that does not come from the model.

Thought, action, observation

  1. ThoughtWhat it knows and needs
  2. ActionOne tool with inputs
  3. Observationnot from the modelYour code's real result
  4. Thought againAct again, or answer
Question: Is order 88213 eligible for a delivery-fee refund?

Thought: I need the order's promised and actual delivery times.
Action: get_order[88213]
Observation: promised 18:00, delivered 19:02, fee ₹25, status delivered

Thought: It was 62 minutes late. I need the rule for late deliveries.
Action: get_policy[late delivery]
Observation: Delivery fee is refunded if the order is more than 30 minutes late.

Thought: 62 is more than 30, so it is eligible. I have what I need.
Final Answer: Yes. Order 88213 arrived 62 minutes late, and the policy
              refunds the delivery fee (₹25) for orders over 30 minutes late.

Reading 'Thought' as optional decoration. Without it the trace shows only tool calls, and when the agent does something odd nobody can tell what it was trying to do.

Why ReAct works

What it is

Four reasons the pattern is effective:

  • Grounding: every thought after the first is based on a real observation, not on what the model guesses a tool would return.
  • Decomposition for free: each thought only has to decide the next small step.
  • Error recovery: a failed or surprising observation is read on the next turn, and the agent can change course.
  • Transparency: the trace is a readable account of what was done and why.

Why it matters

Compare the alternatives.

  • A model reasoning without tools must recall facts, and recalls some wrongly.
  • A model that plans every tool call in advance cannot react to what the first call returns.
  • ReAct gives the model fresh facts at exactly the moment it must decide, which is why it became the default.

In the real world: A legal research agent's made-up citations fell sharply when it moved from answering from memory to a ReAct loop that had to fetch each case before citing it.

Assuming grounding makes the answer correct. The model can still misread an observation, or ignore one that contradicts its first idea. Grounding improves the odds; checks in code make it safe.

Tool usage within ReAct

What it is

There are two ways to connect tools to a ReAct loop:

  • Text ReAct (the original): the model writes Action: tool[input] as plain text. Your code finds that line with a pattern, runs the tool, and appends Observation: .... Generation is cut off at the word Observation: so the model cannot write its own.
  • Function-calling ReAct (the modern form): the model returns a structured tool request, as in module 3. The thought is its reasoning or a short text before the request; the observation is the tool result message.

Why it matters

They are the same loop with different plumbing.

  • Text ReAct works with any model but is fragile: the pattern match breaks on small format changes.
  • Function calling is reliable, supports several tool calls at once, and validates inputs against a schema.
  • New systems use function calling. The text form is still worth knowing, because it shows what the loop really is and is what interviews ask you to write.

In the real world: A team migrating an early agent from text parsing to function calling deletes its regular expressions and a class of 'could not parse action' failures that made up a tenth of all errors.

Visual example

Plumbing

Same cycle. The right-hand column is what to build today.

Text ReAct versus function-calling ReAct
text ReActfunction calling
Action isA line of text to parseA structured tool request
Inputs validatedBy your parserAgainst a JSON schema
Invented observationsPossible without a stop sequenceNot possible
Several tools per turnNoYes
Works with any modelYesNeeds tool-calling support
Typical failureParse errorsWrong tool or inputs

In text ReAct, forgetting the stop sequence. Without it the model writes 'Observation:' itself and invents the tool's result, then reasons from its own invention.

ReAct in pseudocode

What it is

The whole algorithm, in words a person can follow. Everything else in this module is a refinement of these twelve lines.

Why it matters

Pseudocode separates the idea from any library.

  • If you can write this, you can implement ReAct in any language against any model.
  • Each line is a place where a design choice lives: what goes in the history, how the tool is run, when to stop.

In the real world: A whiteboard interview asks for exactly this; candidates who include the step limit and the hand-over line without being prompted are marked as having shipped agents.

function react(question, tools, max_steps):
    history = [instructions, tool descriptions, question]

    repeat up to max_steps times:
        reply = model(history)                  # a thought, then an action or an answer
        add reply to history

        if reply is a final answer:
            return the answer

        result = run reply.tool with reply.inputs   # your code, with checks
        add result to history as an observation

    return 'could not finish' and hand over       # the limit was reached

Writing the loop as while True. The limit is not an optional extra. It is part of the algorithm.

ReAct in Python

What it is

A complete text-ReAct agent in about forty lines. It has four parts:

  • a prompt that teaches the format;
  • a parser that finds the action or the final answer;
  • a tool table mapping names to functions;
  • a loop with a step limit and error handling.

Why it matters

Reading real code removes the last of the mystery.

  • llm is any function that sends text to a model and returns text, stopping at a given word.
  • Every failure mode in the next card corresponds to one line here.
  • Module 10 builds the function-calling version with a real model API.

In the real world: An engineer who had only used frameworks writes this once from scratch, then finds a bug in their production agent that same week because they finally know where to look.

import re

PROMPT = """Answer the question using the tools.

Tools:
{tool_list}

Use exactly this format:
Thought: what you know and what you need next
Action: tool_name[input]
Observation: (the system fills this in; never write it yourself)
... repeat Thought/Action/Observation as needed ...
Thought: I now know the answer
Final Answer: the answer

Question: {question}
"""

ACTION = re.compile(r"Action:\s*(\w+)\[(.*?)\]", re.S)
FINAL = re.compile(r"Final Answer:\s*(.*)", re.S)

def react(question: str, tools: dict, max_steps: int = 8) -> str:
    tool_list = "\n".join(f"- {name}: {fn.__doc__}" for name, fn in tools.items())
    transcript = PROMPT.format(tool_list=tool_list, question=question)

    for _ in range(max_steps):
        reply = llm(transcript, stop=["Observation:"])     # the model must not invent results
        transcript += reply

        if final := FINAL.search(reply):
            return final.group(1).strip()

        action = ACTION.search(reply)
        if not action:
            observation = "Format error. Write 'Action: tool[input]' or 'Final Answer: ...'."
        elif action.group(1) not in tools:
            observation = f"Unknown tool '{action.group(1)}'. Tools: {', '.join(tools)}."
        else:
            try:
                observation = str(tools[action.group(1)](action.group(2)))[:1500]
            except Exception as error:                     # a tool failure is an observation
                observation = f"Tool error: {error}"

        transcript += f"\nObservation: {observation}\n"

    return "I could not finish this within the step limit. Handing over to a person."

Catching the tool error and returning an empty observation. The model reads silence as 'nothing found' and either repeats the call or answers wrongly. The error text is the observation.

Stopping conditions

What it is

A ReAct loop must be able to end in more ways than 'the model says so'. The complete set:

  • Final answer: the model gives an answer and requests no tool. The normal ending.
  • Step limit: a maximum number of turns.
  • Budget limit: a ceiling on tokens, money or wall-clock time for the task.
  • No progress: the same action repeated, or several turns with no new information.
  • Explicit hand-over: the model calls an escalate tool, or a policy forces one.
  • User interruption: a person cancels or redirects.

Why it matters

The model's own judgement about being finished is the least reliable stopping rule.

  • It can stop too early, announcing success on a half-finished task.
  • It can fail to stop, circling the same tools.
  • Limits set in code bound the worst case: you can always say the most a task can cost.
  • How a run ended is worth recording: a rising share of step-limit endings is an early warning.

In the real world: After one runaway task cost ₹4,000 overnight, a team adds a per-task budget; the worst case is now ₹20 and a hand-over note.

Visual example

Stopping

Only the first is decided by the model.

Six ways a ReAct loop ends
conditiondecided bywhat happens next
Final answerThe modelAnswer is checked and returned
Step limitCodeSummarise and hand over
Budget or time limitCodeSummarise and hand over
No progressCodeNudge once, then hand over
Explicit hand-overThe model or a policyA person takes the task with a summary
User interruptionThe userStop cleanly; keep state
def should_stop(state) -> str | None:
    """Return the reason to stop, or None to continue. Checked every turn, in code."""
    if state.step >= 12:
        return "step_limit"
    if state.cost_rupees >= 20:
        return "budget"
    if state.elapsed_seconds >= 120:
        return "time_limit"
    last = state.actions[-3:]
    if len(last) == 3 and len({(a.name, str(a.input)) for a in last}) == 1:
        return "repeating"                      # the same call three times running
    return None

Returning nothing useful when a limit is hit. The user gets silence and the next person starts from zero. On any forced stop, summarise what was done and what remains, and hand over.

ReAct failures: hallucinated tools and observations

What it is

Hallucination is a model stating something made up as if it were real. In ReAct it takes four forms:

  • Hallucinated tool: it requests a tool that does not exist (cancel_subscription).
  • Hallucinated arguments: a real tool with invented inputs, such as an order id it never saw.
  • Hallucinated observation: in text ReAct, it writes the tool's result itself.
  • Ignored observation: the tool returned 'not found', and the answer proceeds as if it had found something.

Why it matters

Each has a specific, mechanical fix. None is fixed by asking the model to be careful.

  • Unknown tool: return an error observation listing the real tools. Function calling with a strict schema makes it rare.
  • Invented arguments: validate ids against what tools actually returned in this run.
  • Invented observation: use a stop sequence, or function calling, where the result can only come from your code.
  • Ignored observation: make empty results explicit ('No order with that id exists') and check the final answer against the evidence.

In the real world: An agent kept 'finding' orders that did not exist because the tool returned {} for a miss; changing it to 'No order 88219 exists for this customer' ended the invented orders.

Visual example

Hallucination

Every fix is in code or in the tool's reply.

Four made-up things in a ReAct loop and the fix for each
what is made upexamplefix
A toolAction: cancel_subscription[412]Error observation listing real tools; strict tool schemas
An argumentget_order[88219], an id never seenCheck ids against earlier results before running
An observationModel writes 'Observation: refunded'Stop sequence, or function calling
A findingAnswers despite 'not found'Explicit empty results; verify the answer against evidence

Returning an empty list or blank string for 'nothing found'. A model fills a blank with its expectation. Say in words what was not found.

ReAct failures: infinite loops and drift

What it is

Two failures of the loop itself:

  • Infinite reasoning loops: the agent repeats the same action, alternates between two, or keeps 'thinking' without acting. Usual causes: a tool that returns nothing useful, an impossible task, or an error message it cannot act on.
  • Drift: over many steps the agent wanders from the goal, answers a different question, or forgets a constraint stated at the start.

Why it matters

Both get worse as the history grows.

  • A loop wastes money without bound unless code stops it.
  • Drift happens because the original request is now far back in a long context, behind many tool results.
  • Fixes: detect repeats in code; make error messages say what to do instead; restate the goal near the end of the context each turn; keep tool results short.

In the real world: An agent called search_flights eleven times with the same inputs because the tool returned 'Error' with no detail; returning 'No flights on that date; try the day before or after' broke the loop at the source.

Visual example

Loops

The cause is almost always in what the tool returned.

Why a ReAct agent gets stuck, and what fixes it
symptomusual causefix
Same call repeatedTool returns nothing actionableExplain the failure and suggest an alternative
Two calls alternatingEach undoes or contradicts the otherDetect the cycle; nudge once; hand over
Endless thinking, no actionTask is impossible or underspecifiedAllow 'ask the user' and 'hand over' as actions
Answers the wrong questionGoal buried in a long historyRestate goal and constraints each turn
Stops half-wayLost track of the partsKeep a to-do list in view
def nudge_if_stuck(state) -> str | None:
    """Give the model one chance to change approach before stopping it."""
    recent = [(a.name, str(a.input)) for a in state.actions[-2:]]
    if len(recent) == 2 and recent[0] == recent[1] and not state.nudged:
        state.nudged = True
        return ("You have made the same call twice with the same result. "
                "Do not repeat it. Try a different tool or input, or call hand_over.")
    return None

# Against drift, append this to the context every turn:
REMINDER = "Goal: {goal}\nConstraints: {constraints}\nStill open: {open_items}"

Fixing a loop by raising the step limit. If the agent is stuck at step 8, it will be stuck at step 30, at four times the cost. Find why the observation is not helping.

When ReAct is the wrong choice

What it is

ReAct is a default, not a universal answer. Prefer something else when:

  • the steps are fixed: use a workflow, with no loop;
  • the task is long with a known shape: add a plan, so the large model is not called for every small step;
  • there are many independent strands: run them in parallel, as sub-agents or a fan-out;
  • latency matters most: each ReAct turn is a full model call in sequence;
  • an action is high-stakes: put a fixed approval step around it, whatever the loop decides.

Why it matters

ReAct's cost grows quickly with length.

  • Each turn resends the whole history, so ten turns cost far more than ten times one turn.
  • Each turn is another chance to go wrong, and errors compound.
  • Knowing when not to use it is what interviewers probe after you have explained how it works.

In the real world: A team's invoice 'agent' ran a ReAct loop to call the same four tools in the same order every time; replacing it with a four-step workflow cut cost by 70 percent and removed every loop failure.

Interview questions: ReAct agents

What it is

The questions interviewers ask about ReAct agents, from the first screening call to the senior round. Cover the answer, say yours aloud, then compare.

Why it matters

Reading an idea and explaining it are different skills.

  • Each answer below is the two or three sentences an interviewer is listening for.
  • The reasoning behind each one is in the cards above. If an answer surprises you, go back to that card.

In the real world: The single most common practical agent question is 'write a ReAct loop'; the follow-up is always 'now, what stops it?'.

Q: How does ReAct work?

The model alternates a thought, an action and an observation. It reasons about what it needs, requests one tool, reads the real result, and reasons again, until it gives a final answer or a limit stops it.

Q: Why does it work better than reasoning alone?

Each step is grounded in a real observation instead of the model's guess, the task is broken into small decisions, errors can be noticed and corrected on the next turn, and the trace explains what happened.

Q: How would you implement it?

A loop: send history and tools, get a reply, stop on a final answer, otherwise validate and run the tool, append the result, repeat up to a step limit. With function calling the action is a structured request; in the original form it is parsed from text with a stop sequence before 'Observation'.

Q: What are the stopping conditions?

A final answer from the model, plus limits in code: steps, tokens or cost, time, repeated identical actions, an explicit hand-over, and user interruption. A forced stop returns a summary and hands over.

Q: What is a hallucinated tool and how do you handle it?

A request for a tool that does not exist. Return an error observation naming the real tools so the model can correct itself; strict tool schemas and a short tool list make it rare; count it as a metric.

Q: How do you prevent infinite loops?

Step and budget limits, detection of repeated or alternating calls, one corrective nudge, then a hand-over. And fix the cause: make tool errors and empty results say what to try instead.

Q: What are ReAct's weaknesses?

Cost grows faster than the number of steps because history is resent; latency is sequential; errors compound; and the agent can drift from the goal over a long run.

Q: When would you not use ReAct?

When steps are fixed, when the task is long and plannable, when work is independent and can run in parallel, or when a step is risky enough to need a fixed approval gate.

Explaining ReAct as a prompt format. It is a loop your code runs; the format is only how the model expresses its next step.

Practise it: Design an AI Customer Support Agent

Exercise: implement ReAct from scratch

What it is

A short build that uses this module's ideas. Write it yourself before looking anything up; the checks at the end tell you whether it works.

Why it matters

An idea you have only read fades in a week.

  • Building it once, even in fifty lines, is what makes it yours.
  • Interviewers can tell the difference between someone who has read about a loop and someone who has debugged one.

In the real world: A hiring team uses this as a ninety-minute pairing exercise; candidates are not judged on the happy path but on what they do when the interviewer removes the stop sequence.

Your task. Write a working ReAct agent with no framework, then break it on purpose and fix each break.

Steps

  1. Write three tools over fake data: get_order(id), get_policy(topic) and calculate(expression).
  2. Implement the text-ReAct loop from this module against any model API, with a stop sequence and a step limit.
  3. Ask it: 'Order 88213 cost ₹480 plus a ₹25 fee and arrived 62 minutes late. What should the customer be refunded in total?'
  4. Break it four ways and observe: remove the stop sequence; make get_order return an empty string for a missing order; ask about a tool that does not exist; set the step limit to 2.
  5. Fix each: restore the stop sequence, return explicit 'not found' text, return the tool list on an unknown tool, and return a summary when the limit is hit.
  6. Add should_stop with a repeated-call check, and log the reason every run ended.

You are done when

  • The agent answers the question with a trace you can read line by line.
  • You have seen, in your own output, an invented observation and an invented order.
  • No input you can think of makes the loop run past the limit or crash.
  • Every run logs one of: final_answer, step_limit, repeating, error.

Stretch. Rewrite the loop using your provider's function calling and compare the lines of code and the failure modes that disappear.