Slow billing doesn't just create friction — it delays cash and compounds across every deal you close.Slow billing doesn't just create friction — it delays cash and compounds across every deal you close.
Most SaaS finance teams assume the gap between contract signature and first invoice is a minor inconvenience. The Tabs Billing Lag Calculator puts a dollar figure on it.
Answer 5 quick questions about your contracts, invoicing process, error rate, and DSO, and the calculator benchmarks your billing lag against top SaaS companies — then shows you exactly what it's costing you.
Two minutes. One number that's hard to ignore.
Calculate your billing lag and see where you stand.
TL;DR: ReAct (Reasoning + Acting) is the pattern where an LLM alternates between thinking and using tools in a loop. It outperforms pure chain of thought because it grounds reasoning in real world data. This article builds a complete ReAct agent from scratch in Python with no frameworks, no LangChain, just raw API calls and a while loop.
Welcome to Grind Engineer, your guide to becoming a better software engineer! No fluff. Pure engineering insights.
Added Job Opening in the end of the article!
Every AI agent you've used, Claude Code, ChatGPT with plugins, Cursor, follows the same core loop under the hood. The model thinks about what to do, takes an action, looks at the result, and repeats. That loop has a name. It's called ReAct. And once you understand it, you can build your own agent from scratch in about 100 lines of Python.

