InfoWok
AI Agents from Scratch in Python · 03Beginner

AI Agent Loop in Python: Build a ReAct Agent From Scratch

Build an AI agent loop in Python from scratch: a framework-free ReAct agent that thinks, calls a tool, observes the result, and repeats until the job is done.

SK
Sukhveer Kaur
Published June 19, 2026
7 min read
Part 3 banner reading The Agent Loop From Scratch with a circular think-act-observe arrow motif, showing the ai agent loop in Python on a dark backgroundAI Agents from Scratch in Python · Part 03
THE AGENT LOOP
On this page +
🧰 New here? Set up your environment first · ~5 min
  1. Install Python 3.11+ — confirm with python3 --version.
  2. Create and activate a virtual environment: python3 -m venv .venv then source .venv/bin/activate (Windows: .venv\Scripts\activate). venv, pip & uv primer →
  3. Install the packages this tutorial lists: pip install -U pip <packages>.
  4. Put your LLM API key in a .env file and never commit it. API key + .env primer →

Full walkthrough → Environment Setup primer

Series: AI Agents from Scratch in Python This is Part 3. So far: Part 1 made your first LLM call, and Part 2 let the model call one of your functions. Here we wrap that single call in an AI agent loop so the model can take several steps on its own. If you can run the Part 2 code, you have everything you need.

The most-liked comment under almost every “AI agent” tutorial is the same five words: an agent is a loop. It shows up on dev.to, Hacker News, and half the engineering blogs, repeated again and again. They are right — and in this post you’ll write that AI agent loop yourself, in plain Python, with no framework.

By the end you’ll have a working AI agent loop that thinks, picks a tool, runs it, reads the result, and keeps going until it can answer.

Part 2 ran a tool exactly once, by hand. A real agent does it again and again, deciding for itself when to stop. That one change — a while loop around the call you already know — is the whole leap from chatbot to agent. Let’s build it.

🟢 Beginner⏱️ 20 minStack: Python 3.10+, openai SDK
Before you start
  • You can define a tool and run a single tool call — that’s Part 2
  • You can read a while loop, try/except, and for/else — the Part 0 primer covers control flow
  • An OpenAI API key
🎯 Key takeaways
  • An agent is a loop: the model thinks, calls a tool, reads the result, and repeats until it answers.
  • The line that makes it an agent is where the model — not your code — decides the next step.
  • Three guards keep it safe: a max-steps cap, a try/except around tools, and a forced final answer.

Recap: The Two Pieces You Already Have#

You are not starting from zero. The loop is just your Part 1 and Part 2 code, repeated.

From Part 1 you have one LLM call: send a list of messages, get a reply. From Part 2 you have one tool call: describe a function, let the model request it, run it, and hand the result back. An AI agent loop is what you get when you stop doing that once and start doing it in a cycle.

That cycle has a name. It is the ReAct pattern — short for Reasoning and Acting — introduced by Yao and colleagues in 2022 (the original paper is here). The model produces a Thought about what to do, takes an Action by calling a tool, sees the Observation that comes back, and repeats.

The classic version parsed all of that out of plain text. Today’s models expose the Action through native tool calling, so you get the same loop with far less glue code — which is exactly why we can build it in about thirty lines.

The AI Agent Loop, in About 30 Lines#

Here is the core of every agent. Read it once, then we will walk through the three moving parts.

python
from openai import OpenAI
import json
 
client = OpenAI()
 
def search(query: str) -> str:
    # Pretend lookup. Real code would call a search API.
    return "Mount Everest is 8,849 metres tall."
 
TOOLS = {"search": search}            # name -> real function
tools_schema = [{
    "type": "function",
    "function": {
        "name": "search",
        "description": "Look up a fact you do not already know.",
        "parameters": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
    },
}]
 
messages = [{"role": "user", "content": "How tall is Everest, in feet?"}]
 
