The ReAct Pattern
Implement the Reasoning + Acting pattern from scratch using the raw OpenAI API — no frameworks — with a drug information agent as a worked example.
The ReAct Pattern
ReAct stands for Reasoning + Acting. It is the foundational pattern for agentic AI — a simple but powerful loop where the LLM alternates between reasoning in plain text and taking concrete actions. Published by Yao et al. in 2022, it became the basis for LangChain's agent executor, AutoGPT, and most production agent systems in use today.
This lesson implements ReAct from scratch using only the OpenAI API. No frameworks. You will understand exactly what is happening at each step.
What Makes ReAct Different
Before ReAct, LLM agents either:
- Generated all reasoning and actions in one shot (prone to errors, no correction)
- Took actions without explicit reasoning (hard to debug)
ReAct interleaves reasoning and action. The model thinks out loud before every action, which serves two purposes:
- Better decisions — Explicit reasoning helps the model avoid common mistakes
- Interpretability — You can read the trace and understand why the model did what it did
The canonical ReAct format looks like this in the prompt:
Thought: I need to find the recommended dosage for ibuprofen.
Action: search[ibuprofen adult dosage]
Observation: Adults may take 200-400mg every 4-6 hours. Max 1200mg per day OTC.
Thought: I have the dosage. The user also asked about interactions with alcohol.
Action: search[ibuprofen alcohol interaction]
Observation: Alcohol increases the risk of stomach bleeding when taken with ibuprofen.
Thought: I now have all the information needed to answer the question.
Final Answer: Ibuprofen dosage for adults is 200-400mg every 4-6 hours...Step-by-Step Breakdown
Step 1: Thought — The model writes a short reasoning trace. It identifies what it knows, what it still needs, and what action to take next.
Step 2: Action — The model selects a tool and provides arguments in a structured format. In the text-based ReAct format, this is a plain text string like search[query]. In modern implementations using OpenAI function calling, this is a structured JSON object.
Step 3: Observation — Your code executes the tool and appends the result to the prompt as an Observation: block.
Step 4: Repeat — The model reads the observation and writes another Thought. This continues until the model writes Final Answer: instead of a new Action.
Two Approaches to ReAct
Text-based ReAct (original paper style) — The model generates Thought/Action/Observation as plain text. Your code parses the text to extract action names and arguments. Fragile but simple.
Function-calling ReAct (modern style) — You use OpenAI's tool/function calling API. The model outputs structured JSON for tool calls, which is more reliable and easier to parse.
This lesson implements both so you understand the tradeoffs.
Text-Based ReAct Implementation
import re
import openai
client = openai.OpenAI()
# Simulated tools
def search(query: str) -> str:
"""Simulated medical information search."""
knowledge_base = {
"ibuprofen adult dosage": (
"Adults: 200-400mg every 4-6 hours as needed. "
"Maximum 1200mg per day for OTC use."
),
"ibuprofen alcohol interaction": (
"Alcohol increases the risk of gastrointestinal bleeding "
"when combined with ibuprofen. Avoid or limit alcohol."
),
"ibuprofen pregnancy": (
"Ibuprofen is generally avoided during pregnancy, especially "
"in the third trimester, due to risk of premature closure of ductus arteriosus."
),
"ibuprofen kidney risk": (
"NSAIDs including ibuprofen can reduce kidney blood flow. "
"Use with caution in patients with existing kidney disease."
),
}
query_lower = query.lower()
for key, value in knowledge_base.items():
if all(word in query_lower for word in key.split()):
return value
return f"No specific information found for: {query}"
AVAILABLE_TOOLS = {"search": search}
# Build the ReAct system prompt
REACT_SYSTEM_PROMPT = """You are a medical information assistant. You answer questions
about medications by using the search tool to look up accurate information.
You have access to the following tools:
- search[query]: Search for medical information about a drug or condition
Use the following format EXACTLY:
Thought: [your reasoning about what to do next]
Action: search[your search query here]
Observation: [the result will be inserted here by the system]
... (repeat Thought/Action/Observation as needed)
Thought: I now have enough information to answer.
Final Answer: [your complete answer to the user's question]
IMPORTANT:
- Always start with a Thought
- Always end with Final Answer
- Do not make up information — only use what you observe from the search tool
"""
def parse_action(text: str):
"""Extract action name and argument from text like 'search[query]'."""
match = re.search(r'Action:\s*(\w+)\[(.+?)\]', text, re.DOTALL)
if match:
return match.group(1).strip(), match.group(2).strip()
return None, None
def parse_final_answer(text: str):
"""Extract the Final Answer if present."""
match = re.search(r'Final Answer:\s*(.+)', text, re.DOTALL)
if match:
return match.group(1).strip()
return None
def run_react_text_based(question: str, max_iterations: int = 8) -> str:
"""
Run a text-based ReAct agent loop.
Parses the model's text output to extract actions.
"""
# Build initial prompt
prompt = f"{REACT_SYSTEM_PROMPT}\n\nQuestion: {question}\n\n"
print(f"Question: {question}\n")
for iteration in range(max_iterations):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
stop=["Observation:"], # Stop before observation — we will fill that in
max_tokens=500,
temperature=0,
)
generated = response.choices[0].message.content
print(f"Model output:\n{generated}")
# Check for final answer
final = parse_final_answer(generated)
if final:
return final
# Parse the action
action_name, action_arg = parse_action(generated)
if not action_name:
# Model did not follow the format — try to extract any text as answer
return generated
# Execute the tool
if action_name in AVAILABLE_TOOLS:
observation = AVAILABLE_TOOLS[action_name](action_arg)
else:
observation = f"Error: unknown tool '{action_name}'"
print(f"Observation: {observation}\n")
# Append the generated text and observation to the prompt
prompt += generated + f"\nObservation: {observation}\n\n"
return "Max iterations reached."
if __name__ == "__main__":
result = run_react_text_based(
"What is the adult dosage for ibuprofen, "
"and is it safe to take with alcohol?"
)
print(f"\n=== FINAL ANSWER ===\n{result}")Function-Calling ReAct Implementation
The text-based approach is brittle — parsing regex from LLM output fails when the model deviates slightly from the format. The modern approach uses OpenAI's structured tool calling:
import json
import openai
client = openai.OpenAI()
# Same search function as above
def search(query: str) -> str:
knowledge_base = {
"paracetamol dosage adult": (
"Adults: 500mg-1000mg every 4-6 hours. Maximum 4000mg (4g) per day. "
"Do not exceed 8 tablets of 500mg strength per day."
),
"paracetamol liver damage": (
"Paracetamol overdose is the leading cause of acute liver failure "
"in the UK. Toxic threshold is around 150mg/kg or 7.5g in adults."
),
"paracetamol alcohol warning": (
"Regular heavy alcohol use significantly increases the risk of "
"liver damage from paracetamol. Occasional alcohol is generally safe "
"with therapeutic doses."
),
}
query_lower = query.lower()
for key, value in knowledge_base.items():
if any(word in query_lower for word in key.split()):
return value
return f"No results found for: {query}"
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "search",
"description": (
"Search a medical knowledge base for drug information including "
"dosage, interactions, contraindications, and warnings."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query, e.g. 'paracetamol adult dosage'",
}
},
"required": ["query"],
},
},
}
]
def run_react_function_calling(question: str, max_iterations: int = 8) -> str:
"""
ReAct agent using OpenAI function calling.
More reliable than text-based parsing.
"""
messages = [
{
"role": "system",
"content": (
"You are a medical information assistant. Use the search tool "
"to look up accurate drug information before answering. "
"Always search for all relevant aspects of the question before "
"providing your final answer. Never guess — only report what you find."
),
},
{"role": "user", "content": question},
]
for iteration in range(max_iterations):
print(f"\n--- Iteration {iteration + 1} ---")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=TOOL_SCHEMAS,
tool_choice="auto",
temperature=0,
)
msg = response.choices[0].message
finish_reason = response.choices[0].finish_reason
# No tool calls means we have a final answer
if finish_reason == "stop" or not msg.tool_calls:
print(f"Reasoning complete. Final answer generated.")
return msg.content
# Process tool calls
print(f"Tool calls: {[tc.function.name for tc in msg.tool_calls]}")
messages.append(msg)
for tool_call in msg.tool_calls:
args = json.loads(tool_call.function.arguments)
query = args.get("query", "")
print(f" Searching: '{query}'")
result = search(query)
print(f" Result: {result[:100]}...")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
return "Max iterations reached without completing the task."
if __name__ == "__main__":
answer = run_react_function_calling(
"What is the maximum safe dose of paracetamol for adults, "
"and what are the risks with alcohol?"
)
print(f"\n=== FINAL ANSWER ===\n{answer}")The FINAL ANSWER Token
In text-based ReAct, the string Final Answer: acts as a special stopping token. When the model generates this token, the loop ends and the text after it is returned as the result.
This is a prompt engineering trick: you define a special string that signals completion, and your loop watches for it. Some implementations also add it to the stop parameter of the API call so the model halts generation as soon as it starts writing Final Answer: — then your code appends the stop token and calls the model one final time without a stop sequence to get the answer.
In function-calling ReAct, the equivalent is finish_reason == "stop" — the model simply stops generating tool calls when it is done.
Common ReAct Failure Modes
Repetitive search loops — The model keeps searching for the same thing with slight variations, never reaching a conclusion. Fix: track previously searched queries and pass them back to the model.
Hallucinated observations — In text-based ReAct, if your stop sequence does not work correctly, the model may generate its own Observation: text instead of waiting for the real tool result. Fix: use function calling instead of text parsing.
Missing Final Answer — The model reaches max_iterations without writing Final Answer:. Fix: add a system message like "You must provide a Final Answer within N iterations" or force-stop and ask the model to summarize what it found.
Summary
- ReAct interleaves Thought, Action, and Observation in a loop
- Text-based ReAct parses the model's plain text output — simple but fragile
- Function-calling ReAct uses structured JSON tool calls — more reliable and production-ready
- The
Final Answer:token (text-based) orfinish_reason == "stop"(function-calling) signals the end of the loop - Always set a
max_iterationslimit to prevent infinite loops - Read the trace — explicit reasoning makes agent behavior auditable
Next: the Plan-and-Execute pattern, which separates planning from execution for better parallelism.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.