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,其错误处理应当覆盖以下全部维度:
- 明确三类错误的分流策略:系统错误重试,模型错误校验,逻辑错误告警
- 指数退避不是可选项:没有退避的重试等于拒绝服务攻击
- 断路器是所有外部依赖的标配:保护上游,也保护自己
- 用户看到的永远不应是技术错误栈:每个内部异常都有对应的用户友好映射
- 静默是最大的敌人:凡关键路径失败,必须有监控告警
下一章《状态管理与事务性是 Skills 工程化的分水岭》将在此基础上更进一步:当 Skill 需要长时间运行、多步骤协作时,如何保证中途崩溃后能优雅恢复,如何处理并发安全与数据一致性——这才是工程化 Skill 与脚本级 Skill 的本质差异。
agent skills 入门到精通
关于 LearnKu