Agent 调用工具失败了,是直接报错,还是有重试?重试策略怎么设计的?

文章目录

一、工具调用的核心链路

python 复制代码
# 工具调用的核心数据结构
# LLM 输出的 tool_call 格式(以 OpenAI function calling 为例)
tool_call = {
    "id": "call_abc123",
    "type": "function",
    "function": {
        "name": "search_insurance_contract",       # 工具名称
        "arguments": '{"query": "等待期", "doc_id": "contract_2024_001"}'  # JSON 参数
    }
}

# 框架层执行工具并返回结果
tool_result = {
    "tool_call_id": "call_abc123",
    "role": "tool",
    "content": "等待期为30天,自保单生效日起计算。"
}

这个链路里有三个地方会出问题:

LLM 生成的参数不合法------参数格式错误、必填字段缺失、参数类型不对。比如工具要求 doc_id 是整数,LLM 却传了字符串 "001"。

工具执行本身失败------数据库连接超时、API 限流、外部服务返回 500。这类错误跟 LLM 无关,是基础设施的问题。

工具返回了结果但结果无用------工具执行成功,但返回了空结果或错误数据(比如数据库查询没有命中、API 返回了 {"status": "no_data"})。

这三类失败的处理策略完全不同,混在一起用同一套重试逻辑,是大多数 Agent 工程实现的第一个坑。

二、LLM生成的参数不合法

特征:工具返回 ValueError、TypeError、参数校验失败,或者 JSON 解析报错。根本原因是 LLM 对工具 schema 理解有偏差,生成了格式不对或语义错误的参数。

处理方式:

  • 带错误反馈的重试(Error-Informed Retry)------把错误信息连同原始 tool_call 一起送回给 LLM,让它自己修正参数,而不是无脑重试原参数。关键是错误信息要够具体,告诉 LLM 哪里错了、期望是什么。
python 复制代码
import json
import time
from typing import Any, Optional

class ToolExecutor:
    """
    带错误分类和重试策略的工具执行器
    区分参数错误、临时服务错误、空结果、永久性错误四类
    """

    # 需要指数退避重试的状态码(临时服务错误)
    TRANSIENT_HTTP_CODES = {429, 500, 502, 503, 504}
    # 永久性错误,直接终止
    PERMANENT_HTTP_CODES = {400, 401, 403, 404}

    def execute_with_retry(
        self,
        tool_call: dict,
        tools: dict,
        llm,
        conversation_history: list,
        max_retries: int = 3
    ) -> dict:
        tool_name = tool_call["function"]["name"]
        tool_fn = tools.get(tool_name)

        if not tool_fn:
            return {"error": f"工具 {tool_name} 不存在", "recoverable": False}

        for attempt in range(max_retries):
            try:
                # 解析 LLM 生成的参数
                args = json.loads(tool_call["function"]["arguments"])
            except json.JSONDecodeError as e:
                # JSON 解析失败 → 参数错误,让 LLM 修正
                if attempt < max_retries - 1:
                    tool_call = self._request_param_fix(
                        tool_call, f"参数JSON格式错误:{e}", llm, conversation_history
                    )
                    continue
                return {"error": "参数格式持续错误", "recoverable": False}

            try:
                result = tool_fn(**args)

                # 检查空结果
                if self._is_empty_result(result):
                    if attempt == 0:  # 只给一次参数泛化机会
                        tool_call = self._request_param_generalize(
                            tool_call, result, llm, conversation_history
                        )
                        continue
                    return {"result": result, "warning": "查询无结果"}

                return {"result": result, "success": True}

            except ValueError as e:
                # 参数值不合法 → 让 LLM 修正
                if attempt < max_retries - 1:
                    tool_call = self._request_param_fix(
                        tool_call, f"参数值错误:{e}", llm, conversation_history
                    )
            except TimeoutError:
                # 超时 → 指数退避重试(参数不变)
                wait = 2 ** attempt
                time.sleep(wait)
            except PermissionError as e:
                # 永久性错误 → 立即终止
                return {"error": str(e), "recoverable": False}

        return {"error": f"工具 {tool_name} 在 {max_retries} 次重试后仍失败", "recoverable": False}

    def _request_param_fix(
        self,
        original_call: dict,
        error_msg: str,
        llm,
        history: list
    ) -> dict:
        """
        把错误信息反馈给 LLM,让它修正工具参数
        关键:错误信息要具体,告诉 LLM 哪里错了、期望什么
        """
        fix_messages = history + [
            {
                "role": "tool",
                "tool_call_id": original_call["id"],
                "content": f"工具调用失败:{error_msg}\n请检查参数格式并重新调用。"
            }
        ]
        # 让 LLM 基于错误反馈重新生成 tool_call
        response = llm.generate(fix_messages, tools=..., tool_choice="required")
        return response.tool_calls[0]

    def _is_empty_result(self, result: Any) -> bool:
        if result is None:
            return True
        if isinstance(result, (list, dict, str)) and len(result) == 0:
            return True
        if isinstance(result, dict) and result.get("status") in ("no_data", "not_found"):
            return True
        return False

