How to Build Your First AI Agent Without a Framework
Want to build an AI agent from scratch and skip the framework overhead? This copy-paste guide gets a working tool-using agent running in about forty lines of Python.
import json
from anthropic import Anthropic
client = Anthropic()
def get_weather(city: str) -> str:
fake = {"paris": "18C, clear", "tokyo": "24C, rain"}
return fake.get(city.lower(), "no data")
TOOLS = {"get_weather": get_weather}
SCHEMAS = [{
"name": "get_weather",
"description": "Get current weather for a city.",
"input_schema": {"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]},
}]
def run_agent(goal, max_steps=6):
messages = [{"role": "user", "content": goal}]
for _ in range(max_steps):
resp = client.messages.create(
model="claude-sonnet-4-5", max_tokens=1024,
tools=SCHEMAS, messages=messages)
messages.append({"role": "assistant", "content": resp.content})
calls = [b for b in resp.content if b.type == "tool_use"]
if not calls:
return "".join(b.text for b in resp.content if b.type == "text")
results = []
for c in calls:
out = TOOLS[c.name](**c.input)
results.append({"type": "tool_result",
"tool_use_id": c.id, "content": str(out)})
messages.append({"role": "user", "content": results})
return "Stopped: step limit reached."
print(run_agent("What's the weather in Paris and Tokyo?"))Do you actually need LangGraph, CrewAI, or any framework to build your first agent? That's the question everyone asks, and the honest answer is no — and building one raw first will make you far better at using a framework later, because you'll know exactly what it's hiding.
So let's build an AI agent from scratch. Working code, no framework, running in about forty lines. By the end you'll understand every moving part, which is worth more than any abstraction.
Quick-Start: Build an AI Agent From Scratch (Copy This Now)
Here's the whole thing. Paste it, add your API key, run it.
[object Object], json
,[object Object], anthropic ,[object Object], Anthropic
client = Anthropic()
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
fake = {,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}
,[object Object], fake.get(city.lower(), ,[object Object],)
TOOLS = {,[object Object],: get_weather}
SCHEMAS = [{
,[object Object],: ,[object Object],,
,[object Object],: ,[object Object],,
,[object Object],: {,[object Object],: ,[object Object],,
,[object Object],: {,[object Object],: {,[object Object],: ,[object Object],}},
,[object Object],: [,[object Object],]},
}]
,[object Object], ,[object Object],(,[object Object],):
messages = [{,[object Object],: ,[object Object],, ,[object Object],: goal}]
,[object Object], _ ,[object Object], ,[object Object],(max_steps):
resp = client.messages.create(
model=,[object Object],, max_tokens=,[object Object],,
tools=SCHEMAS, messages=messages)
messages.append({,[object Object],: ,[object Object],, ,[object Object],: resp.content})
calls = [b ,[object Object], b ,[object Object], resp.content ,[object Object], b.,[object Object], == ,[object Object],]
,[object Object], ,[object Object], calls:
,[object Object], ,[object Object],.join(b.text ,[object Object], b ,[object Object], resp.content ,[object Object], b.,[object Object], == ,[object Object],)
results = []
,[object Object], c ,[object Object], calls:
out = TOOLS[c.name](**c.,[object Object],)
results.append({,[object Object],: ,[object Object],,
,[object Object],: c.,[object Object],, ,[object Object],: ,[object Object],(out)})
messages.append({,[object Object],: ,[object Object],, ,[object Object],: results})
,[object Object], ,[object Object],
,[object Object],(run_agent(,[object Object],))What this does: it defines one tool, describes it to the model, then loops — asking the model what to do, running any tool it requests, and feeding results back until the model answers in plain text. That plain-text answer is the model saying "I'm done."
⚡ Pro tip: Start with exactly one tool, even if your real project needs ten. Getting the loop right with one tool takes an afternoon; debugging the loop and five tools at once takes a week.
Understanding the Moving Parts
Four pieces do all the work, and naming them makes the code stop looking like magic.
The message list is the agent's entire memory. Everything it knows about this run lives in
messagesThe tool schema is how the model learns what it can do. The model never sees your Python function; it sees the JSON description. If the description is vague, the model uses the tool badly. This is the highest-impact text in the whole program, and most beginners write it as an afterthought.
Concretely: a description that reads "gets weather" invites the model to call it for climate trivia, historical averages, and ten-day forecasts it can't actually produce. A description that reads "Returns current conditions for one city right now — not forecasts, not historical data" tells the model exactly when to reach for the tool and when to leave it alone. The second version saves you more debugging than any model upgrade will.
The dispatch step is the
TOOLS[c.name](**c.input)formax_stepsOne more thing worth saying out loud: there is no hidden database, no server-side session, no framework state. When your process ends, the agent's entire mind — that
messages⚡ Pro tip: Treat tool descriptions like function docstrings written for a smart but literal new hire. Say what the tool does, when to use it, and what it returns. Every ambiguity you leave becomes a wrong tool call.
Step-by-Step: The Agent Loop
Walk through one full turn of "weather in Paris and Tokyo," so the trace is concrete.
Step one: you send the goal. The model reads it and realizes it needs weather data it doesn't have, so instead of answering it emits two
tool_useStep two: your code sees those blocks, skips the "no calls, return" branch, and runs
get_weathertool_resultStep three: the loop comes around. Now the model has the weather in context. It has everything it needs, so this time it emits plain text — "Paris is 18C and clear, Tokyo is 24C with rain." No tool calls, so your code returns the text and the loop ends.
Three model calls, two tool executions, one clean answer. That's an agent doing its job.
⚡ Pro tip: Print the
messagesPro-Level Variations
Once the basic loop runs, three upgrades cover most real needs.
Add error handling to the dispatch, because tools fail. Wrap the call so a broken tool returns an error string to the model instead of crashing your program — the model can often recover by trying different arguments.
[object Object],:
out = TOOLS[c.name](**c.,[object Object],)
,[object Object], Exception ,[object Object], e:
out = ,[object Object],What this does: it turns a Python exception into feedback the model can read and respond to, which is exactly what lets an agent recover from a bad first attempt instead of taking your process down with it.
Add a system prompt to set boundaries — what the agent is for, what it must never do, when to give up. Add streaming if a human is waiting on the output, so they see progress instead of a spinner.
Third, add a trace log. One line per step recording the tool called, its arguments, and its result turns your agent from a black box into something you can diagnose.
[object Object],(,[object Object],)What this does: writes a compact, greppable record of every decision the agent made, which is the single most useful thing you can add before letting one run unattended.
⚡ Pro tip: Add the trace log before you add the second tool, not after. The log is how you'll debug every tool you add later, so it pays for itself the moment things get complicated.
⚡ Pro tip: Return errors to the model as plain, instructive text rather than raising. An agent that reads "city not found, check spelling" will often self-correct; an agent that hits an uncaught exception just dies.
Troubleshooting Common Issues
The agent loops forever. Your
max_stepsThe agent never calls the tool. Almost always a weak schema description. Rewrite it to say plainly when the tool should be used.
The agent calls the wrong tool. Your descriptions overlap. Two tools that sound similar will get confused; make each description name what makes it distinct.
The agent drifts from the goal on longer runs. As the message list grows, the original instruction gets buried under tool results, and the model starts optimizing for recent context instead of the actual task. Re-state the goal near the end of the window, or pin it, so it stays in view.
⚠️ Common mistake: Reaching for a framework the moment something breaks, assuming the framework will fix it. It won't — the same weak tool description fails inside LangGraph too. Frameworks add observability, retries, and state management, none of which repair a loop you didn't understand in the first place. Fix it raw, then adopt a framework for the features, not the rescue.
Your Turn
You've now built an AI agent from scratch, and — more importantly — you can name every part: the message list is memory, the schema is capability, the dispatch runs the work, the loop grants agency. Swap in a real tool — a database query, an HTTP call, a file reader — and you've got something useful.
A realistic next tool is a database query. Give the agent a read-only function that runs a parameterized SELECT and returns rows, describe it clearly, and suddenly you have an agent that answers questions about your actual data instead of a toy dictionary. Keep it read-only until you trust the traces — a wrong SELECT just wastes tokens, while a wrong DELETE ruins your afternoon.
Knowing how to build an AI agent from scratch also changes how you evaluate every framework you meet next. You'll see exactly which of these four parts a given framework automates, and you'll be able to decide whether that automation is worth the dependency — instead of adopting the tool on faith and inheriting behavior you can't explain.
As your one tool becomes ten and your system prompt grows, keep those prompts and schemas somewhere versioned rather than buried in source files that only you can find. PromptABCD lets you store and reuse agent prompts and tool descriptions across projects, so the schema you spent an afternoon perfecting doesn't get rewritten from scratch in the next repo.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
