第2134篇:LLM应用的异常处理与降级策略——AI挂了,系统不能跟着挂
2026/4/30大约 7 分钟
第2134篇:LLM应用的异常处理与降级策略——AI挂了,系统不能跟着挂
适读人群:负责LLM应用可靠性的后端工程师 | 阅读时长:约18分钟 | 核心价值:建立完整的LLM应用容错体系,让AI功能降级而不是崩溃,保证核心业务流程不中断
"OpenAI今天又挂了,我们整个客服系统全部不可用,老板炸了。"
这个故事不陌生。公有云LLM服务有SLA,但99.9%的可用性意味着每月可以有8.7小时不可用。而且遇到突发事件(黑色星期五、重大事故),LLM服务的超时率会飙升。
如果你的核心业务流程依赖LLM,没有降级策略就是在裸奔。这篇文章讲如何构建LLM应用的容错体系。
LLM应用的故障模式
/**
* LLM应用可能遇到的故障类型
*
* ===== 类型一:API不可用 =====
*
* 症状:HTTP 503/500,连接超时
* 原因:LLM服务商宕机、网络故障
* 频率:罕见但影响大
* 处理:切换到备用模型/服务商,或降级到规则引擎
*
* ===== 类型二:速率限制(Rate Limit)=====
*
* 症状:HTTP 429,Too Many Requests
* 原因:请求量超过配额
* 频率:常见,特别是高峰期
* 处理:指数退避重试,或降级到缓存/简化模式
*
* ===== 类型三:超时 =====
*
* 症状:请求超时(设定的deadline到了还没响应)
* 原因:模型繁忙、输出太长
* 频率:偶发
* 处理:重试(不同参数),降级到简化模式
*
* ===== 类型四:输出质量不达标 =====
*
* 症状:返回了但结果不符合预期(格式错、内容空)
* 原因:Prompt问题、模型问题
* 频率:低概率但持续存在
* 处理:自动重试,或降级到规则引擎
*
* ===== 类型五:上下文超长 =====
*
* 症状:HTTP 400,Context length exceeded
* 原因:输入token超过模型上限
* 频率:特定场景下频繁
* 处理:截断输入,重新提交
*/多层容错策略实现
/**
* LLM调用的多层容错策略
*
* 层次:
* 1. 智能重试(Retry with backoff)
* 2. 多模型容灾(Failover)
* 3. 功能降级(Graceful Degradation)
* 4. 断路器(Circuit Breaker)
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class FaultTolerantLlmService {
private final Map<String, ChatLanguageModel> modelInstances;
private final LlmCircuitBreaker circuitBreaker;
private final FallbackResponseProvider fallbackProvider;
// 主模型优先列表
private static final List<String> MODEL_PRIORITY = List.of(
"gpt-4o", // 主力模型
"gpt-4o-mini", // 降级模型(便宜,质量略低)
"claude-3-haiku" // 最后备用
);
/**
* 带完整容错的LLM调用
*
* @param primaryModel 首选模型
* @param messages 消息列表
* @param fallbackKey 降级场景的key(用于获取兜底回答)
*/
public LlmCallResult callWithFallback(
String primaryModel,
List<ChatMessage> messages,
String fallbackKey) {
// 1. 检查断路器
if (circuitBreaker.isOpen(primaryModel)) {
log.warn("断路器打开,跳过主模型: model={}", primaryModel);
return tryFallbackModels(messages, primaryModel, fallbackKey);
}
// 2. 尝试主模型(带重试)
try {
String response = callWithRetry(primaryModel, messages, 2);
circuitBreaker.recordSuccess(primaryModel);
return LlmCallResult.success(response, primaryModel, false);
} catch (RateLimitException e) {
log.warn("主模型速率限制: model={}", primaryModel);
circuitBreaker.recordRateLimit(primaryModel);
return tryFallbackModels(messages, primaryModel, fallbackKey);
} catch (ModelUnavailableException e) {
log.error("主模型不可用: model={}", primaryModel, e);
circuitBreaker.recordFailure(primaryModel);
return tryFallbackModels(messages, primaryModel, fallbackKey);
} catch (ContextLengthExceededException e) {
log.warn("上下文超长,尝试截断重试: tokens={}", e.getActualTokens());
return callWithTruncation(primaryModel, messages, fallbackKey, e.getMaxTokens());
}
}
/**
* 带指数退避的重试
*/
private String callWithRetry(String modelId, List<ChatMessage> messages, int maxRetries) {
ChatLanguageModel model = modelInstances.get(modelId);
if (model == null) throw new ModelUnavailableException("模型不存在: " + modelId);
int retryCount = 0;
long waitMs = 1000; // 初始等待1秒
while (true) {
try {
long startMs = System.currentTimeMillis();
String response = model.generate(messages);
log.debug("LLM调用成功: model={}, latency={}ms",
modelId, System.currentTimeMillis() - startMs);
return response;
} catch (Exception e) {
retryCount++;
if (isRetryable(e) && retryCount <= maxRetries) {
log.warn("LLM调用失败,第{}次重试(等待{}ms): model={}, error={}",
retryCount, waitMs, modelId, e.getMessage());
try {
Thread.sleep(waitMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断");
}
waitMs = Math.min(waitMs * 2, 30_000); // 最多等30秒
} else {
throw mapException(e); // 不可重试或超过最大重试次数
}
}
}
}
/**
* 尝试备用模型
*/
private LlmCallResult tryFallbackModels(
List<ChatMessage> messages, String failedModel, String fallbackKey) {
// 找优先级更低的备用模型
List<String> fallbackModels = MODEL_PRIORITY.stream()
.filter(m -> !m.equals(failedModel))
.toList();
for (String fallbackModel : fallbackModels) {
if (circuitBreaker.isOpen(fallbackModel)) continue;
try {
log.info("切换到备用模型: {} → {}", failedModel, fallbackModel);
String response = callWithRetry(fallbackModel, messages, 1);
circuitBreaker.recordSuccess(fallbackModel);
return LlmCallResult.success(response, fallbackModel, true); // isDegraded=true
} catch (Exception e) {
log.warn("备用模型也失败: model={}", fallbackModel, e);
circuitBreaker.recordFailure(fallbackModel);
}
}
// 所有模型都失败,使用规则引擎兜底
return getStaticFallback(fallbackKey);
}
/**
* 截断上下文后重试
*/
private LlmCallResult callWithTruncation(
String modelId, List<ChatMessage> messages,
String fallbackKey, int maxTokens) {
// 截断消息历史(保留系统消息和最新的用户消息)
List<ChatMessage> truncated = truncateMessages(messages, maxTokens - 1000);
try {
String response = callWithRetry(modelId, truncated, 1);
return LlmCallResult.success(response, modelId, false);
} catch (Exception e) {
return tryFallbackModels(truncated, modelId, fallbackKey);
}
}
private List<ChatMessage> truncateMessages(List<ChatMessage> messages, int maxTokens) {
// 保留系统消息,从最新的用户消息往前保留,直到token限制
// 实现略
return messages;
}
/**
* 静态兜底响应(所有LLM都失败时)
*/
private LlmCallResult getStaticFallback(String fallbackKey) {
String fallbackResponse = fallbackProvider.getFallbackResponse(fallbackKey);
log.warn("所有LLM均不可用,使用兜底响应: fallbackKey={}", fallbackKey);
return LlmCallResult.fallback(fallbackResponse);
}
private boolean isRetryable(Exception e) {
// 5xx错误、超时、速率限制可以重试
// 4xx(除了429)通常不可重试
return e instanceof RateLimitException ||
e instanceof TimeoutException ||
(e instanceof HttpException he && he.statusCode() >= 500);
}
private RuntimeException mapException(Exception e) {
if (e instanceof RateLimitException) return (RateLimitException) e;
if (e instanceof ContextLengthExceededException) return (ContextLengthExceededException) e;
return new ModelUnavailableException("LLM调用失败", e);
}
@Data
@Builder
public static class LlmCallResult {
private boolean success;
private String response;
private String modelUsed;
private boolean isDegraded; // 是否使用了降级模型/兜底
private boolean isStaticFallback; // 是否是静态兜底
public static LlmCallResult success(String response, String model, boolean isDegraded) {
return LlmCallResult.builder()
.success(true).response(response)
.modelUsed(model).isDegraded(isDegraded).build();
}
public static LlmCallResult fallback(String response) {
return LlmCallResult.builder()
.success(false).response(response)
.isStaticFallback(true).build();
}
}
}断路器实现
/**
* LLM断路器
*
* 防止对持续失败的模型重复调用
* 自动恢复机制
*/
@Component
@Slf4j
public class LlmCircuitBreaker {
private final Map<String, CircuitState> states = new ConcurrentHashMap<>();
// 断路器配置
private static final int FAILURE_THRESHOLD = 5; // 5次失败后打开
private static final int RATE_LIMIT_THRESHOLD = 3; // 3次限流后打开
private static final Duration HALF_OPEN_TIMEOUT = Duration.ofSeconds(30); // 30秒后尝试半开
public boolean isOpen(String modelId) {
CircuitState state = states.computeIfAbsent(modelId, k -> new CircuitState());
if (state.status == Status.OPEN) {
// 检查是否可以进入半开状态
if (Duration.between(state.openedAt, LocalDateTime.now())
.compareTo(HALF_OPEN_TIMEOUT) > 0) {
state.status = Status.HALF_OPEN;
log.info("断路器进入半开状态: model={}", modelId);
return false;
}
return true;
}
return false;
}
public void recordSuccess(String modelId) {
CircuitState state = states.computeIfAbsent(modelId, k -> new CircuitState());
state.failureCount = 0;
state.rateLimitCount = 0;
if (state.status == Status.HALF_OPEN) {
state.status = Status.CLOSED;
log.info("断路器关闭(恢复): model={}", modelId);
}
}
public void recordFailure(String modelId) {
CircuitState state = states.computeIfAbsent(modelId, k -> new CircuitState());
state.failureCount++;
if (state.failureCount >= FAILURE_THRESHOLD) {
openCircuit(modelId, state);
}
}
public void recordRateLimit(String modelId) {
CircuitState state = states.computeIfAbsent(modelId, k -> new CircuitState());
state.rateLimitCount++;
if (state.rateLimitCount >= RATE_LIMIT_THRESHOLD) {
openCircuit(modelId, state);
}
}
private void openCircuit(String modelId, CircuitState state) {
state.status = Status.OPEN;
state.openedAt = LocalDateTime.now();
log.warn("断路器打开: model={}, failures={}, rateLimits={}",
modelId, state.failureCount, state.rateLimitCount);
}
enum Status { CLOSED, OPEN, HALF_OPEN }
static class CircuitState {
volatile Status status = Status.CLOSED;
volatile int failureCount = 0;
volatile int rateLimitCount = 0;
volatile LocalDateTime openedAt;
}
}实践建议
降级不是失败,是设计
很多工程师把"服务降级"看成失败的标志,实际上恰恰相反:能优雅降级说明系统有弹性、有容错设计。对用户来说,看到"当前AI助手响应较慢,已为您提供基本解答,稍后可获取完整AI回答",远比看到"系统错误500"体验好得多。降级策略要在系统设计阶段就规划好,不要等出了问题再临时想。
每种故障模式需要不同的处理策略
Rate limit(429)应该等待然后重试,不应该直接切换模型;模型不可用(503)应该直接切换模型,不应该无限重试;上下文超长(400)应该截断输入重试,不是切换模型能解决的。统一用一个"catch Exception, retry"来处理所有异常,是最常见的错误。细分故障类型,针对性处理,才能在提高成功率的同时避免浪费重试次数和成本。
备用回答要提前准备,不能临时凑
所谓"静态兜底响应",是提前为每个业务场景准备好的兜底文案。不是让AI实时生成(AI都挂了,怎么实时生成)。比如:客服场景兜底是"您好,AI助手暂时不可用,请点击人工客服或稍后重试";知识问答兜底是"当前系统维护中,请稍后访问或查阅帮助文档"。这些要和PM一起设计,确保在各个场景下都有可接受的用户体验。
