流式输出与提前取消
流式输出通过 SSE 把生成结果边算边返回,首字延迟明显降低,同时带来一个省钱阀:用户点击停止、关闭页面或请求超时断开时,网关把断连传导到上游模型调用,生成立刻中止,之后没有产生的 token 不再计费。反过来,只断开自己这一侧的连接而不通知上游,是长回答场景里最常见的隐性浪费,账单上看不出问题,用量却在持续增长。
基本信息
| 传输方式 | SSE 或 HTTP 分块传输,服务端逐块推送生成结果 |
|---|---|
| 计费口径 | 按实际生成的 token 计费,中断后停止累计 |
| 关键链路 | 客户端断开、网关捕获、上游请求取消,三段必须打通 |
| 易漏点 | 排队中与重试中的请求、后台批处理任务、多路并发生成 |
| 观测指标 | 中断率、平均生成 token、中止后仍产生的 token 占比 |
| 适用层 | 自建网关、模型代理层、各厂商流式接口的调用封装 |
典型使用场景
- 长文生成与代码生成:用户往往看到前几段就决定停,中断即停能省下大半输出费用
- 对话产品的停止生成按钮:体验与成本同时受益,是性价比最高的成本优化之一
- 移动端弱网环境:网络中断频繁,不传导取消就会替用户把整段话生成完
- 批量任务的超时保护:超过时限的生成应当中止而不是继续占配额
- 成本复盘:把中断率与浪费占比纳入观测,定位隐性支出的来源
取消要传导到上游
取消要传导到上游
async def proxy(req):
upstream = client.stream(payload) # 打开指向上游模型的流式请求
try:
async for chunk in upstream: # 逐块转发给前端
yield chunk
except ClientDisconnect:
raise # 客户端走了,交给 finally 收尾
finally:
await upstream.aclose() # 关键一步:真正取消上游生成,停止计费
# 前端侧:AbortController.abort() 触发断连,服务端据此走上面的 finally浪费场景与处理
| 场景 | 原因 | 处理 |
|---|---|---|
| 用户关页面后仍在生成 | 断连未传导到上游 | 网关捕获断连并取消上游请求 |
| 超时后继续占额度 | 只在前端做了超时 | 服务端设置生成超时并主动中止 |
| 重试导致重复生成 | 超时重试但旧请求未取消 | 重试前先取消上一次请求 |
| 后台批量任务长尾 | 无单条上限 | 为每条生成设置 token 与时长上限 |
| 多路并发取首个结果 | 其余分支未取消 | 拿到首个结果后取消其余分支 |
服务端自身的生成超时应短于网关与客户端的超时,否则客户端已经放弃,服务端还在继续生成,等于用最贵的方式产生没人看的内容。
中断后要明确半截内容的处理策略:对用户标记为不完整、对计费按实际已生成量入账、对需要重试的场景记录断点,避免出现内容截断却按完整结果入库。