1. 为什么你调用的模型“感觉慢”:四个指标的真实分工
很多人第一次做 AI 应用选型时,判断标准只有一句“这个模型快不快”。但真正上线后你会发现,用户抱怨的“慢”其实分好几种:有人是点了发送后半天不吐字,有人是第一个字很快但后面像挤牙膏,还有人是一次性批量跑几千条数据时总耗时长得离谱。这背后对应的是四个完全不同的指标——TTFT、TPOT、E2E Latency 和吞吐量。
TTFT(Time to First Token,首 Token 时延)衡量的是从你发出请求到收到第一个 token 的时间,它决定了用户“等待感”的强弱。TPOT(Time Per Output Token,单 token 输出耗时)衡量的是首 token 之后,每多生成一个 token 平均要花多久,它决定了流式输出“顺不顺”。E2E Latency(端到端时延)是整条请求从发出到最后一个 token 落地的总时间。吞吐量则是单位时间内系统能处理多少 token 或多少请求,它决定了你的服务能扛多少并发。
这四个指标不是孤立的,它们之间存在明显的相互制约。你把 batch size 调大,吞吐量上去了,但 TTFT 和 TPOT 往往会被拖慢;你为了压低 TTFT 而减少批处理,吞吐量又会掉下来。所以选型和调优的本质,是在你的业务场景里找到这四者的平衡点。这篇文章会先讲清楚每个指标的定义和测量方式,然后结合 TaoToken 统一 API 通道,给你一套可复制的压测脚本和指标采集配置,让你能亲手验证这些指标随并发变化的趋势。
2. TaoToken 统一 API 通道:为指标采集准备一个稳定入口
要做推理性能测量,第一步是有一个稳定的、可编程调用的模型入口。TaoToken 提供统一 API 通道,把不同模型的调用方式收敛成一套兼容接口,这样你在写压测脚本时不用为每个模型改一遍代码。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。
在开始采集指标之前,你需要先拿到 API Key。进入控制台的 API Keys 页面(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ),创建一个新的 Key。创建时建议给它起一个能区分用途的名字,比如perf-test-ttft,这样后面你同时跑多组压测时不会搞混。Key 只在创建时完整显示一次,复制后先存到本地环境变量里,不要直接写进脚本明文。
拿到 Key 之后,你需要确认两件事:Base URL 和 Model ID。Base URL 就是https://taotoken.net/api,Model ID 则取决于你要测哪个模型。你可以在模型对话页面(https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= )先手动发一条消息,确认模型能正常返回,然后在接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= )里找到对应的模型标识。
这里有一个容易被忽略的点:指标采集对网络稳定性很敏感。如果你在本地用不稳定的网络去测 TTFT,测出来的数字里混了大量网络抖动,根本没法用来做模型选型。所以压测脚本最好跑在一台网络环境相对干净的机器上,或者至少保证压测期间没有其他大流量任务在抢带宽。我试过在同一台机器上一边下载大文件一边跑 TTFT 测试,结果首 token 时延直接从 400ms 飙到 2s 以上,完全是噪声。
另外,TaoToken 的 API 兼容 OpenAI 风格的调用方式,这意味着你可以直接用 OpenAI SDK 或者任何兼容的 HTTP 客户端来发请求。对于指标采集来说,这一点很关键,因为流式响应的解析逻辑可以复用现成的库,你只需要在解析过程中打时间戳就行。下面一节我会给出完整的可复制配置和脚本。
3. 可复制配置:压测脚本与指标采集参数
这一节给你一套可以直接跑的配置。核心思路是用 Python 写一个压测脚本,通过流式接口发请求,在收到第一个 token、每个后续 token、以及最后一个 token 时分别打时间戳,最后算出 TTFT、TPOT、E2E Latency 和吞吐量。
先准备一个配置文件,把 Base URL、Key 和 Model ID 集中管理。你可以用 JSON 格式,路径放在项目根目录的perf_config.json:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-your-key-here", "model_id": "your-model-id", "max_tokens": 256, "temperature": 0.7, "stream": true }注意api_key这里只是占位,实际运行时建议从环境变量读取,避免把 Key 提交到代码仓库。你可以在脚本里用os.environ.get("TAOTOKEN_API_KEY")来取。
接下来是压测脚本bench_llm.py。它用requests库发流式请求,逐行解析 SSE 数据,记录关键时间点:
import json import time import os import requests from concurrent.futures import ThreadPoolExecutor, as_completed with open("perf_config.json", "r") as f: cfg = json.load(f) API_KEY = os.environ.get("TAOTOKEN_API_KEY", cfg["api_key"]) BASE_URL = cfg["base_url"].rstrip("/") MODEL_ID = cfg["model_id"] def one_request(prompt, max_tokens=256): url = f"{BASE_URL}/v1/chat/completions" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } payload = { "model": MODEL_ID, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "temperature": cfg["temperature"], "stream": True } t_start = time.perf_counter() t_first = None t_last = None token_count = 0 with requests.post(url, headers=headers, json=payload, stream=True, timeout=120) as resp: resp.raise_for_status() for line in resp.iter_lines(decode_unicode=True): if not line or not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break try: chunk = json.loads(data) except json.JSONDecodeError: continue delta = chunk.get("choices", [{}])[0].get("delta", {}) if delta.get("content"): now = time.perf_counter() if t_first is None: t_first = now t_last = now token_count += 1 t_end = time.perf_counter() ttft = (t_first - t_start) if t_first else None e2e = t_end - t_start tpot = ((t_last - t_first) / (token_count - 1)) if token_count > 1 else None return { "ttft": ttft, "tpot": tpot, "e2e": e2e, "tokens": token_count } def run_batch(prompt, concurrency): results = [] with ThreadPoolExecutor(max_workers=concurrency) as ex: futures = [ex.submit(one_request, prompt) for _ in range(concurrency)] for fut in as_completed(futures): results.append(fut.result()) return results if __name__ == "__main__": prompt = "请用大约200字解释什么是大语言模型的推理时延。" for conc in [1, 2, 4, 8]: res = run_batch(prompt, conc) ttfts = [r["ttft"] for r in res if r["ttft"]] tpots = [r["tpot"] for r in res if r["tpot"]] e2es = [r["e2e"] for r in res] total_tokens = sum(r["tokens"] for r in res) wall = max(e2es) print(f"并发={conc} | TTFT均值={sum(ttfts)/len(ttfts)*1000:.0f}ms | " f"TPOT均值={sum(tpots)/len(tpots)*1000:.1f}ms | " f"E2E均值={sum(e2es)/len(e2es)*1000:.0f}ms | " f"吞吐={total_tokens/wall:.1f} tok/s")这个脚本的关键点在于:t_start在发请求前记录,t_first在收到第一个带 content 的 delta 时记录,t_last在最后一个 content 时记录,t_end在流结束后记录。TTFT 就是t_first - t_start,TPOT 是首 token 之后到最后一个 token 之间的时间除以 token 数减一,E2E 是t_end - t_start。吞吐量用总 token 数除以整批请求的墙钟时间。
运行前先装依赖:pip install requests。然后设置环境变量export TAOTOKEN_API_KEY=你的key,再执行python bench_llm.py。脚本会依次用并发 1、2、4、8 跑四组,打印每组的指标均值。这样你就能直观看到并发上升时,TTFT 和 TPOT 怎么变,吞吐量又怎么变。
如果你用的是 Claude Code 这类编码工具做长期压测,可以在 Coding Plan 页面(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= )了解适合持续调用的方案。对于需要反复跑压测、对比不同模型参数的场景,统一通道能省掉大量切换成本。
4. 验证请求:观察指标随并发变化的真实趋势
配置好脚本后,你需要实际跑一遍,确认每个指标都能被正确采集,并且观察它们随并发变化的趋势。这一节我带你走一遍验证流程,包括单请求验证和多并发对比。
先做单请求验证。把脚本里的并发列表改成[1],跑一次,看输出是否合理。正常情况下,TTFT 应该在几百毫秒量级,TPOT 在几十毫秒量级,E2E 在几秒量级,吞吐量在几十 tok/s 量级。具体数字取决于你选的模型和当时的服务负载。如果 TTFT 显示为None,说明整个流式响应里没有解析到 content,可能是模型返回格式不同或者请求被拒绝了,需要先检查响应体。
单请求验证通过后,跑完整的并发梯度。你会看到类似这样的趋势:并发从 1 升到 8 时,TTFT 均值从 400ms 升到 900ms,TPOT 从 30ms 升到 55ms,E2E 从 3s 升到 6s,但吞吐量从 40 tok/s 升到 120 tok/s。这就是典型的“时延换吞吐”曲线。如果你看到吞吐量不升反降,说明并发已经超过了服务端的处理能力,请求开始排队甚至超时。
这里有一个测量细节要注意:TPOT 的计算依赖 token 计数。如果你的 prompt 让模型输出很短,比如只生成 5 个 token,那 TPOT 的样本太少,波动会很大。建议压测时把max_tokens设到 128 以上,并且用固定 prompt,保证每次生成的 token 数量接近,这样指标才有可比性。
另外,流式和非流式的测量方式不同。非流式请求你只能拿到总时间,没法拆出 TTFT 和 TPOT。所以做指标采集一定要用stream: true。如果你在脚本里忘了开流式,会发现t_first和t_last几乎同时出现,TTFT 约等于 E2E,TPOT 接近 0,这显然是错的。
验证成功后,你可以把结果整理成表格,方便对比不同模型或不同参数。比如固定并发为 4,分别测max_tokens=128和max_tokens=512,看 TPOT 和 E2E 怎么变。通常输出越长,E2E 线性增长,TPOT 相对稳定,但如果服务端有 KV Cache 优化,TPOT 可能随长度增加而略微上升。
如果你在验证过程中想快速确认某个模型当前是否可用,可以直接在模型对话页面(https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= )手动发一条流式消息,观察首字出现的时间和后续吐字速度,作为脚本测量的交叉验证。
5. 常见报错排查:401、local proxy failed、reading choices 与 OAuth
压测过程中最容易遇到的几个报错,我按出现频率排一下,并给出排查路径。
401 Unauthorized:这是最常见的问题,基本是 Key 不对或没带上。检查三件事:环境变量TAOTOKEN_API_KEY是否真的设置成功(用echo $TAOTOKEN_API_KEY确认);请求头里Authorization是否是Bearer加 Key,注意 Bearer 后面有一个空格;Key 是否已经过期或被删除。如果你在脚本里同时用了配置文件的 Key 和环境变量,确认脚本读的是哪一个。另外,Key 复制时容易带上首尾空格,建议在脚本里做一次strip()。
local proxy failed / connection refused:这个报错通常出现在你本地设置了网络代理,但代理没有正常工作时。压测脚本发请求时如果走了系统代理,而代理不可用,就会报这个。排查方式是检查环境变量HTTP_PROXY和HTTPS_PROXY,如果不需要代理就清掉;如果确实需要,确认代理地址和端口正确。在 Python 里你也可以显式传proxies={"http": None, "https": None}来绕过系统代理。
reading choices 相关报错:这类报错通常出现在解析响应时,choices字段为空或者结构不符合预期。常见原因是请求被服务端拒绝,返回的是一个错误对象而不是正常的 completion 结构。你可以在脚本里先打印原始响应体,确认返回内容。如果返回的是{"error": {...}},那就按错误信息排查,而不是继续解析 choices。另一个原因是流式响应里混入了非 data 行,比如空行或注释行,脚本里已经用startswith("data:")过滤了,但如果你自己改了解析逻辑,要注意保留这个过滤。
OAuth / 认证方式不匹配:如果你用的是某些编码工具(比如 Claude Code 或 Codex 风格的客户端),它们可能默认走 OAuth 而不是 API Key。这时候你需要确认客户端的认证配置。以 Claude Code 为例,它需要设置 Base URL、API Key 和 Model ID 三件套。Base URL 填https://taotoken.net/api,API Key 填你创建的那个,Model ID 填对应模型标识。如果只填了 Key 没填 Base URL,客户端可能仍然往默认地址发请求,导致认证失败。Codex 的auth.json也是类似,需要确保里面的 base_url 和 api_key 都指向 TaoToken 的通道。
排查时有一个通用方法:先用curl发一个最小请求,确认通道本身是通的。命令如下:
curl -s -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"your-model-id","messages":[{"role":"user","content":"hi"}],"max_tokens":8,"stream":true}'如果 curl 能正常返回流式数据,说明 Key 和通道没问题,问题在脚本或客户端配置。如果 curl 也报 401,那就回到 Key 本身排查。这个方法能帮你快速定位问题是在通道层还是在应用层。
6. 把指标用起来:选型与调优的下一步
跑完压测、拿到四组并发下的指标后,你手里就有了一张决策表。如果你的应用是实时对话,优先看 TTFT,尽量控制在 500ms 以内,同时关注 TPOT 是否稳定,避免吐字忽快忽慢。如果是批量文档摘要,优先看吞吐量,TTFT 可以放宽到几秒,因为用户不会盯着屏幕等。如果是 Agent 类应用需要多轮调用,E2E Latency 的累积效应更关键,每一轮都要尽量压低。
调优时记住那个制约关系:提高并发或 batch size 能提升吞吐,但会牺牲 TTFT 和 TPOT。你可以通过压测脚本快速验证不同参数组合,找到你业务能接受的最优平衡点。比如把并发从 4 降到 2,TTFT 可能从 900ms 回到 500ms,而吞吐只从 120 tok/s 降到 80 tok/s,这对实时场景就是划算的。
如果你需要长期跑这类压测,或者把指标采集集成到 CI 流程里,可以了解 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ),它更适合持续性的编码和 Agent 调用场景。接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= )里有完整的接口说明,方便你把采集逻辑固化下来。API Keys 管理页面(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= )可以给不同压测任务分配不同的 Key,方便按用途追踪用量。
最后提醒一个实操细节:压测时尽量固定 prompt 和 max_tokens,每次只改一个变量,否则指标波动你分不清是参数影响还是输入差异。把每次压测的配置和结果记下来,几轮之后你就能摸清所选模型在你业务场景下的性能边界。