错误处理与监控
大约 4 分钟
第十二章:错误处理与监控
12.1 异常处理策略
在 Spring AI 场景里,“错误”比普通接口更常见:超时、限流、供应商短暂不可用、响应结构变化、模型输出不可解析等。想让系统稳定,你需要先把错误分层。
12.1.1 把错误分成三类
1)可重试错误(Transient)
- 网络抖动、连接超时
- 429 限流(要带退避)
- 5xx(供应商故障或网关问题)
2)不可重试错误(Permanent)
- 401/403(API Key 无效/权限不足)
- 4xx 参数错误(提示词/工具参数不合法)
- 业务鉴权失败
3)需要业务降级的错误(Degrade)
- 供应商不可用但业务必须返回:返回兜底文案/历史缓存/转人工
12.1.2 建议的 Controller 返回策略
- 不要把供应商原始错误直接暴露给前端
- 返回稳定的错误码与错误信息,提示用户下一步动作(重试、稍后再试、联系客服)
- 对于可重试错误:保持幂等(同样输入不产生副作用)
12.2 重试机制(可复制运行)
这一节用“纯 Java + Spring Boot”的方式实现一个足够好用的重试器,不依赖额外库。
12.2.1 重试器:指数退避 + 抖动
package com.example.observability.support;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Supplier;
public final class RetrySupport {
private RetrySupport() {
}
public static <T> T withRetry(
Supplier<T> supplier,
int maxAttempts,
Duration baseBackoff,
Duration maxBackoff
) {
RuntimeException last = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return supplier.get();
} catch (RuntimeException ex) {
last = ex;
if (attempt == maxAttempts || !isRetryable(ex)) {
throw ex;
}
sleepWithJitter(exponentialBackoff(attempt, baseBackoff, maxBackoff));
}
}
throw Optional.ofNullable(last).orElseGet(() -> new IllegalStateException("retry failed"));
}
private static boolean isRetryable(RuntimeException ex) {
String msg = String.valueOf(ex.getMessage()).toLowerCase();
return msg.contains("timeout") || msg.contains("429") || msg.contains("5xx") || msg.contains("tempor");
}
private static Duration exponentialBackoff(int attempt, Duration base, Duration max) {
long baseMs = Math.max(1, base.toMillis());
long capMs = Math.max(baseMs, max.toMillis());
long exp = baseMs * (1L << Math.min(10, attempt - 1));
long ms = Math.min(exp, capMs);
return Duration.ofMillis(ms);
}
private static void sleepWithJitter(Duration backoff) {
long ms = backoff.toMillis();
long jitter = ThreadLocalRandom.current().nextLong(0, Math.max(1, ms / 4));
try {
Thread.sleep(ms + jitter);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}12.2.2 把重试应用在 AI 调用层
把 AI 调用集中在一个 Facade 里,Controller 只关心输入输出,这样:
- 重试/超时/日志/脱敏都在一处做
- 单测更容易写
package com.example.observability.service;
import com.example.observability.support.RetrySupport;
import java.time.Duration;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class AiChatFacade {
private static final int MAX_ATTEMPTS = 3;
private static final Duration BASE_BACKOFF = Duration.ofMillis(200);
private static final Duration MAX_BACKOFF = Duration.ofSeconds(2);
private final ChatClient chatClient;
public AiChatFacade(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String ask(String userMessage) {
return RetrySupport.withRetry(
() -> chatClient.prompt()
.user(userMessage)
.call()
.content(),
MAX_ATTEMPTS,
BASE_BACKOFF,
MAX_BACKOFF
);
}
}12.3 日志记录(脱敏 + 关键字段)
AI 请求日志建议记“对排查有用但不泄露隐私”的字段:
- provider、model、耗时、重试次数、失败原因
- prompt 长度(字符数/消息条数),而不是把 prompt 原文全部打出来
- token 用量(如果能拿到),用于成本治理
敏感信息处理建议:
- API Key 永远不进日志
- 用户输入可能包含手机号/身份证/邮箱:先做脱敏或采样
- 工具调用参数必须审计,但也要脱敏(例如只保留订单号后四位)
12.4 性能监控:Actuator + 自定义指标(思路)
最实用的做法是:你不需要一开始就上复杂链路追踪,但至少要有:
- 请求总数、成功率、失败率
- P50/P95 延迟
- 429/5xx 次数
建议引入 Spring Boot Actuator(并在网关层做鉴权):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>在生产环境里,把这些指标接到你的监控系统(Prometheus/Grafana 或云监控),很快就能定位“慢”“贵”“不稳定”的根因。 点击这里👇🏻获取:100万QPS短链系统、复杂的商城微服务系统、智能翻译助手AI Agent、SaaS点餐系统、刷题吧小程序、商城系统、秒杀系统、AI项目、代码生成神器、苏三demo项目、智能天气播报AI Agent、智能代码审查AI Agent等 10 个项目的:项目源代码、开发教程和技术答疑
