在我们最新的测试中,首个可见 token 显示最快的模型,其完成任务的速度却是最慢的。在 2026-10-03,我们通过四个模型流式传输了一个提示词。claude-sonnet-5-5 在首个可见 token 时间上领先(2,981 毫秒),但在总耗时上垫底(12,384 毫秒)。这种差距正是 LLM 延迟与吞吐量的核心所在:一个数字代表用户等待看到文本的时间,另一个数字代表工作完成的速度。本文定义了每个指标,展示了我们的小规模第一方测量结果及其局限性,并提供了一个可针对 TokenLab API 运行的测试工具。
关键要点
- 三个数字,三种用途。 首个 token 时间(TTFT)、总耗时和每秒输出 token 数回答的是不同的问题。聊天、智能体(agents)和批处理任务各自侧重于不同的指标。
- 将首个事件与首个可见 token 分开。 在
deepseek-v4.1-flash上,这两个中位数相差 2,334 毫秒和 3,925 毫秒。推理增量(reasoning deltas)可能在任何可见文本出现之前就已到达。 - 推理 token 会消耗你的
max_tokens配额。 在max_tokens为 400 时,deepseek-v4.1-flash返回了约 0 个可见 token,而gemini-3.8-flash返回了约 14 个。在 2000 时,两者均完整回答。 - 从 usage 块中读取 token 计数。 不要通过字符数来估算。
- 模型速度不会提高你的请求限制。 速率限制、token 限制和模型速度是独立的约束条件。
- 我们的样本量很小。 它包含每个模型五次运行、一个客户端、一个提示词和一天的数据。它展示的是如何测量,而不是谁最快。
三个速度指标:实践中的 LLM 延迟与吞吐量
将“速度”拆分为多个部分,因为每一部分都有不同的责任方:网络、模型和你自己的队列。
发送请求
-> 首个流事件 (任何 SSE 块;可能是推理增量)
-> 首个可见内容 token (第一个非空的 delta.content)
-> 最后一个 token + 最终 usage 块
首个 token 时间 (TTFT) 是从发送请求到第一个 token 到达的时间。我们记录了两个版本。首个流事件是任何类型的第一个 SSE 块。首个可见内容 token 是第一个 choices[0].delta.content 非空的块。对于在回答前进行推理的模型,第一个事件可能是远早于可见文本到达的推理增量(delta.reasoning_content)。仅监视 delta.content 的测试工具测量的是首个可见 token 的时间,而不是首字节时间。用户感知到的是可见的那一个。
总耗时 从发送请求开始,到流结束为止。它包括推理时间、网络时间和你添加的任何重试等待时间。
每秒输出 token 数 是输出 token 总数除以总耗时。从 usage.completion_tokens 中获取 token 计数,其中包含推理 token。我们除以总耗时,而不是可见窗口时间。我们样本中的某些模型以几个大块的形式交付可见文本,因此基于窗口的速率会产生误导。
| 工作负载 | 优先关注 | 其次关注 | 原因 |
|---|---|---|---|
| 聊天 | 首个可见 token | 总耗时 | 在可见文本出现前,用户面对的是空白框。 |
| 智能体 | 总耗时 | 首个可见 token | 每一步通常都依赖于上一步,因此每一步的总耗时会累加。 |
| 批量提取或摘要 | 每秒输出 token 数,以及任务成本 | 请求限制余量 | 没人会盯着第一个 token 看,且账户的请求限制决定了你提交任务的速度。 |
非流式调用(如结构化 JSON 提取)仅暴露总耗时。如果想要拆分数据,请在测试中开启流式传输。
我们在 2026-10-03 的测量结果
方法。 这是 TokenLab 的第一方测量,于 2026-10-03 针对 POST https://api.tokenlab.sh/v1/chat/completions 进行观察(来源)。每个请求均使用 stream=true 和 stream_options.include_usage=true。我们从东亚的一个客户端使用 Node 24 fetch 对每个模型进行了 5 次连续运行。提示词为“Explain how HTTP keep-alive reduces latency for API clients. Write about 200 words.”。每个模型均使用默认推理设置。计时为客户端挂钟时间,下表中的每一行所有运行均成功(5/5)。
模型 (max_tokens 2000) |
首个流事件中位数 | 首个可见 token 中位数 | 总耗时中位数 | 完成 token 中位数 (推理) | 估算 tokens/s |
|---|---|---|---|---|---|
deepseek-v4.1-flash |
2,334 ms | 3,925 ms | 5,204 ms | 453 (225) | 87 |
gemini-3.8-flash |
6,892 ms | 6,892 ms | 8,626 ms | 826 (528) | 96 |
claude-sonnet-5-5 |
2,981 ms | 2,981 ms | 12,384 ms | 418 (未报告) | 34 |
gpt-5.6-luna |
3,262 ms | 3,262 ms | 8,183 ms | 313 (42) | 38 |
tokens/s 列是我们自己的估算值:完成 token 中位数除以总秒数中位数,例如 453 ÷ 5.204 ≈ 87。这是中位数的比率,而非单次运行速率的中位数。
数据所展示的内容(仅限以下):
- 按首个可见 token 排序(
claude-sonnet-5-5,gpt-5.6-luna,deepseek-v4.1-flash,gemini-3.8-flash)与按总耗时排序(deepseek-v4.1-flash,gpt-5.6-luna,gemini-3.8-flash,claude-sonnet-5-5)不同。一个指标不能替代另一个。 deepseek-v4.1-flash在首个事件和首个可见 token 之间有 1,591 毫秒的差距。其可见输出中位数为约 228 个 token,在 1,304 毫秒内通过 87 个事件交付。- 对于
gemini-3.8-flash,首个事件和首个可见 token 的中位数均为 6,892 毫秒。其可见文本在 1,734 毫秒内通过 11 个事件交付,因此流是块状的,而非逐个 token。 claude-sonnet-5-5在 9,186 毫秒内发送了 371 个事件,而gpt-5.6-luna在 4,901 毫秒内发送了 265 个事件。
成本与速度并存。以下价格来自 TokenLab 模型 API,观察于 2026-10-03,定价更新于 2026-10-02T16:53:30.068Z。最后一列是估算值:完成 token 中位数乘以输出费率(仅输出端)。请查看 usage 记录以获取实际费用。
| 模型 | 输入/1M | 输出/1M | 最大输出 token | 输出端估算,中位数运行 | 来源 |
|---|---|---|---|---|---|
deepseek-v4.1-flash |
$0.15 非高峰, $0.30 高峰 | $0.60 非高峰, $1.20 高峰 | 384,000 | 453 × $0.60 / 1M ≈ $0.00027 非高峰, ≈ $0.00054 高峰 | 模型 API |
gemini-3.8-flash |
$0.375 | $1.875 | 65,536 | 826 × $1.875 / 1M ≈ $0.00155 | 模型 API |
claude-sonnet-5-5 |
$0.60 | $3 | 128,000 | 418 × $3 / 1M ≈ $0.00125 | 模型 API |
gpt-5.6-luna |
$0.06 | $0.36 | 128,000 | 313 × $0.36 / 1M ≈ $0.00011 | 模型 API |
对于 gpt-5.6-luna,当输入最多为 272K token 时适用上述费率。超过该值,完整请求将使用 2 倍输入和 1.5 倍输出费率。
本样本的局限性。 从东亚的一个客户端进行的五次连续运行是一个很小的切片。提示词只是一个简短的论文请求,因此结果会随地区、时间、提示词和输出长度而变化。我们没有测试并发性,因此这些数据无法说明负载下的情况。请将该表视为你自己的测试模板,并在生产环境所在的地区运行。如果你想要更广泛的模型视图,TokenLab 开发者 LLM API 排行榜是一个起点。在信任任何速度数据之前,请检查其方法和日期。也不要随意更换模型:可靠性指南(观察于 2026-10-03)指出,替换模型可能会改变质量、价格、上下文长度和工具支持。
推理 token 与 max_tokens:为什么 400 返回几乎为空
我们还运行了 max_tokens 设置为 400 的相同提示词。方法相同,五次运行相同,日期相同(来源)。
模型 (max_tokens 400) |
完成 token 中位数 | 推理 token 中位数 | 可见输出 (约) | 首个可见 token 中位数 | 总耗时中位数 |
|---|---|---|---|---|---|
deepseek-v4.1-flash |
400 | 400 | 0 tokens | 3,795 ms | 4,693 ms |
gemini-3.8-flash |
396 | 382 | 14 tokens | 4,199 ms | 4,240 ms |
claude-sonnet-5-5 |
400 | 未报告 | 400 tokens | 3,088 ms | 11,078 ms |
gpt-5.6-luna |
317 | 42 | 275 tokens | 3,040 ms | 8,083 ms |
中位数是按列计算的,因此一行的各列不一定描述同一次运行。这就是为什么 deepseek-v4.1-flash 显示了首个可见时间,旁边却是零可见事件的中位数。
结论很明确。deepseek-v4.1-flash 和 gemini-3.8-flash 将近 400 个 token 的配额几乎全部花在了推理上,几乎没有返回可见文本。将 max_tokens 提高到 2000 产生了完整的回答。推理 token 计入 usage.completion_tokens。它们在 usage.completion_tokens_details.reasoning_tokens 中单独报告。
当我们看到第一个 400 token 的结果时,失败表现为空回答,而不是预算问题。通过以下三个检查可以使其显现:
- 读取最终的 usage 块(通过
stream_options={"include_usage": True}启用)。如果reasoning_tokens接近completion_tokens且文本为空,则预算被用于推理。 - 检查
finish_reason。流式传输指南(观察于 2026-10-03)将length列为达到 token 限制时的值。 - 与模型的最大输出进行比较。例如,模型 API 列出
deepseek-v4.1-flash为 384,000,gemini-3.8-flash为 65,536,因此 400 只是模型允许范围的一小部分。
聊天完成创建参考(观察于 2026-10-03)还列出了 max_completion_tokens 和 reasoning_effort。它指出支持的值取决于模型,因此在设置前请阅读模型页面。我们没有测试它们,并且我们以默认推理运行了每一行。根据你自己的提示词调整上限:在我们的运行中,200 字的回答产生了约 228 到 418 个可见 token,外加最多 528 个推理 token。
针对 TokenLab 基础 URL 的测量工具
此脚本使用 TokenLab 基础 URL,从 usage 块读取 token 计数,并将首个流事件与首个可见内容 token 分开记录。它还保留响应头以供后续调试。它遵循文档中的客户端设置(timeout=30.0, max_retries=0),因此 SDK 不会向你的计时中添加隐藏的重试。模型 ID 是我们测量过的;你可以替换为任何接受的请求格式包含 openai_chat_completions 的 ID。
import json
import os
import statistics
import time
from openai import OpenAI
client = OpenAI(
api_key=os.environ["TOKENLAB_API_KEY"],
base_url="https://api.tokenlab.sh/v1",
timeout=30.0,
max_retries=0,
)
PROMPT = "Explain how HTTP keep-alive reduces latency for API clients. Write about 200 words."
def measure(model: str, max_tokens: int = 2000) -> dict:
start = time.perf_counter()
raw = client.chat.completions.with_raw_response.create(
model=model,
messages=[{"role": "user", "content": PROMPT}],
max_tokens=max_tokens,
stream=True,
stream_options={"include_usage": True},
)
headers = raw.headers
first_event = first_visible = None
visible_events = 0
usage = None
finish_reason = None
for chunk in raw.parse():
now = time.perf_counter() - start
if first_event is None:
first_event = now
if chunk.usage is not None:
usage = chunk.usage
if not chunk.choices:
continue
choice = chunk.choices[0]
if choice.delta.content:
visible_events += 1
if first_visible is None:
first_visible = now
if choice.finish_reason:
finish_reason = choice.finish_reason
total = time.perf_counter() - start
details = getattr(usage, "completion_tokens_details", None)
completion = usage.completion_tokens if usage else None
return {
"model": model,
"request_id": headers.get("x-request-id"),
"routing_time_ms": headers.get("x-routing-time-ms"),
"first_event_s": first_event,
"first_visible_s": first_visible,
"total_s": total,
"completion_tokens": completion,
"reasoning_tokens": getattr(details, "reasoning_tokens", None),
"visible_events": visible_events,
"finish_reason": finish_reason,
"tokens_per_s": completion / total if completion else None,
}
if __name__ == "__main__":
runs = [measure("gemini-3.8-flash") for _ in range(5)]
print(json.dumps(runs[0], indent=2))
print("median total_s:", statistics.median(r["total_s"] for r in runs))
关于读取输出的几点说明:
first_event_s在任何块上触发,包括角色专用块或推理块。first_visible_s仅在非空的delta.content上触发。如果两者相差数秒,则模型在回答前正在进行推理。- Token 计数来自最终的 usage 块。该块具有空的
choices列表,这就是为什么循环在跳过它之前会检查chunk.usage。 - 运行五次或更多次,记录地区,并报告中位数以及样本量。
- 对于列出
openai_responses的模型,流式传输指南显示 Responses 以response.output_text.delta和response.completed事件流式传输。同样的拆分适用:计时第一个事件,然后是第一个文本增量。 - 将流在
response.completed或stop完成原因之前关闭的情况视为不完整,而不是快速成功。
用于延迟调试的标头和标识符
如果只保留持续时间,慢速请求将很难调试。API 参考和故障排除指南(均观察于 2026-10-03)命名了值得保留的标识符。
| 标识符 | 出现位置 | 用途 |
|---|---|---|
X-Request-ID |
响应头(可用时) | 单个请求的支持和调试 |
request_id |
错误主体、仪表板日志、使用记录 | 请求级支持和对账 |
X-Routing-Time-MS |
响应头(可用时) | 生成前选择交付所花费的时间 |
X-Task-ID, task_id, poll_url |
异步创建和状态响应 | 图像、视频、音乐和 3D 作业 |
X-Billing-Transaction-ID |
最终计费后的标头(可用时) | 成本对账 |
| 你自己的作业或用户 ID | 你的应用程序 | 查找用户投诉背后的请求 |
对于每次调用,记录端点、方法、模型、状态码、时间戳和延迟。添加你发送的字段名称(不是提示词)、重试次数,以及重试是创建了新任务还是恢复了旧任务。我们的测试工具添加了拆分计时和来自 usage 块的 token 计数。
两点警告。流式响应可能会在流结束后结算,因此即使稍后记录了使用情况,计费标头也可能不存在。对于计费金额,请使用管理 API 使用记录,而不是仅使用响应 token 计数。如果你设置了 X-TokenLab-Delivery-Policy 值,也请记录它。参考文档称它接受 auto、verified 或 official,无效值返回 400,不可用的选项返回 503 并带有 code: delivery_tier_unavailable 和 retryable: true。切勿记录 Authorization、API 密钥、签名 URL、完整提示词或个人数据。
控制台请求详情自捕获起保留 30 天,因此请将你需要的 ID 复制到你自己的日志中。在我们的流水线中,我们将这些 ID 与请求视图对齐;TokenLab 请求控制台文章涵盖了该视图。
速率限制、重试与 LLM 延迟与吞吐量
流速度和账户吞吐量是不同的上限。速率限制指南(观察于 2026-10-03)列出了下方的标准层级值。它指出它们可能会随活动配置而变化,并按 API 密钥强制执行。
| 层级 | 每分钟请求数 |
|---|---|
| 用户 | 1,000 |
| 合作伙伴 | 10,000 |
| VIP | 10,000 |
当你超过限制时,API 会返回 429 rate_limit_exceeded 以及 Retry-After 标头。指南示例主体带有 "retryable": true 和 "retry_after": 8。指南建议信任返回的 X-RateLimit-Limit 和 Retry-After 值,而不是任何复制的表格。它还指出更快的模型不会增加你账户的请求限制。
以下是批处理工作的算术逻辑。这是我们根据上述中位数得出的估算,而非负载测试。并发流大致等于每秒请求数乘以每个请求的秒数。在每分钟 1,000 个请求(约每秒 16.7 个)时:
- 使用
deepseek-v4.1-flash(总耗时中位数为 5.204 秒),大约同时运行 87 个流。 - 使用
claude-sonnet-5-5(中位数为 12.384 秒),大约同时运行 206 个流。
较慢的流意味着需要更多的并发量才能达到相同的请求速率。达到限制后,会在生成时间之上增加等待时间。token 限制未包含在我们使用的页面中,因此请检查你的账户和模型页面。
如何在不破坏数据的情况下进行重试:
- 存在时请遵守
Retry-After。否则使用带有随机抖动和硬性尝试上限的指数退避。 - 将 SDK 设置为
max_retries=0并在一个地方进行重试。否则两层都会重试同一个调用,你的计时会重复计算。 - 切勿重试验证、身份验证、余额或权限错误。使用短退避重试瞬态
5xx响应。 - 在第一个事件到达后,中断的流是不完整的,不会自动重启。重复请求可能会产生不同的输出并产生额外费用。
- 将重试次数和等待时间与模型时间分开记录,这样受限的运行就不会看起来像慢速模型。
这是指南的边界重试模式:
import random
import time
from openai import RateLimitError
def chat_with_rate_limit_retry(client, model, messages, attempts=4):
for attempt in range(attempts):
try:
return client.with_options(max_retries=0).chat.completions.create(
model=model,
messages=messages,
)
except RateLimitError as exc:
if attempt == attempts - 1:
raise
header = exc.response.headers.get("Retry-After")
wait = float(header) if header else min(30, 2 ** attempt + random.random())
time.sleep(wait)
还要限制并发,而不仅仅是平均每分钟请求数,并对突发流量进行排队。跟踪每个 API 密钥的重复 429 响应,以便一个客户端不会挤占你应用程序的其他部分。
常见问题解答
更高的每秒 token 数总是意味着更快的用户体验吗?
不。在我们 2026-10-03 的样本中,claude-sonnet-5-5 显示了最早的首个可见 token(2,981 毫秒),但总耗时最长(12,384 毫秒)。deepseek-v4.1-flash 较晚开始显示文本(3,925 毫秒),但最先完成(5,204 毫秒)。哪一个感觉更快取决于用户是盯着流看还是等待完整回答。从一个客户端进行的五次运行无法为你的工作负载确定这一点。
为什么我的流式响应返回时几乎没有文本?
你可能把预算花在推理上了。在 max_tokens 为 400 时,deepseek-v4.1-flash 使用了 400 个推理 token,返回了约 0 个可见 token。检查最终 usage 块中的 usage.completion_tokens_details.reasoning_tokens 和 finish_reason。然后提高上限;在 2000 时,我们测试中的两个推理模型都完整回答了。
更快的模型会提高我的速率限制吗?
不会。速率限制指南指出模型速度、token 限制和账户速率限制是独立的约束条件。更快的流完成得更早并释放了并发槽位,但你的每个密钥请求限制保持不变。如果你需要,请向支持部门申请不同的限制。
流变慢时我应该向支持部门发送哪些 ID?
发送 request_id(或 X-Request-ID)、端点、方法、模型、时间戳和状态码。异步工作添加 task_id 和 poll_url,存在时添加 billing_transaction_id。包括你的拆分计时和安全的错误主体,并先编辑掉密钥、提示词和私人媒体。
在你的提示词上运行上述测试工具,然后将你的测量结果与 TokenLab 模型排名进行比较。
来源
价格更新于 2026-10-03
- TokenLab Docs: Streaming资料更新于 2026-10-03
- TokenLab Docs: Rate limits资料更新于 2026-10-03
- TokenLab Docs: Logs and troubleshooting资料更新于 2026-10-03
- TokenLab Docs: Build a reliable TokenLab integration资料更新于 2026-10-03
- TokenLab Docs: API Reference资料更新于 2026-10-03
- TokenLab Docs: Create Chat Completion资料更新于 2026-10-03
- TokenLab live model API: deepseek-v4.1-flash资料更新于 2026-10-03
- TokenLab live model API: gemini-3.8-flash资料更新于 2026-10-03
