Skip to content

原文:https://www.mstx.cn/harness/424.html · 创建:2026-09-05 · 更新:2026-09-05 · 阅读:1124


工具会失败,这是常态 ​

在理想世界中,工具总是能正确执行:网络畅通、数据库响应迅速、API返回预期的格式。但在真实世界中,工具执行面临各种失败:

  • 网络超时:外部API响应慢或不可达
  • 数据异常:数据库返回了意外的空结果或格式错误的数据
  • 权限问题:API密钥过期或权限不足
  • 限流:调用频率超过了服务提供商的限制
  • 输入问题:LLM填的参数虽然在Schema校验中通过了,但业务逻辑上不合理
  • 依赖故障:工具依赖的下游服务宕机

一个好的Harness不是避免错误(那是不可能的),而是优雅地处理错误。 错误处理策略的好坏,直接决定了Agent在面对真实世界时的鲁棒性。

本节将构建一个完整的工具错误处理与重试系统。核心目标是:让Agent在工具失败时能够恢复,而不是直接崩溃或给出错误回答。

错误的分类 ​

并非所有错误都应该用同样的方式处理。首先需要对错误进行分类:

python
from enum import Enum
from dataclasses import dataclass
from typing import Any, Dict, Optional, Callable

class ErrorType(Enum):
    """工具错误的分类"""
    # 可重试的瞬时错误
    TIMEOUT = "timeout"                    # 执行超时
    NETWORK = "network"                    # 网络错误
    RATE_LIMIT = "rate_limit"              # 限流
    SERVICE_UNAVAILABLE = "service_unavailable"  # 服务暂时不可用

    # 不可重试的永久错误
    INVALID_ARGUMENT = "invalid_argument"  # 参数不合法
    PERMISSION_DENIED = "permission_denied"  # 权限不足
    NOT_FOUND = "not_found"                # 资源不存在
    CONFLICT = "conflict"                  # 状态冲突
    INTERNAL = "internal"                  # 工具内部错误

    # 特殊错误
    UNKNOWN = "unknown"                    # 未知错误

@dataclass
class ToolError:
    """标准化的工具错误"""
    error_type: ErrorType
    message: str
    retryable: bool
    retry_after_seconds: Optional[int] = None  # 建议的重试等待时间
    details: Dict[str, Any] = None

    def __post_init__(self):
        if self.details is None:
            self.details = {}

    def to_llm_message(self) -> str:
        """转换为给LLM看的错误信息"""
        lines = [
            f"错误类型:{self.error_type.value}",
            f"错误信息:{self.message}",
        ]
        if self.retryable:
            if self.retry_after_seconds:
                lines.append(f"这是可重试的瞬时错误,建议在{self.retry_after_seconds}秒后重试。")
            else:
                lines.append("这是可重试的瞬时错误,可以尝试重试。")
        else:
            lines.append("这是不可重试的永久错误,重试不会解决此问题。建议调整参数或使用其他工具。")

        if self.details:
            lines.append(f"详细信息:{self.details}")

        return "\n".join(lines)

这个分类的核心思想是:区分瞬时错误和永久错误。

  • 瞬时错误:重试可能有效。网络超时、限流、服务暂时不可用——这些错误可能在下次尝试时消失。
  • 永久错误:重试无效。参数不合法、权限不足、资源不存在——无论重试多少次,结果都一样。

盲目重试永久错误是浪费资源和时间,而不重试瞬时错误则降低了Agent的可靠性。

错误识别:从异常到结构化错误 ​

工具函数在执行时抛出的是普通Python异常。错误处理系统的第一步是将异常转换为结构化的ToolError:

