工具调用出错了怎么处理?

进阶高频实践约 9 分钟读完

一句话回答

工具出错时不要直接抛异常中断对话,而是把错误作为工具结果回传给模型,让它修正参数、换一种方法,或者如实告诉用户。按错误类型分别处理:参数不合法先按 Schema 校验,把具体错在哪里告诉模型;暂时性故障在代码里做有限次重试,写操作要保证幂等;超时要考虑操作可能已经执行;权限不足直接拒绝,不重试。整个循环设置最大轮数,防止模型反复调用。

详细解析

错误分类

类型 例子 处理方式
工具不存在 模型编造了一个工具名 回传错误,并列出可用的工具
参数不合法 JSON 解析失败、缺字段、类型不对、日期格式错 按 Schema 校验,回传具体哪个字段错在哪里
业务规则不满足 订单已发货不能取消、余额不足 回传原因和可行的替代做法
暂时性故障 网络抖动、下游 5xx、限流 429 代码里自动重试,仍失败再回传
超时 下游接口长时间无响应 读操作可以重试;写操作结果未知,先查询状态
权限不足 查询别人的订单 直接拒绝并说明原因,不重试,也不让模型想办法绕过

分两层处理:暂时性故障在代码层重试,对模型透明;参数错误和业务错误交给模型层,模型看到错误信息后自己修正。工具失败时还要防止模型"假装成功",系统提示里要求它如实说明,不要编造结果。

错误信息写给谁看

  • 给模型:哪里错了、怎么改、值不值得再试,比如"参数 date 格式错误:应为 YYYY-MM-DD,收到'明天'"。不要把堆栈、SQL、内部地址放进去,既浪费 token,又可能被模型复述给用户
  • 给用户:友好、可操作,比如"物流系统暂时不可用,请稍后再试"
  • 给日志:工具名、脱敏后的参数、错误类型、堆栈、重试次数、耗时、链路 ID,见 怎么给 LLM 调用链做链路追踪

有的接口支持给工具结果加错误标记,比如 Anthropic 的 is_error、MCP 的 isError,能更明确地告诉模型这次调用失败了。

重试、幂等和轮数上限

  • 只对暂时性错误重试,次数有限,用指数退避加随机抖动,遵守 Retry-After。调用大模型接口本身的限流和重试见 大模型 API 的限流、重试和降级
  • 读操作重试是安全的;写操作要先做到幂等:请求带上幂等键,服务端用它去重,重复的请求返回第一次的结果
  • 幂等键可以由工具调用 id 生成,它能防住代码层重试造成的重复。但模型在下一轮重新发起同样的操作时,调用 id 是新的,这种重复要靠业务规则拦住,比如同一用户对同一订单只能有一笔进行中的退款
  • 整个循环设置最大轮数;同一工具、同一参数连续失败几次后不再执行,直接告诉模型"已多次失败,请换一种方法或向用户说明"。更完整的防护见 怎么防止 Agent 失控

代码示例

TypeScript
import { z } from 'zod'

class RetryableError extends Error {} // 网络抖动、5xx、限流等暂时性错误
class BizError extends Error {} // 业务错误,信息可以给模型看

interface Tool {
  schema: z.ZodType
  idempotent: boolean // 读操作,或带幂等键的写操作
  execute(args: unknown): Promise<unknown>
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

async function runTool(call: { name: string; arguments: string }, registry: Map<string, Tool>) {
  const tool = registry.get(call.name)
  if (!tool) return { isError: true, content: `不存在工具 ${call.name},可用的有:${[...registry.keys()].join('、')}` }

  // 1. 解析并校验参数,错误具体到字段
  let raw: unknown
  try {
    raw = JSON.parse(call.arguments || '{}')
  } catch {
    return { isError: true, content: '参数不是合法的 JSON,请重新生成' }
  }
  const parsed = tool.schema.safeParse(raw)
  if (!parsed.success) {
    const detail = parsed.error.issues.map((i) => `${i.path.join('.') || '参数'}:${i.message}`).join(';')
    return { isError: true, content: `参数错误:${detail}。请修正后重试` }
  }

  // 2. 执行:只有幂等操作遇到暂时性错误才自动重试
  for (let attempt = 1; ; attempt++) {
    try {
      const result = await tool.execute(parsed.data)
      return { isError: false, content: JSON.stringify(result) ?? '执行成功' }
    } catch (err) {
      console.error({ tool: call.name, attempt, err }) // 完整错误只写日志
      const transient = err instanceof RetryableError
      if (transient && tool.idempotent && attempt < 3) {
        await sleep(2 ** attempt * 200 + Math.random() * 200) // 指数退避加随机抖动
        continue
      }
      if (transient && !tool.idempotent) {
        return { isError: true, content: '操作结果未知,可能已经执行。请先查询状态,不要直接重试' }
      }
      if (err instanceof BizError) return { isError: true, content: `执行失败:${err.message}` }
      return { isError: true, content: transient ? '服务暂时不可用,请稍后再试' : '内部错误,无法完成该操作' }
    }
  }
}

调用方拿到 { isError, content } 后,按所用 SDK 的格式组装成工具结果消息,接口支持错误标记时一并带上。

面试官可能追问

工具调用超时了,可以直接重试吗?

要看操作类型。查询可以重试。写操作超时时,请求可能已经在下游执行成功,只是响应没回来,直接重试可能重复扣款、重复下单。正确做法是用幂等键让重试变得安全,或者先调用查询接口确认状态;回传给模型时说明"结果未知",而不是"失败"。

模型一直用同样的错误参数调用同一个工具,怎么办?

多半是错误信息不够具体,或者工具描述有歧义。先改进错误信息,明确指出正确的格式和示例值。代码层面检测"同一工具加同一参数"的重复失败,超过次数就停止执行,让模型换方法或向用户确认。参数格式问题频繁出现时,可以考虑平台的严格模式来约束参数。

工具失败了,模型却回答"已经帮您办好了",怎么防?

这是工具失败场景下的幻觉。措施:错误结果要明确标记为失败;系统提示里要求"工具失败时如实说明,不要编造结果";关键操作的状态以工具的真实返回为准,比如前端直接渲染工具返回的订单状态,而不是只展示模型写的文字。

参数校验失败,应该返回协议错误还是工具结果?

返回工具结果,让模型能看到并修正。MCP 规范也是这样划分的:未知工具、请求格式不对属于协议错误,以 JSON-RPC 错误返回;参数校验失败、接口调用失败、业务错误属于工具执行错误,放在结果里并标记 isError: true,便于模型自我纠正。

易错点

  • 工具报错时直接 throw,整个对话中断,模型没有机会修正
  • 不区分错误类型一律重试,参数错误和权限错误重试多少次都没用
  • 写操作没有幂等保护就自动重试,造成重复执行
  • 把堆栈和内部错误原样回传给模型,既浪费 token 又有泄露风险

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。