Skip to content

错误处理与重试策略

健壮的错误处理是生产级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++;
            }
        }
    }
}

知识点总结

核心要点

  1. 错误分类:区分可重试和不可重试错误
  2. 重试策略:指数退避+抖动
  3. 熔断器:防止级联故障
  4. 降级策略:多服务商+缓存
  5. 日志监控:结构化日志记录

易错点回顾

易错点正确做法
不区分错误类型根据状态码判断是否重试
固定延迟重试使用指数退避
无限重试设置最大重试次数
不记录错误结构化日志记录

拓展参考资料

进阶学习路径

  1. 本知识点 → 错误处理与重试
  2. 下一步Token优化策略
  3. 进阶流式响应处理
  4. 高级安全与鉴权

💡 记住:健壮的错误处理是生产级AI应用的基石,重试+熔断+降级是构建可靠系统的三驾马车。