Site icon Farid Fadaie

LLM Observability for Voice Agents: Writing Code an AI Can Debug

Magnifying glass over colorful data lines flowing toward a smartphone with a microphone icon, symbolizing voice search or data analysis on mobile.

Voice-agent bugs don’t reproduce. The call that failed existed once, in real time, shaped by a caller who will never say those words that way again, a speech model that will never phrase its reply the same way twice, and a transcription layer that misheard one syllable at exactly the wrong moment. You cannot attach a debugger to a conversation that no longer exists.

So the discipline that matters isn’t reproduction — it’s reconstruction: when a call goes wrong, the logs alone must let an engineer rebuild it turn by turn and name the root cause down to the file and line. This post covers the observability practices that made that routine for a production voice agent, and the coding patterns that follow once you accept the second premise: increasingly, the engineer doing that reconstruction is an LLM. Your code now has two audiences, and the second one will be asked to fix a bug at 2 a.m. with nothing but the repository and a log stream.

Log decisions, not events

Standard logging records what happened: request in, response out. Voice agents fail in the gaps between events — in the decisions: a recovery timer that chose to fire while the model was mid-answer, a gate that approved a hangup, an extraction that silently declined to write a field. The shift that changed everything for me: one structured, greppable line per consequential decision, carrying the verdict and the inputs it was based on.

Not “call ended.” Instead: which path requested the end, what the gate saw (fields collected, what was missing, who was speaking), and what it ruled. When a required answer went missing from a record, the decision trail showed me the exact mechanism in minutes: the model acknowledged the answer without recording it, the recovery deferred four times while audio drained, and a completion path ended the call on an unverified claim. Three subsystems, one greppable timeline, no guesswork — and the same trail later proved a different bug was not in the dialog logic at all but in transcript bookkeeping.

Two constraints make this sustainable. Privacy: log field names, lengths, booleans, verdicts — never values; “recorded field X on turn 3” debugs as well as the value itself and can live in production logs safely. Naming: stable machine-parseable keys (source=, reason=, turn=, verdict=) so the log stream is queryable by a human with grep or by an LLM with a prompt.

The trust boundary

Everything an LLM asserts crosses a verification boundary before it touches state.

The most expensive bug class in LLM-integrated systems is believed claims. My canonical example: an internal result object carried the reason string “caller farewell after required fields collected,” and a code path ended calls on the strength of that sentence — while a sibling code path could plainly see a required field missing. The reason string was a claim. Nothing verified it. The call hung up with the data unrecorded.

The rules that fixed it generalize to any system where model outputs touch state:

Prompts and tool schemas are code

In a tool-driven agent, the model’s behavior is programmed by three text surfaces: the system instructions, the tool descriptions, and — most underrated — the tool result messages, which steer the model’s next move (“recorded; ask for the next missing field: X”). These strings are load-bearing. A one-sentence instruction that told the model to use one language “for the remainder of this call” caused it to answer a later switch-back request in the wrong language; one sentence, reviewed like prose instead of like code, shipped a bug. Treat steering text as an API surface: versioned, reviewed for contradictions across surfaces (two instructions disagreeing about whether to confirm a number is a race condition written in English), and covered by tests that assert the contracts — “no surface tells the model to ask for confirmation” — rather than exact wording.

An anti-pattern bestiary

Recurring bugs from the intersection of async code and LLM calls — all of which I shipped, none of which I’ll ship again:

The forensic loop

Reconstruction over reproduction: every pass through the loop makes the next failure self-explanatory.

Put together, debugging becomes a loop that improves the system every pass: a failure is reported; the decision logs reconstruct the call turn by turn; the root cause gets named at file-and-line precision; the fix lands with a deterministic regression test; and — the step most teams skip — wherever the reconstruction required guesswork, a new decision log is added, so the next failure of that kind is self-explanatory. Observability isn’t a dashboard you build once; it’s a debt you pay down one missing log line at a time.

This loop is also precisely what makes the codebase maintainable by an AI. An LLM engineer can’t set breakpoints in a phone call any more than you can — but given decision-level logs with stable keys, constraint-stating comments (why this check must stay, not what the next line does), single-owner state machines instead of flag webs, and deterministic tests for every past failure, an LLM can reconstruct, diagnose, and fix voice-agent bugs from artifacts alone. I know, because that’s how most of the bugs in this series were found and fixed. Write code for the maintainer who can only see what you logged — whichever species they are.

This closes the series: part one covered the architecture — who owns the next turn — and part two covered testing with simulated callers and LLM-as-a-judge.

Exit mobile version