sse-streaming-latency-metrics · ZH · 2026-10-08

在聚合站上量首 token 延迟与总时长:哪个指标该盯

本文讲解在 LLM API 聚合站上如何区分并分别测量首 token 延迟(TTFT)与总时长(E2E),按模型维度做记录,解释流式输出「开始快、结束晚」的原因,并给出可落地的记录方法与选择指标的判断标准。

为什么这两个指标必须分开看

调用 LLM API 时,用户感知到的「快」其实混合了两件事:

  • 首 token 延迟(TTFT,Time To First Token):从发出请求到收到第一个输出 token 的时间。它决定用户按下回车后要盯着空白等多久。
  • 总时长(E2E,End-to-End Latency):从发出请求到最后一个 token 到达的时间。它决定整段回答什么时候真正结束。

聊天场景里,TTFT 主导「跟手感」;批处理、摘要、结构化抽取场景里,E2E 才是真正的瓶颈。把两者揉成一个「响应时间」平均数,等于把两种完全不同的体验压成一个无法诊断的数字。

流式输出为什么会「前快后慢」

聚合站通常以 SSE(Server-Sent Events)流式返回。一次请求大致分成几个阶段:

  1. 网络往返 + 认证 + 路由到上游渠道
  2. 上游排队与预填充(prefill):把整段 prompt 编码进 KV cache
  3. 逐 token 解码(decode):每生成一个 token 都要前向一次

第 2 步结束时就吐出了第一个 token,也就是 TTFT 的终点。之后进入第 3 步,token 一个一个来。所以:

  • 开头那一下很慢,是因为要等 prefill 完成;一旦开始吐字,用户的等待焦虑立刻大幅下降。
  • 结尾拖得久,是因为输出长度乘以单 token 解码耗时。输出越长,尾巴越长。

这解释了一个常见困惑:体验上「很快」,但整个请求统计出来「很慢」——其实是 TTFT 短、E2E 长,二者并不矛盾。

按模型分别记录,而不是全局平均

不同模型、不同上游渠道的 profile 差别很大。把 Claude、GPT、DeepSeek、Qwen、GLM、Kimi 的延迟混在一起求平均,得到的数字没有任何指导意义。合理做法是按 模型 × 渠道 维度分组记录。

每条记录至少包含:

  • model:请求的模型名
  • ttft_ms:首 token 到达耗时
  • e2e_ms:最后一个 token 到达耗时
  • output_tokens:输出 token 数(没有它就没法算吞吐)
  • input_tokens:输入长度,长 prompt 会抬高 prefill 时间
  • success / error_type:失败请求不能混进成功样本

派生指标

在两个原始指标之上,再算两个派生值,诊断力会强很多:

  • 解码吞吐(tokens/s)≈ (outputtokens − 1) / (e2ems − ttft_ms)。它反映「吐字速度」,和首字快慢是两回事。注意这里减掉第一个 token,因为它是在 TTFT 区间里产生的。
  • 尾部时长 = e2ems − ttftms。它才是真正随输出长度增长的那一段。

怎么测才靠谱

固定输入,只让被测变量变化

测延迟时最忌讳每次都换 prompt。建议:

  • 准备一个固定短 prompt 用于测 TTFT 基线(prefill 影响最小)。
  • 准备一个固定长 prompt 用于测长上下文下的 TTFT。
  • 准备一个要求固定输出长度(例如「输出约 200 个词」)的 prompt,用于测解码吞吐。

控制输出长度

E2E 与输出长度几乎线性相关。比较两个模型的 E2E 时,如果输出长度不同,结论会完全失真。要么用 maxtokens 卡住上限,要么在分析阶段按 outputtokens 做归一化。

取分位数,不取平均值

延迟分布是长尾的。平均值会被少数极慢请求拉高或掩盖。至少在每次采样后记录 p50 / p90 / p99,并关注尾部——用户抱怨的往往是 p99,不是 p50。

多次采样,避开冷启动

上游渠道可能有冷启动或换实例的开销。同一配置连续跑多次,丢弃前若干次预热请求,再统计稳定区间。

一个最小的记录流程

  1. 选定一批要长期观察的模型(覆盖你实际在用的那几个即可)。
  2. 对每个模型跑固定输入 + 固定输出长度的请求,重复若干次。
  3. 每次请求记录 ttftms、e2ems、output_tokens。
  4. 计算 p50/p90,以及解码吞吐。
  5. 按天/周留存,观察趋势,而不是只看某一次的快照。

在聚合站上,同一个 API key 就能切不同模型,因此这套流程可以用同一份脚本跑完全部模型,唯一要改的就是模型名。这是聚合接入在观测上的一个实际便利:对比矩阵里的变量更少。

该盯哪个指标:按场景选

| 场景 | 主看 | 次看 |
| --- | --- | --- |
| 对话 / 补全 / 代码助手 | TTFT | 解码吞吐 |
| 摘要 / 长文生成 | 解码吞吐 | E2E |
| 批处理 / 离线抽取 | E2E | 成功率 |
| 结构化输出 / JSON | E2E | 格式合法率 |
| 流式 UI 的「完成」提示时机 | E2E | TTFT |

判断逻辑可以更简单:如果用户体验是「边出边看」,就优先优化 TTFT;如果用户体验是「等一个完整结果」,就优先优化 E2E。

常见误判

  • 把流式的首包时间当成总时长。很多客户端在第一帧就上报「完成」,导致统计里只有 TTFT。要显式记录最后一帧的时间戳。
  • 拿不同输出长度对比 E2E。输出长一倍,E2E 大致也长一截,这不是模型变慢。
  • 只看平均值。p50 好看不代表没有长尾卡顿。
  • 忽略输入长度。长 context 会显著抬升 prefill,从而抬升 TTFT,但这不代表解码变慢。
  • 把失败请求的即时返回混进成功样本。报错返回很快,会人为把延迟曲线拉低。

小结

在聚合站上做性能观测,核心就三件事:把 TTFT 和 E2E 分开记、按模型分组、用分位数而非平均值。理解「流式体验快」来自低的 TTFT,「最后一帧来得晚」来自输出长度决定的解码时间,你就能判断到底是该换渠道、该压缩 prompt,还是该限制输出长度。