Learnixo
Back to blog
AI Systemsintermediate

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.

Asma Hafeez KhanMay 15, 20269 min read
reactreasoningtool-useopenaipythonagentic-ai
Share:𝕏

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:

  1. Better decisions — Explicit reasoning helps the model avoid common mistakes
  2. 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

Python
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:

Python
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) or finish_reason == "stop" (function-calling) signals the end of the loop
  • Always set a max_iterations limit 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.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.