Appearance
错误处理与重试策略
健壮的错误处理是生产级AI应用的基石
概述
在调用AI API时,网络波动、服务限流、模型过载等问题不可避免。本教程将教你如何构建完善的错误处理和重试机制,确保应用的稳定性。
为什么需要错误处理?
AI API调用可能遇到的问题:
网络层面
├── 网络超时
├── 连接失败
├── DNS解析错误
└── SSL证书问题
服务层面
├── 速率限制 (429)
├── 服务过载 (503)
├── 内部错误 (500)
└── 维护中 (503)
业务层面
├── 参数错误 (400)
├── 认证失败 (401)
├── 权限不足 (403)
├── 资源不存在 (404)
└── 配额不足 (402)常见错误类型
HTTP状态码分类
php
<?php
class APIErrorClassifier
{
public static function classify(int $statusCode): string
{
if ($statusCode >= 500) {
return 'server_error';
}
if ($statusCode === 429) {
return 'rate_limit';
}
if ($statusCode >= 400 && $statusCode < 500) {
return 'client_error';
}
return 'unknown';
}
public static function isRetryable(int $statusCode): bool
{
return in_array($statusCode, [429, 500, 502, 503, 504]);
}
public static function getErrorMessage(int $statusCode): string
{
$messages = [
400 => '请求参数错误',
401 => '认证失败,请检查API Key',
403 => '权限不足或账户被禁用',
404 => '请求的资源不存在',
429 => '请求过于频繁,请稍后重试',
500 => '服务器内部错误',
502 => '网关错误',
503 => '服务暂时不可用',
504 => '网关超时',
];
return $messages[$statusCode] ?? "未知错误 ({$statusCode})";
}
}错误响应解析
php
<?php
class APIErrorParser
{
public static function parseOpenAIError(array $response): array
{
$error = $response['error'] ?? [];
return [
'type' => $error['type'] ?? 'unknown',
'message' => $error['message'] ?? 'Unknown error',
'code' => $error['code'] ?? null,
];
}
public static function parseClaudeError(array $response): array
{
$error = $response['error'] ?? [];
return [
'type' => $error['type'] ?? 'unknown',
'message' => $error['message'] ?? 'Unknown error',
];
}
public static function parseDeepSeekError(array $response): array
{
return [
'type' => $response['error']['type'] ?? 'unknown',
'message' => $response['error']['message'] ?? 'Unknown error',
'code' => $response['error']['code'] ?? null,
];
}
}基础错误处理
Try-Catch模式
php
<?php
class RobustAIClient
{
private $client;
private $apiKey;
public function chat(array $messages): array
{
try {
$response = $this->client->post('/chat/completions', [
'json' => [
'model' => 'gpt-4o-mini',
'messages' => $messages,
],
]);
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
return $this->handleRequestException($e);
} catch (ConnectException $e) {
return $this->handleConnectionError($e);
} catch (Exception $e) {
return $this->handleGenericError($e);
}
}
private function handleRequestException(RequestException $e): array
{
$response = $e->getResponse();
$statusCode = $response ? $response->getStatusCode() : 0;
$body = $response ? $response->getBody()->getContents() : '';
$errorData = json_decode($body, true);
return [
'success' => false,
'error' => [
'code' => $statusCode,
'type' => APIErrorClassifier::classify($statusCode),
'message' => APIErrorClassifier::getErrorMessage($statusCode),
'details' => $errorData,
'retryable' => APIErrorClassifier::isRetryable($statusCode),
],
];
}
private function handleConnectionError(ConnectException $e): array
{
return [
'success' => false,
'error' => [
'code' => 0,
'type' => 'connection_error',
'message' => '网络连接失败:' . $e->getMessage(),
'retryable' => true,
],
];
}
private function handleGenericError(Exception $e): array
{
return [
'success' => false,
'error' => [
'code' => 0,
'type' => 'unknown_error',
'message' => $e->getMessage(),
'retryable' => false,
],
];
}
}重试策略
指数退避重试
php
<?php
class RetryStrategy
{
private int $maxRetries;
private int $baseDelayMs;
private float $multiplier;
private int $maxDelayMs;
public function __construct(
int $maxRetries = 3,
int $baseDelayMs = 1000,
float $multiplier = 2.0,
int $maxDelayMs = 30000
) {
$this->maxRetries = $maxRetries;
$this->baseDelayMs = $baseDelayMs;
$this->multiplier = $multiplier;
$this->maxDelayMs = $maxDelayMs;
}
public function getDelay(int $attempt): int
{
$delay = $this->baseDelayMs * pow($this->multiplier, $attempt);
return min((int)$delay, $this->maxDelayMs);
}
public function shouldRetry(int $attempt, int $statusCode): bool
{
if ($attempt >= $this->maxRetries) {
return false;
}
return APIErrorClassifier::isRetryable($statusCode);
}
}
class RetryableAIClient
{
private $client;
private RetryStrategy $retryStrategy;
public function __construct($client, ?RetryStrategy $strategy = null)
{
$this->client = $client;
$this->retryStrategy = $strategy ?? new RetryStrategy();
}
public function chatWithRetry(array $messages): array
{
$attempt = 0;
$lastError = null;
while (true) {
try {
$result = $this->client->chat($messages);
if (isset($result['success']) && !$result['success']) {
throw new Exception($result['error']['message']);
}
return $result;
} catch (RequestException $e) {
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 0;
$lastError = $e;
if (!$this->retryStrategy->shouldRetry($attempt, $statusCode)) {
break;
}
$delay = $this->retryStrategy->getDelay($attempt);
usleep($delay * 1000);
$attempt++;
} catch (Exception $e) {
$lastError = $e;
break;
}
}
throw new Exception("重试{$attempt}次后仍然失败: " . $lastError->getMessage());
}
}
// 使用示例
$client = new RobustAIClient($apiKey);
$retryClient = new RetryableAIClient($client);
try {
$result = $retryClient->chatWithRetry([
['role' => 'user', 'content' => '你好']
]);
echo $result['choices'][0]['message']['content'];
} catch (Exception $e) {
echo "请求失败: " . $e->getMessage();
}抖动重试
php
<?php
class JitteredRetryStrategy extends RetryStrategy
{
private float $jitterFactor;
public function __construct(
int $maxRetries = 3,
int $baseDelayMs = 1000,
float $multiplier = 2.0,
int $maxDelayMs = 30000,
float $jitterFactor = 0.5
) {
parent::__construct($maxRetries, $baseDelayMs, $multiplier, $maxDelayMs);
$this->jitterFactor = $jitterFactor;
}
public function getDelay(int $attempt): int
{
$baseDelay = parent::getDelay($attempt);
$jitter = $baseDelay * $this->jitterFactor * (mt_rand(0, 1000) / 1000);
return (int)($baseDelay + $jitter);
}
}熔断器模式
实现熔断器
php
<?php
class CircuitBreaker
{
private int $failureThreshold;
private int $recoveryTimeout;
private int $failureCount = 0;
private ?int $lastFailureTime = null;
private string $state = 'closed';
public function __construct(int $failureThreshold = 5, int $recoveryTimeout = 60)
{
$this->failureThreshold = $failureThreshold;
$this->recoveryTimeout = $recoveryTimeout;
}
public function canExecute(): bool
{
if ($this->state === 'closed') {
return true;
}
if ($this->state === 'open') {
if ($this->shouldAttemptReset()) {
$this->state = 'half_open';
return true;
}
return false;
}
return true;
}
public function recordSuccess(): void
{
$this->failureCount = 0;
$this->state = 'closed';
}
public function recordFailure(): void
{
$this->failureCount++;
$this->lastFailureTime = time();
if ($this->failureCount >= $this->failureThreshold) {
$this->state = 'open';
}
}
private function shouldAttemptReset(): bool
{
return $this->lastFailureTime !== null &&
(time() - $this->lastFailureTime) >= $this->recoveryTimeout;
}
public function getState(): string
{
return $this->state;
}
}
class CircuitBreakerClient
{
private $client;
private CircuitBreaker $circuitBreaker;
public function __construct($client, CircuitBreaker $circuitBreaker)
{
$this->client = $client;
$this->circuitBreaker = $circuitBreaker;
}
public function chat(array $messages): array
{
if (!$this->circuitBreaker->canExecute()) {
throw new Exception('服务暂时不可用,请稍后重试');
}
try {
$result = $this->client->chat($messages);
$this->circuitBreaker->recordSuccess();
return $result;
} catch (Exception $e) {
$this->circuitBreaker->recordFailure();
throw $e;
}
}
}降级策略
实现服务降级
php
<?php
class FallbackAIClient
{
private array $clients;
private array $fallbackChain;
public function __construct(array $clients)
{
$this->clients = $clients;
$this->fallbackChain = array_keys($clients);
}
public function chat(array $messages, string $preferredClient = 'primary'): array
{
$chain = $this->buildFallbackChain($preferredClient);
foreach ($chain as $clientName) {
try {
$client = $this->clients[$clientName];
$result = $client->chat($messages);
return array_merge($result, ['provider' => $clientName]);
} catch (Exception $e) {
error_log("Client {$clientName} failed: " . $e->getMessage());
continue;
}
}
throw new Exception('所有服务提供商均不可用');
}
private function buildFallbackChain(string $preferred): array
{
$chain = [$preferred];
foreach ($this->fallbackChain as $client) {
if (!in_array($client, $chain)) {
$chain[] = $client;
}
}
return $chain;
}
}
// 使用示例
$primaryClient = new OpenAIClient($openaiKey);
$backupClient = new DeepSeekClient($deepseekKey);
$tertiaryClient = new QwenClient($qwenKey);
$fallbackClient = new FallbackAIClient([
'primary' => $primaryClient,
'backup' => $backupClient,
'tertiary' => $tertiaryClient,
]);
$result = $fallbackClient->chat([
['role' => 'user', 'content' => '你好']
], 'primary');
echo "响应来自: " . $result['provider'];缓存降级
php
<?php
class CachedFallbackClient
{
private $client;
private string $cacheDir;
private int $cacheTTL;
public function __construct($client, string $cacheDir = '/tmp/ai_cache', int $cacheTTL = 3600)
{
$this->client = $client;
$this->cacheDir = $cacheDir;
$this->cacheTTL = $cacheTTL;
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
}
public function chat(array $messages, bool $useCache = true): array
{
$cacheKey = $this->getCacheKey($messages);
$cacheFile = $this->cacheDir . '/' . $cacheKey . '.json';
if ($useCache && file_exists($cacheFile)) {
$cache = json_decode(file_get_contents($cacheFile), true);
if (time() - $cache['timestamp'] < $this->cacheTTL) {
return array_merge($cache['data'], ['from_cache' => true]);
}
}
try {
$result = $this->client->chat($messages);
file_put_contents($cacheFile, json_encode([
'timestamp' => time(),
'data' => $result,
]));
return array_merge($result, ['from_cache' => false]);
} catch (Exception $e) {
if (file_exists($cacheFile)) {
$cache = json_decode(file_get_contents($cacheFile), true);
return array_merge($cache['data'], [
'from_cache' => true,
'cache_expired' => true,
]);
}
throw $e;
}
}
private function getCacheKey(array $messages): string
{
return md5(serialize($messages));
}
}错误日志与监控
结构化日志
php
<?php
class APILogger
{
private string $logFile;
private string $level;
public function __construct(string $logFile, string $level = 'info')
{
$this->logFile = $logFile;
$this->level = $level;
}
public function logRequest(string $provider, array $params): void
{
$this->log('request', [
'provider' => $provider,
'params' => $this->sanitizeParams($params),
'timestamp' => date('c'),
]);
}
public function logResponse(string $provider, array $response, float $duration): void
{
$this->log('response', [
'provider' => $provider,
'status' => 'success',
'duration_ms' => round($duration * 1000, 2),
'tokens_used' => $response['usage'] ?? null,
]);
}
public function logError(string $provider, Exception $error, float $duration): void
{
$this->log('error', [
'provider' => $provider,
'status' => 'error',
'duration_ms' => round($duration * 1000, 2),
'error_type' => get_class($error),
'error_message' => $error->getMessage(),
'error_trace' => $error->getTraceAsString(),
], 'error');
}
private function log(string $type, array $data, string $level = 'info'): void
{
$entry = json_encode([
'type' => $type,
'level' => $level,
'timestamp' => date('c'),
'data' => $data,
]) . "\n";
file_put_contents($this->logFile, $entry, FILE_APPEND);
}
private function sanitizeParams(array $params): array
{
if (isset($params['messages'])) {
foreach ($params['messages'] as &$message) {
if (isset($message['content']) && strlen($message['content']) > 100) {
$message['content'] = substr($message['content'], 0, 100) . '...[truncated]';
}
}
}
return $params;
}
}
class LoggedAIClient
{
private $client;
private APILogger $logger;
private string $provider;
public function __construct($client, APILogger $logger, string $provider)
{
$this->client = $client;
$this->logger = $logger;
$this->provider = $provider;
}
public function chat(array $messages, array $options = []): array
{
$params = ['messages' => $messages] + $options;
$this->logger->logRequest($this->provider, $params);
$startTime = microtime(true);
try {
$result = $this->client->chat($messages, $options);
$duration = microtime(true) - $startTime;
$this->logger->logResponse($this->provider, $result, $duration);
return $result;
} catch (Exception $e) {
$duration = microtime(true) - $startTime;
$this->logger->logError($this->provider, $e, $duration);
throw $e;
}
}
}常见问题答疑(FAQ)
Q1:如何判断是否应该重试?
回答:根据HTTP状态码判断:
| 状态码 | 是否重试 | 原因 |
|---|---|---|
| 429 | 是 | 速率限制,等待后可恢复 |
| 500 | 是 | 服务器临时错误 |
| 502/503/504 | 是 | 网关或服务临时不可用 |
| 400 | 否 | 请求参数错误 |
| 401 | 否 | 认证失败 |
| 403 | 否 | 权限不足 |
Q2:重试次数应该设置多少?
回答:
php
<?php
// 推荐配置
$retryStrategy = new RetryStrategy(
maxRetries: 3, // 最大重试3次
baseDelayMs: 1000, // 初始延迟1秒
multiplier: 2.0, // 指数退避因子
maxDelayMs: 30000 // 最大延迟30秒
);Q3:如何处理超时问题?
回答:
php
<?php
// 设置合理的超时时间
$client = new Client([
'timeout' => 60, // 总超时60秒
'connect_timeout' => 10, // 连接超时10秒
'read_timeout' => 60, // 读取超时60秒
]);Q4:如何实现优雅降级?
回答:
php
<?php
// 1. 多服务商降级
$fallbackClient = new FallbackAIClient([...]);
// 2. 缓存降级
$cachedClient = new CachedFallbackClient($client);
// 3. 默认响应降级
function chatWithDefaultResponse(array $messages): string
{
try {
return $client->chat($messages);
} catch (Exception $e) {
return '抱歉,服务暂时不可用,请稍后重试。';
}
}Q5:如何监控API调用?
回答:
php
<?php
// 使用日志记录
$logger = new APILogger('/var/log/ai_api.log');
$loggedClient = new LoggedAIClient($client, $logger, 'openai');
// 定期分析日志
// - 错误率
// - 响应时间
// - Token使用量Q6:如何处理并发错误?
回答:
php
<?php
// 使用熔断器防止雪崩
$circuitBreaker = new CircuitBreaker(
failureThreshold: 5, // 5次失败后熔断
recoveryTimeout: 60 // 60秒后尝试恢复
);
$circuitClient = new CircuitBreakerClient($client, $circuitBreaker);实战练习
基础练习
练习1:实现一个带重试机制的API客户端。
参考代码:
php
<?php
class SimpleRetryClient
{
private $client;
private int $maxRetries = 3;
public function chat(array $messages): array
{
for ($i = 0; $i < $this->maxRetries; $i++) {
try {
return $this->client->chat($messages);
} catch (Exception $e) {
if ($i === $this->maxRetries - 1) {
throw $e;
}
usleep(pow(2, $i) * 1000000);
}
}
}
}进阶练习
练习2:实现一个多服务商降级客户端。
参考代码:
php
<?php
class MultiProviderClient
{
private array $providers;
public function chat(array $messages): array
{
foreach ($this->providers as $name => $client) {
try {
return $client->chat($messages);
} catch (Exception $e) {
error_log("Provider {$name} failed: " . $e->getMessage());
}
}
throw new Exception('All providers failed');
}
}挑战练习
练习3:实现一个完整的容错客户端,包含重试、熔断、降级和日志。
参考代码:
php
<?php
class ResilientAIClient
{
private $client;
private RetryStrategy $retry;
private CircuitBreaker $circuitBreaker;
private APILogger $logger;
public function chat(array $messages): array
{
if (!$this->circuitBreaker->canExecute()) {
throw new Exception('Service unavailable');
}
$attempt = 0;
while (true) {
try {
$this->logger->logRequest('resilient', ['messages' => $messages]);
$result = $this->client->chat($messages);
$this->circuitBreaker->recordSuccess();
return $result;
} catch (Exception $e) {
$this->logger->logError('resilient', $e, 0);
$this->circuitBreaker->recordFailure();
if (!$this->retry->shouldRetry($attempt, $this->getStatusCode($e))) {
throw $e;
}
usleep($this->retry->getDelay($attempt) * 1000);
$attempt++;
}
}
}
}知识点总结
核心要点
- 错误分类:区分可重试和不可重试错误
- 重试策略:指数退避+抖动
- 熔断器:防止级联故障
- 降级策略:多服务商+缓存
- 日志监控:结构化日志记录
易错点回顾
| 易错点 | 正确做法 |
|---|---|
| 不区分错误类型 | 根据状态码判断是否重试 |
| 固定延迟重试 | 使用指数退避 |
| 无限重试 | 设置最大重试次数 |
| 不记录错误 | 结构化日志记录 |
拓展参考资料
进阶学习路径
💡 记住:健壮的错误处理是生产级AI应用的基石,重试+熔断+降级是构建可靠系统的三驾马车。
