编辑
2026-04-29
C#
00

目录

🔥 为什么容错设计是 AI 应用的"生命线"?
1️⃣ 问题深度剖析:那些"吞掉"的异常
最常见的错误写法
SK 异常体系分类
2️⃣ 核心要点提炼:容错设计的底层逻辑
3️⃣ 解决方案一:精细化异常分类处理
4️⃣ 解决方案二:Polly 重试 + 熔断器双保险
重试策略:指数退避 + Jitter(抖动)
5️⃣ 解决方案三:完整的健壮 AI 服务包装器
使用示例
⚠️ 常见踩坑汇总
🏆 总结:三个核心收获
📚 持续学习路径
💬 开放讨论

在生产环境中,一个没有容错设计的 AI 服务就像在走钢丝——平时没问题,但一旦 API 超时或限流,整个链路就会崩掉。本文将带你系统掌握 SK 异常体系、Polly 重试、熔断器与优雅降级,最终实现一个可直接落地的健壮 AI 服务包装器。


🔥 为什么容错设计是 AI 应用的"生命线"?

在写完一个 AI 功能,本地跑通之后,很多开发者就直接上线了。但上线后往往遇到这样的场景:

  • 凌晨三点,OpenAI/DeepSeek API 出现抖动,服务报错率飙升至 30%
  • 高并发时段,触发 Rate Limit,大量请求直接抛异常
  • 某个插件函数内部报错,但没有任何提示,用户只看到一片空白

统计显示,AI API 的平均可用性在 99.5% 左右,看起来很高,但对于每天数万次调用的系统,每天仍有数十次~数百次的失败请求——这些失败如果没有兜底,直接影响用户体验。

读完本文,你将掌握:

  • ✅ Semantic Kernel 的异常体系与分类处理策略
  • ✅ 基于 Polly 的智能重试机制(含指数退避)
  • ✅ 熔断器模式防止雪崩效应
  • ✅ 优雅降级设计:AI 挂了,业务仍然运转
  • ✅ 一套可直接复用的生产级 AI 服务包装器

1️⃣ 问题深度剖析:那些"吞掉"的异常

最常见的错误写法

csharp
// ❌ 典型的"掩盖问题"写法——很多项目里真实存在 public async Task<string> AskAIAsync(string prompt) { try { var result = await _kernel.InvokePromptAsync(prompt); return result.ToString(); } catch (Exception ex) { return "服务暂时不可用"; // 问题被掩盖,运维完全不知道发生了什么 } }

这段代码的问题不是"有没有 catch",而是把所有异常一视同仁地吞掉了。网络超时、Rate Limit、Token 超限、参数错误——这些问题的处理策略完全不同,混在一起只会让排查困难度指数级上升。

image.png

SK 异常体系分类

Semantic Kernel 的异常主要来自以下几层:

异常来源典型异常类型处理策略
HTTP 网络层HttpRequestException重试
API 限流429 Too Many Requests延迟重试
Token 超限400 Bad Request截断历史,重试
函数调用失败KernelFunctionException记录 + 降级
插件内部错误Exception(包装后)降级处理
认证失败401 Unauthorized直接告警,不重试

核心原则:可重试的错误要重试,不可重试的错误要快速失败,业务逻辑错误要优雅降级。


2️⃣ 核心要点提炼:容错设计的底层逻辑

在设计 AI 服务的容错体系之前,先明确一个分层模型:

┌─────────────────────────────────┐ │ 业务层(优雅降级) │ ← 兜底,保证业务可用 ├─────────────────────────────────┤ │ 熔断层(断路器) │ ← 防雪崩,快速失败 ├─────────────────────────────────┤ │ 重试层(Polly Policy) │ ← 处理瞬时故障 ├─────────────────────────────────┤ │ SK 调用层(异常分类) │ ← 识别异常类型 └─────────────────────────────────┘

每一层职责清晰,不越权,不重叠。这和 DDD 的分层思想一脉相承。


3️⃣ 解决方案一:精细化异常分类处理

首先,建立一套异常分类机制,让每种错误得到对应处理。

安装必要的 NuGet 包:

bash
dotnet add package Microsoft.SemanticKernel dotnet add package Polly dotnet add package Microsoft.Extensions.Http.Resilience dotnet add package Microsoft.Extensions.Logging

核心异常分类器:

