news 2026/10/15 14:29:36

LLM篇-大模型 API 性能指标:从 TTFT / TPOT 到并发与 TPM 的 TaoToken 实测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM篇-大模型 API 性能指标:从 TTFT / TPOT 到并发与 TPM 的 TaoToken 实测

1. 从一次线上告警说起:TTFT 突然翻倍,问题却不在模型

先说个真实场景。某天下午,一个做 AI 客服的朋友发消息说,他们产品的首字延迟从 0.6 秒涨到了 2.3 秒,用户投诉"发完消息半天不出字"。他第一反应是模型服务挂了,去查供应商状态页——一切正常。又怀疑是自己网络抖动,ping 了一圈也没问题。最后把请求日志拉出来逐条看,才发现是并发数从平时的 30 涨到了 90,接近了账号的并发上限,请求开始排队。

这个案例里,他一开始盯错了指标。首字延迟(TTFT)变差,可能是模型慢、可能是网络慢、也可能是排队慢,三者表现一样但根因完全不同。要定位这类问题,你得先把大模型 API 的几个核心性能指标分清楚:TTFT、TPOT、并发、TPM。这四个指标分别管"多久出第一个字""出字快不快""同时能跑多少""一分钟能搬多少 token",任何一个出问题,用户体验都会崩,但排查方向完全不一样。

这篇内容面向需要做大模型 API 选型、压测、SLA 评估的后端和平台工程师。我会用 TaoToken 的统一 Key 和 API 通道搭一套可复现的压测场景,交付能直接跑的并发脚本和指标采集配置,帮你建立自己的性能基线。TaoToken 在这里的角色是统一入口——一个 Key 打通多个模型,压测时不用来回换配置,指标口径也一致。你可以先注册账号拿到 Key,后面所有脚本都基于它来跑。

需要说明的是,压测本身不复杂,复杂的是"测什么、怎么算、结果怎么解读"。很多人跑完压测只看到一个平均延迟,这远远不够。SLA 看的是 P90/P99,容量规划看的是并发和 TPM 的交叉点,体验优化看的是 TTFT 和 TPOT 的拆分。下面我会把这几个维度都覆盖到。

2. TaoToken 前置准备:统一 Key 与 API 通道配置

在开始压测之前,先把通道配好。TaoToken 提供统一的 API 入口,Base URL 是https://taotoken.net/api,你只需要一个 Key 就能调用多个模型。这样做压测的好处是:切换模型只改一个 model 字段,其他配置不动,指标对比更干净。

2.1 获取 API Key 与确认模型 ID

登录 TaoToken 控制台,在 API Keys 页面创建一个新 Key。建议压测单独用一个 Key,避免和线上业务混在一起,方便观察限流和用量。创建后复制保存,后面脚本里会用到。

模型 ID 需要和你实际要测的模型对应。TaoToken 的模型列表在文档里有,常见的有gpt-4o、claude-3-5-sonnet、deepseek-chat等。压测时建议固定一个模型,先把基线跑出来,再换模型做横向对比。

2.2 环境变量与依赖安装

我习惯把 Key 放在环境变量里,不写死在脚本中。Linux/macOS 下:

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

Python 依赖只需要openai和httpx,压测脚本用asyncio做并发:

pip install openai httpx

如果你用 Node.js,装openai包即可,后面我也会给一个 Node 版本的采集片段。

2.3 配置文件:settings.json 与 auth.json 对照

有些工具链(比如 Claude Code、Codex CLI)会读本地配置文件。如果你要在这些工具里接入 TaoToken,配置要写全三件套:Base URL、Key、Model ID。以 Claude Code 的settings.json为例,路径通常在~/.claude/settings.json:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的key", "ANTHROPIC_MODEL": "claude-3-5-sonnet" } }

Codex CLI 的auth.json路径一般在~/.codex/auth.json,结构类似:

{ "api_key": "sk-你的key", "base_url": "https://taotoken.net/api", "model": "gpt-4o" }

注意:不同工具的字段名不一样,但核心就是这三样。少任何一个都会报 401 或 model not found。如果你用 Cline 或 CC Switch 这类插件,MCP 配置里同样要填全 Base URL、Key、Model ID,缺一不可。

2.4 先做一次连通性验证

配置完别急着压测,先发一个最小请求确认通道通:

curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 5 }'

返回里有choices字段就说明通了。如果报 401,检查 Key 有没有复制错;如果报 model not found,检查模型 ID 拼写。这一步过了,再往下走。

3. 可复制配置:并发压测脚本与指标采集

这一节是核心。我会给一个完整的 Python 异步压测脚本,能同时采集 TTFT、TPOT、总延迟、成功率和 TPM 消耗。脚本用流式请求,因为 TTFT 和 TPOT 只有在流式模式下才有意义。

3.1 压测脚本设计思路

脚本要做几件事:按指定并发数同时发请求;每个请求记录首 token 到达时间(TTFT)和后续每个 token 的间隔(TPOT);统计 P50/P90/P99;最后汇总总 token 数和耗时,算出实际 TPM。为了让结果可复现,请求内容固定,max_tokens 固定,温度设为 0。

3.2 完整 Python 脚本

import asyncio import time import os import statistics from openai import AsyncOpenAI API_KEY = os.environ["TAOTOKEN_API_KEY"] BASE_URL = os.environ["TAOTOKEN_BASE_URL"] MODEL = "gpt-4o" CONCURRENCY = 20 REQUESTS_PER_WORKER = 5 PROMPT = "请用200字介绍大模型推理的prefill和decode阶段。" client = AsyncOpenAI(api_key=API_KEY, base_url=BASE_URL) async def single_request(worker_id, req_id): start = time.perf_counter() ttft = None token_times = [] token_count = 0 try: stream = await client.chat.completions.create( model=MODEL, messages=[{"role": "user", "content": PROMPT}], max_tokens=200, temperature=0, stream=True, ) async for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: now = time.perf_counter() if ttft is None: ttft = now - start token_times.append(now) token_count += 1 end = time.perf_counter() tpot = None if len(token_times) > 1: intervals = [token_times[i+1] - token_times[i] for i in range(len(token_times)-1)] tpot = statistics.mean(intervals) * 1000 return { "worker": worker_id, "req": req_id, "ttft": ttft, "tpot_ms": tpot, "total": end - start, "tokens": token_count, "ok": True, } except Exception as e: return {"worker": worker_id, "req": req_id, "ok": False, "error": str(e)} async def worker(worker_id, results): for i in range(REQUESTS_PER_WORKER): r = await single_request(worker_id, i) results.append(r) async def main(): results = [] start = time.perf_counter() tasks = [worker(w, results) for w in range(CONCURRENCY)] await asyncio.gather(*tasks) elapsed = time.perf_counter() - start ok = [r for r in results if r["ok"]] fail = [r for r in results if not r["ok"]] ttfts = sorted([r["ttft"] for r in ok if r["ttft"]]) tpots = sorted([r["tpot_ms"] for r in ok if r["tpot_ms"]]) total_tokens = sum(r["tokens"] for r in ok) def pct(data, p): if not data: return None idx = int(len(data) * p / 100) return data[min(idx, len(data)-1)] print(f"总请求: {len(results)}, 成功: {len(ok)}, 失败: {len(fail)}") print(f"总耗时: {elapsed:.2f}s") print(f"TTFT P50: {pct(ttfts,50):.3f}s P90: {pct(ttfts,90):.3f}s P99: {pct(ttfts,99):.3f}s") print(f"TPOT P50: {pct(tpots,50):.1f}ms P90: {pct(tpots,90):.1f}ms P99: {pct(tpots,99):.1f}ms") print(f"总输出 token: {total_tokens}") print(f"实际 TPM: {total_tokens / elapsed * 60:.0f} token/min") if fail: print("失败样例:", fail[0]["error"]) if __name__ == "__main__": asyncio.run(main())

