Agent Function Calling 错误分类:网络超时权限与业务异常
Agent Function Calling 错误分类:网络超时权限与业务异常
一、你的 Agent 把所有工具调用失败都当"网络超时"处理,准确率一路下滑
Agent 调用外部工具(API、数据库、文件系统)的失败模式不是"成功"和"失败"两个状态。真实的失败至少可以分解为四类:网络超时、认证权限错误、参数校验失败、业务逻辑异常。这四类的处理方式完全不同——超时需要重试,权限错误需要中断,参数错误需要反馈修正,业务异常需要降级。
如果把所有错误都当成一种来处理,Agent 的行为会不可预知:权限错误被当作超时重试了三次,每次都返回相同的结果;参数校验失败被当作业务异常,Agent 开始自行"修正"参数然后重新调用;业务异常被当作网络问题,Agent 告诉用户"请稍后重试"但实际上这是数据层面的错误,重试一百次也没用。
Function Calling 的错误分类不是可选的优化,是 Agent 行为可预测性的基础。没有分类,Agent 在错误面前的决策就是随机的。
二、底层机制与原理剖析
错误分类的核心是把工具返回的错误映射到一个标准化的"错误处理决策树"中。系统首先根据 HTTP 状态码和响应体内容判定错误类型,随后触发对应的处理策略。具体流程为:若返回 HTTP 2xx 且数据正常则视为成功;若为 408、504 或连接重置则归类为网络超时;若为 401、403 则归类为权限错误;若为 400、422 则归类为参数校验错误;若为 200 但包含错误体则归类为业务逻辑异常。判定完成后,系统将根据错误类型执行重试、中断、修正或降级等操作。
四类错误的处理原则:
网络超时:可重试,但必须有上限和退避。三次重试、指数退避(1s、2s、4s)。超过三次后视为服务不可用,走降级。
权限错误:不可重试,立即终止。权限错误不是临时性的——再试多少次也不会改成 200。继续重试只会浪费 Token 和时间。
参数校验错误:可修正重试一次。如果 API 返回的 error message 中包含参数修正提示(如"city_code 应为 3 位数字"),可以用这个信息自动修正后重试一次。但如果第二次还是参数错误,说明不是简单的修正能解决的,交给用户处理。
业务逻辑异常:不重试,走降级。比如"账户余额不足"、"库存为 0"——这些是正常的业务流程,不是系统故障。需要把业务错误码映射为用户能理解的消息。
三、生产级代码实现
"""
Agent Function Calling 错误分类与处理策略
四类错误:网络超时、权限、参数校验、业务异常
每种错误有独立的退避、重试和降级策略
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any, Callable
from enum import Enum
import time
import re
import asyncio
import aiohttp
class ErrorCategory(Enum):
"""错误大类"""
NETWORK_TIMEOUT = "network_timeout" # 网络超时/连接问题
AUTH_PERMISSION = "auth_permission" # 认证/权限
PARAM_VALIDATION = "param_validation" # 参数校验失败
SERVER_ERROR = "server_error" # 服务端内部错误
BUSINESS_LOGIC = "business_logic" # 业务逻辑异常
UNKNOWN = "unknown" # 未分类
class RetryStrategy(Enum):
"""重试策略"""
NO_RETRY = "no_retry" # 不重试
LINEAR_BACKOFF = "linear_backoff" # 线性退避
EXPONENTIAL_BACKOFF = "exponential" # 指数退避
FIXED_INTERVAL = "fixed_interval" # 固定间隔
@dataclass
class ErrorPolicy:
"""单类错误的处理策略"""
category: ErrorCategory
retry_strategy: RetryStrategy
max_retries: int
base_delay_ms: int # 基础延迟(毫秒)
should_interrupt: bool # 是否中断工具链(不继续调后续工具)
@dataclass
class ErrorClassification:
"""错误分类结果"""
category: ErrorCategory
confidence: float # 分类置信度 0-1
error_code: str # 标准化错误码
original_message: str
sanitized_message: str # 净化后的消息(可安全展示给 LLM)
retriable: bool
auto_fixable: bool # 是否可以自动修正
@dataclass
class ToolCallResult:
"""工具调用结果"""
tool_name: str
success: bool
data: Optional[Any] = None
error: Optional[ErrorClassification] = None
retry_count: int = 0
elapsed_ms: float = 0.0
# 错误分类规则表
ERROR_CLASSIFICATION_RULES = {
# === 网络超时 ===
"timeout": ErrorCategory.NETWORK_TIMEOUT,
"timed out": ErrorCategory.NETWORK_TIMEOUT,
"connection refused": ErrorCategory.NETWORK_TIMEOUT,
"connection reset": ErrorCategory.NETWORK_TIMEOUT,
"no route to host": ErrorCategory.NETWORK_TIMEOUT,
"temporary failure in name resolution": ErrorCategory.NETWORK_TIMEOUT,
"eof": ErrorCategory.NETWORK_TIMEOUT,
"broken pipe": ErrorCategory.NETWORK_TIMEOUT,
# === 权限错误 ===
"unauthorized": ErrorCategory.AUTH_PERMISSION,
"forbidden": ErrorCategory.AUTH_PERMISSION,
"access denied": ErrorCategory.AUTH_PERMISSION,
"permission denied": ErrorCategory.AUTH_PERMISSION,
"invalid api key": ErrorCategory.AUTH_PERMISSION,
"token expired": ErrorCategory.AUTH_PERMISSION,
"insufficient scope": ErrorCategory.AUTH_PERMISSION,
# === 参数校验 ===
"invalid parameter": ErrorCategory.PARAM_VALIDATION,
"missing required": ErrorCategory.PARAM_VALIDATION,
"validation error": ErrorCategory.PARAM_VALIDATION,
"bad request": ErrorCategory.PARAM_VALIDATION,
"unprocessable entity": ErrorCategory.PARAM_VALIDATION,
"invalid format": ErrorCategory.PARAM_VALIDATION,
# === 服务端错误 ===
"internal server error": ErrorCategory.SERVER_ERROR,
"service unavailable": ErrorCategory.SERVER_ERROR,
"bad gateway": ErrorCategory.SERVER_ERROR,
}
# 各类错误的处理策略
DEFAULT_POLICIES = {
ErrorCategory.NETWORK_TIMEOUT: ErrorPolicy(
category=ErrorCategory.NETWORK_TIMEOUT,
retry_strategy=RetryStrategy.EXPONENTIAL_BACKOFF,
max_retries=3,
base_delay_ms=1000,
should_interrupt=False,
),
ErrorCategory.AUTH_PERMISSION: ErrorPolicy(
category=ErrorCategory.AUTH_PERMISSION,
retry_strategy=RetryStrategy.NO_RETRY,
max_retries=0,
base_delay_ms=0,
should_interrupt=True, # 权限错误:立即停止工具链
),
ErrorCategory.PARAM_VALIDATION: ErrorPolicy(
category=ErrorCategory.PARAM_VALIDATION,
retry_strategy=RetryStrategy.FIXED_INTERVAL,
max_retries=1, # 仅重试一次(修正参数后)
base_delay_ms=0,
should_interrupt=False,
),
ErrorCategory.SERVER_ERROR: ErrorPolicy(
category=ErrorCategory.SERVER_ERROR,
retry_strategy=RetryStrategy.LINEAR_BACKOFF,
max_retries=2,
base_delay_ms=2000,
should_interrupt=False,
),
ErrorCategory.BUSINESS_LOGIC: ErrorPolicy(
category=ErrorCategory.BUSINESS_LOGIC,
retry_strategy=RetryStrategy.NO_RETRY,
max_retries=0,
base_delay_ms=0,
should_interrupt=False,
),
}
class ErrorClassifier:
"""错误分类器
基于规则匹配 + 关键词提取,将工具返回的错误归类。
规则是声明式的可维护的表格,不是硬编码的 if-else。
"""
def classify(self, error: Exception, http_status: Optional[int] = None) -> ErrorClassification:
"""分类一个错误"""
error_str = str(error).lower()
error_type = type(error).__name__
# Step 1: HTTP 状态码分类
if http_status:
if http_status == 401 or http_status == 403:
return self._build_result(
ErrorCategory.AUTH_PERMISSION, 0.95,
f"HTTP_{http_status}", str(error), "认证或权限不足"
)
if http_status == 400 or http_status == 422:
return self._build_result(
ErrorCategory.PARAM_VALIDATION, 0.85,
f"HTTP_{http_status}", str(error), "请求参数有误"
)
if http_status >= 500:
return self._build_result(
ErrorCategory.SERVER_ERROR, 0.8,
f"HTTP_{http_status}", str(error), "服务端内部错误"
)
# Step 2: 异常类型分类
if isinstance(error, asyncio.TimeoutError):
return self._build_result(
ErrorCategory.NETWORK_TIMEOUT, 0.95,
"TIMEOUT", str(error), "请求超时"
)
if isinstance(error, (ConnectionError, ConnectionRefusedError, ConnectionResetError)):
return self._build_result(
ErrorCategory.NETWORK_TIMEOUT, 0.9,
"CONNECTION_ERROR", str(error), "网络连接异常"
)
# Step 3: 关键词匹配
for keyword, category in ERROR_CLASSIFICATION_RULES.items():
if keyword in error_str:
return self._build_result(category, 0.8, keyword.upper().replace(" ", "_"), str(error), "")
# Step 4: 降级为 UNKNOWN
return self._build_result(
ErrorCategory.UNKNOWN, 0.3, "UNKNOWN", str(error), "未分类的错误"
)
def _build_result(
self, category: ErrorCategory, confidence: float,
error_code: str, original: str, sanitized: str
) -> ErrorClassification:
policy = DEFAULT_POLICIES.get(category)
return ErrorClassification(
category=category,
confidence=confidence,
error_code=error_code,
original_message=original,
sanitized_message=sanitized or f"{category.value}",
retriable=policy.retry_strategy != RetryStrategy.NO_RETRY if policy else False,
auto_fixable=(category == ErrorCategory.PARAM_VALIDATION),
)
class RetryExecutor:
"""重试执行器
根据 ErrorPolicy 执行不同策略的重试。
"""
def __init__(self):
self.classifier = ErrorClassifier()
async def execute_with_retry(
self,
tool_name: str,
call_fn: Callable,
*args, **kwargs
) -> ToolCallResult:
"""带错误分类的重试执行"""
start_time = time.time()
retry_count = 0
last_error = None
while True:
try:
result = await call_fn(*args, **kwargs)
elapsed = (time.time() - start_time) * 1000
return ToolCallResult(
tool_name=tool_name,
success=True,
data=result,
retry_count=retry_count,
elapsed_ms=elapsed,
)
except Exception as e:
retry_count += 1
last_error = e
# 提取 HTTP 状态码(如果有)
http_status = getattr(e, 'status', None) or getattr(e, 'status_code', None)
# 分类错误
classification = self.classifier.classify(e, http_status)
policy = DEFAULT_POLICIES.get(classification.category)
if not policy:
break # 未知错误不重试
# 权限错误:立即中断
if policy.should_interrupt:
break
# 达到最大重试次数
if retry_count > policy.max_retries:
break
# 参数校验:尝试自动修正
if classification.auto_fixable and retry_count == 1:
kwargs = self._attempt_auto_fix(e, kwargs)
if kwargs is None:
break # 无法自动修正
# 计算退避延迟
delay = self._calculate_delay(policy, retry_count)
await asyncio.sleep(delay / 1000.0)
# 所有重试已耗尽
elapsed = (time.time() - start_time) * 1000
classification = self.classifier.classify(
last_error or Exception("unknown"),
getattr(last_error, 'status', None) if last_error else None
)
return ToolCallResult(
tool_name=tool_name,
success=False,
error=classification,
retry_count=retry_count,
elapsed_ms=elapsed,
)
def _calculate_delay(self, policy: ErrorPolicy, retry_count: int) -> int:
"""计算退避延迟"""
base = policy.base_delay_ms
if policy.retry_strategy == RetryStrategy.EXPONENTIAL_BACKOFF:
return base * (2 ** (retry_count - 1))
elif policy.retry_strategy == RetryStrategy.LINEAR_BACKOFF:
return base * retry_count
elif policy.retry_strategy == RetryStrategy.FIXED_INTERVAL:
return base
return 0
def _attempt_auto_fix(self, error: Exception, kwargs: dict) -> Optional[dict]:
"""尝试从错误消息中自动修正参数"""
# 简化实现:实际应使用 LLM 来解析错误消息中的修正建议
# 例如:API 返回 {"error": "city_code must be 3 digits, got '12'"}
# LLM 可以解析出正确的 city_code 应该是 "012"
return None # 暂时不支持自动修正
def build_response_for_llm(self, result: ToolCallResult) -> str:
"""为 LLM 构建工具调用的响应消息"""
if result.success:
return f"工具 [{result.tool_name}] 调用成功。"
if result.error:
cat = result.error.category
if cat == ErrorCategory.NETWORK_TIMEOUT:
return f"工具 [{result.tool_name}] 网络超时,已重试 {result.retry_count} 次。请稍后重试或跳过此步骤。"
elif cat == ErrorCategory.AUTH_PERMISSION:
return f"工具 [{result.tool_name}] 权限不足,无法继续。请检查 API Key 或权限配置。"
elif cat == ErrorCategory.PARAM_VALIDATION:
return f"工具 [{result.tool_name}] 参数校验失败:{result.error.sanitized_message}。请检查调用参数。"
elif cat == ErrorCategory.SERVER_ERROR:
return f"工具 [{result.tool_name}] 服务端异常,已重试 {result.retry_count} 次。暂时不可用。"
else:
return f"工具 [{result.tool_name}] 调用失败。"
return f"工具 [{result.tool_name}] 调用失败。"
四、边界分析与架构权衡
错误分类的准确性边界:
基于关键词匹配的分类精度在 80-85% 左右。有些 API 返回的错误消息比较模糊(如"Something went wrong"),分类器无法区分是超时还是服务端错误。另外,有些 API 的 4xx 错误信息中包含了可修正的提示——比如 OpenAI 的 429(限流)需要用不同的退避策略处理。分类器需要持续根据 API 特性做规则调整。
参数自动修正的风险:
自动修正参数是一个有风险的操作。修正后的参数可能引入新的错误。比如 API 返回"city_code 应为 3 位数字,当前为 '12'",自动修正为 "012" 看起来正确——但如果 API 期望的 city_code 根本不是 3 位只是恰好名字相似,自动修正反而带来了更大的偏差。建议自动修正在内部环境或开发阶段使用,生产环境由 Agent 提示用户确认。
适用边界:
最适合调用多个不同外部 API 的 Agent。API 种类越多,标准化的错误分类越有价值——避免了为每个 API 单独实现错误处理逻辑。
禁用场景:
不适合只有一两个确定性工具的简单 Agent。也不适合通过中间件(如 API Gateway)已经在入口层统一处理了错误的场景。
五、总结
Function Calling 的错误处理不能一刀切。四类错误对应四种策略:网络超时用指数退避重试(3 次上限)、权限错误立即中断(不能把 403 当 504 处理)、参数校验可尝试自动修正后重试一次、服务端异常可选降级。分类不是用于展示给用户的,而是用于指导 Agent 的下一个动作——是重试、是中断、还是降级。
更多推荐


所有评论(0)