自动重试实现:Claude Code与Codex完全指南

2026-06-12阅读 0热度 0
人工智能

自动重试这个概念,听起来像拨动某个开关就能生效,但在工程现场,情况远非如此。先给出几个核心判断:Claude Code、Codex 这类 Agent CLI 的自动重试,核心难点不在于“每隔几秒再试一次”,而在于“原上下文是否还能延续”。这并非一个简单开关,而是一套分层设计体系。

如何实现 Claude Code 和 Codex 等 Agent CLI 的自动重试

背景

如果你最近在深入使用 AI 编程工具,大概率已经遇到过这种情况:任务并非一开始就失败,而是在执行中途突然中断。

放到普通 HTTP 请求里,这种问题通常只需重新发送一次,最多配合指数退避策略。但 Agent CLI 完全不同。Claude Code、Codex 这类工具采用流式执行,输出逐段推送,过程中会绑定 thread、session 或 resume token。换句话说,它不是单纯的“这一请求是否失败”,而是:

  • 之前已经输出的内容是否仍然有效
  • 当前上下文是否还能继续执行
  • 这次失败是否应该自动恢复
  • 如果要恢复,多久重试、重试时发送什么、原上下文是否复用

很多团队第一次接触这个场景时,都会本能地写一个最简陋的版本:遇到报错就重试一次。这个思路很直观,但真正落地后问题接连涌现。

  • 某些临时故障被误判为最终失败
  • 某些根本不该重试的错误却被反复重放
  • 带 thread 的请求和不带 thread 的请求被统一对待
  • 退避策略没有上限,后台请求自爆

在接入多个 Agent CLI 的过程中,我们也踩过这些坑。尤其是 Codex 一侧,最初暴露的问题是某类 reconnect 报文未被识别为可重试终态,导致已有的恢复机制无法触发。说白了,系统并非没有自动重试能力,而是没有正确判断“这次值得重试”。

所以本文要讲的核心观点很明确:自动重试不是一个开关,而是一套分层设计。

为什么 Agent CLI 的自动重试比普通重试更难

这个问题非常实际,直接给出结论:Agent CLI 的自动重试,难点不在于“隔几秒再试一次”,而在于“原上下文还能不能继续”。

可以把这理解成一次长对话。普通 API 重试,类似于电话占线重拨;而 Agent CLI 重试,更像对方说到一半信号断了——你需要判断是否回拨、回拨后是否从头开始、对方是否还记得刚才聊到哪里。这完全是两种不同的工程问题。

具体来看,有四个最具挑战性的难点。

1. 流式输出特性

一旦输出已发送给用户,就不能像处理普通请求那样默默吞掉失败再重来。因为前面那部分内容已被看到,重放时策略不对,前端很容易看到重复文本、状态错乱,工具调用生命周期也会被打乱。这不是玄学,而是工程现实。

2. 绑定会话上下文

Codex 这类 provider 会绑定 thread,Claude Code 也有 continuation target 或等价的续跑上下文。能真正自动重试的前提,不仅取决于“错误是否像临时故障”,还包括“这次执行是否还有可延续的载体”。

3. 并非所有错误都值得重试

网络抖动、SSE idle timeout、上游临时故障通常可以重试。但遇到认证失败、上下文已丢失、或 provider 根本不具备 resume 能力时,持续重试非但不能恢复,反而制造大量噪音。

4. 必须设定边界

无限自动重试几乎总是错误的选择。技术趋势可能变化,但工程规律长期稳定:失败恢复一定要有边界。系统必须清楚最多尝试几次、每次间隔多久、何时该彻底放弃。

正是这些特点,最终我们并未将自动重试写成某个 provider 内部的几行 try/catch,而是将其提炼为共享层能力。说到底,工程问题还需回到工程方法里解决。

做法:把重试从 Provider 里拿出来

当前这套真实实现可以浓缩为一句话:

共享层统一管理重试流程,具体 Provider 只负责回答两个问题:这个终态值是否值得重试?当前上下文是否还能继续?

