Spring AI 企业增强版(含安全版与合规版

Spring AI 企业增强版(含安全版与合规版)

基于 Spring AI 官方参考文档整理的中文实战教程。

参考版本:Spring AI 1.1.4,支持 Spring Boot 3.4.x / 3.5.x

官方文档入口:https://docs.spring.io/spring-ai/reference/index.html

学习记录

1. 这份文档适合谁

如果你已经会 Spring Boot,但第一次真正落地 AI 功能,这份文档可以直接带你走完一条典型路线:

  1. 接一个聊天模型
  2. 暴露一个 HTTP 接口
  3. 返回结构化结果
  4. 给模型加工具调用
  5. 加会话记忆
  6. 接入 PGvector 做 RAG
  7. 补齐日志、监控、超时、重试、审计与安全边界
  8. 补齐数据分级、模型供应商准入、留存删除与审批闭环

本文默认使用 OpenAI 作为示例模型供应商,因为官方文档示例最完整、上手最快。后续如果你换成 OllamaAnthropicAzure OpenAI,业务层代码大多可以复用,主要改 starter 和配置。

2. Spring AI 到底解决什么问题

Spring AI 的核心价值,不是"帮你调一次大模型接口",而是把 AI 能力放进 Spring 的工程体系里:

  • 用统一抽象访问不同模型提供商
  • 用 Spring Boot 自动配置减少接线代码
  • ChatClientStructured OutputTool CallingAdvisorsChat MemoryRAG 这些组件搭业务链路
  • 让 AI 功能更容易接入现有的 Web、Data、Security、Actuator 体系

一句话理解:

Spring AI = 用 Spring Boot 的方式开发 AI 应用。

3. 实战目标

我们最终希望做出下面这种应用:

  • GET /ai/chat?message=...:普通聊天
  • GET /ai/plan?topic=...:返回结构化对象
  • GET /ai/tool?message=...:让模型自动调用本地工具
  • GET /ai/memory?...:支持多轮对话记忆
  • POST /rag/load:导入知识库文档到向量库
  • GET /rag/ask?question=...:基于私有知识库回答问题

这已经覆盖了大多数企业项目的第一版 AI 能力。

4. 创建项目

4.1 建议环境

  • JDK 21
  • Spring Boot 3.4.x3.5.x
  • Spring AI 1.1.4
  • Maven 或 Gradle
  • PostgreSQL + PGvector(如果要做 RAG)

4.2 推荐依赖

如果你已经有一个 Spring Boot 项目,最实用的方式是在现有项目里加入 Spring AI BOM 和需要的 starter。

下面是一个偏实战的 pom.xml 片段:

xml 复制代码
<properties>
    <java.version>21</java.version>
    <spring-ai.version>1.1.4</spring-ai.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-advisors-vector-store</artifactId>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

这组依赖分别负责:

  • spring-boot-starter-web:暴露 REST 接口
  • spring-ai-starter-model-openai:接入 OpenAI Chat / Embedding
  • spring-ai-starter-vector-store-pgvector:接入 PGvector
  • spring-ai-advisors-vector-store:开箱即用的 RAG Advisor
  • spring-boot-starter-data-jpa:支撑审计落库、会话元数据等关系型存储
  • spring-boot-starter-actuator:暴露健康检查、指标、观测端点
  • micrometer-registry-prometheus:把指标输出给 Prometheus 抓取
  • spring-boot-starter-security:统一接入认证与权限控制
  • spring-boot-starter-oauth2-resource-server:把 AI 接口纳入 JWT/OAuth2 鉴权体系
  • spring-boot-starter-validation:对输入做参数校验和基础拦截

如果你暂时不做 RAG,可以先只保留:

  • spring-ai-starter-model-openai
  • spring-boot-starter-web

如果你暂时不做生产监控,也可以先不加:

  • spring-boot-starter-actuator
  • micrometer-registry-prometheus

如果你暂时不做审计落库,也可以先不加:

  • spring-boot-starter-data-jpa

如果你是在内网做 PoC,暂时也可以先不加:

  • spring-boot-starter-security
  • spring-boot-starter-oauth2-resource-server
  • spring-boot-starter-validation

5. 最小可用配置

5.1 application.yml

yaml 复制代码
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini
          temperature: 0.2
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres

几点说明:

  • spring.ai.openai.api-key 可以直接从环境变量读取
  • gpt-4o-mini 是一个比较适合开发期验证的默认选择
  • temperature 建议从 0.20.5 开始,不要一上来就太高
  • 如果你还没做 RAG,datasource 这段可以先不加

5.2 设置环境变量

PowerShell 示例:

powershell 复制代码
$env:OPENAI_API_KEY="你的OpenAIKey"

如果要长期使用,可以写到系统环境变量或者本地 .env 管理方案中。

6. 第一个可运行接口

Spring AI 官方推荐业务层优先用 ChatClient,它比直接操作 ChatModel 更贴近应用开发。

java 复制代码
package com.example.demo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/ai/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .system("你是一个简洁、专业的 Java 助手。")
                .user(message)
                .call()
                .content();
    }
}

请求示例:

bash 复制代码
curl "http://localhost:8080/ai/chat?message=请解释什么是Spring AI"

这个例子体现了 Spring AI 最基础的调用链:

  • 注入 ChatClient.Builder
  • 构造 prompt
  • 设置 systemuser
  • 调用 call().content() 取文本结果

7. 推荐先掌握的 API

真正写业务时,优先掌握下面几个点就够了:

  • ChatClient:应用层主入口
  • Prompt / 模板参数:组织提示词
  • entity():把输出转成对象
  • tools():让模型调用本地工具
  • advisors():挂记忆、RAG、增强逻辑

如果你只学一个 API,就先学 ChatClient

8. 结构化输出

企业项目里,很多场景不是要一段自然语言,而是要一个"可以继续进业务逻辑"的对象。

8.1 定义返回对象

java 复制代码
package com.example.demo;

import java.util.List;

public record StudyPlan(
        String topic,
        List<String> steps,
        List<String> risks
) {
}

8.2 让模型直接返回对象

java 复制代码
package com.example.demo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PlanController {

    private final ChatClient chatClient;

    public PlanController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/ai/plan")
    public StudyPlan plan(@RequestParam String topic) {
        return chatClient.prompt()
                .system("你是一个技术学习规划助手,请返回清晰、可执行的学习计划。")
                .user(u -> u.text("请为主题 {topic} 生成 5 步学习计划,并补充常见风险。")
                        .param("topic", topic))
                .call()
                .entity(StudyPlan.class);
    }
}

