Most agent failures trace back to a fragile control loop, not model weakness. This ReAct prompting guide lays out a concrete path to wire reasoning and action together so the model can observe, decide, and retry without human hand-holding.
What ReAct actually buys you
ReAct interleaves natural language reasoning with external actions. The model emits a thought, picks an action, receives an observation, and repeats. That loop turns a frozen completion into a stateful debugger.
Pure chain-of-thought reasons in a vacuum. Plain tool-calling dumps arguments but hides the why. ReAct keeps the why inline, which makes mid-flight corrections possible when a tool returns something unexpected.
The cost is latency and complexity. Every step is a model call plus a tool round trip. Treat ReAct as a tool for tasks where the answer depends on retrieved state, not as a default wrapper for every prompt.
The minimal prompt contract
You need a system prompt that forces a predictable structure. For text-based ReAct, demand three tags:
Thought: <reasoning about current state>
Action: <tool_name>(<args>)
Observation: <result injected by system>
A stricter variant uses JSON or native tool schemas. If your client supports function calls, prefer them—parsing gets trivial. But the underlying loop is identical.
SYSTEM = """You are a ReAct agent. Use this cycle:
Thought: describe plan
Action: tool_name with JSON args
Observation: (provided)
Answer when done with Final: <result>"""
This ReAct prompting guide assumes you control the system message and can append observations server-side. Do not let the model generate the Observation line; that defeats the purpose.
Step 1: Define a tight action space
List every tool with a one-line purpose and a strict input schema. Five tools is manageable; twenty is a recall tax. Each extra action dilutes the model’s precision.
{
"search": {"type": "object", "properties": {"query": {"type": "string"}}},
"calc": {"type": "object", "properties": {"expr": {"type": "string"}}}
}
Expose only what the task needs. Wrap internal APIs so the agent sees stable names, not your microservice sprawl. If a tool needs ten parameters, the model will hallucinate half of them—flatten or default aggressively.
Step 2: Implement the loop, not the monolith
Write a small driver. It calls the model, scans for an action, executes, and feeds the observation back. Keep max steps explicit.
def run_agent(messages, tools, max_steps=8):
for step in range(max_steps):
resp = chat_completion(model="gpt-4o-mini", messages=messages, tools=tools)
msg = resp.choices[0].message
if msg.tool_calls:
for call in msg.tool_calls:
obs = execute_tool(call.name, call.arguments)
messages.append({"role": "tool", "content": obs, "tool_call_id": call.id})
else:
return msg.content
return "MAX_STEPS_EXCEEDED"
The loop described in this ReAct prompting guide is deliberately minimal. Real code needs timeout guards around execute_tool and schema validation on call.arguments.
If you’re hitting provider rate limits in production, an OpenAI-compatible gateway such as n4n.ai can automatically fall back when a provider is degraded while honoring your routing directives—same code, fewer 429s.
Step 3: Parse outputs without mercy
Text ReAct needs a regex or a line scanner. Never trust the model to close tags perfectly.
import re
def extract_action(text):
m = re.search(r"Action:\s*(\w+)\((.*)\)", text)
if m:
return m.group(1), m.group(2)
return None, None
With native tool calls, the SDK gives you structured args. Validate them against the schema before execution; a missing field should become an Observation error, not a crash. Pydantic or jsonschema takes ten lines and saves hours.
Step 4: Handle tool failure as data
Tools fail. Network blips, empty results, malformed input. Return that as an observation:
{"observation": "ERROR: search returned 503 after 2s"}
The model can then rethink. Tradeoff: too verbose an error floods context; too terse hides the fix. Cap error strings at 200 characters and include the status code.
Do not auto-retry inside the tool silently. Let the agent decide. If you retry three times and still fail, the loop wastes steps. Expose the failure and let ReAct adapt. You can still retry at the tool layer for transient blips, but surface the final outcome.
Step 5: Bound the loop
Set a hard step limit and a token budget. Unbounded ReAct will spiral on ambiguous tasks. When the limit hits, return the best partial answer and log the transcript.
Track token usage per call. If you’re metering per-token on a gateway, this is just bookkeeping. A 8-step loop with 2k context each is cheap; a 50-step loop with 8k context is not. Kill switches matter: a max_tokens on the model response prevents a single verbose thought from eating the budget.
Common pitfalls
Thought verbosity
Models love to write novels. Truncate thoughts to 500 tokens before the next step or you’ll blow context on navel-gazing.
Action space explosion
Adding “just one more” tool compounds. Cluster similar capabilities; use a router tool instead of ten near-duplicates.
Ignoring observation truncation
Long API responses must be summarized before injection. Raw HTML dumps kill the loop. Strip to the fields the agent asked for.
Assuming the model stops
It won’t always emit Final:. Check for task completion heuristically: no tool call + answer shape, or a sentinel. Add a forced “if you have the answer, output Final:” reminder every three steps.
Leaking secrets into thoughts
The transcript is your debug log. If a tool returns a token, redact it before appending as observation.
A worked example
Task: “What’s the square root of the population of France divided by 1000?”
messages = [{"role":"system","content":SYSTEM},
{"role":"user","content":"sqrt(pop France / 1000)"}]
tools = [SEARCH, CALC]
out = run_agent(messages, tools)
Expected trace:
Thought: Need France population.
Action: search({"query":"France population 2024"})
Observation: 67.4 million
Thought: Compute sqrt(67400000/1000)=sqrt(67400)
Action: calc({"expr":"sqrt(67400)"})
Observation: 259.615
Final: ~259.6
Real runs drift. The model might search “France population” then misread units. Your observation must state “67.4 million” not “67.4” to avoid a 1000x error. This ReAct prompting guide recommends unit-normalized observations wherever possible.
Production tradeoffs
ReAct is slower than a single call. Each step is a round trip. For latency-sensitive paths, cache observations and pre-warm tools.
It is also more observable. You get a reasoning transcript that beats a black-box JSON blob for debugging. Store the full message list; replaying a failure is how you tune the prompt.
If you forward provider cache-control hints, prefix the system prompt and stable tool schemas so repeated loops hit cache. That cuts cost on long tasks where the instruction string is fixed.
When not to use ReAct
For single-shot classification or extraction, skip it. The loop adds latency and failure surface. Use ReAct when the problem requires external state or multi-step correction.
If your tools are deterministic and fast, a plan-then-execute pattern may beat interleaved reasoning. ReAct shines when observations change the plan—a search that returns zero hits should pivot the agent, not stall it.
Evaluation before launch
Log 50 real tasks. Measure step count, token cost, and success rate. If 80% of successes happen in two steps, your action space is likely too timid or too broad. If most failures are MAX_STEPS_EXCEEDED, tighten the thought format or add a planner.
ReAct is not magic. It is a discipline: strict parsing, honest observations, and a hard stop. Build the loop first, then earn the fancy tools.