这个思路很简单,但非常关键。因为职责一旦切分开,Claude Code、Codex 乃至其他 Agent CLI 都能复用同一个骨架。模型会更新,工具会变化,工作流会升级,但工程上的基础盘始终在那里。

第一层:用统一协调器管理重试循环

核心实现代码片段如下:

internal static class ProviderErrorAutoRetryCoordinator
{
    public static async IAsyncEnumerable ExecuteAsync(
        string prompt,
        ProviderErrorAutoRetrySettings? settings,
        Func> executeAttemptAsync,
        Func canRetryInSameContext,
        Func delayAsync,
        Func isRetryableTerminalMessage,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        var normalizedSettings = ProviderErrorAutoRetrySettings.Normalize(settings);
        var retrySchedule = normalizedSettings.Enabled
            ? normalizedSettings.GetRetrySchedule()
            : [];

        for (var attempt = 0; ; attempt++)
        {
            var attemptPrompt = attempt == 0
                ? prompt
                : ProviderErrorAutoRetrySettings.ContinuationPrompt;

            CliMessage? terminalFailure = null;

            await foreach (var message in executeAttemptAsync(attemptPrompt)
                               .WithCancellation(cancellationToken))
            {
                if (isRetryableTerminalMessage(message))
                {
                    terminalFailure = message;
                    break;
                }

                yield return message;
            }

            if (terminalFailure is null)
            {
                yield break;
            }

            if (attempt >= retrySchedule.Count || !canRetryInSameContext())
            {
                yield return terminalFailure;
                yield break;
            }

            await delayAsync(retrySchedule[attempt], cancellationToken);
        }
    }
}

这段代码做的事情非常直接,但效果显著。

  • 中间失败不直接透传,协调器先判断能否恢复
  • 只有重试预算用尽,最终失败才真正返回上层
  • 从第二次尝试开始,不再发送原始 prompt,而是统一发送 continuation prompt

这也就是为什么我一直强调,自动重试并非简单的“再请求一次”。它不是补一个异常分支,而是在管理一条执行生命周期。听起来有点像产品经理的思维,但工程上确实如此。

第二层:把重试策略快照化

另一个极易被忽略的问题是:谁来决定这次请求是否开启自动重试?

答案是,不要依赖某个“当前的全局配置”,而是把策略做成 snapshot,跟着这次请求一起走。这样,会话排队、消息持久化、执行转发、provider 适配,都不会丢失策略。一次成功不叫体系,持续成功才叫体系。

核心结构可以简化为:

public sealed record ProviderErrorAutoRetrySnapshot
{
    public const string DefaultStrategy = "default";

    public bool Enabled { get; init; }

    public string Strategy { get; init; } = DefaultStrategy;

    public static ProviderErrorAutoRetrySnapshot Normalize(bool? enabled, string? strategy)
    {
        return new ProviderErrorAutoRetrySnapshot
        {
            Enabled = enabled ?? true,
            Strategy = string.IsNullOrWhiteSpace(strategy)
                ? DefaultStrategy
                : strategy.Trim()
        };
    }
}

然后在执行层映射成 provider 实际消费的设置对象。这种做法价值直接:

  • 业务层决定“是否应该重试”
  • 运行时决定“如何重试”

两边各管各的,互不干扰。很多问题不是不能做,而是没把代价算清楚。策略快照化,本质上就是在提前把代价算清楚。

第三层:Provider 只做终态判定和上下文判定

到了具体的 Claude Code 或 Codex provider,其职责反而很薄。可以将其理解为增强,而非替代。

以 Codex 为例,最终接入共享协调器时,本质上只需提供三样东西:

await foreach (var message in ProviderErrorAutoRetryCoordinator.ExecuteAsync(
                   prompt,
                   options.ProviderErrorAutoRetry,
                   retryPrompt => ExecuteCodexAttemptAsync(...),
                   () => !string.IsNullOrWhiteSpace(resolvedThreadId),
                   DelayAsync,
                   IsRetryableTerminalFailure,
                   cancellationToken))
{
    yield return message;
}