What Is ReAct?
ReAct stands for Reasoning + Acting. It comes from a 2022 paper by Shunyu Yao et al. at Princeton, later published at ICLR 2023. The core idea is dead simple: instead of asking an LLM to think through a problem in one shot (chain of thought), you let it alternate between thinking and doing.
The loop has three steps:
Step | What Happens | Example |
|---|---|---|
Thought | The model reasons about what to do next | "I need to find the population of France" |
Action | The model picks a tool and provides input |
|
Observation | The system executes the tool and returns the result | "The population of France is 68.4 million" |
The model receives the full history of thoughts, actions, and observations at each turn. It keeps looping until it decides it has enough information to give a final answer, at which point it calls a special finish action.
That's the entire pattern. Every major agent framework, LangChain, CrewAI, AutoGen, implements this under the hood. When you strip away the abstractions, it's just a while loop.
Why ReAct Beats Pure Chain of Thought
Plain chain of thought (CoT) asks the model to reason step by step, but entirely inside its own head. The problem? LLMs make stuff up. They hallucinate numbers, invent facts, and confidently give you wrong answers. They're reasoning in a vacuum.
ReAct fixes this by grounding reasoning in external reality. At each step, the model can reach out, check something, and correct itself.
Approach | Strengths | Weaknesses |
|---|---|---|
Chain of Thought only | No external dependencies, fast | Hallucinations, no access to live data |
Tool use only (no reasoning) | Gets real data | No planning, picks wrong tools, no self correction |
ReAct (reasoning + tools) | Plans before acting, self corrects with real data | More LLM calls, uses more tokens |
The ReAct paper showed this on benchmarks: interleaving reasoning with actions outperformed both pure reasoning and pure tool use on question answering and decision making tasks. But you don't need a paper to see why. If I asked you to find the cheapest flight from Delhi to London, you wouldn't sit there and guess prices. You'd think about which sites to check, go check them, look at the results, and adjust your search. That's ReAct.
Building a ReAct Agent From Scratch
Let's build one. No frameworks. Just Python and the OpenAI SDK (you could swap in Anthropic or any provider with tool calling support).
Step 1: Define your tools.
import json
import math
import openai
# Tool implementations
def calculator(expression: str) -> str:
"""Evaluates a math expression safely."""
try:
allowed = {
"abs": abs, "round": round,
"min": min, "max": max,
"sqrt": math.sqrt, "pow": pow
}
result = eval(expression, {"__builtins__": {}}, allowed)
return str(result)
except Exception as e:
return f"Error: {e}"
def search(query: str) -> str:
"""Simulates a web search. Replace with real API."""
# In production, call Google/Bing/Tavily API here
knowledge = {
"population of france": "The population of France in 2024 is approximately 68.4 million.",
"population of germany": "The population of Germany in 2024 is approximately 84.5 million.",
"eiffel tower height": "The Eiffel Tower is 330 meters tall including antennas.",
}
for key, value in knowledge.items():
if key in query.lower():
return value
return f"No results found for: {query}"
# Registry mapping tool names to functions
TOOLS = {
"calculator": calculator,
"search": search,
"finish": None # special action to end the loop
}
Step 2: Write the system prompt that teaches the model the format.
This is the most important part. The system prompt IS the agent's brain. It tells the model exactly how to structure its output so your code can parse it.
SYSTEM_PROMPT = """You are a helpful assistant that answers questions
by reasoning step by step and using tools when needed.
You have access to these tools:
- search(query): Search for factual information
- calculator(expression): Evaluate math expressions
- finish(answer): Return the final answer to the user
For each step, you MUST respond in EXACTLY this format:
Thought: [your reasoning about what to do next]
Action: [tool_name]
Action Input: [input to the tool]
When you have enough information to answer, use:
Thought: [your final reasoning]
Action: finish
Action Input: [your complete final answer]
RULES:
- Always start with a Thought before taking an Action
- Use one tool per step
- Wait for the Observation before your next Thought
- Do NOT make up facts. Use the search tool to verify information
- If a tool returns an error, try a different approach
"""
Step 3: Build the loop.
def run_react_agent(question: str, max_iterations: int = 10) -> str:
client = openai.OpenAI()
# Start with the user's question
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": question}
]
for i in range(max_iterations):
# Ask the model to think and pick an action
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0
)
assistant_message = response.choices[0].message.content
messages.append({"role": "assistant", "content": assistant_message})
# Parse the action from the model's output
action, action_input = parse_action(assistant_message)
if action is None:
# Model didn't follow the format, nudge it
messages.append({
"role": "user",
"content": "Please respond using the Thought/Action/Action Input format."
})
continue
# Check if the model wants to finish
if action == "finish":
return action_input
# Execute the tool
if action not in TOOLS:
observation = f"Error: Unknown tool '{action}'. Available tools: {list(TOOLS.keys())}"
else:
observation = TOOLS[action](action_input)
# Append the observation and loop back
messages.append({
"role": "user",
"content": f"Observation: {observation}"
})
print(f"--- Step {i + 1} ---")
print(f"Thought + Action: {assistant_message}")
print(f"Observation: {observation}\n")
return "Agent stopped: reached maximum iterations without finishing."
def parse_action(text: str):
"""Extract action and action input from model output."""
action = None
action_input = None
for line in text.strip().split("\n"):
if line.startswith("Action:") and "Action Input" not in line:
action = line.split("Action:", 1)[1].strip().lower()
elif line.startswith("Action Input:"):
action_input = line.split("Action Input:", 1)[1].strip()
return action, action_input
Step 4: Run it.
if __name__ == "__main__":
answer = run_react_agent(
"What is the combined population of France and Germany? "
"Give me the exact sum."
)
print(f"\nFinal Answer: {answer}")
When you run this, here's what happens inside the loop:
--- Step 1 ---
Thought: I need to find the population of France first.
Action: search
Action Input: population of France
Observation: The population of France in 2024 is approximately 68.4 million.
--- Step 2 ---
Thought: Now I need the population of Germany.
Action: search
Action Input: population of Germany
Observation: The population of Germany in 2024 is approximately 84.5 million.
--- Step 3 ---
Thought: I have both numbers. Let me add them.
Action: calculator
Action Input: 68.4 + 84.5
Observation: 152.9
--- Step 4 ---
Thought: I now have the combined population.
Action: finish
Action Input: The combined population of France and Germany is approximately 152.9 million.
Final Answer: The combined population of France and Germany is approximately 152.9 million.
4 steps, 3 tool calls, and a correct answer grounded in actual data. A pure chain of thought approach would have guessed the numbers and probably gotten them wrong.
Failure Modes (And How to Handle Them)
ReAct isn't bulletproof. Here are the ways it breaks and what to do about each one:
Failure Mode | What Happens | Fix |
|---|---|---|
Infinite loops | Model keeps searching without making progress | Set |
Hallucinated tool names | Model calls | Return an error message listing valid tools |
Wrong arguments | Calculator gets | Add examples in the system prompt |
Context window overflow | History gets too long after many steps | Summarize old steps or use sliding window |
Stuck in a rut | Model repeats the same failed action | Track action history, force different approach after 2 repeated failures |
The max_iterations guard is non negotiable. Without it, one bad prompt can burn through your entire API budget in a loop. In production, also add a timeout and a token budget cap.
💡 Key Insight: The system prompt is your agent's entire personality and capability. 90% of agent debugging is prompt debugging. If your agent picks the wrong tool, don't change the code. Change the system prompt. Add examples. Be more explicit about when to use each tool.
How Production Agents Use ReAct
Claude Code is basically a ReAct agent. It receives your request, thinks about what to do (read a file? run a command? edit code?), takes the action, observes the result, and loops. The "tools" are things like file read, file write, bash execution, and search.
ChatGPT with plugins followed the same pattern. The model would reason about which plugin to call, call it, read the response, and decide whether to call another or give the final answer.
The difference between a toy ReAct agent and a production one comes down to:
Toy Agent | Production Agent |
|---|---|
2 to 3 tools | 10 to 50 tools with clear descriptions |
Text parsing for actions | Structured output or native tool calling API |
Simple while loop | State machine with error recovery |
No memory | Conversation history + long term memory |
No guardrails | Rate limits, cost caps, content filtering |
Production agents also use the model's native tool calling API instead of text parsing. Our parse_action function works, but it's fragile. OpenAI, Anthropic, and Google all support structured tool calling where the model returns a JSON object specifying the tool and arguments. That's way more reliable than regex parsing model output.
Where ReAct Fits in Your Agentic AI Journey
If you've been following this series, here's how ReAct connects to everything else:
ReAct is the foundation. Once you understand this pattern, everything else in agentic AI is a variation on top of it. Multi agent systems? Multiple ReAct loops coordinating. Memory? Storing and retrieving past ReAct trajectories. Planning? A more structured version of the "Thought" step.
Build this from scratch once. Run it. Break it. Fix it. You'll understand 90% of what every agent framework is doing under the hood.
Sources:
Job Openings
Graduate Software Engineer @Hewlett Packard Enterprise: Apply Here
Software Engineer Backend @Modulr: Apply Here
Application Engineer I @Bread Financial: Apply Here
Software Engineer I @Honeywell: Apply Here
Full Stack Developer Backend @Intervue.io: Apply Here
See you in the next one!
Scortier, Signing Off!



