基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-08 19:25

流式输出为何没改善整体延迟

dev.to · AI#流式输出#性能优化#大模型
Editor brief · 编辑速览

文章区分首字节时间、完整响应时间与吞吐量,指出流式输出主要改善感知延迟并支持提前取消。通过分块时间线探针分析端点表现,避免把首字节耗时全部归因于模型预填充。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

聊天窗口一开始往外吐字,就容易让人觉得模型变快了。但首字出现、正文持续输出、整段答案完成,是三件不同的事。这篇文章拆解了流式响应的四个常见误区:流式不等于提速,首 token 延迟不全是模型耗时,一个 chunk 也不等于一个 token,开发环境能流式更不代表生产环境能。

作者给出的办法很直接:记录首块时间、块间停顿和总耗时,再沿着真实请求链路定位问题。这个方向适合正在接入 AI 对话的前端和全栈开发者,不过附带的探针没有实测结果,代码本身也有几处需要修正。

字开始动了,为什么总耗时没降?

流式响应改变的是计算结果何时被用户看见。模型仍然要处理输入提示词,也就是预填充(prefill),然后逐个生成输出 token。把结果分批传出来,并不会省掉这些计算。

所以,最终字节到达的时间通常差不多,有时还会因分帧、分块传输等额外开销稍晚一些。作者没有提供流式与非流式的对照数据,这里讲的是工作机制,不能拿来推断某个接口一定慢多少。

流式真正有用的地方有两个:用户可以边生成边阅读;发现答案跑偏,也能在第 20 个 token 时取消,不必等到第 400 个 token。这能改善体验,也给提前终止请求提供机会,但不能直接算成吞吐量提升。

先确定你要缩短的是完整回答时间,还是用户看到第一段有用内容的时间。

二者甚至不能都用「首字时间」代替。接口先发一个角色信息或空 delta,客户端虽然收到了事件,用户却还没有东西可读。

这也是作者第二个提醒的落点:首 token 延迟(TTFT)是一包混合耗时。DNS、TCP 建连、HTTPS 的 TLS 协商、请求体上传、网关排队和鉴权,都可能发生在模型开始输出之前;随后还有预填充、首块发出,以及回程代理或压缩器的缓冲。

如果其中有 300 ms 花在 TLS 握手上,把整段都叫「模型思考时间」就会误导排查。作者建议先预热连接,再比较连接复用与冷连接的差距。这个比较能帮助判断:等待究竟来自模型,还是模型之外的链路。

一个 chunk 不等于一个 token,生产代理还可能把它攒起来

chunk 是传输和接口输出的分块单位,token 是模型处理文本的单位,协议没有保证二者一一对应。

OpenAI-compatible 服务可能合并多个 token,也可能先发只有 role 的 delta,或者在结束时返回带 usage、但没有正文的事件。因此,用 chunk 数除以耗时计算 tokens/s,会得到一个偏差不固定的数字。作者建议把解码后的字符数当作粗略参考,服务提供 usage 时再使用对应数据;缺失的 usage 应标成缺失,不能填成零。

接下来才是前端常遇到的部署问题:本地逐段显示,上线后却整段出现。

可疑位置包括开启响应缓冲的反向代理、等压缩块攒满才刷出的中间件、对 Server-Sent Events(SSE)做 gzip 的应用服务器,以及先读完整个响应体才返回的客户端库。nginx 的 proxy_buffering on 是作者点名的常见原因,对应处理包括响应头 X-Accel-Buffering: no,或在相关 location 设置 proxy_buffering off。

客户端读取方式也会改变结果。原文比较了 requests.get(..., stream=True) 配合 iter_content,以及会缓冲完整内容的 response.text;还提醒 Python 的文本 iter_lines() 可能在载荷中的 \r 处分行,探针因此采用 httpx 的异步行迭代器。

对接 AI 接口时,浏览器、Node 服务、网关和上游接口要作为一条链路检查。只证明上游会分块返回,还不能证明用户端会持续收到正文。

探针该记录什么,代码又漏了什么?

作者提出每次至少观察三项:首块到达时间、块与块之间的间隔,以及总耗时。探针要求 Python 3.10+ 和 httpx,通过 BASE_URL、API_KEY、MODEL 指向 OpenAI-compatible 的 /chat/completions,设置 "stream": True,默认运行 5 次,客户端超时为 60.0 秒。

