All case studies

05Deep dive · traces

Tracing an LLM agent: debugging what the model actually did

Every turn, every tool call, every piece of the prompt, saved and searchable. Bug reports stopped being 'the AI is acting weird' and started being a trace id.

In productionDavid RochaPart of the ZapBeta system
01

The problem

"The AI is being weird" isn't a bug report, and that's what I was getting. An agent that reads a message, calls a tool and decides on its own doesn't repeat the mistake when you go looking, so without a record I was always guessing. I started recording every turn: what the model saw, which tools it called, what those returned and what it replied. A complaint is now an id I open and read.

02

Constraints

  • 01Capture the decision, not the world. The full prompt is enormous; I needed enough to reproduce the turn without storing the universe on every message.
  • 02Off the hot path. Recording can't delay the reply the customer is waiting for.
  • 03Queryable afterwards. If I can't search it, recording it was pointless.
  • 04Safe to reuse. These same traces feed the public 3D office, so the write had to come out clean enough for a filtered view to be built on top of it.
03

Architecture

  1. agent runner

    every turn: the messages in, the tools called, the reply out

  2. trace writer

    written to agent_traces off the hot path, so the reply isn't delayed

  3. query + replay

    agent-trace.mjs turns a complaint into a trace id you can read

  4. reused downstream

    the same traces power the evals and the live 3D office

The trace became the piece the rest of the system runs on. The eval suite asserts against it to know what the agent did that turn, the 3D office draws from it, and the money supervisor checks the reply against the tool results it recorded. Three different consumers reading the same record.

04

The hard part

The hard call was what to keep. Record the full prompt on every turn and the table becomes a landfill I pay for every month; record too little and the one trace I need is missing the piece that explains it. I settled on the decision surface: the messages, the tool calls and what they returned, the outcome.

Then came the part I hadn't planned. Once the trace was clean enough to debug from, it was clean enough to draw from — and the same data that ended arguments about what the agent had done became the live office and the eval oracle. What was my own tool ended up as part of the product.

05

Trade-offs

  • Traces cost storage, and it grows with traffic. Recording the decision surface instead of the full prompt holds that down; a retention window holds the rest.
  • Writing on every turn is extra work on a hot path, so it happens after the reply goes out. I accept the small lag between the turn and its record.
  • A structured trace is more code to maintain than a println. It pays for itself the first time a bug report arrives as an id instead of a screenshot.
06

What I'd do differently

I'd sample instead of keeping everything. Most turns are boring and identical to the last one; what's worth keeping forever is failures and edge cases. A sampler keeping 100% of anything that tripped a guard and a small fraction of the rest would cut the cost hard without losing the traces that actually get re-read.

Let's talk

I'm open to remote work building with LLMs: agents, evals, tooling, the whole product around them. I'd be glad to show you a lot more on a call. Just reach out.

Available for remote roles, working from Brazil.Time zone: UTC−3 (Brazil)