你会发现,真正属于 Provider 自身的判断只有两个:

  • IsRetryableTerminalFailure
  • canRetryInSameContext

Codex 看的是 thread 是否还能延续,Claude Code 看的是 continuation target 是否存在。退避策略、重试次数、后续 prompt,这些都不该让 Provider 重新实现一遍。

这一层拆出来后,接入更多 CLI 的成本大幅降低。无需复制一整套重试状态机,只需将“该 provider 的边界条件”接入即可。写得快不代表写得稳,接得住不代表接得好,能跑起来也不代表能长期维护。

一个很容易做错的点:别把所有报错都当可重试

本次分析中最值得单独拎出来的,不是“如何实现重试”,而是“如何避免错误重试”。

最初的问题切入点,是 Codex 少识别了一条 reconnect 报文。按直觉,很多人会选择最小修复方案:在白名单中再加一条字符串前缀。这个思路不能说错,但它更像是 Demo 阶段的解法,而非长期维护的解法。

从当前落地来看,系统已经朝更稳健的方向迈进。它不再只盯着某个字面字符串,而是将可恢复的终态统一交由共享协调器处理。这样做的好处很明显:

  • 不容易因为某条文案的小改动而彻底失效
  • 测试覆盖可以围绕“终态 envelope”展开,而非单条硬编码文本
  • 同一个 provider 的重试逻辑更一致

当然,这里需要立一个边界:更通用,不等于更宽松。只要当前上下文无法继续,即便报错看起来很像临时故障,也不应盲目 replay。

这点至关重要。真正让人放心的,不是它偶尔灵一次,而是它大多数时候都靠谱。如果一个流程只能靠高手维持,那它离普及还差得远。

实践里最值得保留的三条经验

文章写到这里,可以往实践层面收拢了。如果你准备在自己的项目中实现类似能力,最建议先守住以下三条。

1. 重试预算必须有边界

当前默认的退避节奏是:

  • 10 秒
  • 20 秒
  • 60 秒

这个节奏不一定适合所有系统,但“有边界”这一原则必须保留。否则自动重试很快就会从恢复机制变成事故放大器。

2. continuation prompt 要统一

项目中使用的是固定 continuation prompt,让后续 attempt 明确走“继续当前上下文”的路径,而非重新发起一轮完整请求。这个能力看似不花哨,但在实际项目中不可或缺。很多能力看起来像魔法,拆开后不过是一套被打磨过的工程流程。

3. 共享库和适配层都要有镜像测试

这一点值得多说几句。很多团队会在共享运行时里写一层测试,然后就觉得差不多了。实际上不够。

之所以让人比较放心,是因为两层都补了测试:

  • 共享 Provider 测“是否真的发生了自动续跑”
  • 适配层测“最终错误和流式消息是否被整理坏”

额外补跑了两组相关测试,结果都是 31 个用例全部通过。这个结果本身不说明设计一定完美,但它至少说明一件事:当前这套自动重试并非纸面方案,而是已经被代码和测试共同约束住的能力。Talk is cheap. Show me the code. 放在这里,恰如其分。

总结

如果把整篇文章压缩成一句话,那就是:

Claude Code、Codex 等 Agent CLI 的自动重试,最好不要做成某个 Provider 内部的局部技巧,而应该做成共享协调器 + 策略快照 + 上下文判定 + 镜像测试的组合。

这样做带来的收益非常实在:

  • 逻辑只写一遍,多个 Provider 都能复用
  • 请求是否允许重试,可以稳定地跟着执行链路走
  • 有上下文时继续跑,没上下文时及时停手
  • 前端最终看到的是稳定的完成态或失败态,而不是一堆半途而废的中间噪音

这套方案,是在真实接入多种 Agent CLI 的过程中一点点打磨出来的。谁说 AI 辅助编程就不是新时代的结对编程呢?模型帮你起步、补全、发散,可真正决定体验上限的,往往还是上下文、流程和约束。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策