Vibe Engines
YouTube
AI System Design

Design a Realtime Voice Agent

Step 1 / 9

Learn AI system design by building a realtime voice AI agent like ChatGPT voice mode or an AI phone agent step by step.

The numbers to beatWebRTC / SIPlive audio both waysorchestratorruns the loop + stateinterruptiblestop on barge-in

The whole design, in writing

Learn AI system design by building a realtime voice AI agent like ChatGPT voice mode or an AI phone agent step by step. An interactive guide covering why a sequential pipeline feels robotic, streaming and pipelining STT→LLM→TTS, turn-taking and barge-in, the dialog orchestrator, the end-to-end latency budget, tool calling and transport (WebRTC/SIP), and the unhappy paths (interruptions, noise, cost, end-to-end speech models).

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What makes a voice agent hard?

Talking to an AI out loud has a brutal constraint humans enforce unconsciously: if the reply doesn't start within roughly half a second to a second of you finishing, the silence feels broken — and if the agent talks over you, or won't stop when you interrupt, it feels worse than useless. Under the hood, that means running speech-to-text → LLM → text-to-speech, end to end, fast enough to feel like a conversation.

Userspeaking live
New in this step: User.

A realtime voice agent chains three heavy models into a sub-second loop. The winning moves: stream and pipeline all three stages (never run them sequentially), nail turn-taking (endpointing + barge-in), and coordinate it all with an interruptible orchestrator. Latency and turn-taking — not the models themselves — are the real design.

What the new pieces do

Userclient
A person having a spoken conversation with the agent — over the phone or the web. Expects human-like, low-latency back-and-forth.

Step 1 · Why the obvious way fails

The sequential pipeline is too slow

The naive design: wait for the user to finish, transcribe the whole utterance, send it to the LLM, wait for the full reply, synthesize the whole reply, then play it. Every box is correct. Why does the conversation feel dead?

Userspeaking live
The system as it stands at this step.

Wait-for-full-utterance → STT → LLM → TTS → play, each stage fully before the next. Why does it feel robotic?

  1. Running each stage to completion before starting the next stacks their latencies, so the user hears a long gap after every utterance. Even if each model is fast, the sum blows the conversational budget. You must overlap the stages by streaming.

  2. Accuracy isn't the issue here; latency is. Even with perfect models, doing STT then LLM then TTS strictly in sequence produces seconds of dead air per turn.

  3. Quality doesn't cause the awkward pauses. The problem is that sequential, non-streaming stages sum their latencies into an unnatural silence after every turn.

Run strictly in sequence and the latencies stack: wait for the whole utterance + full transcription + full LLM reply + full synthesis before a single sound plays. Even with fast models, the sum is seconds of silence per turn — far past the conversational budget. The fix isn't faster boxes; it's overlapping them so work in one stage begins before the previous finishes.

Why this piece earns its place

The sequential build is worth doing once, because it is the one you can debug, and it is worth knowing when to keep. Batch speech-to-text reading a whole utterance is more accurate than the partials a streaming decoder emits, and for voicemail transcription, dictation or call summarization nobody is waiting on the other end — there the simple pipeline is the right answer and overlapping is wasted complexity. What makes it a trap for realtime is that nothing about it looks broken: three green stage dashboards and one unusable product. The heavier cost is structural, and it is the one that decides a quarter rather than an afternoon. Once the seams are shaped as transcribe, then generate, then speak, each handing on a finished value, streaming is not a flag you flip — it is a rewrite of every interface between the stages, and of the error handling with them, because a stage whose output has already reached a human cannot be quietly retried. Shape the seams as streams while the pipeline is still small, or pay for it later as a migration.

Step 2 · The loop and its transport

A brain over a live audio link

Before optimizing latency, set the frame: audio must flow both ways in real time, and something must coordinate the three models and the conversation state. What are those two pieces?

TransportWebRTC / SIPOrchestratordialog manager
New in this step: Transport, Orchestrator.

A transport carries live audio both directions with minimal latency — WebRTC for the web, SIP for the phone network — streaming the mic up and playing synthesized audio down. And an orchestrator (dialog manager) sits behind it: it drives the STT→LLM→TTS loop, holds conversation state/context, and — critically — keeps the whole thing interruptible. The transport moves sound; the orchestrator runs the conversation.

Why this piece earns its place

