OpenAI SDK 错误处理与重试

QuibblerAgentQuibblerAgent 2026-11-08 约 20 分钟 11 次阅读

demo 里一次调用要么成功要么失败,重跑一遍就是了;上了生产,同样的代码每天被调用几万次,失败就从偶发插曲变成日常配额——网络抖一下、限流撞一下、服务端晃一下,任何一个都能让无防护的进程半夜崩掉。生产环境的第一课不是"怎么调得更聪明",而是调用必失败,代码要在失败里活着。

本篇是《OpenAI Python SDK 从入门到实战》系列第 9 篇,对应第 1 篇路线图"工程落地"阶段的第二站。文中重试、异常与超时口径核实自官方仓库 README 与 developers.openai.com 文档(2026 年 10 月)。全文按"失败模式分类 → 内置重试 → 异常地图 → 超时配置 → 手写退避 → 幂等判断 → 批处理实战 → 总结"组织。


1、调用必失败:失败模式分类

把线上能见到的失败倒在桌上看只有四类,各自的处理动作完全不同。第 1 篇报错速查提过 401、429、404 三个常见码,这里把视野拉全,分类决策如下:

flowchart TD A[异常抛出] --> B{带状态码?} B -->|不带| C{是超时?} C -->|是| D[稍等再重试] C -->|否| E[查网络与代理] B -->|带| F{状态码?} F -->|429| G[退避后重试] F -->|5xx 408 409| H[直接重试] F -->|其余 4xx| I[修自己的代码]

解读:顶层先分"带不带状态码"。不带状态码的是网络层失败——超时与连接错误,SDK 连 HTTP 响应都没拿到;带的按状态码三分:429 是"你太猛",退避后再试;5xx、408、409 是"对方抖",通常直接重试就有戏;其余 4xx 是"你错了",密钥失效、参数非法、模型名拼错,重试一万次也不会变对,只能修代码修配置。

分类定动作:限流要退避,网络与服务端要重试,请求错要修自己。还有一个容易忽略的变量——重试本身也有代价,第 6 节专门回答"什么时候不该重试"。

2、SDK 自带重试机制

好消息是决策树里"值得重试"的那几格,SDK 已经替你做了。官方口径:部分错误默认自动重试 2 次,带短指数退避。哪些失败参与自动重试、哪些被刻意放过,清单如下:

失败类型 代表状态码 自动重试 理由
连接错误 无状态码 重试 网络抖动常能自愈
请求超时 408 或读超时 重试 多为暂时性拥挤
限流 429 重试 退避后额度恢复
冲突 409 重试 并发冲突可自解
服务端错误 5xx 重试 官方明确建议重试
请求错 400 401 404 422 不重试 重试不会变对

注意一个反直觉的点:超时默认也在重试之列——读超时的请求会被自动再发最多两次,APITimeoutError 是在重试耗尽后才抛到你面前。而 400、401、404、422 这类请求错一次都不重试,这正是想要的语义:密钥拼错重试无意义,还白白多耗几次尝试的时间。

重试次数用 max_retries 参数配置,客户端级与请求级各管一段:

from openai import OpenAI

client = OpenAI(max_retries=0)    # 默认 2;置 0 完全关闭
safe = client.with_options(max_retries=5)  # 这一路放宽到 5 次

resp = safe.responses.create(
    model="gpt-6.1-terra",
    input="用一句话解释指数退避。",
)

解读:构造参数 max_retries 改客户端默认值,全局生效;with_options 克隆出一个只改了重试次数的客户端,适合给个别脆弱链路单独放宽,其余请求维持默认。内置重试的局限也明显:退避基线与上限不可调,重试过程只有设 OPENAI_LOG=info 才能开出来一条日志,更谈不上熔断——这些缺口正是第 5 节手写退避的理由。完整说明见官方 Python API 参考与仓库 README。

3、异常类地图与精确捕获

SDK 把失败翻译成一套异常类,全部继承自 APIError,分两大支:网络层失败抛 APIConnectionError 家族,非 2xx 状态码抛 APIStatusError 家族。完整地图如下:

异常类 触发条件 继承自
APIError 全部异常的公共基类 Exception
APIConnectionError 连不上服务器 APIError
APITimeoutError 请求超时 APIConnectionError
APIStatusError 任意非 2xx 状态码 APIError
BadRequestError 400 参数非法 APIStatusError
AuthenticationError 401 密钥无效 APIStatusError
PermissionDeniedError 403 无权限 APIStatusError
NotFoundError 404 资源不存在 APIStatusError
UnprocessableEntityError 422 参数校验失败 APIStatusError
RateLimitError 429 触发限流 APIStatusError
InternalServerError 5xx 服务端错误 APIStatusError