8.3 什么时候该用结构化输出

下面这些场景,建议优先用 entity()

  • 任务拆解
  • 工单分类
  • 信息抽取
  • 生成审批建议
  • 生成可执行参数
  • 输出给前端渲染的 JSON 结构

不要等拿到一大段文本后再自己正则解析,那样维护成本会高很多。

9. Tool Calling:让模型调用本地能力

这是 Spring AI 很实用的一块。模型自己不能读你本地时间、数据库、业务系统,但它可以"请求调用工具"。

9.1 定义工具类

java 复制代码
package com.example.demo;

import java.time.LocalDateTime;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

@Component
public class TimeTools {

    @Tool(description = "返回当前系统时间,格式为 ISO-8601。")
    public String currentTime() {
        return LocalDateTime.now().toString();
    }
}

9.2 在请求时挂上工具

java 复制代码
package com.example.demo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ToolController {

    private final ChatClient chatClient;
    private final TimeTools timeTools;

    public ToolController(ChatClient.Builder builder, TimeTools timeTools) {
        this.chatClient = builder.build();
        this.timeTools = timeTools;
    }

    @GetMapping("/ai/tool")
    public String tool(@RequestParam String message) {
        return chatClient.prompt(message)
                .tools(timeTools)
                .call()
                .content();
    }
}

测试示例:

bash 复制代码
curl "http://localhost:8080/ai/tool?message=现在时间是什么?请顺便告诉我30分钟后是几点"

这个过程中:

  1. 模型判断自己需要当前时间
  2. Spring AI 执行 currentTime()
  3. 执行结果返回给模型
  4. 模型再组织最终回答

9.3 Tool Calling 的实战建议

  • 工具描述一定要写清楚,否则模型不容易正确调用
  • 工具返回值尽量稳定、可预测
  • 工具本质上是业务入口,注意权限与审计
  • 不要把所有内部方法都暴露给模型

10. Chat Memory:支持多轮对话

LLM 默认是无状态的。你要实现"上文下文记忆",需要加内存层。

10.1 配置记忆 Bean

java 复制代码
package com.example.demo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

    @Bean
    ChatMemory chatMemory() {
        return MessageWindowChatMemory.builder()
                .maxMessages(10)
                .build();
    }

    @Bean
    ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
        return builder
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}

10.2 在请求中传入会话 ID

java 复制代码
package com.example.demo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MemoryController {

    private final ChatClient chatClient;

    public MemoryController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @GetMapping("/ai/memory")
    public String memory(@RequestParam String conversationId, @RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
                .call()
                .content();
    }
}

测试方式:

bash 复制代码
curl "http://localhost:8080/ai/memory?conversationId=u1&message=我叫张三"
curl "http://localhost:8080/ai/memory?conversationId=u1&message=你还记得我叫什么吗"

10.3 Memory 的边界

要特别区分两件事:

  • Chat Memory:给模型提供当前会话上下文
  • Chat History:你业务上真正需要长期保存的全部对话记录

Spring AI 的 ChatMemory 更偏前者,不要把它直接当正式消息库。

11. RAG:让模型基于你的知识库回答

很多项目真正要的不是"会聊天",而是"基于公司资料回答问题"。这就是 RAG。

11.1 启动本地 PGvector

官方文档给出了最直接的本地启动方式:

bash 复制代码
docker run -it --rm --name postgres -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres pgvector/pgvector

11.2 RAG 配置

yaml 复制代码
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini
          temperature: 0.2
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536
        initialize-schema: true

几点注意:

  • initialize-schema: true 需要你显式开启,Spring AI 1.x 不再默认帮你建表
  • dimensions: 1536 是一个常见示例值
  • 如果你使用的 Embedding 维度不是 1536,要保持一致

11.3 导入文档

先写一个最简单的导入接口:

java 复制代码
package com.example.demo;