Start with what the models actually hear, because the transport decides it and nothing downstream gets a vote. A browser hands you wideband audio; a phone leg might too, over HD voice, or might hand you a narrowband codec that has already thrown away the top of the spectrum — and the same agent degrades measurably on the narrow leg for reasons that have nothing to do with the speech-to-text model you picked. If you promise SIP, test on SIP. Operationally this tier is stranger than it looks, because a call is a pinned session and not a request. Audio flows for the whole conversation into one process holding one piece of conversation state, so you size on concurrent calls rather than requests per second, and a rollout has to drain calls instead of cycling instances — there is no retry that puts a dropped conversation back. The dial to know by name here is the jitter buffer: it trades smoothness against delay, it is sized against the network rather than against your turn budget, and a bad connection quietly buys audio quality with milliseconds you had earmarked for the models.

  • WebRTC / SIPlive audio both ways
  • orchestratorruns the loop + state
  • interruptiblestop on barge-in

What the new pieces do

Transportgateway
Carries live audio both ways with low latency (WebRTC for web, SIP for phone). Streams mic in, plays synthesized audio out.
Orchestratorservice
The brain: pipelines STT→LLM→TTS, holds conversation state, and enforces interruptibility — stop everything when the user barges in.

Step 3 · Overlap everything

Stream and pipeline the three stages

You've decided sequential is too slow. Concretely, how do you overlap STT, the LLM, and TTS so the agent starts replying almost as the user stops talking?

Orchestratordialog managerStreaming STTaudio → textStreaming TTStext → audioStreaming LLMtoken stream
New in this step: Streaming STT, Streaming TTS, Streaming LLM.

How do you overlap STT, LLM and TTS to cut per-turn latency?

  1. Smaller models help a little but the stages still run in series, still summing latencies. The structural fix is to stream partial outputs so stages overlap — pipelining, not just speed.

  2. Co-locating doesn't change that output of one stage feeds the next; without streaming they still wait for each other. You need incremental hand-off between stages.

  3. STT emits partial transcripts as the user speaks, so the LLM can begin as soon as the utterance is (semantically) complete; the LLM streams tokens; and TTS turns the first tokens into audio and starts playing while the LLM is still generating. The stages run concurrently, so end-to-end latency approaches just the first chunk of each — not their sum.

Stream every stage and pipeline them. STT emits partial transcripts live; the moment the user's turn ends, the LLM starts and streams tokens; those tokens flow into TTS, which synthesizes and plays the first audio chunk immediately while the LLM is still generating the rest. The stages run concurrently, so time-to-first-audio approaches the first chunk of each stage rather than the sum of all three. That's how the agent replies almost instantly.

Why this piece earns its place

What separates this from a demo is what you let the system do with output that is not final yet. Partial transcripts are provisional — a streaming recognizer revises text it already emitted as more audio arrives, so anything the LLM starts on a partial may have been started on the wrong sentence. Speculative starts are real and useful, and the line to hold is side effects: a speculation you can discard costs nothing but work, while a speculative turn that reaches a tool call has already changed something in the world and cannot be un-run. Speculate up to the first thing that touches a backend, and stop there. Then there is the audio queue. Synthesis runs faster than real time, so the further ahead TTS works, the more finished audio sits waiting to be played. Deep buffering makes playback smooth and makes an interruption expensive, because everything queued has to be discarded and accounted for. Shallow buffering makes barge-in cheap and exposes every synthesis hiccup as a gap mid-sentence. That one number is the clearest case on this page of a choice made here and paid for three steps later.

  • STT partialsfeed LLM early
  • LLM tokensstream to TTS
  • first audioplays while generating

What the new pieces do

Streaming STTservice
Transcribes the user's speech incrementally, emitting partial text so the LLM can start thinking before the sentence ends.
Streaming TTSservice
Synthesizes the reply in chunks and streams the first audio out immediately, so the agent starts speaking fast.
Streaming LLMservice
Generates the reply token-by-token; its streamed tokens feed TTS as they arrive, overlapping thinking with speaking.

Step 4 · Knowing when to talk

Turn-taking: endpointing & barge-in

Humans hand off turns effortlessly; machines don't. The agent must know when the user has finished speaking (so it replies at the right moment, not mid-sentence or after an awkward pause) and must stop instantly when the user interrupts. How?

TransportWebRTC / SIPTurn-takingendpoint · barge-inOrchestratorinterruptibleStreaming STTaudio → text
New in this step: Turn-taking.

