当你的 AI 应用从内部测试走到百万用户,或者你的智能客服得同时应付上千条咨询,一个很残酷的现实就会摆在你面前:大模型 API 的调用,早就不是简单的“发请求-收结果”了,而是一场跟延迟、限流、超时和成本的多维度博弈。这篇文章不扯抽象架构,也不替任何平台站台,纯粹从开发者的实际工程角度,给出一套能直接复现的调优思路——从客户端代码到底层网关,从限流算法到缓存决策,帮你在大模型 API 高并发场景下,把稳定性和响应速度的底线守住了。在这里插入图片描述

核心矛盾:用户感受 vs 平台限制

先想想这个问题:你的系统同时发出 50 个请求,用户能忍受的最长等待时间是多久?不同场景下的容忍度差别其实挺大的:

  • 实时对话型(比如 AI 客服、聊天机器人):关键看首 token 到达时间(TTFT)。用户希望 1 秒内就看到第一个字在闪,超过 3 秒基本就流失了。
  • 批量推理型(比如文档摘要、代码审查):更关注总生成时间(TPS),P95 延迟在 10 秒以内还算能接受。
  • 流式输出型(比如代码补全、实时翻译):TTFT 和令牌到达的稳定性都很重要,中途卡顿特别破坏体验。

而大模型 API 服务商普遍会设速率限制,常见的单位有 RPM(每分钟请求数)和 TPM(每分钟令牌数)。举个例子,OpenAI 的 GPT-4o 在 Tier 1 账户下通常只有 500 RPM 和 30,000 TPM 的配额。一旦超了限流值,你就会收到 429(请求过多)或 503(服务不可用)错误。所以高并发的本质就是在平台限制用户期望之间找平衡——盲目增加并发只会让限流触发得更频繁,最后整体吞吐量反而下降了。

客户端调优:别搞暴力并发

很多新手写代码就像这样:for 循环里挨个发请求,或者用线程池不加限制地怼上去。这在低 QPS 时还行,一旦负载上来,系统很快就不行了。正确的做法是在客户端造一个“智能水管”,控制水流的速度、方向和应急泄洪。

连接池与复用:减少握手开销

每次 HTTP 请求建立 TCP 连接需要 1-2 次 RTT(往返时延),再加上 TLS 握手,耗时可能到 100-300 毫秒。高并发下,这个开销会被放大很多。解决办法就是使用连接池来复用连接。

拿 Python 的 aiohttp 来说:

import aiohttp

connector = aiohttp.TCPConnector(
    limit=50,          # 总连接数上限
    limit_per_host=20, # 每台主机的连接数上限
    ttl_dns_cache=300, # DNS 缓存 5 分钟
    force_close=False, # 保持连接复用
)
  • limit 设成你期望的最大并发数就好,一般别超过 100,不然可能触发服务端连接限制。
  • keepalive 设置(默认 30 秒)能确保连接不会被过早回收。

Java 里可以用 HttpClient 的连接池配置:

HttpClient client = HttpClient.newBuilder()
    .connectionPool(new FixedConnectionPool(50, Duration.ofSeconds(30)))
    .connectTimeout(Duration.ofSeconds(5))
    .build();

Go 语言的话,通过 http.TransportMaxIdleConnsIdleConnTimeout 字段来控制。

限流算法实战:令牌桶 + 滑动窗口

客户端限流是为了防止自己被平台封杀,这就像个护身符。推荐用令牌桶算法:以固定速率往桶里放令牌,每次请求消耗一个,桶满了就丢掉多余的。

import time
import asyncio
from collections import deque

class TokenBucket:
    def __init__(self, rate: float, capacity: int):
        self.rate = rate          # 每秒放入的令牌数
        self.capacity = capacity  # 桶容量
        self.tokens = capacity
        self.last_time = time.monotonic()

    async def acquire(self):
        while True:
            now = time.monotonic()
            elapsed = now - self.last_time
            self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
            self.last_time = now
            if self.tokens >= 1:
                self.tokens -= 1
                return
            await asyncio.sleep(0.1)  # 轮询间隔

