對應原題(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★ 最大的單一來源。系統必須「確定他講完了」才能開始
3ASR(串流,最後一段的定稿)50–150 ms串流 ASR 早就在跑,這裡只算最後的 finalize
4★ LLM TTFT200–800 ms★ 第二大來源。見 Prefill 與 Decode
5TTS 首個音訊塊100–300 ms不是整段合成,是第一個 chunk
6網路下行 + jitter buffer50–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)高

實務做法:

  1. 依句子邊界切(標點、語意完整單位)—— 這是韻律的自然邊界
  2. ★ 第一個 chunk 刻意切小 —— 只要幾個字就先送去合成,搶 TTFB;後續 chunk 恢復正常大小
  3. 前瞻(lookahead) —— 合成當前 chunk 時讓 TTS 多看一點後面的文字,用來決定語調(代價是多等一點)
  4. 跨 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 這兩段,它們佔了大半。

🔗 相關