1. 逐 Token 生成为什么慢:从请求链路看推理耗时
大模型推理耗时这件事,很多人第一反应是"显卡不够快"。但真正上手排查过就会发现,同一个模型、同一张卡,换个 prompt 结构、改个并发参数,延迟能差出好几倍。问题往往不在算力峰值,而在逐 Token 生成的机制本身。
先把概念对齐。LLM 推理分两个阶段:Prefill 阶段把整段输入 prompt 一次性喂进去,算出所有层的注意力状态并写入 KV-cache,这个阶段决定 TTFT(首 Token 时间);Decoding 阶段在已有 KV-cache 基础上逐个吐 Token,每步只追加一个,这个阶段决定 TPOT(每 Token 耗时)。你感受到的"打字机效果"就是 Decoding 在逐 Token 输出。
关键点在于:Decoding 阶段每生成一个 Token,都要把模型权重从显存搬一遍。以 7B 模型 FP16 为例,权重约 14.2GB,而 GPU 的 L2 缓存通常只有几十 MB,根本装不下。于是每步都要从 HBM 重新读取权重,速度上限被显存带宽卡死。RTX 4090 带宽约 1008GB/s,理论下界就是 14.2/1008 ≈ 14.1ms/Token。这就是为什么输出越长越慢,而且慢得很线性。
所以定位推理耗时瓶颈,不能只盯着"模型大不大",要拆成三段看:请求链路(网络+排队)、Prefill(TTFT)、Decoding(TPOT)。本文就按这个顺序,用 TaoToken 统一通道接入,配合可复制的 config.toml 和 settings.json,把逐 Token 耗时测出来、把瓶颈找出来。
2. TaoToken 前置:统一 Key 与 API 通道接入
在本地工具里做耗时对比,最烦的是每个模型都要单独配 Key、单独改 base_url。TaoToken 的价值在于提供一个统一的 API 通道,一个 Key 就能切换不同模型,方便你在同一套配置下对比 TPOT 差异。
接入前先拿 Key。打开控制台页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,登录后创建一个 API Key,复制保存。注意 Key 只在创建时完整显示一次,丢了只能重建。
拿到 Key 后,统一的基础地址是 https://taotoken.net/api ,这个地址不加任何查询参数。所有兼容 OpenAI 协议的工具,把 base_url 指向它、把 Key 填进去即可。如果你用的是 Claude Code 这类 Anthropic 协议工具,走的是另一套接入方式,参考文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里的说明。
这里有个容易踩的坑:base_url 末尾不要自己加/v1或斜杠。很多工具会自动拼接路径,你手动加了反而变成/v1/v1/chat/completions,直接 404。先按原样填,报错再对照文档排查。
注意:Key 属于敏感凭证,不要写进会提交到 Git 的配置文件里。建议用环境变量注入,下面配置示例里我会用占位符标注。
3. 可复制配置:config.toml 与 settings.json 骨架
不同工具读不同格式的配置。命令行类工具(如各类 CLI Agent)常用 config.toml,编辑器插件类常用 settings.json。下面两份骨架你直接改 Key 就能用。
先看 config.toml,适合需要精细控制超时和并发参数的场景:
# ~/.config/taotoken/config.toml # 统一 API 通道配置,用于逐 Token 耗时对比 [provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key = "${TAOTOKEN_API_KEY}" # 从环境变量读取,勿硬编码 [model] # 对比测试时逐个替换,观察 TPOT 差异 name = "gpt-4o-mini" max_tokens = 1024 temperature = 0.2 [request] # 首 Token 超时:Prefill 慢时容易触发,先给宽一点 ttft_timeout_ms = 30000 # 整体请求超时:长输出场景要留足,按 max_tokens * TPOT 估算 total_timeout_ms = 120000 # 流式开关:测 TPOT 必须开流式,否则拿不到逐 Token 时间 stream = true [concurrency] # 并发请求数:定位瓶颈时从 1 开始,逐步加压 max_parallel = 1 # 重试次数:网络抖动时兜底,但会污染耗时统计,压测时设 0 max_retries = 0 [logging] # 记录每个 Token 的到达时间戳,用于算 TPOT log_token_timing = true log_path = "./logs/token_timing.jsonl"再看 settings.json,适合编辑器插件或图形化工具:
{ "taotoken.provider": { "baseUrl": "https://taotoken.net/api", "apiKey": "${TAOTOKEN_API_KEY}", "model": "gpt-4o-mini" }, "taotoken.request": { "stream": true, "ttftTimeoutMs": 30000, "totalTimeoutMs": 120000, "maxTokens": 1024 }, "taotoken.concurrency": { "maxParallel": 1, "maxRetries": 0 }, "taotoken.telemetry": { "recordTokenTiming": true, "outputPath": "./logs/token_timing.jsonl" } }两份配置的核心参数是一致的,对照着看更清楚:
| 参数 | config.toml | settings.json | 作用 |
|---|---|---|---|
| 基础地址 | base_url | baseUrl | 统一 API 通道入口 |
| 流式 | stream | stream | 测 TPOT 必须开 |
| 首 Token 超时 | ttft_timeout_ms | ttftTimeoutMs | 防 Prefill 慢误判 |
| 整体超时 | total_timeout_ms | totalTimeoutMs | 长输出留足 |
| 并发数 | max_parallel | maxParallel | 定位瓶颈从 1 起 |
| 重试 | max_retries | maxRetries | 压测设 0 |
环境变量这样设置,避免 Key 泄漏:
export TAOTOKEN_API_KEY="sk-你的Key"4. 验证请求:逐 Token 耗时对比与成功结果
配置就绪后,先发一个最小请求确认通道通。用 curl 测最直接:
curl -N https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "stream": true, "max_tokens": 200, "messages": [{"role": "user", "content": "用一句话解释什么是 KV-cache"}] }'-N关闭缓冲,你能看到数据一块块吐出来。每个data:行就是一个 Token 片段。成功的话,最后会收到data: [DONE]。
要精确算 TTFT 和 TPOT,用一段 Python 脚本记录时间戳:
import time, json, os from openai import OpenAI client = OpenAI( base_url="https://taotoken.net/api", api_key=os.environ["TAOTOKEN_API_KEY"], ) def measure(prompt, max_tokens=200): t0 = time.perf_counter() ttft = None token_times = [] stream = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], max_tokens=max_tokens, stream=True, ) for chunk in stream: if chunk.choices[0].delta.content: now = time.perf_counter() if ttft is None: ttft = now - t0 token_times.append(now) total = time.perf_counter() - t0 n = len(token_times) tpot = (total - ttft) / (n - 1) if n > 1 else 0 return {"ttft_ms": round(ttft*1000, 1), "tpot_ms": round(tpot*1000, 2), "tokens": n, "total_ms": round(total*1000, 1)} print(json.dumps(measure("写一段 150 字的产品介绍"), ensure_ascii=False))跑出来你会看到类似这样的结果:
{"ttft_ms": 412.3, "tpot_ms": 18.7, "tokens": 156, "total_ms": 3320.5}TTFT 四百多毫秒,TPOT 约 18.7ms/Token。这个 TPOT 和前面算的 14.1ms 理论下界比,差距来自算子开销、调度、网络往返。如果 TPOT 明显偏高(比如超过 50ms),就要往下排查了。
对比测试建议固定 prompt、固定 max_tokens,只改一个变量。比如把 max_tokens 从 200 提到 800,观察 TPOT 是否稳定——如果 TPOT 随输出变长而上升,说明 KV-cache 读取开始成为负担,上下文太长了。
5. 本篇常见错排查
报错 401 Unauthorized:Key 没读到或写错。先echo $TAOTOKEN_API_KEY确认环境变量生效,再检查配置文件里是不是把${TAOTOKEN_API_KEY}当字面量传进去了。有些工具不做变量替换,得手动填。
报错 404 Not Found:base_url 拼错。最常见的是自己加了/v1。统一地址就是https://taotoken.net/api,路径由工具自动补。对照文档确认。
TTFT 特别高但 TPOT 正常:瓶颈在 Prefill,通常是 prompt 太长。检查你的 system prompt 是不是塞了几千字。长 system prompt 场景可以考虑前缀缓存,把固定部分复用起来,TTFT 能砍掉一大截。
TPOT 忽高忽低:并发在干扰。把 max_parallel 降到 1 再测。如果单并发稳定、多并发抖动,说明是排队或带宽争抢,不是模型本身问题。
流式没生效,一次性返回:检查 stream 参数是不是被工具覆盖了。有些 SDK 默认非流式,得显式传stream=True。非流式下你只能拿到总耗时,算不出 TPOT。
超时中断:长输出场景 total_timeout 给太短。按TTFT + TPOT × max_tokens估算,再乘 1.5 倍余量。比如 TTFT 0.5s、TPOT 20ms、输出 1000 Token,总耗时约 20.5s,超时至少设 30s。
耗时统计里混入重试:压测时把 max_retries 设 0。一次重试会把总耗时翻倍,统计就失真了。
6. 优化方向与后续动作
定位清楚瓶颈后,优化就有针对性了。短输出场景 TTFT 占主导,优先做 prompt 缓存、精简 system prompt;长输出场景 TPOT 占主导,考虑权重量化、换带宽更高的卡。想验证不同模型的实际 TPOT 差异,可以直接在模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 里切换模型对比,省去改配置的麻烦。
如果你是要长期跑编码任务或 Agent 工作流,逐 Token 耗时直接影响体验,建议了解一下 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,针对高频调用场景做了通道优化。接入过程中遇到报错,先翻接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,大部分配置问题里面都有对照说明。
最后留个实操建议:把上面那段 Python 测速脚本存成bench.py,每次改完配置跑一遍,把结果追加到logs/token_timing.jsonl。跑上一周,你对自己业务场景的 TPOT 基线就有数了,再出延迟问题一眼就能看出是模型侧还是链路侧。