文章进阶AI 生成初稿 · deepseek-chat

工具调用出错了怎么办:校验、重试与回填

工具调用失败是 Agent 的常态而非意外。本文拆解四类失败模式,给出参数校验、重试与降级、错误回填、超时与幂等的最小实现骨架,并说明哪些错误该重试、哪些该立刻放弃。

约 5 分钟发布于 2026年9月12日更新于 上次校对 2026年9月12日 · shelter123
本文目录

TL;DR

  • 工具调用失败不是异常分支,而是主流程的一部分:模型会编参数、网络会抖、下游会超时。
  • 参数校验要在执行前做,用 JSON Schema 挡住模型幻觉出来的字段,比在下游报错后再补救便宜得多。
  • 重试只对幂等操作安全;对写操作重试前,先确认你带了幂等键(idempotency key)。
  • 把错误结构化回填给模型,让它自己决定换参数还是换工具,比在代码里硬编码所有分支更省事。
  • 超时和重试预算要一起设:没有预算的重试会把一次慢请求放大成一次雪崩。

前置知识

你需要:

  • 写过至少一次 function calling / tool use 的调用循环,知道 tools 参数和 tool_calls 返回长什么样;
  • 能读懂 JSON Schema 的基本字段(typerequiredenum);
  • 熟悉你所用语言的 try/catch 或等价错误处理。

不需要

  • 不需要读过分布式系统教材——幂等和超时这里只讲 Agent 场景下够用的部分;
  • 不需要自己实现重试框架,标准库或现成库就够;
  • 不需要先搞懂 MCP 协议细节,本文的错误处理逻辑与具体工具协议无关。

零基础?先读小白故事线《如果 Agent 是新来的实习生》。

失败从哪里来:先分类,再处理

在写任何 try/catch 之前,先把失败按来源分四类。分类的意义在于:不同来源的失败,正确的应对方式完全不同,混在一起处理只会写出一个什么都兜、什么都兜不住的巨型异常块。

第一类:模型给的参数不合法。 模型生成 tool_calls 时,参数是它“写”出来的文本,不是类型安全的对象。它可能漏掉必填字段、把数字写成字符串、给枚举字段编一个不存在的值,甚至凭空造出一个 schema 里根本没有的字段。这类失败发生在你的代码里,执行前就能发现

第二类:工具执行本身失败。 参数合法,但下游拒绝了:查订单返回 404、写数据库撞唯一键、第三方 API 返回 429。这类失败发生在你的代码之外,你只能观察和转述。

第三类:超时与网络抖动。 请求发出去了,但没在预期时间内回来。它和上一类的关键区别是:你不知道对方到底执行了没有。这个不确定性直接决定了重试是否安全。

第四类:模型对结果的处理失败。 工具成功返回了,但模型把结果理解错了,或者干脆忽略了结果继续瞎编。这类失败最难发现,因为它不抛异常。

前两类是本文重点,第三类决定重试策略,第四类靠回填格式和提示词缓解。

参数校验:在执行前挡住幻觉

最便宜的错误处理,是让错误根本没机会发生。工具执行前加一层校验,成本是几行代码,收益是把一整类下游报错变成一条清晰的反馈。

以 OpenAI 风格的 function calling 为例,工具的 parameters 本来就是一份 JSON Schema。同一份 schema,既交给模型当约束,也用来校验模型返回的参数——不要只把它当提示词用。

import { Validator } from "jsonschema";

const schema = {
  type: "object",
  properties: {
    orderId: { type: "string", pattern: "^[A-Z]{2}-\\d{6}$" },
    reason: { type: "string", enum: ["refund", "cancel", "status"] },
  },
  required: ["orderId", "reason"],
  additionalProperties: false,
};

function validateArgs(rawArgs) {
  const v = new Validator();
  const result = v.validate(JSON.parse(rawArgs), schema);
  if (!result.valid) {
    return { ok: false, errors: result.errors.map((e) => e.stack) };
  }
  return { ok: true, value: JSON.parse(rawArgs) };
}

三个细节值得注意。其一JSON.parse 本身可能抛异常——模型偶尔会返回截断的 JSON,这一步要包在 try 里。其二additionalProperties: false 很关键:模型很爱多塞字段,宽松的 schema 会让这些字段一路流到下游。其三,校验失败时不要抛异常中断循环,而是把错误收集成结构化数据,走下一节的回填路径。

校验通过之后、真正执行之前,还有一层容易被忽略的检查:业务前置条件。schema 只能保证 orderId 格式对,保证不了这个订单存在、属于当前用户、状态允许退款。这类检查放在工具函数内部,失败时同样走回填。

重试与降级:先问幂等,再问次数

重试是最容易被滥用的手段。在决定重试之前,先回答一个问题:这个操作重复执行一次,结果会变吗?