How does the agent know when to start replying, and stop when interrupted?

  1. Endpointing decides the user's turn is over — silence via VAD, refined by whether the utterance is semantically complete — so the agent replies at the right moment. Barge-in detects the user talking while the agent speaks and immediately halts TTS and re-listens. Together they create natural turn-taking.

  2. A fixed timeout is either too slow (dead air after short answers) or cuts people off mid-thought, and it ignores interruptions entirely. Turn-taking must be adaptive (VAD + semantic endpointing) and support barge-in.

  3. Push-to-talk defeats the point of a natural voice conversation. A good agent handles turn-taking automatically, including being interrupted.

Solve turn-taking explicitly. Endpointing decides the user's turn is over — voice-activity detection catches the silence, refined by whether the utterance is semantically complete — so the agent replies promptly without cutting the user off. Barge-in detects the user speaking while the agent is talking and immediately stops TTS, flushing the pending reply and re-listening. This interruptibility is what makes the agent feel like a person, not a recording.

Why this piece earns its place

The part that bites is that not all user speech is an interruption. People say mhm, right, yeah while they are listening — backchannels, the noises that mean keep going — and a detector that fires on voice activity alone stops the agent dead every time the user agrees with it. You want at least a duration floor, and better, a look at what was actually said, before tearing a turn down. The two ways endpointing gets it wrong are also not symmetric, which is worth saying out loud. Waiting a beat too long costs a beat. Cutting in early costs the rest of the sentence, makes the user say it again, and leaves the orchestrator holding half an utterance that it will answer as though it were whole. So bias the threshold toward waiting, then buy the wait back per turn — because the agent knows what it just asked. After a request for an account number, people pause between digit groups and you should be patient; after a yes-or-no question, a short silence is already the entire answer. Make the wait a value the dialog sets on the turn rather than a constant in the config, and a good share of perceived lag disappears without touching a model.

  • endpointingVAD + semantics
  • barge-instop TTS on interrupt
  • re-listenflush pending reply

What the new pieces do

Turn-takingservice
Detects when the user stops speaking (endpointing) and when they interrupt (barge-in), so the agent knows when to speak and when to stop.

Step 5 · The latency budget

Where the milliseconds go

"Under a second" is a budget you have to spend across STT, LLM first token, and TTS first audio — plus network. If any stage blows its slice, the whole turn feels laggy. How do you make it fit?

UserTransportTurn-takingOrchestratorStreaming STTStreaming TTSStreaming LLM
The system as it stands at this step. · swipe to pan the diagram

Break the end-to-end budget into slices and optimize each: STT partial/final latency, LLM time-to-first-token (the usual biggest chunk — use a fast model, short prompts, prefix/KV caching, low-latency serving), TTS time-to-first-audio, plus network/transport. Because you pipeline, the turn latency is dominated by the LLM's first token plus the first TTS chunk — so you attack those hardest. Measure the whole loop, not each box, and keep a tight budget (often targeting a few hundred ms of perceived response start).

Why this piece earns its place

First token is the right shorthand above and not quite the mechanism. What gates audio is the first speakable unit: handed a single token, a synthesizer has no phrase to place stress or intonation on, so low-latency voices buffer to a clause or run a little lookahead, trading a slice of prosody for every millisecond they shave. Treat the gate as the opening clause and the shape of the first sentence becomes a latency decision — a reply that starts with a short acknowledgement makes sound sooner than one that starts with a long qualifying clause, at no cost to the answer. That is the cheapest millisecond on this page. The other thing to hold onto is that a budget written as one number is a median, and nobody experiences a conversation at the median. Three model calls per turn means the turn's tail is the worst of three tails rather than the average of them, so the spread is wider than any single stage suggests, and the one bad turn is the one a user recounts afterwards. Defend the budget at a high percentile, and treat a tail that moves while the median holds as a real regression.

  • budget~500ms–1s end-to-end
  • LLM TTFTusually the biggest slice
  • measurethe whole loop

Step 6 · Interruptible by design

The orchestrator holds it together

Streaming three models concurrently, mid-turn interruptions, tool calls, and conversation state make this genuinely stateful and racy. What does the orchestrator actually manage?

UserTransportTurn-takingOrchestratorStreaming STTStreaming TTSStreaming LLM
The system as it stands at this step. · swipe to pan the diagram

