How to Build an AI Agent with Python: A 2026 Hands-On Guide — Cesar Ayala
← All posts

How to Build an AI Agent with Python: A 2026 Hands-On Guide

An AI agent in Python is an LLM in a loop that calls tools and feeds results back until the task is done. Build it in ~40 lines with a provider SDK: define tools as JSON schemas, send the message history, run any requested tool, append the result, and loop with a hard step cap. Reach for a framework only when you hit a real wall.

What is an AI agent, really (and what isn’t)?

To build an AI agent in Python you write a loop around an LLM: send the model your goal plus a set of tool schemas, let it decide which tool to call, run that tool, feed the result back, and repeat until the task is done — roughly 40 lines against your provider’s native tool-calling API, no framework required.

Now let me kill the mystique before we write more than that. An AI agent is an LLM in a loop that decides which tools to call, observes the results, and keeps going until the task is done or it gives up. That’s it. No magic, no consciousness, no swarm of digital coworkers.

The line that actually carries weight — and I borrow Anthropic’s framing here because it’s the cleanest I’ve seen — is the one between a workflow and an agent. A workflow has control flow YOU define in code: classify, then route, then extract, then send. You orchestrate the steps and you can draw the flowchart in advance. An agent hands that decision to the model: it decides what to do next based on what just happened, and you genuinely cannot predict how many steps it’ll take or in what order.

Here’s the part nobody says out loud: most production “agents” people brag about on LinkedIn are actually workflows. And that’s a feature, not a failure. Workflows are cheaper, testable, and they don’t surprise you at 2 a.m.

When something IS a real agent, it has four parts and only four parts:

  • A model with a goal (the system prompt and the task).
  • A set of tools it can call.
  • A loop that feeds tool results back into the model.
  • A stop condition so it doesn’t run forever.

If you want the deeper conceptual treatment of where agents sit relative to chatbots and pipelines, I wrote a whole piece on what an AI agent actually is. For this guide we’re building one. My lived take after shipping a bunch of these at Nixbly: most agents I put into production are one model, three tools, and a loop with a hard cap — the AI automation and agents work I do almost never needs the exotic stuff that rarely ships.

Do I even need a framework to build one?

No. And I’ll defend that.

The honest 2026 default is to start with no framework at all. Every major provider — Anthropic, OpenAI, Google — has had structured tool-calling baked into the raw API for years now. A working agent is roughly 40 lines of Python: send the messages and your tool schemas, check whether the model asked for a tool, run it, append the result, loop. Frameworks mostly wrap exactly this loop and hide it from you.

That hiding is great for shipping fast and terrible for understanding what’s happening when it breaks — and agents break in the loop. The malformed tool argument, the lost tool_use block, the runaway retry: all of it lives in the mechanics a framework abstracts away. So write the raw loop once. The understanding transfers to every framework because they all wrap the same thing.

In my experience, 80% of real-world agents never need more than the raw loop plus some guardrails. I’ll name frameworks later and give you a decision rule for each — but you get a working agent before I name a single one. That ordering is deliberate.

How does the agent loop actually work in Python?

Here’s a complete, correct minimal agent using the raw Anthropic SDK. Install with pip install anthropic pydantic and set ANTHROPIC_API_KEY. Read it slowly — every line is load-bearing.

"""Minimal AI agent: a loop around the model that calls tools and feeds
results back until the task is done. ~40 lines, correct, readable."""
from anthropic import Anthropic
from pydantic import BaseModel, ValidationError

client = Anthropic()  # reads ANTHROPIC_API_KEY from env
MAX_STEPS = 8         # hard cap — the single most important guardrail

class WeatherArgs(BaseModel):      # validate the tool INPUT shape
    city: str

def get_weather(raw_args: dict) -> str:
    args = WeatherArgs(**raw_args)  # raises if the model hallucinates the shape
    return f"{args.city}: 18C and clear."

# Dispatch by tool NAME — one tool today, but real agents have several.
TOOL_FNS = {"get_weather": get_weather}

TOOLS = [{
    "name": "get_weather",
    # Say WHEN to call it, not just what it does.
    "description": "Get current weather for a city. Call this whenever the "
                   "user asks about current weather or temperature.",
    "input_schema": WeatherArgs.model_json_schema(),
}]