这个脚本跑起来后,你会看到类似这样的输出:

总请求: 100, 成功: 100, 失败: 0 总耗时: 42.31s TTFT P50: 0.612s P90: 1.204s P99: 2.031s TPOT P50: 18.3ms P90: 24.1ms P99: 41.7ms 总输出 token: 18640 实际 TPM: 26430 token/min

3.3 参数调整与场景对照

并发数CONCURRENCY和每 worker 请求数REQUESTS_PER_WORKER是你要调的。建议从 5 并发开始,逐步加到 10、20、50,观察 TTFT 和 TPOT 的变化曲线。当 TTFT 开始明显上升而 TPOT 基本不变时,说明你撞到了并发墙——请求在排队,但模型本身没变慢。

max_tokens控制输出长度,影响 TPOT 的统计样本量和总耗时。测 TTFT 时可以用小 max_tokens(比如 50),测 TPM 时用大 max_tokens(比如 500)让 token 量堆起来。

如果你要测长输入场景,把 PROMPT 换成一整篇长文档,观察 prefill 阶段的耗时。这时候 TTFT 会显著变大,因为输入 token 多,prefill 时间长。

3.4 Node.js 版本采集片段

如果你用 Node,核心采集逻辑类似:

import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL, }); async function measure() { const start = performance.now(); let ttft = null; let last = null; const intervals = []; const stream = await client.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "ping" }], max_tokens: 100, stream: true, }); for await (const chunk of stream) { if (chunk.choices?.[0]?.delta?.content) { const now = performance.now(); if (ttft === null) ttft = now - start; if (last !== null) intervals.push(now - last); last = now; } } const tpot = intervals.reduce((a, b) => a + b, 0) / intervals.length; console.log(`TTFT: ${ttft.toFixed(0)}ms, TPOT: ${tpot.toFixed(1)}ms`); } measure();

两个版本逻辑一致,选你顺手的用。

4. 验证请求与成功结果:建立你的性能基线

脚本跑通只是第一步,关键是把结果解读成可用的基线。这一节讲怎么验证请求成功、怎么读指标、怎么算出你的真实容量。

4.1 验证请求成功的三个信号

第一个信号是 HTTP 200 且返回体里有choices。第二个信号是流式模式下能持续收到delta.content,如果中途断了,说明连接被掐或超时。第三个信号是 token 计数和预期接近,如果 max_tokens=200 但只收到 20 个 token,可能是被限流截断。

压测时一定要统计失败率。失败请求里最常见的是 429(限流)和超时。429 说明你撞到了并发或 TPM 上限,超时可能是网络或服务端排队。把失败原因分类统计,比只看成功率更有用。

4.2 读懂 TTFT 和 TPOT 的分位值

回到开头那个案例。他看到的 TTFT 从 0.6 秒涨到 2.3 秒,如果只看平均值,可能只涨到 1.1 秒,感觉没那么严重。但 P99 可能已经涨到 5 秒以上,那 1% 的用户体验极差,投诉就来自他们。

我的建议是:TTFT 看 P50 和 P90,P50 代表普通用户,P90 代表较差情况;TPOT 看 P50 和 P99,因为 TPOT 的波动通常比 TTFT 小,P99 能抓到极端卡顿。SLA 承诺一般写 P90 或 P99,别用平均值对外承诺。

4.3 从压测结果算真实 TPM 与并发容量

脚本最后输出的"实际 TPM"是你这次压测的真实吞吐。把它和账号的 TPM 规格对比,就知道用了多少。比如你测出 26430 token/min,账号规格是 200 万 TPM,那只用了 1.3%。