python
class ErrorClassifier:
    """将Python异常分类为结构化的ToolError"""

    def __init__(self):
        # 注册异常类型到错误类型的映射
        self.exception_mappings = {}
        self.message_patterns = []

    def register_exception(self, exception_type, error_type: ErrorType,
                           retryable: bool):
        """注册异常类型映射"""
        self.exception_mappings[exception_type] = (error_type, retryable)

    def register_pattern(self, pattern: str, error_type: ErrorType,
                         retryable: bool):
        """注册消息模式映射(通过正则表达式匹配错误消息)"""
        import re
        self.message_patterns.append(
            (re.compile(pattern, re.IGNORECASE), error_type, retryable)
        )

    def classify(self, exception: Exception) -> ToolError:
        """将异常分类为结构化的ToolError"""
        # 第一优先:精确的异常类型匹配
        for exc_type, (error_type, retryable) in self.exception_mappings.items():
            if isinstance(exception, exc_type):
                return ToolError(
                    error_type=error_type,
                    message=str(exception),
                    retryable=retryable,
                )

        # 第二优先:消息模式匹配
        error_message = str(exception)
        for pattern, error_type, retryable in self.message_patterns:
            if pattern.search(error_message):
                return ToolError(
                    error_type=error_type,
                    message=error_message,
                    retryable=retryable,
                )

        # 默认:未知错误(不重试)
        return ToolError(
            error_type=ErrorType.UNKNOWN,
            message=error_message,
            retryable=False,
        )

# 配置默认的分类器
default_classifier = ErrorClassifier()

# 注册常见的异常类型
default_classifier.register_exception(TimeoutError, ErrorType.TIMEOUT, True)
default_classifier.register_exception(ConnectionError, ErrorType.NETWORK, True)
default_classifier.register_exception(PermissionError, ErrorType.PERMISSION_DENIED, False)
default_classifier.register_exception(ValueError, ErrorType.INVALID_ARGUMENT, False)
default_classifier.register_exception(KeyError, ErrorType.NOT_FOUND, False)
default_classifier.register_exception(FileNotFoundError, ErrorType.NOT_FOUND, False)

# 注册常见的错误消息模式
default_classifier.register_pattern(r"rate.?limit", ErrorType.RATE_LIMIT, True)
default_classifier.register_pattern(r"too many requests", ErrorType.RATE_LIMIT, True)
default_classifier.register_pattern(r"service unavailable", ErrorType.SERVICE_UNAVAILABLE, True)
default_classifier.register_pattern(r"503", ErrorType.SERVICE_UNAVAILABLE, True)
default_classifier.register_pattern(r"timeout", ErrorType.TIMEOUT, True)
default_classifier.register_pattern(r"timed out", ErrorType.TIMEOUT, True)
default_classifier.register_pattern(r"unauthorized", ErrorType.PERMISSION_DENIED, False)
default_classifier.register_pattern(r"forbidden", ErrorType.PERMISSION_DENIED, False)
default_classifier.register_pattern(r"not found", ErrorType.NOT_FOUND, False)
default_classifier.register_pattern(r"does not exist", ErrorType.NOT_FOUND, False)
default_classifier.register_pattern(r"already exists", ErrorType.CONFLICT, False)
default_classifier.register_pattern(r"conflict", ErrorType.CONFLICT, False)

错误分类的价值:它让后续的重试策略和LLM反馈策略能够根据错误类型做出差异化的决策。一个TIMEOUT错误和PERMISSION_DENIED错误的处理方式完全不同。

重试策略的设计 ​

重试不是"再试一次"那么简单。一个好的重试策略需要考虑:重试什么、重试多少次、间隔多久、什么条件下停止。

策略一:固定间隔重试

最简单的策略:每次重试之间等待固定的时间。

python
class FixedIntervalRetry:
    """固定间隔重试策略"""

    def __init__(self, max_attempts: int = 3, interval_seconds: float = 1.0):
        self.max_attempts = max_attempts
        self.interval_seconds = interval_seconds

    def get_delay(self, attempt: int) -> Optional[float]:
        """返回第attempt次重试前的等待时间。返回None表示不应重试。"""
        if attempt >= self.max_attempts:
            return None
        return self.interval_seconds

适用场景:对延迟不敏感的场景,简单直接。 缺点:在服务过载时,固定间隔的重试可能加重服务负担。

