你有没有遇到过这种情况:同样是调用大模型 API,有时候几百毫秒就开始吐字,有时候等了好几秒还没动静?这篇文章我们就来系统地测一测主流国产大模型的响应延迟,顺便聊聊怎么在实际项目里把延迟压到最低。
延迟指标怎么定义
调用大模型 API 时,"延迟"不是一个单一的数字,至少要关注三个维度:
TTFB(Time To First Byte / 首 Token 时间)
从发出请求到收到第一个 Token 的时间。对于流式场景,这是用户感知延迟的核心指标。TTFB 高意味着用户盯着空白界面等待,体验极差。
影响 TTFB 的因素:模型加载时间、排队等待、网络 RTT、prompt 预填充(prefill)耗时。
TPS(Tokens Per Second / 生成速度)
模型每秒输出的 Token 数量。TTFB 之后,TPS 决定了内容"流淌"出来的速度感。TPS 低即使 TTFB 快,用户也会觉得卡顿。
影响 TPS 的因素:模型参数量、硬件算力、并发负载、输出 Token 长度。
总响应时间(Total Latency)
完整接收所有输出 Token 的时间。非流式场景最重要,也是后处理逻辑的等待时间。
测量方法:Python 精确计时
用普通的 requests 库无法精确捕获 TTFB,必须用流式模式。下面是基于 httpx 的精确测量代码:
import asyncio
import time
import httpx
async def measure_latency(
api_key: str,
base_url: str,
model: str,
prompt: str,
max_tokens: int = 200,
) -> dict:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens,
"stream": True, # 必须开流式才能测 TTFB
}
t_start = time.perf_counter()
t_first_token = None
token_count = 0
async with httpx.AsyncClient(timeout=60.0) as client:
async with client.stream(
"POST",
f"{base_url}/chat/completions",
headers=headers,
json=payload,
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if not line.startswith("data: "):
continue
data = line[6:]
if data == "[DONE]":
break
import json
chunk = json.loads(data)
delta = chunk["choices"][0]["delta"].get("content", "")
if not delta:
continue
if t_first_token is None:
t_first_token = time.perf_counter()
token_count += 1 # 简单按 chunk 计数,实际可用 tiktoken
t_end = time.perf_counter()
ttfb = (t_first_token - t_start) * 1000 # ms
total = (t_end - t_start) * 1000 # ms
generation_time = (t_end - t_first_token) * 1000
tps = token_count / (generation_time / 1000) if generation_time > 0 else 0
return {
"model": model,
"ttfb_ms": round(ttfb, 1),
"total_ms": round(total, 1),
"tps": round(tps, 1),
"tokens": token_count,
}
批量测试封装:
async def run_benchmark(models: list[dict], prompt: str, rounds: int = 3):
results = []
for m in models:
round_results = []
for _ in range(rounds):
try:
r = await measure_latency(
api_key=m["api_key"],
base_url=m["base_url"],
model=m["model_id"],
prompt=prompt,
)
round_results.append(r)
await asyncio.sleep(1) # 避免触发限速
except Exception as e:
print(f"[SKIP] {m['model_id']}: {e}")
if round_results:
avg = {
"model": m["model_id"],
"ttfb_ms": round(sum(r["ttfb_ms"] for r in round_results) / len(round_results), 1),
"total_ms": round(sum(r["total_ms"] for r in round_results) / len(round_results), 1),
"tps": round(sum(r["tps"] for r in round_results) / len(round_results), 1),
}
results.append(avg)
print(f"{avg['model']:30s} TTFB={avg['ttfb_ms']}ms TPS={avg['tps']}")
return results
# 使用示例
MODELS = [
{"model_id": "deepseek-chat", "base_url": "https://api.deepseek.com/v1", "api_key": "..."},
{"model_id": "deepseek-r1", "base_url": "https://api.deepseek.com/v1", "api_key": "..."},
{"model_id": "qwen-max", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "api_key": "..."},
{"model_id": "qwen-plus", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "api_key": "..."},
{"model_id": "qwen-turbo", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "api_key": "..."},
{"model_id": "glm-4-flash", "base_url": "https://open.bigmodel.cn/api/paas/v4", "api_key": "..."},
{"model_id": "moonshot-v1-8k", "base_url": "https://api.moonshot.cn/v1", "api_key": "..."},
]
PROMPT = "用三句话解释什么是向量数据库。"
asyncio.run(run_benchmark(MODELS, PROMPT, rounds=3))
注意:Token 计数用 chunk 数量近似,如果需要精确值,用
tiktoken或模型官方分词器做后处理。
测试结果(2025 年实测参考)
测试条件:prompt 约 20 token,max_tokens=200,国内网络,工作日白天,3 轮平均。
| 模型 | TTFB (ms) | TPS (tok/s) | 总时间 (ms) | 备注 |
|---|---|---|---|---|
| Qwen-Turbo | 320 | 85 | 2,650 | 最快,轻量模型 |
| GLM-4-Flash | 380 | 72 | 3,100 | 极速档,免费额度多 |
| DeepSeek-V3 (deepseek-chat) | 520 | 58 | 3,900 | 性价比高 |
| Qwen-Plus | 580 | 52 | 4,450 | 均衡 |
| Moonshot-v1-8k | 650 | 48 | 4,800 | 长上下文见长 |
| Qwen-Max | 820 | 40 | 5,900 | 能力最强,延迟最高 |
| DeepSeek-R1 | 2,100 | 32 | 8,200 | thinking 阶段耗时长 |
关键发现
1. R1 的 TTFB 是其他模型的 4-6 倍
DeepSeek-R1 在生成可见输出前有一段内部 thinking 阶段(Chain-of-Thought),这段时间 API 不会返回任何 token。实测 TTFB 普遍在 2s 以上,不适合对话类实时场景。
2. Turbo/Flash 类模型 TTFB < 400ms,体感"秒响应"
经验值:TTFB < 500ms 时用户几乎感知不到等待;500ms-1s 明显感觉到停顿;>2s 会有明显不适。
3. TPS 差异影响长文本体验
生成 1000 token 的内容,TPS=85 只需 12s,TPS=32 需要 31s。长文本总结、代码生成等场景 TPS 的影响远超 TTFB。
4. 并发负载明显拖慢 TTFB
高峰时段测同一模型,TTFB 可能涨 50%-200%。生产环境建议做超时重试。
影响延迟的深层因素
模型规模 vs. 推理加速
参数量越大,prefill 阶段越慢,TTFB 越高。但现代大模型平台普遍做了:
- 投机解码(Speculative Decoding):小模型起草 + 大模型验证,提升 TPS
- 连续批处理(Continuous Batching):动态调度多路请求,降低排队时间
- 量化(Quantization):INT4/INT8 推理,降低显存占用和计算量
这些加速技术让 Qwen-Turbo 这类轻量模型能做到极低延迟,同时保持不错的质量。
Prompt 长度的影响
Prefill 阶段的耗时与输入 token 数近似线性相关。实测:
| 输入 Token 数 | DeepSeek-V3 TTFB 变化 |
|---|---|
| 50 tok | 520ms (基准) |
| 500 tok | 780ms (+50%) |
| 2000 tok | 1,450ms (+179%) |
结论:System prompt 不要堆太长,能精简的就精简。
推理模式(thinking)
R1 类带思维链的模型,thinking token 在后端消耗但不传输给客户端(或单独传输)。这段时间对 TTFB 是纯开销。如果你不需要推理过程,优先选非-R1 版本。
优化策略
策略一:场景驱动的模型选择
| 场景 | 延迟要求 | 推荐模型 |
|---|---|---|
| 实时对话、打字机效果 | TTFB < 500ms | Qwen-Turbo、GLM-4-Flash |
| 代码补全、单次问答 | TTFB < 1s | DeepSeek-V3、Qwen-Plus |
| 复杂分析、长文档 | 可接受 2-5s | Qwen-Max、Moonshot |
| 深度推理、数学/逻辑 | 可接受 >5s | DeepSeek-R1 |
笔者在开发 TheRouter 的路由策略时也引入了延迟作为权重因子:根据各模型的实时 TTFB 监控数据,在同等能力的候选模型中优先选择当前响应最快的一个,这在高峰时段能有效改善用户体感。
策略二:流式输出降低感知延迟
非流式模式下,用户要等全部内容生成完才能看到任何东西;流式模式下,第一个 token 到达就开始渲染。即使总时间相同,流式体验好 5 倍以上。
// 前端接收流式响应示例
const response = await fetch('/api/chat', { method: 'POST', body: ... });
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// 解析 SSE,追加到 UI
appendToUI(parseSSEChunk(chunk));
}
策略三:连接池复用
每次请求都建立新的 TCP 连接会额外增加 50-200ms(TLS 握手)。用 httpx 的 AsyncClient 保持连接池:
# 模块级别创建,复用连接
_client = httpx.AsyncClient(
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
timeout=httpx.Timeout(connect=5.0, read=60.0),
)
async def call_api(payload):
return await _client.post(url, json=payload, headers=headers)
策略四:asyncio 并发优化
需要并行调用多个模型或多轮对话预生成时,用 asyncio.gather 而不是串行等待:
async def parallel_calls(prompts: list[str]) -> list[str]:
tasks = [call_model(p) for p in prompts]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [r for r in results if not isinstance(r, Exception)]
并发数控制在 5-10,避免触发平台限速(通常 RPM/TPM 限制)。
策略五:预热与超时兜底
import asyncio
async def call_with_timeout(coro, timeout_ms: int, fallback=""):
try:
return await asyncio.wait_for(coro, timeout=timeout_ms / 1000)
except asyncio.TimeoutError:
return fallback # 超时返回降级内容
# TTFB 超时检测(进阶):第一个 token 超过 threshold 就切换备用模型
小结
| 优化手段 | 效果 | 成本 |
|---|---|---|
| 换用 Turbo/Flash 模型 | TTFB 降低 40-60% | 可能损失部分质量 |
| 开启流式输出 | 感知延迟降低 80%+ | 接近零成本 |
| 连接池复用 | TTFB 降低 50-150ms | 代码改动小 |
| 精简 Prompt | TTFB 降低 10-30% | 需要 prompt 工程 |
| asyncio 并发 | 吞吐量提升 N 倍 | 需要异步重构 |
选模型之前先想清楚你的场景对 TTFB 还是 TPS 更敏感,再做选型决策。对于大多数对话应用,流式 + Turbo 级模型 是性价比最高的组合。
如果你在生产环境中做过类似的延迟优化,欢迎在评论区分享你的数据和经验 👇
作者:TheRouter 开发者,专注 AI 模型路由网关。项目主页:therouter.ai