3.4. 错误处理设计直接决定了 Skills 的生产可用性

错误处理设计直接决定了 Skills 的生产可用性

2025 年底,一家中型 SaaS 公司的技术负责人复盘了他们的首个 AI Agent 项目——一个自动化财务报告生成的 Skill。上线后第一周看似无事,直到月底结账时,财务团队发现报告里缺少了三个部门的开支数据。排查后的真相令人哭笑不得:Agent 在调用其中一个数据源 API 时,因为一次短暂的 503 错误直接跳过了该步骤,没有重试,没有告警,甚至日志里也只留下一条含糊的“request failed”记录。它就这样静默地失败了,就像什么都没发生过。

这就是生产环境 Agent 与 Demo 的分水岭。在原型阶段,一条try-catch加上 print(e) 或许还能过关;但在真实业务里,你面对的是一系列连锁故障场景:第三方 API 的瞬时抖动、模型返回的 JSON 含非法字符、工具有时候被调用两次、有时候一次都不调用。错误处理不是代码的后缀,它是 Skills 设计阶段就必须嵌入的骨骼。

从当前调研数据和 OpenAI、Claude 的官方最佳实践看,凡是在生产环境中稳定运行超过三个月的 Agent 项目,都在错误处理上投入了至少 20% 的开发资源。本节的结论前置也十分明确:一个没有分层错误处理策略的 Skill,在生产环境下平均存活时间不足两周。


一、分类与处理三类错误:系统、模型、逻辑

先来解构“错误”本身。Agent 执行过程中遇到的异常,性质上分属三个截然不同的类别。将它们混为一谈,是大多数静默失败的根源。

三类错误的特征与处理原则

错误类型 典型场景 可恢复性 处理策略 错误示例
系统错误 API 超时、网络中断、磁盘满、权限不足 高(瞬时故障) 重试 + 断路器 HTTP 503、Socket Timeout
模型错误 幻觉、输出格式非法、误解意图 中(需校验) 校验 + 降级 + 重新提示 JSON 解析失败、不存在的函数名
逻辑错误 代码缺陷、状态机错误、计算偏差 低(需人工介入) 告警 + 终止 + 人工兜底 金额计算错误、错误的函数调用顺序

作者的结论:系统错误要靠工程手段解决;模型错误要靠防御性校验解决;逻辑错误要靠测试和人工兜底解决。三者一旦混淆,就会出现“对幻觉做重试”这种无效操作,或“对网络抖动直接告警”这种过激反应。

系统错误:瞬时故障的典型特征

在生产环境中,第三方 API 返回 5xx 状态码的概率不是“会不会发生”的问题,而是“多久发生一次”的问题。常见的触发场景包括:

  • 限流:API 返回 429 Too Many Requests
  • 超时:网络延迟导致请求超过预设阈值
  • DNS 解析失败:上游服务域名暂时不可达
  • 连接重置:中间代理断开 TCP 连接

系统错误的核心特征是临时性与不可控性。对于这类错误,Skill 应当采取自动重试机制,而不是直接向上抛出异常。

模型错误:不确定性的代价

LLM 的输出本质上是概率性采样,这意味着即便 prompt 写得再精确,仍有约 2%~5% 的概率出现:

  • 函数调用的参数名错误
  • 生成的 JSON 中多了一个逗号或缺少闭合括号
  • 工具选择错误(该用 search 却用了 calculate)
  • 幻觉出根本不存在的 API 端点

截至当前调研资料(2025 年底至 2026 年初),各主流模型在工具调用场景下的格式违规率如下:

模型 工具调用格式违规率 来源说明
Claude 3.5 Sonnet 约 1.5% 从当前调研资料综合评估
GPT-4o 约 2.8% 从当前调研资料综合评估
GPT-4o-mini 约 4.2% 从当前调研资料综合评估
Gemini 1.5 Pro 约 3.0% 从当前调研资料综合评估

数据说明:上述比例来自社区工程团队的分享和官方文档的引用,实际表现因 prompt 设计复杂度而异。保守策略应假设 5% 的调用会产生格式层面的问题

逻辑错误:最难检测的一类

逻辑错误往往看起来“一切正常”——API 返回了 200,模型输出的 JSON 也能解析,但结果就是错了。这类错误只能在语义校验层捕获,通常需要预定义业务规则。

例如,一个执行折扣计算的 Skill,如果输出了负数价格,系统层面没有任何异常,但这显然是逻辑缺陷。处理这类错误的唯一有效手段是断言式校验

def validate_discount_result(result: dict) -> bool:
    """业务逻辑校验:折扣后价格不得为负,且不得超过原价"""
    if result.get("discounted_price", 0) < 0:
        raise BusinessLogicError("折扣后价格为负")
    if result.get("discounted_price", 0) > result.get("original_price", 0):
        raise BusinessLogicError("折扣后价格超过原价")
    return True

二、实现指数退避与断路器模式

当系统错误发生时,最常见做法是“立即重试”。但盲目重试会把瞬时故障变成雪崩——上游服务刚从短暂过载中恢复,紧接着就被几十个同时到达的重试请求再次压垮。

指数退避:用时间换空间

指数退避(Exponential Backoff)的核心思想很简单:每次重试的等待时间呈指数增长,并加入随机抖动。标准的实现模式如下:

import time
import random
from typing import Callable