策略二:指数退避重试

每次重试的等待时间按指数增长。

python
class ExponentialBackoffRetry:
    """指数退避重试策略"""

    def __init__(self, max_attempts: int = 5,
                 base_delay: float = 0.5,
                 max_delay: float = 30.0,
                 multiplier: float = 2.0):
        self.max_attempts = max_attempts
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.multiplier = multiplier

    def get_delay(self, attempt: int) -> Optional[float]:
        if attempt >= self.max_attempts:
            return None

        import random
        # 指数增长 + 随机抖动
        delay = self.base_delay * (self.multiplier ** attempt)
        # 添加抖动,避免多个客户端同时重试
        jitter = random.uniform(0, 0.1 * delay)
        delay = delay + jitter
        # 限制最大延迟
        return min(delay, self.max_delay)

适用场景:网络服务调用、限流恢复等待。指数退避是分布式系统中的最佳实践——它给服务恢复留出时间,同时随机抖动防止"惊群效应"。

策略三:不重试

某些工具根本不应该重试:

python
class NoRetry:
    """不重试策略"""

    def get_delay(self, attempt: int) -> Optional[float]:
        return None

不应该重试的场景:

  • 非幂等操作:发送邮件、执行支付、创建订单。重试可能导致重复执行。
  • 权限错误:重试不会改变权限状态。
  • 参数错误:重试不会修正错误的参数。

重试策略的选择逻辑

python
def get_retry_strategy(tool_def, error: ToolError):
    """根据工具定义和错误类型选择合适的重试策略"""

    # 永久错误不重试
    if not error.retryable:
        return NoRetry()

    # 工具自身配置了不重试
    if tool_def.max_retries <= 0:
        return NoRetry()

    # 非幂等操作不重试(即使错误是瞬时的)
    if tool_def.category in (ToolCategory.COMMUNICATE, ToolCategory.DANGEROUS):
        if tool_def.metadata.get("idempotent", False) is False:
            return NoRetry()

    # RATE_LIMIT错误使用指数退避(尊重服务端的限流)
    if error.error_type == ErrorType.RATE_LIMIT:
        return ExponentialBackoffRetry(
            max_attempts=tool_def.max_retries,
            base_delay=1.0,
            max_delay=error.retry_after_seconds or 60.0,
        )

    # TIMEOUT错误使用指数退避(服务可能暂时繁忙)
    if error.error_type == ErrorType.TIMEOUT:
        return ExponentialBackoffRetry(
            max_attempts=tool_def.max_retries,
            base_delay=0.5,
            max_delay=10.0,
        )

    # 其他可重试错误使用固定间隔
    return FixedIntervalRetry(
        max_attempts=tool_def.max_retries,
        interval_seconds=1.0,
    )

幂等性:重试安全的关键 ​

幂等性是重试策略中最关键的概念之一。一个操作是幂等的,意味着执行一次和执行多次的效果相同。

  • 幂等操作:查询订单状态(查100次结果一样)、更新用户昵称(设置同一个昵称10次效果一样)
  • 非幂等操作:发送邮件(发2次用户收到2封)、创建订单(执行2次生成2个订单)、增加余额(加100元执行2次变成加200元)

对于非幂等操作,任何自动重试都是危险的。这就是为什么send_email工具在4.1节中被配置为max_retries=1——它不应该被自动重试。

如果你的工具函数天然不幂等,但你需要某种形式的重试,有两个选择:

选择一:让工具函数幂等。

在设计工具时就考虑幂等性。比如create_order工具可以接受一个idempotency_key参数:

python
def create_order(order_id: str, product_id: str, quantity: int,
                 idempotency_key: Optional[str] = None) -> str:
    """创建订单(幂等)"""
    # 如果提供了幂等键,先检查是否已存在相同键的订单
    if idempotency_key:
        existing = find_order_by_idempotency_key(idempotency_key)
        if existing:
            return f"订单已存在:{existing.id}"
    # 创建新订单...