def run_agent(goal: str) -> str:
    messages = [{"role": "user", "content": goal}]
    for _ in range(MAX_STEPS):           # bounded loop, never `while True`
        resp = client.messages.create(
            model="claude-haiku-4-5",     # cheap model for a simple task
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,            # STATELESS: resend the whole history
        )
        if resp.stop_reason != "tool_use":
            return "".join(b.text for b in resp.content if b.type == "text")

        # Append the ENTIRE assistant turn — preserves the tool_use blocks.
        messages.append({"role": "assistant", "content": resp.content})

        results = []
        for block in resp.content:        # model can ask for SEVERAL tools at once
            if block.type == "tool_use":
                try:
                    out = TOOL_FNS[block.name](block.input)  # route by name
                except ValidationError as e:
                    out = f"Invalid tool input: {e}"   # feed the error back
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,           # MUST match the request
                    "content": out,
                })
        messages.append({"role": "user", "content": results})
    raise RuntimeError("Hit step limit — likely a loop.")  # fail loud

if __name__ == "__main__":
    print(run_agent("What's the weather in Puebla right now?"))

That’s the whole pattern. Everything else — LangGraph nodes, CrewAI crews, handoffs — is structure layered on top of this.

The three traps practitioners get wrong

First, append the entire assistant content, not just the text. If you strip the tool_use blocks and keep only the prose, the next turn breaks because the tool_result you send has nothing to attach to. Append resp.content whole.

Second, every tool_result must carry the exact tool_use_id from the request. The model pairs request to result by that ID. Get it wrong and the conversation desyncs silently.

Third, the model can request multiple tools in one turn. Run them all, return all results in a single user message, and dispatch each one by block.name through a {name: fn} map — that’s what the TOOL_FNS registry is for. Beginners loop on the first tool_use block and drop the rest, or hardcode a single handler.

And the one that bites everyone: the API is stateless. There is no server-side memory of your conversation. You resend the full, growing messages list every single call. That’s the cost driver we’ll come back to — and the number one beginner bug.

What about ReAct?

If you’ve read older tutorials, you’ve seen ReAct — Reason, Act, Observe — with hand-prompted “Thought:/Action:/Observation:” scaffolding parsed by regex. That’s the lineage of what you just built, but in 2026 you don’t reimplement it by hand. Native tool calling makes Act and Observe structured (typed tool_use/tool_result), and adaptive thinking makes Reason a first-class model feature. The loop above is ReAct, productized.

One note: the SDKs ship a beta tool-runner (it lives under the beta namespace) that auto-executes this loop for you. Use it when you want speed. Use the manual loop when you need human-in-the-loop gating, custom logging, or conditional execution — which, for anything touching production, you will.

The agent loop, end to end

Goal inUser task + system prompt seed the messages list
Model thinksReasons, returns text or tool_use blocks
tool_use?stop_reason decides: act or finish
Run tool(s)Your harness dispatches by name — validate the input first
tool_result backAppend results keyed by tool_use_id, resend history
end_turnNo tool requested → return the final answer
Every iteration resends the full transcript. The loop ends at end_turn or the step cap — whichever comes first.

How do I design tools my agent won’t misuse?

Tool misuse — wrong tool, malformed arguments — is the single most common agent-specific failure I see. A bad argument at step two silently corrupts everything downstream. Tool design is where agents actually succeed or fail, and almost none of it is about the model.

Write descriptions that say WHEN to call the tool, not just what it does. “Call this when the user asks about current prices or recent events” measurably raises the correct-call rate over a bare “Gets prices.” The 2026 frontier models reach for tools more conservatively, so trigger conditions in the description earn their keep.

Keep the tool set small and focused. Too many tools confuse the model. My rule: start broad — a single “run code” or “run bash” tool gives you enormous breadth for free. Promote an action to a dedicated, typed tool exactly when you need to gate it, audit it, render custom UI for it, or run it safely in parallel. Don’t pre-build twenty tools the agent will never disambiguate correctly.

Validate every tool input against a schema. That’s what the Pydantic model does in the example above. A hallucinated argument shape fails cleanly and gets fed back as a tool_result correction instead of silently writing garbage to your database. Validation handles structure; your prompt handles intent.

The harness, not the model, owns the security boundary. The model only emits a tool call — your code decides whether to actually run it. So gate every irreversible or destructive action — send_email, process_payment, delete_record — behind human confirmation in a manual loop, never the auto-runner. And tool_choice (auto, any, a named tool, or none) lets you force or forbid tool use when a step demands it. This is the same discipline that makes internal tools and dashboards safe to put in front of a team.

Which Python agent framework should I actually pick?

