← 返回资讯

大模型 API 超时与连接管理

2026-06-26

大模型 API 的超时比普通 HTTP 接口复杂:非流式请求需等模型生成完毕(可能 30-120s),流式请求则需持续读取直到 [DONE]。错误配置超时会导致大量假性失败或内存泄漏,本文给出各场景的参考值与代码示例。

先说个真实场景:你把某个接口从传统 REST 服务切到大模型 API,线上跑了两天,监控面板突然一堆 504,报错信息长这样:

httpx.ReadTimeout: The read operation timed out

或者 Node 这边是:

FetchError: request to https://api.xxx.com/v1/chat/completions failed, reason: read ECONNRESET

排查半天发现服务端其实在正常生成,只是你用的还是旧接口那套「5 秒超时」的老配置——大模型吐 token 是按秒甚至几秒一个字往外挤的,尤其长文生成或推理模型(o1/DeepSeek-R1 这类会先”思考”再输出),首字延迟(TTFT,Time To First Token)动辄五六秒起步,你拿老配置去卡新接口,超时率蹭蹭涨,而这些请求在服务端其实是成功的,纯粹是客户端等不及先断了——这就是本文开头说的”假性失败”:日志里一堆超时,但对方账单里这些 token 照样计费。

超时类型与推荐值

超时类型含义推荐值
连接超时(connect timeout)TCP 握手 + TLS 建立5-10s
请求超时(request timeout)从发出请求到收到完整响应非流式:60-120s;流式:300s+
读超时(read timeout)两个数据包之间的最大间隔30-60s(流式逐 token 推送时适用)
总超时(total timeout)整个操作的上限视业务 SLA 定,建议 120-180s

这张表里最容易被搞混的是”请求超时”和”读超时”——两者不是一回事。请求超时是从你发出请求到拿到完整响应的总时长,流式场景下这个值要给得很大(因为整个流可能持续几分钟);读超时则是相邻两个数据包/chunk 之间的间隔,只要模型还在陆续吐 token,哪怕总耗时超过 5 分钟也不该触发读超时。反过来,如果模型卡住不吐字了(比如上游排队、GPU 资源紧张),读超时才是真正保护你的那道闸——它能在合理时间内判定”这次请求死掉了”,而不是傻等到总超时那么久。

连接超时给 5-10s 是因为大模型服务商大多走国际线路或专线中转,跨境 TLS 握手(尤其是 TLS 1.3 的一次往返 + DNS 解析)比国内接口慢,给太短(比如默认的 3s)在网络抖动时会误杀正常请求;但也别无脑设成 30s,那样一旦服务商真的挂了,你的请求会白白卡住半分钟才报错,拖慢整体的失败重试节奏。

Python:openai SDK 超时配置

import os
from openai import OpenAI
import httpx

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url=os.environ.get("OPENAI_BASE_URL", "https://api.lidayun.com/v1"),
    timeout=httpx.Timeout(
        connect=10.0,    # TCP+TLS 握手
        read=60.0,       # 两包之间最大间隔
        write=10.0,      # 发送请求体
        pool=5.0,        # 从连接池获取连接
    ),
    max_retries=2,       # 超时自动重试次数
)

# 单次请求覆盖全局超时
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "写一篇 500 字文章"}],
    timeout=120.0,   # 覆盖为 120s 总超时
)
print(resp.choices[0].message.content)

这段配置里有个容易踩的坑是 pool=5.0——这个参数管的是”从连接池里拿到一个空闲连接”这个动作的等待时间,不是网络请求本身。如果你的并发量超过了连接池的最大连接数(httpx 默认 max_connections=100),后来的请求会在这里排队,排队超过 5 秒就抛 PoolTimeout,报错信息类似:

httpx.PoolTimeout: All connection attempts failed

看到这个错误第一反应不该是调大 pool 的秒数,而是应该去查你的并发上限设置对不对——把等待时间调大只是把问题往后拖,真正的修法是显式配置 httpx.Limits(max_connections=200, max_keepalive_connections=50) 并传给 httpx.Client,再套进 OpenAI(http_client=...) 里,让连接池匹配你实际的并发规模。max_retries=2 这行也值得多说一句:openai SDK 内置的重试只对连接类错误、429、5xx 生效,且默认走指数退避加抖动,不需要你自己再包一层——除非你想自定义退避策略或加日志埋点,否则没必要重复造轮子。

Python:流式请求超时

流式请求应把 read 超时设大,因为模型生成慢时两包间隔可能超过默认值:

import os
from openai import OpenAI
import httpx