幂等操作可以放心重试。 查询、读取、GET 类请求,重试 N 次和重试 1 次结果一样。这类操作配上指数退避(exponential backoff)就够了。

非幂等操作重试前必须带幂等键。 下单、支付、发消息这类操作,盲目重试可能造成重复扣款或重复发送。正确做法是让调用方生成一个唯一键随请求带上,下游用它去重。截至 2026-09-12,主流支付与云服务 API 普遍支持这种模式,具体字段名各家不同,以官方文档为准。

async function callWithRetry(fn, { maxAttempts = 3, baseDelayMs = 300 } = {}) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return { ok: true, value: await fn() };
    } catch (err) {
      const retryable = err.status === 429 || err.status >= 500;
      if (!retryable || attempt === maxAttempts) {
        return { ok: false, error: err, attempts: attempt };
      }
      await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** (attempt - 1)));
    }
  }
}

这段骨架里有两个决策点,比代码本身更重要。

哪些错误该重试? 经验规则:429(限流)和 5xx(服务端错误)值得重试,4xx 里的 400/401/403/404 重试多少次都一样,属于“重试也白搭”,应当直接失败并回填。把不可重试的错误反复重试,只会浪费你的重试预算,还拖慢整个循环。

重试失败之后怎么办? 这就是降级(fallback)。降级的选项按优先级大致是:换一个等价工具(比如主搜索 API 挂了换备用源)、返回缓存或默认值、把失败如实回填给模型让它换个思路。降级不等于静默吞掉错误——如果最终结果不可信,必须让模型和用户都知道。

把错误回填给模型:让它自己修

这是 Agent 相比传统程序最有意思的一点:错误信息本身可以是给模型的输入。传统程序里,异常要么被处理要么被抛出;在 Agent 里,异常还有第三个去处——转成一条 tool 角色的消息,塞回对话,让模型决定下一步。

关键在于回填的格式。一句 "Error: something went wrong" 对模型毫无价值,它只能瞎猜。有效的回填至少包含三部分:哪个参数错了、期望什么、你可以怎么改

function toolErrorPayload(toolName, reason, hint) {
  return JSON.stringify({
    tool: toolName,
    status: "error",
    reason,           // 例如 "missing required field: orderId"
    hint,             // 例如 "orderId 形如 AB-123456,请向用户确认"
    retryable: false, // 明确告诉模型这次别重试
  });
}

几个实践要点。明确 retryable 字段:模型看到一个可重试的错误会倾向于重试,看到不可重试的会更倾向于换参数或换工具,这个信号能显著减少无效循环。给出可操作的 hint:与其说“参数无效”,不如说“reason 只能是 refund / cancel / status 之一”。限制重试轮数:即使回填写得再好,也要在循环层面设一个上限(比如同一工具连续失败 3 次就停止),否则模型可能陷入“改一点、再错、再改一点”的死循环。

回填还有一个常被忽略的用途:记录。把每次失败的原始参数和错误一起写进日志,是后续调 prompt 和改 schema 的第一手材料。

超时与幂等:别让一次慢请求变成雪崩

超时和重试是一对必须一起设计的参数。没有超时的重试等于没有重试——如果单次调用可能挂 60 秒,三次重试最坏情况就是 180 秒,用户的等待时间被放大三倍,而成功率未必提高。

实践上分两层设预算。单次调用超时:给每个工具一个合理的上限,查询类可以短(比如 5 秒),生成类可以长(比如 60 秒)。整个工具调用的总预算:从 Agent 收到请求开始计时,超过总预算就停止重试,把当前状态回填给模型。这样最坏情况下的延迟是可预测的。

超时还牵出幂等问题。当一次调用超时,你面对的是一个未知状态:请求可能根本没到下游,也可能到了、执行了、只是响应没回来。对幂等操作,重试即可;对非幂等操作,重试前必须靠幂等键去重,否则一次超时可能变成两次扣款。所以幂等键的生成时机应该在首次调用之前,而不是在重试时——重试要复用同一个键。

最后,把超时和幂等一起写进工具的元数据里,而不是散落在各处:

const toolMeta = {
  name: "create_order",
  timeoutMs: 10_000,
  idempotent: false,   // 决定超时后能否自动重试
  maxAttempts: 2,
};

这份元数据让重试逻辑可以统一处理所有工具,而不必为每个工具写一套 if-else。完整版的重试与幂等实现见官方文档,这里给的是足以启动理解的骨架。

截至 2026-09-12,各家模型厂商对 tool use 的错误回填格式没有强制统一标准,具体字段以你所用平台的官方文档为准。

延伸阅读

站内搜索

    搜索为本站本地索引增强;正文浏览不依赖 JavaScript。