Building AI Agents: A Developer’s Guide to Autonomous Workflows
Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.
Every few years a new abstraction shows up in software engineering that changes how we ship products. Containers did it for deployment. Kubernetes did it for orchestration. Now agentic AI is doing it for automation. If you’ve spent any time on engineering Twitter or Hacker News in the last year, you’ve seen the term thrown around constantly, but most of the content out there is either hand-wavy product marketing or academic papers you need a PhD to parse.
This guide skips both. We’re going to treat building AI agents like any other DevOps problem: define the architecture, write the code, containerize it, deploy it, and monitor it in production. By the end you’ll have a working agent running in Docker, with a clear path to scaling it on real infrastructure.
What Is an AI Agent, Actually?
An AI agent is a program that uses a large language model (LLM) not just to generate text, but to make decisions in a loop: observe the current state, decide on an action, execute that action (often by calling a tool or API), and then observe the result before deciding again. The key difference from a simple chatbot is autonomy — an agent can chain multiple steps together without a human clicking “send” after every message.
Agents vs. Simple LLM API Calls
A lot of people confuse “calling GPT-4 in a script” with “building an agent.” They’re not the same thing. A single API call is stateless: you send a prompt, you get a completion, done. An agent wraps that call in a loop with memory, tool access, and a stopping condition. Concretely, an agent architecture usually includes:
If your script doesn’t have at least a loop and a tool layer, you’ve built a prompt wrapper, not an agent. That distinction matters when you’re scoping infrastructure — agents are stateful, long-running processes, which changes how you deploy and monitor them compared to a stateless API endpoint.
Why Self-Hosting Matters for Agent Workloads
Most tutorials assume you’ll run your agent on a managed platform or a laptop. Neither works well in production. Managed agent platforms lock you into their pricing and rate limits, and a laptop obviously can’t run 24/7. If your agent needs to poll a queue, watch a filesystem, or run scheduled tasks, you need a real server.
This is also where cost control becomes a real concern. LLM API calls aren’t free, and a poorly bounded agent loop can burn through your token budget in minutes if it gets stuck retrying a failed tool call. Self-hosting the orchestration layer — while still calling out to an LLM API — gives you full control over rate limiting, retry logic, and logging, none of which you get with a black-box SaaS agent builder.
Building a Simple AI Agent in Python
Let’s build something real. We’ll use the OpenAI API for the model calls and keep the orchestration logic in plain Python so you can see exactly what’s happening at each step — no heavy framework required to start.
First, set up your environment:
mkdir agent-demo && cd agent-demo
python3 -m venv venv
source venv/bin/activate
pip install openai requests python-dotenv
Now create agent.py:
import os
import json
import requests
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def get_weather(city: str) -> str:
"""Tool: fetch current weather for a city."""
resp = requests.get(f"https://wttr.in/{city}?format=3", timeout=5)
return resp.text.strip()
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a given city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
]
def run_agent(user_goal: str, max_steps: int = 5):
messages = [{"role": "user", "content": user_goal}]
for step in range(max_steps):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content
for call in message.tool_calls:
args = json.loads(call.function.arguments)
if call.function.name == "get_weather":
result = get_weather(args["city"])
else:
result = "Unknown tool"
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
return "Max steps reached without a final answer."
if __name__ == "__main__":
print(run_agent("What's the weather like in Lisbon right now?"))
This is the entire skeleton of an agent: a loop, a tool the model can call, and a hard stop at max_steps so a broken loop can’t run forever and rack up API charges. Every production agent framework — LangChain, CrewAI, AutoGen — is a more elaborate version of this same pattern. Understanding this loop first will save you hours of debugging once you move to a framework, because you’ll actually know what’s happening under the abstraction.
Adding Memory with a Vector Store
Once your agent needs to recall information across sessions, a plain message list isn’t enough. Most production agents pair the loop above with a vector database like Chroma or Pinecone to store and retrieve embeddings of past interactions or documents. The pattern is straightforward: embed incoming text, store it with metadata, and query the store for relevant context before each LLM call. We won’t build the full retrieval pipeline here, but keep in mind that memory is a separate concern from the agent loop — don’t couple them tightly, or you’ll struggle to swap vector stores later.
Containerizing Your Agent with Docker
Once the logic works locally, package it so it runs identically anywhere. If you’re new to multi-service setups, our guide to Docker Compose fundamentals covers the basics this section builds on.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY agent.py .
ENV PYTHONUNBUFFERED=1
CMD ["python", "agent.py"]
Running a Multi-Agent Stack with Docker Compose
Real-world setups rarely involve a single agent. You’ll typically have an agent process, a vector database for memory, and maybe a Redis instance for task queuing. Here’s a docker-compose.yml that ties them together:
version: "3.9"
services:
agent:
build: .
env_file: .env
depends_on:
- redis
- chroma
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
chroma:
image: chromadb/chroma:latest
ports:
- "8000:8000"
volumes:
- chroma-data:/chroma/chroma
restart: unless-stopped
volumes:
chroma-data:
Bring it up with docker compose up -d --build, and you have an isolated, reproducible agent stack that behaves the same on your laptop as it does on a production VPS. This matters more than it sounds — LLM agent behavior can be subtly sensitive to Python and library versions, and Docker eliminates the “works on my machine” class of bugs entirely.
Monitoring and Scaling Agents in Production
Agents fail differently than typical web services. Instead of a clean 500 error, an agent might silently loop, hallucinate a tool call that doesn’t exist, or quietly burn through your API budget over a weekend while nobody’s watching. Standard uptime monitoring isn’t enough here — you need to track step counts, token usage, and tool-call failure rates.
Logging Every Step for Debuggability
At minimum, log the full message history for every agent run, not just the final output. When an agent misbehaves, the failure is almost always buried three or four steps back in the transcript, not in the final response. Structure your logs as JSON so they’re easy to query later:
import logging
import json
logger = logging.getLogger("agent")
def log_step(step: int, message: dict):
logger.info(json.dumps({"step": step, "message": message}))
Ship those logs somewhere durable rather than relying on docker logs, which rotates and disappears. For teams that don’t want to run their own ELK stack, a hosted option like BetterStack gives you centralized log aggregation and uptime alerting without the operational overhead — genuinely useful once you have more than one agent running unattended. If you’re comparing observability options more broadly, see our breakdown of self-hosted monitoring stacks for the tradeoffs.
Setting Hard Limits to Control Cost
Beyond max_steps, add a token budget check before every LLM call and kill the run if it’s exceeded. This single guardrail has saved more than a few teams from a surprise five-figure API bill after an agent got stuck in a retry loop over a long weekend.
Choosing Infrastructure for Your Agent Workloads
Agents that run continuously — polling queues, watching webhooks, executing scheduled jobs — need a server, not a laptop or a serverless function with a 15-minute timeout. A small VPS is more than enough to start. DigitalOcean droplets are a solid default if you want a simple, well-documented control panel and predictable pricing while you’re iterating on the architecture. If you’re optimizing for cost once your agent stack stabilizes, Hetzner cloud instances offer significantly cheaper compute per dollar for the same workload, which adds up once you’re running multiple agents around the clock.
Whichever provider you pick, harden the box before you deploy anything — our Linux server hardening checklist is a good starting point, since an agent with API keys and tool access on a poorly secured server is a bigger liability than a typical web app.
Security Considerations for Autonomous Agents
An agent that can execute code, hit arbitrary URLs, or write to a filesystem is functionally similar to giving a script root-ish access to your infrastructure. A few non-negotiables:
That last point deserves emphasis: prompt injection through tool results is one of the most common real-world agent vulnerabilities, and it’s easy to overlook because it doesn’t look like a traditional security bug.
Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.
FAQ
What’s the difference between building AI agents and using a chatbot API?
A chatbot API call is a single stateless request-response exchange. Building AI agents involves wrapping model calls in a loop with memory, tool access, and a decision-making process that can span multiple steps without human input at each stage.
Do I need a framework like LangChain to build an agent?
No. As shown in the Python example above, you can build a working agent loop with plain code and the OpenAI SDK. Frameworks add convenience for complex multi-agent orchestration, but understanding the raw loop first makes debugging framework-based agents much easier.
How much does it cost to run an AI agent in production?
Costs depend on token usage per step and how many steps your loop runs before stopping. A well-bounded agent with a hard max_steps limit and token budget checks typically costs a few cents to a few dollars per run, depending on the model. Unbounded loops are the main cause of runaway bills.
Can I run AI agents entirely on my own hardware without cloud APIs?
Yes, using local models served through tools like Ollama or vLLM instead of a hosted API. This removes per-token costs but requires GPU hardware and generally produces weaker reasoning than frontier hosted models, so it’s a tradeoff between cost and capability.
Is Docker required to deploy an AI agent?
Not strictly, but it’s strongly recommended. Agent behavior can be sensitive to Python and dependency versions, and Docker guarantees your local testing environment matches production exactly, which matters more for agents than for typical stateless services.
How do I prevent an agent from getting stuck in an infinite loop?
Always enforce a hard max_steps limit in your executor loop, plus a token budget check before each LLM call. Log every step so you can diagnose why a loop got stuck rather than just killing and restarting it blindly.
Wrapping Up
Building AI agents isn’t magic — it’s a loop, a tool layer, some memory, and a lot of the same DevOps discipline you’d apply to any other production service: containerize it, monitor it, log everything, and put hard limits on anything that costs money per call. Start with the plain Python loop above, containerize it with Docker once it works, and deploy it on a small VPS you actually control. That gets you further, faster, than reaching for a heavyweight framework before you understand what it’s abstracting away.
Leave a Reply