標籤: 低延遲語音 AI

  • 低延遲語音 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 數據