不只是调大模型 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 能做运营增强,但不能参与抽奖决策。这个边界比模型回答是否“聪明”更重要。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