那怎么把 RPM 转成令牌桶参数呢?比如说平台限制 500 RPM,那 rate = 500/60 ≈ 8.33 令牌/秒;要是允许突发,可以把 capacity 设为 rate * 2。对于 TPM 限制,得先估算每个请求平均消耗多少 Token(可以通过响应头 x-ratelimit-remaining-tokens 动态调整),或者更简单点,把 TPM 除以平均每请求的 Token 数,得到一个等效的 RPM 上限。

滑动窗口适合更精细的时段控制,但实现起来稍微复杂一些。大多数场景下令牌桶已经够用了。

重试与熔断:指数退避加抖动

遇到 429 或 503 时,直接重试只会让情况更糟。标准做法是指数退避,再加上随机抖动。

import random
import asyncio

async def retry_with_backoff(coro_factory, max_retries=3, base_delay=1.0):
    for attempt in range(max_retries):
        try:
            return await coro_factory()
        except (aiohttp.ClientResponseError, aiohttp.ServerTimeoutError) as e:
            if e.status == 429 or e.status == 503:
                if attempt == max_retries - 1:
                    raise
                delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
                await asyncio.sleep(delay)
            else:
                raise
  • 如果平台返回了 Retry-After 响应头,那就优先用这个值作为延迟,不用管指数退避了。
  • 熔断器(Circuit Breaker):当错误率达到某个阈值(比如 10 秒窗口内 50% 的请求都失败),自动熔断,返回降级结果或者报错,防止连锁故障。

不同 API 的错误码处理有点区别:OpenAI 返回 429 并且带 Retry-After,Claude API 可能返回 529(负载过高)或 503。聚合平台(比如兼容接入的服务)通常会透传原始错误码,但有些平台会统一包成 500。

流式响应优化:背压与分块

高并发下流式请求(SSE)有两个问题:一是客户端处理速度跟不上接收速度,导致内存越堆越多;二是网络抖动导致流中断。

背压(Backpressure):用异步迭代器,每次只处理下一个数据块,不缓存整个流。

async def consume_stream(response):
    async for chunk in response.content.iter_chunks():
        # 处理 chunk,千万别在循环里做耗时操作
        process_chunk(chunk[0])
        await asyncio.sleep(0)  # 让出事件循环,防止阻塞

超时控制:给流式请求分别设连接超时、读取超时和空闲超时。避免因为网络问题导致连接一直挂在那。

timeout = aiohttp.ClientTimeout(
    total=60,       # 整个请求最大时长
    connect=5,
    sock_read=10,   # 两个数据块之间的最大间隔
)

另外,流式请求更容易触发平台限流,因为每个令牌都会消耗配额。建议在客户端对单个流内部的 Token 生成速率也做个粗略监控,要是发现速率持续低于某个阈值,主动重连或者降级。

服务端/网关层架构:中转与聚合的工程实现

如果你的应用需要给多个下游用户提供 API 聚合能力(比如做个兼容接入的网关),或者需要跨多个模型、平台来分发请求,那服务端架构的稳定性设计比客户端调优还要重要。

负载均衡策略

大模型 API 跟传统 Web 服务不一样:模型请求的延迟差异很大(从 500ms 到 30s 不等),而且每个请求消耗的 Token 也不一样。所以最少连接策略比轮询要好,因为它会把新请求分配给当前负载最轻的后端。一致性哈希适合需要缓存路由的场景(比如同一用户的请求固定到同一节点)。

关键要注意:后端如果出现慢请求(比如一个超长的生成任务),会长时间占用连接,导致其他请求排队。解决办法是设置超时熔断:如果某个后端的 P95 延迟超过阈值,就主动把它降权或者隔离掉。

缓存层设计:哪些请求真的可以缓存?