def with_exponential_backoff(
    func: Callable,
    max_retries: int = 5,
    base_delay: float = 1.0,
    max_delay: float = 60.0,
    backoff_factor: float = 2.0
):
    """指数退避装饰器,带随机抖动"""
    for attempt in range(max_retries):
        try:
            return func()
        except TemporaryFailureError as e:
            if attempt == max_retries - 1:
                raise
            delay = min(base_delay * (backoff_factor ** attempt), max_delay)
            jitter = random.uniform(0, delay * 0.3)  # 30% 抖动
            time.sleep(delay + jitter)
            print(f"[重试 {attempt + 1}/{max_retries}] 等待 {delay + jitter:.1f}s 后重试...")

重试参数的选择指南:

参数 推荐值 原因
max_retries 3~5 次 超过 5 次重试通常意味着上游恢复时间过长,不如直接降级
base_delay 1 秒 足够避开极短时间的网络抖动
max_delay 30~60 秒 多数 API 限流窗口在 60 秒内,有必要给够时间
jitter 10%~30% 避免“惊群效应”——大量请求在同一毫秒重试

断路器:当一个 API 彻底不可用

如果某个依赖的服务已经持续失败 5 次以上,继续发送重试请求只会浪费系统资源。此时需要断路器(Circuit Breaker) 主动熔断。

断路器的三种状态:

[CLOSED] ──连续成功──> 正常工作
[CLOSED] ──连续失败N次──> [OPEN] ──熔断,直接返回错误
[OPEN] ──等待timeout──> [HALF-OPEN] ──试探性请求
[HALF-OPEN] ──成功──> [CLOSED]
[HALF-OPEN] ──失败──> [OPEN] (继续熔断)

在生产 Skill 中的简洁实现:

from datetime import datetime, timedelta

class CircuitBreaker:
    def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = "CLOSED"

    def call(self, func: Callable):
        if self.state == "OPEN":
            if self._recovery_timeout_elapsed():
                self.state = "HALF-OPEN"
            else:
                raise CircuitBreakerOpenError("断路器已熔断")

        try:
            result = func()
            if self.state == "HALF-OPEN":
                self.state = "CLOSED"
                self.failure_count = 0
            return result
        except Exception as e:
            self.failure_count += 1
            self.last_failure_time = datetime.now()
            if self.failure_count >= self.failure_threshold:
                self.state = "OPEN"
            raise

重试策略的选择决策表

并非所有错误都适合重试。错误的决策会导致更严重的问题:

错误类型 是否重试 策略 作者的结论
HTTP 429(限流) 指数退避,读取 Retry-After 头 必须尊重上游的限流策略
HTTP 503(服务不可用) 指数退避 + 断路器 短时间恢复概率高
HTTP 403(权限不足) 直接告警,通知管理员 重试一万次也不会变成 200
HTTP 400(参数错误) 记录错误,通知开发者 是请求本身的问题
模型 JSON 解析失败 有限重试 最多 2 次,重新提示模型 超过 2 次说明 prompt 有问题
模型幻觉函数名 降级到默认工具或人工 模型不会因为重试而“想起来”正确的名字

三、用户友好的错误信息转换

即使后端的所有重试和断路策略都设计得当,最终用户看到的错误信息仍然是体验的底线。向用户直接抛出 CircuitBreakerOpenError: 断路器已熔断,failure_count=5 属于生产事故。

三级错误信息映射

构建一个错误映射层,将内部异常翻译为用户可理解的信息:

ERROR_MAPPING = {
    "TemporaryAPIError": {
        "level": "transparent",
        "user_message": "正在重试请求,请稍候...",
        "action": "auto_retry"
    },
    "CircuitBreakerOpenError": {
        "level": "informational",
        "user_message": "数据服务暂时不可用,我们已收到通知。预计 {recovery_time} 后自动恢复。",
        "action": "notify_admin"
    },
    "ModelParseError": {
        "level": "degraded",
        "user_message": "当前对话处理遇到一点问题,已切换至基础模式。您可以直接告诉我需要什么帮助。",
        "action": "fallback_mode"
    },
    "BusinessLogicError": {
        "level": "critical",
        "user_message": "系统在处理您的请求时发现异常,该请求已被记录。请稍后重试或联系支持团队。",
        "action": "alert_and_halt"
    }
}

静默失败的终结:必须告警的场景

没有任何用户愿意在三天后才发现机器人什么都没做。以下场景必须触发告警,不允许静默处理:

场景 告警级别 通知方式 响应时效
断路器熔断超过 5 分钟 P1 电话 + 即时消息 15 分钟内
模型连续 3 次格式异常 P2 即时消息 1 小时内
业务逻辑校验失败 P1 电话 + 即时消息 30 分钟内
用户显式请求“重试”但依然失败 P2 即时消息 2 小时内
单日重试总量超过阈值(如 1000 次) P3 邮件日报 当天内

总结:错误处理的工程化清单

一个具备生产可用性的 Skill,其错误处理应当覆盖以下全部维度:

  1. 明确三类错误的分流策略:系统错误重试,模型错误校验,逻辑错误告警
  2. 指数退避不是可选项:没有退避的重试等于拒绝服务攻击
  3. 断路器是所有外部依赖的标配:保护上游,也保护自己
  4. 用户看到的永远不应是技术错误栈:每个内部异常都有对应的用户友好映射
  5. 静默是最大的敌人:凡关键路径失败,必须有监控告警

下一章《状态管理与事务性是 Skills 工程化的分水岭》将在此基础上更进一步:当 Skill 需要长时间运行、多步骤协作时,如何保证中途崩溃后能优雅恢复,如何处理并发安全与数据一致性——这才是工程化 Skill 与脚本级 Skill 的本质差异。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 查看所有版本


暂无话题~