How to Build a Full-Duplex Voice Agent with Streaming ASR, LLM, and TTS
Adding voice to an agent is a systems-integration problem, not a model problem. The difference between a clunky sequential pipeline and a natural conversation is a few hundred lines of buffering, cancellation, and scheduling code that most tutorials skip.
Voice interaction for AI agents requires three services to run in lockstep: speech recognition, a streaming LLM, and real-time text-to-speech. A full-stack implementation wires a browser's MediaRecorder to Alibaba Cloud's qwen3-asr-flash model, feeds the recognized text into a LangChain-powered SSE chat stream, and then forwards each sentence fragment over a WebSocket to qwen3-tts-flash-realtime for synthesis. The server acts as a transparent relay, buffering text until a punctuation mark or a 15-character threshold triggers a TTS dispatch.
On the frontend, an AudioContext-based player schedules decoded MP3 chunks with precise start times so playback is seamless across segments. The same streamChat function handles both typed and spoken input, and an AbortController cancels in-flight requests when the user sends a new message. The result is a Doubao-style experience where the agent begins reading a reply aloud before the LLM has finished generating it.
The core engineering problem is not any single API call but the buffering and scheduling logic that keeps ASR, LLM, and TTS from drifting apart. A message queue inside the TTS WebSocket handler holds text until the DashScope session is ready, and the client-side player tracks scheduled versus finished segments to know when playback is truly done.
Most voice-agent demos treat ASR, LLM, and TTS as sequential steps. The real product experience comes from running them as overlapping streams, which turns the server into a timing-sensitive relay rather than a simple proxy.
The 15-character fallback threshold is a pragmatic hack that acknowledges punctuation alone is unreliable for real-time TTS dispatch, especially with LLMs that generate run-on sentences or code blocks.
Using server_commit mode offloads sentence-boundary detection to the cloud model, but it also means the client loses control over when audio is emitted—a trade-off that matters if you need word-level synchronization with animations or transcripts.