大模型 API 最大的特点就是非确定性:同样的 prompt,每次输出都可能不一样。但下面这些场景缓存命中率还不错:

  • Embedding 查询:相同文本映射到相同向量,缓存能命中 80% 以上。
  • 格式化输出:要求输出固定 JSON 结构的请求,只要 prompt 一致,结果通常也一致。
  • 知识库问答:当上下文是预先定义好的片段时,可以用语义近似缓存(比如向量数据库检索)。

不过要注意:缓存会带来“过时”的风险。实际中建议设一个较短的 TTL(5-30 秒),或者让用户手动刷新。对于实时性要求高的对话,不推荐用缓存。

多模型路由与故障转移

当单个 API 后端不可用或者被限流时,自动切换到备用模型或备用平台。健康检查应该基于真实请求,而不是简单的心跳:发一个小 prompt(比如“1+1=?”),检测响应质量和延迟。

降级预案举个例子:

  • 主线路:GPT-4o → 限流时降级到 GPT-4o-mini
  • 后备:Claude 3.5 Sonnet → 再失败就用通义千问
  • 最后兜底:返回缓存结果或者友好的错误提示

实现时要注意:不同模型的 Token 计价差异很大,建议在路由策略里加上成本权重,避免自动降级导致预算超支。

资源隔离与配额管理

如果你的服务同时服务多个客户,就需要做租户隔离。最简单的方案是按 API Key 划分,每个 Key 限制并发数和速率。底层实现可以用 Redis 的原子计数器加上滑动窗口,或者直接集成熟网关(比如 Kong、APISIX)的限流插件。

选型决策指南:基于你的并发场景

不同规模的业务需要完全不同的解决方案。下面是一些基于常见场景的保守建议(具体性能数值会因平台、模型版本和网络环境而有差异,请以实际测试为准):

低并发(< 10 QPS)

方案:直接接入官方 API + 客户端限流。

  • 成本最低,不需要中间层。
  • 注意:就算 QPS 不高,有些任务(比如长文档总结)的 Token 消耗可能很大,还是得算算 TPM 有没有超限。
  • 推荐用 Python 或 Node.js 写个轻量脚本,配合前面说的令牌桶和重试机制。

中等并发(10-100 QPS)

方案:官方 API + 聚合平台/兼容接入服务。

  • 聚合平台通常能提供更高的默认速率限制(比如把多个底层账户的配额合并),还支持多线路自动切换。
  • 但得权衡:聚合平台可能会引入额外延迟(中转节点),而且有些平台对模型版本的支持不如官方及时。
  • 企业用户的话,可以关注聚合平台是否提供子账号管理、用量审计、发票这些企业功能。如果不确定,先申请个试用账号,拿真实负载(包含流式请求)做 24 小时压力测试,记录 P95 延迟和错误率。

高并发(> 100 QPS)

方案:私有化部署开源模型 + 自建网关。

  • 当 100 QPS 全部用 GPT-4o 时,月 Token 成本可能高达几十万美元,而且官方也很难保证 SLA。这时候私有化部署一个顶级的开源模型(比如 DeepSeek-V2、Qwen2.5-72B)反而是经济又可控的选择。
  • 自建网关负责负载均衡、缓存、限流、故障转移这些职责。技术栈推荐用 Go 或 Rust 写高性能代理层,后端连接多副本的模型推理服务。
  • 注意:私有部署需要自己维护硬件(比如多卡 GPU 服务器)、做模型量化(INT8 或 FP16)来降低延迟,还要设计弹性扩缩容的机制。

写在最后:大模型 API 的高并发调优没有银弹。这篇文章给的代码和策略,是基于通用经验的最佳实践,但每个平台的具体限制、每个模型的特性都不一样。建议在开发阶段就设计好可观测性(比如把每个请求的延迟、Error Code、Token 消耗都记录到日志里),然后用真实的业务流量反复压测。稳定性的提升不是一次性的,而是一个持续迭代的过程。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