
Spring AI 企业增强版(含安全版与合规版)
基于 Spring AI 官方参考文档整理的中文实战教程。
参考版本:Spring AI
1.1.4,支持 Spring Boot3.4.x / 3.5.x。官方文档入口:https://docs.spring.io/spring-ai/reference/index.html
学习记录
1. 这份文档适合谁
如果你已经会 Spring Boot,但第一次真正落地 AI 功能,这份文档可以直接带你走完一条典型路线:
- 接一个聊天模型
- 暴露一个 HTTP 接口
- 返回结构化结果
- 给模型加工具调用
- 加会话记忆
- 接入 PGvector 做 RAG
- 补齐日志、监控、超时、重试、审计与安全边界
- 补齐数据分级、模型供应商准入、留存删除与审批闭环
本文默认使用 OpenAI 作为示例模型供应商,因为官方文档示例最完整、上手最快。后续如果你换成 Ollama、Anthropic、Azure OpenAI,业务层代码大多可以复用,主要改 starter 和配置。
2. Spring AI 到底解决什么问题
Spring AI 的核心价值,不是"帮你调一次大模型接口",而是把 AI 能力放进 Spring 的工程体系里:
- 用统一抽象访问不同模型提供商
- 用 Spring Boot 自动配置减少接线代码
- 用
ChatClient、Structured Output、Tool Calling、Advisors、Chat Memory、RAG这些组件搭业务链路 - 让 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.x或3.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 / Embeddingspring-ai-starter-vector-store-pgvector:接入 PGvectorspring-ai-advisors-vector-store:开箱即用的 RAG Advisorspring-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-openaispring-boot-starter-web
如果你暂时不做生产监控,也可以先不加:
spring-boot-starter-actuatormicrometer-registry-prometheus
如果你暂时不做审计落库,也可以先不加:
spring-boot-starter-data-jpa
如果你是在内网做 PoC,暂时也可以先不加:
spring-boot-starter-securityspring-boot-starter-oauth2-resource-serverspring-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.2到0.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
- 设置
system和user - 调用
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分钟后是几点"
这个过程中:
- 模型判断自己需要当前时间
- Spring AI 执行
currentTime() - 执行结果返回给模型
- 模型再组织最终回答
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 AI1.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
可以按下面方式落地:
config:ChatClient、ChatMemory、VectorStore相关配置controller:HTTP 接口service:AI 编排逻辑tool:给模型调用的工具rag:文档导入、检索增强model:结构化输出对象、DTO
13. 实战时最值得遵守的 8 条建议
- 业务代码优先用
ChatClient,不要一开始就沉到过低层 API。 - 需要稳定结果时优先用结构化输出,而不是解析自然语言。
- 工具调用只暴露必要能力,不要把内部服务一股脑交给模型。
- 多轮对话一定要传
conversationId,否则记忆会串。 - RAG 先从小规模文档验证,不要一开始就全量导库。
temperature先低后高,业务系统通常更需要稳定而不是发散。- 日志里要记录模型、耗时、token、错误类型和命中的知识来源。
- 把 AI 看成"不稳定外部依赖",加超时、重试、降级和兜底提示。
14. 企业增强:可观测性
Spring AI 官方文档明确提供了观测能力,覆盖:
ChatClientAdvisorChatModelEmbeddingModelVectorStore
如果你准备上线,建议第一天就把可观测性接上,而不是等问题出现后再补。
14.1 最小监控依赖
上面依赖中的这两个组件就是最常用组合:
spring-boot-starter-actuatormicrometer-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:可有限重试
- 参数错误、提示词错误、权限错误:不要重试
- 超时后是否重试:要看接口是否是强交互场景
建议原则:
- 最多
1到2次重试 - 使用指数退避
- 把每次重试记录到审计日志
- 流式响应场景谨慎重试
15.3 给出明确降级策略
企业项目不能只返回 500。至少要准备三种兜底:
- 文本兜底:
当前服务繁忙,请稍后再试 - 规则兜底:改走关键词检索或固定模板
- 人工兜底:进入工单或人工审核
16. 企业增强:审计、日志与敏感信息治理
很多团队上线后第一个问题不是"模型答得准不准",而是"出了问题到底是谁、在什么时候、用哪个模型、对哪条数据做了什么"。
16.1 审计日志至少记录这些字段
traceIdconversationIduserIdmodelproviderlatencyMsinputTokenoutputTokentoolNamesknowledgeSourcesresultStatusfailureReason
16.2 不要直接记录原始 Prompt
官方文档也明确提示:Prompt 和 Completion 可能包含敏感信息,默认不应该直接导出。企业里更建议这么做:
- 日志记录摘要和长度,不记录全量正文
- 对手机号、身份证号、邮箱、地址做脱敏
- 对内部文档片段只记录
sourceId、documentId - 把完整会话单独进入受控审计存储,而不是普通应用日志
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 官方文档提供了 Evaluator 和 RelevancyEvaluator 这条路线。
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 检索要带上数据权限条件
如果你的知识库按租户、部门、项目隔离,向量检索不能只做"语义最相近",还要做"当前用户能看见"。
推荐做法:
- 文档入库时写入
tenantId、departmentId、visibility等 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 对高风险操作增加确认票据
企业里更建议把高风险动作拆成两步:
- 模型生成操作建议
- 用户点击确认或审批通过后再执行
这比"模型一决定就直接写库"安全得多。
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 推荐的实现思路
审计最常见的失败方式不是"不会写库",而是"写得太散,最后串不起来"。比较稳妥的做法是把整个流程固定成一条链:
- 请求进入 controller 或 service 时创建审计上下文
- 提前拿到
traceId、userId、tenantId、conversationId - 对原始输入做脱敏和摘要,不直接保存全文
- 调用模型、工具、RAG 检索
- 成功时统一落一条成功审计
- 失败时统一落一条失败审计
- 高风险工具和关键检索命中再补充事件日志
这里最重要的原则是:
- 审计要围绕一次完整请求聚合
- 审计和普通应用日志分开
- 审计写失败不能影响主业务结果
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;
}
}
上面这个实现里,有几个点很关键:
start和success/failure分开,便于统一收口saveQuietly保证审计失败不会拖垮主流程traceId优先从链路上下文取,没有再自动生成promptDigest和promptLength替代原始正文
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 检索事件
最简单的做法有两种:
- 先把工具名、命中文档 ID 聚合到主表字段里
- 如果链路更复杂,再单独建一张事件表
一个常见的事件表思路是:
traceIdeventTypeeventNamestatusdurationMspayloadDigest
事件类型可以先约定成:
MODEL_REQUESTTOOL_CALLTOOL_RESULTRAG_RETRIEVALMODEL_RESPONSEFAILURE
如果现在还不想加第二张表,至少先保证主表里能记录:
toolNamesknowledgeSourcesresultStatusfailureReason
24.10 controller 层最好补哪些字段
controller 层最适合补的是"身份和租户"相关信息,例如:
tenantIdconversationIdPrincipal- 请求来源系统
最不适合在 controller 层做的,是把整套审计组装逻辑写死在那里。更推荐:
- controller 只收集身份上下文
- service 负责业务编排
AiAuditService负责审计聚合与落库
24.11 审计表设计的几个实战建议
- 给
traceId、createdAt、userId、conversationId建索引 failureReason不要无限长,建议截断toolNames和knowledgeSources如果很长,优先存摘要或事件表- 高并发场景下,审计落库可以异步化或写消息队列
- 审计表要有归档和定期清理策略
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:内部流程说明、非敏感 FAQP2:订单片段、工单内容、客户沟通记录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 服务真正推进生产,至少确认下面这些问题已经有答案:
- 输入给模型的数据是否做了分级和脱敏。
- 不同模型供应商的允许场景和禁止场景是否有书面结论。
- 是否存在必须走本地模型或私有部署的场景。
- 审计日志是否可查到用户、会话、模型、工具和结果状态。
- 是否定义了日志和会话的保留与删除策略。
- 高风险工具是否有角色控制和二次确认。
- RAG 命中的文档是否有权限过滤。
- 是否能对外证明某次结果经过了哪些流程和谁审批。
- 是否向用户明确告知了 AI 生成、限制和责任边界。
- 法务、合规、审计、业务方是否都已确认上线边界。
这 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,建议按这个顺序来:
- 跑通最小聊天接口
- 把返回值改成结构化对象
- 给模型加一个工具
- 再做会话记忆
- 给接口接入认证和角色控制
- 再做 RAG
- 补审计、限流、降级和安全治理
- 最后把数据分级、供应商准入、审批链和留存删除制度补完整
这条路线最接近真实项目推进顺序,也最不容易一开始就把自己绕晕。
35. 参考链接
- Spring AI Reference: https://docs.spring.io/spring-ai/reference/index.html
- Getting Started: https://docs.spring.io/spring-ai/reference/getting-started.html
- Chat Client API: https://docs.spring.io/spring-ai/reference/api/chatclient.html
- Advisors API: https://docs.spring.io/spring-ai/reference/api/advisors.html
- Structured Output: https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html
- Tool Calling: https://docs.spring.io/spring-ai/reference/api/tools.html
- Chat Memory: https://docs.spring.io/spring-ai/reference/api/chat-memory.html
- Retrieval Augmented Generation: https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html
- Observability: https://docs.spring.io/spring-ai/reference/observability/index.html
- Evaluation Testing: https://docs.spring.io/spring-ai/reference/api/testing.html
- ETL Pipeline: https://docs.spring.io/spring-ai/reference/api/etl-pipeline.html
- PGvector: https://docs.spring.io/spring-ai/reference/api/vectordbs/pgvector.html
- OpenAI Chat: https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html
- Spring Security Reference: https://docs.spring.io/spring-security/reference/