跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
编程语言Agent RuntimeTool CallingRetryIdempotencyExponential Backoff

《Agent Runtime 工程化》给 Tool Call 加 Exponential Backoff Retry

给 Tool Call 加 Exponential Backoff Retry 很多人写 retry,第一版都是这样: 看上去挺负责。 坏也坏在这里。 在 Agent Runtime 里,retry 不是“失败了再来一次”。它要先回答三个问

陈堂会发布于 —2 浏览
《Agent Runtime 工程化》给 Tool Call 加 Exponential Backoff Retry

给 Tool Call 加 Exponential Backoff Retry

很多人写 retry,第一版都是这样:

for (let i = 0; i < 3; i++) {
  try {
    return await callTool();
  } catch (error) {
    await sleep(1000);
  }
}

看上去挺负责。

坏也坏在这里。

在 Agent Runtime 里,retry 不是'失败了再来一次'。它要先回答三个问题:

这个错误是不是瞬时错误?
这个工具能不能安全重放?
这次重试会不会绕过权限、预算或用户拒绝?

这三个问题没回答清楚,exponential backoff 只是更有节奏地制造事故。

先区分模型请求和工具请求

Provider 调用和 Tool Call 不是一类东西。

模型请求通常没有外部业务副作用。网络抖动、429、5xx,短重试是合理的。OpenAI 的 rate limit 指南和 cookbook 里也会建议遇到限流时使用指数退避。Google Cloud、AWS 也都长期建议在分布式系统里使用 exponential backoff,AWS 还专门强调 jitter,避免大量客户端在同一时间一起重试。

Tool Call 不一样。

读文件可以重试。搜索代码可以重试。查订单状态可以重试。

但创建订单、发邮件、扣款、提交 PR、删除文件、执行数据库迁移,不能因为超时就自动再跑一次。

所以不要写全局 retry,然后所有工具套进去。

RetryContext 要先出现

一个 Agent Runtime 里的 retry,应该从上下文开始:

type RetryContext = {
  operation: "model_generate" | "local_tool_call" | "mcp_tool_call";
  toolName?: string;
  errorCode?: string;
  attempt: number;
  maxAttempts: number;
  idempotency: "idempotent" | "conditional" | "non_idempotent";
  replayPolicy: "auto" | "ask" | "never";
  userApproved: boolean;
  remainingRunTokens: number;
  remainingWallClockMs: number;
};

然后再判断:

function shouldRetry(ctx: RetryContext) {
  if (ctx.attempt >= ctx.maxAttempts) return false;
  if (ctx.replayPolicy === "never") return false;
  if (ctx.idempotency === "non_idempotent") return false;
  if (!ctx.userApproved && ctx.operation !== "model_generate") return false;
  if (ctx.remainingRunTokens <= 0) return false;
  if (ctx.remainingWallClockMs <= 0) return false;

  return ["RATE_LIMIT", "TRANSIENT_NETWORK", "SERVER_ERROR", "TIMEOUT"].includes(
    ctx.errorCode ?? ""
  );
}

注意,TIMEOUT 是否可重试,要看工具。

同样是 timeout,read_file 超时和 create_order 超时不是一回事。前者大概率可以重试,后者可能已经产生副作用。

前面讲过重复下单,就是这个坑。

Backoff 要加 jitter

普通指数退避:

const delay = baseMs * 2 ** attempt;

问题是,如果很多 Agent 同时遇到限流,它们会在同一时间重试。服务端刚缓过来,又被一波整齐的重试打回去。

所以要加 jitter。

function backoffWithJitter(input: {
  baseMs: number;
  capMs: number;
  attempt: number;
}) {
  const exponential = Math.min(input.capMs, input.baseMs * 2 ** input.attempt);
  return Math.floor(Math.random() * exponential);
}

这是 full jitter 的简化版。AWS 的文章里专门讲过 backoff 和 jitter 的组合,核心思路就是不要让所有客户端同时醒来。

如果上游返回 Retry-After,优先尊重它:

function computeRetryDelay(error: {
  retryAfterMs?: number;
}, attempt: number) {
  if (error.retryAfterMs) return error.retryAfterMs;

  return backoffWithJitter({
    baseMs: 500,
    capMs: 10_000,
    attempt,
  });
}

Agent Runtime 不应该像没礼貌的爬虫一样撞接口。