选择二:标记为非幂等,禁止自动重试。

在工具定义中明确标记:

python
@registry.tool(
    description="发送邮件...",
    category=ToolCategory.COMMUNICATE,
    max_retries=0,  # 不自动重试
    metadata={"idempotent": False},  # 标记为非幂等
)
def send_email(to: str, subject: str, body: str) -> str:
    ...

将错误处理集成到执行流程中 ​

更新ToolRegistry.execute方法,集成错误分类和重试:

python
class ToolRegistry:
    def __init__(self,
                 default_timeout: int = 10,
                 default_max_memory: int = 512,
                 default_max_output: int = 10000,
                 error_classifier: Optional[ErrorClassifier] = None):
        # ... 之前的初始化 ...
        self.error_classifier = error_classifier or default_classifier

    def execute(self, name: str, arguments: Dict[str, Any],
                user_confirmed: bool = False,
                user_role: str = "user") -> ToolResult:
        """执行工具,带错误处理和重试"""
        import time

        start = time.time()

        if name not in self._tools:
            return ToolResult(
                success=False, content="",
                error=f"未知工具:{name}",
                tool_name=name
            )

        tool_def = self._tools[name]

        # 安全检查(第4.3节的内容)
        permission_error = self.permission_manager.check_permission(
            tool_def, arguments, user_confirmed, user_role
        )
        if permission_error:
            return ToolResult(
                success=False, content="",
                error=str(permission_error),
                tool_name=name
            )

        validation_errors = self.validator.validate(name, arguments)
        if validation_errors:
            return ToolResult(
                success=False, content="",
                error=f"参数校验失败:{'; '.join(validation_errors)}",
                tool_name=name
            )

        # 执行(带重试)
        attempt = 0
        last_error: Optional[ToolError] = None

        while True:
            try:
                with self.limiter.time_limit(tool_def.timeout_seconds or self.default_timeout):
                    result = tool_def.func(**arguments)

                result_str = self.limiter.limit_output(str(result))
                duration = int((time.time() - start) * 1000)

                return ToolResult(
                    success=True,
                    content=result_str,
                    tool_name=name,
                    duration_ms=duration,
                    metadata={"attempts": attempt + 1, "retried": attempt > 0}
                )

            except Exception as e:
                # 分类错误
                tool_error = self.error_classifier.classify(e)
                last_error = tool_error
                attempt += 1

                # 选择重试策略
                retry_strategy = get_retry_strategy(tool_def, tool_error)
                delay = retry_strategy.get_delay(attempt - 1)  # attempt-1因为第一次尝试不算重试

                if delay is not None:
                    print(f"  [工具重试] {name} 第{attempt}次尝试失败:"
                          f"{tool_error.error_type.value}。"
                          f"将在{delay:.1f}秒后重试(第{attempt + 1}次尝试)")
                    time.sleep(delay)
                else:
                    # 不再重试
                    duration = int((time.time() - start) * 1000)
                    return ToolResult(
                        success=False,
                        content="",
                        error=tool_error.to_llm_message(),
                        tool_name=name,
                        duration_ms=duration,
                        metadata={
                            "attempts": attempt,
                            "error_type": tool_error.error_type.value,
                            "retryable": tool_error.retryable,
                        }
                    )

将错误信息反馈给LLM ​

工具失败后,错误信息会被放入对话历史,供LLM查看。错误信息的质量直接影响LLM能否有效恢复。

糟糕的错误信息:

工具执行失败:Error: Connection refused

LLM看到这个信息后不知道该做什么。是重试?换工具?还是放弃?

良好的错误信息(使用ToolError.to_llm_message()):

错误类型:network
错误信息:Connection refused
这是可重试的瞬时错误,可以尝试重试。

更完整的版本还应该包含上下文信息:

python
def build_error_feedback(call: ToolCallRequest, tool_error: ToolError,
                         attempts: int) -> str:
    """构建给LLM的完整错误反馈"""
    lines = [
        f"工具 '{call.name}' 执行失败。",
        f"调用参数:{json.dumps(call.arguments, ensure_ascii=False)}",
        f"尝试次数:{attempts}",
        "",
        tool_error.to_llm_message(),
        "",
        "你可以选择:",
        "1. 修改参数后重试",
        "2. 使用其他工具完成此任务",
        "3. 告知用户当前无法完成此操作,并说明原因",
    ]
    return "\n".join(lines)

关键设计:错误反馈中包含了调用参数和可选的后续行动。这让LLM不仅能理解"发生了什么",还能知道"接下来可以怎么做"。

一个完整的错误处理流程示例 ​

用户:"帮我查一下订单ORD12345678的状态"

Agent循环:
1. LLM决定调用check_order_status(order_id="ORD12345678")
2. Runtime执行工具
3. 工具执行:网络请求超时
4. 错误分类:TIMEOUT,可重试
5. 重试策略:指数退避,等待0.5秒后重试
6. 第2次尝试:仍然超时
7. 重试策略:等待1.0秒后重试
8. 第3次尝试:仍然超时
9. 重试策略:达到最大次数,停止重试
10. 错误反馈给LLM:
    工具 'check_order_status' 执行失败。
    调用参数:{"order_id": "ORD12345678"}
    尝试次数:3
    错误类型:timeout
    错误信息:连接超时
    这是可重试的瞬时错误...
    你可以选择:
    1. 修改参数后重试
    2. 使用其他工具完成此任务
    3. 告知用户当前无法完成此操作

11. LLM看到错误后决定:
    "抱歉,查询订单状态的服务暂时不可用。这可能是因为网络问题。
    请您稍后再试,或者提供更多信息让我尝试其他查询方式。"

注意LLM在步骤11中的行为:它理解了错误(服务不可用),向用户解释了情况(网络问题),提供了后续选项(稍后再试或换方式)。这就是良好错误反馈的价值。

错误处理的几个设计原则 ​

原则一:区分"工具错误"和"Agent错误"

  • 工具错误:工具本身执行失败(网络超时、权限不足、参数不合法)
  • Agent错误:Agent的决策有问题(选错了工具、填错了参数、推理偏差)

工具错误可以在Harness层面处理(重试、分类、反馈),Agent错误需要通过反馈让LLM自我纠正。不要试图在Harness层面修复Agent的决策错误——那是LLM的职责。

原则二:错误信息是给LLM看的,不是给开发者看的

工具执行失败时,错误信息会被放入对话历史,由LLM来理解并应对。因此,错误信息应该:

  • 使用LLM能理解的语言
  • 包含足够的上下文(参数、尝试次数、错误类型)
  • 给出可能的后续行动建议

原则三:记录原始错误,展示结构化错误

在日志中记录原始异常(包括堆栈),在对话中展示结构化错误。两者用途不同:原始异常用于开发者调试,结构化错误用于LLM理解和决策。

原则四:失败是信息,不是终点

工具失败后,Agent的循环不应该终止。失败信息应该被反馈给LLM,让它决定下一步。这是Agent优于传统程序的关键特性——传统程序遇到错误就崩溃,Agent遇到错误会想办法。

本节小结 ​

本节构建了完整的工具错误处理与重试系统:

  • 错误分类:区分瞬时错误(可重试)和永久错误(不可重试)
  • 重试策略:固定间隔、指数退避、不重试三种策略,根据错误类型和工具特性选择
  • 幂等性意识:非幂等操作不自动重试
  • 错误反馈:结构化错误信息让LLM理解发生了什么并决定下一步

核心原则:错误处理的目标不是"避免所有错误",而是"让Agent在错误发生时能够优雅地恢复或合理地放弃"。一个好的Harness应该让工具失败成为可管理的常态,而非致命的事件。

在下一节中,我们将实践构建一组完整的工具,将前面讨论的所有原则——定义、Schema、安全、错误处理——应用到实际代码中。