下面是原探针里负责读取 SSE 和计时的关键部分:

                async for line in r.aiter_lines():
                    if not line.startswith("data:"):
                        continue
                    payload = line[5:].strip()
                    if payload == "[DONE]":
                        break
                    now = time.perf_counter()
                    if first is None:
                        first = now - t0
                    gaps.append(now - last)
                    last = now
                    delta = json.loads(payload)["choices"][0].get("delta", {})
                    if delta.get("content"):
                        content_deltas += 1

要看清 first = now - t0:它记录的是首个非 [DONE] 的 data: 事件,事件里未必有正文。因此,结果字段虽然叫 ttft_s,更准确的理解仍是首事件延迟。

另一处问题在 gaps.append(now - last)。原脚本把 last 初始化为请求开始时间 t0,所以第一项间隔包含首块等待时间。最终计算出来的 max_gap_ms,并不是纯粹的块间最大停顿。后面若拿它判断「首块慢但后续稳定」,就可能被这项初始等待干扰。

还有一个兼容性缺口:["choices"][0] 假设 choices 至少有一个元素,遇到空列表会报错。作者前面提醒过末尾事件可能没有正文,这段解析却没有完整防住这种情况。

原文另给了一段 curl 检查,适合先查看连接耗时、首字节耗时和响应头:

curl -sS -N -D /tmp/headers.txt -o /tmp/stream.txt \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"model\":\"$MODEL\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"Count to 40\"}]}" \
  "$BASE_URL/chat/completions" \
  -w 'connect=%{time_connect} first_byte=%{time_starttransfer} total=%{time_total}\n'

grep -i -E 'content-encoding|x-accel-buffering|transfer-encoding' /tmp/headers.txt

这里要注意 -N,它关闭的是 curl 自身的缓冲,不能替你关闭服务器或代理缓冲。

作者把时间形状分成三类:首块慢而后续间隔小,先查冷连接、长提示词或排队;首块快但中途长停顿,查缓冲、超时和上游重试;首块快、分块多、总时间又接近首块时间,可能只是生成内容很短。

这些都是排查线索。原文把 first_byte 接近 total 直接判成「没有流式」,说得过满,因为它自己也列出了短回答的情况,不能只凭两个数接近就定性。

我的判断

我赞同把模型性能与链路体验分开测量。对于聊天界面,首段正文出现得早不早、后面会不会长时间停住,比单报一个平均总耗时更有解释力。

但这篇提供的是诊断框架和未经执行的脚本,不是性能报告。作者明确说没有对读者的端点运行探针,也没给出证明 p95 改善的对照数据。默认跑 5 次适合先看现象,不能据此得出稳定的尾延迟结论。

相比直接换模型或升级实例,先检查连接复用和代理缓冲是合理的排查顺序;若证据最终指向模型解码速度,再讨论模型选择。批处理任务则应优先看总耗时和吞吐量,没必要照搬聊天界面的指标。

文章还披露它属于 MonkeyCode 的产品推广。免费模型访问和免费服务器都是运营方提供的可用性说法,不是基准测试。固定探针所在区域有助于比较,但这一方法并不依赖某个免费服务。

能拿来做什么

  • 给 AI 聊天界面分别记录首事件、首段非空正文和完整结束时间,让网络可达性与用户实际等待各有对应指标。
  • 在 Node 转发层或代理配置变更前后,用相同提示词和固定区域复测,并保留请求 ID,方便把一次停顿对应到日志。
  • 改造探针时先排除首次等待对 max_gap_ms 的影响,再处理空 choices、空 delta 和缺失 usage,避免测量代码制造错误结论。
  • 把连接复用测试与冷连接测试分开执行;先跑 5 次观察形状,再根据真实流量补充样本,评估 p95。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
用阶段凭据发现多智能体静默停滞
下一篇
防止智能体靠改测试数据蒙混过关

资讯速览

dev.to · AI · 2026-11-08 19:25

AI 评分 8.0 / 10

相关标签

#流式输出#性能优化#大模型

继续阅读

  • 更换编程 AI,如何保住项目决策
  • 用阶段凭据发现多智能体静默停滞
  • 防止智能体靠改测试数据蒙混过关
  • 给 AI 文档加上可持续校验的源码引用
  • 编程代理绕过网络隔离暴露沙箱风险