1. 为什么你测的 TTFT 和线上体验对不上很多人第一次做 LLM 推理性能评测都会遇到一个很尴尬的情况本地脚本跑出来的 TTFT 只有 200ms感觉挺快结果上线后用户还是抱怨打字机效果卡顿。问题往往不在模型本身而在于测量方式把三个指标混在一起了。先把概念理清楚。TTFTTime To First Token是从你发出请求到收到第一个 token 的时间它决定了用户点下发送后要盯着空白屏幕多久。TPOTTime Per Output Token是第一个 token 之后平均每个 token 的生成间隔它决定了文字流出来的顺滑程度。Throughput吞吐量则是单位时间内所有并发请求加起来生成了多少 token它直接对应你的推理成本。这三个指标的关系可以用一个公式串起来Latency TTFT TPOT × 输出 token 数。而 TPS单请求每秒 token 数 输出 token 数 / Latency。注意 TPS 和 Throughput 不是一回事前者是单请求视角后者是全局视角。常见的测量误区有这么几个。第一用非流式接口测 TTFT那测出来的其实是完整 Latency因为你要等所有 token 生成完才拿到响应。第二客户端计时把网络往返、DNS 解析、TLS 握手全算进去导致 TTFT 虚高换个网络环境数据就完全变了。第三固定并发下只测一次忽略了首请求冷启动、KV Cache 未命中的影响。第四输入长度不固定prompt 从 50 token 到 2000 token 混着测TPOT 波动巨大却找不到原因。我试过用同一份 prompt 在不同通道上跑TTFT 能差出三倍后来发现是有的通道做了请求排队。所以要做可复现的评测必须固定三件事输入长度、并发数、测量通道。这篇就围绕这三点给一套能直接复制运行的压测流程通道统一走 TaoToken 的 Key/API避免多供应商差异干扰。适合谁看正在做推理服务选型、想给模型接口做性能基线、或者需要向团队解释为什么并发一高就变慢的开发和运维同学。下面从环境准备开始一步步把三个指标测准。2. TaoToken 统一 Key 通道准备与压测环境搭建要做可复现的评测第一步是让所有对比实验走同一条通道。如果 A 模型走这家、B 模型走那家网络路径和网关策略都不一样测出来的差异根本说不清是模型的问题还是通道的问题。TaoToken 提供统一的 Key 和 API 入口把模型切换收敛到一个 Base URL 上这样你换模型时只需要改 Model ID网络层保持不变。先拿 Key。访问 https://taotoken.net/api-keys 登录后在控制台创建 API Key。建议给压测单独建一个 Key方便后续在用量页面区分实验流量和正常业务流量。创建后复制保存它只显示一次。Base URL 统一用 https://taotoken.net/api 注意这个地址不带任何查询参数。模型对话的调试入口在 https://taotoken.net/models 可以先用它确认某个 Model ID 是否可用再去写压测脚本。环境准备分两块。Python 侧建议用独立虚拟环境装 openai 和 httpxpython -m venv venv source venv/bin/activate pip install openai httpx numpy如果你更习惯命令行压测可以用 curl 配合 time 命令做单请求粗测但并发场景还是脚本更可控。设置环境变量避免 Key 硬编码进脚本export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api这里有个容易踩的坑Base URL 结尾不要自己加/v1。OpenAI SDK 会自动拼接路径你手动加了会变成/v1/v1/chat/completions直接 404。如果你用的是某些只认完整路径的老客户端才需要显式写全但用官方 SDK 就按上面这样。固定输入长度是评测的关键。准备一份约 500 token 的 prompt 作为基准可以用一段技术文档或者重复的说明文字。为什么强调固定因为 TTFT 和 TPOT 都受输入长度影响prefill 阶段要处理的 token 越多首 token 越慢。你可以在脚本里用 tiktoken 估算长度或者干脆用固定字符串保证每次一致。并发控制用 asyncio semaphore不要用多线程硬怼否则测出来的是你本机 CPU 的瓶颈而不是服务端的。下面进入具体配置。2.1 可复制的压测配置与参数说明先给一份完整的 Python 压测脚本它同时采集 TTFT、TPOT 和 Throughput。核心思路是用流式接口记录每个 chunk 到达的时间戳。import asyncio import time import os import numpy as np from openai import AsyncOpenAI API_KEY os.environ[TAOTOKEN_API_KEY] BASE_URL os.environ[TAOTOKEN_BASE_URL] MODEL_ID gpt-4o-mini # 换成你要测的 Model ID client AsyncOpenAI(api_keyAPI_KEY, base_urlBASE_URL) PROMPT 请用大约300字解释什么是KV Cache以及它对推理性能的影响。 * 3 async def single_request(sem, results, idx): async with sem: start time.perf_counter() first_token_time None token_times [] token_count 0 try: stream await client.chat.completions.create( modelMODEL_ID, messages[{role: user, content: PROMPT}], streamTrue, temperature0, max_tokens256, ) async for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: now time.perf_counter() if first_token_time is None: first_token_time now token_times.append(now) token_count 1 end time.perf_counter() ttft (first_token_time - start) * 1000 if first_token_time else None if token_count 1: intervals np.diff(token_times) tpot float(np.mean(intervals)) * 1000 else: tpot None latency (end - start) * 1000 results.append({ idx: idx, ttft_ms: ttft, tpot_ms: tpot, latency_ms: latency, tokens: token_count, }) except Exception as e: results.append({idx: idx, error: str(e)}) async def run_benchmark(concurrency, total_requests): sem asyncio.Semaphore(concurrency) results [] tasks [single_request(sem, results, i) for i in range(total_requests)] wall_start time.perf_counter() await asyncio.gather(*tasks) wall_end time.perf_counter() wall_time wall_end - wall_start ok [r for r in results if error not in r and r[ttft_ms]] total_tokens sum(r[tokens] for r in ok) throughput total_tokens / wall_time print(f并发{concurrency} 成功{len(ok)}/{total_requests}) print(fTTFT P50{np.percentile([r[ttft_ms] for r in ok],50):.1f}ms fP95{np.percentile([r[ttft_ms] for r in ok],95):.1f}ms) print(fTPOT P50{np.percentile([r[tpot_ms] for r in ok],50):.1f}ms) print(fThroughput{throughput:.1f} tokens/s) if __name__ __main__: for c in [1, 4, 8, 16]: asyncio.run(run_benchmark(c, total_requestsc * 4))几个参数要说明。temperature0是为了让输出长度尽量稳定减少随机性对 TPOT 的干扰。max_tokens256限制输出上限避免个别请求生成超长文本拉偏吞吐。total_requests concurrency * 4保证每个并发档位有足够样本同时不会跑太久。如果你用 Cline 或 Claude Code 这类工具做接入测试配置项要写全三件套Base URL 填https://taotoken.net/apiAPI Key 填你的 KeyModel ID 填具体模型名。缺任何一个都会连接失败。Codex 用户如果走auth.json把 base_url 和 api_key 对应字段填好即可。2.2 固定并发与输入长度的实验设计实验设计决定了你的数据能不能用。建议按并发 1、4、8、16 四档跑每档至少 4 倍并发的请求数。为什么从 1 开始因为并发 1 是基线能反映单请求的真实 TTFT 和 TPOT没有排队干扰。并发上去之后如果 TTFT 明显上升而 TPOT 变化不大说明瓶颈在请求排队如果 TPOT 也涨说明 GPU 计算资源被抢占了。输入长度固定后还要注意输出长度。虽然设了 max_tokens但模型可能提前停止。所以统计 TPOT 时要过滤掉 token 数过少的样本比如少于 20 个 token 的请求否则均值会被拉偏。跑之前先做一次预热请求把连接池和模型加载的冷启动成本排除掉。预热就是正常发一个请求等它完成再开始正式计时。这一步很多人省掉结果第一档并发数据总是偏高。3. 验证请求与成功结果解读配置写好后先别急着跑全量。用一条 curl 命令验证通道是否通这是最快的排障手段。curl https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: 说一句话}], stream: true }如果返回一串data: {...}的流式内容说明 Key、Base URL、Model ID 三件套都对。如果返回 401检查 Key 是否复制完整、有没有多余空格。如果返回 404八成是 Base URL 多写了/v1。跑压测脚本后你会看到类似这样的输出并发1 成功4/4 TTFT P50312.4ms P95358.1ms TPOT P5018.7ms Throughput52.3 tokens/s 并发8 成功32/32 TTFT P50890.2ms P951420.5ms TPOT P5024.1ms Throughput310.6 tokens/s怎么读这组数据并发从 1 到 8TTFT 从 312ms 涨到 890ms涨了近三倍说明请求在网关或服务端排队了。TPOT 从 18.7ms 涨到 24.1ms涨幅不大说明单请求的生成速度还稳。Throughput 从 52 涨到 310接近 6 倍说明并发确实提升了整体产出但没到线性8 倍存在资源竞争。这就是典型的用 TTFT 换 Throughput的权衡。在线流式应用对 TTFT 敏感并发不能开太高离线批处理对 Throughput 敏感可以牺牲 TTFT 把并发拉满。你的业务属于哪种决定了你该看哪个指标。验证成功还有一个标志所有请求都返回了内容没有 error。如果出现部分失败先看错误信息再对照下一节的排查表。4. 常见报错与排查对照压测过程中最容易撞上的几类错误这里逐个拆解。401 UnauthorizedKey 无效或没带上。检查Authorization头格式是不是Bearer sk-xxx中间有空格。环境变量有没有正确 export脚本里读的是不是同一个变量名。404 Not Found路径拼错。最常见的是 Base URL 写成https://taotoken.net/api/v1SDK 又拼了一次/chat/completions变成/api/v1/chat/completions之外的错误路径。正确写法就是https://taotoken.net/api。local proxy failed / connection error本机网络或代理配置问题。检查有没有设置HTTP_PROXY、HTTPS_PROXY环境变量指向了不可用的地址。压测脚本里可以显式传http_client禁用代理或者清掉这些环境变量。reading choices 相关报错通常是流式响应解析时chunk.choices为空数组代码没做判空。上面脚本里if chunk.choices and chunk.choices[0].delta.content就是防这个的。有些模型在流结束时发一个空 choices 的 chunk不判空会直接抛异常。OAuth / auth.json 报错如果你用 Codex 或 Claude Code 这类工具认证方式可能不是简单的 API Key。Codex 走auth.json时要确认字段名和层级正确Claude Code 走 Anthropic 兼容接口时 Base URL 和 Key 的填法要按文档来。这类工具报 OAuth 错误多半是认证模式选错了不是 Key 本身的问题。TPOT 为 None说明只收到了一个 token 或零个 token。检查 max_tokens 是不是设得太小或者 prompt 触发了模型的拒答。也可能是流式解析把内容过滤掉了。排查顺序建议先用 curl 确认单请求通再跑并发 1 的脚本最后上高并发。这样能把通道问题和并发问题分开定位。5. 把评测流程固化成可复现的基线测准一次不难难的是每次都能测出可对比的数据。建议把上面这套流程固化成三件事一份固定的 prompt 文件、一个记录实验参数的表格、一套统一的统计口径。prompt 文件存成 txt脚本读取保证每次输入完全一致。实验参数表格记录日期、Model ID、并发数、输入 token 数、max_tokens、TTFT P50/P95、TPOT P50、Throughput。统计口径统一用 P50 和 P95不要只看均值均值会被极端值带偏。每次换模型或换配置只改一个变量其他保持不变。这样你才能说清楚TTFT 涨了 200ms 是因为换了模型不是因为并发变了。如果你要长期做编码类或 Agent 类应用的性能跟踪可以考虑用 Coding Plan 把常用模型的调用额度固定下来避免每次实验都受额度波动影响。模型对话入口可以用来快速验证某个 Model ID 的可用性和大致响应速度确认没问题再写进压测脚本。最后提醒一点TTFT、TPOT、Throughput 三个指标没有哪个最重要只有哪个对你的场景最重要。在线聊天看 TTFT批量生成看 Throughput长文生成看 TPOT。把这三个数测准、记下来、能复现比追求某个绝对值更有意义。