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 PDF