The orchestrator is the stateful conductor. It manages the conversation context (history, system prompt, user/session memory), sequences the streaming pipeline, and enforces interruptibility: on barge-in it must cancel in-flight LLM generation and TTS playback cleanly, keep only what was actually spoken as context, and re-listen. It handles turn state (listening / thinking / speaking), coordinates tool calls (pausing the voice while a function runs, or speaking a filler), and recovers from stage errors — all without dropping the thread of the conversation.

Why this piece earns its place

Cancellation carries one subtlety that decides whether the transcript is even true. What was spoken is not what the synthesizer emitted — audio sits in the send path and in the listener's playback buffer, so at the instant of a barge-in the user has heard strictly less than the server produced. The cut point therefore has to come back from the playback clock: the client reports how far it actually got, and the orchestrator maps that position back onto the text it queued. That reporting path is real engineering, and it is the part people leave out of the design — without it the orchestrator has only a server-side count of what it generated, which is the wrong number. Interruption is not free either. A cancelled generation still bills the tokens it produced, and truncating the last assistant turn invalidates the cached prefix from that turn onward — the system prompt and every earlier exchange still match and are still reused, so what gets re-prefilled is the shortened tail plus the new user turn. On discarded output alone, a heavily interrupted call costs more per turn than a polite one, and that is the call your unhappiest user is having.

  • statelistening/thinking/speaking
  • cancelclean barge-in teardown
  • contextonly what was really said

Step 7 · Doing things, not just talking

Tools, integration & transport

A useful voice agent doesn't just chat — it looks up your order, books the appointment, checks the balance. And it lives on real channels (a phone line, a web widget). How does action and integration fit into a latency-sensitive loop?

Streaming LLMtoken streamToolsfunctions · data
New in this step: Tools.

Give the LLM tools (function calling) to query backends and take actions mid-conversation. Because a tool call adds latency, the orchestrator masks it: speak a natural filler ("let me check that…") or acknowledge while the function runs, then continue with the result — so the pause feels intentional. Integrate with telephony (SIP) for phone agents and WebRTC for web, and connect to knowledge (RAG) and memory for continuity. The agent becomes a spoken interface to real systems, not just a talking LLM.

Why this piece earns its place

Voice changes the contract a tool has to meet. There is no screen, so a function that returns ten options or a page of structured data forces the model to compress it under time pressure, and what it drops is whatever it judged least interesting, which is not the same as least important. Shape voice tools to return few fields, already narratable. The sharper issue is that speech has no confirmation dialog and no undo. Recognition mishears exactly the values that matter — fifteen and fifty, B and D, one digit inside an account number — so anything that moves money or changes state should read the parsed arguments back and wait for an explicit yes. That round trip, not the function call, is where the real latency of an action goes, and it is the one spend here worth defending. Last, the filler is a promise. Once the agent has said it is checking, silence stops being neutral, so every tool call needs a deadline and a line to say when it expires.

  • toolslook up / act
  • fillermask tool latency
  • SIP / WebRTCphone + web

What the new pieces do

Toolsdata
Function calls the agent can make mid-conversation — look up an order, book a slot, query a backend — grounding the dialog in real actions.

Step 8 · The sharp edges

Noise, overlap, cost & end-to-end models

Live conversation is messy: background noise and cross-talk confuse endpointing, interruptions race with generation, three model calls per turn cost money and can each fail, and a new class of models threatens to collapse the whole pipeline.

UserTransportTurn-takingOrchestratorStreaming STTStreaming TTSStreaming LLMTools
The system as it stands at this step. · swipe to pan the diagram

Handle noise/cross-talk with robust VAD, echo cancellation, and denoising so the agent doesn't mistake noise for speech (or barge-in). Make interruptions race-safe with clean cancellation. Control cost (three inferences per turn) with right-sized models, caching, and ending calls gracefully. Build in error recovery — if a stage fails, apologize/retry rather than freeze. And watch the frontier: end-to-end speech-to-speech models (audio in, audio out, no separate STT/LLM/TTS) can cut latency and preserve tone/emotion — a possible replacement for the pipeline, with less modular control.

Why this piece earns its place

