大模型通识 / 大模型应用开发入门
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命令未实机执行。