大模型 API 响应延迟实测与优化策略

8 阅读7分钟

你有没有遇到过这种情况:同样是调用大模型 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 的时间。非流式场景最重要,也是后处理逻辑的等待时间。

TotalTTFB+output_tokensTPS\text{Total} \approx TTFB + \frac{\text{output\_tokens}}{TPS}


测量方法: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-Turbo320852,650最快,轻量模型
GLM-4-Flash380723,100极速档,免费额度多
DeepSeek-V3 (deepseek-chat)520583,900性价比高
Qwen-Plus580524,450均衡
Moonshot-v1-8k650484,800长上下文见长
Qwen-Max820405,900能力最强,延迟最高
DeepSeek-R12,100328,200thinking 阶段耗时长

关键发现

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 tok520ms (基准)
500 tok780ms (+50%)
2000 tok1,450ms (+179%)

结论:System prompt 不要堆太长,能精简的就精简。

推理模式(thinking)

R1 类带思维链的模型,thinking token 在后端消耗但不传输给客户端(或单独传输)。这段时间对 TTFB 是纯开销。如果你不需要推理过程,优先选非-R1 版本。


优化策略

策略一:场景驱动的模型选择

场景延迟要求推荐模型
实时对话、打字机效果TTFB < 500msQwen-Turbo、GLM-4-Flash
代码补全、单次问答TTFB < 1sDeepSeek-V3、Qwen-Plus
复杂分析、长文档可接受 2-5sQwen-Max、Moonshot
深度推理、数学/逻辑可接受 >5sDeepSeek-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 握手)。用 httpxAsyncClient 保持连接池:

# 模块级别创建,复用连接
_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代码改动小
精简 PromptTTFB 降低 10-30%需要 prompt 工程
asyncio 并发吞吐量提升 N 倍需要异步重构

选模型之前先想清楚你的场景对 TTFB 还是 TPS 更敏感,再做选型决策。对于大多数对话应用,流式 + Turbo 级模型 是性价比最高的组合。


如果你在生产环境中做过类似的延迟优化,欢迎在评论区分享你的数据和经验 👇


作者:TheRouter 开发者,专注 AI 模型路由网关。项目主页:therouter.ai