The trade nobody names when speech-to-speech comes up is observability. In this pipeline the transcript is a by-product you get for free, and almost everything downstream leans on it: quality review, offline evaluation, compliance recording, analytics, and the whole story for debugging a bad turn, where you can point at text and say the transcription was fine, the reply drifted. A model that takes audio in and returns audio out removes that seam, and bolting a transcription pass beside it does not hand the seam back. That pass runs off the conversational path, so what it costs is money and a second system to keep aligned rather than time-to-first-audio — but what it returns is a record of what was said, not of what the model was reasoning over. You can still see the bad turn and still not know which half of it went wrong. One correction to the cost line above, too: right-sizing the three models is a win you collect once, while the bill that keeps growing is context. Each turn re-sends the conversation so far, so a late exchange in a long call costs far more than the first, which makes the orchestrator's summarization policy the lever that keeps paying.

You did it

You just designed a realtime voice agent.

UserTransportTurn-takingOrchestratorStreaming STTStreaming TTSStreaming LLMTools
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • A sequential STT→LLM→TTS pipeline sums latencies into seconds of dead air — it feels robotic.
  • A low-latency transport (WebRTC/SIP) + a stateful orchestrator frame the real-time loop.
  • Stream and pipeline all three stages so they run concurrently — time-to-first-audio, not the sum.
  • Turn-taking is explicit: endpointing (when the user finished) + barge-in (stop instantly on interrupt).
  • Spend the latency budget on LLM first-token and TTS first-chunk — the two that gate the reply start.
  • The orchestrator manages state and clean cancellation so barge-in never corrupts the conversation.
  • Tools (with filler to mask latency) make it act; watch end-to-end speech-to-speech models emerging.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why does a straightforward STT→LLM→TTS pipeline feel robotic, and what actually fixes it?

    Because running the three stages strictly one after another stacks their latencies into a long silence after every user turn. If you wait for the user to finish, then fully transcribe the utterance, then send the complete text to the LLM and wait for its entire reply, then synthesize that whole reply, and only then start playing audio, the user hears nothing until the sum of all those stage latencies has elapsed — which, even with individually fast models, is easily multiple seconds per turn, far past the roughly half-second-to-second window that makes conversation feel natural. The fix isn't primarily faster models; it's overlapping the stages by streaming so they run concurrently instead of in series. Streaming STT emits partial transcripts as the user speaks so the LLM can start the moment the turn ends; the LLM streams its reply token by token; and those tokens feed streaming TTS, which synthesizes and begins playing the first chunk of audio while the LLM is still generating the rest. With this pipelining, perceived latency drops to roughly the first chunk of each stage plus small overlaps rather than the total of all three, which is what lets the agent start replying almost immediately.

  2. What is turn-taking, and why does it matter more than model quality for feeling human?

    Turn-taking is the set of behaviors humans use unconsciously to coordinate who is speaking: knowing when the other person has finished so you can respond at the right moment, and stopping immediately when they start talking over you. For a voice agent these have to be made explicit, and getting them right dominates the perception of naturalness. Endpointing is deciding the user's turn is over — detecting the silence with voice-activity detection but refining it with whether the utterance is semantically complete, so a thoughtful mid-sentence pause isn't mistaken for the end (which would cut the user off) and a finished short answer isn't followed by an awkward gap. Barge-in is detecting that the user has started speaking while the agent is still talking and immediately halting TTS playback and the pending reply, then re-listening. Even a brilliant LLM feels broken if the agent talks over you, won't stop when you interrupt, or leaves long dead pauses; conversely, a modest model with crisp turn-taking feels remarkably human. That's why endpointing and barge-in are first-class concerns in the design rather than afterthoughts.

  3. How do you fit a whole STT→LLM→TTS turn under a ~1 second budget?

    You treat the end-to-end budget as something to spend across the stages and optimize the parts that actually gate the start of the reply. Because the pipeline overlaps stages, what the user perceives is time-to-first-audio — how quickly the agent starts speaking — which is roughly the LLM's time-to-first-token plus the TTS first-chunk latency, since STT largely overlaps with the user still finishing their sentence. The LLM's first-token latency is usually the biggest slice, so you attack it hardest: use a fast model, keep prompts short, exploit prefix/KV caching, and run on low-latency serving infrastructure. TTS first-chunk latency is minimized with streaming synthesis and a low-latency voice model. You also account for network and transport latency (which is why you use a real-time UDP-based transport like WebRTC or SIP rather than request/response HTTP). Crucially, you measure the whole loop as the user experiences it, not each box in isolation, because the interactions and overlaps are what determine perceived responsiveness — and you keep a tight budget, often targeting only a few hundred milliseconds before the reply audio begins.

  4. Why is clean cancellation on barge-in a core responsibility of the orchestrator?

    Because at the moment a user interrupts, multiple stages are mid-flight — the LLM may be halfway through generating a reply and the TTS may be synthesizing or already playing audio — and simply "stopping" is not enough; you have to tear all of that down consistently and update the conversation state to reflect reality. If the orchestrator cancels sloppily, the agent can end up with context that includes text it generated but never actually spoke, so on the next turn it "remembers" saying things the user never heard, producing confusing, incoherent dialogue. So on barge-in the orchestrator must immediately halt TTS playback, cancel the in-flight LLM generation, discard the queued and half-generated output, and retain as context only what was truly spoken to the user before the interruption, then transition back to listening. This is genuinely stateful and racy — audio playback, token generation, and user speech detection are all happening concurrently — which is why interruptibility and clean cancellation are treated as a central design concern owned by the orchestrator rather than something bolted on, and why the orchestrator explicitly tracks turn state (listening, thinking, speaking) to manage the transitions correctly.

  5. How does the agent take real actions without breaking the conversational flow, and what might replace this whole pipeline?

    A genuinely useful voice agent needs to do things — look up an order, check a balance, book an appointment — which is enabled by giving the LLM tools via function calling, so mid-conversation it can invoke a backend and use the result. The challenge is that a tool call adds latency that would otherwise show up as dead air, so the orchestrator masks it the way a human would: it speaks a natural filler or acknowledgment ("let me check that for you...") while the function runs, then continues with the result, making the pause feel intentional rather than like a freeze. Integration also means living on real channels — SIP for telephone agents, WebRTC for web — and connecting to knowledge via retrieval and to memory for continuity across turns and sessions, which turns the agent into a spoken interface to real systems rather than just a talking model. Looking forward, the entire three-model pipeline may be replaced by end-to-end speech-to-speech models that take audio in and produce audio out directly, without separate STT, LLM, and TTS stages; these promise lower latency and better preservation of tone and emotion (which the pipeline tends to flatten), at the cost of less modular control over each stage — so the STT→LLM→TTS architecture is the current best engineering trade-off, not necessarily the permanent one.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. A sequential STT→LLM→TTS pipeline feels robotic because…

    Running stages in series stacks their latencies; you must pipeline (stream) to overlap them.

  2. Streaming and pipelining the stages means…

    Concurrent stages make latency ≈ first-chunk of each, not STT+LLM+TTS end-to-end.

  3. Turn-taking requires…

    VAD + semantic endpointing decide when to speak; barge-in halts TTS when interrupted.

  4. With pipelining, the latency you optimize hardest is…

    What the user feels is time-to-first-audio; attack the first token and first audio chunk.

  5. On barge-in, the orchestrator must…

    Sloppy cancellation makes the agent "remember" replies it never actually said.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Converse by voice: spoken back-and-forth over the phone or web, human-like and low-latency.
  • Understand: transcribe the user’s speech to text (STT) as they talk.
  • Reply: an LLM generates the response and streams it to speech (TTS).
  • Turn-taking: reply when the user finishes and stop instantly when they interrupt.
  • Act: call tools mid-conversation to look things up or take actions.

