|
| 1 | +--- |
| 2 | +title: 如何实现 Claude Code 和 Codex 等 Agent CLI 的自动重试 |
| 3 | +date: 2026-04-18 |
| 4 | +tags: [Agent CLI, Claude Code, Codex, 自动重试, HagiCode] |
| 5 | +--- |
| 6 | + |
| 7 | +## 如何实现 Claude Code 和 Codex 等 Agent CLI 的自动重试 |
| 8 | + |
| 9 | +> 自动重试这个词,看着像是个小开关,真落到工程现场里,完全不是那么回事。全民制作人们大家好,我是 HagiCode 制作人俞坤。今天这篇我们不聊空话,就聊 Claude Code、Codex 这类 Agent CLI 的自动重试到底该怎么做,才能既接得住异常,又不把系统带进无休止的重复执行里。 |
| 10 | +
|
| 11 | +## 背景 |
| 12 | + |
| 13 | +如果你最近也在折腾 AI 编程,那这类问题你大概率已经碰到过了:任务不是一上来就挂,而是跑到一半断掉。 |
| 14 | + |
| 15 | +这事儿放到普通 HTTP 请求里,很多时候就是重发一下,顶多补个指数退避。可是 Agent CLI 不一样。Claude Code、Codex 这类工具通常是流式执行的,输出是一段一段往外推,过程中还会绑定 thread、session 或 resume token。换句话说,它不是“这一请求失败了没有”,而是: |
| 16 | + |
| 17 | +- 前面已经吐出来的内容还算不算数 |
| 18 | +- 当前上下文还能不能接着跑 |
| 19 | +- 这次失败该不该自动恢复 |
| 20 | +- 如果要恢复,多久再试,试的时候发什么,原上下文还要不要复用 |
| 21 | + |
| 22 | +很多团队第一次做这里,都会下意识写一个最朴素的版本:报错了就再试一次。你说得非常正确,这个想法很自然,可是真进项目里,问题就一个接一个冒出来了。 |
| 23 | + |
| 24 | +- 有些错误明明是暂时故障,却被当成最终失败 |
| 25 | +- 有些错误根本不值得重试,却被系统反复重放 |
| 26 | +- 有 thread 的请求和没有 thread 的请求,被一把梭地一视同仁 |
| 27 | +- 退避策略没边界,后台请求自己把自己打爆 |
| 28 | + |
| 29 | +HagiCode 在接多种 Agent CLI 的过程中,也踩过这些坑。尤其是 Codex 这一侧,最初暴露出来的问题就是某类 reconnect 报文没有被识别成可重试终态,结果原本已有的恢复机制根本没机会生效。说白了,不是系统没有自动重试,而是系统没把“这次值得重试”认出来。 |
| 30 | + |
| 31 | +所以这篇文章想讲的核心点很明确:**自动重试不是一个按钮,而是一套分层设计。** |
| 32 | + |
| 33 | +## 关于 HagiCode |
| 34 | + |
| 35 | +本文分享的方案,来自我们在 [HagiCode](https://hagicode.com) 项目里的真实实践。HagiCode 要做的事情,不是把某一个模型接上就完事,而是把多种 Agent CLI 的流式消息、工具调用、失败恢复、会话上下文,统一成一套能长期维护的执行模型。 |
| 36 | + |
| 37 | +我平时最关心的事情之一,就是怎么让 AI 编程这件事真正落到工程现场。写 Demo 不难,难的是把 Demo 变成团队真的愿意长期使用的东西。HagiCode 之所以认真做自动重试,不是因为这个功能看起来高级,而是因为长链路、流式、可续跑的 CLI 执行如果接不稳,用户看到的就不是智能助手,而是一个动不动半路掉线的命令包装器。 |
| 38 | + |
| 39 | +如果你想先看看项目入口,这里先放两个: |
| 40 | + |
| 41 | +- GitHub: [github.com/HagiCode-org/site](https://github.com/HagiCode-org/site) |
| 42 | +- 官网: [hagicode.com](https://hagicode.com) |
| 43 | + |
| 44 | +再往前走一步讲,HagiCode 现在也已经上架 Steam 了,有 Steam 的朋友可以先加个愿望单: |
| 45 | + |
| 46 | +- [Steam 商店页(加入愿望单 / 查看详情)](https://store.steampowered.com/app/4625540/Hagicode/) |
| 47 | + |
| 48 | +## 为什么 Agent CLI 的自动重试比普通重试更难 |
| 49 | + |
| 50 | +这个问题提得很实在,我们直接上结论:Agent CLI 的自动重试,难点不在“隔几秒再试一次”,而在“还能不能在原上下文里继续”。 |
| 51 | + |
| 52 | +你可以把它理解成一次长对话。普通 API 重试,更像电话占线再拨一遍;而 Agent CLI 重试,更像对方刚讲到一半信号断了,你得先判断要不要回拨,回拨以后要不要从头说,对方还记不记得刚刚聊到哪。谁说这两者是一回事呢?它们压根不是一个工程问题。 |
| 53 | + |
| 54 | +具体看,有四个难点最典型。 |
| 55 | + |
| 56 | +### 1. 它是流式的 |
| 57 | + |
| 58 | +一旦输出已经发给用户,你就不能像处理普通请求那样,把失败偷偷吞掉然后悄悄重来。因为前面那部分内容已经被看到了,再次重放时如果策略不对,前端很容易看到重复文本、错乱状态,工具调用生命周期也会一起乱套。这波不是玄学,是工程。 |
| 59 | + |
| 60 | +### 2. 它通常绑定会话上下文 |
| 61 | + |
| 62 | +Codex 这类 provider 会绑定 thread,Claude Code 一类实现也会有 continuation target 或等价的续跑上下文。真正能自动重试的前提,不只是“这个错误长得像暂时故障”,还包括“这次执行还有没有继续下去的载体”。 |
| 63 | + |
| 64 | +### 3. 它不是所有错误都值得重试 |
| 65 | + |
| 66 | +网络抖动、SSE idle timeout、上游临时故障,这些通常可以试一试。可如果你遇到的是认证失败、上下文已经丢了,或者 provider 根本没有 resume 能力,那继续重试多数不是恢复,而是在制造噪音。 |
| 67 | + |
| 68 | +### 4. 它需要边界 |
| 69 | + |
| 70 | +无限自动重试几乎总是错的。技术趋势可以热闹一阵子,工程规律往往会稳定很多年,其中一条就是:失败恢复一定要有边界。系统必须知道自己最多试几次、每次隔多久、什么时候该停手承认这回真不行了。 |
| 71 | + |
| 72 | +也正因为这几个特点,HagiCode 最后没有把自动重试写成某个 provider 里的几行 `try/catch`,而是把它提炼成一层共享能力。说到底,工程问题还是要回到工程方法里解决。 |
| 73 | + |
| 74 | +## HagiCode 的做法:把重试从 Provider 里拿出来 |
| 75 | + |
| 76 | +HagiCode 当前这套真实实现,可以压缩成一句话: |
| 77 | + |
| 78 | +**共享层统一管理重试流程,具体 Provider 只负责回答两个问题:这个终态值不值得重试?当前上下文还能不能继续?** |
| 79 | + |
| 80 | +这件事不复杂,可是很关键。因为一旦把职责切开,Claude Code、Codex,甚至其他 Agent CLI 都能复用同一个骨架。模型会说,工具会变,工作流会升级,但工程上的基本盘一直都在那里。 |
| 81 | + |
| 82 | +### 第一层:用统一协调器管理重试循环 |
| 83 | + |
| 84 | +项目中的核心实现片段大概是下面这样: |
| 85 | + |
| 86 | +```csharp |
| 87 | +internal static class ProviderErrorAutoRetryCoordinator |
| 88 | +{ |
| 89 | + public static async IAsyncEnumerable<CliMessage> ExecuteAsync( |
| 90 | + string prompt, |
| 91 | + ProviderErrorAutoRetrySettings? settings, |
| 92 | + Func<string, IAsyncEnumerable<CliMessage>> executeAttemptAsync, |
| 93 | + Func<bool> canRetryInSameContext, |
| 94 | + Func<TimeSpan, CancellationToken, Task> delayAsync, |
| 95 | + Func<CliMessage, bool> isRetryableTerminalMessage, |
| 96 | + [EnumeratorCancellation] CancellationToken cancellationToken) |
| 97 | + { |
| 98 | + var normalizedSettings = ProviderErrorAutoRetrySettings.Normalize(settings); |
| 99 | + var retrySchedule = normalizedSettings.Enabled |
| 100 | + ? normalizedSettings.GetRetrySchedule() |
| 101 | + : []; |
| 102 | + |
| 103 | + for (var attempt = 0; ; attempt++) |
| 104 | + { |
| 105 | + var attemptPrompt = attempt == 0 |
| 106 | + ? prompt |
| 107 | + : ProviderErrorAutoRetrySettings.ContinuationPrompt; |
| 108 | + |
| 109 | + CliMessage? terminalFailure = null; |
| 110 | + |
| 111 | + await foreach (var message in executeAttemptAsync(attemptPrompt) |
| 112 | + .WithCancellation(cancellationToken)) |
| 113 | + { |
| 114 | + if (isRetryableTerminalMessage(message)) |
| 115 | + { |
| 116 | + terminalFailure = message; |
| 117 | + break; |
| 118 | + } |
| 119 | + |
| 120 | + yield return message; |
| 121 | + } |
| 122 | + |
| 123 | + if (terminalFailure is null) |
| 124 | + { |
| 125 | + yield break; |
| 126 | + } |
| 127 | + |
| 128 | + if (attempt >= retrySchedule.Count || !canRetryInSameContext()) |
| 129 | + { |
| 130 | + yield return terminalFailure; |
| 131 | + yield break; |
| 132 | + } |
| 133 | + |
| 134 | + await delayAsync(retrySchedule[attempt], cancellationToken); |
| 135 | + } |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +这段代码干的事情,其实非常朴素,但很有力。 |
| 141 | + |
| 142 | +- 中间失败先不直接透传,协调器先判断能不能恢复 |
| 143 | +- 只有重试预算耗尽,最终失败才真正回到上层 |
| 144 | +- 第二轮开始不再发送原始 prompt,而是统一发送 continuation prompt |
| 145 | + |
| 146 | +这也就是为什么我前面一直强调,自动重试不是简单的“再请求一次”。它不是在补一个异常分支,而是在管理一条执行生命周其。听起来有点像产品经理,但工程上确实如此。 |
| 147 | + |
| 148 | +### 第二层:把重试策略快照化 |
| 149 | + |
| 150 | +另一个很容易被忽略的问题是:谁来决定这次请求是否开启自动重试? |
| 151 | + |
| 152 | +HagiCode 的答案是,不要依赖某个“此刻的全局配置”,而是把策略做成 snapshot,跟着这次请求一起走。这样一来,会话排队、消息持久化、执行转发、provider 适配,都不会把策略弄丢。一次成功不叫体系,持续成功才叫体系。 |
| 153 | + |
| 154 | +核心结构可以简化成这样: |
| 155 | + |
| 156 | +```csharp |
| 157 | +public sealed record ProviderErrorAutoRetrySnapshot |
| 158 | +{ |
| 159 | + public const string DefaultStrategy = "default"; |
| 160 | + |
| 161 | + public bool Enabled { get; init; } |
| 162 | + |
| 163 | + public string Strategy { get; init; } = DefaultStrategy; |
| 164 | + |
| 165 | + public static ProviderErrorAutoRetrySnapshot Normalize(bool? enabled, string? strategy) |
| 166 | + { |
| 167 | + return new ProviderErrorAutoRetrySnapshot |
| 168 | + { |
| 169 | + Enabled = enabled ?? true, |
| 170 | + Strategy = string.IsNullOrWhiteSpace(strategy) |
| 171 | + ? DefaultStrategy |
| 172 | + : strategy.Trim() |
| 173 | + }; |
| 174 | + } |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +然后在执行侧再映射成 provider 真正消费的设置对象。这个做法的价值很直接: |
| 179 | + |
| 180 | +- 业务层决定“该不该重试” |
| 181 | +- 运行时决定“怎么重试” |
| 182 | + |
| 183 | +两边各管一摊,互相不打架。很多问题不是不能做,只是没把代价算明白。把策略快照化,本质上就是在提前把代价算清楚。 |
| 184 | + |
| 185 | +### 第三层:Provider 只做终态判定和上下文判定 |
| 186 | + |
| 187 | +到了具体的 Claude Code 或 Codex provider,这里的职责反而很薄。你可以把它理解成增强,不要把它误会成代替。 |
| 188 | + |
| 189 | +以 Codex 为例,它最终接入共享协调器时,本质上只需要提供三样东西: |
| 190 | + |
| 191 | +```csharp |
| 192 | +await foreach (var message in ProviderErrorAutoRetryCoordinator.ExecuteAsync( |
| 193 | + prompt, |
| 194 | + options.ProviderErrorAutoRetry, |
| 195 | + retryPrompt => ExecuteCodexAttemptAsync(...), |
| 196 | + () => !string.IsNullOrWhiteSpace(resolvedThreadId), |
| 197 | + DelayAsync, |
| 198 | + IsRetryableTerminalFailure, |
| 199 | + cancellationToken)) |
| 200 | +{ |
| 201 | + yield return message; |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +你会发现,真正属于 Provider 自己的判断只有两个: |
| 206 | + |
| 207 | +- `IsRetryableTerminalFailure` |
| 208 | +- `canRetryInSameContext` |
| 209 | + |
| 210 | +Codex 看的是 thread 还能不能续上,Claude Code 看的是 continuation target 还在不在。退避策略、重试次数、后续 prompt,这些通通不该让 Provider 自己重新发明一遍。 |
| 211 | + |
| 212 | +这一层拆出来以后,HagiCode 接更多 CLI 的成本就低很多了。你不用复制一整套重试状态机,只要把“这个 provider 的边界条件”接进来就行。写得快,不等于写得稳;接得住,不等于接得好;能跑起来,也不等于能长期维护。 |
| 213 | + |
| 214 | +## 一个很容易做错的点:别把所有报错都当可重试 |
| 215 | + |
| 216 | +这次分析里,我觉得最值得单拎出来讲的,不是“怎么实现重试”,而是“怎么避免错误重试”。 |
| 217 | + |
| 218 | +最开始的问题切入口,是 Codex 少识别了一条 reconnect 报文。按直觉,很多人会选一个最小修法:往白名单里再加一条字符串前缀。这个思路不能说错,只是它更像 Demo 时期的解法,不太像长期维护的解法。 |
| 219 | + |
| 220 | +从当前 HagiCode 的落地来看,系统已经往更稳的方向走了一步。它不再只盯着某个字面字符串,而是把可恢复的终态统一交给共享协调器处理。这样做的好处很明显: |
| 221 | + |
| 222 | +- 不容易因为某条文案的小改动就彻底失效 |
| 223 | +- 测试覆盖可以围绕“终态 envelope”展开,而不是单条硬编码文本 |
| 224 | +- 同一个 provider 的重试逻辑会更一致 |
| 225 | + |
| 226 | +当然,这里要立一个边界:更通用,不等于更宽松。**只要当前上下文不能继续,哪怕报错看起来很像暂时故障,也不应该盲目 replay。** |
| 227 | + |
| 228 | +这点很关键。真正让人安心的,不是它偶尔灵一次,而是它大多数时候都靠谱。如果一个流程只能靠高手维持,那它离普及还差得远。 |
| 229 | + |
| 230 | +## 实践里最值得保留的三条经验 |
| 231 | + |
| 232 | +文章写到这里,差不多可以往实践层收一收了。如果你准备在自己的项目里实现类似能力,我最建议先守住下面三条。 |
| 233 | + |
| 234 | +### 1. 重试预算必须有边界 |
| 235 | + |
| 236 | +HagiCode 当前默认的退避节奏是: |
| 237 | + |
| 238 | +- 10 秒 |
| 239 | +- 20 秒 |
| 240 | +- 60 秒 |
| 241 | + |
| 242 | +这个节奏不一定适合所有系统,但“有边界”这件事必须保留。要不然,自动重试很快就会从恢复机制变成事故放大器。别急着把名字起得太大,先看看这东西能不能在团队里活过两个迭代。 |
| 243 | + |
| 244 | +### 2. continuation prompt 要统一 |
| 245 | + |
| 246 | +项目里使用的是固定 continuation prompt,让后续 attempt 明确走“继续当前上下文”的路径,而不是重新发起一轮完整请求。这个能力不花哨,可是你真做项目时离不开。很多能力看起来像魔法,拆开以后不过是一套被打磨过的工程流程。 |
| 247 | + |
| 248 | +### 3. 共享库和适配层都要有镜像测试 |
| 249 | + |
| 250 | +这点我很想多说一句。很多团队会在共享运行时里写一层测试,然后觉得差不多了。其实不够。 |
| 251 | + |
| 252 | +HagiCode 这边之所以让我比较放心,是因为两层都补了测试: |
| 253 | + |
| 254 | +- 共享 Provider 测“是否真的发生了自动续跑” |
| 255 | +- 适配层测“最终错误和流式消息有没有被整理坏” |
| 256 | + |
| 257 | +我这次也额外补跑了两组相关测试,结果都是 31 个用例全部通过。这个结果本身说明不了设计一定完美,可它至少能说明一件事:当前这套自动重试不是纸面方案,而是已经被代码和测试共同约束住的能力。Talk is cheap. Show me the code. 放到这里,恰好合适。 |
| 258 | + |
| 259 | +## 总结 |
| 260 | + |
| 261 | +如果把整篇文章压缩成一句话,那就是: |
| 262 | + |
| 263 | +**Claude Code、Codex 等 Agent CLI 的自动重试,最好不要做成某个 Provider 内部的局部技巧,而应该做成共享协调器 + 策略快照 + 上下文判定 + 镜像测试的组合。** |
| 264 | + |
| 265 | +这样做带来的收益,其实非常实在: |
| 266 | + |
| 267 | +- 逻辑只写一遍,多个 Provider 都能复用 |
| 268 | +- 请求是否允许重试,可以稳定地跟着执行链路走 |
| 269 | +- 有上下文时继续跑,没上下文时及时停手 |
| 270 | +- 前端最终看到的是稳定的完成态或失败态,而不是一堆半途而废的中间噪音 |
| 271 | + |
| 272 | +这套方案,是 HagiCode 在真实接入多种 Agent CLI 的过程中一点点打磨出来的。谁说 AI 辅助编程就不是新时代的结对编程呢?模型帮你起步、补全、发散,可真正决定体验上限的,往往还是上下文、流程和约束。 |
| 273 | + |
| 274 | +如果本文对你有帮助,也欢迎顺手看看 HagiCode 的公开入口: |
| 275 | + |
| 276 | +- GitHub: [github.com/HagiCode-org/site](https://github.com/HagiCode-org/site) |
| 277 | +- 官网: [hagicode.com](https://hagicode.com) |
| 278 | +- 30 分钟实战演示: [www.bilibili.com/video/BV1pirZBuEzq/](https://www.bilibili.com/video/BV1pirZBuEzq/) |
| 279 | +- Desktop 安装入口: [hagicode.com/desktop/](https://hagicode.com/desktop/) |
| 280 | +- Steam: [Steam 商店页(加入愿望单 / 查看详情)](https://store.steampowered.com/app/4625540/Hagicode/) |
| 281 | + |
| 282 | +HagiCode 现在已经上架 Steam 了,这不是画饼,链接也给你放这儿了。有 Steam 的朋友可以先加个愿望单,自己点进去看一眼,比我在这儿多说十句都来得直接。 |
| 283 | + |
| 284 | +先把这件事讲到这里,剩下的我们继续在真实项目里见。 |
| 285 | + |
| 286 | +## 参考资料 |
| 287 | + |
| 288 | +- HagiCode 项目主页: [https://hagicode.com](https://hagicode.com) |
| 289 | +- HagiCode GitHub 仓库: [https://github.com/HagiCode-org/site](https://github.com/HagiCode-org/site) |
| 290 | +- 官方演示视频: [https://www.bilibili.com/video/BV1pirZBuEzq/](https://www.bilibili.com/video/BV1pirZBuEzq/) |
| 291 | +- Desktop 安装说明: [https://hagicode.com/desktop/](https://hagicode.com/desktop/) |
| 292 | + |
| 293 | +## 版权说明 |
| 294 | + |
| 295 | +感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。 |
| 296 | +本内容采用人工智能辅助协作,最终内容由作者审核并确认。 |
| 297 | +- 本文作者: [newbe36524](https://www.newbe.pro) |
| 298 | +- 原文链接: [https://docs.hagicode.com/blog/2026-02-11-agent-cli-automatic-retry/](https://docs.hagicode.com/blog/2026-02-11-agent-cli-automatic-retry/) |
| 299 | +- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处! |
0 commit comments