并发容量怎么算?用 Little's Law:并发数 ≈ RPS × 单请求平均耗时。假设你测出单请求平均耗时 2 秒,想支撑 10 RPS,那稳态并发约 20。如果你的账号并发上限是 50,那还有余量;如果上限是 15,就会排队。

这里有个容易踩的坑:并发和 TPM 是两道独立的闸门,先撞哪道取决于业务形态。长会话业务(聊天、Agent)占坑久,先撞并发墙;批量短任务槽位复用快,先撞 TPM 墙。压测时两种场景都要模拟,别只测一种。

4.4 建立基线的实操步骤

第一步,固定模型和 prompt,跑 5 并发、10 并发、20 并发三组,记录 TTFT/TPOT 的 P50/P90/P99。第二步,找出 TTFT 开始明显上升的并发点,那就是你的实际并发容量。第三步,用大 max_tokens 跑一组,算出实际 TPM 消耗速率。第四步,把这三组数据存成表格,作为后续对比的基线。

以后每次换模型、换版本、调参数,都拿新结果和基线对比,就能快速判断是变好了还是变差了。

5. 本篇常见错排查:401、429、超时与配置错误

压测过程中最容易遇到几类报错,这一节逐个拆解。

5.1 401 Unauthorized:Key 或 Base URL 错

报错长这样:

openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API key'}}

原因通常是三个:Key 复制时多了空格或换行;环境变量没生效(比如在错误的 shell 里 export);Base URL 写错,比如漏了/api或写成了别的路径。排查方法:先echo $TAOTOKEN_API_KEY确认 Key 存在且无空格,再确认 Base URL 是https://taotoken.net/api。如果你在 Claude Code 里报 401,检查settings.json里的ANTHROPIC_API_KEY字段有没有写对。

5.2 429 Too Many Requests:撞到并发或 TPM 墙

报错长这样:

openai.RateLimitError: Error code: 429 - {'error': {'message': 'Rate limit exceeded'}}

429 分两种:并发超限和 TPM 超限。区分方法是看压测时的并发数和 token 速率。如果并发数接近账号上限,那是并发墙;如果并发不高但 token 速率很高,那是 TPM 墙。应对方式:并发墙就降并发或加账号并发额度;TPM 墙就降低单请求 token 量或做流量错峰。

脚本里可以加退避重试:

import asyncio async def request_with_retry(fn, max_retry=3): for i in range(max_retry): try: return await fn() except Exception as e: if "429" in str(e) and i < max_retry - 1: await asyncio.sleep(2 ** i) else: raise

5.3 超时与连接中断

报错可能是httpx.ReadTimeout或Connection reset by peer。原因通常是服务端排队太久,或者客户端超时设置太短。OpenAI SDK 默认超时 600 秒,一般够用。如果你自己设了短超时,调大一点。另外,流式请求如果长时间没数据,可能是 prefill 阶段在跑长输入,耐心等或减小输入。

5.4 配置类错误:model not found 与 local proxy failed

model not found说明模型 ID 拼错了,或者你的账号没有该模型权限。去 TaoToken 文档核对模型列表。

local proxy failed通常出现在工具链里,比如 Claude Code 或 Cline 配置了本地代理但代理没启动。检查你的配置文件里 Base URL 是不是直接指向https://taotoken.net/api,不要经过本地代理。如果你用了 CC Switch 或 MCP,确认三件套(Base URL、Key、Model ID)都填了,缺一个就会报这类错。

5.5 指标异常:TTFT 正常但 TPOT 很高

这种情况说明首字来得快,但后续出字慢。原因可能是模型在 decode 阶段负载高,或者你的 max_tokens 太大导致后期变慢。排查方法:换小 max_tokens 再测,如果 TPOT 降下来,说明是输出长度问题;如果还是高,可能是模型服务端 decode 瓶颈,换个模型对比。

6. 把压测变成日常:持续监控与容量规划

