不只是调大模型 API:我在 Spring AI 项目里做的熔断、降级、审计和安全治理
不只是调大模型 API:我在 Spring AI 项目里做的熔断、降级、审计和安全治理
摘要
很多 AI 应用 Demo 看起来很简单:
用户输入
-> 调大模型 API
-> 返回回答
但真正把 AI 接进业务系统时,问题会立刻变多:
- API Key 没配怎么办?
- 模型服务超时怎么办?
- 第三方限流怎么办?
- 模型输出格式不合法怎么办?
- AI 能不能访问业务数据?
- AI 能不能修改业务状态?
- 谁调用了 AI,要不要记录?
- AI 挂了,会不会影响主业务?
我的抽奖系统接入了 Spring AI + DeepSeek,用于活动草稿生成、运营智能问答、中奖通知文案生成。项目里我没有只写一个模型调用,而是补了几类工程保护:
- 环境变量注入敏感配置;
AiCircuitBreaker熔断;- 本地降级结果;
AI_AUDIT审计日志;- 管理员权限校验;
- Tool Calling 只读工具;
- JSON 结构化输出解析和字段校验;
- 明确限制 AI 不参与中奖决策。
本文复盘这些 AI 工程化保护措施。
为什么 AI 功能需要工程保护
传统后端接口通常依赖:
- MySQL;
- Redis;
- RabbitMQ;
- 内部服务。
AI 功能多了一个不稳定外部依赖:
- 模型 API;
- 网络;
- Key;
- 额度;
- 模型输出格式;
- Prompt 行为。
如果把 AI 当作强依赖,一旦模型服务不可用,业务系统就会跟着出问题。
所以我的设计原则是:
AI 是旁路增强能力,不能影响核心业务确定性。
在抽奖系统里,核心业务仍然是:
- 活动创建;
- 奖品管理;
- 抽奖状态流转;
- 中奖记录落库;
- 通知发送。
AI 只能增强:
- 活动草稿;
- 运营问答;
- 通知文案。
配置治理:敏感信息走环境变量
Spring AI 配置如下:
spring:
ai:
openai:
api-key: ${AI_API_KEY:NO_API_KEY_CONFIGURED}
base-url: ${AI_BASE_URL:https://api.deepseek.com}
chat:
options:
model: ${AI_CHAT_MODEL:deepseek-chat}
temperature: ${AI_CHAT_TEMPERATURE:0}
lottery:
ai:
enabled: ${LOTTERY_AI_ENABLED:true}
circuit-breaker:
failure-threshold: ${LOTTERY_AI_CB_FAILURE_THRESHOLD:3}
open-duration: ${LOTTERY_AI_CB_OPEN_DURATION:60s}
notification:
max-length: ${LOTTERY_AI_NOTICE_MAX_LENGTH:180}
这里的设计点:
AI_API_KEY不写死;AI_BASE_URL可切换 OpenAI 兼容服务;AI_CHAT_MODEL可切模型;AI_CHAT_TEMPERATURE默认 0,适合业务场景;LOTTERY_AI_ENABLED可一键关闭 AI;- 熔断阈值和打开时间可配置。
这让 AI 能力变成可运维模块,而不是硬编码实验代码。
熔断器:失败太多就暂时关闭 AI
项目里的熔断器:
@Component
public class AiCircuitBreaker {
private final LotteryAiProperties properties;
private final AtomicInteger failures = new AtomicInteger();
private volatile Instant openUntil = Instant.MIN;
public boolean allowRequest() {
return properties.isEnabled() && Instant.now().isAfter(openUntil);
}
public void recordSuccess() {
failures.set(0);
openUntil = Instant.MIN;
}
public void recordFailure() {
int currentFailures = failures.incrementAndGet();
if (currentFailures >= properties.getCircuitBreaker().getFailureThreshold()) {
openUntil = Instant.now().plus(properties.getCircuitBreaker().getOpenDuration());
}
}
}
逻辑很简单:
AI 调用成功 -> failures 清零
AI 调用失败 -> failures + 1
失败次数达到阈值 -> 熔断一段时间
熔断期间 -> 不再调用模型,直接降级
它解决的问题是:当模型服务持续异常时,不要让每个请求都继续等待失败。
降级:AI 不可用时仍能返回
运营问答里,如果熔断器不允许请求:
if (!circuitBreaker.allowRequest()) {
result.setDegraded(true);
result.setAnswer("AI 服务暂时不可用,请通过活动列表、奖品列表和中奖记录页面查询。");
auditService.record(requestId, operatorId, "AI_OPS_CHAT", false, true);
return result;
}
调用模型失败时:
catch (Exception e) {
circuitBreaker.recordFailure();
result.setDegraded(true);
result.setAnswer("AI 服务调用失败,请稍后重试,或通过后台页面查询活动和中奖记录。");
auditService.record(requestId, operatorId, "AI_OPS_CHAT", false, true);
return result;
}
活动草稿生成也有本地降级:
result.setDegraded(true);
result.setDraft(fallbackService.buildDraft(prompt));
result.setWarnings(List.of("AI 服务调用失败,已生成本地活动草稿。"));
本地草稿服务:
@Service
public class AiActivityDraftFallbackService {
public AiActivityDraftDTO buildDraft(String prompt) {
String normalizedPrompt = StringUtils.hasText(prompt) ? prompt.trim() : "抽奖活动";
AiActivityDraftDTO draft = new AiActivityDraftDTO();
draft.setActivityName(buildActivityName(normalizedPrompt));
draft.setDescription("活动构想:" + normalizedPrompt + "。请继续圈选奖品和参与人员后创建活动。");
draft.setActivityPrizeList(new ArrayList<>());
draft.setActivityUserList(new ArrayList<>());
return draft;
}
}
降级的核心目标:
模型不可用
-> 用户仍能继续操作
-> 主业务不受影响
-> 前端知道 degraded=true
审计日志:AI 调用必须可追踪
项目中有专门的 AI 审计服务:
@Service
public class AiAuditService {
private static final Logger logger = LoggerFactory.getLogger("AI_AUDIT");
public void record(String requestId, String operatorId, String operation, boolean success, boolean degraded) {
logger.info("ai_audit requestId={} operatorId={} operation={} success={} degraded={}",
requestId, operatorId, operation, success, degraded);
}
}
每次 AI 调用都会生成 requestId:
String requestId = UUID.randomUUID().toString();
审计字段包括:
- requestId:本次 AI 请求;
- operatorId:操作人;
- operation:操作类型;
- success:是否成功;
- degraded:是否降级。
为什么要审计?
因为 AI 一旦接入业务系统,就不是普通工具,而是新的业务入口。必须能回答:
- 谁调用了 AI?
- 调用了哪个能力?
- 成功了吗?
- 是否发生降级?
- 是否需要排查?
权限控制:AI 接口不能裸奔
AI 运营接口在 Controller 层先校验管理员:
String operatorId = aiAccessService.requireAdmin(request);
AiAccessService:
public String requireAdmin(HttpServletRequest request) {
String token = request.getHeader("user_token");
if (!StringUtils.hasText(token)) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
Claims claims = JWTUtil.parseJWT(token);
if (claims == null) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
String identity = String.valueOf(claims.get("identity"));
if (UserIdentityEnum.forName(identity) != UserIdentityEnum.ADMIN) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
Object id = claims.get("id");
return id == null ? "unknown" : String.valueOf(id);
}
AI 能查询业务数据,所以必须像后台接口一样做权限校验。
Tool Calling 边界:只读工具
运营助手使用 Spring AI Tool Calling,但只暴露只读工具:
@Tool(description = "Query activity detail by activityId. This is a read-only operation.")
public String getActivityDetail(Long activityId) { ... }
@Tool(description = "Query winning records by activityId and optional prizeId. This is a read-only operation.")
public String getWinningRecords(Long activityId, Long prizeId) { ... }
@Tool(description = "Query prize list by page and pageSize. This is a read-only operation.")
public String findPrizeList(Integer page, Integer pageSize) { ... }
没有暴露:
- 抽奖;
- 创建活动;
- 修改状态;
- 删除数据;
- 发送短信;
- 发送邮件。
这是最重要的安全边界。
Prompt 可以约束模型,但真正的边界必须在代码层。模型拿不到写工具,就不能改状态。
System Prompt 不是安全边界,但必须写清楚
运营助手系统提示词:
private static final String SYSTEM_PROMPT = """
You are an operations assistant for an enterprise lottery system.
Answer in Simplified Chinese.
You can use the provided read-only tools to query activities, prizes, and winning records.
Never invent business data. If a tool result is empty, say no matching data was found.
Never create activities, draw prizes, modify state, or decide winners.
""";
这个 Prompt 约束模型行为:
- 不编造业务数据;
- 工具为空就说没查到;
- 不创建活动;
- 不抽奖;
- 不改状态;
- 不决定中奖人。
但我不会把 Prompt 当作唯一安全措施。真正安全的是:
权限校验 + 只读工具 + 不暴露写方法 + 审计日志
结构化输出校验
活动草稿生成要求模型返回 JSON:
String content = chatClient.prompt()
.system(SYSTEM_PROMPT)
.user(prompt)
.call()
.content();
AiActivityDraftDTO draft = jsonParser.parseObject(content, AiActivityDraftDTO.class);
List<String> errors = validator.validate(draft);
JSON 解析器会提取模型返回中的 JSON:
private String extractJson(String content) {
String text = content.trim();
if (text.startsWith("```")) {
text = text.replaceFirst("^```[a-zA-Z]*\\s*", "");
text = text.replaceFirst("\\s*```$", "");
}
int objectStart = text.indexOf('{');
int objectEnd = text.lastIndexOf('}');
if (objectStart >= 0 && objectEnd > objectStart) {
return text.substring(objectStart, objectEnd + 1);
}
return text;
}
Validator 校验:
if (!StringUtils.hasText(draft.getActivityName())) {
errors.add("activityName is required.");
}
if (ActivityPrizeTiersEnum.forName(prize.getPrizeTiers()) == null) {
errors.add("invalid prizeTiers: " + prize.getPrizeTiers());
}
if (prizeAmount > draft.getActivityUserList().size()) {
errors.add("total prize amount cannot exceed participant amount.");
}
这说明 AI 输出不能直接进业务系统,必须先解析、校验、再决定是否使用。
AI 不参与核心决策
抽奖系统里,AI 不能做这些事:
- 决定谁中奖;
- 修改活动状态;
- 修改奖品状态;
- 删除中奖记录;
- 直接发送通知;
- 绕过后端校验创建活动。
AI 只能做:
- 活动草稿建议;
- 运营数据查询;
- 中奖通知文案生成;
- 辅助说明。
核心抽奖决策仍由后端业务链路完成:
RabbitMQ 消费
-> 参数校验
-> 状态流转
-> 中奖记录落库
-> Redis 缓存刷新
-> 通知发送
怎么保证 AI 功能稳定可控
我没有只写一个模型调用,而是把 AI 当成旁路增强能力做工程化保护。首先敏感配置全部走环境变量,比如 AI_API_KEY、AI_BASE_URL、AI_CHAT_MODEL,并通过 LOTTERY_AI_ENABLED 支持一键关闭。其次我实现了 AiCircuitBreaker,连续失败达到阈值后会在一段时间内直接降级,避免每次请求都阻塞在模型服务上。
在业务安全上,AI 接口必须先通过 JWT 校验管理员身份;Tool Calling 只暴露活动详情、中奖记录、奖品列表三个只读工具,不暴露抽奖、改状态、删除等写操作。系统 Prompt 也明确要求模型不能编造业务数据、不能决定中奖人。所有 AI 调用都会记录 AI_AUDIT,包括 requestId、operatorId、operation、success、degraded。
对结构化输出场景,我还做了 JSON 解析和字段校验,AI 返回的活动草稿如果字段缺失、枚举非法或奖品数量超过参与人数,就不会直接进入业务流程。AI 不可用时,会返回本地降级草稿或提示用户走传统后台页面。
总结
AI 工程化落地,不只是会调模型 API。
真正接入业务系统时,至少要考虑:
- 配置安全;
- 权限校验;
- 工具边界;
- 熔断;
- 降级;
- 审计;
- 结构化输出校验;
- 核心业务和 AI 能力解耦。
我的抽奖系统里,AI 能做运营增强,但不能参与抽奖决策。这个边界比模型回答是否“聪明”更重要。
更多推荐


所有评论(0)