第1923篇:服务降级的AI感知设计——不同级别模型的优雅降级链
第1923篇:服务降级的AI感知设计——不同级别模型的优雅降级链
有一次深夜值班,我们的大模型服务宕了。
那是一个给客服系统提供自动回复的AI服务,底层是自研的7B参数模型,部署在几台A100上。凌晨两点多,一台GPU服务器硬件故障,直接带走了两个AI服务实例。剩下的实例瞬间被涌入的请求打爆,开始大量超时。
当时我们的降级策略是:AI服务挂了,就走规则引擎。但规则引擎是基于关键词匹配的老系统,准确率很差,客服主管直接打电话来投诉"机器人完全乱回答了"。
事后我反思:从7B模型直接降级到关键词匹配,这个跨度太大了。中间完全可以有更细粒度的降级链——比如先降级到1.3B的小模型,再降级到基于检索的模板匹配,最后才是纯规则。每一级的能力边界都比下一级强,用户体验的下降是渐进的,而不是悬崖式的。
这就是今天要讲的:AI感知的服务降级,或者叫"模型降级链"。
降级的本质:能力边界的有序退出
普通微服务的降级通常是二元的:服务可用 → 返回兜底值/默认数据。但AI服务的"输出质量"是连续可调的,这给了我们更大的设计空间。
想象一个NLP任务的能力谱:
每一级降级,都是在"质量"和"可用性"之间做取舍。设计降级链的关键是:
- 每一级都能独立提供有意义的服务,不能是"降了等于没有"
- 降级触发条件要精准,避免误降级
- 降级是可逆的,当上级服务恢复时,能自动升回去
- 降级对调用方透明,调用方不需要关心当前在用哪一级
整体架构:降级链协调器
核心是一个DegradationChain(降级链)协调器:
@Component
public class DegradationChain {
private final List<ModelLevel> levels;
private final CircuitBreakerRegistry circuitBreakerRegistry;
private final DegradationMetrics metrics;
// 每个实例维护当前生效的级别
private final AtomicInteger currentLevel = new AtomicInteger(0);
public DegradationChain(List<ModelLevel> levels,
CircuitBreakerRegistry circuitBreakerRegistry,
DegradationMetrics metrics) {
// 按级别从高到低排列(0=最强,levels.size()-1=最弱)
this.levels = levels;
this.circuitBreakerRegistry = circuitBreakerRegistry;
this.metrics = metrics;
}
public AIResponse execute(AIRequest request) {
int startLevel = currentLevel.get();
for (int level = startLevel; level < levels.size(); level++) {
ModelLevel modelLevel = levels.get(level);
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker(modelLevel.getName());
try {
AIResponse response = cb.executeCallable(() ->
modelLevel.execute(request, level)
);
// 执行成功
metrics.recordSuccess(level, modelLevel.getName());
// 如果当前在降级状态,尝试在后台恢复
if (level > 0) {
response.setDegradedLevel(level);
response.setDegradedReason(modelLevel.getName());
scheduleRecoveryProbe(startLevel);
}
return response;
} catch (Exception e) {
log.warn("级别{}({})执行失败: {}, 尝试降级",
level, modelLevel.getName(), e.getMessage());
metrics.recordFailure(level, modelLevel.getName(), e);
// 更新当前生效级别
if (level + 1 < levels.size()) {
currentLevel.compareAndSet(level, level + 1);
}
}
}
// 所有级别都失败了,返回兜底响应
metrics.recordFullDegradation();
return buildFallbackResponse(request);
}
private void scheduleRecoveryProbe(int targetLevel) {
// 每隔一段时间探测上级是否恢复
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(30_000); // 30秒后探测
ModelLevel targetModelLevel = levels.get(targetLevel);
boolean recovered = targetModelLevel.healthCheck();
if (recovered) {
currentLevel.compareAndSet(currentLevel.get(), targetLevel);
log.info("服务已恢复到级别{}", targetLevel);
metrics.recordRecovery(targetLevel);
}
} catch (Exception e) {
// 探测失败,下次再试
}
});
}
private AIResponse buildFallbackResponse(AIRequest request) {
AIResponse response = new AIResponse();
response.setContent("抱歉,服务暂时不可用,请稍后重试");
response.setFallback(true);
response.setDegradedLevel(-1);
return response;
}
}各级别的实现
第一级:大模型(正常服务)
@Component
public class LargeModelLevel implements ModelLevel {
@Autowired
private LargeModelClient largeModelClient;
private static final Duration TIMEOUT = Duration.ofSeconds(30);
private static final int PRIORITY = 0;
@Override
public String getName() {
return "large-model-72b";
}
@Override
public int getPriority() {
return PRIORITY;
}
@Override
public AIResponse execute(AIRequest request, int currentLevel) {
// 大模型调用完整功能
LargeModelRequest modelRequest = LargeModelRequest.builder()
.model("qwen-72b")
.messages(buildMessages(request))
.temperature(0.7)
.maxTokens(request.getMaxTokens())
.timeout(TIMEOUT)
.build();
LargeModelResponse response = largeModelClient.chat(modelRequest);
return AIResponse.builder()
.content(response.getContent())
.modelUsed("qwen-72b")
.tokensUsed(response.getUsage().getTotalTokens())
.build();
}
@Override
public boolean healthCheck() {
try {
AIRequest probe = AIRequest.builder()
.prompt("你好")
.maxTokens(10)
.build();
execute(probe, PRIORITY);
return true;
} catch (Exception e) {
return false;
}
}
}第二级:中型模型(轻度降级)
@Component
public class MediumModelLevel implements ModelLevel {
@Autowired
private MediumModelClient mediumModelClient;
private static final Duration TIMEOUT = Duration.ofSeconds(10);
@Override
public String getName() {
return "medium-model-13b";
}
@Override
public int getPriority() {
return 1;
}
@Override
public AIResponse execute(AIRequest request, int currentLevel) {
// 中型模型,参数少一些,但基本功能完整
// 降级时可以适当削减输出长度
int adjustedMaxTokens = request.getMaxTokens() != null
? Math.min(request.getMaxTokens(), 512) // 降级时限制输出长度
: 256;
MediumModelRequest modelRequest = MediumModelRequest.builder()
.model("qwen-13b")
.prompt(request.getPrompt())
.maxTokens(adjustedMaxTokens)
.temperature(0.6) // 降低随机性,更稳定
.timeout(TIMEOUT)
.build();
MediumModelResponse response = mediumModelClient.generate(modelRequest);
return AIResponse.builder()
.content(response.getText())
.modelUsed("qwen-13b")
.degradedLevel(1)
.build();
}
@Override
public boolean healthCheck() {
// 同上,略
return true;
}
}第三级:小模型(中度降级)
@Component
public class SmallModelLevel implements ModelLevel {
@Autowired
private SmallModelClient smallModelClient;
@Override
public String getName() {
return "small-model-1b3";
}
@Override
public int getPriority() {
return 2;
}
@Override
public AIResponse execute(AIRequest request, int currentLevel) {
// 小模型,速度快,但质量下降
// 对prompt做简化,避免超过小模型的上下文限制
String simplifiedPrompt = simplifyPrompt(request.getPrompt(), 512);
SmallModelRequest modelRequest = new SmallModelRequest(
simplifiedPrompt, 128, 0.5
);
String result = smallModelClient.generate(modelRequest);
return AIResponse.builder()
.content(result)
.modelUsed("qwen-1.8b")
.degradedLevel(2)
.build();
}
private String simplifyPrompt(String originalPrompt, int maxChars) {
if (originalPrompt.length() <= maxChars) {
return originalPrompt;
}
// 截断并添加提示
return originalPrompt.substring(0, maxChars) +
"\n(请基于以上内容简要回复)";
}
@Override
public boolean healthCheck() {
return true;
}
}第四级:向量检索模板匹配(重度降级)
这一级是AI服务降级链里的关键设计——不是纯规则,而是基于向量检索的模板匹配,能处理语义相近但字面不同的查询:
@Component
public class RetrievalMatchingLevel implements ModelLevel {
@Autowired
private VectorSearchClient vectorSearch;
@Autowired
private EmbeddingService embeddingService;
@Autowired
private TemplateRepository templateRepo;
@Override
public String getName() {
return "retrieval-template-matching";
}
@Override
public int getPriority() {
return 3;
}
@Override
public AIResponse execute(AIRequest request, int currentLevel) {
try {
// 将用户请求向量化
float[] queryVector = embeddingService.embedLocal(request.getPrompt());
// 注意:这里用的是本地embedding模型(比如m3e-base),不依赖大模型
// 在预构建的知识库中检索最相似的模板
List<TemplateMatch> matches = vectorSearch.search(
queryVector,
TopKQuery.builder()
.topK(3)
.minScore(0.75f) // 相似度低于0.75不用
.build()
);
if (matches.isEmpty()) {
// 没有高置信度匹配,返回通用兜底
return buildGenericResponse(request);
}
// 取最高分的模板
TemplateMatch best = matches.get(0);
String response = renderTemplate(best.getTemplate(), request);
return AIResponse.builder()
.content(response)
.modelUsed("retrieval-template")
.degradedLevel(3)
.matchScore(best.getScore())
.build();
} catch (Exception e) {
log.error("检索匹配失败: {}", e.getMessage());
return buildGenericResponse(request);
}
}
private String renderTemplate(ResponseTemplate template, AIRequest request) {
// 简单的模板变量替换
String content = template.getContent();
// 从请求中提取关键词填充模板变量
Map<String, String> variables = extractVariables(request.getPrompt());
for (Map.Entry<String, String> var : variables.entrySet()) {
content = content.replace("${" + var.getKey() + "}", var.getValue());
}
return content;
}
private Map<String, String> extractVariables(String text) {
// 这里可以用简单的NER或关键词提取
// 实际实现省略
return new HashMap<>();
}
private AIResponse buildGenericResponse(AIRequest request) {
// 通用兜底,通常根据任务类型给不同的友好提示
String message = switch (request.getTaskType()) {
case "customer_service" -> "您好,您的问题我已记录,客服人员将在24小时内联系您";
case "content_generation" -> "抱歉,内容生成服务暂时不可用,请稍后再试";
default -> "服务暂时繁忙,请稍后重试";
};
return AIResponse.builder()
.content(message)
.modelUsed("fallback")
.degradedLevel(4)
.build();
}
@Override
public boolean healthCheck() {
return vectorSearch.isAvailable() && embeddingService.isLocalAvailable();
}
}熔断器的差异化配置
不同级别的熔断器配置应该不同,越低级的服务应该越稳定,熔断阈值应该越松:
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
Map<String, CircuitBreakerConfig> configs = new HashMap<>();
// 大模型:允许较高失败率,因为它本身较慢也较贵
configs.put("large-model-72b", CircuitBreakerConfig.custom()
.failureRateThreshold(30) // 失败率30%触发熔断
.slowCallRateThreshold(50) // 慢调用率50%触发熔断
.slowCallDurationThreshold(Duration.ofSeconds(25)) // 超25秒算慢
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断30秒后半开
.slidingWindowSize(20)
.minimumNumberOfCalls(5)
.build());
// 中型模型:适中配置
configs.put("medium-model-13b", CircuitBreakerConfig.custom()
.failureRateThreshold(40)
.slowCallRateThreshold(50)
.slowCallDurationThreshold(Duration.ofSeconds(8))
.waitDurationInOpenState(Duration.ofSeconds(15))
.slidingWindowSize(20)
.minimumNumberOfCalls(5)
.build());
// 小模型:更宽松,快速失败
configs.put("small-model-1b3", CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slowCallDurationThreshold(Duration.ofSeconds(3))
.waitDurationInOpenState(Duration.ofSeconds(10))
.slidingWindowSize(10)
.minimumNumberOfCalls(3)
.build());
// 检索匹配:最宽松,这一级必须极度稳定
configs.put("retrieval-template-matching", CircuitBreakerConfig.custom()
.failureRateThreshold(70)
.slowCallDurationThreshold(Duration.ofSeconds(1))
.waitDurationInOpenState(Duration.ofSeconds(5))
.slidingWindowSize(10)
.minimumNumberOfCalls(3)
.build());
return CircuitBreakerRegistry.of(configs);
}
}降级感知:让调用方知道当前状态
降级发生了,调用方和运营人员需要知道。设计统一的降级响应头:
@Component
public class DegradationResponseDecorator {
public AIResponse decorate(AIResponse response, DegradationContext context) {
// 在响应中附加降级信息
if (response.isDegraded()) {
response.addMetadata("X-AI-Degraded", "true");
response.addMetadata("X-AI-Degraded-Level",
String.valueOf(response.getDegradedLevel()));
response.addMetadata("X-AI-Degraded-Model",
response.getDegradedReason());
response.addMetadata("X-AI-Quality-Score",
estimateQualityScore(response.getDegradedLevel()));
}
return response;
}
private String estimateQualityScore(int level) {
// 各级别的质量分(主观估算,供调用方展示)
return switch (level) {
case 0 -> "1.0"; // 大模型,满分
case 1 -> "0.85"; // 中型模型
case 2 -> "0.65"; // 小模型
case 3 -> "0.45"; // 检索匹配
default -> "0.2"; // 兜底
};
}
}调用方可以根据降级级别做相应的UI处理,比如:
- 降级到小模型:正常展示结果,但在底部加一行小字"当前处于节能模式,回答质量可能略有下降"
- 降级到检索匹配:展示结果,但主动引导用户去人工客服
- 完全兜底:直接显示"系统繁忙,已为您转接人工"
降级触发的智能判断
不是所有失败都应该触发降级。有些失败是"请求本身的问题",而不是"服务的问题":
@Component
public class DegradationDecisionMaker {
public boolean shouldDegrade(Exception exception, AIRequest request) {
// 以下情况不降级,而是直接报错
// 1. 输入验证失败:不是服务的问题
if (exception instanceof InvalidInputException) {
return false;
}
// 2. 认证鉴权失败:不是服务的问题
if (exception instanceof AuthenticationException) {
return false;
}
// 3. 请求本身超长:小模型处理不了,降了也没意义
if (exception instanceof ContextTooLongException &&
request.getPrompt().length() > 8000) {
return false;
}
// 以下情况触发降级
// 4. 超时
if (exception instanceof TimeoutException) {
return true;
}
// 5. 服务不可用
if (exception instanceof ServiceUnavailableException) {
return true;
}
// 6. GPU内存不足
if (exception instanceof OOMException ||
exception.getMessage() != null &&
exception.getMessage().contains("CUDA out of memory")) {
return true;
}
// 7. 默认:未知异常降级
return true;
}
public DegradationLevel decideDegradationLevel(Exception e,
int currentLevel,
DegradationHistory history) {
// 如果近10分钟已经连续降级了,不要每次都只降一级,可以跳级
int recentDegradations = history.getRecentDegradationCount(Duration.ofMinutes(10));
if (recentDegradations > 5) {
// 情况很不好,直接跳到第三级
return DegradationLevel.of(Math.min(currentLevel + 2, 3));
}
return DegradationLevel.of(currentLevel + 1);
}
}主动降级:预防性地降级
被动降级(失败了才降)是最常见的,但高级的设计是主动降级——在压力上来之前提前降级,避免雪崩:
@Component
public class ProactiveDegradationManager {
@Autowired
private GpuMonitor gpuMonitor;
@Autowired
private DegradationChain degradationChain;
@Scheduled(fixedDelay = 5000) // 每5秒评估一次
public void evaluateAndAdjust() {
GpuMetrics metrics = gpuMonitor.getCurrentMetrics();
// GPU利用率超过90%:预降级到中型模型
if (metrics.getGpuUtilization() > 0.90) {
log.warn("GPU利用率{}%,预降级到中型模型", (int)(metrics.getGpuUtilization() * 100));
degradationChain.forceLevel(1);
return;
}
// GPU显存剩余不足2GB:强制降级
if (metrics.getFreeMemoryGb() < 2.0) {
log.warn("GPU显存不足{}GB,预降级到小模型", metrics.getFreeMemoryGb());
degradationChain.forceLevel(2);
return;
}
// 请求队列积压超过阈值:适度降级
int queueDepth = degradationChain.getQueueDepth();
if (queueDepth > 100) {
log.warn("请求队列积压{},降低服务级别", queueDepth);
degradationChain.forceLevel(Math.min(degradationChain.getCurrentLevel() + 1, 2));
return;
}
// 情况好转,尝试升级
if (metrics.getGpuUtilization() < 0.60 &&
metrics.getFreeMemoryGb() > 5.0 &&
queueDepth < 20) {
int current = degradationChain.getCurrentLevel();
if (current > 0) {
log.info("系统资源充裕,尝试升级服务级别");
degradationChain.tryUpgrade();
}
}
}
}降级日志与告警
降级事件必须被记录和告警,这对运维和优化都很重要:
@Component
@Slf4j
public class DegradationEventListener {
@Autowired
private AlertService alertService;
@EventListener
public void onDegradation(DegradationEvent event) {
String message = String.format(
"[AI服务降级] 时间:%s 从级别%d降级到级别%d 原因:%s 请求量:%d/分钟",
LocalDateTime.now(),
event.getFromLevel(),
event.getToLevel(),
event.getReason(),
event.getRequestRate()
);
log.warn(message);
// 降级到第3级(重度降级)需要立即告警
if (event.getToLevel() >= 3) {
alertService.sendPagerDutyAlert(message);
}
// 其他降级发送普通通知
else if (event.getToLevel() > 0) {
alertService.sendSlackNotification("#ai-ops", message);
}
}
@EventListener
public void onRecovery(DegradationRecoveryEvent event) {
String message = String.format(
"[AI服务恢复] 时间:%s 恢复到级别%d 停留时长:%d秒",
LocalDateTime.now(),
event.getToLevel(),
event.getDegradationDurationSeconds()
);
log.info(message);
alertService.sendSlackNotification("#ai-ops", message);
}
}踩坑:降级链的几个反直觉问题
问题一:降级不是越快越好
我曾经把熔断的失败率阈值设得很低(10%),结果网络抖动导致偶发失败,系统一直在降级和恢复之间来回横跳,反而造成了更差的用户体验。失败率阈值要设得适度保守,结合最小调用数使用。
问题二:不同任务类型的降级策略应该独立
"摘要生成"降级到小模型,输出质量下降有限;但"代码生成"降级到小模型,结果可能直接错误。应该为不同任务类型配置独立的降级链,或者对某些高敏感任务直接禁止降级到低级模型(宁可报错,也不给错误答案)。
问题三:降级要考虑上下文连续性
在多轮对话场景里,降级可能导致上下文理解能力下降。如果第1轮用大模型,第2轮降级到小模型,小模型可能无法正确理解前面的上下文。对话类场景,要么整个会话锁定一个级别,要么降级时主动提示用户"建议重新开始对话"。
小结
AI感知的服务降级,核心思路是把"服务可用性"从二元变成多级连续。
构建降级链时要注意三点:每一级都有独立价值、降级条件精准、恢复机制完善。
最重要的一条原则:降级链的最末端必须无限稳定。第四级、第五级服务不能依赖任何可能挂掉的组件,它们的存在意义就是在一切都挂掉时保住最后的体验底线。
下一篇聊分布式Session在多Agent系统中的管理,那个话题同样很有意思。
