ReAct From Scratch: Make Your LLM Reason and Act in ~40 Lines (No LangChain)

ReAct (Reason + Act) is already baked into the frontier models you use every day — it started as a 2022 paper and became the backbone of modern AI agents. Here's what it actually is: the Thought → Action → Observation loop, a real trace, and a ~40-line build from scratch in Python, TypeScript, and Java — without the model's native tool-calling — so every moving part is visible.

Banner

Prefer to watch? ▶ ReAct in 60 seconds ▶ Code it from scratch (Ep2, Python) ✈ Telegram

Here's something most engineers don't realize: ReAct is already running inside the frontier models you use every day. When Gemini Flash, GPT, or Claude thinks, calls a tool, reads the result, and keeps going — that's ReAct. It started as a 2022 research paper (ReAct: Synergizing Reasoning and Acting in Language Models) and quietly became the backbone of nearly every modern agentic model.

So why build it from scratch? Because the built-in version is a black box. The fastest way to actually understand what an "agent" is doing is to wire the loop yourself — without the model's native tool-calling — so every moving part is visible. That's what this post does.

ReAct — short for Reason + Act — is the idea, and it's almost embarrassingly simple: instead of forcing the model to blurt one final answer, you let it think out loud and take actions in a loop, grounding each step in a real result before it continues.


The loop: Thought → Action → Observation

A ReAct agent doesn't answer. It iterates:

  1. Thought — reason about what it needs to do next.
  2. Action — call a tool (search, a calculator, an API, your own function).
  3. Observation — read the real result that tool returned.
  4. …then loop back to Thought with that new fact in hand — until it has enough to answer.

Here's a real trace for the Zurich question:

Thought: I don't know today's weather. I should look it up.
Action: get_weather("Zurich")
Observation: 18°C, raining
Thought: It's raining and mild. A jacket makes sense.
Answer: Wear a light waterproof jacket — it's 18°C and raining in Zurich.

Notice what changed. The final answer is no longer a guess pulled from training data — it's grounded in an observation the model actually fetched. And because each step is explicit, the model can self-correct: if get_weather had returned an error, the next Thought would react to that instead of barreling ahead.


Why not just chain-of-thought? Or just tool-calling?

ReAct sits exactly between the two things people usually reach for, and it fixes what each one is missing.

  • Chain-of-thought makes the model reason ("let's think step by step") — but it still can't do anything. It reasons beautifully over stale, made-up facts. Great thinking, no hands.
  • Raw tool-calling lets the model act — but a single tool call doesn't reason between calls. It fires one function and answers. It can't chain "look this up → now that I know X, look up Y."

ReAct interleaves both: reason, act, observe, reason again. That interleaving is the whole trick. It's why an agent can break a fuzzy request into steps, gather facts as it goes, and recover when a step fails.


ReAct in ~40 lines (no framework)

To see the machinery clearly, we hand-roll the loop — no LangChain AgentExecutor, no graph library, and crucially no native tool-calling API. We parse the model's plain-text Action: ourselves so nothing is hidden. The entire loop is a while, a prompt, and a tiny tool registry. Here it is end to end — in Python, TypeScript, or Java; click the tab for your stack:

import re
from openai import OpenAI   # or any chat model client

client = OpenAI()

# 1. Your tools — just normal Python functions.
def get_weather(city: str) -> str:
    fake = {"Zurich": "18°C, raining", "Cairo": "34°C, sunny"}
    return fake.get(city, "unknown")

TOOLS = {"get_weather": get_weather}

# 2. The system prompt teaches the model the ReAct format.
SYSTEM = """You solve tasks in a loop. Each turn output exactly one of:
Thought: <your reasoning>
Action: <tool_name>(<arg>)
...or, when you can finally answer:
Answer: <final answer>

Available tools: get_weather(city)"""

def react(question: str, max_steps: int = 5) -> str:
    messages = [{"role": "system", "content": SYSTEM},
                {"role": "user", "content": question}]

    for _ in range(max_steps):
        reply = client.chat.completions.create(
            model="gpt-4o-mini", messages=messages
        ).choices[0].message.content
        messages.append({"role": "assistant", "content": reply})

        if "Answer:" in reply:                      # the model is done
            return reply.split("Answer:", 1)[1].strip()

        m = re.search(r'Action:\s*(\w+)\((.*?)\)', reply)   # parse the action
        if not m:
            continue
        name, arg = m.group(1), m.group(2).strip().strip('"\'')
        observation = TOOLS[name](arg)              # run the real tool
        messages.append({"role": "user",
                         "content": f"Observation: {observation}"})

    return "Stopped: no answer within step budget."

print(react("What should I wear in Zurich today?"))

The Java sample keeps callModel(messages) as the one thing you wire to your provider — a plain HTTPS POST to the chat-completions endpoint that returns the assistant's text. The ReAct loop itself is identical across all three languages.

That's the whole pattern. The model emits a Thought and an Action; your code runs the tool and feeds the Observation back; the loop repeats until the model emits an Answer. The max_steps guard is the one thing people forget — without it, a confused agent loops forever.

Everything fancier — multiple tools, retries, memory, parallel actions, JSON-structured actions instead of regex parsing — is a refinement of these forty lines, not a replacement for them.


Where to go next

If you want to watch this get built line by line — including swapping the brittle regex for clean structured tool-calls — the full "ReAct From Scratch" coding episode walks through it in Python. The 60-second version is here if you just want the mental model.

In production you usually won't hand-write this — the frontier models already do ReAct natively through their built-in tool-calling, and that's what you'll reach for. But that's exactly why building it once, by hand, is worth it: an "agent" stops being a magic product and becomes something you can see. It's just a model allowed to reason, act, and look at what actually happened — in a loop — instead of guessing once. That loop is ReAct, it's been around since 2022, and you can hold the entire thing in your head.

ReAct From Scratch: Make Your LLM Reason and Act in ~40 Lines (No LangChain) | Vahid Aghajani