Retry 要记录进 Tool Ledger

只在最后记录'成功/失败'不够。

每一次 retry 都应该进入 ledger 和 trace:

type ToolAttempt = {
  attempt: number;
  startedAt: string;
  endedAt?: string;
  errorCode?: string;
  delayBeforeNextMs?: number;
};

type ToolLedgerEntry = {
  callId: string;
  stepId: string;
  toolName: string;
  inputHash: string;
  status: "started" | "succeeded" | "failed" | "uncertain";
  idempotency: "idempotent" | "conditional" | "non_idempotent";
  replayPolicy: "auto" | "ask" | "never";
  attempts: ToolAttempt[];
};

为什么要这么细?

因为事故复盘时,你会问:

这次工具到底调用了几次?
每次间隔多久?
最后一次失败是什么?
有没有尊重 Retry-After?
有没有在用户拒绝后继续重试?
有没有在预算耗尽后继续重试?

没有 attempts,你只能猜。

给模型看的 observation 要短

retry 细节应该进 trace,不应该全塞给模型。

模型需要知道的是下一步怎么做:

{
  "ok": false,
  "code": "RATE_LIMIT",
  "content": "search_issues 连续 3 次遇到限流,Runtime 已按 Retry-After 等待后仍失败。本轮不再自动重试。请改用本地缓存结果,或请用户稍后再试。",
  "metadata": {
    "retryable": false,
    "attempts": 3
  }
}

不要把每次 HTTP 头、堆栈、长日志都塞进上下文。

模型上下文要短,trace 证据要完整。

权限拒绝不能被 retry 绕过

这个坑很隐蔽。

用户拒绝了工具调用,runtime 不应该换个 provider、换个工具名、换个参数继续尝试同样风险动作。

比如:

用户拒绝 run_command("npm install")
  ↓
Agent 换成 shell_readonly("npm install")
  ↓
Runtime 如果只看工具名,可能放过

Retry 策略必须看权限决策。

PERMISSION_DENIED 默认不可重试。

如果模型想找低风险替代方案,可以继续规划,但不能自动重放原动作。比如用户拒绝安装依赖,Agent 可以读 package.json 或解释缺少依赖;不能绕过用户意思偷偷安装。

这也是为什么重试策略要在 Runtime,而不是在工具函数里各写各的。

条件幂等怎么处理

很多工具不是简单幂等或非幂等,而是条件幂等。

写文件就是典型例子。

如果你基于相同 base hash 应用同一个 patch,重复执行可以被设计成安全的。可如果文件已经被用户手动改过,再重放 patch 就可能覆盖用户改动。

所以工具定义可以这样写:

type ToolDefinition = {
  name: string;
  idempotency: "idempotent" | "conditional" | "non_idempotent";
  replayPolicy: "auto" | "ask" | "never";
  retryPolicy: {
    maxAttempts: number;
    baseDelayMs: number;
    maxDelayMs: number;
  };
};

条件幂等必须有校验函数:

type ReplayCheck = {
  ok: boolean;
  reason: string;
};

async function canReplayWriteFile(input: {
  path: string;
  expectedBaseHash: string;
}) : Promise<ReplayCheck> {
  const currentHash = await hashFile(input.path);
  if (currentHash !== input.expectedBaseHash) {
    return { ok: false, reason: "FILE_CHANGED_AFTER_PLAN" };
  }
  return { ok: true, reason: "BASE_HASH_MATCH" };
}

没有条件校验,就别叫 conditional idempotent。

AbortSignal 要传到底

重试循环必须支持取消。

用户取消后,不能睡眠结束再重试。

async function sleep(ms: number, signal: AbortSignal) {
  await new Promise<void>((resolve, reject) => {
    const timer = setTimeout(resolve, ms);
    signal.addEventListener(
      "abort",
      () => {
        clearTimeout(timer);
        reject(new Error("aborted"));
      },
      { once: true }
    );
  });
}

真实实现还要处理 listener 清理、abort reason、子进程退出、HTTP stream 中断。

这不是洁癖。用户界面已经显示'取消',机器上却还有 retry 在睡觉,过几秒继续发请求,这是很糟糕的体验。

一个可用的 retry 包装器

综合起来,可以写成这样:

async function retryCall<T>(input: {
  run: () => Promise<T>;
  context: Omit<RetryContext, "attempt" | "errorCode">;
  classifyError(error: unknown): string;
  signal: AbortSignal;
}) {
  let lastError: unknown;

  for (let attempt = 0; attempt < input.context.maxAttempts; attempt++) {
    try {
      return await input.run();
    } catch (error) {
      lastError = error;
      const errorCode = input.classifyError(error);
      const ctx: RetryContext = {
        ...input.context,
        attempt: attempt + 1,
        errorCode,
      };

      if (!shouldRetry(ctx)) break;

      const delayMs = backoffWithJitter({
        baseMs: 500,
        capMs: 10_000,
        attempt,
      });

      await sleep(delayMs, input.signal);
    }
  }

  throw lastError;
}

这段代码还不是完整产品实现。它没写 ledger、trace、Retry-After、条件幂等校验。

但它至少把判断放在了正确的位置:先看语义,再决定是否 retry。

Eval 要测什么

Retry 不能只靠线上出事后复盘。

Eval 里至少写这些 case:

模型请求 RATE_LIMIT,重试 2 次后成功。
read_file TIMEOUT,可自动重试。
create_order TIMEOUT,标记 SIDE_EFFECT_UNCERTAIN,不自动重试。
PERMISSION_DENIED,不重试。
用户取消 retry sleep,立即停止。
Retry-After 存在时,delay 使用服务端建议。
非幂等工具 maxAttempts 即使配置为 3,也只执行 1 次。

这些 case 最好用 fake model 和 mock tool 跑。不要每次都打真实外部服务。

runtime 逻辑要可复现。

最后

Exponential backoff 是好东西。

但在 Agent Runtime 里,它必须被三条线约束:

幂等性
权限
预算

没有幂等性,retry 可能重复副作用。

没有权限约束,retry 可能绕过用户拒绝。

没有预算,retry 会把一次小任务拖成昂贵长跑。

所以,不要急着写 sleep(2 ** attempt)。

先问:这一步能不能重放?

参考资料

  • AWS Architecture Blog: Exponential Backoff And Jitter
  • Google Cloud Docs: Retry strategy
  • OpenAI Cookbook: How to handle rate limits
  • OpenAI API Docs: Rate limits
  • Stripe Docs: Idempotent requests

推荐阅读

  • 为什么 Tool 必须支持 Idempotency?
  • 如何实现 Agent Checkpoint?
  • 从 Agent Loop 重构成 Agent Runtime

目录

  1. 给 Tool Call 加 Exponential Backoff Retry
  2. 先区分模型请求和工具请求
  3. RetryContext 要先出现
  4. Backoff 要加 jitter
  5. Retry 要记录进 Tool Ledger
  6. 给模型看的 observation 要短
  7. 权限拒绝不能被 retry 绕过
  8. 条件幂等怎么处理
  9. AbortSignal 要传到底
  10. 一个可用的 retry 包装器
  11. Eval 要测什么
  12. 最后
  13. 参考资料
  14. 推荐阅读
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 在 Linux 桌面里跑 Windows 应用:Winboat 实战笔记
  • AI 编程工具深度对比:Trae、Cursor、Copilot 与 Windsurf
  • 论文阅读--Agent AI 探索多模态交互的前沿领域(一)
  • OpenCode 开源 AI 编程助手使用教程
  • Linux diff 与 patch 命令实战指南
  • 网络安全工程师职业定义、核心技能与认证体系详解
  • Python 为何如此流行?深度解析其核心优势与应用场景
  • 大疆无人机如何导出日志并解析
  • 前端 WebSocket 通信实战与最佳实践
  • OpenClaw 进阶教程:记忆系统、定时任务、多模型与子代理解析
  • Python 爬虫实战指南:从基础请求到分布式框架
  • Kubernetes Python 客户端实战教程
  • Java IO 核心:BufferedReader、BufferedWriter、PrintStream 与 PrintWriter 详解
  • AI 辅助编程工具:GitHub Copilot 安装与使用指南
  • 基于 Rokid AR 眼镜的聚会游戏助手开发实践
  • 百瑞互联 BR8654A02 蓝牙 6.0 SOC 芯片规格介绍
  • Windows 7 安装 Python 3.9+ 配置指南
  • 前端加密:常用方式与使用示例
  • GitHub Copilot 学生认证教程(2026 版)
  • openclaw-termux:在 Android 上部署 OpenClaw AI Gateway

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online