Engineering

How AI agents actually work (and why most fail)

March 20264 min read

Everyone is building AI agents. Startups pitch them. Enterprises budget for them. Twitter threads explain them in 12 steps. But most agents deployed to production fail in ways their creators didn't anticipate, and for reasons that have nothing to do with the underlying model's intelligence.

This post breaks down what an AI agent actually is under the hood, why the dominant architecture creates specific failure modes, and what patterns we've found that actually survive contact with real users.

What an agent actually is

Strip away the marketing and an AI agent is a loop. It is not a single prompt-response pair. It is a loop that runs until a goal is met or a limit is hit. The agent plans what to do, does it, looks at what happened, and decides whether to keep going.

That's it. Four steps, repeated. Every agent framework (LangChain, CrewAI, AutoGen, or custom) implements some version of this cycle.

Interactive. Click each step

The simplest possible agent looks like this:

typescriptagent-loop.ts
1
async function agentLoop(task: string) {
2
const memory: Message[] = [];
3
4
for (let step = 0; step < MAX_STEPS; step++) {
5
  // Plan: ask the model what to do next
6
  const action = await llm.decide(task, memory);
7
8
  if (action.type === "finish") {
9
    return action.result;
10
  }
11
12
  // Act: execute the chosen tool
13
  const result = await tools.execute(action);
14
15
  // Observe: feed the result back into memory
16
  memory.push({ role: "tool", content: result });
17
18
  // Decide: the next loop iteration re-evaluates
19
}
20
21
throw new Error("Agent exceeded step limit");
22
}

This looks simple because it is. The complexity isn't in the loop. It's in everything that goes wrong inside it.

Where agents fail

We've shipped agents in production across three products. Here are the failure modes that actually matter, ranked by how often they bite you.

1. Context window overflow

Every iteration of the loop adds to the conversation history. Tool results are often verbose: a database query returns 200 rows, an API returns nested JSON, a web scrape returns a full page. By step 8 or 10, the agent is spending most of its context window on historical observations instead of reasoning about the current step.

2. Error cascades

When an agent makes a wrong decision on step 3, it doesn't just fail on step 3. It fails on steps 4 through 15 because every subsequent plan is built on a corrupted history. The model sees its own wrong action as context and builds on it. One hallucinated function call compounds into a chain of increasingly confused reasoning.

This is the agent equivalent of a snowball rolling downhill. By the time you detect the problem, the agent has wasted 90% of its budget on the wrong path.

3. The grounding problem

Agents hallucinate tool calls. Not the well-known "making up facts" kind of hallucination. Instead, they invent actions that don't exist. The model calls database.queryAll() when the actual tool is db.find(query). It passes { format: "csv" } to a function that expects { outputType: "csv" }.

4. Evaluation hell

How do you test a system whose output is non-deterministic and whose execution path varies on every run? You can't unit test an agent the way you unit test a function. The same input can produce different, equally valid action sequences.

Most teams skip evaluation entirely and ship based on vibes. This works until it doesn't.

The best agents fail gracefully. The worst ones fail confidently.

What actually works

After hundreds of iterations, here are the patterns that consistently improve agent reliability in production.

Constrained action spaces

Don't give an agent 40 tools. Give it the 4-6 it needs for the current task. The fewer options the model has to choose from, the more likely it picks the right one. We dynamically adjust the available tool set based on the current step and context.

typescripttool-selection.ts
1
function getToolsForStep(step: AgentStep): Tool[] {
2
// Early steps: research tools only
3
if (step.phase === "research") {
4
  return [tools.search, tools.read, tools.summarize];
5
}
6
// Writing phase: creation tools
7
if (step.phase === "create") {
8
  return [tools.write, tools.edit, tools.validate];
9
}
10
// Review: only approval or rejection
11
return [tools.approve, tools.reject, tools.revise];
12
}

Human-in-the-loop checkpoints

Not every step needs human approval, as that defeats the purpose. But high-stakes decision points should pause for confirmation. The agent runs autonomously through research and drafting, but stops before publishing, sending, or deleting. This catches cascading errors before they become irreversible.

Memory architecture

The conversation history isn't enough. Production agents need at least two layers of memory:

  • Working memory, which is the current loop context, aggressively summarized between steps
  • Long-term memory, which stores facts, preferences, and past decisions that persist across sessions. This is where vector databases actually earn their keep.

Multi-agent delegation

One agent doing everything is like one person doing every job in a company. It works for simple tasks. For complex workflows, break the work into specialized agents that communicate through structured handoffs. A planner agent delegates to a researcher, a writer, and a reviewer. Each has a narrow focus and a small tool set.

This is the architecture behind our content operations system, and it's the subject of our next post.


Agents aren't magic. They're loops with tool access and the judgment of a language model. The teams that succeed with them are the ones that design for failure: constrained tools, aggressive summarization, human checkpoints at the right moments, and evaluation pipelines that catch regressions before users do.

The loop is simple. Making it reliable is the hard part.