對應原題(Common ‧ Multimodal, Speech and Voice AI)
- Budget the latency for a real-time voice agent: VAD, ASR, LLM, TTS, network. Where does the time go?
— Asked at: Sarvam AI, ElevenLabs- Design barge-in / interruption handling for a voice agent. — Asked at: ElevenLabs
- Explain streaming TTS chunking and jitter-buffer sizing. — Asked at: ElevenLabs
🎯 什麼情境該想到我
當你「做語音對話產品,使用者說『它反應好慢』」的時候。
★ 目標基準:人類對話的自然輪替間隔約 200 ms;超過約 500 ms 開始感覺不自然;超過 1 秒明顯尷尬。
⚙️ Part 1:延遲預算
★ 逐段拆解(要能現場列出來)
| # | 階段 | 典型 | 說明 |
|---|---|---|---|
| 1 | 網路上行 | 20–50 ms | 使用者 → 伺服器 |
| 2 | ★ VAD / 端點偵測(endpointing) | 200–500 ms | ★ 最大的單一來源。系統必須「確定他講完了」才能開始 |
| 3 | ASR(串流,最後一段的定稿) | 50–150 ms | 串流 ASR 早就在跑,這裡只算最後的 finalize |
| 4 | ★ LLM TTFT | 200–800 ms | ★ 第二大來源。見 Prefill 與 Decode |
| 5 | TTS 首個音訊塊 | 100–300 ms | 不是整段合成,是第一個 chunk |
| 6 | 網路下行 + jitter buffer | 50–150 ms | |
| 7 | 播放緩衝 | 50–100 ms | |
| 合計 | ≈ 700–2,000 ms |
★ 兩個關鍵洞察
洞察 1:最大的槓桿是 endpointing 和 LLM TTFT,不是 ASR 或 TTS。
很多團隊花力氣換更快的 TTS,但那只佔 100–300 ms。該先動的是第 2 和第 4 項。
Endpointing 的兩難:
等太久 → 使用者覺得系統遲鈍;等太短 → 打斷正在思考的使用者(他只是停頓了一下)。
| 改善手段 | 說明 |
|---|---|
| ★ 語意 endpointing | 不只看靜音長度,用小模型判斷「這句話語法/語意上完整了嗎」。「我想要訂一個⋯」明顯沒講完,可以多等;「我想要訂明天下午三點」就可以立刻開始 |
| 自適應閾值 | 依使用者的說話節奏、語言(不同語言的停頓習慣差很多)動態調整 |
| 提前起跑 | 靜音一到門檻就先送去 LLM,使用者又開口就取消 |
洞察 2:★ 真正的解法是「管線重疊」而不是「每段變快」。
❌ 串列(各段等前一段完成)
使用者說話 ──▶ VAD ──▶ ASR 全文 ──▶ LLM 全文 ──▶ TTS 全段 ──▶ 播放
總延遲 = 各段完整時間之和
✅ 重疊(各段的「首個輸出」就往下送)
使用者說話 ──▶ VAD
└─▶ ASR 串流部分結果 ──▶ LLM 已可開始預熱 prefill
└─▶ 第一句出來 ──▶ TTS 第一個 chunk
└─▶ 立刻開始播
★ 總延遲 ≈ 各段「首個輸出」時間之和
具體做法:
- ASR 的部分結果就送 LLM 做 prefill(推測性執行)——反正 prefix 大多不會變
- LLM 一產出第一個完整句子就送 TTS(sentence-level streaming),不等整段
- TTS 第一個音訊 chunk 就開始播
推測性執行的代價:使用者後面又改口 → 白做的 prefill 要丟棄。
這是拿 token 成本換延遲——和 推測解碼 Speculative Decoding 是同一個交易思路。
⚙️ Part 2:Barge-in(打斷處理)
要處理的是四件事,不是一件:
① 偵測:使用者在 TTS 播放時開口
★ 前提是回音消除(AEC)。沒有 AEC,麥克風會收到自己正在播放的聲音,系統會不斷把自己當成使用者在說話。
這在免持/擴音場景是硬需求。
② 立即停止播放
必須快(< 100 ms)。停得慢,使用者會覺得「它無視我繼續講」——這是語音產品最惹人厭的體驗之一。
同時要清空播放緩衝區,不是只停止新的音訊。
③ ★ 狀態調和(最容易被漏掉的一項)
LLM 生成了: 「好的,我幫您查到三個選項。第一個是⋯第二個是⋯」
實際播出去: 「好的,我幫您查到三個選——」 ← 使用者在這裡打斷
❌ 錯誤做法:把完整生成的文字寫進對話歷史
→ 模型以為使用者聽過三個選項了,後續對話全部錯亂
✅ 正確做法:★ 對話歷史只記錄「實際播放出去的部分」
並標記「(此處被使用者打斷)」
這需要 TTS 播放端把「播到哪裡」回報給對話狀態管理——一個實作上很容易漏的回路。
④ 取消進行中的請求
中止 LLM 生成與 TTS 合成,省下 token 與合成費用。高併發下這筆錢不小。
★ 誤觸發:區分「真打斷」與「附和」
使用者說「嗯」「對」「OK」——這是 backchannel(附和),不是打斷。全部當打斷處理會讓對話支離破碎。
| 判斷依據 | 說明 |
|---|---|
| 能量 + 持續時間 | 短促的單音節多半是附和 |
| 語意 | 用小模型判斷內容是否構成新的意圖 |
| 確認窗口 | 偵測到聲音先降低音量而不是立刻停止,持續超過門檻才真的中斷 |
| 背景噪音門檻 | 環境噪音、咳嗽、他人談話 |
⚙️ Part 3:Streaming TTS Chunking 與 Jitter Buffer
TTS 分塊:韻律 vs 延遲的取捨
TTS 需要看到足夠的文字才能決定語調、重音、連讀與句尾語氣。
| chunk 大小 | 問題 |
|---|---|
| 太小 | 韻律不自然、斷句生硬、每個 chunk 的固定開銷累積 |
| 太大 | 首字延遲(TTFB)高 |
實務做法:
- 依句子邊界切(標點、語意完整單位)—— 這是韻律的自然邊界
- ★ 第一個 chunk 刻意切小 —— 只要幾個字就先送去合成,搶 TTFB;後續 chunk 恢復正常大小
- 前瞻(lookahead) —— 合成當前 chunk 時讓 TTS 多看一點後面的文字,用來決定語調(代價是多等一點)
- 跨 chunk 的韻律狀態要延續 —— 否則每個 chunk 聽起來像不同人講的
Jitter Buffer 設定
問題:網路封包到達時間不均勻(jitter)。沒有緩衝就會斷斷續續。
緩衝太小 ──▶ underrun(沒音訊可播)──▶ 卡頓、爆音
緩衝太大 ──▶ 延遲直接增加
| 做法 | 說明 |
|---|---|
| 固定緩衝 | 簡單。語音場景常見 40–100 ms |
| ★ 自適應 jitter buffer | 持續量測網路抖動(如封包間隔的變異),動態調整緩衝深度。網路好時縮小省延遲,網路差時放大保流暢 |
| 在靜音處調整 | ★ 改變緩衝大小會改變播放速度或造成跳躍——在靜音段做調整,使用者聽不出來 |
⚠️ 注意 / 什麼時候不適用
- ★ 要量的是「輪替延遲」不是「各元件延遲」。使用者感受的是「我說完到它開口」,元件各自的 p50 加起來不等於它。要端到端量,並看 p95。
- LLM 的長回答會讓打斷變頻繁。語音場景要明確要求簡短回應——這同時省延遲、省成本、減少被打斷。
- 工具呼叫會炸掉延遲預算。一次 RAG 檢索 + 二次 LLM 呼叫就超過 1 秒。→ 要有「請稍等,我查一下」的填充語音(filler),或把工具呼叫設計成可預取。
- 多語言的 endpointing 參數不同。停頓習慣、語速、語尾語氣詞(中文的「呢」「吧」)差異很大(見 級聯與原生語音)。
- 行動網路的抖動遠大於 WiFi。緩衝策略要依連線類型調整。
- AEC 不是可選項。免持場景沒有 AEC,barge-in 根本不能用。
- 不要用文字 chatbot 的 prompt 直接做語音。語音回應需要更短、更口語、避免條列與符號(TTS 念不好)。
🧪 我實際套用的紀錄
- 2026-09-20:從面試題庫展開。待辦:做語音產品時先量 endpointing 與 TTFT 這兩段,它們佔了大半。
🔗 相關
- 級聯與原生語音 —— 架構選擇怎麼改變這張延遲表
- Prefill 與 Decode —— TTFT 的來源與優化
- 推測解碼 Speculative Decoding —— 同樣是「拿成本換延遲」
- 前綴快取 Prompt Caching —— 語音對話歷史是天然的成長前綴
- 視覺語言模型 —— 另一個模態的即時性挑戰
- ReAct 與 Agent Loop —— 語音 agent 的工具呼叫延遲預算