压测不是跑一次就完事。模型版本会更新,你的业务量会增长,账号规格可能调整,所以性能基线要定期复测。我的做法是每周跑一次固定脚本,把结果存进表格,画成趋势图。一旦 TTFT 或 TPOT 的 P90 出现明显上升,就提前排查,别等用户投诉。

容量规划上,记住那个核心公式:并发数 ≈ RPS × 单请求平均耗时。你的业务 QPS 乘以平均响应时间,就是需要的并发数。再乘以单请求平均 token 量,就是需要的 TPM。两个数都算出来,和账号规格对比,就知道该不该扩容。

最后给一个实用技巧:压测时把 prompt 和 max_tokens 固定成你线上业务的典型值,别用"ping"这种超短请求。因为 TTFT 和 TPOT 都受输入输出长度影响,用真实业务形态测出来的基线才有参考价值。如果你要测多个模型,用 TaoToken 的统一 Key 切换 model 字段就行,其他配置不动,对比结果最干净。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 14:29:34

彻底搞懂System.out本质:PrintStream源码全解析

如果你写过哪怕一行 Java 代码&#xff0c;你十有八九敲过System.out.println()。这行语句陪着无数人度过了初学阶段&#xff0c;但真被问到“System.out到底是什么”的时候&#xff0c;很多人会愣一下。答案其实很明确&#xff1a;它是java.io.PrintStream的一个实例&#xff…

作者头像 李华
网站建设 2026/10/15 14:29:14

基于Matlab的QR二维码生成与识别:从编码到纠错的完整实现

简介&#xff1a;面向Matlab开发者和图像处理学习者的二维码生成与识别方案&#xff0c;完整演示如何借助Zxing开源库完成二维码的编码与解码。实现中将生成与识别两部分分别封装成独立函数&#xff0c;调用入口非常清晰&#xff0c;适合希望快速集成或深入理解二维码原理的读者…

作者头像 李华
网站建设 2026/10/15 14:28:58

JavaWeb图书馆管理系统源码:从环境配置到部署运行全攻略

简介&#xff1a;这份资源是一套基于Java Web的图书馆管理系统源码&#xff0c;采用MySQL数据库存储数据&#xff0c;面向高校计算机专业学生、Java初学者及需要快速搭建管理系统的开发者。系统围绕图书信息、读者账户、借阅归还等核心业务展开&#xff0c;涵盖Servlet、JSP、J…

作者头像 李华
网站建设 2026/10/15 14:28:09

RT-Thread Nano 3.1.3移植实战:从裸机到多任务

简介&#xff1a;RT-Thread Nano是由RT-Thread开源实时操作系统裁剪出的轻量级内核&#xff0c;面向物联网、智能家居、工业控制等资源受限的嵌入式场景&#xff0c;解决小内存微控制器上多任务调度与实时响应问题。该压缩包共含692个文件&#xff0c;约10.65兆字节&#xff0c…

作者头像 李华
网站建设 2026/10/15 14:28:07

Windows 上绕开 Docker 和 WSL:用 Sandcastle 轻量沙箱跑通 Ralph 自动编程

1. 为什么我决定绕开 Docker 和 WSL 这套组合拳在 Windows 上折腾自动编程工具&#xff0c;大多数人的第一反应是装 Docker Desktop&#xff0c;或者干脆开一个 WSL 子系统&#xff0c;把整个开发环境搬到 Linux 里跑。这套思路本身没问题&#xff0c;Docker 提供了隔离性&…

作者头像 李华
网站建设 2026/10/15 14:27:56

SQL LIKE性能优化:通配符位置与索引匹配原理

1. 为什么LIKE不是“模糊搜索”的代名词&#xff0c;而是精准模式匹配的起点很多人第一次在SQL里看到LIKE&#xff0c;下意识就把它当成“搜关键词”“找包含某段文字的内容”——比如想查所有姓“王”的用户&#xff0c;就写WHERE name LIKE %王%。这确实能跑通&#xff0c;但…

作者头像 李华