stream_client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url=os.environ.get("OPENAI_BASE_URL", "https://api.lidayun.com/v1"),
    timeout=httpx.Timeout(connect=10.0, read=120.0, write=10.0, pool=5.0),
)

stream = stream_client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "写一篇 2000 字技术博客"}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content or ""
    print(delta, end="", flush=True)

这里把 read 从 60s 提到 120s 是因为流式场景下”两包间隔”更容易被拉长——推理模型在思考阶段可能十几秒都没有 chunk 吐出来,普通对话模型在生成到复杂段落(比如代码块、表格)时偶尔也会卡顿一两秒。判断该给多大的 read 超时,最简单的办法是先跑一段时间线上日志,统计相邻 chunk 的间隔分布,取 p99 再乘 1.5-2 倍作为安全边界,而不是拍脑袋定一个数字。

还有个常被忽略的点:流式请求里,for chunk in stream 这个循环本身也可能因为网络中断而卡死不报错——这时候光靠 httpx 的超时不够,建议配合业务层的”心跳检测”,比如记录上一个 chunk 到达的时间戳,超过阈值主动 stream.close() 并触发重试,而不是干等 SDK 内部超时生效。

Node.js:超时配置

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL ?? "https://api.lidayun.com/v1",
  timeout: 90_000,        // 全局 90s 总超时(毫秒)
  maxRetries: 2,
});

// 单次请求覆盖
const resp = await client.chat.completions.create(
  { model: "gpt-4o-mini", messages: [{ role: "user", content: "你好" }] },
  { timeout: 30_000 }     // 第二参数覆盖全局
);
console.log(resp.choices[0].message.content);

Node.js 这边底层用的是 undici(Node 18+ 内置 fetch 的实现),跟 Python 的 httpx 有个明显区别:undici 默认的连接超时和请求超时是合在一起算的,不像 httpx 那样把 connect/read/write/pool 拆成四个独立维度。所以你在 timeout: 90_000 这里设的其实是一个”总盘子”,如果你需要更细粒度的控制(比如单独设连接超时),得自己传 dispatcher,用 undici.Agent({ connect: { timeout: 10_000 } }) 构造后塞进 fetch 的 dispatcher 选项——多数业务场景不需要走到这一步,但如果你发现连接慢和生成慢分不清是谁的锅,这是唯一能拆开看的办法。

另外提醒一句:Node 项目里如果用的是自己拼的 fetch 而不是 openai 官方 SDK,超时得靠 AbortController 手动实现,官方 SDK 帮你把这层封装好了,没有特殊需求没必要自己重写这段逻辑。

连接池复用

openai SDK 底层(Python 用 httpx,Node.js 用 undici)默认启用 HTTP/1.1 keep-alive 连接复用。建议复用 client 单例,避免每次请求新建 TCP 连接:

# 推荐:模块级单例
_client: OpenAI | None = None

def get_client() -> OpenAI:
    global _client
    if _client is None:
        _client = OpenAI(
            api_key=os.environ["OPENAI_API_KEY"],
            base_url=os.environ.get("OPENAI_BASE_URL", "https://api.lidayun.com/v1"),
        )
    return _client
// 推荐:模块级单例
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export default client;

为什么强调”单例”这件事,值得展开说说背后的成本账。每次新建连接都要走一遍 TCP 三次握手 + TLS 握手,跨境线路下这一趟往返可能就是 100-300ms,如果你在 Serverless 函数里每次调用都 new OpenAI()(而不是复用容器间的模块级实例),相当于每次请求都白白多付出这几百毫秒,量一大就是实打实的延迟和成本。下面这张表是两种写法在真实压测下的大致差异(具体数值随网络环境浮动,仅供判断量级参考):

写法首次请求耗时后续请求耗时适用场景
每次新建 client + 新建连接正常每次都多 100-300ms 握手开销几乎没有合理场景
单例 client + keep-alive 复用正常复用已有 TCP 连接,省去握手常驻服务、Web 后端
Serverless 冷启动首次较慢同一容器内后续请求可复用需注意容器复用窗口

Serverless 场景要格外注意:只有同一个执行环境(容器)被复用时,模块级单例才有意义;如果平台每次都冷启动新容器,复用效果会打折扣,这时候更该关注的是缩短冷启动本身,而不是纠结连接池配置。

超时后的兜底

import httpx, time

for attempt in range(3):
    try:
        resp = get_client().chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": "你好"}],
            timeout=30.0,
        )
        break
    except httpx.TimeoutException:
        if attempt == 2:
            raise
        time.sleep(2 ** attempt)

