How to Build an AI Agent: A Practical Developer’s Guide
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.
If you’ve spent any time on developer Twitter or Hacker News in the last year, you’ve seen the term “AI agent” thrown around constantly. Most of the explanations are either too abstract (“it’s software that thinks!”) or too shallow (a single API call wrapped in a for-loop). This guide skips the hype and shows you how to build an AI agent that actually does something useful, then run it in production like any other service in your stack — containerized, monitored, and secured.
We’ll build a real agent in Python, containerize it with Docker, and deploy it to a VPS. By the end, you’ll understand the architecture well enough to build your own agents for DevOps automation, log triage, or customer support.
What Is an AI Agent, Really?
An AI agent is a program that uses a large language model (LLM) as its reasoning engine, but unlike a chatbot, it doesn’t just generate text — it takes actions. It calls functions, hits APIs, reads files, queries databases, and uses the results to decide what to do next. The defining trait is the loop: observe, reason, act, repeat — until the task is done or a stopping condition is hit.
Agents vs. Simple Chatbots
A chatbot answers one prompt and stops. An agent:
If you’ve used GitHub’s Copilot Workspace or any coding assistant that can run commands and fix its own errors, you’ve already used an agent. The same pattern applies to server automation, incident response, and content pipelines — which is exactly the kind of workload this site’s readers deal with daily.
Core Components of an AI Agent
Every agent, regardless of framework, is built from the same four pieces:
The Reasoning Loop
The control loop is the part most tutorials gloss over. In practice it’s a simple while loop: send the model the current state, parse its response for a tool call, execute that tool, feed the result back in, repeat. The loop terminates when the model returns a final answer instead of a tool call, or when you hit a max-iteration safety limit (always set one — an ungoverned loop can burn through API credits fast).
Tools and Function Calling
Modern LLM APIs support structured “function calling,” where you describe available functions in JSON Schema and the model returns a structured call instead of free text. This is far more reliable than parsing natural language for commands.
Memory and State
For short tasks, an in-memory list of messages is enough. For anything long-running — like an agent that monitors your infrastructure over days — you’ll want persistent storage. Redis works well for this: fast, simple key-value storage for conversation state and tool results.
Building Your First AI Agent in Python
Here’s a minimal but functional agent that can execute shell commands and read files — useful as a base for a DevOps automation bot. It uses OpenAI’s function-calling API, but the same pattern works with Anthropic’s Claude API or any local model server.
import json
import subprocess
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "run_shell_command",
"description": "Run a read-only shell command and return its output",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string"}
},
"required": ["command"]
}
}
}
]
ALLOWED_COMMANDS = ("df", "uptime", "free", "docker ps", "systemctl status")
def run_shell_command(command: str) -> str:
if not command.startswith(ALLOWED_COMMANDS):
return "Error: command not permitted by allowlist"
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=10)
return result.stdout or result.stderr
def run_agent(user_goal: str, max_steps: int = 6):
messages = [{"role": "user", "content": user_goal}]
for _ in range(max_steps):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content # final answer
for call in message.tool_calls:
args = json.loads(call.function.arguments)
output = run_shell_command(args["command"])
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": output,
})
return "Max steps reached without a final answer."
if __name__ == "__main__":
print(run_agent("Check disk usage and container status, summarize any issues"))
Note the ALLOWED_COMMANDS allowlist. This is not optional — an agent with unrestricted shell access is a remote code execution vulnerability wearing a trench coat. Always constrain what tools your agent can actually invoke.
If you’d rather not hand-roll the loop, frameworks like LangChain or LlamaIndex handle tool orchestration, retries, and memory for you, at the cost of extra abstraction layers you’ll eventually need to peel back to debug anything.
Deploying Your AI Agent with Docker
Once the agent works locally, containerize it so it runs identically in staging and production — the same reasoning we cover in our Docker Compose guide for multi-container apps applies here.
Containerizing the Agent
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY agent.py .
# Never bake API keys into the image — inject at runtime
ENV OPENAI_API_KEY=""
USER nobody
CMD ["python", "agent.py"]
Build and run it with the key injected as an environment variable, not hardcoded:
docker build -t my-ai-agent .
docker run --rm
-e OPENAI_API_KEY="$OPENAI_API_KEY"
--memory=512m
--cpus=1
my-ai-agent
The --memory and --cpus flags matter more for agents than for typical web services — a runaway reasoning loop can spike CPU usage if your max-step guard fails, so hard resource limits are cheap insurance.
Running as a Persistent Service
For an agent that needs to stay alive and respond to triggers (webhooks, cron, a message queue), wrap it in a small FastAPI service instead of a one-shot script, and run it under docker-compose alongside Redis for state:
services:
agent:
build: .
restart: unless-stopped
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- REDIS_URL=redis://redis:6379
depends_on:
- redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
redis-data:
Hosting Your AI Agent in Production
An agent that calls external tools and possibly touches production infrastructure deserves a properly isolated VPS, not a shared dev box. A basic 2-vCPU / 4GB droplet from DigitalOcean is enough for most single-agent workloads, and their managed Redis add-on saves you from running your own. If you’re optimizing for cost-per-vCPU on long-running background workers, Hetzner cloud instances are consistently cheaper for the same specs — a detail we go into in our VPS cost comparison guide.
Once deployed, you need visibility into whether the agent is actually alive and completing tasks, not silently stuck in a retry loop. BetterStack uptime and log monitoring can alert you the moment your agent’s health-check endpoint stops responding, which matters a lot more for autonomous services than for a static website — nobody’s refreshing a page to notice an agent has hung.
Securing Your AI Agent
Agents with tool access are a bigger attack surface than a typical CRUD app, because a cleverly crafted input (prompt injection) can trick the model into calling a tool it shouldn’t. Bake these controls in from day one:
If your agent is public-facing (say, behind a webhook), put it behind Cloudflare for DDoS protection and WAF rules before traffic reaches your container — this is standard practice for any exposed endpoint, agent or not.
Wrapping Up
Building an AI agent isn’t fundamentally different from building any other backend service: you still need containerization, resource limits, monitoring, and a security model. The LLM is just the decision-making component in the middle. Start small — a single-tool agent solving one narrow problem — before reaching for multi-agent frameworks or long-running autonomous systems. Get the loop, the tool allowlist, and the deployment pipeline right first; complexity can come later once you trust the foundation.
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
Do I need a framework like LangChain to build an AI agent?
No. For a single-purpose agent with a handful of tools, a plain function-calling loop (as shown above) is easier to debug and has fewer moving parts. Frameworks earn their keep once you’re managing many agents, complex memory, or multi-step planning across sessions.
Which LLM should I use for an agent?
GPT-4o and Claude models both support reliable function calling and handle multi-step reasoning well. For cost-sensitive, high-volume agents, smaller or open-source models (Llama 3, Mistral) can work if the tool set is narrow and well-defined — test accuracy on your specific tasks before committing.
How do I stop an agent from running forever or looping?
Always cap iterations with a max_steps counter and enforce per-call timeouts on any tool the agent invokes. Combine this with container-level CPU and memory limits so a stuck loop can’t consume unbounded resources.
Is it safe to let an agent run shell commands on my server?
Only with a strict allowlist of specific commands, run as a non-root, non-privileged user, ideally inside an isolated container with no access to secrets it doesn’t need. Never give an agent unrestricted shell=True execution against production.
Can I run an AI agent without paying for a cloud LLM API?
Yes — self-hosted models via Ollama or a GPU VPS can replace the OpenAI/Anthropic API for many agent workloads, at the cost of managing your own inference infrastructure and generally lower reasoning quality on complex tool-use tasks.
How much does it cost to run an AI agent in production?
Costs split into two buckets: LLM API usage (billed per token, scales with loop iterations) and hosting (a small VPS is usually $10-40/month). Set hard iteration limits and log token usage per run so costs stay predictable as usage grows.
Leave a Reply