← 全部极客知识库

大模型通识 / 大模型应用开发入门

8. 请求失败怎么办:超时、错误与有限重试

原文备份 · 第 10 / 19 篇 · 查看极客时间原知识库 ↗

本篇目录 · 10 节

来源:大模型应用开发入门

复习卡程序可能昨天还好好的,今天却在相同输入上失败。看到报错后连续按几次回车,有时能恢复,也可能只是重复发送了同一个有问题的请求。

本篇要给助手增加的能力,是先判断失败属于哪一类,再决定是否等待重试。认证失败不能靠多试两次解决,输出的 JSON 不合格也不等于网络暂时故障。即使某种错误值得重试,也要知道最多会发几次、每次等多久、最终留下哪些记录。

完成后,程序会用中文提示常见错误,默认仍只尝试一次;你显式指定上限后,才对小范围的临时 HTTP 错误进行有限重试。它不会自动充值、更换密钥或反复修复答案。任务仍是上一课的三张 JSON 复习卡,保留模式、预算与人工审核规则。

环境沿用 Python 3.11+、openai==3.24.0、DeepSeek 官方 API 和 deepseek-flash。资料核实日期为 2026 年 10 月 4 日;代码只做离线验证,所有演示错误由作者构造,没有故意向真实服务制造限流或付费请求。

一、先问“失败发生在哪一层”

把一次调用分成三段,会更容易判断:

本地准备与保存 → 向API发送请求 → 收到响应并检查卡片
       ↓                 ↓                  ↓
 配置/磁盘问题      HTTP错误或连接超时      截断/JSON/引用问题

如果 config.json 写错,API 还没收到请求。如果返回 401,说明服务返回了认证失败的 HTTP 状态。如果正常收到 200,却发现卡片引用了 N9,那么请求传输可能成功,任务输出仍然失败。

这三个问题不应统一处理成“再问一遍”。本篇只在发送请求这一段加入重试;收到正常响应后,上一课的结束状态、JSON 和引用校验照常执行,失败便保留记录退出。

二、看懂最常用的错误,而不是背完整列表

DeepSeek 官方错误码页列出了以下常见情况。本课将它们转换成短提示,表中的处理策略则是本项目的选择。DeepSeek:错误码

情况 含义 本课处理
400 请求格式错误 停止,核对请求结构
401 API Key认证失败 停止,自行核对密钥与账户
402 账户余额不足 停止,自行查看账户情况
422 请求参数错误 停止,检查模型名、字段和值
429 请求受限 有剩余尝试次数时等待后重试
500 服务内部故障 有剩余次数时有限重试
503 服务繁忙 有剩余次数时有限重试

429 不能一律解释成“没钱了”,余额问题在这份文档里是 402。当前 DeepSeek 的限速页也说明,超过账号或相应用户的并发限制可能产生 429。即使本程序每次只发一个请求,同一账户的其他程序也可能正在工作。DeepSeek:限速与隔离

本课自动重试的 HTTP 白名单只有 429、500、503。502、504 等其他状态并没有被我们宣称“永远不能重试”,只是没有纳入这个小项目的自动策略;遇到时先看当前官方说明和实际运行环境。

SDK 会把不同错误变成 Python 异常对象,例如 APIStatusError 可以读取 status_code,APITimeoutError 表示超时,APIConnectionError 表示连接层异常。本篇使用这些类型做分类,不通过搜索错误消息中有没有“timeout”来猜测。SDK 类型与服务错误含义是两个层次,不能把 OpenAI 服务的余额规则直接套到 DeepSeek。OpenAI 官方文档:Python库异常类型

三、为什么本课不自动重发超时请求

没有收到结果,不等于远端没有工作。可能请求还没发出去,也可能已经生成了答案,只是在返回途中断线。普通的超时或连接异常不能总让应用判断清楚究竟是哪一种。

