在生产环境中,一个没有容错设计的 AI 服务就像在走钢丝——平时没问题,但一旦 API 超时或限流,整个链路就会崩掉。本文将带你系统掌握 SK 异常体系、Polly 重试、熔断器与优雅降级,最终实现一个可直接落地的健壮 AI 服务包装器。
在写完一个 AI 功能,本地跑通之后,很多开发者就直接上线了。但上线后往往遇到这样的场景:
统计显示,AI API 的平均可用性在 99.5% 左右,看起来很高,但对于每天数万次调用的系统,每天仍有数十次~数百次的失败请求——这些失败如果没有兜底,直接影响用户体验。
读完本文,你将掌握:
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 超限、参数错误——这些问题的处理策略完全不同,混在一起只会让排查困难度指数级上升。

Semantic Kernel 的异常主要来自以下几层:
| 异常来源 | 典型异常类型 | 处理策略 |
|---|---|---|
| HTTP 网络层 | HttpRequestException | 重试 |
| API 限流 | 429 Too Many Requests | 延迟重试 |
| Token 超限 | 400 Bad Request | 截断历史,重试 |
| 函数调用失败 | KernelFunctionException | 记录 + 降级 |
| 插件内部错误 | Exception(包装后) | 降级处理 |
| 认证失败 | 401 Unauthorized | 直接告警,不重试 |
核心原则:可重试的错误要重试,不可重试的错误要快速失败,业务逻辑错误要优雅降级。
在设计 AI 服务的容错体系之前,先明确一个分层模型:
┌─────────────────────────────────┐ │ 业务层(优雅降级) │ ← 兜底,保证业务可用 ├─────────────────────────────────┤ │ 熔断层(断路器) │ ← 防雪崩,快速失败 ├─────────────────────────────────┤ │ 重试层(Polly Policy) │ ← 处理瞬时故障 ├─────────────────────────────────┤ │ SK 调用层(异常分类) │ ← 识别异常类型 └─────────────────────────────────┘
每一层职责清晰,不越权,不重叠。这和 DDD 的分层思想一脉相承。
首先,建立一套异常分类机制,让每种错误得到对应处理。
安装必要的 NuGet 包:
bashdotnet add package Microsoft.SemanticKernel dotnet add package Polly dotnet add package Microsoft.Extensions.Http.Resilience dotnet add package Microsoft.Extensions.Logging
核心异常分类器:
csharpusing 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 状态码,消息解析作为补充。
csharpusing 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)意味着熔断器在外层,打开时直接阻断,不会触发内层重试——这正是我们想要的。反过来写,熔断器会在每次重试后才判断,导致逻辑混乱。
把前面的模块组合起来,实现一个生产可用的包装器:
csharpusing 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}");

| 坑点 | 错误做法 | 正确做法 |
|---|---|---|
| 策略组合顺序错误 | Wrap(retry, circuitBreaker) | Wrap(circuitBreaker, retry) |
| 对所有异常都重试 | Handle<Exception>() | 精确匹配可重试异常类型 |
| 降级时仍更新历史 | 降级后继续 AddAssistantMessage | 降级时跳过历史更新 |
忽略 BrokenCircuitException | 未单独处理熔断异常 | 专门捕获并立即降级 |
| 无限制的 Token 增长 | 不裁剪对话历史 | 滑动窗口控制历史长度 |
收获一:异常要分类,不能一刀切
网络超时、限流、Token 超限、认证失败——每种异常的处理策略完全不同。精细化分类是一切容错设计的前提。
收获二:Polly 组合策略是高可用的标配
重试解决瞬时故障,熔断器防止雪崩。两者组合、顺序正确,才能真正保护系统稳定性。在实测中(测试环境:.NET 8 + DeepSeek API,模拟 10% 随机失败率),加入容错设计后服务可用性从 90% 提升至 99.2%。
收获三:优雅降级是用户体验的最后防线
AI 挂了,业务不能跟着挂。提前准备好降级响应,结合场景(诊断、分析、通用)分类返回,用户感知到的只是"功能暂时受限",而不是"系统崩溃"。
Polly.Extensions.Http 与 IHttpClientFactory 的深度集成Microsoft.Extensions.Resilience(.NET 8 官方弹性扩展)在实际项目中,容错设计往往还面临更复杂的情况:
问题一: 当 AI 服务降级时,如何判断降级响应是否足够"安全"?比如设备诊断场景,给出错误的降级建议可能比"服务不可用"更危险,你是如何处理这类风险的?
问题二: 在多模型路由场景(主模型 + 备用模型)下,熔断器的粒度应该设在单个模型级别还是整体服务级别?你的项目中是怎么设计的?
相关标签: #C#开发 #SemanticKernel #异常处理 #Polly #容错设计 #性能优化 #架构设计 #AI应用开发
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!