n4nAI

ReAct prompting explained: reasoning plus acting

ReAct prompting interleaves reasoning with tool actions in an LLM loop. This guide explains the pattern, shows a Python implementation, and debunks myths.

n4n Team4 min read793 words

Audio narration

Coming soon — every post will get a voice note here.

ReAct prompting is a language-model interaction pattern that alternates free-form reasoning statements (Thoughts) with structured action requests (Actions) and external observations, enabling the model to decide and execute tool calls mid-generation. Unlike static chain-of-thought, ReAct prompting closes the loop by feeding real environment feedback back into the context, so each subsequent reasoning step is grounded in actual results rather than the model’s priors.

How ReAct prompting works

The core mechanic is a tight loop of three signal types emitted in text:

  • Thought: A natural-language rationale explaining what the model knows and what it needs next.
  • Action: A structured invocation of an external tool, typically tool_name(argument).
  • Observation: The string returned by executing that tool, injected back into the context.

The model generates a Thought and an Action. Your runtime executes the Action, captures the Observation, appends it, and calls the model again. The cycle repeats until the model emits a terminal action (e.g., finish(answer)).

The canonical prompt shape

Question: What is the capital of the state where Apple's HQ is located?

Thought: I need to find Apple's headquarters location first.
Action: search("Apple headquarters address")
Observation: Apple Park, Cupertino, California.
Thought: Cupertino is in California. I need the state capital.
Action: search("capital of California")
Observation: Sacramento.
Thought: I now have the answer.
Action: finish("Sacramento")

This is not a special API. It is a prompt convention plus a parser. The LLM never directly runs code; your harness does.

Parsing actions without fragility

Treat the Action line as a contract. A minimal regex works for prototypes:

import re

def parse_action(response: str):
    match = re.search(r"Action:\s*(\w+)\((.*)\)", response)
    if match:
        tool = match.group(1)
        args = match.group(2).strip().strip('"')
        return tool, args
    return None, None

In production you will want stricter schema validation (JSON actions, constrained decoding, or a grammar). But the loop stays identical.

Why ReAct matters for agentic systems

Grounding kills hallucination. Pure chain-of-thought lets the model invent facts silently. ReAct forces an explicit Action and returns a real Observation. If the search tool says “no results,” the model sees that and adapts.

Control flow becomes emergent. You do not hardcode a pipeline. The model chooses the next tool based on the question and prior observations. This handles branching queries that a fixed DAG would miss.

Tool composability is trivial. Any function you can wrap as name(string) becomes a capability. Databases, APIs, code interpreters, and internal services all look the same to the loop.

Auditability. Because every step is text, you get a human-readable trace of why the agent did what it did. That trace is the debugging surface.

A minimal working ReAct agent

Below is a runnable skeleton using the OpenAI SDK. It is intentionally bare:

from openai import OpenAI

client = OpenAI()  # reads OPENAI_API_KEY from env

TOOLS = {
    "search": lambda q: f"Mock result for '{q}'",
    "finish": lambda a: f"ANSWER: {a}"
}

def run_react(question: str, max_steps: int = 5):
    messages = [
        {"role": "system", "content": (
            "You are a ReAct agent. Always output:\n"
            "Thought: <reasoning>\n"
            "Action: <tool>(<arg>)\n"
            "Use finish(answer) when done."
        )},
        {"role": "user", "content": f"Question: {question}"}
    ]
    for _ in range(max_steps):
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            stop=["Observation:"]
        )
        text = resp.choices[0].message.content
        messages.append({"role": "assistant", "content": text})

        tool, arg = parse_action(text)
        if tool == "finish":
            return arg
        if tool in TOOLS:
            obs = TOOLS[tool](arg)
            messages.append({"role": "user", "content": f"Observation: {obs}"})
        else:
            messages.append({"role": "user", "content": "Observation: unknown tool"})
    return "max steps exceeded"

The stop=["Observation:"] trick makes the model yield before writing the observation itself; your harness supplies it. This halves wasted tokens.

Productionizing ReAct loops

The skeleton above breaks under load in three ways:

  1. Context inflation. Every observation piles into the context. Long investigations hit context limits. Use summarization or sliding windows.
  2. Provider flakiness. A single rate limit throws the whole agent. When you scale this pattern across providers, a single OpenAI-compatible endpoint that covers 240+ models with automatic fallback on rate limits—like n4n.ai—keeps the agent resilient without code changes. It also forwards provider cache-control hints, so repeated Thought prefixes stay cheap.
  3. Cost metering. Per-token usage metering is non-negotiable for multi-step agents; you must attribute spend per trace to detect runaway loops.

Set hard max_steps. A ReAct agent that loops 20 times is usually failing, not thinking.

Common misconceptions about ReAct prompting

“It’s just few-shot prompting.” Few-shot examples help, but ReAct is a control protocol. The parser, the observation injection, and the termination condition are engineering, not prompt text.

“The model knows when to stop.” Left alone, many models emit finish prematurely or never. You need an explicit stop tool and a step cap.

“Actions must be search or APIs.” Actions can be anything: a database transaction, a message send, a unit test run. The pattern is tool-agnostic.

“ReAct replaces planning.” ReAct is reactive, not deliberative. For multi-hour tasks, pair it with a planner that sets sub-goals; let ReAct execute each sub-goal.

“It makes the model trustworthy.” The model can still emit a wrong Thought and then cherry-pick observations. You still need validation on critical actions.

When ReAct is the wrong tool

If your task is a single classification, a fixed retrieval-augmented generation call, or a latency-sensitive path under 100 ms, ReAct’s round-trips will hurt. Use it when the sequence of operations is not known at write time.

For deterministic workflows, a plain function chain is faster and cheaper. ReAct earns its overhead only when the branching factor is high and the inputs are unstructured.

Debugging a stuck loop

When the agent repeats the same Action, print the last three Thoughts. Usually the Observation was ambiguous or the model ignored it. Fix by tightening the tool output format or adding a rule: “If Observation contains ‘no results’, try a different query.”

If the model writes an Observation itself despite the stop token, your stop string mismatched. Use explicit tokenization or a structured output mode.

ReAct prompting is not magic. It is a disciplined way to bolt a language model onto the real world, one observed step at a time.

Tagsreact-promptingprompt-engineeringai-agentsreasoning

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All prompt engineering for agentic systems posts →