The qualities that shape everything

Each one names the mechanism that buys it.

Sub-second time-to-first-audio
Stream and pipeline STT→LLM→TTS so the stages run concurrently — the agent starts speaking on the first chunk, not after the sum of all three.
Feel human, not robotic
Explicit turn-taking: endpointing (VAD refined by semantic completeness) decides when the user finished, and barge-in stops TTS the instant they interrupt.
Real-time audio both ways
A UDP-based transport (WebRTC for web, SIP for phone) streams the mic up and plays synthesized audio down — like a call, not request/response HTTP.
Fit the latency budget
Because stages overlap, the user feels LLM time-to-first-token + TTS first-chunk, so you attack those slices — fast model, short prompts, KV caching, low-latency serving.
Interruptions never corrupt context
The orchestrator cleanly cancels in-flight LLM generation and TTS playback on barge-in and keeps only what was actually spoken as context.
Do things, not just talk
The LLM calls tools mid-conversation, and the orchestrator masks the added latency with a natural filler so the pause feels intentional.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Stream and pipeline the stages over running STT→LLM→TTS sequentially

Running each stage to completion before the next stacks their latencies into seconds of dead air per turn — even with fast models. Streaming partial output overlaps the stages so time-to-first-audio approaches the first chunk of each, not the sum of all three.

