把 401 当作普通瞬时错误去退避重试,是 API 集成错误处理里代价最高的一类实现。5xx 和 429 自带“过一段时间可能恢复”的语义,重试策略围绕等待时间展开是合理的;401 的含义却是“请求凭据未被接受”。如果凭据本身已经失效,5 秒后重放与 5 分钟后重放的结果不会有差别,多出来的只是请求开销、日志量和告警噪音。无论接入的是 DeepSeek 还是其它大模型 API,这一条都成立:尽量把 401 当作“凭据状态事件”来处理,而不是塞进以时间为轴的退避重试链路。

先回答一个问题:这个 401 会因为重试而变好吗

401 的成因不同,可重试性完全不同。至少可以分成四类:

  1. 凭据静态配置错误:Key 复制不完整、误带换行或引号、环境变量串位。
  2. 凭据状态已变化:Key 被轮换、撤销或停用,而应用还在使用旧值。
  3. 时间相关校验失败:请求采用带时间戳的签名鉴权时,客户端时钟偏差超过服务端允许窗口。
  4. 链路问题:代理、网关或 SDK 在转发时改写或剥离了鉴权头信息。

第一类要修配置,第二类要先拿到新凭据,第三类要先做时钟同步,第四类要先修转发链路。它们都不是“等一等就好”的瞬时故障。所以基本原则是:不要问“401 能否重试”,而要问“引发这个 401 的因素是否已被修正”。重试只能绑定在修正动作之后,不能绑定在退避计时器之后。

一个必须克制的点是:不要拿上一个厂商的错误语义去推断当前 API。同一个 401,不同服务对“Key 不存在”“Key 过期”“账户欠费”的区分可能完全不同,甚至不一定都落在 401 上。接入任何新 API 的第一件事,是把官方文档里的错误码说明整理成一张自己的分类表。对 DeepSeek 的接入也是这样:它如何定义 401、错误体里带什么字段,要用 DeepSeek 自己的官方说明核对,而不是沿用它厂经验。

把 401 从通用重试链路里拆出去

常见的错误实现,是给 HTTP 客户端挂一个通用重试中间件,遇到 5xx 指数退避重试,顺手把 401 也放进去。结果 Key 失效一次,中间件就按 5xx 的节奏重放好几轮,每轮请求都带着同一个无效凭据打向上游。

正确的做法是让 401 不进退避队列,而是转入独立的凭据恢复路径。伪代码示意:

async def send_with_auth(request):
    resp = await http_client.send(request)
    if resp.status != 401:
        return resp

    # 收到 401:先修正凭据,再决定是否重放
    credential = await credential_store.refresh_if_needed()
    if credential is None:
        circuit_breaker.record_failure("auth")
        raise AuthUnavailable(resp)   # 进入告警与降级,而不是重试

    resp = await http_client.send(request.with_auth(credential))
    if resp.status == 401:
        circuit_breaker.record_failure("auth")
        raise AuthRejected(resp)      # 修正后仍 401,说明问题不在凭据,交给熔断
    return resp

两个细节值得强调。第一,刷新凭据后只允许重放一次,不要再进入任何退避循环;第二,修正后仍然返回 401,说明最初的判断很可能错了——可能是 Key 被撤销但配置源没更新,也可能是错误分类本身有问题。此时继续猜测没有意义,应该让熔断器接管并触发告警。

多 Key 池:摘除要带软隔离

在需要管理多个 Key 的场景里,比如按业务线拆分凭据、或者用多个 Key 分摊上游配额,处理逻辑要围绕“单 Key 失效”来设计,而不是围绕“整池失效”。

推荐的通用做法:每个 Key 带元数据(所属业务、接入时间、最近一次 401 时间),收到 401 先标记为 suspect,连续多次后才从可用池摘除。摘除应该采用软隔离:停止新请求使用它,但保留元数据和审计记录,便于事后确认到底是 Key 被轮换了还是配置错误。同时必须监控可用 Key 的数量:如果存量已经低于兜底阈值,摘除动作会加速整池耗尽,这时要比摘除本身更早触发告警。