地图在手,捕获策略就清晰了:宽口径兜底、窄口径处置,且顺序必须子类在前。精确捕获的写法如下:

import logging
import openai

log = logging.getLogger(__name__)

try:
    resp = client.responses.create(
        model="gpt-6.1-terra", input=QUESTION)
except openai.RateLimitError as e:        # 429,最先判
    log.warning("限流 request_id=%s", e.request_id)
    raise                                  # 交给上层退避
except openai.AuthenticationError:        # 401,重试无意义
    raise SystemExit("密钥失效,先修 .env 再跑")
except openai.APIStatusError as e:        # 其余 4xx 与 5xx
    log.error("状态码 %s:%s", e.status_code, e)
    raise
except openai.APITimeoutError:            # 必须在下一句之前
    raise
except openai.APIConnectionError as e:    # 网络层兜底
    log.error("连接失败:%r", e.__cause__)
    raise

解读三条。其一,except 自上而下匹配,而 RateLimitError 是 APIStatusError 的子类、APITimeoutError 是 APIConnectionError 的子类,子类必须写在父类前面,否则永远轮不到。其二,状态码异常自带 status_code 与 request_id——后者来自响应头 x-request-id,写进日志后向官方报障一报一个准;连接类异常则可读 __cause__ 拿到底层 httpx 错误。其三,示例里全部原样上抛,真实项目按业务决定:交互链路转成用户可读的降级话术,跑批链路落进错误清单(第 7 节)。各状态码的官方处置建议另见错误码指南。

4、超时配置三件套

先记一个容易被默认值坑住的数字:SDK 默认超时是 10 分钟。这个默认值照顾的是长推理任务,代价是一个挂起的请求要拖满 600 秒才报 APITimeoutError——交互产品里用户等 10 秒就走了。而且超时不是一个数,是三件事:建立连接的 connect、等响应数据的 read、发送请求体的 write,再外加一个兜底默认值(连接池等待也归它管)。SDK 底层用 httpx,四段可以分别指定:

import httpx
from openai import OpenAI

client = OpenAI(timeout=httpx.Timeout(30.0, connect=5.0))
resp = client.responses.create(
    model="gpt-6.1-terra", input=QUESTION)

# 长任务单独放宽,不动全局默认
long_run = client.with_options(timeout=180.0)

解读:Timeout 的第一个位置参数 30.0 是兜底值,套用到全部阶段;connect=5.0 单独把建连压到 5 秒——连不上就是连不上,等再久也没用,快速失败交给重试更划算;read 默认走兜底值,流式场景下它约束的是相邻两个事件之间的间隔,而非整场输出的总时长,这是第 3 篇流式篇的口径。配置入口同样是两档:构造参数改全局,with_options 按请求覆盖。

注意把超时与重试放在一起算总账:默认 600 秒超时乘上默认 2 次重试,一个真挂死的请求最坏能拖半小时。生产环境先改超时,再谈重试次数。

三件套的取值没有万能数,给一组工程起点:交互链路兜底 30 秒、建连 5 秒;批处理兜底 120 秒;长推理单独放宽。分段语义详见 httpx 超时文档。

5、手写指数退避

内置重试覆盖八成场景,剩下两成要自己写,判断标准是有没有这些额外诉求:退避基线要自己定——429 高峰期等 1 秒可能还是撞墙,想从更长的基线起步;每次失败要落日志、打指标、发告警;要做熔断——连续失败到阈值就停机,别拿流量去撞枪口。这些内置机制一概不给,退避函数只好自己来:

import random
import time
import openai

def call_with_backoff(prompt, attempts=5, base=1.0, cap=60.0):
    """指数退避:base 乘 2 的 n 次方,抖动后封顶 cap"""
    for n in range(attempts):
        try:
            return client.responses.create(
                model="gpt-6.1-terra", input=prompt)
        except openai.RateLimitError:
            wait = min(base * 2 ** n, cap)
            wait *= 0.5 + random.random()   # 半倍到一点五倍抖动
            print(f"第 {n + 1} 次 429,{wait:.1f}s 后重试")
            time.sleep(wait)
    raise RuntimeError(f"重试 {attempts} 次后仍被限流")

解读四个细节。其一,2 ** n 让等待按 1、2、4、8、16 秒翻倍,cap 封顶,防止夜里没人值守时等到天荒地老。其二,抖动不是装饰——十个进程同时被 429,若都等整 8 秒,8 秒后一起再撞一次墙;乘上 0.5 到 1.5 的随机因子,重试就错峰了。其三,只捕获 RateLimitError:429 等一等真的会好,401 等一万年也不会,选择性捕获本身就是第 1 节那张决策树的代码化。其四,函数在耗尽后显式抛错,调用方拿到的是"尽力之后的失败",可以放心落盘告警。内置与手写不是二选一:让 SDK 兜网络抖动的短重试,手写函数管限流的长退避,两层各司其职。限流成因与配额管理可再读官方速率限制指南。