Adaptive endpointing + barge-in over a fixed pause timeout or push-to-talk

A fixed timeout is either too slow (dead air after short answers) or cuts people off mid-thought, and it ignores interruptions entirely. VAD refined by semantic completeness decides when the turn really ended, and barge-in halts TTS when the user jumps in.

A UDP-based real-time transport (WebRTC/SIP) over request/response HTTP

Live audio must flow both ways continuously with minimal latency, like a phone call. Request/response HTTP can’t carry a streaming, interruptible conversation — you need a real-time transport under the loop.

A modular STT→LLM→TTS pipeline over a unified end-to-end speech-to-speech model

End-to-end speech-to-speech models can cut latency and preserve tone/emotion, but give up modular control over each stage. The pipeline is the current best trade — steerable, debuggable stages — not necessarily the permanent one.

The answer, out loud

What a strong answer to “Design a Realtime Voice Agent” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0-4 min

    Start with the constraint, not the boxes

    Before I draw anything I want to name the constraint, because it is the whole design. If the reply does not start within about half a second to a second of me finishing, the silence feels broken, and if the agent talks over me, or will not stop when I interrupt, that is worse than slow. So the hard requirement is not accuracy — it is that a spoken turn comes back fast and hands the floor back cleanly. Underneath, we are chaining speech-to-text, an LLM and text-to-speech into that window: three heavy models, one conversational budget. I would say it plainly up front. Latency and turn-taking are the design here, not the models.

  2. 4-9 min

    Why the obvious pipeline feels dead

    The obvious version is one box handing off to the next. Nothing happens until I stop talking; then my whole sentence gets transcribed, the text goes to the model, we sit through the entire reply, all of it gets synthesized, and only then does a sound come out. Every box is doing its job and the conversation is dead, because those waits are serial and they add — even with quick models, seconds of nothing after every turn. So I would say early that the fix is not faster models, it is arranging them so each starts before the one in front of it has finished. I am not picking models here, I am designing the seams between them.

    Built in step 1: The sequential pipeline is too slow
  3. 9-15 min

    The frame: a live link and a conductor

    Then two pieces go up. A transport first, because sound has to move continuously both ways with almost no delay — WebRTC in a browser, SIP on a phone line. That is a live link, the way a call is a live link, and it is why request-and-response over HTTP is not an option: you cannot interrupt a request. Behind it, an orchestrator, the dialog manager. I would be careful not to call it glue, because it is the only thing on the board that remembers anything. The three models each finish and forget; what makes them add up to a conversation, and stoppable mid-sentence, is the one component holding state and driving the loop.

    Built in step 2: A brain over a live audio link
  4. 15-22 min

    Stream every stage, pipeline all of them

    Now the move the design turns on: every stage streams, and they overlap. Speech-to-text is producing text while the person is mid-sentence. The instant their turn ends the model is reading and pushing out tokens. Those go straight into synthesis, which speaks the opening of the reply while the model is still writing the end of it. Nobody waits for a stage to finish, so what the user sits through stops being the total and becomes the first piece of each. That is the difference between an agent that feels alive and one that feels like a broken phone menu, and it is structural — you cannot buy it by making the models smaller.

    Built in step 3: Stream and pipeline the three stages
  5. 22-29 min

    Turn-taking, made explicit

    Next I would solve turn-taking on purpose, because it does more for feeling human than model quality does. Endpointing decides the user's turn is over: voice-activity detection catches the silence, refined by whether what they said is semantically complete, so a thinking pause mid-sentence is not mistaken for the end. Barge-in is the other half — if the user starts speaking while the agent is talking, we stop synthesis immediately, flush the pending reply, and go back to listening. I would name the two easy answers and reject them. A fixed two-second pause is either dead air or cutting people off, and push-to-talk gives up on the thing we are building.

    Built in step 4: Turn-taking: endpointing & barge-in
  6. 29-35 min

    Spend the budget where it shows

    Then I would spend the budget out loud. Call the turn half a second to a second and break it up: transcription, the model's first token, the first chunk of audio, the network under all of it. Since the stages overlap, what a person feels is how long before sound starts — and transcription mostly happened while they were still talking, so the model and the voice gate it. First token is usually the fat slice, so that is where I would work: short prompts, reuse the prefix across turns, serving tuned for latency rather than throughput. And the number I hold the team to is one number for the turn, taken from the user's side of the microphone.

    Built in step 5: Where the milliseconds go
  7. 35-41 min

    Interruption is a state problem

    The piece I would most want to be asked about is cancellation. When somebody cuts in, the model is probably mid-reply and audio mid-playback, and halting both is not the hard part. The hard part is writing back into the context only the portion that reached their ears — less than what got generated, because some was still in flight. Get that boundary wrong and the next turn is built on a version of events the user never lived through, incoherent for a reason nothing in the logs explains. So the orchestrator tracks where the turn is and owns the teardown. Three things are live at that instant, and I would treat it as design, not error handling.

    Built in step 6: The orchestrator holds it together
  8. 41-45 min

    Tools, then the trade-off

    Last, it has to act and not just talk, so the model gets tools — look up an order, check a balance, book a slot. Calling a backend costs time the conversation does not have, so the orchestrator says something while it waits and the pause reads as deliberate rather than as a freeze. I would close on the architecture as the trade: three separate stages are steerable and debuggable a piece at a time, while a single speech-to-speech model gives that up and hands back latency plus the tone this pipeline flattens. Today's best trade, not a permanent one. With more time I would go at the ugly parts — a noisy room fooling the endpointer, three inferences billed on every turn, and what the agent says when a stage falls over.

    Built in step 7: Tools, integration & transport