因此,本课遇到 APITimeoutError 或 APIConnectionError 时,会保存分类并停止,即使还有剩余尝试次数。这是偏保守的项目策略,不是说这两类错误在所有系统里都不应该重试。你可以先检查网络、代理、证书、记录和平台信息,再决定是否手动重新运行。

这里还没有预约、扣款、发送邮件等工具操作,只是生成卡片。即便如此,重发也可能增加生成费用。以后接入有外部副作用的工具时,还需要额外处理重复操作,不能把“模型请求可重试”推导成“任何动作都可重做”。

同样,timeout_seconds: 60 是传入客户端的超时设置,不是远端推理在第 60 秒必然取消的承诺,也不是整个多次尝试流程的严格总时限。保持连接的服务端等待、网络处理和多次调用都可能影响实际耗时。

四、先关掉SDK重试,再定义自己的上限

如果你写一个最多三次的循环,SDK 每次内部又重试两次,实际 HTTP 请求就可能比你想的多。本项目从第 02 篇起已设置 max_retries=0,本篇继续保持,重试由自己的代码负责。

配套 api_client.py 中这段完整函数没有变化:

def create_client(settings, key):
    return OpenAI(api_key=key, base_url=BASE_URL,
                  timeout=settings["timeout_seconds"], max_retries=0)

本篇核查了固定 SDK 3.24.0 的本地实现:其默认重试次数为 2,但这里被显式覆盖成 0。离线测试也检查了实际客户端收到这个设置,以及“429、503、成功”是否恰好产生三次 HTTP 请求。

新的 --attempts 表示总尝试次数,只能为 1、2、3,默认是 1。--attempts 3 是首次调用加上最多两次重试,不是首次调用之后再重试三次。成功拿到响应,或出现不适合重试的错误,都会提前结束。

官方重试建议也强调:应用自行处理时,要关闭或计入 SDK 重试,并尊重服务端要求的等待时间。本课采用这些通用原则,但具体白名单和次数限制由我们自己定义。OpenAI 官方文档:重试与限速处理

五、准备代码,并预演重试设置

下载 lesson-08 完整代码包,解压后与旧快照并列,进入 lesson-08 目录。新增文件是 retrying.py 与离线演示模块 retry_demo.py;cards.py 增加参数,card_pipeline.py 增加逐次记录。

macOS / Linux 首次准备:

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
cp config.example.json config.json
.venv/bin/python cards.py --check
.venv/bin/python cards.py --dry-run --thinking off --budget 1024 --attempts 3

Windows PowerShell:

py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
Copy-Item config.example.json config.json
.\.venv\Scripts\python.exe cards.py --check
.\.venv\Scripts\python.exe cards.py --dry-run --thinking off --budget 1024 --attempts 3

Windows 只有 python 时可替换 py -3;已有 config.json 不要覆盖。仍使用同一份固定依赖,首次安装需要网络,环境问题参考第 02 篇。

预演会先显示“本地总尝试上限:3(不是API参数)”,随后打印请求。请求体仍是模型、两条消息、JSON 格式、思考开关和生成预算,不会把 max_attempts 作为模型参数发过去。这是客户端的行为设置,不是要求模型自己生成三次。

配置文件仍只有原来的三个字段;--thinking、--budget、--attempts 都是本次运行的选择,不写回文件。原流式聊天入口 main.py 保持原有行为,本篇的重试只接入 JSON 卡片入口。

六、先在本地看四种典型结果

macOS / Linux:

.venv/bin/python cards.py --demo-retry
.venv/bin/python report.py --include-demo

Windows PowerShell:

.\.venv\Scripts\python.exe cards.py --demo-retry
.\.venv\Scripts\python.exe report.py --include-demo

--demo-retry 固定以总上限 3 演示四种情况,打印提示说明会跳过真实等待。它构造 SDK 异常对象,不读取 Key,也不连接服务;即使不传 --attempts 3,演示仍使用自己的固定设置。