核心思路是:把错误信息结构化地放回对话历史,让 LLM 理解自己哪里出错了,然后重新生成 tool_call。

  • 错误处理是补救,更好的方式是在源头减少 LLM 生成错误参数的概率------这取决于工具 Schema 的质量。
python 复制代码
# 差的 Schema(过于简洁,LLM 容易猜错)
{
    "name": "query_contract",
    "description": "查询合同信息",
    "parameters": {
        "type": "object",
        "properties": {
            "id": {"type": "string"},
            "type": {"type": "string"}
        }
    }
}

LLM 不知道 id 是什么格式、type 有哪些合法值,于是各种乱传------id 传了合同名称的文本、type 传了"意外险"(实际上应该是枚举值)。

python 复制代码
# 好的 Schema(类型约束 + 枚举值 + 示例 + 说明)
{
    "name": "query_contract",
    "description": "按合同编号查询保险合同的详细条款信息。仅支持通过合同编号查询,不支持模糊搜索。",
    "parameters": {
        "type": "object",
        "properties": {
            "contract_id": {
                "type": "string",
                "description": "合同编号,格式为 INS-YYYY-NNNN,例如 INS-2024-0023",
                "pattern": "^INS-\\d{4}-\\d{4}$"  # 正则约束
            },
            "insurance_type": {
                "type": "string",
                "description": "险种类型,只能从以下值中选择",
                "enum": ["accident", "health", "life", "property"],  # 枚举约束
                "enumDescriptions": {
                    "accident": "意外伤害险",
                    "health": "健康险(含重疾险)",
                    "life": "人寿险",
                    "property": "财产险"
                }
            }
        },
        "required": ["contract_id"]  # 明确必填字段
    }
}

改了 Schema 之后,参数错误率从 23% 降到了 8%。几乎不需要改任何 LLM 调用逻辑,只是把工具描述写清楚了。Schema 质量是工具调用可靠性的第一道防线。

三、临时服务错误(基础设施问题)

特征:网络超时、HTTP 429(限流)、HTTP 503(服务不可用)、数据库连接中断。这类错误跟 LLM 生成的参数无关,是底层服务的短暂不可用。

处理方式:指数退避重试(Exponential Backoff)------第 1 次失败等 1 秒重试,第 2 次等 2 秒,第 3 次等 4 秒。重试时用完全一样的参数,因为问题出在服务端,不在参数。

四、空结果或无用结果(工具逻辑问题)

特征:工具执行成功(HTTP 200),但结果是空列表、null、no_data。这意味着查询条件没有命中数据,而不是服务故障。

处理方式:参数泛化重试------把 LLM 召回来,告诉它"查询没有命中结果,请尝试用更宽泛的条件重新查询",让 LLM 生成一个调整过参数的新 tool_call。

五、永久性错误(不可恢复)

特征:HTTP 400(请求本身有根本性错误)、HTTP 401/403(权限问题)、工具不存在等。这类错误重试没有任何意义。

处理方式:立即终止,向上报告------停止当前工具调用链,生成一个对用户友好的错误说明,让 LLM 基于当前已有信息给出力所能及的回答。

五、工具调用的可观测性:出了问题能看到在哪里

Agent 系统在生产环境里最大的挑战之一是调试困难------一个任务跑了十几步工具调用,中间某一步失败了,如果没有完整的调用链日志,根本找不到在哪里出的问题。

关键观测指标:

工具级别: 每个工具的调用次数、成功率、平均延迟、错误分布(参数错误 vs 服务错误 vs 空结果)。如果某个工具的错误率突然升高,可能是 Schema 不够清晰(LLM 理解偏了)或者后端服务出了问题。

任务级别: 每个 Agent 任务的总步数、工具调用总次数、重试次数、最终成功/失败。重试次数多的任务往往说明工具 Schema 有问题,或者 LLM 在这类任务上的工具选择不稳定。

