1. 事故复盘:我那天经历了什么
1.1 日常 AI 基建:Claude Code / Codex / Grok 各司其职
我属于那种把 AI 当成“数字同事”的人,工作流里同时挂着三套主流服务:Claude Code 跑在终端里负责主力编码,Codex CLI 处理跨仓库重构这类批量任务,Grok bot 则用来做文案润色和快速问答。这三个工具各司其职,正常情况下配合得相当顺滑,但有一天它们突然集体翻车,我精心搭建的 Agent 工作流差点就瘫痪了。
Claude Code 是我最依赖的一个。安装很简单,一行npm install -g @anthropic-ai/claude-code就搞定,配好 API key 后直接在项目目录里启动会话。它真正厉害的地方不只是代码生成,而是能把整个仓库的上下文拉起来理解,然后一步步改代码、跑测试、修 bug。我习惯每天早上开工先开一个 Claude Code 会话,把前一天没跑完的需求继续往前推。用了几个月下来,它几乎成了我肌肉记忆的一部分。
Codex CLI 是我的“批处理专员”。它擅长的是那种“给你一个任务,你自己去读文件、改代码、跑检查”的无人值守模式。我不会拿它做探索式开发,但跨模块重命名、统一日志格式、升级依赖这种事,交给它效率极高。安装用的是官方脚本,配置里要指定 API 端点和模型名。我通常会给它喂一个需求 markdown,让它拆成十几个子任务逐条执行。这种批量能力让你很难割舍。
Grok 则相对轻量,我主要用它写提交说明、PR 描述,或者让它在两个模型回答不一致时投个票。它最大的价值是“语气更接近人”,生成的东西不会太像说明书。比如有一次我让三个模型分别解释同一个报错,最后 Grok 的表述最直白,直接说“这个错误是类型不匹配导致的,建议去第 37 行看看”。三家服务有各自的账号和 API key,我原以为这种“分布式”布局已经足够稳,后来才知道这种想法有多天真。
1.2 故障爆发:429、连接重置、网页转圈一起袭来
那天下午两点左右,我在做一个“升级日志系统”的专项任务。Claude Code 刚跑完第一轮仓库扫描,正准备把结果传给 Codex CLI 时,终端忽然被一堆红色报错刷屏。最典型的是:
Error: claude-code: Request failed with status 429 Retrying in 15s...刚开始我以为是订阅套餐的并发上限被撞了,心想等几秒就好。可过了十分钟,429 没消失,反而 Codex CLI 那边开始报更严重的错:codex endpoint /responses连接被重置,偶尔返回一个 200 状态码却带着空响应体。这种“假成功”最坑——我的编排脚本只认状态码,空响应也会被当成正常结果继续往下走,然后生成一堆基于幻觉的后续动作,等到人工发现时已经浪费了很多时间。
与此同时,Grok bot 在网页端也开始转圈,最后弹出一句“服务暂时不可用”。我关掉网页试 API,同样超时。那一刻我才意识到,不是我的账号出了问题,而是“AI 服务大宕机”:多家主流模型服务商在差不多同一时间段集体翻车。这种感觉就像你平时依赖的三条高速路同时封路,导航再怎么规划也都是死路。
1.3 连锁反应:我的 Agent 工作流差点整体瘫痪
我当时正在跑一条相当典型的 Agent 流水线,环节大概长这样:
- Claude Code 扫描所有仓库,找出所有旧日志 API 调用点;
- Codex CLI 根据扫描结果生成替换补丁并逐个应用;
- 每应用完一个补丁,Grok 负责润色提交说明;
- 自动化脚本执行测试并推送异常检测。
看起来很清晰,但故障一出现就全卡住了。Claude Code 在第一步就停了,后面环节全在等待,任务队列越积越长;两个依赖 Claude 输出的子 Agent 在等待超时后直接报错退出,连带着整条流水线标记失败。我想手动接管,却发现每个环节都是自动触发的,根本没有“暂停键”,只能看着任务一个个失败。
这次事故让我彻底想通一件事:表面上我有三家服务互为备份,但因为它们都是云端 API,底层基础设施、网络链路、甚至故障频段都可能重叠,真出大事的时候,所谓“多模型协作”和“单点依赖”没有本质区别。后来我不止一次拿这件事提醒团队,与其追求“用最多的 AI 工具”,不如先把“断了一个怎么办”想清楚。
2. 我的 Agent 工作流架构:表面上多元,骨子里单点
2.1 工作流的逻辑分层与依赖关系
我搭的这套 Agent 工作流,从逻辑上分成四层:
- 入口层:一个常驻的 Python 编排脚本,监听 Git 仓库事件和需求文档变化,有新任务就自动拆解入队;
- 规划层:用 LLM 把任务拆成子任务,并决定每个子任务的执行模型,这一层同样依赖云端 API;
- 执行层:Claude Code 和 Codex CLI 在本地终端跑,通过 API 调用云端模型完成代码修改和命令执行;
- 交付层:生成 commit、PR 描述和变更日志,用 Grok 做润色,或者直接用本地模板。
每一层都有明确的分工,看起来没有任何问题。但仔细看依赖关系就会发现,每一层都卡在“云端 API 必须可用”这个前提上。规划层要调 LLM 做任务拆分,执行层要调 LLM 做代码生成,交付层还要调 LLM 做文案润色。三层全走云端,任何一层断了,整个流水线都会停转。更无语的是,规划层和交付层其实都是很小的任务,完全可以用本地小模型完成,但我当时为了省事全走了云端,把风险全部放在了一起。
2.2 选择这三家的原因,以及忽略的隐患
选 Claude Code 是因为它长上下文理解和 agentic coding 的稳定度确实是我用过最好的;选 Codex CLI 是看中它在批量代码重构上的执行力,以及和现有工程流程的匹配度;选 Grok 则是纯粹喜欢它输出内容的语气,比自己写 PR 描述生动得多。工具本身都很好,问题出在我的架构设计。
但我忽略了三件非常实际的事。
- 三家服务商虽然品牌不同,但它们的高可用机房可能部署在同一个区域,区域性的网络抖动完全可能把它们同时带走;
- 三家的限流策略各不相同,但高峰期的“限流风暴”是同步出现的——当整体请求量暴涨,所有服务商都会在差不多同一时间开始拒绝新增请求;
- 我的编排脚本没有任何降级逻辑,所有失败都只会固定等待后重试,重试失败就带着整条流水线一起死掉。
这几个隐患平时根本不显眼,直到“集体宕机”才一次性暴露。我后来反思,真正的“高可用”不是你有几个模型订阅,而是你的系统能不能在任意一个模型不可用时继续运转。你把三家都买了,不等于你就有了三个选择,你只是把同一场事故买了三份。
2.3 容易忽略的隐形依赖:DNS、网络链路与账户配额
除了模型服务商本身,我的工作流还依赖很多“看不见”的公共设施。比如本地到 API 的网络链路、系统 DNS 解析、API 网关的限流头信息,以及账号本身的配额状态。这些因素平时都不起眼,但一旦出问题,表现起来和“服务宕机”一模一样。
事故当天我第一件事是检查本地网络:用curl -I到一些常用站点,延迟和丢包都正常,说明问题不在我这端。接着我查了各家 API 控制台,发现自己的配额并没有耗尽,但请求依然被拒。这才锁定问题出在服务端侧。这段排查其实花了我不少时间,因为普通的网络检查根本查不出“云服务商内部故障”这类问题。
还要提一笔账户配额。很多时候“大宕机”并不是全线瘫痪,而是限流风暴。由于某个时段请求暴涨,服务商为了保障核心用户,开始对普通订阅做激进限流;你越重试,越触发限流,最终进入恶性循环。所以排查时,一定要把“配额不足”和“服务端故障”分开看,两者处理方式完全不同。前者是省钱和规划问题,后者是容灾和降级问题,混在一起处理只会越搞越乱。
3. 从报错到定位:我的排障实录
3.1 把报错信息当线索,而不是噪音
面对一堆报错时,最忌讳的是看到什么就重启什么。我会先把报错归类,按照下面的表快速判断问题层面:
| 错误类型 | 典型症状 | 可能原因 |
|---|---|---|
| 429 | Request failed with status 429 | 配额不足、限流策略触发 |
| 5xx | 500 / 502 / 503 | 服务端异常或网关故障 |
| 连接层 | Connection reset / timeout | 网络链路、CDN、基础设施 |
| 假成功 | 空响应但状态码 200 | 服务端返回异常,需要校验内容 |
这张表的价值在于,它把“用户可控”和“用户不可控”的报错分开来了。429 和 5xx 虽然是服务端返回的,但前者还掺杂着配额因素,后者基本就是服务端问题;连接层错误则可能来自本地的网络链路。先分类,再决定是重试还是停机,能省下大量无效操作。
那天我一开始看到 429 就以为是配额问题,结果 Codex CLI 那边跟着出现连接层错误,这基本就排除了“只限流”的可能。再去翻三个官方状态页,发现都在不同时段出现了异常,同时第三方监测平台也有大量用户反馈,基本可以断定是行业级事故。这时候再看自己终端的报错,每条都像诊断报告,不再是无意义的噪音。
3.2 排障顺序:先本地后云端,先验证再假设
我整理了一个固化下来的排查流程,现在每次出问题都照着走,效率提升不少:
- 先用
curl -I直接打 API 端点,看 HTTP 状态码和响应时间,确认本地链路是否正常; - 检查本地 CLI 工具的配置,比如环境变量、API key 是否过期,以及本地是否有非必要的中转规则;
- 打开各家官方状态页和官方账号,看有没有发布故障公告;
- 用第三方监测平台做交叉验证,判断影响范围;
- 确认是远端故障后,立即停掉自动重试,启动降级方案。
如果前两步都是正常的,基本可以断定是远端问题。这时候千万别继续重试,重试只会让限流更严重,正确做法是暂停相关任务,改走备用通道。我记得那天我还在犹豫要不要继续等,后来发现等下去毫无意义,果断把主流程切到了人工模式,至少保住了最核心的仓库扫描结果。
3.3 复盘时发现的定时炸弹:睡死重试与级联失败
排查过程中我发现了工作流里的两个致命设计缺陷。
第一个是“睡死重试”。我的旧编排脚本遇到 429 后固定等待 15 秒再重试,一旦遇到持续限流,几百个子任务就会同时卡在重试循环里。这不仅拖垮了我自己的任务队列,还给 API 侧增加了很多无效压力。后来我把重试改成了指数退避,并且加上最大重试次数,超过阈值就熔断。这个改动虽然不起眼,但效果立竿见影,至少不会再出现“任务把自己卡死”的傻事。
第二个是“级联失败”。旧工作流里,一个子任务失败后,它下游的所有任务都会跟着失败,整条主流程归零。这个设计在“所有服务都正常”时看不出问题,可一旦上游进入降级状态,下游全得陪葬。后来我给每个子任务加了执行状态机,支持跳过、重试、标记人工处理,才算堵住这个坑。其实这些概念做后端的人天天讲,但真正做到 Agent 工作流里的很少,大家都默认 API 永远可用。
4. 高可用改造:给 Agent 工作流加逃生通道
4.1 多供应商冗余:把写死的模型改成动态路由
我做的第一件事,是把执行层从“写死调用某一家”改成动态路由。核心思路是:为每个模型封装统一的接口,让路由层根据健康状态和优先级自动选择供应商。这样做的目的不是追求“最强模型”,而是追求“什么时候都有的用”。
MODEL_TIERS = [ {"name": "claude", "client": claude_client, "priority": 10}, {"name": "codex", "client": codex_client, "priority": 9}, {"name": "grok", "client": grok_client, "priority": 8}, {"name": "local", "client": ollama_client, "priority": 5}, ] def execute(task): for tier in MODEL_TIERS: if not is_healthy(tier["name"]): continue try: result = tier["client"].run(task) return validate_result(result) except ServiceUnavailable: mark_unhealthy(tier["name"], cooldown=120) continue raise AllServicesDown()这里有一个细节:每个任务不会盲目选择优先级最高的模型,而是从任务的preferred_models列表里选。比如某个任务明确依赖 Claude 特有的 artifacts 能力,那就把 claude 设为唯一首选,其他模型作为兜底;如果某个任务只是普通的代码格式化,则让本地模型优先,节省 API 配额。我还给路由层加了一个健康状态缓存,每个服务连续失败两次就会被标记为不健康,冷却两分钟后再探测,避免每次都去试错。
4.2 本地模型兜底:云端全挂时的最后防线
当所有云端供应商都不可用时,本地模型是唯一的逃生通道。我在工作流里接入了 Ollama,跑一个 7B 级别的代码模型。它写复杂逻辑的能力确实不如云端大模型,但做基础补全、格式化、生成提交说明完全够用。安装和配置都不复杂,官方镜像拉下来就能在本地起服务,接口风格也和 OpenAI 兼容,改几行 base_url 就行。
具体做法是:路由层检测到所有云端服务都不健康时,自动把执行层切换到本地 Ollama,同时把任务标记成“降级模式”。降级模式下,我只允许它处理低风险任务——比如生成 commit message、修 lint 错误、跑静态检查;涉及核心业务逻辑改动的任务则进入待人工审批队列,绝不会让本地模型直接改代码。这道限制看着保守,但非常有必要,因为本地小模型的“幻觉率”相比云端大模型还是要高一些,让它碰核心逻辑只会帮倒忙。
这一步很重要,它的目的不是追求效果,而是保证工作流不断线,给云端恢复争取时间。实测下来,那天三家服务修好后,我的任务队列里只积压了少量降级任务,整体损失比之前预计的小得多。
4.3 超时、退避、熔断与降级:把“集体宕机”变成“局部抖动”
我封装了一个调用 API 的健壮性中间件,关键参数调整如下:
| 参数 | 原值 | 修改后 | 作用 |
|---|---|---|---|
| 单次请求超时 | 120s | 45s | 快速失败,避免线程阻塞 |
| 第一次重试延迟 | 15s | 5s | 缩短等待,快速探测恢复 |
| 最大重试次数 | 无限 | 3次 | 防止死循环 |
| 重试退避公式 | 固定等待 | 指数退避base * 2^attempt | 减轻服务端压力 |
| 熔断阈值 | 无 | 连续失败5次熔断2分钟 | 保护配额和系统资源 |
熔断器的逻辑有点像家里停电后的总闸:一旦检测到连续失败,直接切断对该服务的调用,而不是硬着头皮反复试。2 分钟后放一个探测请求,成功才恢复服务。这个改造带来的最大变化是,即便某个模型还在故障,也不会继续消耗我的配额,整体的“故障半径”被控制住了。
另外,我把“超时”也分成了连接超时和读超时两层。连接超时设成 10 秒,读超时设成 45 秒,避免某些服务“已经建连但一直不返回响应”把线程拖死。这套参数在不同网络环境下可以调,但整体思路就是快速失败、快速切换,哪怕是误判,也比一直挂起强。
4.4 断点续跑:让任务队列不因一次故障清零
断点续跑也是这次改造的重头戏。我给每个子任务加了一个幂等状态,用 SQLite 记录执行状态,取值包括pending、running、completed、failed、manual。工作流中断后,编排脚本重启时会读取这个状态库,跳过已经completed的任务,只重跑pending和failed的任务。
举个例子:20 个仓库补丁任务跑到第 7 个时宕机了,以前只能清空队列从头跑,现在重启后脚本直接从第 8 个继续。这个能力在日常开发里价值巨大,因为断网、断电、系统重启都难免发生,只要任务能断点续跑,就不会白白浪费时间。
写状态库的时候要注意一个细节:必须在每个子任务真正完成后才更新状态,而不是在任务开始前就标记。否则任务执行一半崩了,状态却显示完成,重启后就会跳过它,造成数据丢失。我的做法是在任务函数末尾统一调用mark_completed(task_id),执行过程中不管怎么异常都不会误标。“先做事,后记账”这个原则,做任务系统的人应该都很熟悉,但真的很容易忽略。
5. 日常避坑与监控告警经验
5.1 为 AI 服务加监控:比官方状态页快一步
官方状态页往往有延迟,而且只反映“是否挂了”,不一定反映“是否可用”。我后来自己写了一个简单的健康检查脚本,每两分钟请求一次各家 API 的真实推理接口,记录响应码和时延,一旦连续三次非 200,就通过群机器人推告警到手机。这个脚本本身没什么技术含量,关键是它把“人工盯着状态页”变成了“系统主动通知你”。
这里有一个小技巧:不要只探测像/models这种轻量管理接口,最好真正发一个小成本的推理请求,比如max_tokens=1的最简 completion。因为某些服务可能“状态页正常但推理服务挂了”,只有真实请求才能暴露问题。虽然这种探测会带来一点点额外费用,但相比一次事故造成的损失,这点钱完全可以忽略。
另外,健康检查的数据也可以直接喂给路由层。每次探测成功就把结果写入一个本地 JSON 文件,路由层读取后就能知道哪些服务当前是健康的,而不是每次任务都要先试错一遍。这个联动改造之后,我的 Agent 工作流在故障期间的切换速度明显快了。
5.2 配额预警:别等到 429 才后悔
另一个经验是把配额管理前置。我每个月会预估各家服务的使用量,并在 API 控制台设好额度告警。比如某家服务当日消耗量达到 80%,就自动停掉非关键任务,只保留核心任务。这样既避免月底超支,也大幅降低触发限流的概率。很多人的 429 其实不是服务商挂了,就是自己把配额用光了,然后在最不该出问题的时候卡住。
同时,我在路由配置里把任务分成不同优先级,核心任务走付费和稳定性更高的服务,非核心任务走更便宜的备用模型。这不仅能省钱,还能避免“所有任务都挤在同一份配额里,大家一起撞线”的尴尬。比如批量文档润色我全丢给 Grok 或者本地模型,Claude 的配额都留给代码改造。实践证明,这种分流比单纯提高订阅等级划算得多。
5.3 常见问题速查表
我把这段时间遇到的高频问题整理成了一张速查表,也分享给了团队同学,大家反馈说比翻官方文档快得多。尤其是那种“看着像宕机,其实是自己配置问题”的情况,表格里基本都有对应解法。
| 症状 | 最可能原因 | 解决思路 |
|---|---|---|
| Claude 突然 429 | 账户额度或区域限流 | 查用量控制台,切换备选模型 |
| Codex CLI 报 Connection reset | 服务端网关故障 | 停止自动重试,查看状态页 |
| Grok 网页端转圈 | 官方服务异常 | 等待恢复,或改用 API 调用 |
| 空响应却显示成功 | 服务端返回了畸形 body | 增加响应校验,空内容判为失败 |
| 编排脚本定时卡住 | 超时过长或重试死循环 | 缩短超时,启用熔断机制 |
| 本地模型参与后质量下降 | 模型能力不足 | 限定为低风险任务,核心逻辑人工处理 |
表格里最值得说的是“空响应却显示成功”这一行。很多 agent 框架只校验 HTTP 状态码,不校验内容,这导致服务端返回空 body 时也会被判为成功。我后来在所有解析响应的地方统一加了内容校验,只要content为空就抛异常,这才堵住这个坑。
5.4 把宕机当常态:Agent 工作流的设计原则
最后说点非技术的事。这次事故彻底改变了我设计系统的心态,从“默认 AI 服务永远可用”变成了“默认 AI 服务随时可能挂掉”。任何一个新的 Agent 工作流上线前,我都会问自己三个问题:
- 如果所有云端模型同时挂掉,我还能手动完成任务吗?
- 如果某个模型连续失败,工作流能自动降级到可用模型吗?
- 任务中断后重启,能不能从断点继续,而不是从头再来?
这三个问题的答案,我会写进架构评审 checklist 里。把逃生通道当成第一需求,而不是事后补救,才是这次“AI 服务大宕机”给我上的最重要一课。技术的细节可以慢慢调,但如果整个系统压根没有“逃生”的概念,那平时跑得再顺,也可能在某一天突然归零。
这次“AI 服务大宕机”最后持续了几个小时,三家服务才陆续恢复。我虽然没有损失关键数据,但那种看着工作流瘫痪却无从下手的感觉,真的不想再体验第二遍。后来我在每一次架构评审里都把“逃生通道”列为必查项:本地模型兜底、多供应商路由、熔断降级、断点续跑,缺一样都不允许上线。如果你也在跑类似的 Agent 工作流,趁着服务都正常,赶紧把这些能力补上。等真出事了再想对策,就像着火之后才找灭火器,不是每次都这么幸运的。