Spring AI异常处理与重试机制
前置知识
- Spring AI基础调用
- Spring Retry机制
- 了解LLM API限流与错误码
- 熔断器模式基础
核心概念
LLM调用可能因网络、限流、错误输入等多种原因失败。Spring AI使用Spring Retry和自定义错误处理机制保证系统健壮性。生产环境需处理: API超时、Rate Limited(429)、认证失败、内容审核拒绝等多种异常类型。
异常类型分类
| 类型 | HTTP状态码 | 可重试 | 建议策略 |
|---|---|---|---|
| Rate Limited | 429 | 是 | 指数退避 |
| 服务器错误 | 500/502/503 | 是 | 固定间隔重试 |
| 请求超时 | 408 | 是 | 增加超时时间后重试 |
| 认证失败 | 401/403 | 否 | 立即告警 |
| 内容过滤 | 400 | 否 | 返回给用户 |
| 上下文超长 | 400 | 否 | 截断上下文后重试 |
完整实现
1. Spring AI统一异常处理
java
/**
* Spring AI异常基类层次
* - AiException
* - RuntimeException
* - 最终用户可见异常
*/
@RestControllerAdvice
@Slf4j
public class AiExceptionHandler {
@ExceptionHandler(RateLimitExceededException.class)
public ResponseEntity<ErrorResponse> handleRateLimit(
RateLimitExceededException e) {
log.warn("LLM限流: {}", e.getMessage());
return ResponseEntity.status(429)
.body(new ErrorResponse(
"RATE_LIMITED",
"请求频繁,请稍后重试",
Map.of("retry_after", e.getRetryAfterSeconds())
));
}
@ExceptionHandler(AiException.class)
public ResponseEntity<ErrorResponse> handleAiException(AiException e) {
log.error("AI调用异常", e);
return ResponseEntity.status(502)
.body(new ErrorResponse(
"AI_SERVICE_ERROR",
"AI服务暂时不可用",
Map.of("detail", e.getMessage())
));
}
@ExceptionHandler(ContentFilterException.class)
public ResponseEntity<ErrorResponse> handleContentFilter(
ContentFilterException e) {
return ResponseEntity.status(400)
.body(new ErrorResponse(
"CONTENT_FILTERED",
"输入内容不符合安全规范",
Map.of("reason", e.getFilterReason())
));
}
@ExceptionHandler(ContextTooLongException.class)
public ResponseEntity<ErrorResponse> handleContextTooLong(
ContextTooLongException e) {
return ResponseEntity.status(400)
.body(new ErrorResponse(
"CONTEXT_TOO_LONG",
"输入内容过长",
Map.of("max_tokens", e.getMaxTokens())
));
}
@ExceptionHandler(TimeoutException.class)
public ResponseEntity<ErrorResponse> handleTimeout(TimeoutException e) {
log.warn("LLM调用超时: {}", e.getMessage());
return ResponseEntity.status(504)
.body(new ErrorResponse(
"TIMEOUT",
"AI响应超时",
Map.of("timeout_ms", e.getTimeoutMs())
));
}
}
2. 智能重试配置
java
@Configuration
@EnableRetry
public class RetryConfig {
/**
* LLM调用重试配置
*/
@Bean
public RetryTemplate llmCallRetryTemplate() {
// 重试策略 - 针对可重试异常
Map<Class<? extends Throwable>, Boolean> retryable = Map.of(
ResourceAccessException.class, true, // 网络异常
HttpServerErrorException.class, true, // 服务端错误
TimeoutException.class, true, // 超时
RateLimitException.class, true, // 限流
AiException.class, false, // AI内部异常不重试
AuthenticationException.class, false // 认证异常不重试
);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(
3, retryable, true);
// 退避策略 - 指数退避
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(1000); // 初始间隔1秒
backOff.setMultiplier(2); // 倍数2
backOff.setMaxInterval(30000); // 最大间隔30秒
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(retryPolicy);
template.setBackOffPolicy(backOff);
// 注册监听器
template.registerListener(new RetryListener() {
@Override
public <T> boolean open(RetryContext context, RetryCallback<T> callback) {
log.info("开始LLM调用");
return true;
}
@Override
public <T> void close(RetryContext context, RetryCallback<T> callback,
Throwable throwable) {
if (throwable != null) {
log.error("LLM调用最终失败, 重试{}次后放弃",
context.getRetryCount());
} else if (context.getRetryCount() > 0) {
log.info("LLM调用在第{}次重试后成功",
context.getRetryCount());
}
}
@Override
public <T> void onError(RetryContext context, RetryCallback<T> callback,
Throwable throwable) {
log.warn("LLM调用失败 (第{}次): {}",
context.getRetryCount(), throwable.getMessage());
}
});
return template;
}
}
3. 带重试的调用服务
java
@Service
@Slf4j
public class ResilientChatService {
private final ChatClient chatClient;
private final RetryTemplate retryTemplate;
private final CircuitBreaker circuitBreaker;
public ResilientChatService(ChatClient chatClient,
RetryTemplate retryTemplate,
CircuitBreaker circuitBreaker) {
this.chatClient = chatClient;
this.retryTemplate = retryTemplate;
this.circuitBreaker = circuitBreaker;
}
/**
* 带重试和熔断的调用
*/
public String chatWithResilience(String message) {
return circuitBreaker.executeSupplier(() ->
retryTemplate.execute(context -> {
if (context.getRetryCount() > 0) {
log.info("第{}次重试调用LLM", context.getRetryCount());
}
return callLlm(message);
}, context -> {
log.error("LLM调用在{}次重试后均失败", context.getRetryCount());
return "抱歉,AI服务暂时不可用,请稍后重试";
})
);
}
/**
* 带分区重试 - 不同异常使用不同重试策略
*/
public String chatWithPartitionedRetry(String message) {
try {
// 第一次尝试 - 快速失败
return callLlm(message);
} catch (RateLimitException e) {
// 限流 - 等待后重试
return handleRateLimitRetry(message, e);
} catch (TimeoutException e) {
// 超时 - 增加超时时间后重试一次
return handleTimeoutRetry(message);
} catch (ResourceAccessException e) {
// 网络异常 - 最多重试2次
return handleNetworkRetry(message, 2);
} catch (ContentFilterException e) {
// 内容审核失败 - 不重试
throw e;
} catch (AuthenticationException e) {
// 认证失败 - 不重试,告警
alertService.sendAlert("LLM认证失败", e.getMessage());
throw e;
}
}
private String handleRateLimitRetry(String message, RateLimitException e) {
long waitSeconds = e.getRetryAfterSeconds();
if (waitSeconds > 30) {
throw new BusinessException("限流时间过长: " + waitSeconds + "秒");
}
log.info("限流等待{}秒后重试", waitSeconds);
try {
Thread.sleep(waitSeconds * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
return callLlm(message);
}
private String handleTimeoutRetry(String message) {
log.warn("LLM调用超时,增加超时时间后重试");
return chatClient.prompt()
.user(message)
.options(DashScopeChatOptions.builder()
.withTimeout(Duration.ofSeconds(120)) // 增加到120秒
.build())
.call()
.content();
}
private String handleNetworkRetry(String message, int maxRetries) {
for (int i = 0; i <= maxRetries; i++) {
try {
return callLlm(message);
} catch (ResourceAccessException e) {
if (i == maxRetries) throw e;
try {
Thread.sleep((long) Math.pow(2, i) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
throw new RuntimeException("网络重试失败");
}
/**
* 带降级策略的调用
*/
public String chatWithFallback(String message) {
try {
String response = callLlm(message);
// 清除失败计数
failureCount.set(0);
return response;
} catch (Exception e) {
int failures = failureCount.incrementAndGet();
if (failures >= 3) {
// 连续失败, 切换到简单回复模式
log.warn("连续{}次失败,使用降级响应", failures);
return generateDegradedResponse(message);
}
throw e;
}
}
/**
* 降级响应 - 不调用LLM的简单回复
*/
private String generateDegradedResponse(String message) {
// 简单的关键词匹配回复
if (message.contains("天气")) {
return "抱歉,天气服务暂时不可用,请稍后查询。";
}
if (message.contains("订单")) {
return "抱歉,订单系统暂时不可用,请稍后联系客服。";
}
return "抱歉,服务暂时繁忙,请稍后再试。我们的工程师正在紧急处理。";
}
private final AtomicInteger failureCount = new AtomicInteger(0);
private String callLlm(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
4. 自定义错误重试回调
java
/**
* 重试回调 - 记录和监控
*/
@Component
public class LlmRetryCallback extends RetryListenerSupport {
private final MeterRegistry meterRegistry;
private final NotificationService notificationService;
public LlmRetryCallback(MeterRegistry meterRegistry,
NotificationService notificationService) {
this.meterRegistry = meterRegistry;
this.notificationService = notificationService;
}
@Override
public <T> void onError(RetryContext context, RetryCallback<T> callback,
Throwable throwable) {
int attempt = context.getRetryCount() + 1;
String exceptionType = throwable.getClass().getSimpleName();
// 记录指标
meterRegistry.counter("llm.retry")
.tag("attempt", String.valueOf(attempt))
.tag("error", exceptionType)
.increment();
// 记录详细日志
log.warn("LLM调用第{}次失败: {} - {}",
attempt, exceptionType, throwable.getMessage());
// 第3次重试时发送告警
if (attempt >= 3) {
notificationService.sendWarning(
"LLM连续失败",
String.format("模型调用已连续失败%d次, 最后错误: %s",
attempt, throwable.getMessage())
);
}
// 如果是限流, 记录详细限流信息
if (throwable instanceof HttpServerErrorException httpError) {
if (httpError.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
String retryAfter = httpError.getHeaders()
.getFirst("Retry-After");
log.warn("限流响应, Retry-After: {}秒", retryAfter);
meterRegistry.counter("llm.rate_limited").increment();
}
}
}
@Override
public <T> void close(RetryContext context, RetryCallback<T> callback,
Throwable throwable) {
if (context.getRetryCount() > 0) {
meterRegistry.timer("llm.call.with_retry")
.record(context.getRetryCount(), TimeUnit.SECONDS);
}
}
}
5. 上下文截断自动重试
java
@Service
public class ContextAwareRetryService {
private final ChatClient chatClient;
private final TokenCounter tokenCounter;
private final int maxContextTokens;
public ContextAwareRetryService(ChatClient chatClient,
TokenCounter tokenCounter,
@Value("${llm.max-context-tokens:4000}")
int maxContextTokens) {
this.chatClient = chatClient;
this.tokenCounter = tokenCounter;
this.maxContextTokens = maxContextTokens;
}
/**
* 上下文超限后自动截断后重试
*/
public String chatWithAutoTruncate(String message, List<Message> history) {
try {
return chatClient.prompt()
.messages(history)
.user(message)
.call()
.content();
} catch (ContextTooLongException | HttpServerErrorException e) {
if (isContextTooLongError(e)) {
log.warn("上下文超长, 自动截断后重试");
List<Message> truncated = truncateHistory(
history, message, 0.5); // 截断50%
return chatClient.prompt()
.messages(truncated)
.user(message)
.call()
.content();
}
throw e;
}
}
/**
* 智能截断 - 保留关键信息
*/
private List<Message> truncateHistory(List<Message> history,
String currentMessage,
double keepRatio) {
int totalTokens = history.stream()
.mapToInt(m -> tokenCounter.count(m.getContent()))
.sum() + tokenCounter.count(currentMessage);
int targetTokens = (int) (totalTokens * keepRatio);
List<Message> result = new ArrayList<>();
// 保留system消息
history.stream()
.filter(m -> m.getMessageType() == MessageType.SYSTEM)
.forEach(result::add);
int currentTokens = result.stream()
.mapToInt(m -> tokenCounter.count(m.getContent()))
.sum();
// 从尾部尽可能保留
for (int i = history.size() - 1; i >= 0; i--) {
Message msg = history.get(i);
if (msg.getMessageType() == MessageType.SYSTEM) continue;
int msgTokens = tokenCounter.count(msg.getContent());
if (currentTokens + msgTokens > targetTokens) {
break;
}
result.add(0, msg);
currentTokens += msgTokens;
}
return result;
}
/**
* 带压缩的重试
*/
public String chatWithCompressedRetry(String message,
List<Message> history) {
try {
return chatClient.prompt()
.messages(history)
.user(message)
.call()
.content();
} catch (ContextTooLongException e) {
// 压缩历史消息
List<Message> compressed = compressHistory(history);
return chatClient.prompt()
.messages(compressed)
.user(message)
.call()
.content();
}
}
private List<Message> compressHistory(List<Message> history) {
// 将旧消息压缩为摘要
if (history.size() <= 4) return history;
List<Message> old = history.subList(0, history.size() - 4);
Message summary = new SystemMessage("[历史] " +
summarizeMessages(old));
List<Message> result = new ArrayList<>(List.of(summary));
result.addAll(history.subList(history.size() - 4, history.size()));
return result;
}
private boolean isContextTooLongError(Exception e) {
if (e instanceof ContextTooLongException) return true;
if (e instanceof HttpServerErrorException httpError) {
return httpError.getMessage() != null &&
(httpError.getMessage().contains("context_length_exceeded") ||
httpError.getMessage().contains("maximum context length"));
}
return false;
}
}
6. 流式调用的异常处理
java
@Service
public class StreamingExceptionService {
private final ChatClient chatClient;
private final MeterRegistry meterRegistry;
/**
* 流式调用异常处理
*/
public Flux<ServerSentEvent<String>> streamWithExceptionHandling(
String message) {
AtomicReference<StringBuilder> buffer =
new AtomicReference<>(new StringBuilder());
AtomicInteger retryCount = new AtomicInteger(0);
return chatClient.prompt()
.user(message)
.stream()
.content()
.map(chunk -> {
buffer.get().append(chunk);
return ServerSentEvent.<String>builder()
.data(chunk)
.build();
})
.timeout(Duration.ofSeconds(30))
.onErrorResume(TimeoutException.class, e -> {
log.warn("流式响应超时,已接收{}字符",
buffer.get().length());
return Flux.just(ServerSentEvent.<String>builder()
.event("timeout")
.data("{\"partial\":\"" + escapeJson(buffer.get().toString())
+ "\",\"message\":\"响应超时,已返回部分内容\"}")
.build());
})
.onErrorResume(Exception.class, e -> {
log.error("流式响应异常", e);
return Flux.just(ServerSentEvent.<String>builder()
.event("error")
.data("{\"error\":\"" + e.getMessage() + "\"}")
.build());
})
.doOnComplete(() -> {
meterRegistry.timer("llm.stream.duration")
.record(buffer.get().length() / 100, TimeUnit.SECONDS);
})
.doFinally(signalType -> {
if (signalType == SignalType.ON_ERROR) {
meterRegistry.counter("llm.stream.error").increment();
}
});
}
/**
* 带断点续传的流式响应
*/
public Flux<String> streamWithResume(String messageId, String message) {
// 检查是否有已缓存的部分响应
String cached = streamCache.getIfPresent(messageId);
Flux<String> cachedPart = cached != null
? Flux.fromStream(cached.lines())
: Flux.empty();
AtomicLong receivedBytes = new AtomicLong(
cached != null ? cached.length() : 0);
return cachedPart.concatWith(
chatClient.prompt()
.user(message)
.stream()
.content()
.doOnNext(chunk -> {
receivedBytes.addAndGet(chunk.length());
// 实时缓存
streamCache.put(messageId,
cached + chunk);
})
.timeout(Duration.ofSeconds(30))
.retryWhen(Retry.backoff(2, Duration.ofSeconds(1))
.filter(e -> e instanceof TimeoutException))
);
}
private final Cache<String, String> streamCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
}
总结
Spring AI异常处理与重试机制的关键技术点:
- 异常分类: 可重试/不可重试错误区分处理
- 指数退避: 限流场景的标准重试策略
- 熔断器: 连续失败时快速失败防止雪崩
- 降级响应: 不可用时返回兜底回复
- 上下文截断: 超长时自动压缩后重试