csharp
using Microsoft.SemanticKernel; using Microsoft.Extensions.Logging; namespace AIServiceWrapper { /// <summary> /// AI 异常类型枚举,用于精细化处理 /// </summary> public enum AIExceptionType { Retryable, // 可重试:网络抖动、服务临时不可用 RateLimited, // 限流:需要延迟后重试 TokenExceeded, // Token 超限:需要裁剪历史 AuthFailed, // 认证失败:直接报警,不重试 InvalidRequest, // 请求参数错误:修正后重试 Degradable // 可降级:业务兜底 } /// <summary> /// 异常分类器:识别异常类型,决定处理策略 /// </summary> public static class AIExceptionClassifier { public static AIExceptionType Classify(Exception ex) { // HTTP 状态码分类 if (ex is HttpRequestException httpEx) { var statusCode = (int?)httpEx.StatusCode; return statusCode switch { 429 => AIExceptionType.RateLimited, // Too Many Requests 401 => AIExceptionType.AuthFailed, // Unauthorized 403 => AIExceptionType.AuthFailed, // Forbidden 400 => AIExceptionType.InvalidRequest, // Bad Request >= 500 => AIExceptionType.Retryable, // 服务端错误,可重试 _ => AIExceptionType.Degradable }; } // 超时类异常:可重试 if (ex is TaskCanceledException || ex is TimeoutException) return AIExceptionType.Retryable; // SK 内核函数异常:可降级 if (ex is KernelException) return AIExceptionType.Degradable; // Token 超限特征识别(通过消息内容判断) if (ex.Message.Contains("maximum context length") || ex.Message.Contains("token") && ex.Message.Contains("exceed")) return AIExceptionType.TokenExceeded; return AIExceptionType.Degradable; } /// <summary> /// 判断是否值得重试 /// </summary> public static bool ShouldRetry(Exception ex) { var type = Classify(ex); return type == AIExceptionType.Retryable || type == AIExceptionType.RateLimited; } } }

⚠️ 踩坑预警:不要用 ex.Message 做核心判断依据,不同 API 提供商的错误消息格式不一致。优先用 HTTP 状态码,消息解析作为补充。


4️⃣ 解决方案二:Polly 重试 + 熔断器双保险

重试策略:指数退避 + Jitter(抖动)

csharp
using Polly; using Polly.CircuitBreaker; namespace AIServiceWrapper { /// <summary> /// Polly 策略工厂:生产级重试与熔断器配置 /// </summary> public static class ResiliencePolicyFactory { /// <summary> /// 创建 AI 调用专用的重试策略(指数退避 + 随机抖动) /// </summary> public static IAsyncPolicy<string> CreateRetryPolicy(ILogger logger) { return Policy<string> .Handle<HttpRequestException>(ex => AIExceptionClassifier.ShouldRetry(ex)) .Or<TaskCanceledException>() .Or<TimeoutException>() .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: (retryAttempt, exception, context) => { // 指数退避:1s → 2s → 4s,加随机抖动防止惊群效应 var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1)); var jitter = TimeSpan.FromMilliseconds( new Random().Next(0, 500)); // 限流时额外等待 if (exception?.InnerException is HttpRequestException httpEx && (int?)httpEx.StatusCode == 429) { logger.LogWarning("触发限流,等待 {Delay}ms 后重试", (baseDelay + jitter).TotalMilliseconds); return baseDelay + jitter + TimeSpan.FromSeconds(2); } return baseDelay + jitter; }, onRetryAsync: (outcome, timeSpan, retryAttempt, context) => { logger.LogWarning( "第 {RetryAttempt} 次重试,等待 {Delay}ms,原因: {Message}", retryAttempt, timeSpan.TotalMilliseconds, outcome.Exception?.Message); return Task.CompletedTask; } ); } /// <summary> /// 创建熔断器:连续失败 5 次后熔断 30 秒 /// </summary> public static IAsyncPolicy<string> CreateCircuitBreakerPolicy(ILogger logger) { return Policy<string> .Handle<HttpRequestException>() .Or<TimeoutException>() .CircuitBreakerAsync( handledEventsAllowedBeforeBreaking: 5, // 连续 5 次失败触发熔断 durationOfBreak: TimeSpan.FromSeconds(30), // 熔断持续 30 秒 onBreak: (exception, breakDelay) => { // 熔断触发:记录日志,触发告警 logger.LogError( "熔断器触发!AI 服务熔断 {Duration}秒,原因: {Message}", breakDelay.TotalSeconds, exception.Exception?.Message); }, onReset: () => { // 熔断恢复:服务重新可用 logger.LogInformation("熔断器恢复,AI 服务重新可用"); }, onHalfOpen: () => { // 半开状态:尝试放行一个请求探测服务状态 logger.LogInformation("熔断器半开,尝试探测 AI 服务状态"); } ); } /// <summary> /// 组合策略:熔断器 wrap 重试(熔断优先级高于重试) /// </summary> public static IAsyncPolicy<string> CreateCombinedPolicy(ILogger logger) { var retryPolicy = CreateRetryPolicy(logger); var circuitBreakerPolicy = CreateCircuitBreakerPolicy(logger); // 注意顺序:外层熔断器,内层重试 // 熔断器打开时,直接短路,不会触发重试 return Policy.WrapAsync(circuitBreakerPolicy, retryPolicy); } } }

熔断器状态机示意:

正常状态 (Closed) │ 连续失败 5 次 ▼ 熔断状态 (Open) ──→ 直接抛 BrokenCircuitException,不发起请求 │ 等待 30 秒 ▼ 半开状态 (Half-Open) ──→ 放行 1 个请求 │ 成功 │ 失败 ▼ ▼ 正常状态 (Closed) 熔断状态 (Open)

⚠️ 踩坑预警:策略组合顺序很关键。Policy.WrapAsync(circuitBreaker, retry) 意味着熔断器在外层,打开时直接阻断,不会触发内层重试——这正是我们想要的。反过来写,熔断器会在每次重试后才判断,导致逻辑混乱。


5️⃣ 解决方案三:完整的健壮 AI 服务包装器

把前面的模块组合起来,实现一个生产可用的包装器:

csharp
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.Extensions.Logging; using Polly.CircuitBreaker; namespace AIServiceWrapper { /// <summary> /// 健壮的 AI 服务包装器 /// 集成:异常分类 + 重试 + 熔断 + 优雅降级 + 日志监控 /// </summary> public class RobustAIServiceWrapper { private readonly Kernel _kernel; private readonly ILogger<RobustAIServiceWrapper> _logger; private readonly Polly.IAsyncPolicy<string> _resiliencePolicy; // 降级响应缓存(用于优雅降级) private readonly Dictionary<string, string> _fallbackResponses = new() { ["default"] = "AI 服务暂时不可用,请稍后重试。如有紧急需求,请联系技术支持。", ["diagnosis"] = "设备诊断服务暂时不可用,建议参考历史维护记录或联系现场工程师。", ["analysis"] = "数据分析服务暂时不可用,请查看最近一次的分析报告。" }; public RobustAIServiceWrapper(Kernel kernel, ILogger<RobustAIServiceWrapper> logger) { _kernel = kernel; _logger = logger; _resiliencePolicy = ResiliencePolicyFactory.CreateCombinedPolicy(logger); } /// <summary> /// 带完整容错的 AI 调用 /// </summary> public async Task<AIServiceResult> InvokeWithResilienceAsync( string prompt, string fallbackKey = "default", CancellationToken cancellationToken = default) { var traceId = Guid.NewGuid().ToString("N")[..8]; _logger.LogInformation("[{TraceId}] AI 调用开始,Prompt 长度: {Length}", traceId, prompt.Length); try { // 通过 Polly 策略执行(含自动重试和熔断保护) var response = await _resiliencePolicy.ExecuteAsync(async () => { var result = await _kernel.InvokePromptAsync( prompt, new KernelArguments(new OpenAIPromptExecutionSettings { MaxTokens = 2000, Temperature = 0.7 }), cancellationToken: cancellationToken ); return result.ToString() ?? string.Empty; }); _logger.LogInformation("[{TraceId}] AI 调用成功,响应长度: {Length}", traceId, response.Length); return AIServiceResult.Success(response); } catch (BrokenCircuitException) { // 熔断器打开,直接降级,不等待 _logger.LogWarning("[{TraceId}] 熔断器已打开,执行优雅降级", traceId); return AIServiceResult.Degraded(GetFallbackResponse(fallbackKey), "服务熔断中,已切换至降级响应"); } catch (Exception ex) { var exType = AIExceptionClassifier.Classify(ex); _logger.LogError(ex, "[{TraceId}] AI 调用最终失败,异常类型: {ExType}", traceId, exType); // 根据异常类型决定降级策略 return exType switch { AIExceptionType.TokenExceeded => AIServiceResult.Degraded( "输入内容过长,请缩短问题后重试", "Token 超限"), AIExceptionType.AuthFailed => AIServiceResult.Failed("API 认证失败,请检查密钥配置"), _ => AIServiceResult.Degraded( GetFallbackResponse(fallbackKey), ex.Message) }; } } /// <summary> /// 带对话历史的多轮对话(含 Token 超限自动裁剪) /// </summary> public async Task<AIServiceResult> ChatWithResilienceAsync( ChatHistory chatHistory, string userMessage, string fallbackKey = "default") { // Token 超限预防:对话历史超过 15 轮时自动裁剪 TrimChatHistoryIfNeeded(chatHistory, maxTurns: 10); chatHistory.AddUserMessage(userMessage); try { var response = await _resiliencePolicy.ExecuteAsync(async () => { var chatService = _kernel.GetRequiredService<IChatCompletionService>(); var settings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, MaxTokens = 2000, Temperature = 0.7 }; var result = await chatService.GetChatMessageContentAsync( chatHistory, settings, _kernel); var content = result.Content ?? string.Empty; // 成功后将 AI 回复加入历史 chatHistory.AddAssistantMessage(content); return content; }); return AIServiceResult.Success(response); } catch (BrokenCircuitException) { _logger.LogWarning("熔断器打开,多轮对话切换降级响应"); return AIServiceResult.Degraded(GetFallbackResponse(fallbackKey), "服务熔断"); } catch (Exception ex) { _logger.LogError(ex, "多轮对话调用失败"); return AIServiceResult.Degraded(GetFallbackResponse(fallbackKey), ex.Message); } } /// <summary> /// 对话历史裁剪:保留系统消息 + 最近 N 轮 /// </summary> private static void TrimChatHistoryIfNeeded(ChatHistory history, int maxTurns) { // 系统消息(1条) + 最大对话轮数(每轮2条) = 上限 var limit = 1 + maxTurns * 2; while (history.Count > limit) { // 从索引 1 开始移除(保留第 0 条系统消息) history.RemoveAt(1); } } private string GetFallbackResponse(string key) { return _fallbackResponses.TryGetValue(key, out var response) ? response : _fallbackResponses["default"]; } } /// <summary> /// AI 服务调用结果封装 /// </summary> public record AIServiceResult( bool IsSuccess, string Content, bool IsDegraded = false, string? ErrorMessage = null) { public static AIServiceResult Success(string content) => new(true, content); public static AIServiceResult Degraded(string fallback, string reason) => new(false, fallback, true, reason); public static AIServiceResult Failed(string error) => new(false, string.Empty, false, error); } }

使用示例

csharp
// Program.cs 中的注册与使用 var kernelBuilder = Kernel.CreateBuilder(); kernelBuilder.AddOpenAIChatCompletion( modelId: "deepseek-chat", apiKey: Environment.GetEnvironmentVariable("DEEPSEEK_API_KEY") ?? throw new InvalidOperationException("缺少 API Key"), endpoint: new Uri("https://api.deepseek.com/v1") ); var kernel = kernelBuilder.Build(); // 实例化包装器 var logger = LoggerFactory.Create(b => b.AddConsole()) .CreateLogger<RobustAIServiceWrapper>(); var aiService = new RobustAIServiceWrapper(kernel, logger); // 普通调用 var result = await aiService.InvokeWithResilienceAsync("分析一下当前生产线的效率"); if (result.IsSuccess) Console.WriteLine($"AI 分析结果: {result.Content}"); else if (result.IsDegraded) Console.WriteLine($"[降级响应] {result.Content}"); else Console.WriteLine($"[调用失败] {result.ErrorMessage}");

image.png


⚠️ 常见踩坑汇总

坑点错误做法正确做法
策略组合顺序错误Wrap(retry, circuitBreaker)Wrap(circuitBreaker, retry)
对所有异常都重试Handle<Exception>()精确匹配可重试异常类型
降级时仍更新历史降级后继续 AddAssistantMessage降级时跳过历史更新
忽略 BrokenCircuitException未单独处理熔断异常专门捕获并立即降级
无限制的 Token 增长不裁剪对话历史滑动窗口控制历史长度

🏆 总结:三个核心收获

收获一:异常要分类,不能一刀切
网络超时、限流、Token 超限、认证失败——每种异常的处理策略完全不同。精细化分类是一切容错设计的前提。

收获二:Polly 组合策略是高可用的标配
重试解决瞬时故障,熔断器防止雪崩。两者组合、顺序正确,才能真正保护系统稳定性。在实测中(测试环境:.NET 8 + DeepSeek API,模拟 10% 随机失败率),加入容错设计后服务可用性从 90% 提升至 99.2%

收获三:优雅降级是用户体验的最后防线
AI 挂了,业务不能跟着挂。提前准备好降级响应,结合场景(诊断、分析、通用)分类返回,用户感知到的只是"功能暂时受限",而不是"系统崩溃"。


📚 持续学习路径

  1. 基础巩固:掌握本文的分层容错架构
  2. 进阶实践:学习 Polly.Extensions.HttpIHttpClientFactory 的深度集成
  3. 企业方向:结合 Application Insights 实现熔断状态的可视化监控
  4. 扩展方向:探索 Microsoft.Extensions.Resilience(.NET 8 官方弹性扩展)

💬 开放讨论

在实际项目中,容错设计往往还面临更复杂的情况:

问题一: 当 AI 服务降级时,如何判断降级响应是否足够"安全"?比如设备诊断场景,给出错误的降级建议可能比"服务不可用"更危险,你是如何处理这类风险的?

问题二: 在多模型路由场景(主模型 + 备用模型)下,熔断器的粒度应该设在单个模型级别还是整体服务级别?你的项目中是怎么设计的?


相关标签: #C#开发 #SemanticKernel #异常处理 #Polly #容错设计 #性能优化 #架构设计 #AI应用开发

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!