这段代码用的是最基础的指数退避(2 ** attempt:2s、4s),能用但有个隐患——如果你的服务在高并发下集中触发超时(比如上游服务商短暂抖动,同时影响了你几百个并发请求),这些请求会在完全相同的时间点集体重试,形成新一轮的流量脉冲,反而可能把刚恢复的服务又打挂,这就是所谓的”重试风暴”(thundering herd)。生产环境建议在退避基础上加个随机抖动:

import random

sleep_seconds = (2 ** attempt) + random.uniform(0, 1)
time.sleep(sleep_seconds)

加了 random.uniform(0, 1) 之后,同时失败的请求会被打散到一个时间窗口里逐个重试,而不是撞在同一秒,这是分布式系统里退避重试的标准做法,成本几乎为零但能显著降低重试造成的二次冲击。

还有一点容易被忽略:重试次数不是越多越好。像本例这样重试 3 次、每次退避 2-5 秒,最坏情况下一个请求可能要拖到十几秒才最终失败并抛出异常,如果你的业务对响应时间敏感(比如同步等待结果的用户交互场景),2 次重试、退避时间压到 1-3 秒会更合适;如果是后台批量任务对时延不敏感,重试次数可以放宽到 5 次并配合更长的退避。这个取舍没有标准答案,取决于你的业务对”晚一点拿到结果”和”多消耗一点重试成本”哪个更能接受。

常见问题

Nginx 反向代理后流式超时怎么处理? Nginx 的 proxy_read_timeout 默认 60s,流式推送慢时会被截断,需调大:proxy_read_timeout 300s;,同时设 proxy_buffering off;

超时时间设多长合适? 非流式对话类建议 60s,长文生成建议 120s,批量任务建议 300s。过长会占用连接资源,过短会产生大量假失败。

超时后请求是否还在服务端执行? 大多数平台会在连接断开后终止生成,但计费可能已计入已生成的 token。建议在超时重试前评估是否真有必要重发。

用 async/await 时超时如何设置? Python 异步场景用 asyncio.wait_for 包裹,或在 AsyncOpenAI 初始化时同样传 httpx.Timeout,参数相同。

超时和 429 限流报的错看着很像,怎么区分? 超时是客户端等待时间到了主动断开(异常类型是 TimeoutException/ReadTimeout),本质是”还没等到结果”;429 是服务端明确返回了状态码,告诉你”这次请求我拒绝了”,本质是”配额或速率超限”。两者的处理逻辑完全不同——超时可以直接按固定间隔重试,429 必须尊重响应头里的 Retry-After 字段等够时间再重试,否则只会越限越死。限流的具体处理见文末链接的 429 专题文章。

同一个请求,为什么有时候 3 秒返回、有时候 30 秒才返回? 大概率是撞上了服务商那边的排队。大模型推理是排他性占用 GPU 资源的,高峰时段(尤其是国内工作日上午 10 点和晚上 8-10 点这两个使用高峰)请求要先在服务端排队等资源释放,这段排队时间对你来说就是纯粹的”白等”,跟你自己的网络或代码质量无关。如果你的业务对延迟敏感,一个实用的做法是给关键请求设置比平时更短的超时(比如 15-20s)并快速失败转人工兜底话术,而不是让用户对着加载动画等一分钟。

超时配置要不要按不同接口区分? 要。同一个服务里,“生成一句欢迎语”和”生成一篇长文报告”不该用同一套超时参数——如果全局统一设成 120s,短请求超时时也要等 2 分钟才能判定失败,体验很差。更合理的做法是在业务层按接口类型维护一张超时配置表,短请求给短超时(20-30s),长文/批量任务单独传更大的 timeout 参数覆盖全局值(就是本文示例代码里单次请求覆盖全局配置那种写法)。

自查清单

配置改完别急着上线,照着这几条过一遍,能避开大部分线上超时相关的坑:

  1. 区分了流式和非流式两套超时参数,没有用同一套配置糊弄两种场景。
  2. 连接超时给了 5-10s 的余量,不是照抄某个默认值(很多 HTTP 库默认连接超时只有几秒甚至更短)。
  3. 重试逻辑加了随机抖动,不是纯粹的 2 ** attempt 死板退避。
  4. 客户端用的是模块级单例,没有每次请求都新建 client 实例。
  5. 反向代理(Nginx/Caddy 等)的超时设置跟客户端超时对齐了,没有出现代理侧先掐断连接、客户端还傻等的情况。
  6. 日志里能区分出”超时失败”和”429 限流失败”和”401 鉴权失败”这三类,而不是混在一起的一个大坨”请求失败”。

更多接入基础见大模型 API 接入完全指南接入教程专题。速率限制触发的 429 处理见 429 限流:指数退避与重试;鉴权问题见 401 鉴权失败排查