考察点
流式输出是每个 LLM 应用的标配,但这道题考的不是"会用 EventSource",而是全链路理解:SSE 协议本身的格式和约束、网关到推理引擎的流式链路怎么搭、以及最见功底的异常处理——客户端断连了推理还在烧钱怎么办、网络抖动数据丢了怎么恢复。面试官常从"用户关掉页面后你的 GPU 还在跑吗"这种问题切入,考资源回收意识。
参考答案
SSE 的基本面
SSE(Server-Sent Events)是基于 HTTP 长连接的服务端单向推送协议,实现极其简单:响应头 Content-Type: text/event-stream,保持连接不断开,服务端按格式持续写数据:
data: {"choices":[{"delta":{"content":"你"}}]}
data: {"choices":[{"delta":{"content":"好"}}]}
data: [DONE]
每个事件以 data: 开头、空行结尾。相比 WebSocket,SSE 走普通 HTTP,过代理过网关零障碍、自动重连(浏览器 EventSource 自带)、协议轻,且 LLM 场景本来就是服务端单向推送,不需要双向通信——所以 OpenAI 带起的行业标准就是 SSE。注意几个工程细节:响应要禁缓冲(nginx 加 X-Accel-Buffering: no),否则代理层攒批推送,"流式"名存实亡;心跳要设计,长时间无 token(比如工具调用卡了 10 秒)连接可能被中间层掐断,定期发 : ping 注释行保活。
推理侧的流式链路
服务端链路:客户端 → API 网关 → 推理服务(vLLM/TGI)。推理引擎 decode 每产出一个 token 就通过异步迭代器吐出来(vLLM 的 AsyncLLMEngine.generate 返回 async generator),服务端逐 token(或小批量攒几个,减少包数)封装成 SSE 事件推下去。这里有个权衡:逐 token 推送最流畅但包数量大,按 2-4 个 token 或几十毫秒攒批能显著降低网络开销,感知差异很小。另外增量文本涉及 detokenize 的边界问题——一个 token 可能是半个 UTF-8 字符,直接转发增量 token 字符串会出乱码,正确做法是推理侧输出增量文本时处理字符边界(vLLM 内部已处理),自己手写 tokenizer 链路的要留意。
客户端断连:最关键的资源回收
用户关掉页面,TCP 连接断开,如果服务端没感知,推理引擎会继续把这个请求生成完——GPU 白烧,高并发下这是实打实的成本漏洞。处理分三层:
- 服务端感知:往 socket 写数据时发现连接已关闭(写失败/收到 RST),框架层面 aiohttp/FastAPI 会抛异常或触发取消回调。
- 取消推理:感知到断连后必须显式 abort 推理引擎里的请求(vLLM 的
engine.abort(request_id)),释放 KV Cache 槽位,continuous batching 的调度器会立刻把空出的位置给排队请求。只断 HTTP 不 abort,等于漏掉一半。 - 网关层传递:链路中每一跳(nginx、网关、业务服务)都要把连接关闭事件向后传递,任何一跳"吞掉"断连信号,上游就继续空转。nginx 默认
proxy_http_version 1.1+ 正确配置可以把客户端关闭传给后端。
网络中断与恢复
SSE 协议本身支持断点续传:每条事件可带 id: 字段,浏览器 EventSource 断线重连时自动带 Last-Event-ID 头,服务端据此续发。但 LLM 场景特殊:生成是实时计算,不是消息队列,中间断了几秒的 token 通常没地方补。务实的策略是:
- 生成过程落一份增量结果(Redis/内存),重连时先把已生成部分一次性补发,再接着流式推新 token——前提是推理没被中断。
- 如果断连导致推理已 abort,那就无法续传,客户端重试时重新发起完整请求,可以把已生成的部分作为上下文让模型续写,但更常见的是直接重新生成(答到一半的答案业务上往往也不可用)。
- 超时与取消要成对设计:客户端设置 idle 超时(比如 60 秒无新 token 主动断开重试),服务端设置最大生成时长兜底,防止极端请求挂死占槽。
一句话给面试官
流式输出的协议层很简单,真正的工程量在连接生命周期和推理生命周期的对齐:连上的时候要流得顺(禁缓冲、心跳、攒批),断开的时候要收得干净(感知、abort、释放显存),恢复的时候要有明确策略(补发 or 重试)。
可能的追问
- 为什么不用 WebSocket? LLM 输出是单向流,WebSocket 的双向能力用不上;SSE 基于 HTTP 天然穿透代理、浏览器自动重连、实现简单。只有需要客户端实时上行(语音流、实时打断输入)才上 WebSocket。
- 用户刷新页面,怎么保证不重复扣费/重复生成? 断连即 abort,计费按实际生成 token 结算;幂等性上给请求带 client_request_id,重试时服务端能识别是同一逻辑请求,避免重复执行副作用操作。
- 大模型生成中调工具,工具执行 30 秒,连接会怎样? 无 token 输出期间连接可能被 LB/网关 idle 超时掐断。对策:工具执行期间定期发心跳或进度事件(
: ping或自定义 event),同时把各层 idle timeout 调到大于最长工具耗时。