Photo by Daniil Komov on Pexels
Most "how to build an AI agent" tutorials start with installing LangChain and 47 dependencies. Then you spend 3 hours debugging import errors before writing a single line of useful code.
This guide is different. We'll build a real, working AI agent from scratch using nothing but Python and an API key. No frameworks. No vector databases. No Kubernetes. Just a script that runs, makes decisions, and does useful work.
By the end, you'll have a fully autonomous agent that can browse the web, analyze content, and take actions — the same pattern behind tools like Devin, Cursor, and Claude Code.
An AI agent isn't a chatbot. A chatbot waits for your input and responds. An agent acts on its own.
The difference is a loop. Here's the core pattern every AI agent follows:
while not done:
observation = perceive(environment)
thought = think(observation, goal)
action = decide(thought)
result = execute(action)
done = evaluate(result, goal)
That's it. Every AI agent — from a $50M startup's product to a weekend project — follows this loop. The LLM is the "think" step. Everything else is just code you already know how to write.
You need Python 3.10+ and an API key from any major LLM provider. We'll use examples with multiple providers so you can pick what fits your budget:
| Provider | Model | Cost per 1M tokens | Best for |
|---|---|---|---|
| Anthropic | Claude Sonnet 4.6 | $3 / $15 | Complex reasoning |
| OpenAI | GPT-5.4 | $2.50 / $10 | General purpose |
| DeepSeek | V3.2 | $0.27 / $1.10 | Cost-sensitive apps |
| Gemini 3.1 Flash | $0.25 / $1.25 | High throughput |
# Install the only dependency you need
pip install requests
# That's literally it. No langchain, no llamaindex, no crew.
Here's a minimal but complete AI agent in ~50 lines of Python. This agent can use tools, reason about results, and decide when it's done:
import json
import requests
API_KEY = "your-api-key-here"
API_URL = "https://api.anthropic.com/v1/messages"
TOOLS = {
"search_web": lambda q: requests.get(
f"https://api.duckduckgo.com/?q={q}&format=json"
).json().get("AbstractText", "No results"),
"read_url": lambda url: requests.get(url, timeout=10).text[:3000],
"save_file": lambda content: open("output.txt", "w").write(content) or "Saved!",
}
SYSTEM = """You are an autonomous agent. You have these tools:
- search_web(query): Search the web
- read_url(url): Read a webpage
- save_file(content): Save content to a file
Respond with JSON: {"thought": "...", "tool": "tool_name", "args": "...", "done": false}
When finished: {"thought": "...", "done": true, "answer": "..."}"""
def call_llm(messages):
resp = requests.post(API_URL, json={
"model": "claude-sonnet-4-6-20250514",
"max_tokens": 1024,
"system": SYSTEM,
"messages": messages,
}, headers={
"x-api-key": API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
})
return resp.json()["content"][0]["text"]
def run_agent(goal, max_steps=10):
messages = [{"role": "user", "content": f"Goal: {goal}"}]
for step in range(max_steps):
response = call_llm(messages)
print(f"\n--- Step {step + 1} ---")
print(response)
data = json.loads(response)
if data.get("done"):
return data.get("answer", "Done")
# Execute the tool
tool_fn = TOOLS.get(data["tool"])
result = tool_fn(data["args"]) if tool_fn else "Unknown tool"
messages.append({"role": "assistant", "content": response})
messages.append({"role": "user", "content": f"Tool result: {result}"})
return "Max steps reached"
# Run it
answer = run_agent("Find the latest news about AI agents and summarize the top 3 stories")
print(f"\nFinal answer: {answer}")
That's a complete agent. ~50 lines. It observes (tool results), thinks (LLM), decides (JSON output), and acts (tool execution). No framework needed.
Agents without memory forget everything between steps. There are two types of memory you need:
This is already built into the code above — the messages list carries the full conversation. But it grows fast and eats tokens. Add a simple summary mechanism:
def compress_memory(messages, keep_last=4):
"""Summarize old messages to save tokens."""
if len(messages) <= keep_last:
return messages
old = messages[:-keep_last]
summary = call_llm([{
"role": "user",
"content": f"Summarize this conversation concisely:\n{json.dumps(old)}"
}])
return [{"role": "user", "content": f"Previous context: {summary}"}] + messages[-keep_last:]
For agents that run across sessions, save key facts to a JSON file:
import json
from pathlib import Path
MEMORY_FILE = Path("agent_memory.json")
def remember(key, value):
memory = json.loads(MEMORY_FILE.read_text()) if MEMORY_FILE.exists() else {}
memory[key] = value
MEMORY_FILE.write_text(json.dumps(memory, indent=2))
def recall(key):
memory = json.loads(MEMORY_FILE.read_text()) if MEMORY_FILE.exists() else {}
return memory.get(key)
Real agents need to handle failures gracefully. The #1 cause of agent failures is the LLM returning malformed output. Here's the fix:
import re
def parse_response(text):
"""Extract JSON from LLM response, even if surrounded by text."""
# Try direct parse first
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# Find JSON block in response
match = re.search(r'\{[\s\S]*\}', text)
if match:
try:
return json.loads(match.group())
except json.JSONDecodeError:
pass
# Fallback: ask the LLM to fix it
return {"thought": "Failed to parse response", "done": True, "answer": text}
def safe_execute(tool_fn, args, timeout=30):
"""Execute a tool with error handling."""
try:
return str(tool_fn(args))
except Exception as e:
return f"Error: {type(e).__name__}: {e}"
The power of an agent comes from its tools. Here are the most useful ones you can add in a few lines each:
import subprocess
import sqlite3
TOOLS = {
# Web browsing
"search_web": lambda q: requests.get(
f"https://api.duckduckgo.com/?q={q}&format=json"
).json().get("AbstractText", "No results"),
"read_url": lambda url: requests.get(url, timeout=10).text[:5000],
# File operations
"read_file": lambda path: Path(path).read_text()[:5000],
"write_file": lambda args: Path(args["path"]).write_text(args["content"]),
"list_files": lambda dir: str(list(Path(dir).glob("*"))),
# Code execution (sandboxed)
"run_python": lambda code: subprocess.run(
["python3", "-c", code],
capture_output=True, text=True, timeout=30
).stdout[:2000],
# Database
"query_db": lambda sql: str(
sqlite3.connect("data.db").execute(sql).fetchall()[:20]
),
# Send notification
"notify": lambda msg: requests.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
json={"chat_id": CHAT_ID, "text": msg}
).json(),
}
We track the latest AI agent tools, frameworks, and deployments from 11+ sources — 3x/week, free.
Subscribe to AI Agents WeeklyAn agent that only runs when you manually start it isn't really autonomous. Here's how to make it run on its own:
# Run every day at 8am
# Edit with: crontab -e
0 8 * * * cd /path/to/agent && python3 agent.py >> agent.log 2>&1
import time
def main_loop():
while True:
try:
result = run_agent("Check for new events and take action")
print(f"[{time.strftime('%H:%M')}] {result}")
except Exception as e:
print(f"Error: {e}")
time.sleep(3600) # Run every hour
if __name__ == "__main__":
main_loop()
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
data = request.json
result = run_agent(f"Handle this event: {json.dumps(data)}")
return {"status": "ok", "result": result}
# Run with: python3 -m flask run --port 5000
Let's put it all together. Here's a complete agent that monitors tech news and sends you a daily digest — the same pattern we use to run AI Agents Weekly:
#!/usr/bin/env python3
"""Autonomous news monitoring agent."""
import json, requests, time
from datetime import datetime
API_KEY = "your-key"
SOURCES = [
"https://hn.algolia.com/api/v1/search?query=AI+agent&tags=story",
"https://hn.algolia.com/api/v1/search?query=LLM+autonomous&tags=story",
]
def fetch_news():
articles = []
for url in SOURCES:
try:
data = requests.get(url, timeout=10).json()
for hit in data.get("hits", [])[:5]:
articles.append({
"title": hit["title"],
"url": hit.get("url", ""),
"points": hit.get("points", 0),
})
except Exception:
continue
return sorted(articles, key=lambda x: x["points"], reverse=True)[:10]
def summarize(articles):
text = "\n".join(f"- {a['title']} ({a['points']} pts)" for a in articles)
resp = requests.post("https://api.anthropic.com/v1/messages", json={
"model": "claude-sonnet-4-6-20250514",
"max_tokens": 500,
"messages": [{"role": "user", "content":
f"Summarize these top AI stories in 3 bullet points:\n{text}"}],
}, headers={
"x-api-key": API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
})
return resp.json()["content"][0]["text"]
def send_digest(summary, articles):
msg = f"🤖 AI Agent Daily Digest — {datetime.now().strftime('%b %d')}\n\n"
msg += summary + "\n\n📰 Top stories:\n"
msg += "\n".join(f"• {a['title']}\n {a['url']}" for a in articles[:5])
# Send via Telegram, email, Slack, etc.
print(msg)
if __name__ == "__main__":
articles = fetch_news()
summary = summarize(articles)
send_digest(summary, articles)
We just built agents without any framework. So when should you use one?
| Use a framework when... | Skip the framework when... |
|---|---|
| Multiple agents need to collaborate | You have a single agent |
| You need complex state machines | A simple loop is enough |
| You want built-in observability | Print statements work fine |
| Your team needs standardized patterns | You're a solo builder |
| You need production-grade retries/fallbacks | try/except covers your needs |
If you do want a framework, check out our comparison of the top 7 AI agent frameworks in 2026.
max_steps limit. Agents that run forever cost real money.You now have a working AI agent. Here's what to explore next:
The AI agent space moves fast. We cover the latest tools, frameworks, and deployment patterns in AI Agents Weekly — free, 3x/week, no fluff.
14-page guide comparing the top 10 AI agent tools of 2026 with ratings, pricing, and use cases.
Download free PDFGet the first chapter of The AI Agent Playbook delivered to your inbox. Learn what AI agents really are and see real production examples.
Get Free Chapter →