Here’s the decision rule, not a feature dump. No framework “won” in 2026 — the honest consensus is that it’s use-case-dependent, and the value is knowing which wall you’ve hit.

  • Raw SDK loop — the default, and how you learn. Reach for it first, always.
  • OpenAI Agents SDK — light structure with handoffs and guardrails. Model-agnostic via LiteLLM despite the name, so it doesn’t lock you to OpenAI models.
  • Pydantic AI — type-safe structured outputs, built-in UsageLimits (token and tool-call caps baked into config), clean for FastAPI backends. My pick for a typed single agent.
  • LangGraph — reach for it only when control flow is the hard part: cyclic workflows, approval gates, durable checkpointing that resumes after a crash. Most verbose, steepest curve.
  • CrewAI — fastest way to prototype role-based multi-agent crews. Also the easiest way to over-engineer.
  • smolagents — tiny, hackable, with a code-as-action paradigm. The CodeAgent writes Python as its action, so it needs a sandbox (E2B, Modal, Docker) — never run model-written code on your host.
  • LlamaIndex — when retrieval over your documents is the actual hard problem, not orchestration.
  • Claude Agent SDK — batteries-included autonomous coding/computer-use agent.

One distinction that trips people up: the raw anthropic package (you write the loop) is not the claude-agent-sdk package (the Claude Code harness with built-in file, bash, and web tools). Different packages, different problems. Don’t conflate them.

A lived data point: I rebuilt the same reference chat app a few ways. It came out to roughly 160 lines in Pydantic AI, around 280 in LangGraph, and about 420 in CrewAI. The verbosity gap is real, and it tracks the over-engineering trap — five CrewAI agents where one loop and three tools would be cheaper, faster, and more reliable.

Finally, MCP (Model Context Protocol) is the integration layer, not a framework for building the agent. It’s how tools reach any agent via standardized servers — now cross-vendor, with both the OpenAI Agents SDK and Claude Agent SDK acting as MCP clients. Think of it as “USB for tools,” not “the thing that runs your loop.”

Which framework, and when

Stay simple (start here)

  • Raw SDK loop — default + for learning every agent
  • Pydantic AI — typed outputs, usage limits, FastAPI backends
  • OpenAI Agents SDK — light handoffs/guardrails, model-agnostic

Escalate only when...

  • LangGraph — durable state, resumable runs, approval gates
  • CrewAI — quick role-based multi-agent prototypes
  • smolagents — code-as-action (needs a sandbox); LlamaIndex — retrieval is the hard part
No winner — a decision rule. Start left, escalate right only when you hit the named wall.

How do I keep it from running away and burning money?

Reliability is the hard part, not the loop. The loop is a weekend project. The guardrails are the job.

Here’s why agents get expensive: every loop iteration resends the entire growing transcript. So cost scales super-linearly with steps — until you cache the stable prefix, which is exactly why prompt caching is lever number one below. A single agentic task commonly fires 5 to 20-plus LLM calls and burns roughly 5 to 30 times the tokens of one chat completion — frame those as typical, not precise. An unconstrained coding agent can run a few dollars per task. The failure mode that haunts me: I’ve watched a runaway loop hammer the same broken tool hundreds of times in a few minutes before anyone caught it — no cap, no alarm, just a meter spinning.

Approximate mid-2026 list pricing per million tokens: Claude Haiku 4.5 around $1 in / $5 out, Sonnet 4.6 around $3 / $15, Opus 4.8 around $5 / $25. Multiply that by a transcript you’re resending 15 times and you see the problem.

Three cost levers, in order of leverage:

  1. Prompt caching. Cache reads cost roughly 0.1x the base input price — a 90% discount. Put your stable content first (frozen system prompt, a sorted tool list) and your volatile content last. The silent killers: a datetime.now() in the system prompt, or an unsorted json.dumps (use sort_keys=True), either of which invalidates the cache and makes you pay full price without warning. Verify it’s working by checking cache_read_input_tokens in the usage object.
  2. Model routing. Run routing, extraction, and classification on Haiku; reserve a frontier model for the steps that actually need reasoning. Caveat: don’t route blindly — a cheap model that fails and triggers three retries costs more than doing it once on the smart model.
  3. Parallel and programmatic tool calls. Run independent tools concurrently; let the model write a script that chains calls so intermediate results never re-enter the context window.

