第1738篇:AI功能的渐进式增强——降级策略保证基础功能在模型不可用时正常工作
第1738篇:AI功能的渐进式增强——降级策略保证基础功能在模型不可用时正常工作
2023年某个周五晚上,我们的生产环境突然收到大量报警:AI功能请求全部超时,错误率飙到90%。
原因是上游 LLM 服务商当晚做了不预期的维护,所有 API 调用全部 504。
更糟糕的是,我们的产品里有几个核心功能强依赖 AI——AI不可用,这些功能完全用不了,用户看到的是白屏和"服务异常"的提示。
那晚紧急处理完之后,我反思了很久:一个成熟的AI产品,应该在模型不可用的时候,仍然能提供基础价值,这就是渐进式增强和降级策略要解决的问题。
渐进式增强的核心思路
渐进式增强在传统 Web 开发里是个老概念:先实现基础功能,再用更好的技术来增强体验。放到 AI 产品里,逻辑是一样的:
关键原则是:层之间要有明确的降级路径,上层失败自动回落到下层,用户感知要最小化。
降级策略的四个层次
层次1:完全降级——不依赖任何AI
每个AI功能,都要问自己:如果AI完全不存在,这个功能的最基础版本是什么?
比如"AI智能搜索",降级版本就是普通关键词搜索。 比如"AI写作建议",降级版本就是不显示建议,用户自己写。 比如"AI问答",降级版本可能是FAQ列表或者人工客服入口。
层次2:缓存降级——用历史好回复代替实时生成
很多问题是高频的,用户问的问题往往高度重复。可以把历史的高质量回复缓存起来,模型不可用时优先查缓存:
@Service
public class CachedResponseFallback {
@Autowired
private VectorSearchService vectorSearchService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final double SIMILARITY_THRESHOLD = 0.85;
/**
* 从缓存中找最相似的历史回复
*/
public Optional<CachedResponse> findSimilarCachedResponse(String query) {
// 先查精确匹配缓存(高频完全相同的问题)
String exactCacheKey = "cache:exact:" + DigestUtils.md5DigestAsHex(query.getBytes());
String exactMatch = redisTemplate.opsForValue().get(exactCacheKey);
if (exactMatch != null) {
return Optional.of(CachedResponse.builder()
.content(exactMatch)
.similarity(1.0)
.isExactMatch(true)
.build());
}
// 再查语义相似的
List<SimilarResult> similar = vectorSearchService.searchSimilar(query, 5);
return similar.stream()
.filter(r -> r.getSimilarity() >= SIMILARITY_THRESHOLD)
.findFirst()
.map(r -> CachedResponse.builder()
.content(r.getContent())
.similarity(r.getSimilarity())
.isExactMatch(false)
.build());
}
/**
* 将高质量回复存入缓存
*/
public void cacheHighQualityResponse(String query, String response, double qualityScore) {
if (qualityScore < 7.0) return; // 只缓存高质量回复
// 精确匹配缓存
String exactKey = "cache:exact:" + DigestUtils.md5DigestAsHex(query.getBytes());
redisTemplate.opsForValue().set(exactKey, response, Duration.ofDays(7));
// 同时存入向量索引(用于语义搜索)
vectorSearchService.index(query, response, qualityScore);
}
}层次3:轻量模型降级——本地或更便宜的模型
当主模型不可用时,切换到更小的本地模型或更便宜的备用模型。质量低一些,但总比完全不可用好:
@Service
public class ModelFallbackChain {
// 按优先级排列的模型列表
private final List<ModelConfig> modelChain = Arrays.asList(
ModelConfig.builder()
.name("gpt-4o")
.type(ModelType.CLOUD_PRIMARY)
.maxRetries(2)
.timeoutMs(30000)
.build(),
ModelConfig.builder()
.name("gpt-3.5-turbo")
.type(ModelType.CLOUD_FALLBACK)
.maxRetries(2)
.timeoutMs(20000)
.build(),
ModelConfig.builder()
.name("llama-3-8b-local") // 本地部署的小模型
.type(ModelType.LOCAL)
.maxRetries(1)
.timeoutMs(60000)
.build()
);
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
/**
* 按照模型链自动降级
*/
public ModelResponse generateWithFallback(String prompt, GenerationOptions options) {
for (ModelConfig model : modelChain) {
CircuitBreaker breaker = circuitBreakerRegistry.circuitBreaker(model.getName());
// 如果熔断器开启,跳过这个模型
if (breaker.getState() == CircuitBreaker.State.OPEN) {
log.info("模型 {} 熔断器开启,跳过", model.getName());
continue;
}
try {
ModelResponse response = breaker.executeCallable(() ->
callModel(model, prompt, options)
);
// 记录使用了哪个模型(透明地告知用户或用于监控)
response.setActualModel(model.getName());
response.setWasFallback(model != modelChain.get(0));
return response;
} catch (Exception e) {
log.warn("模型 {} 调用失败,尝试下一个: {}", model.getName(), e.getMessage());
}
}
// 所有模型都失败了,抛出明确的异常
throw new AllModelsUnavailableException("所有模型都不可用,请稍后重试");
}
}层次4:规则引擎降级——用业务规则代替模型
有些场景,可以用预先编写的规则来部分替代模型的功能。比如意图识别,可以有基于关键词的轻量规则版本:
@Service
public class RuleBasedIntentRecognizer {
// 意图 -> 触发关键词列表
private static final Map<String, List<String>> INTENT_RULES = Map.of(
"QUERY_ORDER", Arrays.asList("订单", "快递", "发货", "物流", "查快递"),
"RETURN_REFUND", Arrays.asList("退款", "退货", "换货", "不想要了"),
"PRODUCT_INQUIRY", Arrays.asList("多少钱", "价格", "规格", "参数", "怎么样"),
"COMPLAINT", Arrays.asList("投诉", "差评", "不满意", "骗人", "假货"),
"GREETING", Arrays.asList("你好", "在吗", "hello", "hi")
);
/**
* 规则版本的意图识别,当 NLP 模型不可用时使用
*/
public IntentResult recognizeByRules(String userInput) {
String input = userInput.toLowerCase();
for (Map.Entry<String, List<String>> entry : INTENT_RULES.entrySet()) {
for (String keyword : entry.getValue()) {
if (input.contains(keyword)) {
return IntentResult.builder()
.intent(entry.getKey())
.confidence(0.7) // 规则匹配固定置信度
.method("RULE_BASED")
.build();
}
}
}
return IntentResult.builder()
.intent("UNKNOWN")
.confidence(0.0)
.method("RULE_BASED")
.build();
}
}熔断器:让降级自动化
熔断器(Circuit Breaker)是降级自动化的核心组件。用 Resilience4j 实现:
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
// 主模型的熔断配置
io.github.resilience4j.circuitbreaker.CircuitBreakerConfig primaryModelConfig =
io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率超50%触发熔断
.waitDurationInOpenState(Duration.ofSeconds(60)) // 熔断60秒后尝试半开
.slidingWindowSize(20) // 基于最近20次请求计算失败率
.minimumNumberOfCalls(5) // 至少5次调用才开始统计
.permittedNumberOfCallsInHalfOpenState(3)
.recordExceptions(
IOException.class,
TimeoutException.class,
RateLimitException.class
)
.build();
return CircuitBreakerRegistry.of(Map.of(
"gpt-4o", primaryModelConfig,
"gpt-3.5-turbo", primaryModelConfig,
"llama-3-8b-local", io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.ofDefaults()
));
}
}配合监听器,在熔断状态变化时触发告警和降级通知:
@Component
public class CircuitBreakerEventListener {
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@Autowired
private AlertService alertService;
@PostConstruct
public void registerListeners() {
circuitBreakerRegistry.getAllCircuitBreakers().forEach(cb -> {
cb.getEventPublisher()
.onStateTransition(event -> {
log.warn("熔断器状态变更: {} {} -> {}",
cb.getName(),
event.getStateTransition().getFromState(),
event.getStateTransition().getToState());
if (event.getStateTransition().getToState() == CircuitBreaker.State.OPEN) {
alertService.sendCritical(
"模型服务熔断",
String.format("模型 %s 熔断器开启,已自动降级", cb.getName())
);
}
});
});
}
}降级时的用户体验设计
降级不是秘密,但也不需要大张旗鼓地宣告。好的做法是"静默降级,必要时告知":
@Service
public class DegradedResponseWrapper {
/**
* 包装降级响应,添加必要的用户提示
*/
public ApiResponse<String> wrap(
ModelResponse response,
DegradationLevel level) {
return switch (level) {
case NONE -> ApiResponse.success(response.getContent());
case MINOR -> ApiResponse.success(
response.getContent(),
"当前服务繁忙,可能影响回答质量"
);
case MODERATE -> ApiResponse.success(
response.getContent(),
"AI服务在恢复中,当前使用精简模式,部分功能受限"
);
case SEVERE -> ApiResponse.builder()
.success(true)
.data(response.getContent())
.degraded(true)
.degradedMessage("当前AI服务不可用,以下为历史相关回答,仅供参考")
.build();
case COMPLETE -> ApiResponse.builder()
.success(false)
.errorCode("AI_UNAVAILABLE")
.errorMessage("AI服务暂时不可用,请稍后重试。您也可以联系人工客服。")
.fallbackAction(FallbackAction.SHOW_MANUAL_ENTRY)
.build();
};
}
}降级策略的配置化
降级策略不能写死在代码里,要能动态配置和调整:
@Service
public class DegradationConfigService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String CONFIG_KEY = "ai:degradation:config";
@Data
@Builder
public static class DegradationConfig {
private boolean enableCacheFallback; // 是否启用缓存降级
private boolean enableLocalModelFallback; // 是否启用本地模型降级
private boolean enableRuleFallback; // 是否启用规则降级
private double cacheHitThreshold; // 缓存命中阈值,低于此值才调用模型
private int maxFallbackAgeMinutes; // 缓存降级时最多用多久前的缓存
// 不同功能的独立降级开关
private Map<String, Boolean> featureDegradationSwitches;
}
public DegradationConfig getConfig() {
String configJson = redisTemplate.opsForValue().get(CONFIG_KEY);
if (configJson == null) {
return defaultConfig();
}
try {
return objectMapper.readValue(configJson, DegradationConfig.class);
} catch (Exception e) {
return defaultConfig();
}
}
/**
* 运维人员可以通过这个接口动态调整降级策略
*/
@PutMapping("/admin/degradation-config")
@PreAuthorize("hasRole('ADMIN')")
public void updateConfig(@RequestBody DegradationConfig config) {
try {
String json = objectMapper.writeValueAsString(config);
redisTemplate.opsForValue().set(CONFIG_KEY, json);
log.info("降级配置已更新: {}", json);
} catch (JsonProcessingException e) {
throw new RuntimeException("配置序列化失败", e);
}
}
private DegradationConfig defaultConfig() {
return DegradationConfig.builder()
.enableCacheFallback(true)
.enableLocalModelFallback(true)
.enableRuleFallback(true)
.cacheHitThreshold(0.85)
.maxFallbackAgeMinutes(24 * 60) // 最多使用24小时前的缓存
.featureDegradationSwitches(new HashMap<>())
.build();
}
}完整的降级决策流程
把所有策略串起来,形成一个完整的降级决策树:
@Service
public class AIGatewayService {
@Autowired
private ModelFallbackChain modelFallbackChain;
@Autowired
private CachedResponseFallback cachedResponseFallback;
@Autowired
private RuleBasedIntentRecognizer ruleRecognizer;
@Autowired
private DegradationConfigService degradationConfig;
public AIResponse process(AIRequest request) {
DegradationConfigService.DegradationConfig config = degradationConfig.getConfig();
// Step 1: 先检查缓存(如果启用且命中率够高)
if (config.isEnableCacheFallback()) {
Optional<CachedResponse> cached = cachedResponseFallback
.findSimilarCachedResponse(request.getQuery());
if (cached.isPresent() && cached.get().getSimilarity() > 0.95) {
// 高相似度缓存命中,直接返回,不调用模型
return AIResponse.fromCache(cached.get());
}
}
// Step 2: 尝试调用模型链(含自动降级)
try {
ModelResponse modelResponse = modelFallbackChain
.generateWithFallback(request.getPrompt(), request.getOptions());
// 成功后缓存结果
if (config.isEnableCacheFallback()) {
cachedResponseFallback.cacheHighQualityResponse(
request.getQuery(), modelResponse.getContent(), 8.0
);
}
return AIResponse.fromModel(modelResponse);
} catch (AllModelsUnavailableException e) {
log.error("所有模型不可用,进入最终降级", e);
}
// Step 3: 所有模型都不可用,回退到缓存(放宽相似度阈值)
if (config.isEnableCacheFallback()) {
Optional<CachedResponse> cached = cachedResponseFallback
.findSimilarCachedResponse(request.getQuery());
if (cached.isPresent()) {
return AIResponse.fromDegradedCache(cached.get());
}
}
// Step 4: 最终兜底——返回规则处理结果或者明确的错误信息
if (config.isEnableRuleFallback()) {
return AIResponse.fromRules(handleByRules(request));
}
// Step 5: 完全兜底
return AIResponse.completelyUnavailable();
}
private RuleBasedResponse handleByRules(AIRequest request) {
IntentResult intent = ruleRecognizer.recognizeByRules(request.getQuery());
// 根据意图返回预设的规则响应
return ruleResponseRepository.findByIntent(intent.getIntent())
.orElse(RuleBasedResponse.generic());
}
}演练:模拟降级测试
再好的降级策略,不测试就是纸上谈兵。要定期做故障演练:
@Service
@ConditionalOnProperty(name = "chaos.engineering.enabled", havingValue = "true")
public class ChaosEngineeringService {
@Value("${chaos.engineering.model.failure.rate:0}")
private double modelFailureRate;
private final Random random = new Random();
/**
* 混沌工程:随机注入模型调用失败
* 只在非生产环境启用
*/
public boolean shouldInjectFailure(String modelName) {
if (modelFailureRate <= 0) return false;
return random.nextDouble() < modelFailureRate;
}
// 在拦截器里使用
@Around("execution(* com.example.service.ModelService.call(..))")
public Object injectChaos(ProceedingJoinPoint pjp) throws Throwable {
String modelName = extractModelName(pjp);
if (shouldInjectFailure(modelName)) {
log.info("[混沌工程] 注入模型调用失败: {}", modelName);
throw new RuntimeException("混沌工程:模拟模型调用失败");
}
return pjp.proceed();
}
}在 staging 环境定期跑混沌测试,验证降级路径是否真的能走通。
总结
降级策略的本质是承认不确定性,提前设计应对方案。AI 服务比传统服务更不稳定,这不是借口,而是我们必须在设计上考虑的事实。
构建健壮的降级体系:
- 四层防护:完全降级 → 缓存降级 → 轻量模型 → 规则降级
- 自动熔断:Resilience4j 自动检测故障并触发降级
- 配置化:降级策略可以动态调整,不需要发布代码
- 用户感知最小化:能静默降级就静默,实在不行才告知用户
- 定期演练:混沌工程确保降级路径真实有效
最后一句话:在AI故障时能优雅降级的产品,和直接崩掉的产品,用户留存率差距会非常大。这不是技术问题,是产品生死存亡的问题。