演示情况 实际模拟次数 应观察到的结果
429 → 503 → 成功 3 前两次计划等待,第三次返回卡片并通过规则检查
401 1 认证分类,立即停止
超时 1 timeout分类,不自动重发
429且要求等60秒 1 server_wait_exceeds_limit,停止本轮

前两次计划等待约 1 秒、2 秒,并有很小的随机浮动。演示只打印这个计划,不真正休眠,更不是测量了线上恢复时间。最终应显示“四个离线重试情景均符合预期”。这些失败是预设的测试目标,因此整个演示符合预期时退出码为 0。

如果目录里没有旧记录,本次新增四组请求/结果、六份 attempt 文件,以及唯一成功组的一份 response 文件,共 15 个文件。打开成功组,应该能看到 attempt-01 的 429、attempt-02 的 503、attempt-03 的 received;结果状态为 validated_needs_review。

received 只表示这次收到响应,不代表卡片内容正确。校验失败的正常响应也会先记录为 received,再在整个任务结果中标为 validation_failed。原来的 --demo 仍可演示正确、坏引文与截断三种内容情况。

七、等多久:退避、随机浮动和Retry-After

马上重发可能再次撞上繁忙状态。所谓退避,就是失败后先等一会儿,再逐步增加等待。本课在没有有效服务端提示时,第一次重试等待 1 秒加 0—0.25 秒随机浮动,第二次等待 2 秒加相同范围的浮动。

随机浮动通常称为 jitter,用来减少很多客户端同时醒来再次请求的机会。本例只有一个小程序,不需要引入复杂调度框架,但要理解等待并不是随便添加的动画。

如果异常响应带有有效 Retry-After,程序优先使用它。该字段可能是秒数,也可能是一个 HTTP 日期。本篇支持这两种形式;缺失、无法解析或负数时,才使用本地退避策略。这里是对“如果服务返回这个头”的兼容处理,不承诺 DeepSeek 的每一次错误都会带上它。

本课单次等待上限为 10 秒。假如服务要求等待 60 秒,程序会停止本轮,让你稍后再决定,不会擅自把60秒截成10秒然后提前请求。总共最多两次等待,因此计划等待总和最多20秒;这不等于所有网络调用加起来也只会持续20秒。

核心决策原样摘自 retrying.py:

    delay = server_delay(exc)
    if delay is None:
        delay = 2 ** (number - 1) + random.uniform(0, 0.25)
    if delay > MAX_DELAY_SECONDS:
        return "server_wait_exceeds_limit", None
    return "retry", delay

这段之前还有错误白名单、总次数和服务端显式停止提示的检查。把完整流程放在一起看:先判断是否应该重试,再判断还有没有次数,最后决定等待;不是每个异常都直接走到 sleep()。

等待期间按 Ctrl+C,程序会停止,不再发下一次请求。如果能正常处理键盘中断且磁盘可写,当前 attempt 的决策会改成 interrupted_during_wait,任务结果记为 interrupted。

八、逐次记录怎样帮助你排查

同一个任务仍只保存一份完整 request,因为每次重试使用相同消息、模型与参数。每次尝试另存 同ID-attempt-01.json 等文件,发送前先写 started,结束后更新这一份文件。

attempt字段 含义
number、status 第几次;started、failed、received或interrupted
category、http_status 程序分类与可用的HTTP状态;网络超时可能没有状态码
error_type、hint 异常类型与我们写的中文处理建议,不保存原始异常正文
decision retry、not_retryable、attempt_limit或其他停止原因
planned_wait_seconds 下次等待计划,空值表示没有该计划
elapsed_seconds 这一次send调用的本地耗时;演示为null
usage 此次正常返回响应中拿到的用量;错误时可能未知

它是每次尝试的最新状态文件,不是不可修改的审计事件日志。进程被强制终止时,可能只留下 started;不能把它解释成“没有发出请求”。如果写 attempt 文件失败,程序停止,不为了补日志而再发一次请求。