LLM 决策质量: 工具选择的合理性(LLM 有没有调用不该调用的工具)、参数错误率(LLM 生成错误参数的频率)。这两个指标可以帮你判断是 Schema 问题还是 LLM 能力问题。

python 复制代码
import time
from functools import wraps
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class ToolCallRecord:
    """工具调用记录,用于可观测性"""
    tool_name: str
    arguments: dict
    start_time: float
    end_time: Optional[float] = None
    success: bool = False
    error_type: Optional[str] = None   # param_error / transient / empty / permanent
    retry_count: int = 0
    result_summary: Optional[str] = None  # 结果摘要,不存原始结果(可能很大)

    @property
    def latency_ms(self):
        if self.end_time:
            return (self.end_time - self.start_time) * 1000
        return None


def track_tool_call(tool_name: str):
    """
    工具调用追踪装饰器
    自动记录调用时间、成功/失败、错误类型
    """
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            record = ToolCallRecord(
                tool_name=tool_name,
                arguments=kwargs,
                start_time=time.time()
            )
            try:
                result = fn(*args, **kwargs)
                record.success = True
                record.result_summary = str(result)[:100]  # 只记录摘要
                return result
            except Exception as e:
                record.error_type = classify_error(e)
                raise
            finally:
                record.end_time = time.time()
                # 推送到监控系统(如 Prometheus / Datadog / 内部日志)
                metrics.record(record)
        return wrapper
    return decorator


# 使用方式
@track_tool_call("query_contract")
def query_contract(contract_id: str, insurance_type: str = None):
    # 工具实现
    ...

总结:

在 Agent 开发中,工具调用失败不能笼统处理,按四类场景拆分,针对性治理:

  • 第一类,参数错误。属于大模型生成问题,比如参数缺失、格式错误、枚举越界、入参不合理。
  • 第二类,临时服务异常。属于基建问题,比如接口超时、限流、网络波动、服务短暂不可用。
  • 第三类,空结果返回。查询类工具正常执行,但无数据命中,不是报错,是业务无结果。
  • 第四类,永久性错误。比如权限不足、资源不存在、非法操作,属于不可恢复问题。

针对四类问题,采用差异化重试策略,严格区分修正重试和无脑重试:

  • 参数错误:采用错误反馈式重试,把接口报错、字段校验失败信息原样回传给大模型,让模型自主修正入参后二次调用;
  • 临时服务错误:使用指数退避重试,不修改任何参数,只错开瞬时波动;
  • 空结果场景:触发参数泛化重试,放宽检索条件、精简关键词,扩大查询范围;
  • 永久错误:直接终止流程、上报异常,禁止无效重试,避免死循环和资源浪费。

同时,从源头通过 Schema 优化降错。通过完善工具入参描述、增加枚举约束、正则校验、字段必填说明、补充调用示例,强约束模型输出。

最后,配套完整可观测性体系做长效治理:

搭建工具维度的调用成功率、失败类型大盘监控,结合任务级调用链路追踪,快速定位高频失败场景、模型倾向性问题,持续沉淀 bad case、反哺优化提示词与工具描述,形成闭环迭代,长期稳定工具调用成功率。

相关推荐
冰西瓜6004 小时前
深度学习的数学原理(三十)—— Transformer的子层连接:残差+层归一化
人工智能·深度学习·transformer
HERR_QQ6 小时前
dirving transformer详读
人工智能·深度学习·transformer
自动驾驶小学生6 小时前
Transformer和LLM前沿内容(4):Long-Context LLM
人工智能·深度学习·transformer
冰西瓜6006 小时前
深度学习的数学原理(三十一)—— Transformer前馈网络FFN(为什么要先升维再降维)
人工智能·深度学习·transformer
一只独角兽6 小时前
DeepSeek-V4-Pro 部署实战指南:H100/H200/B200/B300/GB200/GB300 全硬件配置详解
自然语言处理·gru·transformer·vllm
AI-Frontiers8 小时前
transformer进阶之路:#1 整体概述
transformer
Gh0st_Lx1 天前
【6】持续学习方法概述:在数据集 B 上变强了,在数据集 A 上却暴跌?
人工智能·语言模型·transformer
机器学习之心1 天前
GA-Transformer模型回归+SHAP分析+新数据预测+多输出!深度学习可解释分析(附MATLAB代码)
深度学习·回归·transformer
nancy_princess1 天前
Transformer
人工智能·深度学习·transformer