Available for work·Book a call
All articles/Building AI Agents That Don't Go Off the Rails
AI AgentsLLMPythonAutomation

Building AI Agents That Don't Go Off the Rails

June 20, 20264 min read

Everyone is building AI agents. The demo looks great: the model calls a tool, gets a result, calls another tool, and solves the problem. Then you put it in front of real users and it loops forever, hallucinates a tool name, or confidently does something it was never supposed to do.

Here's what actually makes agents reliable.

The core problem: agents amplify uncertainty

A single LLM call has some error rate. When you chain five calls together, that error rate compounds. A 95%-reliable single call becomes a 77%-reliable five-step chain. This isn't a model quality problem — it's math.

The implication: you can't build reliable agents by just making the model smarter. You have to design for failure at every step.

Define tight tool contracts

Every tool your agent can call should have:

  • A precise, unambiguous description (the model reads this to decide when to use it)
  • Strict input validation that returns a clear error message rather than silently misbehaving
  • A bounded output size — never return 10,000 tokens when 100 will do
@tool
def search_database(query: str, limit: int = 10) -> list[dict]:
    """
    Search the product database. Returns matching products.
    Use this only when looking up specific product information.
    Do NOT use for general knowledge questions.
    """
    if not query.strip():
        raise ValueError("Query cannot be empty")
    if limit > 50:
        raise ValueError("Limit cannot exceed 50 to prevent context overflow")
    return db.search(query, limit=limit)

Vague tool descriptions are the number-one cause of agents calling the wrong tool.

Build a control loop, not a free-run loop

Most agent frameworks let the model run until it decides it's done. That's dangerous. Build your own loop with explicit guardrails:

MAX_STEPS = 15
TIMEOUT_SECONDS = 60

for step in range(MAX_STEPS):
    action = model.decide_next_action(context)
    
    if action.type == "finish":
        return action.result
    
    if action.type == "tool_call":
        result = execute_tool(action.tool, action.args)
        context.append(result)
    
    # Never let the model run forever
raise AgentTimeoutError(f"Agent did not complete in {MAX_STEPS} steps")

A timeout that surfaces a clear error is always better than an agent that spins.

Make the agent explain its plan first

Before executing anything, have the model produce a brief plan. This serves two purposes:

  1. It catches obviously wrong reasoning before any tools run
  2. It gives you a log for debugging when things go wrong
SYSTEM: Before taking any action, state your plan in 2-3 sentences.
Then execute one step at a time, checking the result before proceeding.

The plan step costs one extra LLM call. It saves hours of debugging.

Implement human-in-the-loop for destructive actions

Not every action should be autonomous. Any tool that writes, deletes, sends, or charges should pause for confirmation:

DESTRUCTIVE_TOOLS = {"send_email", "delete_record", "charge_card", "deploy_code"}

if action.tool in DESTRUCTIVE_TOOLS:
    confirmed = await ask_human(
        f"About to run {action.tool} with args: {action.args}. Proceed?"
    )
    if not confirmed:
        context.append("User declined. Find an alternative approach.")
        continue

The goal is automation, not unsupervised autonomy. Keep humans in the loop where it matters.

Log the full trace

When an agent fails, you need the full execution trace — every tool call, every result, every model decision. Store this in a structured format:

{
  "run_id": "abc123",
  "steps": [
    {"step": 1, "action": "search_database", "args": {"query": "..."}, "result": "...", "tokens": 312},
    {"step": 2, "action": "finish", "result": "...", "tokens": 89}
  ],
  "total_tokens": 401,
  "completed": true,
  "duration_ms": 3420
}

Without traces, debugging agents is essentially impossible.


Agents are powerful precisely because they can take many steps autonomously. That power is also the risk. Design for failure, keep humans in the loop for anything irreversible, and log everything. The agents that work in production are boring by design.