LLM Agents in Production – What Changed in 2025
The model got smarter, but the reason agents work in production in 2025 is the harness around them. We run them in customer-support triage, internal ops, and incident summarization, and the lessons are consistent: agents fail through runaway loops, tool misuse, cost blowups, and confidently-wrong answers — and all four are harness problems, not model problems. This is the shape of what works, and the shape of what still doesn’t.
The loop and its failure modes
An agent is an LLM plus tools plus memory plus a loop. The loop looks simple: model emits a tool call, we execute it, append the result, and go again until the model says it’s done. The failure modes are where the engineering actually lives:
- Runaway loops. The model decides it needs one more lookup, then one more, then repeats one it already did. Unbounded, an agent on a moderately ambiguous task will happily burn 40 steps and 30k tokens.
- Tool misuse. Right tool, wrong arguments — often the model passing the summary it just read instead of the ID it needs to fetch details.
- Cost blowups. The 11th retry on the same tool call is pure spend.
- Silent wrongness. The agent completes, claims success, and the result is subtly wrong in exactly the place nobody checks.
Every fix below is aimed at those four.
The budgeted loop
Budgets are the cheapest guardrail we shipped and the one with the biggest effect. Step cap, token cap, wall-clock cap, and a per-tool timeout — all of them, always.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AgentBudget:
def __init__(self, max_steps=15, max_tokens=12000):
self.max_steps, self.max_tokens = max_steps, max_tokens
self.steps = self.tokens = 0
def run_agent(model, task, tools, budget):
messages = [{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": task}]
while budget.steps < budget.max_steps and budget.tokens < budget.max_tokens:
msg = model.call(messages, max_tokens=min(2048, budget.max_tokens - budget.tokens))
budget.tokens += msg.usage
budget.steps += 1
messages.append(msg)
if msg.tool_calls:
for tc in msg.tool_calls:
budget.tokens += execute_with_timeout(tools[tc.name], tc.args)
messages.append({"role": "tool", "name": tc.name,
"content": last_result})
continue
if msg.is_final:
return messages
raise RuntimeError(f"budget exceeded: steps={budget.steps} tokens={budget.tokens}")
Two details that mattered: the tool result appended is the truncated, summarized result (we cap at ~800 tokens and strip noisy logs — agents read what they’re given, and a 5k-token tool dump hurts them), and a repeated identical tool call within a session is detected and blocked. The no-op-call detector alone cut median run cost by a third.
Sandboxing and the human gate
Tools are read-only by default. Anything that mutates state — deploying, deleting, editing production records — requires an explicit approval step; the agent presents a plan and waits. It costs latency on those few calls and buys us a clean audit trail on every state-changing action. Exec tools run in a sandboxed container with an egress allow-list: the agent can reach the app’s internal APIs but cannot arbitrarily fetch from the internet. This is non-negotiable — an agent with curl and no egress rules is a RCE with a chat interface.
Memory, in three tiers
Most “memory” complaints are actually context-management failures. We use three tiers and reach for them in that order:
- Context. Keep it short. Summarize or drop old tool results once the conversation grows; the budgeted loop forces this to happen.
- RAG. Facts from documentation, pulled on demand. This is not memory, it’s lookup, but agents treat it as memory and it works.
- Long-term episodic store. The agent’s own past actions, keyed by user and session, written after each run. This is genuinely useful for “what did we try last time” — and genuinely the last thing most teams should build. Tier 1 discipline removes most of the demand for tier 3.
Observability and evaluation
Every tool call is logged with arguments, result, token counts, and per-step latency, all tied to a trace ID. Our support agent averages 11.2 steps and ~8,400 tokens per run with a p90 of 21 steps; those numbers are the dashboard the whole team watches, because a shift in steps-per-run is a canary for prompt drift long before quality reports show it.
Answer-level evaluation misses agent bugs, so we grade trajectories: golden traces plus an LLM-judge that scores whether the agent took the right path — right tools, right order, no wasted steps — with a safety check on every step. We keep a replay harness that re-runs trajectories deterministically against recorded tool outputs, so a regression in the harness shows up in CI, not in a customer ticket.
When not to build an agent
An agent is a product decision, not a sophistication contest. For “look it up and answer,” a single-shot RAG prompt beats an agent on cost, latency, and predictability. For a two-step conditional flow, a script beats an agent. Agents earn their complexity on multi-step workflows with branching and tool dependencies — and even then, start with a prompt, then a script, then an agent. We shipped two agent systems in 2025 and deleted one of them after realizing a 40-line script did the same job at one-twentieth the cost. The other one earns its keep because its tool calls genuinely branch.