最近在做一个语音大模型助手的前端接入,项目推进到技术选型时,团队内部为同一个问题吵了好几轮:“语音流接大模型,到底是走 WebSocket 还是 WebRTC?”这不是拍脑袋就能定的问题,它背后牵扯到延迟预算、弱网表现、服务端改造成本和后续功能迭代空间。我把自己的踩坑过程和最终落地的结论整理出来,希望对正在纠结同样问题的朋友有参考价值。
这里说的“语言接入大模型”,如果只是文本对话,那基本不用纠结,WebSocket 几乎是最优解;但如果你要接的是“语音”,让用户像跟人说话一样跟大模型对话,这个问题就值得掰开揉碎聊清楚。这篇文章会先讲两种协议的本质区别,再结合文本流式输出、语音实时对话两种典型场景,给出选型逻辑、落地流程和一整套排查经验。无论你是前端开发者、语音产品负责人,还是正在研究大模型应用接入的初学者,都能从中拿到一套可以直接执行的选择思路。
1. 先搞明白:“语言接入大模型”到底是接文本,还是接语音
很多人上来就问“用哪个协议”,其实真正该问的是“我的产品到底需要什么样的交互”。文本对话和语音对话,背后是两套完全不同的实时性要求和数据特征。选错协议,往往不是某个功能做不出来,而是在后续的性能优化和体验迭代上会被一直卡着。
1.1 文本流式对话和语音实时对话不是一回事
文本场景的典型链路是:用户在输入框里打字,前端把请求发给大模型,模型边生成边把 token 推回来,前端流式渲染成打字机效果。这种场景下,数据是文本、JSON、事件流,单条消息体积很小,对实时性的要求相对宽松,几百毫秒的网络延迟用户也能接受。WebSocket 在这里几乎是完美匹配:一次握手建立长连接,客户端和服务端随时互相推送,天然支持流式输出,而且浏览器原生支持,开发成本极低。
语音场景就完全不一样了。它的链路是:用户对着麦克风说话,前端采集音频流不断上行,服务端做语音识别(ASR),把识别出的文字送进大模型,大模型生成回复文字,再经语音合成(TTS)变成音频流回传,前端解码播放。这里面的数据是连续的音频帧,对实时性、连贯性、对丢包和抖动的容忍度都有完全不同的要求。TCP 语义下的 WebSocket 在这种场景会遇到队头阻塞、重传延迟堆积、弱网卡顿等问题;WebRTC 从协议层开始就是为这种场景设计的。
这两种场景常常被混为一谈,是因为很多语音产品的“第一步”都是从文本接口入手的:先用 WebSocket 把用户语音转成文字,以文本形式送进大模型,再把回复文本转成语音播出来。于是“WebSocket 传语音”和“WebSocket 传文本”就被看成了一件事。实际上,传文本走 WebSocket 是顺理成章,传实时音频走 WebSocket,是拿短跑运动员去跑马拉松,能跑,但跑不远。
1.2 先算清延迟预算,再谈协议选型
选协议之前,我一直建议先做一道延迟拆解题。语音交互的端到端延迟由这几段组成:前端采集编码、网络上行、ASR 识别、大模型生成首 token、TTS 合成首帧、网络下行、播放缓冲。
正常人能接受的语音助手响应速度:端到端在 1 秒以内,体验还算自然;超过 1.5 秒就会觉得“有点迟钝”;超过 2 秒,用户基本就不愿意继续对话了。这不是苛刻,是人在日常对话中的本能预期。而我们自己测试时,光是“网络上行 + ASR + LLM 首 token + TTS 首帧”这几段,在服务端一切顺利的情况下就要花掉 500 到 900 毫秒,留给网络传输的预算本来就很薄。这时候如果协议还在弱网下疯狂重传、把延迟拖到 1 秒以上,整个对话体验就直接崩了。
还要区分“半双工”和“全双工”。半双工是按住说话,说完松开,然后等大模型回复;全双工是用户一边说、大模型一边听,甚至可以在大模型回复过程中随时打断它、插嘴进去。半双工对实时性的要求低一些,WebSocket 完全能扛;全双工要求音频上行和下行同时进行,而且随时可能调转方向,对丢包、抖动、回声消除都提出了更高要求,这个时候 WebRTC 的优势才真正凸显出来。所以我在内部评审时习惯先说一句话:先别问选什么协议,先说你做的是“对讲机”还是“电话”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. WebSocket:消息推送的王者,接大模型文本流是天然适配
WebSocket 在接大模型这块的地位,不是靠炒作,是靠着它简单、直接、可靠换来的。你现在去看各大模型厂商的流式接口,几乎都有 WebSocket 或者 SSE 两个选项。大部分前端在接文本流式输出时,第一反应就是 WebSocket,因为这个方案确实足够顺手。
2.1 WebSocket 为什么是接大模型文本流的主流姿势
WebSocket 在 OSI 模型里跑在 TCP 之上,是一条全双工通信通道。一次握手之后,客户端和服务端可以随时互相发消息,不用像 HTTP 轮询那样反复建立连接、反复带请求头。这对大模型场景特别重要:大模型生成文本是流式的,一个几百字的回答可能要分十几次推送,WebSocket 模式下服务端生成一个 chunk 就可以立刻推给客户端,首字延迟能做到很低。
前端用起来也极其简单,原生 WebSocket 对象就能搞定,不需要引任何第三方 SDK。我在项目里常用的最小接法是:建立连接、监听 onmessage、判断消息类型、处理文本或者 JSON。配合二进制数据,它还可以传 ArrayBuffer,这让它不仅能传文本,也能传音频数据帧。很多人正是因为“能传二进制”这一点,才把 WebSocket 顺手用在了语音场景里。
从服务端角度看,WebSocket 也很好处理。Python 有 FastAPI/websockets,Node 有 ws、socket.io,Java 有 Netty,任何主流语言都有成熟的 WebSocket 库。如果你用 Ollama、vLLM 或各类云厂商 API 做本地大模型部署,它们提供的流式接口也都能很轻松地转成 WebSocket 网关,团队改造成本很低。
2.2 用 WebSocket 传语音的常见做法和隐藏成本
我第一次做语音接入时,走的也是 WebSocket 路线。当时的做法是:前端用 AudioWorklet 做音频采集,拿到 16kHz 采样率、16bit 量化的 PCM 数据,通过 postMessage 交给主线程,再用 WebSocket 的 send 方法把二进制帧推给服务端。服务端收到后送进流式 ASR,识别文本再送大模型,最后把 TTS 合成的音频帧通过同一个 WebSocket 推回来。
这个方案能跑通,而且不少商用产品早期也是这么干的。但它的“能跑通”是有前提的:网络环境要好,用户和设备不能有太多并发,而且你得自己处理回声、降噪、抖动缓冲这些本该由音视频引擎处理的问题。我实际测试时发现,在纯内网、低并发场景下,WebSocket 传 PCM 音频的端到端延迟能控制在可接受范围内;一旦丢包率超过 2%,声音就开始出现明显的断续和卡顿,用户体感非常差。
隐藏成本还在播放端。WebSocket 本身不关心“什么时候播、怎么播”,它只负责把音频帧交给你。你需要自己在客户端维护一个播放队列,控制好每一帧的播放时机,还要处理网络抖动造成的音频饥饿。这些逻辑全部要自己写,而且很难写得好。做出来是 60 分还是 90 分,完全看团队在音频工程上的积累。
2.3 实测下来,WebSocket 传语音的三个痛点
第一个痛点是 TCP 队头阻塞。TCP 为了保证可靠性,会把数据按序交付。WebSocket 跑在 TCP 上,意味着前面的一个音频包如果丢了,后面的包就算已经到了接收端,也要等在缓冲区里,等前面那个包重传成功后才能交给应用层。这在弱网环境下就是灾难:一个包丢了 100 毫秒,后面几十个音频帧全部排队,累积起来就是几百毫秒的延迟尖峰。
第二个痛点是重传导致的延迟堆积。音频数据其实并不需要 100% 可靠,偶尔丢一个采样点,用隐藏算法补一下,人耳根本听不出来;但 WebSocket 底层的 TCP 不管这些,它会拼命重传,把延迟越拖越高。这类问题在日志里最常见的表现就是:stream disconnected before completion,或者声音越来越卡,最后连接直接断掉。
第三个痛点是没有内置音频处理能力。回声消除、噪声抑制、自动增益控制、抖动缓冲这些语音通话的基本能力,WebSocket 一个都没有。你要么在前端用 Web Audio API 自己搭信号链,要么在后端做 DSP,无论哪种,工程量和算法门槛都远超普通前端团队能接受的范围。坦白讲,如果只是做“按住说话、说完听回放”的低频场景,这些问题还能忍;一旦要做全双工、要谈弱网体验,趁早放弃 WebSocket 传语音的执念。
3. WebRTC:为实时音视频而生的老将,语音大模型场景的进阶答案
WebRTC 不是什么新东西,它在实时音视频领域已经跑了很多年,视频会议、在线课堂、连麦直播都在用它。大模型语音交互火起来之后,这个“老将”又回到了聚光灯下,因为它从设计之初就是在解决“如何在不确定的网络上低延迟传音视频”这个问题。
3.1 WebRTC 到底做了什么,让弱网音视频能保持可用
WebRTC 的核心传输层走的是 UDP,配合 SRTP 做加密,用 RTCP 做反馈控制。它默认不保证可靠交付,而是“尽力而为”,这恰恰是实时音视频想要的:丢几个音频包没关系,但不能因为一个包堵住后面所有的数据。
它还有一整套自带的音视频引擎。音频方面有回声消除(AEC)、噪声抑制(NS)、自动增益控制(AGC),还有非常关键的 NetEQ,也就是自适应抖动缓冲和丢包隐藏算法。NetEQ 会在网络抖动时缓存一部分音频数据,平滑播放节奏;遇到丢失的包,它会用算法补出相近的语音信号,让用户几乎察觉不到。这一整套能力,正是 WebSocket 方案里需要自己造的轮子。
弱网优化方面,WebRTC 还做了一堆事情:FEC 前向纠错会在发送时加入冗余包,丢包后可以本地恢复;拥塞控制会根据网络状况动态调整发送码率;音频默认使用 Opus 编码,支持前向纠错和可变码率,在弱网下可以把音质调低来保连接。这些机制层层叠加,才让它的通话质量在远不如有线的移动网络上依然稳定。
3.2 大模型语音场景里,WebRTC 的不可替代之处
大模型语音交互里,WebRTC 最大的价值是支持真正的全双工。用户说话的同时,大模型可以把回复音频往回调,用户可以随时打断。这种打断机制在 WebSocket 方案里实现起来很别扭:你要额外定义一套控制协议,处理音频上行、下行、打断信号的优先级,还要随时清理播放缓冲区。而 WebRTC 天然就是双向媒体流,打断只是把下行播放停下来,上行音频继续送,逻辑简单得多。
现在不少语音大模型服务,比如 OpenAI 的 Realtime API、国内厂商的实时语音接口,都已经支持 WebRTC 接入。它们的文档通常都会给出一套信令协商流程,前端建立一个 RTCPeerConnection,把麦克风音频轨推给服务端,服务端把合成音频和事件通过媒体轨或 DataChannel 推回来。在这种架构下,连接建立之后,媒体传输的可靠性、延迟控制都交给 WebRTC 处理,产品只需要关注“说什么、什么时候打断”。
另外,WebRTC 支持 DataChannel,可以和大模型之间传附加信息,比如识别出的文本、工具调用指令、事件通知。一条 RTCPeerConnection 里同时跑音频轨道和数据通道,音频走 SRTP,控制走 SCTP,互相隔离又共用连接,这在做“语音 + 文本 + 指令混合交互”的大模型 Agent 场景里特别顺手。很多语音 Agent 框架,比如基于 LiveKit 的解决方案,就是靠这个能力同时处理语音流和文本事件流的。
3.3 上车 WebRTC 要付出的代价:信令、TURN 和服务端改造
WebRTC 不是零成本方案,它最麻烦的点在连接建立阶段。WebRTC 本身只解决媒体传输,不解决“双方如何交换连接信息”:你需要自己搭或者接入一个信令服务器,负责交换 offer、answer 和 ICE candidate。前端还要处理 STUN 和 TURN 的配置,复杂网络环境里 NAT 穿透失败时,要能通过 TURN 服务器做中继转发。
服务端接入成本更高。如果大模型服务端只暴露了 WebSocket 接口,你的 WebRTC 方案就没法直连它,需要中间加一层媒体网关,把 WebRTC 的音频流转成服务端能消费的数据流,再把结果转回去。这一层的复杂度不低,常见的做法是引入 LiveKit、mediasoup、Janus 这类媒体服务器来搞定,但这也意味着多一个基础设施要和业务一起运维。
所以我的建议是:不要把 WebRTC 当成“更好的 WebSocket”,它是一套完整的实时通信系统。你如果真的需要全双工、需要打断、需要弱网下有稳定的语音体验,这代价花得值;如果只是做个最简单的文本流式输出,强行上 WebRTC 就属于杀鸡用牛刀,给自己添堵。
4. 实操落地:两条路径如何选,怎么做
选定方案之后,落地细节决定最终体验。我把两条路径的关键步骤、代码骨架和容易踩的坑都整理出来,可以直接作为参考。
4.1 路径 A:用 WebSocket 搭一套轻量语音问答链路
如果你的产品是半双工模式,用户按住说话、说完等待回复,那么 WebSocket 方案性价比很高,落地也快。关键流程分四步。
第一步是采集音频。建议用 AudioWorklet,不要再用被废弃的 ScriptProcessor。AudioWorklet 跑在独立音频线程里,不容易被主线程阻塞。核心代码大概是这样:
javascript复制// pcm-processor.js 注册在 AudioWorklet 中
class PCMProcessor extends AudioWorkletProcessor {
process(inputs) {
const channelData = inputs[0][0];
if (channelData) {
// 将 Float32 转为 16-bit PCM 的 ArrayBuffer
const buffer = new ArrayBuffer(channelData.length * 2);
const view = new DataView(buffer);
for (let i = 0; i < channelData.length; i++) {
const s = Math.max(-1, Math.min(1, channelData[i]));
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
this.port.postMessage(buffer);
}
return true;
}
}
registerProcessor('pcm-processor', PCMProcessor);
第二步是建立 WebSocket 连接,注意把 binaryType 设为 arraybuffer,否则收到的二进制数据会变成 Blob,播放前还得再转,多一步不必要的处理。发送端把 AudioWorklet 传过来的 ArrayBuffer 直接 send 出去即可。很多 ASR 服务喜欢收 base64 编码的音频,但你额外编码一次会增加 CPU 开销,如果能直接收二进制,尽量用二进制。
第三步是处理返回。服务端通常会在消息里区分类型:type 为 text 的是识别结果或大模型回复,type 为 audio 的是 TTS 音频帧。前端根据类型走不同分支,文本走状态机更新,音频走播放队列。
第四步是播放。播放音频不要直接 new Audio 一个 src 就完事,那样在连续音频流下会反复创建播放器,延迟不可控。正确做法是维护一个 AudioContext,把收到的音频帧解码后放进一个任务队列,每帧结束前预留一点时间调度下一帧解码,做到无缝衔接。这个播放队列就是整个方案里最需要打磨的地方,网络抖动时尤其考验你的缓冲策略。
4.2 路径 B:用 WebRTC 做全双工实时语音对话
做全双工语音对话,我会直接选择 WebRTC 或基于它封装好的平台。以浏览器原生 API 为例,最简流程是这样的:
javascript复制// 1. 先获取用户麦克风的音频轨
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
});
// 2. 创建 RTCPeerConnection,配置 STUN/TURN
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:your-stun-server' },
{ urls: 'turn:your-turn-server', username: 'user', credential: 'pass' }
]
});
// 3. 把音频轨加入连接
stream.getAudioTracks().forEach((track) => pc.addTrack(track, stream));
// 4. 监听远端音视频轨道(大模型返回的音频流)
pc.ontrack = (event) => {
const remoteAudio = document.getElementById('remote-audio');
remoteAudio.srcObject = event.streams[0];
};
// 5. 创建 offer,交给信令服务器
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: pc.localDescription });
// 6. 收到 answer 和 ICE candidate 后补齐连接
signaling.onmessage = async (msg) => {
if (msg.type === 'answer') {
await pc.setRemoteDescription(msg.sdp);
} else if (msg.type === 'candidate') {
await pc.addIceCandidate(msg.candidate);
}
};
这段代码看起来不复杂,但项目里能不能顺利跑起来,很大程度依赖信令服务器的可用性、STUN/TURN 的部署质量,以及你和服务端同学对连接状态机的理解。如果服务端已经支持 WebRTC,比如直接对接 LiveKit Agent 或云厂商的实时语音 API,那前端压力会小很多;如果服务端只有 WebSocket,前端要上 WebRTC 就得先推动服务端加一层媒体网关,这也可能是整个项目里最大的成本项。
4.3 决策表:什么场景选什么方案
我用一张表总结选型逻辑,方便对照。
| 场景特征 | 推荐方案 | 核心原因 |
|---|---|---|
| 纯文本对话、流式输出 token、打字机效果 | WebSocket | 简单、可靠、首字延迟低,浏览器原生支持 |
| 半双工语音问答(按住说话,说完等回复) | WebSocket 足够 | 网络压力小,音频处理要求低,团队改造成本最低 |
| 全双工语音对话(边听边说、随时打断) | WebRTC | 原生双向媒体流、内置回声消除/降噪/抖动缓冲,打断逻辑简单 |
| 对弱网体验要求高、需要稳定通话质量 | WebRTC | 内置 FEC、拥塞控制、NetEQ,WebSocket 没有这些能力 |
| 服务端只提供 WebSocket,且团队没有音视频经验 | 先用 WebSocket 做 POC | 快速验证产品需求,但要做好后续上 WebRTC 的预案 |
加一句个人经验:如果你主导的是一个新项目,需求是语音助手,但又没法确定用户对打断和连续对话的接受度,不妨先做半双工 WebSocket 版本去测试市场。等确认用户确实希望“像打电话一样”对话,再切 WebRTC。用最小成本验证业务,再用最好的技术做体验升级,这比一上来就铺大方案稳妥得多。
5. 常见问题与排查实录
实操中总会遇到各种奇怪问题,下面几个是我在实际项目里踩过的坑,列成速查表,能省不少排查时间。
5.1 流式输出中途断连,服务端在响应完成前关闭连接
如果你在 WebSocket 流式输出时看到报错:stream disconnected before completion: websocket closed by server before res,大概率不是前端的问题。优先检查服务端:是不是触发了某些超时设置,比如空闲超时、单条消息超长时间;是不是在返回最后一个 chunk 之后没有按正常协议发结束帧就主动 close;是不是反代网关把超过一定时长没有新数据的连接断开了。
前端侧能做的事是加自动重连和断点续传逻辑。WebSocket 断线后,前端不要马上清空整个对话状态,应该把已接收的内容缓存起来,重连后请求服务端从断点继续返回剩余结果。心跳保活也要做,哪怕只是每分钟发一个 ping,也能让中间网络设备别把空闲连接回收掉。
还有一个比较容易忽略的点:页面的 HTTPS/WSS 限制。浏览器高版本下,HTTPS 页面默认不允许发起非安全的 ws:// 请求,也不允许混合内容。如果你的页面是 https,但 WebSocket 地址是 ws://xxx,连接基本创建不起来,或者一直处于 CONNECTING 状态。遇到这种情况,优先确认地址是否为 wss://,证书是否有效。如果内网环境无法上 https,也要保证 localStorage 或配置里明确走 ws 时页面本身是用 http 打开,否则规则上就是不兼容。
5.2 WebRTC 连接一直失败,穿透不过去
WebRTC 连接失败的常见原因集中在 ICE 协商阶段。前端这里可以加一个 iceConnectionState 的监听,定位到 failure 状态后再排查。你打开 chrome://webrtc-internals 看日志,会发现三种典型情况:没发现可用的 candidate、P2P candidate 全部失败、TURN 中继没配置或者配置了但鉴权失败。
公司的企业网络环境里,UDP 端口被封是很常见的事,P2P 穿透基本没戏,这时候必须依赖 TURN 做中继。所以生产环境里,TURN 服务器不是可有可无,而是必须部署的保底方案。STUN/TURN 的配置要放在 ICE servers 列表里,并且 TURN 的用户名密码不要硬编码在前端,最好通过接口动态获取,控制有效时长,既安全又方便定期轮换。
另外注意,很多自建的 TURN 服务器默认只开了 3478 端口用于 HTTP/UDP 探测,媒体中继的实际端口范围很宽,防火墙如果只放行 3478,照样会失败。部署 coturn 这类服务时,要提前规划好 media 端口段,并在防火墙规则里放行,否则线上总会偶现“大部分用户正常、部分网络连不上”的诡异问题。
5.3 WebSocket 收音频在弱网上卡顿、声音断续
如果你暂时不换 WebRTC,还想用 WebSocket 保音频体验,我能给的最直接建议是引入缓冲策略。在播放端做 200 到 300 毫秒的 playout buffer,宁可先多等一点,也不要让声音播到一半断掉。缓冲的好处是用统一的初始延迟换取后续的平滑播放,这点和视频播放器的缓冲原理一样。
但缓冲也会带来副作用:如果服务端持续推送且网络恢复正常,缓冲里的数据会越积越多,延迟会逐渐增大。所以我建议做动态缓冲,检测到播放队列持续增加时,适当加快播放速度,比如把 playbackRate 调到 1.02 或 1.05,人耳几乎感知不到。这个技巧在 Web Audio 里实现起来不复杂,但能明显降低长时间通话的延迟漂移问题。
如果弱网实在严重,WebSocket 方案救不回来,别硬扛,直接切换 WebRTC。它对弱网有一套完整的自适应机制,包括前向纠错、拥塞控制、动态码率调整,这些确实是自己写不出来的。
5.4 语音延迟高的排查套路
语音延迟高,先拆解再定位,不要一上来就怀疑协议。我的排查顺序是:先本地观察前端,再抓服务端各环节耗时,最后看网络传输。
前端环节,看它是不是在播放端做了过长的缓冲;AudioContext 有没有初始化失败导致走退化逻辑;音频采集是不是用了过高采样率但服务端又做了重采样。服务端环节,ASR 是否用了流式接口、有没有等整句结束才返回;LLM 首 token 延迟是多少;TTS 是不是等整句合成完才返回。网络环节,用 WebRTC 的话可以在 chrome://webrtc-internals 看 RTT 和丢包率,用 WebSocket 的话就是要最有意义的测试手段:直接测 TCP 往返延迟和实际下载/上传带宽。
如果赌在服务端上,最常见的凶手其实是 TTS。很多 TTS 服务端是拿到完整文本后才开始合成,而不是边生成边合成。流式 TTS 和普通 TTS 的首帧延迟差别很大,有时候能差出一倍以上。接入大模型语音链路时,务必确认 TTS 是否支持流式返回,不支持就建议另选服务或加一层缓冲优化。
最后再多说一句
我个人的体会是,协议选型这件事,没有绝对的“对错”,只有“匹配不匹配”。如果你的产品核心体验是文字的流式输出,WebSocket 就是你最好的朋友;如果核心体验是语音的实时对话,那 WebRTC 的投入是绕不开的。从我踩坑的经验来看,最怕的不是选错,而是选了之后既不挖掘能力上限,也不提前规划演进路径,最后在项目中期被性能问题拖死。
再分享一个小技巧:不管选哪条路,都建议把音频层面的“业务逻辑”和“传输逻辑”彻底解耦。前端只负责采集和播放,传输层只负责搬运;这样即使未来从 WebSocket 切换到 WebRTC,你也不需要重写业务代码,只需要换一个 transport 适配层。我后来把语音接入从 WebSocket 切到 WebRTC 时,就是靠这个抽象把改动控制在几天之内。这个设计思路,比纠结某一个协议本身更值钱。