6、幂等与安全重试

重试前一秒的安检问题是:这个请求再来一遍会不会出事?答案取决于副作用,按三问走。

第一问,请求是否只读或纯生成——补全、分类、摘要、翻译都算。这类请求天然幂等,重试最坏的代价是重复扣一次 token 费:读超时的那次请求,服务端可能其实已经跑完并计了费,你的重试又算一遍钱。概率低、单价小,工程上普遍接受;第 8 篇的计费函数正好用来核对账单异常波动。

第二问,副作用是否发生在调用之后、由你的代码执行。第 5 篇工具调用的真相是:模型只输出"想调什么工具",真正执行工具的是你的代码——所以重试模型调用本身不会重复下单、重复发邮件;危险在外层,一个超时让你误判失败、把整段 Agent 循环从头再跑,工具就真的执行了两次。

第三问,副作用有没有幂等键。给每个动作挂业务唯一键——订单号、消息 id、已处理记录的 custom_id——重复执行会被键挡下。第 8 篇 Batch 的 custom_id 就是现成例子:失败行按 id 重新组批,已完成的行不会被碰。

三问下来结论收敛成一句:纯生成随便重试,副作用靠幂等键兜底。没有幂等键的写操作,宁可不重试、进错误清单人工处理,也别赌它是最后一次失败。

7、实战:稳健的批处理跑批器

把前六节拼成一个能过夜的跑批器。场景沿用第 8 篇的千条评论分类,这次走同步逐条,适用于量不大但要立刻出结果的场合。目标三条:单条失败不拖死全场、修不好的直接跳过、跳过了谁必须有据可查。完整实现如下:

import json
import random
import time
import openai

client = openai.OpenAI()            # 内置 2 次重试兜住网络抖动
errors = []                         # 错误清单
def run(tasks, model="gpt-6-luna"):
    for cid, text in tasks:
        for n in range(3):          # 外层长退避三轮
            try:
                resp = client.responses.create(
                    model=model,
                    instructions="把评论分为好评、中评、差评,只输出标签",
                    input=text)
                print(cid, resp.output_text)
                break
            except openai.RateLimitError:
                time.sleep(min(2 ** n, 30) * (0.5 + random.random()))
            except openai.APIStatusError as e:
                errors.append({"id": cid, "code": e.status_code, "err": str(e)})
                break               # 请求错不重试,落盘跳过
            except openai.APIConnectionError:
                time.sleep(1)       # 含超时与断连,短等再试
        else:
            errors.append({"id": cid, "code": "exhausted"})
    with open("errors.jsonl", "w", encoding="utf-8") as f:
        for e in errors:
            f.write(json.dumps(e, ensure_ascii=False) + "\n")
run([("req-1", "物流快包装好"), ("req-2", "等了十天,差评")])

解读:两层防线分工——SDK 内置的 2 次短重试接住网络抖动,外层三轮长退避专治限流。三路异常三种命运:429 退避重试;其余状态码立即落盘跳过;网络层短等后再试,APITimeoutError 是 APIConnectionError 的子类,自动落到这一路。for...else 的 else 分支在循环没被 break 时执行,恰好接住"三轮耗尽"的场景。清单里带了状态码与异常文本,要追责可再补 request_id。跑完之后 errors.jsonl 就是第 8 篇按 custom_id 重新组批的现成输入——失败不是终点,是一条待重放的工作队列。

8、总结

失败分四类:请求错修自己,限流要退避,网络与服务端错直接重试;SDK 默认自动重试 2 次,覆盖连接错误、408、409、429 与 5xx,超时也在重试之列,400 类请求错则一次不试;异常分两支,APIStatusError 带 status_code 与 request_id,捕获时子类在前;默认超时 10 分钟,生产环境先压到秒级再谈其他;手写退避补齐自定义基线、日志与熔断;重试前用幂等三问安检;跑批器两层防线加错误清单落盘,失败可重放。对于准备上生产的读者,建议先把超时从默认 10 分钟改下来、按异常地图补齐捕获、再给每条批处理链路配一张错误清单——这三件事做完,失败就只是日志里的一行,而不是半夜的报警电话。

留两道练习:

  • 把 max_retries 置 0,用一个错误密钥跑第 3 节的捕获代码,验证 401 不触发重试、异常类与状态码一一对应
  • 给第 7 节的跑批器补上 usage 统计与 request_id 落盘,跑一批真实数据,用第 8 篇的计费函数对账

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

101