📌 本文重點
- 目標是在 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幀是常見折衷(Opus20ms/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:
- 語音
ASR:Speech → Text - 文字
LLM:Text → Response tokens TTS:Text → Speech
在低延遲場景下可以這樣優化:
- 早啟動推理:
ASR用 streaming 模式,每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 Gateway:
Node/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–80ms(20ms幀 +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 數據


發佈留言