AI 基础设施实验日志

“探测通过”的端点,未必是线上走的端点

结论先行: 一次 Codex 上下文压缩 503 的故障,最后证明不是上游 OpenAI 的问题,而是本地网关自己加的一道"能力门禁"太严,把本该原样转发的请求拦在了门口。更值得记住的是前半段的弯路:探测端点返回 200,让我们一度以为"能力没问题"——但探测的端点和线上真正走的链路,根本不是同一条协议。

时间线:探测通过,故障依旧

2026 年 8 月 3 日,自建的多模型网关开始集中出现一类报错:Codex 客户端做上下文压缩(remote compaction)时,网关对外返回 503。普通对话请求正常,只有自动压缩触发的请求失败。

排障第一步是做能力探测。晚上 22:01,在后台对承载流量的上游账号执行了一次压缩能力探测,探测成功,数据库和缓存里都记下了"该账号支持压缩"。但 22:05 到 22:06 的真实压缩请求仍然 503,22:14 用最小参数复现也依然 503。

探测通过,线上照挂。两者差了四分钟,差的是两个不同的东西。

定位:先看请求死在哪一层

网关日志里有一条关键线索:失败的请求在调度阶段记录了"无可用账号",而失败记录里的上游账号 ID、上游模型、上游错误三个字段全部为空。这意味着请求在选中账号之前就失败了,根本没有出网关。同账号、同分组、同模型的普通请求当时是 200。

这个判断把故障圈定在本地调度层,不在上游。后续排查不再盯着上游状态,转而审查本地 fork 在官方 remote compaction v2 原生链路之上,自己加的那层严格能力门禁:OpenAI 账号必须同时满足 Responses 可用、且标记为支持远程压缩,才能承接压缩请求。而标记的来源,正是上面那个 22:01 通过的旧探测。

真根因:探测的不是同一条协议

旧的压缩探测调用的是旧端点 /v1/responses/compact;而真正的 remote compaction v2,走的是带 beta 请求头和压缩触发参数的流式 /v1/responses。两者不是同一上游协议。探测通过只能证明旧端点可用,不能证明新链路具备能力——把旧端点的探测结果提升为新链路的准入证明,是这道门禁在逻辑上的原罪。

修复也相应地干脆:handler 里不再为 remote compaction v2 注入本地的能力前置要求,原样转发给上游处理。回归验证用发布前相同的最小请求,HTTP 从 503 变为 200,SSE 里拿到了压缩任务的创建事件,日志确认请求已从接入侧成功选中账号并传到出站侧。

值得一提的是发布纪律。修的是生产网关,晚上 22 点多动手:发布前先做了 PostgreSQL 自定义格式备份并校验,保留发布前的 rollback tag 和旧镜像,蓝绿切换、容器健康、致命日志计数逐项确认,最小请求回归从 503 变 200 才算收尾。排障的勇气,来自回滚按钮真实可用。

弯路值得单独记一笔

在这之前,团队先走了一条有官方依据的岔路:上游确实存在按客户端身份头分桶降载的已知行为(官方主分支有相关提交),于是先做了一个"身份归一化"的热修复,把已知的降载身份收敛到另一个身份。修完后 21:52 和 21:55 仍然复现同类 503,且失败依然停在本地选号阶段。实验证伪后,这个方向被正式排除,没有硬说"可能修好了"。先修已知原因是对的,但修完必须看数据,没数据支持就认栽、换方向。

收束

这次故障留下四条可复用的纪律:

  1. 探测端点必须与线上链路同构。 探测的是旧协议,线上走的是新协议,探测的 200 一文不值。
  2. 做代理转发,不要给自己加比上游更严的门。 网关的职责是原样转发;本地发明的准入条件,迟早拦截一个本该通过的请求。
  3. 定位先看"请求死在哪一层"。 上游账号 ID 为空,是最便宜的定界证据。
  4. 实验证伪要认。 修已知原因没修好,不是白修——它排除了一个选项,排除了也算进展。

"探测通过"四个字,只对探测过的那条路径负责。

© 2026 rotor®GitHub 开源CC BY-NC-ND · 转载请注明出处