这篇学完你能回答什么
- 流式输出到底怎么实现的?SSE 和 WebSocket 各自的边界在哪,什么时候必须上 WebSocket?
- 流式一旦上线,会连带毁掉哪些东西——结构化输出、内容审核、限流精度、计费口径?
- 实时语音链路怎么搭?级联 ASR→LLM→TTS 与端到端语音模型各自的代价是什么?
从一个真实故障讲起
一个对话产品,本地开发时流式很顺:敲完回车,字一个个往外蹦;预发环境也正常。上线第二天用户反馈"AI 变卡了"——具体表现是转圈 8 秒,然后整段文字一次性砸出来。
团队查了三天:先怀疑模型慢了,拉日志发现首字延迟稳定在 380 毫秒,正常;再怀疑前端渲染,改了状态更新方式,没用;又回滚了两个 SDK 版本,还是一样。最后有人用 curl 直接打后端服务,流是正常的;打生产域名,就是攒着一次性出。
真因是生产链路上多了一层 nginx,而 proxy_buffering 默认就是 on——它把上游响应先攒进缓冲区,攒够或流结束才往下发。开发和预发直连服务,只有生产走网关,所以三个环境行为不一样。
修复只有一行配置。但故障留下了更值钱的东西:团队从此把 TTFT 拆成两个指标——
Pipeline TTFT :第一个字节到达客户端 ← 对外 SLO 用这个 Model TTFT :推理返回第一个 delta ← 隔离模型本身 两者背离时,问题一定在"非模型环节":网关缓冲、检索、上下文拼装、鉴权
这正是第 5 章那把刀的又一形态:固定住一个变量,把问题一分为二。 Q2-17 固定文档切检索侧与生成侧,金丝雀集固定输入切流量侧与系统侧,这里固定"模型"这一段切模型侧与链路侧。能主动把 TTFT 拆成两个的人,一定排过障。
用户反馈「AI 变卡了」
转圈 8 秒,然后整段一次性砸出
先怀疑模型慢了
拉日志:首字延迟稳定在 380 毫秒
改前端、回滚 SDK
都没用,前后一共查了三天
curl直打后端服务流是正常的;打生产域名就攒着出
生产多一层 nginx断点
proxy_buffering默认就是 on
curl 直连固定了「模型」这一段,链路侧与模型侧就此切开 —— 两个 TTFT 一背离,问题一定不在模型。核心概念:先打比方,再给定义
SSE(Server-Sent Events,服务器推送事件)。它不是新协议,就是一次普通的 HTTP 响应,只不过响应体是长连接、按约定格式一帧一帧发。SSE 像单向广播电台——调到频道就一直听,但没法对着收音机说话;WebSocket 像对讲机,两头都能说。
流式帧格式。一帧就是几行文本,空行结束:
event: content_block_delta
data: {"type":"text_delta","text":"你"}
← 空行 = 一帧结束
: 注释行,常用作心跳WebSocket。HTTP 握手后把连接升级成全双工长连接,两端随时互发文本或二进制帧。一句人话:它买的是"双向"和"二进制",卖的是"有状态"。
barge-in(打断)。用户在 AI 说话时插话,系统要立刻停下来听——这是实时语音区别于"语音版聊天机器人"的分水岭。
VAD(Voice Activity Detection,语音活动检测)。判断用户在不在说话、说完了没有。server VAD 靠静音时长做声学切分,semantic VAD 用分类器读已说出的语义内容判断是否说完。一句人话:前者听"有没有声音",后者听"这句话完整了吗"。
调到频道就一直听
但你没法对着收音机说话
一帧就是几行文本,空行结束
: 开头是注释行,常用作心跳;规范只在网络错误时重连,非 200 或 Content-Type 不对属永久失败
event: / data:Last-Event-ID 续传HTTP/1.1 同域限 6 连接两头随时都能说
代价是这条线得一直占着,换个人接就得重来
买的是双向和二进制
卖的是有状态:绑死单实例,滚动发布踢掉活跃会话,断线补发没有对等机制
原理拆解
| 量的是什么 | 数值 | 读法 |
|---|---|---|
| 人类对话的轮次间隔 | 约 200 毫秒 | 实时语音要对齐的目标基线 |
| 说一个词的言语产出准备 | 600 毫秒 | 已经比轮次间隔本身还长 |
| 说一个短句的准备 | 1500 毫秒 | 是那 200 毫秒的 7.5 倍 |
| 那人是怎么做到的 | 边听边预测 | 不是反应快 —— semantic VAD 想模仿的正是这个 |
打断答到「用 VAD 检测」只是及格,完整链路三层:① 检测层,server VAD 听声音,semantic VAD 读语义判断说完没;② 执行层,必须发「截断」指令,把未播放的部分从服务端历史删掉,否则每一轮的上下文都是错的;③ 传输层,WebRTC 自动处理,裸 WebSocket 自己管。
浏览器端一定要有一个自建 AppServer 做 signaling 与临时凭证签发:原生 WebSocket 设不了 Authorization 头,WebRTC 的 SDP 交换又受 CORS 限制。声称「纯前端直连」的方案,一定在某处泄露了长期密钥。
一、三家 API 的流式:协议同构,语义完全不兼容
截至 2026-08,OpenAI、Anthropic、Gemini 的文本推理流式全部是 SSE over HTTP POST,没有一家用 WebSocket,但事件语义互不兼容:
| 厂商 | 事件形态 | 结束标志 |
|---|---|---|
| Anthropic | 块级状态机:message_start → content_block_start → 多个 content_block_delta → content_block_stop → message_delta → message_stop |
没有 [DONE],以 message_stop 结束 |
| OpenAI Chat Completions | delta 增量:chunk.choices[0].delta.content |
data: [DONE] |
| OpenAI Responses / Gemini Interactions | 语义事件流:response.output_text.delta 等 |
语义完成事件 |
推论:所谓"换个 base_url 就能切模型"在流式场景是假的。 网关必须写一层事件翻译,这是 T6-3 里"跨厂商 fallback 没那么便宜"的第一条证据。Anthropic 的 delta 还分四种:text_delta、input_json_delta(工具入参的部分 JSON)、thinking_delta、signature_delta——能说出 input_json_delta 的人一定接过工具流式,因为那是坑最多的地方。
二、SSE 的五个坑(都写在规范或官方文档里)
坑 1:EventSource 不能设请求头。 它的构造器只接受 url 和 { withCredentials },没有 header 参数,所以带 Authorization 的 API 永远不能用它直连。实践上一律改成 fetch() + ReadableStream 自己解帧,代价是规范白送的自动重连、Last-Event-ID 续传、retry: 退避全要自己重写。
坑 2:代理缓冲。 就是开头那个故障。nginx 的 proxy_buffering 默认 on;上游可发 X-Accel-Buffering: no 关掉,但这个头可能被网关的 proxy_ignore_headers 吃掉。
坑 3:空闲超时。 nginx 的 proxy_read_timeout 默认 60 秒,算的是"两次成功读取之间"。模型长时间思考不吐字,60 秒后连接就被切——表现是"流到一半断"。规范给的解法很明白:每 15 秒左右发一个注释行当心跳。
坑 4:HTTP/1.1 的 6 连接限制。 浏览器对同一域名最多开 6 个连接,SSE 长期占用一个;开到第 7 个标签页就永远连不上。Chrome 和 Firefox 都标成 Won't fix。HTTP/2 下不再是问题——变成协商的并发流,默认上限 100。
坑 5:失败与重连的区别。 规范规定网络错误才自动重连;状态码不是 200、或 Content-Type 不是 text/event-stream,属永久失败,不重连。所以后端返回 500 时 SSE 是"静默死掉"的,前端必须自己兜。
三、WebSocket 什么时候才必要
只有满足任一条时才值得付出代价:真双向(客户端要在响应过程中持续发指令)、二进制音视频帧、单连接多路复用。文本推理三条都不满足——所以三家都没用它。代价这一侧同样清楚:
- 浏览器原生 WebSocket 同样不能设
Authorization头(构造器只有url和protocols),浏览器端一律要走临时凭证; - 连接有状态,绑死单实例,滚动发布会踢掉活跃会话;SSE 每次是无状态 HTTP,重连即换实例;
- 断线补发要自己做——SSE 有规范级的
Last-Event-ID,WebSocket 没有对等机制。
一个 2026 的现成案例(截至 2026-08):MCP 规范在 2026-07-28 那版把传输层进一步去状态化——移除协议级 session 与握手,也移除了 SSE 流的续传与消息补发(断流就重发新请求)。这是"有状态可靠传输"的代价被公开承认的一次:粘连、无法水平扩展、CDN 与网关不友好,最后被无状态 + 客户端重试换掉了。
四、流式的四个连带后果
① 和结构化输出的冲突不在文本,在工具入参。 结构化输出本身与流式兼容。真正的坑是工具参数:Anthropic 官方警告 API 不会在流式过程中缓冲或校验工具入参,你可能收到"部分或非法的 JSON",且 max_tokens 截断会把参数切一半。两条路二选一:攒到 content_block_stop 再解析(牺牲 TTFT),或用容错解析器边流边补括号(牺牲正确性)。
② 响应侧护栏与流式架构性互斥。 Kong 官方原话是无法"对每个响应 token 查一次外部系统"。NeMo Guardrails 把取舍做成了参数:按 token 块检查(默认 200),而 stream_first 默认为 True——先吐给用户再检查。违规内容已经在用户屏幕上了,事后只能撤回 UI、撤不回截图;设成 False 则退化成每 200 token 一次审核延迟。
③ 限流精度必然下降。 Azure API Management 文档写得最直白:stream: true 时输入 token 强制估算(忽略配置),输出 token 也是估算——流式下要等流完才知道真实用量。
④ 计费与中断口径要自己实测。 三家官方文档都没明文说"客户端断开后上游会不会停止生成、按已生成还是已传输计费",不要假设。另外 OpenAI 流式默认不返回用量,要显式打开 stream_options.include_usage,且只在最后一个 chunk 返回。
五、实时语音:两条路的真实取舍
级联 麦克风 →[ASR]→ 文本 →[LLM]→ 文本 →[TTS]→ 音频
↑ 每一段都有中间文本态,可审可评可控
端到端(S2S) 麦克风 →[ 一个多模态模型 ]→ 音频
↑ 没有中间文本态人类对话的基线是轮次之间约 200 毫秒。但同一份研究给了更有说服力的对照:说一个词的言语产出准备要 600 毫秒,短句要 1500 毫秒——人类是靠"边听边预测"做到 200 毫秒的,不是靠反应快。这正是 semantic VAD 想模仿的东西。
不要背"端到端一定更快"。 各家博客给的总延迟互相矛盾(有说级联 2 秒、端到端 200–300 毫秒的,也有厂商反过来说"工程良好的级联比部分端到端模型更快"的),原因是级联各段在流式下是重叠的,名义串行加总本就不成立。可靠的说法是:级联的瓶颈从来在 LLM 那一段,不在 ASR/TTS,所以杠杆在模型选型和流式重叠,不在换架构。
端到端确定的代价有五条,每条都有出处:
- 上下文窄一个量级:实时语音模型多在 32K–128K,同期文本模型是 200K–1M。
- 音频 token 贵得多(截至 2026-08):OpenAI 音频输入是文本的 8 倍、输出 2.7 倍;百炼 omni-realtime 音频输入约 8.2 倍、含音频输出约 5.4 倍。
- 没有精确的文本-音频对齐——OpenAI 官方自己写的:打断截断时"会同时删掉未播放的音频和它对应的文本"。对齐不准 ⇒ transcript 不可靠 ⇒ 评测与合规审计都难做。
- 指令跟随会退化:有学术评测量化过语音模型相对其纯文本底座的"遗忘率",最差超过 −50%。
- 审核无法前置:级联能在用户听到之前过滤,端到端做不到——音频已经在扬声器上了。
打断答到"用 VAD 检测"只是及格。 完整链路是三层:
① 检测层:server VAD(静音时长)vs semantic VAD(读语义判断说完没)
后者才能正确处理"嗯…让我想一下""对对对"这类不该抢话的停顿
② 执行层:插话 → 取消进行中的响应 → 客户端必须发"截断"指令,
把【未播放的部分】从服务端会话历史里删掉。
不做:模型以为自己说完了整段,用户只听到一半,
此后所有轮次的上下文都是错的 ← 这个 bug 不上生产遇不到
③ 传输层:WebRTC / SIP 自动处理;裸 WebSocket 下播放与截断全靠自己管六、浏览器端为什么绕不开一个自建后端
这是 Q6-05 最锋利的一刀,三家殊途同归:
- 浏览器原生 WebSocket 设不了
Authorization头(MDN 构造器签名摆在那),不能拿长期密钥直连; - WebRTC 的 SDP 交换受 CORS 限制,也不能直接 POST 给厂商服务端——阿里云百炼官方文档逐字写了这条,并明确"正式使用时由业务 AppServer 代理完成";
- OpenAI 的
client_secrets、Google 的 ephemeral token,都是同一问题的另一种答法。
结论:浏览器端实时语音一定要有一个自建 AppServer 做 signaling 与临时凭证签发。任何声称"纯前端直连"的方案,一定在某处泄露了长期密钥。
至于 WebRTC 为什么比 WebSocket 适合语音:TCP 的队头阻塞会让丢包变成"先静音一段、再一口气爆出积压音频",而丢一个 20 毫秒的音频帧几乎听不出来,等 200 毫秒重传却非常明显。WebRTC 还自带抖动缓冲和媒体专用拥塞控制。好消息是接大厂 Realtime API 时 STUN/TURN 通常由厂商兜住(百炼文档明确写"无需配置 ICE 服务器")——只有自建 SFU、多方会议或私有化部署时,这笔成本才回到你头上。
工程实践(截至 2026-08)
选型判据
| 场景 | 选什么 | 理由 |
|---|---|---|
| LLM 文本流式输出 | SSE(fetch + ReadableStream,不用 EventSource) |
单向够用,无状态,重连即换实例 |
| 客户端要在生成中持续发指令 | WebSocket | 唯一真双向的选项 |
| 浏览器端实时语音 | WebRTC + 自建 AppServer 做 signaling | UDP 抗丢包、自带回声消除与抖动缓冲 |
| 服务端已有音频管道(电话/IVR) | WebSocket 或 SIP | 服务端无 CORS 与 header 限制 |
| 强可控 / 强合规的语音场景 | 级联 | 有中间文本态:审核可前置、可评测、可审计 |
| 追求自然感与情绪 | 端到端 | 接受上下文窄、音频贵、对齐不准 |
三家实时语音现状(截至 2026-08,易变,以官方为准)
| 厂商 | 接入方式 | 会话上限 | 备注 |
|---|---|---|---|
| OpenAI Realtime | WebRTC / WebSocket / SIP,浏览器官方推荐 WebRTC | 60 分钟(Azure 30 分钟) | 已 GA;支持 function calling 与 effort |
| Google Gemini Live | WebSocket | 纯音频 15 分钟 | Preview;官方已下线 half-cascade,只剩原生音频 |
| 阿里云百炼 Qwen-Omni | WebSocket / WebRTC / AOQ | 120 分钟 | 官方推荐 semantic_vad;WebRTC 内置回声消除 |
一个值得咀嚼的现实:Google 下线 half-cascade 后,官方论坛出现了持续数月的请愿——开发者反馈半级联版本工具调用成功率明显更高,而原生音频版本强制产出音频,纯文本场景被迫白付音频 token。"端到端"不是免费升级。
避坑清单
- 网关三件套先配好:
proxy_buffering off、15 秒心跳、proxy_read_timeout调大。 - TTFT 拆两个指标上报;语音场景另测"首个音频块"。
- 工具入参流式默认不开,除非已有容错 JSON 解析器且处理了截断。
- 上了流式别指望响应侧同步护栏:要么接受"先出后审"并做撤回 UI,要么关键场景关流式。
- 语音打断一定要做"截断已播放位置",否则会话历史会静默漂移。
| 场景 | 选什么 | 理由 |
|---|---|---|
| LLM 文本流式输出默认起点 | SSE(fetch + ReadableStream,不用 EventSource) | 单向够用,无状态,重连即换实例 |
| 客户端要在生成中持续发指令 | WebSocket | 唯一真双向的选项 |
| 浏览器端实时语音 | WebRTC + 自建 AppServer 做 signaling | UDP 抗丢包、自带回声消除与抖动缓冲 |
| 服务端已有音频管道(电话 / IVR) | WebSocket 或 SIP | 服务端没有 CORS 与 header 限制 |
| 强可控 / 强合规的语音场景 | 级联 | 有中间文本态:审核可前置、可评测、可审计 |
| 追求自然感与情绪 | 端到端 | 接受上下文窄、音频贵、对齐不准 |
- 网关三件套
proxy_buffering off、每 15 秒发一个注释行当心跳、proxy_read_timeout调大- 三家事件语义
- Anthropic 块级状态机、没有
[DONE];OpenAI Chat 是delta.content+[DONE] - 流式的四处连带损伤
- 限流精度降为估算、响应侧护栏架构性互斥、工具入参可能非法、计费口径要自己实测
- OpenAI Realtime
- WebRTC / WebSocket / SIP,浏览器官方推荐 WebRTC;60 分钟(Azure 30 分钟),已 GA
- Google Gemini Live
- WebSocket,纯音频 15 分钟,Preview;官方已下线 half-cascade,只剩原生音频
- 阿里云百炼 Qwen-Omni
- WebSocket / WebRTC / AOQ,120 分钟;官方推荐
semantic_vad,WebRTC 内置回声消除
Last-Event-ID 续传与 retry: 退避的规范。换成 fetch 手工解帧是在放弃这些能力 —— 这笔代价要算进选型里。面试视角
- 先定性SSE 不是新协议,是 HTTP 响应体的约定格式,单向、自带重连
- 再给判据真双向 / 二进制 / 多路复用,三条才值得上 WebSocket
- 主动抛一个坑代理缓冲,或
EventSource根本不能设请求头 - 语音题走五步澄清 → 基线走级联 → 加挂端到端并说代价 → 评测 → 演进
- 用打断收尾截断要把未播放的部分从服务端历史里删掉
- 只会说「SSE 单向、WebSocket 双向,看场景」,报不出一个事件名
- 「上线后不流式了」先怀疑模型和前端,不问中间过了几层代理
- 「前端用
EventSource就行」—— 不知道它带不了鉴权头 - 背「端到端更快」却说不出代价;打断只答到「用 VAD 检测」
- 认为浏览器可以直连厂商的实时语音服务
- 随口说出 Anthropic 没有
[DONE],并补一句网关得写事件翻译层 - 主动把 TTFT 拆成 Pipeline 与 Model,背离就说明问题在非模型环节
- 知道
X-Accel-Buffering: no可能被proxy_ignore_headers吃掉 - 打断答到「把未播放部分从服务端历史删掉」,且知道 WebRTC 自动处理
- 一句点破浏览器端必须有自建 AppServer,并说得出那两条约束
Q6-05 真正的考点在那个反问上:端到端更快,为什么不全用它。五条代价每条都有出处 —— 上下文只有 32K–128K、音频输入贵 8 倍、对齐不准、指令跟随遗忘率最差超 −50%、审核不能前置。面试官怎么问
答题结构建议
分水岭信号
暴露只读过:
- 只会说"SSE 单向、WebSocket 双向,看场景",报不出任何一个具体事件名
- 遇到"上线后不流式了"先怀疑模型和前端,不问"中间过了几层代理"
- 说"前端用
EventSource就行"——不知道它不能带鉴权头 - 背"端到端更快"却说不出它的代价;打断只答到"用 VAD 检测"
- 认为浏览器可以直连厂商的实时语音服务
证明真做过:
- 随口说出 Anthropic 没有
[DONE]、OpenAI Chat 有,并补一句"所以网关得写事件翻译层" - 主动把 TTFT 拆成 Pipeline 与 Model 两个指标,并说"两者背离就说明问题在非模型环节"
- 知道
X-Accel-Buffering: no可能被proxy_ignore_headers吃掉;知道换fetch手工解帧的代价是重连与续传全得自己写 - 说得出流式会同时让限流精度、响应侧护栏、结构化输出三件事降级
- 打断答到"必须把未播放部分从服务端历史里删掉",并知道 WebRTC 自动处理、裸 WebSocket 要自己管
- 一句话点破"浏览器端必须有自建 AppServer",并说得出两条约束(设不了 header、SDP 受 CORS 限制)
小结与延伸
-
SSE 不是"简单版 WebSocket",是一个自带重连与续传语义的规范。 换成
fetch手工解帧是在放弃这些能力。 -
流式的成本不在实现,在它连带毁掉的东西:限流变估算、响应侧护栏失效、工具入参可能非法、计费口径变模糊。这四条才是这道题的真正深度。
-
实时语音的取舍不是"快不快",是"有没有中间文本态"。 有就能审核前置、能评测、能审计;没有,换来的是自然感和情绪。
-
浏览器端一定要有自建后端,这是 Web 平台的既有约束,不是厂商懒。
继续深入
本篇归属第 6 章「部署与成本」,去做这一章的题。