Then the boring, non-negotiable limits: a hard max-iteration cap (never while True), a per-run token and cost budget, per-tool timeouts and bounded retries, and detection of repeated identical tool calls (a loop smell). Fail loud at the cap. And observability is mandatory — Langfuse, LangSmith, Braintrust, Sentry, Arize, all OpenTelemetry-native. You cannot ship what you cannot trace, and because agents are non-deterministic, naive unit tests don’t work — you grade them with eval sets.

Agent economics, mid-2026 (approximate)

Tokens vs one chat call~5–30x (transcript resent every step)
LLM calls per agentic task~5–20+
Haiku 4.5 / Opus 4.8 per 1M~$1/$5 vs ~$5/$25
Prompt-cache read price~0.1x base input (≈90% off)
#1 guardrailHard step cap — never while True
Why the transcript-resend model makes agents cost-sensitive — and the levers that matter.

When should I NOT build an agent?

This is the section almost no competing article writes, and it’s the most valuable thing here. Before you build, run the four-question gate:

  1. Complexity — is the task genuinely multi-step and hard to specify up front? If you can draw the flowchart of every step, build a workflow.
  2. Value — does the outcome justify higher cost, higher latency, and non-determinism?
  3. Viability — is the model actually good at this task today?
  4. Cost of error — can mistakes be caught and rolled back, or do they ship straight to a customer or database?

If any answer is no, drop down a tier. Concretely, here’s what not to build an agent for: a single classification, extraction, or summarization task is one LLM call. A fixed “summarize then format then email” pipeline is a deterministic workflow — three calls, no loop. Arithmetic, status lookups, rule-based decisions are plain code. RAG, extraction, and classification are not agent jobs.

Agents earn their cost only when the model must decide which step comes next. My blunt line, the one I give founders: the question is almost never “can we build an agent.” It’s “is an agent the cheapest reliable thing that solves this” — and usually it isn’t. The expensive failure is reaching for autonomy when a 50-line script would do, which is also why I push people to price the alternative first and read what a SaaS MVP actually costs before they assume an agent is the cheap path.

The four-question gate before you build

  1. 1. ComplexityMulti-step and hard to specify? If you can draw the flowchart → workflow, not agent
  2. 2. ValueDoes the result justify 5–30x cost, seconds-to-minutes latency, and variance?
  3. 3. ViabilityIs the model genuinely capable at this task type today?
  4. 4. Cost of errorCan mistakes be caught and rolled back? If irreversible + unguarded → don't
  5. FallbackAny 'no' → deterministic workflow, single LLM call, or plain code
Any 'no' drops you down a tier — to a workflow, or a single call, or plain code.

Frequently asked questions

Do I need a framework to build an AI agent in Python? No. Start with the raw ~40-line loop against your provider’s native tool-calling API. Reach for a framework only when you hit a specific wall.

Which framework is best for beginners? The raw SDK loop, to actually learn how agents work. Then Pydantic AI for a typed single agent with clean ergonomics and built-in usage limits.

How much does running an agent cost? Roughly 5 to 30 times a single chat call, because the full transcript gets resent every step. Cache the stable prefix aggressively (cache reads are about 0.1x input price) and route cheap-vs-smart between Haiku and a frontier model.

Can I use local or open models? Yes. Most frameworks are model-agnostic — via routing layers like LiteLLM, local servers like Ollama, or transformers directly. smolagents and Pydantic AI both support local models.

Agent vs chatbot vs workflow vs RAG — how do I decide? It’s a ladder: single call, then workflow, then agent — escalate only when you need the next tier. A chatbot is a single call in a UI. RAG is retrieval, not an agent. An agent is the model deciding its own trajectory.

How do I stop runaway loops, and how do I handle a tool that fails? A hard step cap, always — never while True. When a tool fails, feed the error back as a tool_result so the model can self-correct, and fail loud when you hit the cap rather than silently burning tokens.

Start with the loop, earn the framework

The loop is a weekend project. The guardrails are the job. Build the raw ~40-line loop, run it until it breaks, and escalate only then — to a framework when control flow is genuinely hard, to gating when actions are irreversible, to observability the moment it touches production.

That’s the honest shape of the work: the demo takes an afternoon, and the caps, the input validation, the human-in-the-loop gating, and the tracing take the rest of the sprint. Anyone who tells you otherwise is selling you a demo.

If you’re scoping whether an agent — or honestly, just a workflow — is the right thing for your product, that’s exactly the kind of decision I help founders get right at Nixbly. Start with the loop. Earn the framework.