for step in range(5):                 # max-steps guard (more on this below)
    response = client.chat.completions.create(
        model="gpt-5.4-mini", messages=messages, tools=tools_schema,
    )
    reply = response.choices[0].message
    messages.append(reply)            # remember what the model just said
 
    if not reply.tool_calls:          # no tool wanted -> the loop is done
        print(reply.content)
        break
 
    for call in reply.tool_calls:     # run every tool the model asked for
        fn = TOOLS[call.function.name]
        args = json.loads(call.function.arguments)
        result = fn(**args)
        messages.append({
            "role": "tool", "tool_call_id": call.id, "content": result,
        })

Run that and the model does something it could not do in Part 2: it reaches for search on its own, reads back “8,849 metres,” and then converts the answer to feet in its final reply — about 29,032 feet. You never told it to search and never told it to convert. It decided both.

The three moving parts are small. The for loop is the agent — it lets the model take more than one step. The if not reply.tool_calls check is the exit — when the model stops asking for tools, it has an answer. And appending every message (both the model’s reply and each tool result) is the memory of this run: each pass through the loop, the model sees everything that happened before. Lose that, and the agent forgets what it just learned.

Where It Stops Being a Workflow and Becomes an Agent#

This is the question readers actually ask: are we building a real agent here, or just a script with extra steps? The honest answer is in one line of that code.

In a normal program, you decide the order: call the API, then parse, then format. The control flow lives in your code. In the loop above, the line if not reply.tool_calls hands that decision to the model. The model — not your code — chooses whether to act again or to stop. That is the line where a workflow becomes an agent.

🔑 Key point

If the model decides what happens next, it’s an agent. If your code decides, it’s a workflow. That single distinction is the whole point of the loop.

A quick contrast makes it concrete. Imagine the hardcoded version of the same task:

python
# A workflow: YOU decide the steps, every time, in order.
facts = search("height of Mount Everest")
answer = convert_to_feet(facts)
print(answer)

That works, but only for this exact question. Ask it “how tall is Everest and K2?” and it breaks, because the steps are fixed. The agent loop handles both, because the model can choose to search twice before answering.

When people say an agent is a loop where the model decides control flow, this is what they mean — and it is the definition VentureBeat and others settled on across 2026. If you want the full conceptual breakdown, the complete guide to AI agents covers it; here we care that you can see it in your own code.

Parsing Actions Safely and Knowing When to Stop#

The bare loop works, but two things will bite you in real use: a tool that throws, and a loop that never ends. Both have simple fixes.

First, never let one tool crash the whole agent. The model fills in the arguments, and it can get them wrong — a missing field, or a function name that does not exist. Wrap the dispatch so a failure becomes a message the model can recover from, not a stack trace:

python
for call in reply.tool_calls:
    args = json.loads(call.function.arguments)
    try:
        fn = TOOLS[call.function.name]      # KeyError if the name is invented
        output = fn(**args)
    except Exception as error:
        output = f"Tool error: {error}"     # hand the problem back to the model
    messages.append({
        "role": "tool", "tool_call_id": call.id, "content": output,
    })

When the model sees "Tool error: ..." as an observation, it usually corrects itself on the next pass — picks a real tool, or fixes the argument. That recovery is only possible because the result goes back into messages.

Second, the stop condition. The loop ends naturally when reply.tool_calls is empty — the model has nothing left to do, so it answered. The range(5) around the loop is the safety net for when that never happens, which the next section explains.

⚠️ Always cap the loop

Without a max-steps guard, a stuck agent can loop forever and burn real money — I once watched a 2-cent task become a 40-cent runaway. Treat the cap as required, not optional.

💡 Debug by printing each step

A single print(step, reply.tool_calls) at the top of the loop turns the agent from a black box into a readable trace — you watch it think, act, and observe in real time, and instantly see a wrong tool, a repeated call, or an early exit.

Common Failures (and the Fixes)#

Every from-scratch agent hits the same three problems. Here is each one and the line that fixes it.

1. The infinite loop. The model keeps calling tools and never gives a final answer, burning tokens on every pass. The fix is the for step in range(5) cap you already saw. Pick a number that fits the task — most simple agents finish in two or three steps — and force an answer if you hit it:

python
for step in range(5):
    ...                                   # the loop body from above
