ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Codex 报错频发?手写本地代理实现自动重试

Codex 报错频发?手写本地代理实现自动重试 1. 从一次深夜报错说起为什么我要自己写一个重试工具那天晚上十一点半我正用 Codex 跑一个批量重构任务三百多个文件要逐个过一遍。前一百个文件跑得挺顺到第一百零几个的时候终端突然吐出一行红字Selected model is at capacity。我愣了一下手动敲了个回车重试过了。又跑了十几个文件这次换了个报错service is overloaded。再重试又过了。问题不在于报错本身而在于它出现的时机完全随机。有时候连着跑五十个请求都没事有时候三个请求里挂两个。我那天晚上手动重试了大概四十多次手指都敲酸了最后实在受不了关掉终端去睡觉。第二天起来我就想这事不该由人来干机器能判断的失败就该让机器自己重试。于是就有了这个本地自动重试工具。它做的事情说起来很简单拦截 Codex 发出的请求如果返回的是容量不足或者服务过载这类临时性错误就自动等待一小段时间后重试直到成功或者达到重试上限。但真正做起来里面有不少细节值得聊——怎么判断哪些错误该重试、等待多久合适、重试几次放弃、怎么避免把真正的错误也一起重试了。这些问题如果不想清楚工具要么没用要么帮倒忙。这篇文章适合两类人看。一类是正在用 Codex 做实际开发、被这两个报错反复打断节奏的人你想知道怎么让流程不中断另一类是对本地代理、请求拦截、自动重试机制感兴趣的人你想知道一个轻量级的重试层该怎么设计和落地。我会把整个思路、关键决策、实操步骤和踩过的坑都摊开讲你照着做就能复现一个属于自己的版本。2. 先搞清楚对手这两个报错到底在说什么2.1 Selected model is at capacity 的真实含义这个报错翻译过来就是“所选模型当前容量已满”。它不是说你的请求有问题也不是说你的账号有问题而是服务端在告诉你现在这一刻排队要处理这个模型的请求太多了暂时接不了新的。你可以把它想象成一家很火的餐厅。你走到门口服务员说“现在满座了您稍等一会儿”。注意他没说“你不能吃”也没说“你点的菜没有”只是说“现在没位置”。过五分钟再来大概率就有位置了。这个报错的性质就是临时性容量瓶颈跟你的请求内容完全无关。关键点在于它是瞬时的。可能一秒前还满着一秒后就有空位了。所以正确的应对方式不是改请求、不是换模型而是等一小会儿再发一次。2.2 service is overloaded 和上一个有什么不同service is overloaded说的是“服务过载”。这个比容量满的范围更大一些它不是说某个具体模型满了而是整个服务层面的负载过高。可能是请求量突增可能是后端某个环节在扩容也可能是例行维护导致的短暂性能下降。打个比方上一个报错是“这家餐厅满座”这个报错是“整条美食街都挤爆了”。影响范围更广恢复时间可能稍长一点但性质是一样的临时性、可恢复、重试大概率能过。这两个报错在实际使用中经常交替出现有时候同一个任务里两种都能碰到。它们的共同点是都不是你的错都不需要改代码都值得重试。2.3 哪些错误不该重试一张判断表这里是最容易踩坑的地方。如果你把所有错误都拿去重试会出大问题。比如认证失败你重试一百次也还是失败白白浪费时间比如请求格式错误重试多少次结果都一样。所以必须先分清楚哪些该重试、哪些不该。错误类型典型报错信息是否重试原因容量不足Selected model is at capacity是临时性等待后可恢复服务过载service is overloaded是临时性等待后可恢复限流rate limit exceeded是临时性等待后可恢复网关超时502 / 503 / 504是通常是临时性认证失败auth token is unavailable否凭证问题重试无用模型不支持model is not supported否请求本身有问题配置错误unrecognized configuration setting否需要人工修正配置请求格式错误invalid request body否请求内容有误这张表是我在实际使用中慢慢总结出来的。核心判断逻辑就一条这个错误是不是等一会儿再试就有可能成功。如果是就重试如果不是就立刻报错让用户处理。注意限流和容量不足虽然都该重试但等待策略可以不同。限流通常有明确的恢复时间窗口容量不足则更随机。后面讲等待策略时会细说。3. 工具的整体设计为什么选本地代理这条路3.1 三种可选方案对比要做自动重试摆在面前的路其实有好几条。我当初考虑了三种第一种是修改调用代码。在你自己的脚本里包一层重试逻辑捕获异常后 sleep 再重试。这个最直接但问题是只对你自己的代码有效。如果你用的是现成的 CLI 工具、IDE 插件、或者别人写好的脚本你改不了它们的代码。第二种是用现成的重试库。比如某些 HTTP 客户端自带重试机制。但问题是 Codex 的调用链路里重试逻辑不一定暴露给你配置而且判断条件往往不够精细可能把不该重试的也重试了。第三种是在本地起一个代理层。让 Codex 的请求先经过你的代理代理负责转发和重试。对上层工具完全透明它以为自己直连了服务端实际上中间多了一层。这个方案的好处是通用——不管你是用 CLI、插件还是自己写的脚本只要请求走这个代理就自动获得重试能力。我选了第三种。原因很简单我不想每换一个工具就重新改一遍代码我要的是一劳永逸。3.2 代理层的核心工作流程代理层的工作流程可以拆成四步接收请求监听本地某个端口Codex 配置里把请求地址指向这个端口。转发请求把收到的请求原样转发给真正的服务端。判断响应拿到响应后检查状态码和响应体判断是否属于可重试的错误。决定重试或返回如果是可重试错误且未达上限等待后重新转发否则把响应原样返回给上层。这个流程里第三步是核心。判断逻辑写得好不好直接决定工具是帮忙还是添乱。3.3 为什么不做请求内容的修改有人可能会问既然都做代理了能不能顺便改改请求内容比如换个模型、调调参数我的答案是不要。代理层的职责应该尽可能单一。它只做一件事——在临时性失败时重试。一旦开始修改请求内容就会引入一堆新问题改错了怎么办上层工具的行为和预期不一致怎么办调试的时候怎么区分是原始请求的问题还是代理改出来的问题保持代理层“透明”只做重试是我踩过坑之后坚持的原则。工具越简单出问题时越好排查。4. 核心实现细节判断、等待与重试4.1 错误判断不能只看状态码最开始我图省事只判断 HTTP 状态码。5xx 就重试4xx 就不重试。结果发现不行——Selected model is at capacity和service is overloaded这两个报错返回的状态码并不固定有时候是 429有时候是 503有时候甚至是 200 但响应体里带着错误信息。所以判断逻辑必须同时看状态码和响应体。我的做法是先看状态码如果是 5xx 或 429标记为“疑似可重试”。再看响应体如果包含容量不足、服务过载、限流等关键词确认为“可重试”。如果状态码是 4xx 且响应体里是认证、格式、模型不支持等关键词直接判定为“不可重试”。这里有个细节响应体可能是 JSON也可能是纯文本还可能是流式返回。流式返回的情况最麻烦因为错误信息可能夹在数据流中间。我的处理方式是先缓冲一小段响应检查里面有没有错误关键词如果没有再继续透传。4.2 等待策略固定间隔还是指数退避等待多久再重试这个参数很关键。等太短服务端还没恢复重试也是白搭等太长整体效率被拖慢。我试过三种策略固定间隔每次失败都等 2 秒。简单但在服务端持续过载时会形成规律性的重试风暴反而加重负担。指数退避第一次等 1 秒第二次 2 秒第三次 4 秒以此类推。这个策略在业界很常见好处是随着失败次数增加等待时间拉长给服务端更多恢复时间。但缺点是如果前几次都是瞬时抖动后面等待时间会变得很长。指数退避加随机抖动在指数退避的基础上每次等待时间加一个随机量。比如本来该等 4 秒实际等 3.2 到 4.8 秒之间的某个值。这个能避免多个客户端同时重试造成的“惊群效应”。我最终选了第三种。具体参数是基础等待 1 秒每次翻倍最大等待 30 秒抖动范围是等待时间的正负 20%。实测下来大部分临时性错误在第一次或第二次重试就能过很少需要等到 30 秒。4.3 重试上限什么时候该放弃重试不能无限进行。如果服务端真的挂了你重试一百次也没用只会让用户干等。所以必须设一个上限。我的设置是最多重试 5 次。算一下最坏情况的总等待时间1 2 4 8 16 31 秒加上抖动大概在 25 到 37 秒之间。这个时间对于交互式使用来说是可以接受的——用户能感觉到卡了一下但不至于以为程序死了。如果 5 次都失败代理就把最后一次的错误原样返回给上层。这时候用户看到的就是真实的错误信息可以自己决定是继续等还是换个时间再试。提示如果你跑的是批量任务可以把重试上限调高一些比如 8 次因为批量任务对单次延迟不敏感更看重整体成功率。但交互式使用建议保持 5 次以内。4.4 并发控制别让重试变成攻击还有一个容易被忽略的点并发。如果你同时跑多个请求每个请求失败后都独立重试短时间内可能向服务端发出大量重试请求。这不仅帮不上忙还可能让服务端更过载。我的做法是在代理层加一个简单的并发闸门同时进行的重试请求数量设一个上限比如 3 个。超出的请求排队等待等有位置了再重试。这样既能保证重试效率又不会给服务端造成额外压力。这个闸门的实现不复杂用一个计数器加条件变量就行。但效果很明显——加了闸门之后我观察到的重试成功率反而提高了因为服务端没有被重试请求二次冲击。5. 实操落地从零搭起你的重试代理5.1 环境准备与依赖选择我用的是 Python因为写起来快调试方便而且标准库里的http.server和urllib足够应付这个场景不需要额外装框架。如果你更喜欢 Node.js思路完全一样用http模块也能实现。需要的环境很简单Python 3.8 以上标准库即可不需要 pip 安装任何东西一个文本编辑器整个工具就是一个单文件脚本大概两百多行。我故意不引入第三方依赖因为这东西要长期在后台跑依赖越少越稳定。5.2 代理服务的核心代码结构代码分成四个部分第一部分是配置区放在文件开头方便修改LISTEN_PORT 8787 TARGET_HOST api.example.com TARGET_PORT 443 MAX_RETRIES 5 BASE_DELAY 1.0 MAX_DELAY 30.0 JITTER_RATIO 0.2 MAX_CONCURRENT_RETRIES 3 RETRYABLE_KEYWORDS [ at capacity, overloaded, rate limit, too many requests, temporarily unavailable, ] NON_RETRYABLE_KEYWORDS [ auth token is unavailable, not supported, invalid request, unrecognized configuration, ]第二部分是判断逻辑决定一个响应是否该重试def should_retry(status_code, body_text): body_lower body_text.lower() for keyword in NON_RETRYABLE_KEYWORDS: if keyword in body_lower: return False for keyword in RETRYABLE_KEYWORDS: if keyword in body_lower: return True if status_code in (429, 500, 502, 503, 504): return True return False注意这里的顺序先检查不可重试的关键词再检查可重试的。这是因为有些响应可能同时包含两类词比如错误信息里既有“not supported”又有“overloaded”这种情况下应该以不可重试为准避免做无用功。第三部分是等待计算实现指数退避加抖动import random def calculate_delay(attempt): delay min(BASE_DELAY * (2 ** attempt), MAX_DELAY) jitter delay * JITTER_RATIO return delay random.uniform(-jitter, jitter)第四部分是主循环接收请求、转发、判断、重试def handle_request(request): for attempt in range(MAX_RETRIES 1): response forward_request(request) status_code, body response if not should_retry(status_code, body): return response if attempt MAX_RETRIES: return response delay calculate_delay(attempt) time.sleep(delay) return response这四部分拼起来就是一个能用的重试代理了。当然实际代码还要处理流式响应、并发闸门、日志输出等细节但核心逻辑就是这些。5.3 把 Codex 的请求指向本地代理代理跑起来之后需要让 Codex 的请求走这个代理。具体怎么配置取决于你用的工具形态。如果你用的是 CLI 工具通常可以通过环境变量或者配置文件指定请求地址。把地址从官方端点改成http://127.0.0.1:8787就行。有些工具支持base_url配置项直接改这个值。如果你用的是 IDE 插件一般在插件的设置里能找到“自定义端点”或“代理地址”之类的选项填上本地地址即可。如果你用的是自己写的脚本那就更简单了把请求的 host 和 port 改成代理的地址。配置完之后建议先做个简单验证启动代理发一个正常请求看代理日志里有没有记录到这次请求和响应。如果有说明链路通了。5.4 验证重试是否真的生效光看日志还不够得实际验证重试逻辑。我的做法是写一个小的测试脚本故意触发可重试的错误。具体做法是在代理代码里临时加一个开关让它在收到特定请求时返回一个模拟的“at capacity”错误。然后发请求观察代理是否自动重试、等待时间是否符合预期、最终是否成功返回。这个测试很重要因为重试逻辑里的 bug 往往很隐蔽——比如等待时间算错了、重试次数数错了、判断条件写反了。不实际跑一遍你根本不知道它有没有在工作。验证通过后把模拟开关关掉代理就进入正常工作状态了。6. 踩过的坑与排查技巧实录6.1 流式响应被破坏这是我遇到的第一个大坑。Codex 的很多请求是流式返回的数据一块一块地传过来。我最初的实现是把整个响应缓冲完再判断结果流式响应变成了“等全部传完才返回”用户体验完全变了——本来应该逐字显示的内容变成了卡半天然后一次性蹦出来。解决办法是边转发边检查。收到第一块数据时先检查里面有没有错误关键词。如果没有就正常透传后续数据块直接转发不再检查。如果有错误关键词就中断转发进入重试逻辑。这样既保证了正常情况下的流式体验又能在出错时正确重试。6.2 重试把不可重试的错误也重试了有一次我跑一个任务代理一直在重试等了三十多秒才报错。我一看错误信息是“model is not supported”。这个错误根本不该重试但我的判断逻辑漏掉了它。原因是我的不可重试关键词列表不够全。后来我养成了一个习惯每次遇到新的不可重试错误就把它加到列表里。现在这个列表已经积累了十几条覆盖了大部分常见情况。提示你可以把代理的日志打开定期检查有没有“重试了但本不该重试”的情况。发现一条就补一条列表会越来越准。6.3 并发重试导致的连锁失败前面提到过并发问题但我实际踩的坑比预想的严重。有一次我同时跑了十个请求结果服务端返回了一批容量不足。十个请求同时进入重试等待时间又差不多结果它们几乎同时再次发出请求再次全部失败。如此反复十个请求像商量好了一样一起冲击服务端。加了并发闸门和随机抖动之后这个问题就解决了。闸门限制了同时重试的数量抖动打散了重试的时间点。现在即使十个请求同时失败它们也会错开重试成功率明显提高。6.4 常见问题速查表现象可能原因排查方法解决办法代理启动后请求全部失败目标地址配置错误检查 TARGET_HOST 和 TARGET_PORT改成正确的服务端地址重试一直不成功错误被误判为可重试查看代理日志里的错误信息把该错误加入不可重试列表流式响应变卡顿响应被整体缓冲检查是否在转发前做了完整缓冲改为边转发边检查重试间隔越来越长指数退避正常行为确认最大等待时间设置如觉得太长可调小 MAX_DELAY多个请求同时重试缺少并发控制观察日志里重试时间是否集中加并发闸门和随机抖动代理占用 CPU 过高请求体过大或死循环用 top 或任务管理器观察检查代码里是否有未退出的循环这张表里的每一条都是我实际遇到过的。尤其是“错误被误判为可重试”这一条出现的频率最高因为服务端的错误信息格式偶尔会变关键词列表需要持续维护。6.5 日志该怎么打日志是排查问题的命脉。我的代理会记录每一条请求的以下信息请求时间请求方法 and 路径响应状态码是否触发了重试如果是重试第几次、等待了多久最终结果这些信息用一行一条的格式输出方便用grep过滤。比如我想看所有重试记录就grep RETRY想看最终失败的就grep FAILED。日志不要打太多否则文件会迅速膨胀。请求体和响应体只在出错时记录正常请求只记一行摘要。这样既能排查问题又不会占用太多磁盘。7. 一些进阶玩法和个人体会7.1 按错误类型区分等待策略基础的指数退避对所有可重试错误一视同仁。但实际使用中我发现不同类型的错误恢复时间不一样。限流通常几秒就恢复容量不足可能要等十几秒服务过载则介于两者之间。所以后来我做了个改进根据错误类型设置不同的基础等待时间。限流用 0.5 秒基础容量不足用 2 秒基础服务过载用 1 秒基础。这样在保证成功率的同时整体等待时间更短。实现方式就是在calculate_delay里多传一个错误类型参数根据类型选择不同的BASE_DELAY。7.2 把重试统计暴露出来跑了一段时间之后我开始好奇到底重试了多少次成功率是多少哪些错误最常见于是在代理里加了一个简单的统计模块记录总请求数、重试次数、重试成功次数、最终失败次数。每处理一百个请求就在日志里打印一次统计摘要。这个统计帮我做了几个决策比如发现容量不足的错误集中在某个时间段我就把批量任务调整到其他时间跑发现重试成功率有 95% 以上说明重试策略是有效的发现某类错误重试成功率很低就把它移出了可重试列表。7.3 个人体会工具的价值在于让人忘记它的存在这个工具我用了几个月最大的感受是好的工具应该让人忘记它的存在。刚开始用的时候我还会时不时看一眼代理日志确认它在工作。后来就完全不看了因为它一直在后台默默干活该重试的重试该报错的报错。我的注意力重新回到了任务本身而不是被一个个临时性报错打断。这也是我做这个工具的初衷——不是为了炫技而是为了解决一个真实存在的、反复出现的痛点。如果你也经常被这两个报错打断不妨试试这个思路。代码不复杂但带来的体验提升是实实在在的。最后分享一个小技巧如果你不确定某个错误该不该重试可以先手动重试一次。如果手动重试能过那它就值得自动重试如果手动重试还是同样的错误那大概率不该重试。这个简单的判断方法帮我省了很多纠结的时间。
返回列表