新 Key 上线时,尽量避免“上线即切流量”的做法。可行的工程策略是新旧 Key 共存一段时间,旧 Key 只承担小比例流量并观察其 401 变化,确认稳定后再完成切换。这会把单次轮换的风险摊薄,而不是让业务在某个部署时刻集体撞上 401。

缓存失效时,真正要防的是刷新惊群

大模型 API 的凭据经常在 SDK 或网关层被缓存,用于连接复用或减少重复计算。缓存带来的副作用是:当凭据已在上游轮换而各节点的缓存还是旧值时,众多实例会在同一段时间内同时收到 401。

如果把每一个 401 都变成一次独立的“刷新 + 重放”,几百个实例的微服务就会在瞬间对配置源或凭据接口发起几百次并发刷新——这不是重试风暴,而是刷新惊群。修复手段是收敛刷新动作:进程内用 single-flight 模式,只允许一个刷新任务在途,其它请求等待同一个刷新结果;跨节点时用分布式锁或租约限制并发,刷新成功后再各自重放一次原请求。刷新失败则直接快速失败并告警,而不是各节点分别重试。

缓存 TTL 的设计也要跟着改。本地 TTL 不能明显长于上游凭据的真实剩余有效期,否则上游已经轮换,节点仍在用过期缓存,等到峰值流量时统一暴露成 401。TTL 过短又会放大刷新频率和请求延迟,所以取值应该依据凭据的实际生命周期和允许的时钟偏差来留余量,而不是拍脑袋给一个固定值。

用重试预算与熔断器收口,而不是无限退避

把 401 从退避链路里拆出来之后,还需要两个兜底机制。

第一是重试预算。即使判定为“修正后重试”,也要给单位时间窗口内的 401 重试总量设置上限。预算的作用不是限制正常流量,而是当修正逻辑本身有缺陷时,例如配置源返回的仍是同一个旧 Key,不会让每次 401 都触发一次低水平重复刷新。

第二是独立的熔断器。认证失败的熔断状态要和 5xx、429 的熔断分开记录,因为它们指向完全不同的故障域。连续 401 达到阈值后就打开认证熔断,新请求快速失败,不进入任何刷新和重试路径;半开探测时,用最小的探测请求验证凭据是否恢复。如果 API 不提供低成本探活方式,就等到下一个真实请求再验证。

在日志侧,401 的记录要留下可归因的信息:凭据的指纹而非完整 Key、请求的 endpoint、鉴权头是否存在、对应的 request_id。没有这些字段,401 分类就只能靠猜。

接到新 401 时的最小验证路径

把上面的判断固化成一套动作,处理任何新 API 的 401 时按顺序执行:

  1. 绕过 SDK,用原始 HTTP 工具构造一个最小请求,区分是 SDK 拼装问题还是凭据本身的问题;
  2. 对照该 API 官方文档,检查鉴权头的名称、Scheme、大小写和拼接方式,不要凭经验假设;
  3. 从官方错误码表确认 401 是否细分,“过期、撤销、无效、欠费”是否对应不同状态;
  4. 检查配置源里存的凭据与文档格式是否一致,排除换行、引号等不易察觉的字符;
  5. 检查请求链路上的代理和网关是否原样放行鉴权头;
  6. 如果鉴权依赖时间戳,检查各节点时钟同步状态;
  7. 把这套验证结果填进团队自己的 API 错误分类表。

对 DeepSeek 的接入工程师,这套路径同样适用。401 处理质量取决于你对错误的分类是否来自官方资料,而不是取决于重试算法写得有多精巧。

一次 Key 失效,理想情况下只应该产生一组受控动作:单飞刷新一次、重放一次、再失败就熔断并告警。任何用退避时间去对抗凭据问题的实现,本质上都是在把配置错误伪装成瞬时故障,最后让监控面板用更高的并发来提醒你这个事实。