else:                                     # runs only if we never `break`
    messages.append({"role": "user",
                     "content": "Give your best final answer now, no tools."})
    final = client.chat.completions.create(model="gpt-5.4-mini", messages=messages)
    print(final.choices[0].message.content)

That for/else is real Python: the else runs only when the loop finishes without a break. It guarantees the user always gets an answer, even when the agent runs out of steps.

2. Hallucinated tools. The model asks for a function you never defined. Without a guard, TOOLS[call.function.name] raises KeyError and the program dies. The try/except from the previous section turns that into a recoverable error message instead — the single most common reason a beginner’s agent crashes.

3. No final answer. Sometimes the model calls a tool and then stops mid-thought because of a token limit, leaving no plain reply. Raise max_tokens for the final step, or rely on the for/else fallback above to ask one more time. Between the step cap, the try/except, and the forced final answer, the loop is now hard to break.

One more thing worth saying plainly: this is the loop that every framework wraps. When you eventually compare LangGraph, CrewAI, and AutoGen, you will recognise this exact cycle under all three — they add retries, tracing, and state, but the heartbeat is what you just wrote.

Quick Recap#

The whole agent loop, in five points:

  • The loop sends messages to the model, runs any tool it asks for, appends the result, and repeats.
  • It stops when the model replies with no tool call.
  • It’s an agent because the model, not your code, picks the next step.
  • Guard against runaways with a max-steps cap and a forced final answer.
  • Guard against crashes by wrapping every tool call in try/except.

Conclusion#

You built a real agent from scratch: a loop that lets the model think, call a tool, read the result, and decide its own next step — about thirty lines, no framework, with guards for the failures that actually happen. The moment your code stopped dictating the steps and let the model choose, it became an agent. That is the whole idea, and now it is yours.

There is one honest limit. Your agent forgets everything the instant the loop ends — start a new question and it has no idea what came before. Giving it memory, so it remembers across turns, is exactly what Part 4 tackles.

When you ran the loop, what was the first tool your agent reached for — a search, a calculator, a database query? Tell me in the comments; the choices people make here are always interesting.

Read next: LangGraph vs CrewAI vs AutoGen (2026) — see the loop you just wrote hiding inside all three frameworks.

🧭 Where to go from here

Frequently asked questions

What is the agent loop in an AI agent? +
It is a repeating cycle where the model produces a thought, requests an action (a tool call), your code runs it and returns the result as an observation, and the loop repeats. It keeps going until the model decides it has enough to give a final answer. That loop is what turns a one-shot tool call into an agent.
What is a ReAct agent? +
ReAct stands for Reasoning and Acting. It is a pattern, introduced by Yao et al. in 2022, that interleaves the model's reasoning (Thought) with tool use (Action) and the result (Observation) in one loop. Modern models do this through native tool calling, so you no longer have to parse the reasoning out of plain text.
Do I need a framework like LangChain to build an agent loop? +
No. The core loop is about thirty lines of plain Python around a normal LLM call. Frameworks add memory, retries, and tracing on top, but the loop underneath is the same one you build here. Understanding it raw makes every framework easier to read later.
How do I stop an agent loop from running forever? +
Use a max-steps guard so the loop can only run a fixed number of times, and stop as soon as the model returns a response with no tool call. If the loop hits the cap, make one final call without tools to force a plain answer.

References

  1. ReAct: Synergizing Reasoning and Acting in Language Models (arXiv)
  2. Anthropic — Building Effective AI Agents
  3. OpenAI — Function calling guide
Written by
Sukhveer Kaur
Sukhveer KaurSoftware Developer & AI Engineer

Sukhveer is a software developer specialising in AI systems and backend engineering. She has hands-on experience designing agentic AI applications, working with large language model pipelines, autonomous agent frameworks, and cloud-native services in Java and Python. At InfoWok, she bridges the gap between cutting-edge AI research and practical implementation — helping developers understand and apply emerging technologies through clear, experience-backed writing.

Get the next part the day it lands

One email per new part. No digest spam.

Comments