What this teaches

Learn AI system design by building a realtime voice AI agent like ChatGPT voice mode or an AI phone agent step by step. An interactive guide covering why a sequential pipeline feels robotic, streaming and pipelining STT→LLM→TTS, turn-taking and barge-in, the dialog orchestrator, the end-to-end latency budget, tool calling and transport (WebRTC/SIP), and the unhappy paths (interruptions, noise, cost, end-to-end speech models).

Key takeaways

  • A sequential STT→LLM→TTS pipeline sums latencies into seconds of dead air — it feels robotic.
  • A low-latency transport (WebRTC/SIP) + a stateful orchestrator frame the real-time loop.
  • Stream and pipeline all three stages so they run concurrently — time-to-first-audio, not the sum.
  • Turn-taking is explicit: endpointing (when the user finished) + barge-in (stop instantly on interrupt).
  • Spend the latency budget on LLM first-token and TTS first-chunk — the two that gate the reply start.
  • The orchestrator manages state and clean cancellation so barge-in never corrupts the conversation.
  • Tools (with filler to mask latency) make it act; watch end-to-end speech-to-speech models emerging.

Concepts covered

  • What makes a voice agent hard?
  • The sequential pipeline is too slow
  • A brain over a live audio link
  • Stream and pipeline the three stages
  • Turn-taking: endpointing & barge-in
  • Where the milliseconds go
  • The orchestrator holds it together
  • Tools, integration & transport
  • Noise, overlap, cost & end-to-end models
RUN IT YOURSELF

The account number the agent answered half of

Endpointing is a timer: wait N ms of silence, then take the turn. This file runs six real turns through two endpointers — one constant for every turn, and one threshold per dialog state — and prints the dead air each pays to cut nobody off, plus the words it loses when it gets it wrong. Change SILENCE_MS, hit Run, and watch the caller reading out an account number get cut off mid-digits.

HOW TO READ THE CODE — 5 IDEAS
  1. A VAD only sees silence, and a pause mid-utterance is the same silence as the end of one — so the timeout is the whole decision (step 4).
  2. The two errors are not symmetric: at SILENCE_MS=500 the agent waits 500 ms on every turn and hands the orchestrator 3 of 8 words of an account number, which it answers as though it were whole.
  3. The cheapest constant that cuts nobody off is 920 ms, because it must cover the longest pause anywhere — and it charges that to the yes-or-no turn that was over in one word.
  4. Thresholds set per dialog state are safe at 546 ms mean: 373 ms of dead air back on every turn, with no model touched (step 4) — the latency step 5 spends the rest of its budget chasing.
  5. The saving IS the spread between prompts (920/540/180). The last printed line refits one threshold over the same calibration and the saving goes to exactly 0 ms — so the number comes from the mechanism, not the toy.
CPython · WebAssembly
built to talk back before the awkward silence sets in — make the calls, break the turn-taking, run the gauntlet.
Finished this one? 0 / 65 AI System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs