低延遲語音 AI 架構實戰解析

低延遲語音 AI 架構實戰解析

📌 本文重點

  • 目標是在 200–400ms 內提供高品質雙向語音互動
  • 關鍵在通訊管線穩定與三模型 streaming 並行
  • 難點是 tail latency 與企業網路環境下的可用性
  • 先用 WebRTC/WS + 開源模型做 MVP 再優化

要把GPT‑5 級推理塞進即時通話,最大痛點是:總延遲必須壓在 200–400ms 內,還要撐住大量並發。這篇用 OpenAI 近期的語音架構做藍本,從通訊層、模型層、系統層拆解,並給出一個用「常見雲 + WebRTC/WS + 開源語音模型」的最小可行方案(MVP),協助你評估:

  • 專案能不能做到「類 GPT-Realtime-2」的體驗
  • 目前架構要改哪些地方
  • 延遲預算怎麼抓、實測怎麼調

重點說明:三層拆解你應該先想清楚什麼

1. 通訊層:WebRTC / Streaming API 管線

核心結論:語音幀越小、管線越穩定,LLM 才有操作空間。

關鍵設計:

  • 雙通道設計
  • WebRTC:負責雙向音訊(RTP),盡量 P2P,失敗時回退 TURN/Relay
  • WebSocket / gRPC streaming:負責把編碼後音訊送進推理後端,收 TTS 音訊回來
  • 幀大小與編碼
  • 單向 20ms 幀是常見折衷(Opus 20ms/packet),RTT + 解碼後約 40–60ms
  • OpenAI 類似設計:低 bit‑rate Opus / 自家 codec + 小幀 + 伺服端聚合
  • 回退策略
  • NAT/防火牆下 P2P 常失敗,要有 ICE + TURN,並在 handshake 階段就降級到「WebRTC → TURN relay → 伺服器」模式

💡 關鍵: 前端小幀 + 後端穩定回退(ICE + TURN)是把延遲壓到 200–400ms 的第一道門檻

2. 模型層:語音↔文字↔推理鏈路的裁剪與並行

核心結論:不要把語音→文字→LLM→TTS 串成一條同步鏈,要做 streaming 並行。

典型 full pipeline:

  1. 語音 ASR:Speech → Text
  2. 文字 LLM:Text → Response tokens
  3. TTS:Text → Speech

在低延遲場景下可以這樣優化:

  • 早啟動推理
  • ASRstreaming 模式,每 100–200ms 一個 partial transcript
  • 當句子結構「大致明朗」時(看到疑問詞或語尾),就把目前 transcript 丟給 LLM不用等語音結束
  • 分段生成 + streaming tokens
  • LLM 開啟 streaming,邊出 token 邊餵給 TTS
  • 控制 max_tokens / per-turn token budget,避免一次生成長篇大論造成尾端延遲
  • TTS 緩衝策略
  • 不要等完整句子才播,通常 150–300ms 音訊緩衝就可以開始播放
  • 但也不能太小,避免「一卡一卡」;常見做法是根據 網路 jitter + 解碼時間 動態調整

OpenAI 最新的 GPT-Realtime-2 / -Translate / -Whisper 其實就是把這條鏈路收斂成幾個特化模型,讓內部共享語音表徵與推理能力,減少中間編碼/解碼開銷。你在自建時不一定能做到單一多模態模型,但至少要做到三模型 streaming 並行

💡 關鍵: 把 ASR、LLM、TTS 並行 streaming,通常能把首次開口時間從秒級壓到 300ms 左右

3. 系統與部署層:分布式推理與 tail latency 控制

核心結論:平均延遲不難,難的是 99th percentile。

設計要點:

  • 模型路由與排程
  • 輕量語音模型(ASR/TTS)可以 多實例 + 每 GPU 多 worker,吃滿 GPU
  • LLM 建議走 集中式推理叢集 + router,依 session 粘性綁定同一实例
  • GPU 利用率
  • 啟用 batching + token 并行,但要對語音場景限縮 batch size,避免增加 tail latency
  • 長回應可切段生成:先生成 1–2 秒的語音對應文字,再補充後半段
  • tail latency 監控
  • 重要指標:錄音開始 → 第一個回傳音訊 的 p50/p95/p99
  • 以 tracing 把 pipeline 切開:上傳 / ASR / LLM queue / LLM compute / TTS / 下行網路
  • 一旦 p99 失控,先檢查 排程佇列(排隊時間)而不是模型本身

💡 關鍵: 真正破壞體驗的是 p99 延遲而不是平均值,監控時要把 queue time 單獨拉出來看


實作範例:一個最小可行架構(MVP)

架構概覽

  • 前端:Browser WebRTC(音訊 capture) + WebSocket(控制訊息)
  • 後端:
  • Signaling + API GatewayNode / Go 都可
  • gRPC streaming 到推理服務
  • 推理服務:Python + 開源 Whisper streaming + 開源 LLM + VITS/TTS(示意)

前端:WebRTC + WebSocket 管線

// 簡化版:建立 WebRTC 傳音訊 + WS 傳控制
const pc = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});

const ws = new WebSocket("wss://your-api.example.com/signaling");

async function startCall() {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  stream.getAudioTracks().forEach(t => pc.addTrack(t, stream));

  pc.onicecandidate = e => {
    if (e.candidate) ws.send(JSON.stringify({ type: 'candidate', data: e.candidate }));
  };

  pc.ontrack = e => {
    // 收到 TTS 音訊(伺服端用 WebRTC 回推)
    const audio = document.getElementById('remote') as HTMLAudioElement;
    audio.srcObject = e.streams[0];
  };

  const offer = await pc.createOffer({ offerToReceiveAudio: true });
  await pc.setLocalDescription(offer);

  ws.onopen = () => {
    ws.send(JSON.stringify({ type: 'offer', data: offer }));
  };

  ws.onmessage = async (msg) => {
    const { type, data } = JSON.parse(msg.data);
    if (type === 'answer') {
      await pc.setRemoteDescription(data);
    }
  };
}

注意:

  • 幀大小主要在伺服端設定 Opus encoder(例:20ms),前端維持預設即可
  • 若 WebRTC 被防火牆擋住,signaling 伺服端要下指令讓前端切換為 WebSocket 直接上傳 PCM/Opus 模式

後端:gRPC streaming 推理服務(示意)

假設有一個 SpeechService 接收 Opus 幀,回傳已編碼好的音訊幀:

// speech.proto
service SpeechService {
  rpc Converse(stream AudioFrame) returns (stream AudioFrame) {}
}

message AudioFrame {
  bytes data = 1;      // Opus 或 raw PCM
  int64 timestamp = 2; // client capture ts
}

伺服端 Python(簡化,忽略實際音訊處理細節):

class SpeechService(servicer_pb2_grpc.SpeechServiceServicer):
    async def Converse(self, request_iterator, context):
        # 1) 啟動 ASR/LLM/TTS 協程
        asr_queue = asyncio.Queue()
        llm_queue = asyncio.Queue()
        tts_queue = asyncio.Queue()

        async def asr_worker():
            async for frame in request_iterator:
                # 解碼 Opus -> PCM -> ASR partial text
                text_partial = asr_model.transcribe_stream(frame.data)
                await llm_queue.put(text_partial)

        async def llm_worker():
            async for partial in llm_queue:
                # 送入 LLM streaming,邊出 token 邊丟給 TTS
                async for chunk in llm.stream(partial, max_tokens=64):
                    await tts_queue.put(chunk.text)

        async def tts_worker():
            async for txt in tts_queue:
                # 生成短語音片段(200–300ms)
                audio_bytes = tts_model.synthesize(txt)
                yield speech_pb2.AudioFrame(
                    data=audio_bytes,
                    timestamp=int(time.time() * 1000)
                )

        await asyncio.gather(asr_worker(), llm_worker(), tts_worker())

實務上你會:

  • 更細緻的 queue 協調(包含會話 ID、句子邊界)
  • 控制 llm.stream 的 token 長度與 stop 條件,避免超長句
  • 在 TTS 部分先緩衝幾個 frame,再開始透過 WebRTC/WS 推到 client

延遲 budget 規劃(示意)

在穩定網路下可以先抓:

  • 上行錄音 + 傳輸:40–80ms20ms 幀 + RTT
  • ASR streaming:40–80ms(小模型 + GPU)
  • LLM 推理:80–150ms(取決於 token 數與模型大小)
  • TTS 生成 + 下行傳輸:60–120ms

整體 p50 目標:220–350ms 第一個回應音訊開始播放

優化策略:

  • 第一輪回應:用 較短回應模板(像「嗯、好的,我來看一下…」)快速回覆,爭取後面長推理時間
  • 持續對 ASR/LLM/TTS 做 A/B test:看哪一段是主要瓶頸,優先調那裡的 model size / batch / GPU 排程

建議與注意事項:幾個常見坑

1. NAT / 防火牆導致 WebRTC P2P 失敗

  • 坑點:只測局域網或開放網路,實際部署到企業網路立刻掛掉
  • 建議:
  • 一開始就部署 TURN server,並在前端暴露 ICE 連線狀態,回報到後端
  • 若連線失敗,API 層切到 純 WebSocket 音訊通道,雖然成本高但能保證可用性

2. 語音切段過粗,造成「打斷感」

  • 坑點:以 1 秒幀或整句才送 ASR,LLM 只在句尾發言,對話像對講機
  • 建議:
  • 200ms 以內 的 audio chunk;ASR 使用 partial result callback
  • 根據語氣停頓(VAD)+ 標點預測,判斷何時啟動 LLM 回應

3. TTS 緩衝策略不當,導致「一卡一卡」

  • 坑點
  • 緩衝太短 → 網路 jitter 就卡
  • 緩衝太長 → 首次開口延遲拉高
  • 建議:
  • 先以 250ms 緩衝 做 baseline,再按實測 jitter 動態調整在 200–400ms
  • 在 client 端維護一個小 buffer,利用 AudioContext / Web Audio API 自行排程播放,而不是一次性丟給 <audio>

4. GPU 利用率 vs tail latency 的拉扯

  • 坑點:最大化 batch size 很爽,但 p99 延遲爆炸
  • 建議:
  • 把語音場景的推理服務與一般 chat/RAG 分開,語音路徑限制 max batch size
  • 使用 token-level scheduling(類似 OpenAI 做法),避免長上下文會話拖累短 query

對專案的實際好處

  • 如果你現在線上只有「按鈕錄音 → 傳檔 → 回文字」,這套設計可以讓你在 1–2 週內做出可 demo 的雙向語音助理
  • 用 WebRTC/WS + 開源模型的 MVP,可以先驗證:
  • 使用者對 latency 敏感度
  • 需要多強的推理(是否真的要 GPT‑5 級推理)
  • 實際 GPU 成本與擴展上限
  • 後續要接上 OpenAI 類似 GPT-Realtime-2 的託管服務時,這套三層思路與接口方式幾乎可以直接沿用,只是把內部 ASR/LLM/TTS 換成單一多模態 API。

🚀 你現在可以做的事

  • 在現有專案中畫出完整語音管線,標註各節點預估延遲(上傳、ASR、LLM、TTS、下行)
  • 用 WebRTC + WebSocket 加上任一開源 Whisper + TTS,實作一個能雙向講話的最小 demo
  • 部署基本的 tracing(例如在每一階段打 log),實測並記錄「錄音開始 → 第一個回應音訊」的 p50/p95/p99 數據

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *