大模型应用为什么要做流式输出、取消和超时控制?
简化版
流式输出能显著降低用户感知延迟,取消和超时控制能避免无效生成继续消耗 token 与连接资源。生产系统通常用 SSE 或 WebSocket 返回增量 token,同时在服务端做超时、断连检测、重试和幂等处理。
详细版
- 首 token 延迟决定用户是否觉得系统“有反应”。
- 流式输出不等于总耗时变短,但能改善等待体验。
- 取消生成要通知模型服务或停止读取,否则可能继续计费。
- 超时要分连接超时、首 token 超时、总生成超时和工具调用超时。
- 前端、网关、后端和模型服务的超时要协调,否则容易出现半断连。
完整版教学
易错点:流式输出解决的是“感知等待”,取消和超时解决的是“资源泄漏”。两者必须一起设计。
一、流式、取消和超时分别解决什么
流式输出改善的是感知延迟:用户更早看到首 token,但模型仍需完成已生成 token 的计算。取消必须从浏览器沿网关、编排、模型客户端和工具传播,否则前端断开后后台仍在计费。
超时则定义各阶段和整条任务最多等待多久,防止连接永久占用。三者组合后,系统才能既早反馈、又允许用户停止,并在依赖失控时确定性收尾。
二、核心原理和工程边界
自回归模型逐 token 生成,天然适合边生成边返回。应用层通过 SSE 或 WebSocket 把增量 token 推给前端,前端边收边渲染。与此同时,后端要监听客户端断开和用户取消,把取消信号传到上游模型或中断请求链路。
三、带数字的工程算例
一次回答总生成耗时 12 秒。如果非流式,用户 12 秒后才看到内容;如果首 token 1.2 秒返回,之后每秒 20 token,用户会明显觉得系统更快。若 20% 用户在 3 秒内取消,不做取消传播会浪费大量输出 token。
用户感知延迟 ≈ 首 token 延迟,而不是完整生成耗时
浪费 token = 取消后继续生成的 token 数 * 取消请求量
总超时 >= 首 token 超时 + 预期输出长度 / 生成速率
四、典型链路怎么跑
可以把这类问题拆成下面的链路来理解:
前端发起请求
|
后端创建模型流
|
模型返回增量 token
|
SSE/WebSocket 推送前端
|
用户取消或断连
|
服务端传播 abort 信号并记录日志
五、方案对比和选择标准
| 方式 | 优势 | 适用场景 |
|---|---|---|
| 普通 HTTP | 实现简单 | 短回答、批处理 |
| SSE | 浏览器友好、单向流 | 聊天、生成式问答 |
| WebSocket | 双向实时 | 协同编辑、语音交互 |
| 轮询 | 兼容性强 | 异步任务进度 |
什么时候该流式、什么时候必须等完整结果,见「什么时候用流式输出,什么时候必须等模型完整返回?」;流式对话的消息什么时候落库、中途失败怎么收尾,见「流式对话的消息什么时候落库?中途失败、被截断或用户停止时怎么处理?」。
六、上线后最容易出问题的地方
-
只在前端隐藏加载状态不算取消,后端和上游仍可能继续执行。
-
代理层缓冲会破坏流式体验,需要确认 Nginx/CDN 是否关闭缓冲。
-
TTFT 从请求到首个内容 token,不能把连接确认或空心跳当首 token;TPOT 则要排除工具等待并单独报告。
七、Deadline 怎样跨层传播
入口生成绝对 Deadline,各下游根据 remaining = deadline - now 决定是否启动,并把更短的阶段超时传递给模型与工具。各层独立设置 30 秒会串行叠加,不能保证用户看到的总时限。
取消信号与 Deadline 都应幂等传播;写工具超时后若执行状态未知,不能简单重试,而要先查询 operation_id。超时是控制边界,不等于证明外部动作没有发生。
八、常见误区与追问
- 误区:流式输出能降低模型计算成本。 仅分块返回不会减少生成计算;只有用户提前取消且取消真正传到推理端时才可能节省后续 token。
- 误区:用户关页面后请求自然就完全停止。 代理和服务端可能继续读取上游响应,必须传播 cancel signal 并关闭工具与模型调用。
- 误区:所有超时设置成同一个值最简单可靠。 连接、首 token、token 间隔、工具和总 deadline 的故障含义不同,应分层设置且服从总预算。
- 追问:SSE 和 WebSocket 在 LLM 场景怎么选? 单向文本流优先 SSE,需双向实时控制或多路事件时选 WebSocket,同时考虑代理与重连语义。
- 追问:如何统计首 token 延迟? 从服务端接收请求开始,到收到并成功写出第一个非空内容 token,按 P50/P95/P99 报告。
- 追问:工具调用阶段如何向前端反馈进度? 发送受控状态事件如 searching/executing,不泄露思维链;事件要有序号并支持取消。
九、加强记忆
记住“流式管体验,取消管浪费,超时管边界”。面试时把前端、服务端、模型上游、代理层四处都讲到,答案就很完整。
项目实战落地
项目里怎么做的
《AI Agentic RAG高级企业知识库平台》的问答应用用 SSE 把答案流式推给页面:后端用 LangChain 的 astream 一段段取模型输出,每段立刻转成一个 delta 事件发出去,同时收集起来,跑完拼成完整答案落库。前端这一侧有几处要点:
- 不用浏览器原生的
EventSource。 它只支持 GET,也没法自定义请求头带 token,而问答接口是 POST 且要带 token,所以用fetch读流、自己解析 SSE。 - 处理残缺报文。 网络分片和 SSE 的事件边界没有关系,一次
read()可能读到两个半事件:
buffer += decoder.decode(value, { stream: true })
// 空行是一个事件的结束标志,按它切分
const blocks = buffer.split('\n\n')
// 最后一块可能是残缺的,留到下一轮再拼
buffer = blocks.pop()
- 中文不被切坏。
decode带上stream: true:一个中文字符占三个字节,可能被分片切开,不加这个参数会解码出乱码。 - 失败也发事件。 回答过程中抛异常时,把原因写进消息记录,再发一个
error事件。 - 光标在
finally里关。 正在接收时末尾显示一个闪动的竖条,接收完消失;成功失败都要关掉。
《AI Agent 智慧医院智能导诊就诊系统》后端用的是回调式的流式模型:回调在框架自己的线程上执行,方法要等生成结束才能拿到完整正文,所以用 CountDownLatch 等待并设超时,回调里收到的异常先存进 AtomicReference,回到本线程再统一翻译抛出。《AI智能客服与工单处理系统》的「停止生成」按钮调用 abort(),让 reader.read() 抛出异常、读取循环结束,页面不再追加新内容;教程写明这个按钮只作用在前端这一侧。
为什么这样取舍
- SSE 出错必须显式发事件。 普通接口抛异常,前端的
.catch()能收到;SSE 流中途出错,前端只知道流断了、不知道为什么,会一直转圈。 - 流式光标是标配。 没有它,用户分不清是在慢慢输出还是卡住了;失败时不关,光标会一直闪。
- 等待一定要带超时。 模型既不返回内容也不回调错误时,不带超时的等待会让线程一直挂着,连着的 SSE 连接也不会释放。
面试官还会追问
- 引用来源为什么在正文开始生成之前就发给前端?
- 一条资料都没召回时,这一轮还会调模型吗?页面上看到的是什么?
- 自动化测试怎么验证「答案被拆成很多段发出去,拼起来还是完整的」?
学完《AI Agentic RAG高级企业知识库平台》,上面这些追问你都会迎刃而解。