Build a Simple AI Agent in Python (No Framework Needed)
August 15, 2026

Build a Simple AI Agent in Python (No Framework Needed)

Every “build an AI agent” tutorial I ran into lately starts with pip install langchain or pip install langgraph. Useful tools, but they hide the actual mechanism. Before reaching for a framework, I wanted to see the loop with my own eyes — so I built the smallest working agent I could, in plain Python.

Turns out it’s not much code. The core loop fits in about 60 lines.

What an “agent” actually is

Strip away the marketing and an AI agent is one thing: a loop where the model can call functions, look at the results, and decide what to do next — instead of just returning text once. This is the ReAct pattern (Reason + Act) — the model reasons about what it needs, calls a tool, observes the output, and repeats until it has an answer.

Setup

You need Python 3.10+, an API key from a provider (OpenAI, Anthropic, etc.), and their SDK:

pip install openai
export OPENAI_API_KEY="your-key-here"

The tools

An agent is useless without tools. Let’s give it two: a calculator and a fake “search” function.

def calculator(expression: str) -> str:
    try:
        return str(eval(expression, {"__builtins__": {}}))
    except Exception as e:
        return f"error: {e}"

def search(query: str) -> str:
    # stand-in for a real search API
    return f"Top result for '{query}': (mocked) relevant info here."

TOOLS = {
    "calculator": calculator,
    "search": search,
}

Describing tools to the model

The model needs to know what tools exist and how to call them:

tool_schema = [
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "Evaluate a math expression",
            "parameters": {
                "type": "object",
                "properties": {"expression": {"type": "string"}},
                "required": ["expression"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search",
            "description": "Search the web for information",
            "parameters": {
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"],
            },
        },
    },
]

The agent loop

This is the whole thing — reason, act, observe, repeat:

from openai import OpenAI
import json

client = OpenAI()

def run_agent(user_input: str, max_steps: int = 5) -> str:
    messages = [{"role": "user", "content": user_input}]

    for _ in range(max_steps):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=tool_schema,
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content  # model is done, final answer

        for call in msg.tool_calls:
            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,
            })

    return "Max steps reached without a final answer."

Run it:

print(run_agent("What's 47 * 12, and search for what that number might mean?"))

The model decides on its own to call calculator, sees the result, decides whether it needs search too, and only returns text once it’s actually done — that’s the whole agent.

Where frameworks earn their keep

This toy loop breaks down fast in production: no retry logic, no persistent state across restarts, no human-in-the-loop pausing, no memory beyond the current run. That’s exactly the gap LangChain and LangGraph fill — LangChain for fast prototyping with a huge integration library, LangGraph when the agent needs to pause, resume, or survive a crash with explicit state graphs instead of implicit chains.

But building the raw loop first made picking between them a lot easier — I could see exactly which problem each one was solving instead of taking it on faith.

Next steps

From here: add real memory (conversation history trimming), real tools (an actual search API, file access), and error handling around tool failures. At that point, a framework starts paying for itself — but you’ll understand what it’s doing under the hood.

Share X / Twitter LinkedIn
Previous Python You should know these 5 Powerful functions in Python!

Related Posts

Follow me

I work on everything coding and share developer memes