最终 result.json 中的 attempts 列表汇总这些尝试。顶层 elapsed_seconds 的含义从第 07 篇的单次调用等待,变成整个尝试序列的耗时,包括中间退避和 attempt 文件写入;仍不含最前面的请求记录保存、后面的卡片校验与最终结果保存。记录里的 elapsed_scope 明确标出了这一变化。

顶层 usage 只代表最后收到的正常响应,usage_scope 为 last_received_response_only。假设前两次失败用量未知,最后一次报告20个输出Token,不能说整个任务只消耗了20个。不要把未知用量填0,也不要把 attempt里的成功用量与顶层同一份usage再加一次。

report.py 新增 max_attempts、attempt_count、耗时范围和用量范围。报告默认仍排除演示,使用 --include-demo 才能查看模拟情景。最终成功不会抹掉前面的失败,这些记录为下一课核对成本留下依据。

九、自行执行时,先保持一次,再按需要开启

如果你要用自己的账户请求,先保持一次尝试。以下命令会隐藏输入 Key 并发送可能计费的请求:

macOS / Linux:

.venv/bin/python cards.py --thinking off --budget 1024 --attempts 1

Windows PowerShell:

.\.venv\Scripts\python.exe cards.py --thinking off --budget 1024 --attempts 1

确认任务与参数无误,并愿意接受有限重复调用时,将最后的 1 改为 3。不必故意把真实服务打到429来测试;离线演示已经提供了这条路径。增加上限不等于每次一定发送三次,第一次成功就会返回。

请求返回后仍可能因 length、空正文、坏 JSON 或错误引用被拒绝。这些属于输出检查,程序不会自动再请求一遍。否则你可能只是连续付费生成几个同样不满足要求的答案,而真正该修改的是输入、提示或预算。

本篇也不保证网络重试具有业务上的幂等性——幂等可以粗略理解为重复执行不会产生额外业务效果。我们没有未经核实地向 DeepSeek 加一个所谓“去重Key”就宣布重复请求只收费一次。模型调用与将来工具执行的去重,要分别设计。

十、用两个判断题检查是否理解

第一题:设置 --attempts 3,实际依次返回429、401。应该再发第三次吗?

不应该。每次错误都重新分类;第二次是认证失败,立即停止,不能因为还剩一次额度就继续。可以查看 attempt-02 的分类与提示,排查账户问题。

第二题:第一次503,第二次HTTP成功,但卡片引用N9。还要发第三次吗?

不发。重试层已经完成“收到响应”的工作,随后验证器发现内容错误。结果应为 validation_failed,第二次attempt可以是 received,两者并不矛盾。

最后按症状定位:

现象 优先检查
明明还有次数却不重试 看decision;错误是否在白名单、服务要求等待是否超限
一直401或402 自行核对账户;改预算和增加重试次数不能解决
429不断出现 检查同账户其他请求、发送频率和并发;不要缩短等待硬冲
超时后没自动恢复 本课特意停下,查看记录和网络后再决定是否手动重发
卡片错误但attempt写received 收到响应与任务通过校验是两件事,查看result.status
文件只有request或started 可能写入失败或进程中断,不能据此推断没有计费
多次请求比预想的多 核对API客户端仍是max_retries=0,以及自己是否又加了外层循环

下一篇把这些记录用于费用核对:区分单次用量与任务总成本,理解上下文和缓存怎样影响消耗,避免只看到最后那份成功答案的账面数字。


上一篇:思考模式与预算 · 本部分大纲 · 完整代码包 · 运行说明

验证范围:Python 3.11.8、openai 3.24.0,22项离线测试通过,包括实际SDK+模拟HTTP的429→503→成功、次数边界、永久错误、Retry-After、等待中断和写盘失败。没有真实API调用、个人凭据或线上费用实测。Windows / Linux命令未实机执行。