---
title: "ReAct From Scratch: Make Your LLM Reason and Act in ~40 Lines (No LangChain)"
description: "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."
keywords: "ReAct, ReAct agent, reason and act, AI agent, LLM agent, agent loop, tool calling, chain of thought, build agent from scratch, thought action observation, agentic AI, ReAct Python TypeScript Java, no LangChain"
created_at: "2026-06-26T11:00:00"
post_type: "anonym_post"
content_type: "technical_article"
---

![Banner](./banner.webp)

<div style="display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; padding: 1rem 1.25rem; margin: 1.5rem 0 2rem; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border: 1px solid #e2e8f0; border-radius: 12px;">
  <span style="font-size: 0.95rem; font-weight: 600; color: #475569; margin-right: 0.25rem;">Prefer to watch?</span>
  <a href="https://youtube.com/shorts/OU7S-ThO7vM" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.55rem 1rem; border-radius: 8px; background: #ff0000; color: #ffffff; font-size: 0.875rem; font-weight: 600; text-decoration: none;">▶ ReAct in 60 seconds</a>
  <a href="https://youtu.be/NR-XjoiAKDM" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.55rem 1rem; border-radius: 8px; background: #ff0000; color: #ffffff; font-size: 0.875rem; font-weight: 600; text-decoration: none;">▶ Code it from scratch (Ep2, Python)</a>
  <a href="https://t.me/SoftwareEngineerBlog" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.55rem 1rem; border-radius: 8px; background: #229ed9; color: #ffffff; font-size: 0.875rem; font-weight: 600; text-decoration: none;">✈ Telegram</a>
</div>

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](https://arxiv.org/abs/2210.03629) (*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:

```text
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:

```codetabs
### python | Python
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?"))
### typescript | TypeScript
import OpenAI from "openai";   // or any chat model client

const client = new OpenAI();

// 1. Your tools — just normal functions behind a registry.
function getWeather(city: string): string {
  const fake: Record<string, string> = { Zurich: "18°C, raining", Cairo: "34°C, sunny" };
  return fake[city] ?? "unknown";
}

const TOOLS: Record<string, (arg: string) => string> = { get_weather: getWeather };

// 2. The system prompt teaches the model the ReAct format.
const 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)`;

async function react(question: string, maxSteps = 5): Promise<string> {
  const messages: any[] = [
    { role: "system", content: SYSTEM },
    { role: "user", content: question },
  ];

  for (let i = 0; i < maxSteps; i++) {
    const res = await client.chat.completions.create({ model: "gpt-4o-mini", messages });
    const reply = res.choices[0].message.content ?? "";
    messages.push({ role: "assistant", content: reply });

    if (reply.includes("Answer:")) return reply.split("Answer:")[1].trim();  // done

    const m = reply.match(/Action:\s*(\w+)\((.*?)\)/);                       // parse the action
    if (!m) continue;
    const name = m[1], arg = m[2].trim().replace(/^["']|["']$/g, "");
    const observation = TOOLS[name](arg);                                    // run the real tool
    messages.push({ role: "user", content: `Observation: ${observation}` });
  }
  return "Stopped: no answer within step budget.";
}

console.log(await react("What should I wear in Zurich today?"));
### java | Java
import java.util.*;
import java.util.function.Function;
import java.util.regex.*;

public class ReActAgent {

    // 1. Your tools — plain functions behind a registry.
    static String getWeather(String city) {
        return Map.of("Zurich", "18°C, raining", "Cairo", "34°C, sunny")
                  .getOrDefault(city, "unknown");
    }

    static final Map<String, Function<String, String>> TOOLS =
        Map.of("get_weather", ReActAgent::getWeather);

    // 2. The system prompt teaches the model the ReAct format.
    static final String 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)""";

    static final Pattern ACTION = Pattern.compile("Action:\\s*(\\w+)\\((.*?)\\)");

    static String react(String question, int maxSteps) {
        List<Map<String, String>> messages = new ArrayList<>();
        messages.add(Map.of("role", "system", "content", SYSTEM));
        messages.add(Map.of("role", "user", "content", question));

        for (int i = 0; i < maxSteps; i++) {
            String reply = callModel(messages);          // your LLM HTTP call -> assistant text
            messages.add(Map.of("role", "assistant", "content", reply));

            if (reply.contains("Answer:"))               // the model is done
                return reply.split("Answer:", 2)[1].trim();

            Matcher m = ACTION.matcher(reply);           // parse the action
            if (!m.find()) continue;
            String name = m.group(1);
            String arg = m.group(2).trim().replaceAll("^[\"']|[\"']$", "");
            String observation = TOOLS.get(name).apply(arg);   // run the real tool
            messages.add(Map.of("role", "user", "content", "Observation: " + observation));
        }
        return "Stopped: no answer within step budget.";
    }

    public static void main(String[] args) {
        System.out.println(react("What should I wear in Zurich today?", 5));
    }
}
```

> 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](https://youtu.be/NR-XjoiAKDM)** walks through it in Python. The [60-second version is here](https://youtube.com/shorts/OU7S-ThO7vM) 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.