import java.util.List;
import java.util.Map;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RagLoadController {

    private final VectorStore vectorStore;

    public RagLoadController(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    @PostMapping("/rag/load")
    public String load(@RequestBody List<String> texts) {
        List<Document> documents = texts.stream()
                .map(text -> new Document(text, Map.of("source", "manual")))
                .toList();

        vectorStore.add(documents);
        return "ok";
    }
}

请求示例:

powershell 复制代码
Invoke-RestMethod -Method Post `
  -Uri "http://localhost:8080/rag/load" `
  -ContentType "application/json" `
  -Body '["Spring AI是Spring生态下的AI应用开发框架","ChatClient是推荐的应用层入口"]'

11.4 基于知识库提问

java 复制代码
package com.example.demo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RagAskController {

    private final ChatClient chatClient;
    private final QuestionAnswerAdvisor qaAdvisor;

    public RagAskController(ChatClient.Builder builder, VectorStore vectorStore) {
        this.chatClient = builder.build();
        this.qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(SearchRequest.builder()
                        .similarityThreshold(0.8d)
                        .topK(6)
                        .build())
                .build();
    }

    @GetMapping("/rag/ask")
    public String ask(@RequestParam String question) {
        return chatClient.prompt()
                .user(question)
                .advisors(qaAdvisor)
                .call()
                .content();
    }
}

测试示例:

bash 复制代码
curl "http://localhost:8080/rag/ask?question=Spring AI 推荐用什么作为应用层入口?"

11.5 RAG 质量为什么经常不稳定

很多人以为 RAG 效果差是模型不行,实际常见问题在这里:

  • 文档切分太粗或太碎
  • 导入时没清洗脏数据
  • 检索阈值不合适
  • topK 太小或太大
  • Prompt 没限制"找不到就别瞎编"

也就是说,RAG 是一个"数据工程 + 检索工程 + Prompt 工程"的组合问题,不只是模型调用。

12. 推荐的项目结构

一个干净的 Spring AI 项目,建议至少这样分层:

text 复制代码
src/main/java/com/example/demo
├─ config
├─ controller
├─ service
├─ tool
├─ rag
└─ model

可以按下面方式落地:

  • configChatClientChatMemoryVectorStore 相关配置
  • controller:HTTP 接口
  • service:AI 编排逻辑
  • tool:给模型调用的工具
  • rag:文档导入、检索增强
  • model:结构化输出对象、DTO

13. 实战时最值得遵守的 8 条建议

  1. 业务代码优先用 ChatClient,不要一开始就沉到过低层 API。
  2. 需要稳定结果时优先用结构化输出,而不是解析自然语言。
  3. 工具调用只暴露必要能力,不要把内部服务一股脑交给模型。
  4. 多轮对话一定要传 conversationId,否则记忆会串。
  5. RAG 先从小规模文档验证,不要一开始就全量导库。
  6. temperature 先低后高,业务系统通常更需要稳定而不是发散。
  7. 日志里要记录模型、耗时、token、错误类型和命中的知识来源。
  8. 把 AI 看成"不稳定外部依赖",加超时、重试、降级和兜底提示。

14. 企业增强:可观测性

Spring AI 官方文档明确提供了观测能力,覆盖:

  • ChatClient
  • Advisor
  • ChatModel
  • EmbeddingModel
  • VectorStore

如果你准备上线,建议第一天就把可观测性接上,而不是等问题出现后再补。

14.1 最小监控依赖

上面依赖中的这两个组件就是最常用组合:

  • spring-boot-starter-actuator
  • micrometer-registry-prometheus

14.2 推荐配置

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when_authorized

spring:
  ai:
    chat:
      observations:
        log-prompt: false
        log-completion: false
        include-error-logging: true
      client:
        observations:
          log-prompt: false
          log-completion: false
    vectorstore:
      observations:
        log-query-response: false

这里有两个关键点:

  • Prompt 和 Completion 默认不要落日志,避免敏感信息泄漏
  • 错误日志可以打开,但要配合脱敏规则

14.3 上线后重点看什么指标

结合 Spring AI 的观测模型,建议至少盯住这些维度:

  • gen_ai_client_operation:模型调用耗时
  • gen_ai_client_token_usage:输入、输出、总 token 用量
  • gen_ai_chat_client_operation:应用层 ChatClient 调用耗时
  • db_vector_client_operation:向量库查询和写入耗时

最有业务价值的看板通常是:

  • 每分钟请求量
  • 平均耗时和 P95/P99 耗时
  • 每模型 token 消耗
  • 工具调用次数与失败率
  • RAG 检索命中率
  • 兜底返回比例

15. 企业增强:超时、重试与降级

AI 调用和普通数据库调用最大的区别,是它更慢、更贵、也更不稳定。企业系统里最好把模型调用当成外部依赖来治理。

15.1 先定超时策略

最实用的做法不是一味延长等待时间,而是按场景分层:

  • 用户同步问答:宁可快失败,也不要无上限等待
  • 后台批处理:可以给更长超时
  • RAG 链路:要单独考虑向量检索和模型生成两段耗时

一个简单思路是把 AI 编排放到 service 层,再做超时包裹:

java 复制代码
package com.example.demo.service;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class AiGatewayService {

    private final ChatClient chatClient;

    public AiGatewayService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String askWithTimeout(String message) {
        return CompletableFuture.supplyAsync(() ->
                chatClient.prompt()
                        .user(message)
                        .call()
                        .content())
                .orTimeout(8, TimeUnit.SECONDS)
                .exceptionally(ex -> "当前 AI 服务较忙,请稍后重试。")
                .join();
    }
}

这个示例不是唯一实现方式,但它表达了一个很重要的原则:

超时和兜底应该放在应用层明确控制,而不是完全交给用户等待。

15.2 重试要克制

AI 调用不是所有错误都适合重试。更合理的策略通常是:

  • 网络抖动、瞬时 5xx:可有限重试
  • 参数错误、提示词错误、权限错误:不要重试
  • 超时后是否重试:要看接口是否是强交互场景

建议原则:

  • 最多 12 次重试
  • 使用指数退避
  • 把每次重试记录到审计日志
  • 流式响应场景谨慎重试

15.3 给出明确降级策略

企业项目不能只返回 500。至少要准备三种兜底:

  • 文本兜底:当前服务繁忙,请稍后再试
  • 规则兜底:改走关键词检索或固定模板
  • 人工兜底:进入工单或人工审核

16. 企业增强:审计、日志与敏感信息治理

很多团队上线后第一个问题不是"模型答得准不准",而是"出了问题到底是谁、在什么时候、用哪个模型、对哪条数据做了什么"。

16.1 审计日志至少记录这些字段

  • traceId
  • conversationId
  • userId
  • model
  • provider
  • latencyMs
  • inputToken
  • outputToken
  • toolNames
  • knowledgeSources
  • resultStatus
  • failureReason

16.2 不要直接记录原始 Prompt

官方文档也明确提示:Prompt 和 Completion 可能包含敏感信息,默认不应该直接导出。企业里更建议这么做:

  • 日志记录摘要和长度,不记录全量正文
  • 对手机号、身份证号、邮箱、地址做脱敏
  • 对内部文档片段只记录 sourceIddocumentId
  • 把完整会话单独进入受控审计存储,而不是普通应用日志

16.3 一个简单的审计对象示例

java 复制代码
package com.example.demo.audit;

import java.time.Instant;
import java.util.List;

public record AiAuditLog(
        Instant timestamp,
        String traceId,
        String userId,
        String conversationId,
        String model,
        long latencyMs,
        Integer inputTokens,
        Integer outputTokens,
        List<String> toolNames,
        List<String> knowledgeSources,
        String resultStatus,
        String failureReason
) {
}

这个对象不复杂,但足够支撑大部分排障和审计诉求。

17. 企业增强:内容安全与权限边界

Spring AI 官方文档里的 Advisors 里专门提到了 SafeGuardAdvisor。这很适合做第一层输入拦截。

17.1 用 SafeGuardAdvisor 做敏感词拦截

java 复制代码
package com.example.demo.config;

import java.util.List;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;

@Configuration
public class GuardrailConfig {

    @Bean
    ChatClient guardedChatClient(ChatClient.Builder builder) {
        SafeGuardAdvisor safeGuardAdvisor = new SafeGuardAdvisor(
                List.of("银行卡密码", "身份证原件", "绕过审核", "导出全部客户数据"),
                "请求中包含敏感内容,已拒绝处理,请联系管理员。",
                Ordered.HIGHEST_PRECEDENCE);

        return builder
                .defaultAdvisors(safeGuardAdvisor)
                .build();
    }
}

这里的重点不是"敏感词列表有多全",而是先建立一条明确边界:

  • 先拦危险请求
  • 再决定是否调用模型
  • 最后再决定是否触发工具

17.2 工具权限要单独控制

企业里最容易出事的不是聊天本身,而是工具调用。建议至少做到:

  • 工具和普通聊天接口分开授权
  • 高风险工具按角色控制
  • 工具调用落审计日志
  • 对写操作类工具加人工确认或审批

尤其是下面这些工具,不能只靠模型自己判断:

  • 导出数据
  • 修改订单
  • 发消息
  • 调资金
  • 删除记录

18. 企业增强:RAG 结果校验与幻觉治理

只做 RAG 还不够,生产环境里还要关心"答得像不像真的"。Spring AI 官方文档提供了 EvaluatorRelevancyEvaluator 这条路线。

18.1 为什么要加评估

RAG 常见失败方式不是完全答错,而是:

  • 检索到了相关片段,但模型扩写过度
  • 检索上下文不够,模型开始脑补
  • 文档过期,但回答看起来很自信

18.2 可行的治理方式

  • 没检索到结果时,明确要求返回"未找到依据"
  • 把引用来源返回给前端
  • 对高风险问答增加 RelevancyEvaluator
  • 低分回答走二次确认或人工审核

如果是法规、财务、医疗、合同类场景,这一步尤其重要。

19. 企业增强:推荐的生产分层

如果准备长期维护,建议把原来的基础结构再细分一点:

text 复制代码
src/main/java/com/example/demo
├─ config
├─ controller
├─ service
├─ tool
├─ rag
├─ audit
├─ security
├─ observability
├─ fallback
└─ model

推荐职责:

  • service:AI 编排主流程
  • audit:审计日志与调用留痕
  • security:权限、脱敏、输入校验、内容安全
  • observability:指标、trace、日志统一封装
  • fallback:超时、重试、降级、人工接管逻辑

20. 企业安全版:接口认证与角色鉴权

企业里最先要收住的,不是模型能力,而是"谁能访问哪些 AI 接口"。

一个比较稳妥的默认原则是:

  • 普通问答接口:登录即可访问
  • RAG 查询接口:需要业务角色
  • 文档导入接口:只允许知识库管理员
  • 工具调用接口:只允许明确授权的角色
  • 高风险工具:只允许后台服务账号或审批后执行

20.1 一个基础的 Spring Security 配置

下面这个示例把普通聊天、RAG 导入、管理接口分开了权限边界:

java 复制代码
package com.example.demo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                        .requestMatchers("/ai/chat", "/ai/plan", "/ai/memory").authenticated()
                        .requestMatchers("/ai/tool").hasAnyRole("AI_TOOL_USER", "ADMIN")
                        .requestMatchers("/rag/ask").hasAnyRole("KNOWLEDGE_USER", "ADMIN")
                        .requestMatchers("/rag/load").hasAnyRole("KNOWLEDGE_ADMIN", "ADMIN")
                        .anyRequest().denyAll())
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
                .build();
    }
}

这个示例最重要的不是写法本身,而是这三个原则:

  • AI 接口不要默认裸奔
  • 工具调用权限要比普通问答更严格
  • 写操作和导入操作要单独隔离

20.2 方法级权限更适合业务控制

有些权限不适合只写在 URL 层,尤其是服务复用时。可以继续加方法级控制:

java 复制代码
package com.example.demo.service;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class RagAdminService {

    @PreAuthorize("hasRole('KNOWLEDGE_ADMIN')")
    public void loadDocuments() {
        // 导入向量库
    }
}

这样即使 controller 之外有别的入口调用,也不会绕过权限。

21. 企业安全版:会话隔离、租户隔离与数据权限

AI 系统最容易被忽略的问题,是"回答对了,但回答给错人了"。

21.1 不要让前端自由传 conversationId

前面的 demo 为了便于理解,直接把 conversationId 作为请求参数传入。但企业系统里更安全的做法通常是:

  • 从登录态里拿 userId
  • 再拼上租户、应用、会话标识
  • 由服务端生成最终会话 ID

例如:

java 复制代码
package com.example.demo.security;

import java.security.Principal;
import org.springframework.stereotype.Component;

@Component
public class ConversationIdFactory {

    public String create(String tenantId, Principal principal, String sessionId) {
        return tenantId + ":" + principal.getName() + ":" + sessionId;
    }
}

这样可以避免用户伪造别人的会话上下文。

21.2 RAG 检索要带上数据权限条件

如果你的知识库按租户、部门、项目隔离,向量检索不能只做"语义最相近",还要做"当前用户能看见"。

推荐做法:

  • 文档入库时写入 tenantIddepartmentIdvisibility 等 metadata
  • 查询时按当前用户权限拼检索过滤条件
  • 返回引用来源时也不要泄露其他租户信息

哪怕模型本身回答得再准,只要越权命中了一段资料,这就是安全事故。

22. 企业安全版:高风险工具授权与二次确认

工具调用的真正风险,不在"模型会不会调用",而在"调用后会不会造成真实业务影响"。

22.1 把工具分级

建议最少分成三类:

  • 只读工具:查天气、查状态、查配置
  • 低风险写工具:创建草稿、生成建议、写临时记录
  • 高风险写工具:发消息、改订单、导数据、调资金、删数据

22.2 高风险工具不要直接暴露

一个比较稳妥的做法,是让模型只输出"意图",真正执行前必须经过业务授权层:

java 复制代码
package com.example.demo.security;

import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

@Service
public class ToolAuthorizationService {

    public void checkCanExecute(String toolName, Authentication authentication) {
        boolean isAdmin = authentication.getAuthorities().stream()
                .anyMatch(a -> "ROLE_ADMIN".equals(a.getAuthority()));

        if ("exportCustomerData".equals(toolName) && !isAdmin) {
            throw new IllegalStateException("当前用户无权执行高风险工具");
        }
    }
}

你可以把它理解为:

  • 模型负责提出"想调用什么"
  • Spring AI 负责调用链编排
  • 业务授权层负责决定"准不准执行"

22.3 对高风险操作增加确认票据

企业里更建议把高风险动作拆成两步:

  1. 模型生成操作建议
  2. 用户点击确认或审批通过后再执行

这比"模型一决定就直接写库"安全得多。

23. 企业安全版:输入校验、脱敏与提示词注入防护

很多 AI 安全问题并不是传统漏洞,而是"输入太自由,模型被带偏了"。

23.1 先做普通输入校验

先把最基础的校验补上,这一步不要省:

java 复制代码
package com.example.demo.api;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record ChatRequest(
        @NotBlank(message = "message不能为空")
        @Size(max = 4000, message = "message长度不能超过4000")
        String message
) {
}

controller 示例:

java 复制代码
package com.example.demo.controller;

import com.example.demo.api.ChatRequest;
import jakarta.validation.Valid;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecureChatController {

    private final ChatClient chatClient;

    public SecureChatController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @PostMapping("/ai/chat")
    public String chat(@Valid @RequestBody ChatRequest request) {
        return chatClient.prompt()
                .system("你是企业知识助手,只回答授权范围内的问题。")
                .user(request.message())
                .call()
                .content();
    }
}

23.2 不要把用户输入直接拼进 system 提示词

这是非常常见的错误写法:

  • 把用户问题拼进 system
  • 把数据库原文不加处理塞进 system prompt
  • 让用户输入覆盖你的规则提示

更稳妥的原则是:

  • 固定规则放 system
  • 用户问题放 user
  • RAG 上下文走专门的 advisor 或受控模板

23.3 脱敏要发生在进入模型之前

如果用户输入里可能含有手机号、身份证、银行卡号、合同编号,可以先做脱敏或替换标记,再进入模型。

例如:

  • 13800138000 -> [PHONE]
  • 310xxxxxxxxxxxxx -> [ID_CARD]
  • 6222xxxxxxxxxxxx -> [BANK_CARD]

这样即使日志、监控、模型提供商链路中有暴露风险,也能明显降低影响面。

24. 企业安全版:审计落库

前面的增强版里提过审计字段,这里把它进一步落成"可持久化"的最小结构。

24.1 一个简单的审计实体

java 复制代码
package com.example.demo.audit;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.Instant;

@Entity
public class AiAuditRecord {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Instant createdAt;
    private String traceId;
    private String tenantId;
    private String userId;
    private String conversationId;
    private String provider;
    private String model;
    private String requestType;
    private String promptDigest;
    private Integer promptLength;
    private Integer responseLength;
    private Long latencyMs;
    private Integer inputTokens;
    private Integer outputTokens;
    private String resultStatus;
    private String toolNames;
    private String knowledgeSources;
    private String failureReason;

    protected AiAuditRecord() {
    }
}

上面的实体为了突出字段结构,省略了 getter/setter、构造器、索引和表注解细节。真实项目里你可以:

  • 手写 getter/setter
  • 使用 Lombok
  • 或者在 repository 层改成构造方式写入

24.2 一个最小仓库接口

java 复制代码
package com.example.demo.audit;

import org.springframework.data.jpa.repository.JpaRepository;

public interface AiAuditRecordRepository extends JpaRepository<AiAuditRecord, Long> {
}

24.3 审计落库时机

建议至少在下面这些节点记录:

  • 请求进入前
  • 模型调用完成后
  • 工具调用前后
  • RAG 检索完成后
  • 发生异常和降级时

这会让你之后追一条链路时轻松很多。

24.4 推荐的实现思路

审计最常见的失败方式不是"不会写库",而是"写得太散,最后串不起来"。比较稳妥的做法是把整个流程固定成一条链:

  1. 请求进入 controller 或 service 时创建审计上下文
  2. 提前拿到 traceIduserIdtenantIdconversationId
  3. 对原始输入做脱敏和摘要,不直接保存全文
  4. 调用模型、工具、RAG 检索
  5. 成功时统一落一条成功审计
  6. 失败时统一落一条失败审计
  7. 高风险工具和关键检索命中再补充事件日志

这里最重要的原则是:

  • 审计要围绕一次完整请求聚合
  • 审计和普通应用日志分开
  • 审计写失败不能影响主业务结果

24.5 审计上下文对象

先用一个上下文对象把请求期间的关键信息收拢起来,会比在各层到处传零散参数更稳。

java 复制代码
package com.example.demo.audit;

import java.time.Instant;

public record AiAuditContext(
        Instant startedAt,
        String traceId,
        String tenantId,
        String userId,
        String conversationId,
        String provider,
        String model,
        String requestType,
        String promptDigest,
        Integer promptLength
) {
}

这个对象的作用很简单:

  • start 时生成
  • 整个调用链往下传
  • 成功或失败时统一补全并落库

24.6 先做脱敏和摘要

审计不是把原始输入"原封不动存下来",而是先处理再存。一个实用做法是:

  • 保存脱敏后的摘要
  • 保存长度
  • 保存摘要哈希
  • 不保存原始正文

下面是一个简化版脱敏与摘要工具:

java 复制代码
package com.example.demo.audit;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;

@Component
public class SensitiveDataMasker {

    private static final Pattern PHONE = Pattern.compile("1\\d{10}");
    private static final Pattern ID_CARD = Pattern.compile("\\d{17}[\\dXx]");
    private static final Pattern BANK_CARD = Pattern.compile("\\d{16,19}");

    public String mask(String text) {
        if (text == null || text.isBlank()) {
            return "";
        }

        String masked = PHONE.matcher(text).replaceAll("[PHONE]");
        masked = ID_CARD.matcher(masked).replaceAll("[ID_CARD]");
        masked = BANK_CARD.matcher(masked).replaceAll("[BANK_CARD]");
        return masked;
    }

    public String digest(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] bytes = md.digest(mask(text).getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(bytes);
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException("SHA-256 not available", ex);
        }
    }
}

这个工具不复杂,但已经能满足大多数审计最小需求:

  • 同一段输入可稳定生成相同摘要
  • 不直接暴露原始敏感内容
  • 后续仍然可以基于摘要做排查和比对

24.7 审计服务怎么封装

比较推荐的方式,是让所有 AI 请求都先经过一个统一的审计服务,而不是每个 controller 自己写一遍。

java 复制代码
package com.example.demo.audit;

import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;

@Service
public class AiAuditService {

    private final AiAuditRecordRepository repository;
    private final SensitiveDataMasker masker;

    public AiAuditService(AiAuditRecordRepository repository, SensitiveDataMasker masker) {
        this.repository = repository;
        this.masker = masker;
    }

    public AiAuditContext start(
            String tenantId,
            Principal principal,
            String conversationId,
            String provider,
            String model,
            String requestType,
            String rawPrompt) {

        String traceId = readTraceId();
        String masked = masker.mask(rawPrompt);

        return new AiAuditContext(
                Instant.now(),
                traceId,
                tenantId,
                principal == null ? "anonymous" : principal.getName(),
                conversationId,
                provider,
                model,
                requestType,
                masker.digest(masked),
                masked.length());
    }

    public void success(
            AiAuditContext context,
            String responseContent,
            List<String> toolNames,
            List<String> knowledgeSources,
            Integer inputTokens,
            Integer outputTokens) {

        AiAuditRecord record = new AiAuditRecord();
        record.setCreatedAt(context.startedAt());
        record.setTraceId(context.traceId());
        record.setTenantId(context.tenantId());
        record.setUserId(context.userId());
        record.setConversationId(context.conversationId());
        record.setProvider(context.provider());
        record.setModel(context.model());
        record.setRequestType(context.requestType());
        record.setPromptDigest(context.promptDigest());
        record.setPromptLength(context.promptLength());
        record.setResponseLength(responseContent == null ? 0 : responseContent.length());
        record.setLatencyMs(Duration.between(context.startedAt(), Instant.now()).toMillis());
        record.setInputTokens(inputTokens);
        record.setOutputTokens(outputTokens);
        record.setToolNames(String.join(",", toolNames));
        record.setKnowledgeSources(String.join(",", knowledgeSources));
        record.setResultStatus("SUCCESS");

        saveQuietly(record);
    }

    public void failure(AiAuditContext context, Throwable ex) {
        AiAuditRecord record = new AiAuditRecord();
        record.setCreatedAt(context.startedAt());
        record.setTraceId(context.traceId());
        record.setTenantId(context.tenantId());
        record.setUserId(context.userId());
        record.setConversationId(context.conversationId());
        record.setProvider(context.provider());
        record.setModel(context.model());
        record.setRequestType(context.requestType());
        record.setPromptDigest(context.promptDigest());
        record.setPromptLength(context.promptLength());
        record.setLatencyMs(Duration.between(context.startedAt(), Instant.now()).toMillis());
        record.setResultStatus("FAILED");
        record.setFailureReason(ex.getClass().getSimpleName() + ":" + ex.getMessage());

        saveQuietly(record);
    }

    private void saveQuietly(AiAuditRecord record) {
        try {
            repository.save(record);
        } catch (Exception ignored) {
            // 审计失败不能反向打挂主业务
        }
    }

    private String readTraceId() {
        String traceId = MDC.get("traceId");
        return traceId == null || traceId.isBlank() ? UUID.randomUUID().toString() : traceId;
    }
}

上面这个实现里,有几个点很关键:

  • startsuccess/failure 分开,便于统一收口
  • saveQuietly 保证审计失败不会拖垮主流程
  • traceId 优先从链路上下文取,没有再自动生成
  • promptDigestpromptLength 替代原始正文

24.8 在 AI service 里怎么接

最适合接审计的位置,通常不是 controller,而是你真正编排 ChatClient、工具和 RAG 的 service。

java 复制代码
package com.example.demo.service;

import com.example.demo.audit.AiAuditContext;
import com.example.demo.audit.AiAuditService;
import java.security.Principal;
import java.util.List;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class EnterpriseChatService {

    private final ChatClient chatClient;
    private final AiAuditService auditService;

    public EnterpriseChatService(ChatClient.Builder builder, AiAuditService auditService) {
        this.chatClient = builder.build();
        this.auditService = auditService;
    }

    public String chat(String tenantId, Principal principal, String conversationId, String message) {
        AiAuditContext audit = auditService.start(
                tenantId,
                principal,
                conversationId,
                "openai",
                "gpt-4o-mini",
                "CHAT",
                message);

        try {
            String result = chatClient.prompt()
                    .system("你是企业知识助手,只回答授权范围内的问题。")
                    .user(message)
                    .call()
                    .content();

            auditService.success(
                    audit,
                    result,
                    List.of(),
                    List.of(),
                    null,
                    null);

            return result;
        } catch (Exception ex) {
            auditService.failure(audit, ex);
            throw ex;
        }
    }
}

这里示例里把 token 先传成 null,是因为不同模型提供商、不同响应路径的取值方式会不同。企业里更实用的做法通常是:

  • 优先通过 Spring AI 的 observability 指标看 token
  • 如果 provider 元数据好取,再补到审计表
  • 不要因为 token 还没接上,就卡住整个审计体系

24.9 工具调用和 RAG 检索怎么记

如果你的 AI 请求里会发生多个关键事件,建议不要只记最终结果,还要把关键节点补进去。最常见的有两类:

  • 工具调用事件
  • RAG 检索事件

最简单的做法有两种:

  1. 先把工具名、命中文档 ID 聚合到主表字段里
  2. 如果链路更复杂,再单独建一张事件表

一个常见的事件表思路是:

  • traceId
  • eventType
  • eventName
  • status
  • durationMs
  • payloadDigest

事件类型可以先约定成:

  • MODEL_REQUEST
  • TOOL_CALL
  • TOOL_RESULT
  • RAG_RETRIEVAL
  • MODEL_RESPONSE
  • FAILURE

如果现在还不想加第二张表,至少先保证主表里能记录:

  • toolNames
  • knowledgeSources
  • resultStatus
  • failureReason

24.10 controller 层最好补哪些字段

controller 层最适合补的是"身份和租户"相关信息,例如:

  • tenantId
  • conversationId
  • Principal
  • 请求来源系统

最不适合在 controller 层做的,是把整套审计组装逻辑写死在那里。更推荐:

  • controller 只收集身份上下文
  • service 负责业务编排
  • AiAuditService 负责审计聚合与落库

24.11 审计表设计的几个实战建议

  • traceIdcreatedAtuserIdconversationId 建索引
  • failureReason 不要无限长,建议截断
  • toolNamesknowledgeSources 如果很长,优先存摘要或事件表
  • 高并发场景下,审计落库可以异步化或写消息队列
  • 审计表要有归档和定期清理策略

24.12 审计失败时怎么办

企业里更稳的处理方式通常是:

  • 主业务成功,审计失败:记录错误日志并报警,但不回滚主请求
  • 主业务失败,审计成功:保留失败审计,便于排障
  • 主业务失败,审计也失败:至少保证普通应用日志里还有 traceId

换句话说:

审计很重要,但审计系统本身不应该成为 AI 主链路的单点故障。

25. 企业安全版:流量控制与滥用防护

AI 接口比普通 CRUD 更容易被滥用,因为:

  • 成本更高
  • 响应更慢
  • 一次请求可能触发模型、向量库、工具三段开销

25.1 至少限制这三类维度

  • 按用户限流
  • 按租户限额
  • 按接口分级限频

建议:

  • /ai/chat 可以相对宽松
  • /ai/tool/rag/load 要更严格
  • 管理类接口要有更低频率和更强审计

25.2 把成本控制也纳入安全治理

企业安全不只是"防攻击",还包括"防失控成本"。建议至少做:

  • 单用户每日 token 配额
  • 单租户月度预算
  • 高成本模型白名单
  • 超预算后的自动降级

这类策略对 AI 系统尤其重要。

26. 企业合规版:先定义治理边界

安全解决的是"能不能安全运行",合规解决的是"这样运行是否符合公司制度、客户承诺和监管要求"。

这里先给一个很重要的前提:

这部分内容适合作为企业内部落地参考,但不能替代你们公司的法务、合规、审计结论。

也就是说,Spring AI 可以帮你把能力接起来,但下面这些边界必须先由组织定义清楚:

  • 哪些数据允许进入外部模型
  • 哪些数据只能走本地模型或私有部署
  • 哪些场景必须人工审批
  • 哪些输出必须留痕和可追溯
  • 哪些日志允许保存,保存多久,谁能查看

如果这些边界没定好,技术实现再漂亮,后面也很容易返工。

27. 企业合规版:数据分级与最小化原则

AI 项目里最常见的合规问题,不是"模型答错",而是"本不该发出去的数据被发出去了"。

27.1 先做数据分级

建议至少把进入模型的数据分成四档:

  • P0:公开信息,可以进入外部模型
  • P1:内部一般信息,允许在受控条件下进入外部模型
  • P2:敏感业务信息,只允许脱敏后进入模型
  • P3:高度敏感信息,只允许本地模型、专有环境或完全禁止进入模型

典型例子:

  • P0:公开产品说明、帮助文档
  • P1:内部流程说明、非敏感 FAQ
  • P2:订单片段、工单内容、客户沟通记录
  • P3:身份证号、银行卡号、完整合同、财务底账、核心源代码密钥

27.2 最小化原则

进入模型前,优先遵循这三件事:

  • 能不传的字段就不传
  • 能摘要的内容不要传原文
  • 能脱敏的内容不要裸传

这条原则对 Prompt、RAG 上下文、工具参数、日志内容都成立。

27.3 一个简单的数据分级配置对象

java 复制代码
package com.example.demo.compliance;

import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.ai.compliance")
public class AiComplianceProperties {

    private boolean allowExternalModel = true;
    private Set<String> blockedTags = Set.of("P3", "SECRET");
    private Set<String> approvalRequiredTags = Set.of("P2", "FINANCE", "LEGAL");
    private int defaultRetentionDays = 30;

    public boolean isAllowExternalModel() {
        return allowExternalModel;
    }

    public void setAllowExternalModel(boolean allowExternalModel) {
        this.allowExternalModel = allowExternalModel;
    }

    public Set<String> getBlockedTags() {
        return blockedTags;
    }

    public void setBlockedTags(Set<String> blockedTags) {
        this.blockedTags = blockedTags;
    }

    public Set<String> getApprovalRequiredTags() {
        return approvalRequiredTags;
    }

    public void setApprovalRequiredTags(Set<String> approvalRequiredTags) {
        this.approvalRequiredTags = approvalRequiredTags;
    }

    public int getDefaultRetentionDays() {
        return defaultRetentionDays;
    }

    public void setDefaultRetentionDays(int defaultRetentionDays) {
        this.defaultRetentionDays = defaultRetentionDays;
    }
}

对应配置示意:

yaml 复制代码
app:
  ai:
    compliance:
      allow-external-model: true
      blocked-tags: [P3, SECRET]
      approval-required-tags: [P2, FINANCE, LEGAL]
      default-retention-days: 30

这类配置的价值,在于把"制度要求"落成"系统可执行规则"。

28. 企业合规版:模型供应商准入与第三方管理

企业 AI 系统不是只选"哪个模型更聪明",还要评估"这个供应商能不能进生产"。

28.1 供应商准入至少看这些项

  • 数据是否会被用于模型训练
  • 是否支持关闭训练或关闭数据保留
  • 数据存储和处理区域在哪里
  • 是否支持企业合同、DPA、SLA
  • 是否支持专有网络、私有化或区域隔离
  • 是否提供调用审计、费用明细和权限管理

28.2 给每个供应商建立准入档案

建议为每个模型供应商维护一个最小档案:

  • 供应商名称
  • 允许使用的业务场景
  • 禁止输入的数据类型
  • 默认保留策略
  • 是否允许生产使用
  • 是否允许敏感场景使用
  • 是否允许跨境传输

你可以把这件事理解成"模型供应商白名单"。

28.3 一个简单的供应商合规档案对象

java 复制代码
package com.example.demo.compliance;

public record ModelVendorProfile(
        String vendor,
        boolean productionAllowed,
        boolean crossBorderAllowed,
        boolean trainingOptOutSupported,
        boolean sensitiveDataAllowed,
        String retentionPolicy,
        String approvedUseCases
) {
}

这个对象本身不复杂,但它能帮助团队把"采购/法务结论"变成"系统配置输入"。

29. 企业合规版:留存、删除与可追溯

很多团队会做审计,却忘了"审计数据也属于需要治理的数据"。

29.1 哪些内容需要定义留存策略

建议至少明确下面这些对象的保存期限:

  • 原始用户请求
  • 脱敏后的 Prompt 摘要
  • 模型响应结果
  • 工具调用记录
  • RAG 命中文档引用
  • 审计日志
  • 成本和 token 统计

29.2 留存策略不要一刀切

比较常见的实践是:

  • 普通问答日志:短期保存
  • 高风险操作审计:中长期保存
  • 敏感内容正文:尽量不保存或只保存脱敏摘要
  • 训练/评估样本:单独走审批与标识

29.3 一个最小保留策略对象

java 复制代码
package com.example.demo.compliance;

public record RetentionPolicy(
        String dataType,
        int retentionDays,
        boolean storeRawContent,
        boolean requireMaskedStorage,
        boolean allowUserDeletionRequest
) {
}

29.4 删除流程要可执行

企业里更实际的问题往往是:

  • 用户要求删除某次会话怎么办
  • 客户要求删除某租户历史数据怎么办
  • 员工离职后相关会话如何处理

所以你最好提前设计:

  • userId 删除
  • tenantId 删除
  • conversationId 删除
  • 按日期批量清理

删除本身也要记审计日志,否则后面追不回来。

30. 企业合规版:人工审批与责任链

不是所有 AI 输出都能直接执行。很多企业场景里,模型最多只能做到"建议生成",最后决定仍然需要人来签字。

30.1 哪些场景建议强制审批

  • 对外发送正式文本
  • 涉及合同、价格、财务、法务内容
  • 导出客户数据
  • 修改订单、库存、资金、权限
  • 生成可能影响客户权益的结论

30.2 把 AI 输出区分成三类

  • assist:仅供参考,不直接生效
  • review:需要人工复核后才能继续
  • execute:经过授权和确认后才能执行

这类分层非常适合接到你的工作流或 BPM 系统里。

30.3 一个简单的审批状态枚举

java 复制代码
package com.example.demo.compliance;

public enum AiDecisionMode {
    ASSIST,
    REVIEW,
    EXECUTE
}

这个枚举很简单,但很适合做系统内约束:

  • 默认 ASSIST
  • 高风险场景强制 REVIEW
  • 只有极少数白名单场景进入 EXECUTE

31. 企业合规版:输出声明、用户告知与使用边界

很多合规问题不在模型内部,而在"用户是不是知道这是一段 AI 生成内容"。

31.1 建议明确告知用户的内容

  • 当前内容由 AI 生成或辅助生成
  • 回答可能存在不完整或不准确情况
  • 高风险问题需要人工复核
  • 输入内容可能进入模型处理链路
  • 哪些内容不允许输入

31.2 高风险场景建议追加引用和免责声明

尤其是在这些场景:

  • 政策解读
  • 合同条款说明
  • 财务建议
  • 医疗建议
  • 合规判断

建议至少返回:

  • 引用来源
  • 更新时间
  • 审核状态
  • "最终以人工/正式制度/原始文件为准"的说明

32. 企业合规版:上线前检查清单

如果你准备把 Spring AI 服务真正推进生产,至少确认下面这些问题已经有答案:

  1. 输入给模型的数据是否做了分级和脱敏。
  2. 不同模型供应商的允许场景和禁止场景是否有书面结论。
  3. 是否存在必须走本地模型或私有部署的场景。
  4. 审计日志是否可查到用户、会话、模型、工具和结果状态。
  5. 是否定义了日志和会话的保留与删除策略。
  6. 高风险工具是否有角色控制和二次确认。
  7. RAG 命中的文档是否有权限过滤。
  8. 是否能对外证明某次结果经过了哪些流程和谁审批。
  9. 是否向用户明确告知了 AI 生成、限制和责任边界。
  10. 法务、合规、审计、业务方是否都已确认上线边界。

这 10 条做完,你的系统不一定"完全没风险",但通常已经从 demo 阶段走到了可治理阶段。

33. 常见坑

33.1 只会调用模型,不会设计输出

很多项目一开始只会:

  • 拼字符串 Prompt
  • 拿一段大文本回来
  • 再手动拆字段

这很快会变难维护。正确思路通常是:

  • 能对象化就对象化
  • 能工具化就工具化
  • 能检索增强就别纯靠模型记忆

33.2 把工具调用当成万能 Agent

Tool Calling 很强,但不代表适合所有流程。下面这类场景更稳:

  • 明确工具清单
  • 明确输入输出
  • 明确调用边界
  • 明确失败处理

如果业务链路非常复杂,建议先把流程拆成确定性步骤,再让模型参与其中一部分,而不是一开始就做全自动 Agent。

33.3 向量库维度不匹配

这是最常见的 RAG 问题之一。Embedding 模型变了,维度也可能变。你需要确认:

  • 向量表的维度
  • 当前 Embedding 模型输出维度
  • 老数据是否需要重建

否则你会遇到插入失败或检索异常。

33.4 没有观测就直接上线

没有监控的 AI 服务通常会遇到这些问题:

  • 用户说"很慢",你不知道慢在模型、向量库还是工具
  • 成本突然上涨,你不知道是哪类请求导致
  • 出现幻觉,你拿不到当时的上下文和命中来源

企业里这不是"优化项",而是基本盘。

33.5 会话 ID 和用户身份脱钩

如果 conversationId 由前端自由传入,又没有绑定用户和租户,很容易出现:

  • 读到别人的上下文
  • 会话串线
  • 审计记录无法还原真实操作者

所以生产环境里最好由服务端生成和校验会话 ID。

33.6 把高风险工具直接交给模型

如果模型能直接执行"导出数据""修改订单""删除记录"这类操作,而没有角色控制和二次确认,这基本就是埋雷。

33.7 只做安全,不做合规边界

很多团队会把鉴权、脱敏、日志做得不错,但没有回答这些问题:

  • 哪些数据可以进外部模型
  • 哪些场景必须人工复核
  • 哪些结果允许自动执行
  • 哪些日志必须定期清理

这会导致系统"技术上能跑",但组织上没人敢真正承担责任。

34. 一条最实用的学习顺序

如果你要真正学会 Spring AI,建议按这个顺序来:

  1. 跑通最小聊天接口
  2. 把返回值改成结构化对象
  3. 给模型加一个工具
  4. 再做会话记忆
  5. 给接口接入认证和角色控制
  6. 再做 RAG
  7. 补审计、限流、降级和安全治理
  8. 最后把数据分级、供应商准入、审批链和留存删除制度补完整

这条路线最接近真实项目推进顺序,也最不容易一开始就把自己绕晕。

35. 参考链接

相关推荐
醉卧考场君莫笑2 小时前
NLP(命名实体识别NER)
人工智能·自然语言处理
Hello world.Joey2 小时前
YOLO和SiamFC的不同之处
人工智能·计算机视觉·目标跟踪
我是无敌小恐龙2 小时前
Java SE 零基础入门Day03 数组核心详解(定义+内存+遍历+算法+实战案例)
java·开发语言·数据结构·人工智能·算法·aigc·动态规划
Byron__2 小时前
AI学习_03_LangChain_RAG基础概念
人工智能·学习·langchain
科技AI训练师2 小时前
2026工业风机行业观察:英飞风机在中高端通风排烟领域表现
大数据·人工智能
月诸清酒2 小时前
39-260422 AI 科技日报 (OpenAI 发布 GPT-Image-2:视觉理解力登顶)
人工智能·gpt
Yu_Lijing2 小时前
Python数据分析和数据处理库Pandas(数据组合函数)
人工智能·数据挖掘·数据分析·pandas
繁星星繁2 小时前
【AI】Langchain(一)
人工智能·langchain
中科天工2 小时前
中科天工智能包装技术是什么?
大数据·人工智能