☰
LLM 推理的核心指标:TTFT、TPOT 与 Throughput 怎么测才准?TaoToken 统一 Key 通道实测
2026/10/17 10:53:06 网站建设 项目流程

1. 为什么你测的 TTFT 和线上体验对不上

很多人第一次做 LLM 推理性能评测,都会遇到一个很尴尬的情况:本地脚本跑出来的 TTFT 只有 200ms,感觉挺快,结果上线后用户还是抱怨"打字机效果卡顿"。问题往往不在模型本身,而在于测量方式把三个指标混在一起了。

先把概念理清楚。TTFT(Time To First Token)是从你发出请求到收到第一个 token 的时间,它决定了用户点下发送后要盯着空白屏幕多久。TPOT(Time 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 和 httpx:

python -m venv venv source venv/bin/activate pip install openai httpx numpy

如果你更习惯命令行压测,可以用 curl 配合 time 命令做单请求粗测,但并发场景还是脚本更可控。设置环境变量,避免 Key 硬编码进脚本:

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://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_key=API_KEY, base_url=BASE_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( model=MODEL_ID, messages=[{"role": "user", "content": PROMPT}], stream=True, temperature=0, max_tokens=256, ) 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(f"TTFT P50={np.percentile([r['ttft_ms'] for r in ok],50):.1f}ms " f"P95={np.percentile([r['ttft_ms'] for r in ok],95):.1f}ms") print(f"TPOT P50={np.percentile([r['tpot_ms'] for r in ok],50):.1f}ms") print(f"Throughput={throughput:.1f} tokens/s") if __name__ == "__main__": for c in [1, 4, 8, 16]: asyncio.run(run_benchmark(c, total_requests=c * 4))

几个参数要说明。temperature=0是为了让输出长度尽量稳定,减少随机性对 TPOT 的干扰。max_tokens=256限制输出上限,避免个别请求生成超长文本拉偏吞吐。total_requests = concurrency * 4保证每个并发档位有足够样本,同时不会跑太久。

如果你用 Cline 或 Claude Code 这类工具做接入测试,配置项要写全三件套:Base URL 填https://taotoken.net/api,API Key 填你的 Key,Model 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 P50=312.4ms P95=358.1ms TPOT P50=18.7ms Throughput=52.3 tokens/s 并发=8 成功=32/32 TTFT P50=890.2ms P95=1420.5ms TPOT P50=24.1ms Throughput=310.6 tokens/s

怎么读这组数据?并发从 1 到 8,TTFT 从 312ms 涨到 890ms,涨了近三倍,说明请求在网关或服务端排队了。TPOT 从 18.7ms 涨到 24.1ms,涨幅不大,说明单请求的生成速度还稳。Throughput 从 52 涨到 310,接近 6 倍,说明并发确实提升了整体产出,但没到线性(8 倍),存在资源竞争。

这就是典型的"用 TTFT 换 Throughput"的权衡。在线流式应用对 TTFT 敏感,并发不能开太高;离线批处理对 Throughput 敏感,可以牺牲 TTFT 把并发拉满。你的业务属于哪种,决定了你该看哪个指标。

验证成功还有一个标志:所有请求都返回了内容,没有 error。如果出现部分失败,先看错误信息,再对照下一节的排查表。

4. 常见报错与排查对照

压测过程中最容易撞上的几类错误,这里逐个拆解。

401 Unauthorized:Key 无效或没带上。检查Authorization头格式是不是Bearer sk-xxx,中间有空格。环境变量有没有正确 export,脚本里读的是不是同一个变量名。

404 Not Found:路径拼错。最常见的是 Base URL 写成https://taotoken.net/api/v1,SDK 又拼了一次/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。把这三个数测准、记下来、能复现,比追求某个绝对值更有意义。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询