ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

终结Antigravity Agent限流终止错误:自动重试与指数退避实战

终结Antigravity Agent限流终止错误:自动重试与指数退避实战 我最近这一个月被 Antigravity 的 Agent terminated due to error 折磨到怀疑人生。任务跑到一半代码逻辑完全没问题结果后台一组限流冲过来直接把 Agent 掐死然后我就得像守着老式打印机一样盯着界面等它转圈、报错、再手动点一次 Retry。更恶心的是只要错误信息里出现 exceeded retry limit, last status: 429 too many requests你手动点得越快它挂得就越快因为每一次手动重试都在继续消耗已经被限流的配额到头来就是一个人跟一个限流器互殴。这篇文章就是来终结这种手动重试循环的。我会从错误根因讲起聊清楚什么时候该重试、什么时候不该重试然后给你一套可以直接抄的自动重试方案核心是一个带指数退避的 CLI 封装脚本外加没有命令行条件时的 UI 自动化兜底最后分享一些我从根源上降低 429 触发概率的实操手法。适合用 Antigravity 跑长时间任务、批量任务或者想把 Agent 接进自动化流程的开发者。这套方法我实际跑了两周夜间批量任务成功率从 60% 出头提到了 95% 左右下面全部是实测过的经验。1. 从手动点 Retry到自动爬起来先搞清楚 Agent 是怎么死的1.1 错误信息链条一段典型的崩溃现场Antigravity 这类 agent 型编程平台的运行方式其实是一个云端 Agent 在替你写代码它要调用大模型做推理、要读写仓库文件、要执行测试命令、还可能要去访问外部工具比如 Figma 这类设计稿来源。这整个链条里任何一环不稳定最终都会反映成一句冷冰冰的话——Agent terminated due to error。但这句话只是结论真正的死因要看它前面的日志。我遇到最多的组合是exceeded retry limit, last status: 429 too many requests, request id: 021789然后紧接着进入 terminated 状态。这条链路的意思是Agent 内部其实已经自己尝试重试过若干次但每次都撞上 429Too Many Requests内部重试次数用尽之后才把任务标记为失败。换句话说我们看到最终报错的时候Agent 自己已经抢救过一轮了只是没救回来。这个细节极其重要因为它直接决定了后面的策略如果你在界面上手动点 Retry其实是在给一个已经吃了很多次限流的任务继续加码。点一次两次可能没事点快了就是火上浇油。这个问题不只在 Antigravity 上出现Codex CLI 这类同类 agent 工具在流量高峰期也经常报同样的 exceeded retry limit本质上都是后端共享配额被瞬间打满。1.2 哪些错误值得重试哪些重试了也白搭在写任何自动重试逻辑之前先把错误分类这件事想清楚能帮你省掉大量无意义的操作。我按能不能通过重试恢复把常见错误分成了三类。第一类是典型的瞬时错误429 限流、5xx 服务端错误、连接重置、网络超时。这类错误的特点是只要等一段时间再试很大概率就好了因为它们通常是因为短时间请求太密集后端需要缓一口气。第二类是环境或资源类错误比如沙箱内存不够、某个临时目录被占、依赖安装超时。这类错误重试有意义但前提是你得先把环境问题解决掉否则重试十次也是同一个结果。第三类是永久性错误鉴权失败401/403、参数格式错误400、权限不足。这类错误重试一万次也不可能成功自动重试脚本最怕的就是在它们上面死循环。错误类别典型表现重试价值应对策略瞬时错误429 / 5xx / connection reset高指数退避重试环境资源类OOM / 磁盘满 / 依赖安装失败中修环境后重跑永久性错误401 / 403 / 400无立即中止并告警所以后面脚本的核心判断逻辑就一句话只对瞬时错误做自动重试其他错误直接退出并把日志留给你。2. 四种自动重试路线我为什么最后选了脚本封装一开始我以为自动重试不就是写个循环吗真正动手才发现选择比想象中多而且每条路线的成本和稳定性差异巨大。我前后评估了四种做法也算把弯路都走了一遍。2.1 CLI 封装最稳的一条路如果你的 Antigravity 环境提供了 CLI我用的是antigravity run 任务描述这种形态具体参数以你自己环境为准那最优解基本没什么悬念在外面套一层重试循环捕捉子进程的输出发现失败特征就按策略重启。这条路稳在哪里第一进程级重试和界面完全解耦不受窗口、按钮位置、UI 改版影响哪怕前端换个皮肤都不影响脚本工作第二你可以拿到完整的 stdout/stderr错误识别可以做得非常精准而不是靠猜第三天然适合接进 CI 和定时任务跑一晚上不用人管。2.2 UI 自动化没有 CLI 时的备用方案如果你的环境只有图形界面没有可用的命令行入口那就只能走 UI 自动化识别错误提示自动点击 Retry 按钮。这个方案能做但很脆后面我会专门拿一整章讲它的坑。这里先给结论能用 CLI 就别碰 UI 自动化。2.3 任务 API 探测与前端轮询第三种思路是看 Antigravity 的 Web 前端有没有暴露任务管理接口。很多这类平台的纯 Web 版后端都有一组任务接口来创建任务、查状态你可以自己写个小客户端轮询任务状态发现失败就调用重新运行的接口。这个方案稳定性介于 CLI 和 UI 自动化之间但前提是你能通过正常渠道拿到接口信息且平台允许你这么做。我没有拿到稳定的官方文档所以只是验证了思路没有投入精力做完整实现。2.4 方案对比与我的取舍方案稳定性实现成本适用场景CLI 封装高低可脚本化、要接 CIUI 自动化低中只有图形界面任务 API中中有接口文档且允许调用Agent 内提示词低极低临时应急顺便说一下第四种Agent 内提示词我试过在任务描述里直接写如果遇到限流请等待后自动重试期望 Agent 自己在内部处理。实测效果很不稳定因为 Agent 的内部重试策略已经被 429 打穿了才会走到 terminated提示词能起到的兜底作用非常有限。作为临时应急可以指望它不如指望自己的重试层。我的取舍很简单主力方案选 CLI 封装备用方案留一手 UI 自动化任务 API 暂时搁置。这也是这篇文章主要讲前两条路的原因。3. 核心交付一个带指数退避策略的 antigravity 重试脚本3.1 脚本骨架与错误识别规则直接上脚本。这个脚本我把失败识别规则集中放在最前面你后续可以根据自己实际看到的错误信息往里加正则维护起来很方便。#!/usr/bin/env python3 antigravity 自动重试封装识别瞬时错误并按指数退避计划重试。 import argparse import logging import re import subprocess import sys import time import random logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[logging.StreamHandler(sys.stdout)], ) log logging.getLogger(antigravity-retry) # 内部重试已经放弃的“失败终态”特征 FAILURE_PATTERNS [ rAgent terminated due to error, rexceeded retry limit, rlast status: 429 too many requests, rError running remote, rconnection reset, r503 Service Unavailable, ] # 永久性错误见到就停不重试 FATAL_PATTERNS [ rAuthentication failed, rinvalid api key, r401 Unauthorized, r403 Forbidden, r400 Bad Request, ] def classify_output(text: str) - str: 返回 fatal / transient / success for pat in FATAL_PATTERNS: if re.search(pat, text, re.IGNORECASE): return fatal for pat in FAILURE_PATTERNS: if re.search(pat, text, re.IGNORECASE): return transient return success def run_once(cmd: list[str], log_file) - tuple[int, str]: proc subprocess.Popen( cmd, stdoutsubprocess.PIPE, stderrsubprocess.STDOUT, textTrue, ) output [] for line in proc.stdout: output.append(line) log_file.write(line) log_file.flush() proc.wait() return proc.returncode, .join(output) def main(): parser argparse.ArgumentParser(descriptionantigravity auto retry) parser.add_argument(--max-retries, typeint, default5) parser.add_argument(--base-delay, typeint, default30) parser.add_argument(--max-delay, typeint, default600) parser.add_argument(--jitter, actionstore_true, defaultTrue, help在退避时间上加入随机抖动避免多个任务同时重试) parser.add_argument(--log-file, defaultantigravity-retry.log) parser.add_argument(cmd, nargsargparse.REMAINDER) args parser.parse_args() if not args.cmd: log.error(请在脚本后传入要执行的 antigravity 命令) sys.exit(2) delay args.base_delay for attempt in range(1, args.max_retries 1): log.info(第 %d 次尝试命令: %s, attempt, .join(args.cmd)) with open(args.log_file, a, encodingutf-8) as f: code, output run_once(args.cmd, f) status classify_output(output) if status success and code 0: log.info(任务成功退出。) return 0 if status fatal: log.error(检测到永久性错误放弃重试。) return code if code ! 0 else 1 if attempt args.max_retries: log.error(达到最大重试次数 %d任务最终失败。, args.max_retries) return code real_delay min(delay * (2 ** (attempt - 1)), args.max_delay) if args.jitter: real_delay real_delay * (0.8 0.4 * random.random()) log.warning(检测到瞬时错误%.0f 秒后重试。, real_delay) time.sleep(real_delay) if __name__ __main__: main()使用方式python3 antigravity_retry.py \ --max-retries 5 \ --base-delay 30 \ --max-delay 600 \ antigravity run 清理仓库里的 TODO 注释并提交需要注意不同版本的 Antigravity CLI 命令签名可能不一样跑之前先用antigravity --help或antigravity run --help确认参数不要直接照抄。脚本默认是成功退出码 无失败特征才算成功这个判断标准已经足够覆盖大多数场景。3.2 重试节奏设计为什么要退避和抖动这个脚本里最核心的参数不是重试多少次而是隔多久重试一次。我见过很多人的第一版重试脚本是sleep(5)然后循环结果把一次 429 硬生生重试成了连续 429甚至把服务端打到 503。指数退避的意义在于第一次失败后等 30 秒第二次 60 秒第三次 120 秒……给后端留出恢复余量而不是在伤口上反复撒盐。抖动jitter也很重要。如果你同时跑 10 个任务每个任务第一次重试都精确地等 30 秒那 30 秒后这 10 个任务会同时打向服务端等于自己制造一波新的限流。我在退避值上加了上下 20% 的随机扰动让重试时间点散开就像 TCP 的随机重传一样避免全局同步。还有一个反直觉的经验遇到 429 时第一次重试等 30 秒到 60 秒成功率最高等 5 分钟以上虽然稳定但浪费了太多时间。所以脚本默认--max-delay 600超过 5 分钟就封顶不再继续翻倍这也是实测下来时间成本和成功率的平衡点。3.3 接入实测从命令行到无人值守脚本写完后我先跑了一遍原本必死的任务一个中等规模仓库的重构任务之前 10 次有 8 次死在 429 上。用上脚本后第一次尝试确实还是挂了但 30 秒后自动重试直接成功全程我没有任何操作。随后我把它接进了 systemd 定时器每天晚上跑一遍批量任务早上起来看日志就行。如果你要把这个脚本接到 crontab建议加一段锁逻辑避免上一个任务还没结束、下一个任务又启动。最简单的做法是用flock包一层flock -n /tmp/antigravity_retry.lock \ python3 antigravity_retry.py --max-retries 5 antigravity run 夜间批量任务-n参数的意思是拿不到锁就直接退出宁可漏跑一次也不要两个任务同时跑互相踩。4. 没有命令行接口的日子UI 自动化接管 Retry 按钮4.1 识别Agent terminated due to error的三种方式有些环境里 Antigravity 只有网页版或桌面客户端没有给你 CLI。这时候自动重试只能走 UI 自动化。第一步是让程序看到错误我试过三种识别方式。第一种是图像模板匹配。截一张错误横幅出现时的界面截图用 OpenCV 的模板匹配找到错误区域在屏幕上的位置。缺点是界面稍微改一点、窗口大小变一下模板就失效属于最脆的一种。第二种是 OCR。定时截屏后用 Tesseract 或 PaddleOCR 识别屏幕文字遇到 Agent terminated due to error 或者 429 too many requests 就触发重试。比模板匹配抗界面变化的能力强一些但 OCR 本身有误差偶尔会误判。第三种是监听日志文件。如果桌面前端会把日志写到本地文件直接 tail 这个文件来检测错误是最准的方案。很多时候你装个桌面客户端日志其实就在某个隐藏目录下值得先翻一翻。4.2 用 pyautogui OCR 实现点击 Retry我实际用的是OCR 判断 pyautogui 点击的组合。逻辑很简单每隔一段时间截屏OCR 识别是否出现错误文案出现之后在预设的按钮区域里找 Retry 按钮的坐标并点击点击后进入冷却等待防止连续触发。import time import pytesseract import pyautogui from PIL import ImageGrab COOLDOWN 60 ERROR_KEYWORD Agent terminated due to error def main(): last_retry_at 0 while True: now time.time() img ImageGrab.grab() text pytesseract.image_to_string(img, langeng) if ERROR_KEYWORD in text and now - last_retry_at COOLDOWN: # 建议用模板匹配确定按钮位置不要写死坐标 retry_btn pyautogui.locateOnScreen(retry_button.png, confidence0.8) if retry_btn: center pyautogui.center(retry_btn) pyautogui.click(center) last_retry_at now time.sleep(10) if __name__ __main__: main()注意这个代码只是演示框架真正用的时候要处理的问题比这多得多多显示器坐标换算、Retry 按钮可能不在同一点、OCR 把 Retry 识别成 Relry 等等。可以先用截图工具固定一个按钮模板并且在一段时间内观察识别准确率再跑长任务。4.3 UI 自动化最坑的几个细节UI 自动化方案我用了一个礼拜就放弃了最大的原因是它带来的问题比解决的问题还多。几个印象最深的坑重试按钮不是每次都在同一个位置。窗口大小变化、错误详情展开与否都会影响布局必须用图像匹配而不是写死坐标。高频率截屏 OCR 会占用大量 CPU我试过 3 秒一次的轮询风扇直接起飞。后来改成 10 秒一次配合只在有新日志时触发 OCR 才算消停。最致命的是点击之后没有任何反馈。有时你以为点了 Retry其实点到的是旁边某个元素任务状态完全没变脚本却进入了冷却期等于把问题掩盖了。一定要有熔断开关。我给自己留了一个全局快捷键按一下就终止整个自动重试进程防止脚本在一个不可恢复的错误上疯狂空转。提示如果你确实只有 UI 这条路我的建议是优先找日志文件做旁路判断用文件内容变化触发 OCR 或按钮匹配不到万不得已不要用纯 OCR 驱动关键操作。5. 治本之策怎么让 429 压根别出现自动重试说到底只是止血真正值钱的是让 429 少发生。跑了半个月之后我的重试成功率越来越高不是因为脚本变聪明了而是我把任务的触发方式改了。5.1 并发拉平少开几个并行会话Antigravity 是按会话消耗资源的同时开 5 个会话跑 5 个任务表面上是并行实际是在共享同一个配额池。429 最常出现在多任务同时冲到高潮的时候。我把并行会话数从 5 压到 2 之后限流次数肉眼可见地下降。如果你的任务没有严格时间要求一次只跑一个限流基本消失。一种很实用的调度方式是令牌桶思路全局只允许 2 个任务同时运行每完成一个从队列里补一个进来。这个逻辑用 Python 的ThreadPoolExecutor(max_workers2)就能简单实现比把 5 个任务一股脑全塞给 Antigravity 要稳得多。5.2 任务拆细把大任务变成小步快跑大任务意味着长时间上下文、密集的工具调用序列中间任何一个突发请求激增都可能引发限流。把一个大重构任务拆成先写方案、再改文件、再跑测试三个小任务每个任务之间休息几分钟不仅限流减少出错了定位问题也更快。我现在的经验是单个任务尽量控制在 10~20 分钟内能跑完的范围超过这个时间就拆。拆任务还有一个额外好处即使某个子任务真的失败了重试的代价也小得多不需要把整个大任务从头跑一遍。这其实是用任务粒度换可靠性是很划算的买卖。5.3 限流头与请求节奏观察、节奏、错峰如果你能拿到接口层面的响应头多留意 X-RateLimit-Remaining、X-RateLimit-Reset 这类字段。我的实际做法是在重试脚本里先解析这些头如果剩余配额快见底就主动多等一会如果 Reset 时间在 1 分钟内干脆等到重置后再试。这比重试脚本盲等要精准得多。错峰也是一个很有效的办法。我观察过一段时间工作日白天 10 点到 12 点是 429 的高发期周末和深夜明显好很多。这背后的逻辑很直白——大家都在用同一批底层资源。所以我的批量任务全部挪到凌晨跑白天只跑交互式的短任务。如果你有调度权限尽量把重活安排在低峰期。6. 跑了一周的真实数据哪些策略是有效的6.1 我记录的 30 次失败任务样本为了让这篇文章不是拍脑袋我专门记录了一周内 30 次失败的 Antigravity 任务包括失败类型、重试后是否成功、用了多久。抽样结果如下失败类型次数自动重试成功率平均恢复耗时429 too many requests1782%14/171~3 分钟含等待5xx / 连接重置771%5/72~5 分钟环境资源类沙箱/依赖450%2/4修复后重试成功永久性错误20%不重试数据里最值得说的是429 的重试成功率最高但前提是遵守退避策略。我早期写的第一版脚本只等 5 秒就重试成功率连 30% 都不到改成 30 秒起步后直接翻倍。重试不是不能干而是不能急着干。6.2 翻车现场与修补过程在数据记录过程中踩了几个印象很深的坑。第一个是输出一行行读导致进程卡死。用proc.stdout.readline()逐行读的时候如果子进程输出太快管道缓冲区会被占满导致子进程阻塞整个脚本就停在那里不动了。修补很简单改成逐行读的同时写文件并 flush或者直接用communicate()不让管道成为瓶颈。第二个是重试不是幂等的。有一次任务失败时其实已经提交了一次 commit自动重试后又提交了一次git 历史里多了两条内容差不多的记录。从那以后我每次跑批量任务前都先打一个快照标签重试前确认工作区状态干净必要时git reset --hard回到失败前的位置。这一点在自动重试里特别重要因为你无法保证 Agent 每次都是从同一个起点开始的。第三个是把 403 当瞬时错误重试了 5 次。后来我在 FATAL_PATTERNS 里补上了权限相关的正则这类错误直接跳过省下了大量无意义等待。建议你也维护一份自己项目里常见的永久错误清单越早识别自动重试的体验越好。6.3 怎么把这套东西接到夜间批量任务上现在的最终形态是一个夜间批量任务编排crontab 定时触发 → flock 保证不重复执行 → Python 重试脚本逐条跑任务 → 每条任务的结果写日志 → 全部跑完后通过企业微信或钉钉机器人推送汇总。具体到命令层面就是0 2 * * * flock -n /tmp/ant_batch.lock /opt/ant/run_batch.sh /var/log/ant_batch.log 21run_batch.sh 内部遍历任务清单逐条调用重试脚本任何一条最终失败都会在汇总消息里标红。这套东西跑了两周夜间任务成功率从 60% 出头提升到了 95% 左右剩下的 5% 基本是永久性错误需要人介入这恰好是我们想要的效果——把人的精力留到真正需要人的地方。最后再说一个小细节我现在的重试脚本会把每次重试时的命令、耗时、错误摘要单独存成 JSON 行文件方便之后做趋势分析。哪个任务、哪个时间段最容易触发限流拉一张图就一目了然。如果你也打算长期依赖 Agent 跑任务这个习惯建议从一开始就养成。毕竟自动重试只是解决挂了怎么办想真正省心还是得靠数据帮你把预判限流、主动规避这件事从玄学变成科学。
返回列表