Creating an AI Agent: A Practical Guide for Developers

Creating an AI Agent: A Developer’s Guide to Building and Deploying Autonomous Systems

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 months a new framework promises to make creating an AI agent as simple as writing a prompt. In practice, a production-grade agent is a small distributed system: it has a reasoning loop, a set of tools, persistent memory, logging, and a deployment target that needs to stay online. This guide walks through the real architecture, gives you runnable code, and shows how to containerize and host the result.

What Is an AI Agent?

An AI agent is a program that uses a language model to decide what action to take next, executes that action through a tool (an API call, a shell command, a database query), observes the result, and repeats the loop until a goal is satisfied. The key difference from a plain chatbot is autonomy over multiple steps: the agent decides when to stop, which tool to call, and how to interpret the result — you don’t hand-code the control flow for every scenario.

Agents vs. Chatbots vs. Automation Scripts

It’s easy to conflate these three, but they solve different problems:

  • Chatbot: single-turn or conversational text generation with no tool access and no ability to take real-world action.
  • Automation script: fixed, deterministic control flow (cron job, CI pipeline) — no reasoning, just “if X then Y.”
  • AI agent: dynamic control flow driven by a model’s reasoning, with tool calls chosen at runtime based on context.
  • If your task has a fixed sequence of steps, a script is faster, cheaper, and more reliable than an agent. Reach for an agent when the sequence of steps genuinely depends on information you don’t have ahead of time — for example, diagnosing why a Docker container is crash-looping by inspecting logs, checking resource limits, and deciding what to try next.

    The Core Architecture of an AI Agent

    Every working agent, regardless of framework, is built from the same four pieces: a model, a set of tools, a memory store, and a control loop that ties them together. Understanding this before you touch a framework will save you hours of debugging abstracted-away behavior.

    The Perceive-Reason-Act Loop

    At its core, an agent runs a loop:

    1. Perceive — gather the current state (user input, tool output, environment data).
    2. Reason — send that state to the LLM and get back a decision: call a tool, or produce a final answer.
    3. Act — execute the chosen tool and capture its output.
    4. Feed the output back into step 1 and repeat until the model signals completion or you hit a step limit.

    The step limit matters more than people expect. Without one, a model that gets stuck in a reasoning loop will happily burn through your API budget calling the same tool over and over.

    Tools, Memory, and Guardrails

    Tools are just functions with a schema the model can read. Memory can be as simple as an in-context conversation history or as complex as a vector database for retrieval-augmented recall. Guardrails are the boring-but-critical part: input validation, allow-lists for shell commands, timeouts, and spend caps. A few non-negotiables for anything that touches a real system:

  • Never let the model construct raw shell commands without an allow-list or sandbox.
  • Always cap the number of reasoning steps and total token spend per run.
  • Log every tool call and its arguments — you will need this for debugging and for security review.
  • Treat tool output as untrusted input; sanitize before it reaches downstream systems.
  • Creating an AI Agent Step by Step

    Below is a minimal but complete agent written in Python, using the OpenAI API directly so you can see exactly what a framework like LangChain is doing under the hood. You can swap in any model provider that exposes a chat-completions style endpoint.

    Step 1: Pick a Framework and Model

    For learning, skip the framework and call the OpenAI API directly — it exposes exactly how the request/response cycle works. Once you understand the loop, LangChain, CrewAI, or the Anthropic Agent SDK will save you boilerplate for multi-agent setups. Pick a model based on the task: smaller, cheaper models (gpt-4o-mini, Claude Haiku) are fine for well-scoped tool-calling tasks; reserve larger models for open-ended reasoning.

    Step 2: Define the Tool Interface

    Each tool needs a name, a description the model can read, and a Python function that actually executes it:

    TOOLS = {
        "check_container_status": {
            "description": "Returns the status of a Docker container by name.",
            "fn": lambda name: run_shell(f"docker inspect -f '{{{{.State.Status}}}}' {name}"),
        },
        "get_container_logs": {
            "description": "Returns the last 50 log lines for a container.",
            "fn": lambda name: run_shell(f"docker logs --tail 50 {name}"),
        },
    }
    
    def run_shell(cmd):
        import subprocess
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
        return result.stdout or result.stderr

    Notice the timeout argument — any tool that shells out needs a hard timeout, or a hung process will hang your whole agent.

    Step 3: Build the Reasoning Loop

    This is the actual control loop that perceives, reasons, and acts:

    import os, json
    from openai import OpenAI
    
    client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
    
    SYSTEM_PROMPT = """You are a DevOps diagnostic agent. You can call tools to inspect
    Docker containers. When you have a final answer, prefix it with FINAL:."""
    
    def run_agent(user_goal, max_steps=6):
        messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_goal},
        ]
    
        for step in range(max_steps):
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=messages,
            )
            reply = response.choices[0].message.content
            print(f"[step {step}] {reply}")
    
            if reply.startswith("FINAL:"):
                return reply.replace("FINAL:", "").strip()
    
            # naive tool-call parsing for demonstration
            for tool_name, tool in TOOLS.items():
                if tool_name in reply:
                    arg = reply.split(tool_name)[1].strip(" ():"'n")
                    output = tool["fn"](arg)
                    messages.append({"role": "assistant", "content": reply})
                    messages.append({"role": "user", "content": f"Tool output: {output}"})
                    break
            else:
                messages.append({"role": "assistant", "content": reply})
    
        return "Agent did not converge within the step limit."
    
    if __name__ == "__main__":
        result = run_agent("Check why the container named 'web' keeps restarting.")
        print(result)

    In production you’d replace the naive string-matching tool dispatch with the model provider’s native function-calling API, which returns structured JSON instead of free text — far more reliable to parse.

    Step 4: Add Persistent Memory

    In-context history works for a single session, but it resets every time the process restarts. For an agent that needs to remember past incidents or user preferences across runs, persist state to a lightweight store like SQLite or Redis rather than a full vector database — most agents don’t need semantic search, they need a reliable key-value log of what happened and when.

    Containerizing and Deploying Your Agent

    Once the agent works locally, package it the same way you’d package any long-running service. If you haven’t containerized a Python service before, our Docker Compose primer covers the basics this section builds on.

    Writing the Dockerfile

    FROM python:3.12-slim
    
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY agent.py .
    
    CMD ["python", "agent.py"]

    Keep the base image slim, pin your Python version, and never bake API keys into the image layer — pass them at runtime as environment variables.

    Orchestrating with Docker Compose

    version: "3.9"
    services:
      ai-agent:
        build: .
        environment:
          - OPENAI_API_KEY=${OPENAI_API_KEY}
        restart: unless-stopped
        volumes:
          - ./data:/app/data
        deploy:
          resources:
            limits:
              memory: 512M
              cpus: "1.0"

    Deploy it with:

    docker compose up -d --build
    docker compose logs -f ai-agent

    The restart: unless-stopped policy matters for agents that call external APIs — a transient network blip shouldn’t require a manual restart. See the official Docker documentation for the full compose spec if you need GPU passthrough or multi-service setups.

    Choosing a VPS for Your Agent Workloads

    An agent that only calls hosted LLM APIs is CPU-light — you’re mostly waiting on network I/O, not doing local inference. A 2-vCPU / 4GB instance is plenty for most single-agent workloads. If you’re running a fleet of agents or doing any local embedding generation, size up. We’ve had good results running agent workloads on DigitalOcean droplets for their predictable pricing and one-click Docker images, and on Hetzner when the priority is raw compute per dollar for heavier batch jobs. For a deeper comparison of specs and pricing, see our VPS hosting guide for Docker workloads.

    Monitoring, Logging, and Securing Your Agent

    An agent that silently fails is worse than one that crashes loudly — you want to know the moment it stops converging or starts burning through API spend. At minimum, ship structured logs (JSON, one line per reasoning step) to a log aggregator, and set up uptime and error-rate alerting with a service like BetterStack so you get paged before a runaway loop drains your API budget. Our Linux server monitoring tools roundup covers self-hosted alternatives if you’d rather not depend on a third-party SaaS.

    Security deserves equal attention. Treat every tool call as a potential injection point — a malicious or malformed tool output can trick the model into taking an unintended action, a class of vulnerability documented in the OWASP Top 10 for LLM Applications. Practical mitigations:

  • Run shell-executing tools inside a restricted container or namespace, never on the host directly.
  • Strip or escape any tool output before it’s re-injected into a prompt that will trigger further tool calls.
  • Rotate API keys regularly and store them in a secrets manager, not in your compose file.
  • Rate-limit and cap spend per agent run at the provider level, not just in your own code.
  • Common Pitfalls When Creating an AI Agent

    Most failed agent projects share the same handful of mistakes:

  • No step limit, leading to infinite loops and runaway API bills.
  • Overly broad tool permissions — giving the agent unrestricted shell access instead of a narrow, purpose-built function.
  • Treating the model’s output as trustworthy JSON without validating schema before executing it.
  • Skipping observability until something breaks in production, by which point you have no logs to debug with.
  • Choosing a framework before understanding the loop, which makes debugging opaque failures much harder.
  • Start with the raw API call, get the loop working, and only add a framework once you understand what it’s abstracting away.

    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

    Q: Do I need a specialized framework for creating an AI agent, or can I build one from scratch?
    A: You can build a working agent with nothing but the raw model API and about 50 lines of Python, as shown above. Frameworks like LangChain or CrewAI become useful once you need multi-agent coordination, built-in retry logic, or a large library of pre-built tool integrations.

    Q: What’s the cheapest way to run an AI agent in production?
    A: Since most agents are I/O-bound waiting on API responses, a small 2-vCPU VPS running the agent in Docker is usually sufficient, and your main cost driver will be LLM API tokens, not compute. Cap your step limit and add spend alerts to keep token costs predictable.

    Q: How do I stop my agent from getting stuck in a loop?
    A: Enforce a hard max_steps limit in your control loop, and add a secondary timeout at the process level. If the agent hasn’t produced a FINAL: response by the step limit, return a fallback message instead of retrying indefinitely.

    Q: Can an AI agent run entirely offline with a local model?
    A: Yes — tools like Ollama let you run open-weight models locally, and the same perceive-reason-act loop applies. You’ll trade API costs for local compute requirements, so budget for more RAM and CPU (or a GPU) on your host.

    Q: How is an AI agent different from a Zapier or n8n automation?
    A: Automation tools execute a fixed workflow you design ahead of time. An AI agent decides its own sequence of steps at runtime based on the model’s reasoning, which makes it more flexible but also less predictable — you need stronger guardrails and logging as a result.

    Q: Is it safe to give an AI agent access to my production servers?
    A: Only with strict guardrails: a sandboxed execution environment, an allow-list of permitted commands, and full audit logging of every tool call. Never point an agent’s shell tool directly at a production host without these controls in place.

    Creating an AI agent that’s actually reliable in production comes down to treating it like any other service: bound its resource usage, log everything, containerize it, and monitor it the same way you would a web server. The reasoning loop is the interesting part, but the boring infrastructure work — Docker, deployment, logging, alerting — is what determines whether it survives contact with real traffic.

    Comments

    Leave a Reply

    Your email address will not be published. Required fields are marked *