在一把聚合 key 上量首 token 时间与总延迟:为什么体感最快的后端并不是真最快
解释为什么在聚合 API 场景下要分别测量首 token 时间(TTFT)与总延迟(total latency),说明流式输出会掩盖后端真实速度差异,并给出一个可复现的测量方法,帮助你在同一把 key 下横向比较各模型后端的真实表现。
两个不同的指标被混为一谈
当你在同一把聚合 key 上切换模型时,最容易产生的错觉是:某个后端「体感最快」。但体感快慢其实来自两个完全不同的时间量:
- 首 token 时间(TTFT / 首字节时间):从发出请求到收到第一个可读 token 的时间。它决定你多久能在界面上看到「有反应」。
- 总延迟(total latency):从发出请求到流式结束、最后一段被读完的时间。它决定整段回答真正完成要多久。
两者可以不相关。一个后端可以先吐字、再慢吞吞地吐完;另一个后端静默很久、然后一口气刷满屏幕。不开流式时你只能看到后者,开了流式就只感觉到前者。
流式为什么会制造「假快」
流式(streaming)把响应切成增量返回,于是第一个 token 一到,界面就开始渲染。这意味着:
- 只要 TTFT 短,用户就认为「快」,无论后面还要等多久。
- 一个总耗时长、但 TTFT 短的后端,会击败一个总耗时短、但 TTFT 长的后端。
- 如果你只比较总耗时,就会误判;如果你只比较「看到第一个字」的时间,同样会误判。
更隐蔽的一点:在聚合站上,网关本身也会引入排队与转发开销,这部分常常体现在 TTFT 上,而不是总时间上。于是不同模型的体感差异,可能混着路由层的差异。
一个可复现的测量方法
目标是在同一把 key、同一网络环境下,把两个指标拆开记录。核心做法是:记录请求发出时刻、第一个增量到达时刻、流结束时刻。
概念流程如下(伪代码,与具体 SDK 无关):
t0 = now()
ttft = null
for chunk in stream(request):
if ttft is null:
ttft = now() - t0
last = now()
total = last - t0
要点:
- 必须用同一个 prompt(长度固定),并固定 max tokens 之类的上限,否则比较没有意义。
- 每个后端跑多次,看中位数而不是单次,网络抖动很大。
- 同时记录输出 token 数,用「总时长 ÷ 输出 token 数」估算生成速度,这比裸的总时间更可比。
- 关掉重试与自动降级,否则一次失败重试会污染数据。
三个常见误区
- 只测总耗时:在流式下,总耗时受输出长度支配。输出多的样本自然慢,但这不代表后端慢。
- 只测 TTFT:短 TTFT 可能只是拿到第一个 token 快,后续生成速率可能很低,长回答会拖很久。
- 用不同 prompt 对比:长度、复杂度不同,两个指标都会变。对比必须先控制变量。
怎么用这两个数字做决策
- 交互式场景(聊天、补全、IDE 助手):优先看 TTFT,因为它决定「有反应」的等待感。
- 批处理/长文生成:优先看生成速率与总延迟,TTFT 影响很小。
- 两者都重要时,看「TTFT + 生成速率」的组合,而不是单一数字。
在聚合站上用一把 key 调多个模型时,这套测量尤其有用:你能判断体感差异是来自模型本身,还是来自网关与路由。把 TTFT 与总延迟分开记录,你才能知道「感觉快」和「真的快」之间差在哪里。