Spring AI 从零到精通验收清单
这页用来验收你是否真的把 Spring AI 学到了“能解释原理、能写 demo、能做商业项目、能排查线上问题、能面试回答”的程度。
Spring AI 不是“Java 调一下大模型接口”。真正学懂要能说清楚:
- 为什么企业项目需要 Spring AI,而不是直接在 Controller 里写 HTTP 调模型。
ChatClient、ChatModel、Prompt、Advisor、EmbeddingModel、VectorStore、Tool Calling分别负责什么。- 一次 AI 请求从前端进入后端,到模型返回,中间经过哪些安全、编排、检索、工具、解析和观测步骤。
- RAG 为什么要先切分、向量化、保存元数据、权限过滤、召回、重排,再交给模型。
- Tool Calling 为什么不能让模型直接决定权限,为什么写操作必须幂等和二次确认。
- 结构化输出为什么会失败,生产中如何做校验、重试、降级。
- 上线后如何监控 token、耗时、召回质量、幻觉、安全、成本和失败率。
总学习路线
flowchart TD
A["阶段1:理解 Spring AI 解决什么问题"] --> B["阶段2:ChatClient 和 ChatModel 调用链"]
B --> C["阶段3:Prompt 模板和结构化输出"]
C --> D["阶段4:Advisor 扩展请求链路"]
D --> E["阶段5:Embedding 和 VectorStore"]
E --> F["阶段6:RAG 从入库到问答全过程"]
F --> G["阶段7:Tool Calling 调用业务能力"]
G --> H["阶段8:记忆、会话和上下文治理"]
H --> I["阶段9:生产安全、评估、成本和排查"]
I --> J["阶段10:面试和商业项目表达"]这条路线的重点是先建立“后端 AI 编排层”的概念。模型只是能力来源,真正的业务系统还要负责权限、上下文、资料、工具、安全、监控和失败兜底。
阶段1:Spring AI 到底解决什么问题
最原始的模型调用可能这样写:
@PostMapping("/chat")
public String chat(@RequestBody ChatRequest request) {
return httpClient.post("https://model.example.com/chat", request.question());
}Demo 可以跑,但商业系统会很快失控:
| 问题 | 不治理的后果 |
|---|---|
| 模型供应商接口散落在业务代码里 | 换模型要改很多地方 |
| Prompt 写在各个 Controller | 不可版本化、不可评估、不可回滚 |
| RAG 检索逻辑分散 | 召回质量难排查,权限过滤容易漏 |
| 工具调用没有统一安全边界 | 模型可能调用越权参数或高风险操作 |
| 输出没有结构化校验 | JSON 解析失败,业务流程中断 |
| 没有 token 和耗时统计 | 成本失控,慢请求不知道慢在哪里 |
Spring AI 的定位是把这些能力变成 Spring 风格的工程组件:
flowchart TD
A["业务 Controller"] --> B["AI 编排 Service"]
B --> C["ChatClient"]
C --> D["Advisor 链"]
D --> E["ChatModel"]
D --> F["VectorStore"]
D --> G["Tool Callback"]
E --> H["模型供应商或本地模型"]
F --> I["向量数据库"]
G --> J["业务系统"]如果你只会 ChatClient.prompt().user().call(),只是入门;真正要掌握的是这条链路上每一层为什么存在、出了问题怎么定位。
阶段2:核心对象关系
Spring AI 核心对象可以按“请求、模型、上下文、资料、工具、治理”理解:
| 对象 | 解决的问题 | 类比 |
|---|---|---|
ChatClient | 面向业务的模型调用入口 | 类似 RestTemplate/WebClient 的 AI 版本 |
ChatModel | 底层模型适配 | 真正和模型服务交互 |
Prompt | 发送给模型的完整输入 | 系统指令、用户问题、上下文 |
Message | Prompt 中的消息 | system、user、assistant、tool |
Advisor | 调用前后增强 | 类似拦截器,补 RAG、记忆、安全规则 |
EmbeddingModel | 文本转向量 | 把语义变成可检索向量 |
VectorStore | 存储和检索向量 | RAG 的检索层 |
Tool Callback | 暴露 Java 方法给模型 | 模型选择工具,后端执行 |
ChatResponse | 模型返回结果 | 包含文本、元数据、token 信息 |
一次普通聊天调用:
@RestController
public class AiChatController {
private final ChatClient chatClient;
public AiChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是企业内部技术助手,回答要准确、简洁、可执行。")
.build();
}
@GetMapping("/ai/chat")
public String chat(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}这段代码看起来简单,但底层发生了几件事:
flowchart TD
A["Controller 接收 question"] --> B["ChatClient 创建请求"]
B --> C["合并默认 system 和 user message"]
C --> D["执行 Advisor 前置逻辑"]
D --> E["调用 ChatModel"]
E --> F["模型服务生成结果"]
F --> G["执行 Advisor 后置逻辑"]
G --> H["返回 content"]面试时不要只说“调用模型返回字符串”,要说出 Prompt 组装、Advisor 增强、ChatModel 适配和响应解析。
阶段3:ChatClient 调用链为什么要分层
ChatClient 面向业务,ChatModel 面向模型适配。
为什么不让业务直接调 ChatModel?
直接调 ChatModel 的问题 | 使用 ChatClient 的好处 |
|---|---|
| 每次都要手工组装 Prompt | 可以统一默认 system、advisor、options |
| 不好复用 RAG、记忆、安全逻辑 | Advisor 可以统一增强 |
| 业务代码绑定模型细节 | 更容易替换模型 |
| 流式、结构化输出、工具调用代码分散 | API 更清晰 |
同步调用适合短文本:
String answer = chatClient.prompt()
.system("你是订单客服助手。")
.user("订单 1001 为什么还没发货?")
.call()
.content();流式调用适合长回答和聊天体验:
@GetMapping(value = "/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}流式输出并不一定减少总耗时,它主要降低首字等待,让用户更早看到内容。如果业务要求完整 JSON 校验,直接流式返回可能不合适,因为你必须等完整结果才能解析和校验。
阶段4:Prompt 和结构化输出
Prompt 不是一句“你是专家”。生产 Prompt 至少包含:
| 部分 | 作用 |
|---|---|
| 角色 | 让模型知道身份和回答风格 |
| 任务 | 明确要做什么 |
| 上下文 | 提供资料、规则、业务数据 |
| 约束 | 禁止编造、必须引用、不能泄露 |
| 输出格式 | JSON、表格、步骤、字段 |
| 拒答条件 | 资料不足、权限不足、风险过高 |
结构化输出 Demo:
public record TicketClassifyResult(
String category,
String priority,
String reason
) {
}TicketClassifyResult result = chatClient.prompt()
.system("""
你是工单分类助手。
只能输出 JSON。
category 只能是 DATA_COLLECT、ASSET_MANAGE、SYSTEM_ERROR。
priority 只能是 LOW、MEDIUM、HIGH。
""")
.user("采集任务连续失败,提示数据库连接超时")
.call()
.entity(TicketClassifyResult.class);为什么结构化输出仍然会失败?
| 失败原因 | 现象 | 处理 |
|---|---|---|
| Prompt 没约束字段枚举 | 输出字段乱写 | 加 JSON Schema 或明确枚举 |
| 用户输入诱导模型改格式 | 多出解释文本 | 输出校验失败后重试 |
| 模型能力不足 | JSON 缺字段 | 换模型或降低任务复杂度 |
| 上下文太长 | 关键格式要求被稀释 | 把格式规则放到更稳定位置 |
生产做法是:模型输出不能直接信任,必须解析、校验、失败重试或降级。
public TicketClassifyResult classify(String text) {
try {
TicketClassifyResult result = chatClient.prompt()
.system(CLASSIFY_PROMPT)
.user(text)
.call()
.entity(TicketClassifyResult.class);
validateResult(result);
return result;
} catch (Exception ex) {
return new TicketClassifyResult("SYSTEM_ERROR", "MEDIUM", "AI 分类失败,进入人工复核");
}
}阶段5:Advisor 是什么
Advisor 可以理解为 AI 请求链路上的拦截器。它可以在模型调用前后加入通用能力,例如:
- 加入历史对话记忆。
- 根据问题检索知识库。
- 加入安全提示和拒答规则。
- 记录日志、token、耗时。
- 对输出做后处理。
flowchart TD
A["用户问题"] --> B["ChatClient"]
B --> C["Advisor1:安全规则"]
C --> D["Advisor2:RAG 检索"]
D --> E["Advisor3:会话记忆"]
E --> F["ChatModel"]
F --> G["模型响应"]
G --> H["Advisor 后处理和日志"]为什么 Advisor 重要?
如果没有 Advisor,RAG、记忆、安全、日志都要写进每个业务方法。代码重复只是表面问题,更大的问题是安全策略和日志指标不一致,出了线上问题无法统一排查。
阶段6:Embedding 和 VectorStore
Embedding 是把文本转换成向量,让系统能用数学距离近似表达语义相似。
flowchart TD
A["文本:数据库连接超时"] --> B["EmbeddingModel"]
B --> C["向量:[0.12, -0.31, ...]"]
C --> D["VectorStore"]为什么入库和查询必须使用同一个 Embedding 模型?
因为向量距离只在同一个向量空间里有意义。文档用模型 A 转向量,问题用模型 B 转向量,距离比较就像用两把不同刻度的尺子量同一个物体,结果不可信。
入库 Demo:
@Service
public class DocIngestService {
private final VectorStore vectorStore;
public DocIngestService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void ingest(Long docId, String title, List<String> chunks) {
List<Document> documents = chunks.stream()
.map(chunk -> new Document(chunk, Map.of(
"docId", docId,
"title", title,
"status", "active",
"tenantId", "hospital-a",
"source", "/docs/" + docId
)))
.toList();
vectorStore.add(documents);
}
}元数据不是可有可无。没有 tenantId、status、source、docId,你就无法做权限过滤、删除同步、引用来源和问题排查。
阶段7:RAG 全过程
RAG 分成离线入库和在线问答两条链路。
flowchart TD
A["原始文档"] --> B["解析和清洗"]
B --> C["脱敏和去重"]
C --> D["按标题、段落、语义切分 Chunk"]
D --> E["补元数据:来源、版本、权限、租户"]
E --> F["Embedding 向量化"]
F --> G["写入 VectorStore"]flowchart TD
A["用户问题"] --> B["鉴权和租户识别"]
B --> C["问题改写和关键词提取"]
C --> D["问题向量化"]
D --> E["按权限过滤检索 VectorStore"]
E --> F["召回 TopK Chunk"]
F --> G["Rerank 和阈值判断"]
G --> H["组装带引用的 Prompt"]
H --> I["模型基于资料回答"]
I --> J["引用校验和日志记录"]RAG 查询 Demo:
@RestController
public class RagController {
private final ChatClient chatClient;
public RagController(ChatClient.Builder builder, VectorStore vectorStore) {
this.chatClient = builder
.defaultSystem("""
你是企业知识库助手。
必须优先依据检索资料回答。
如果资料不足,明确回答“当前资料不足”。
回答末尾列出引用来源。
""")
.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).build())
.build();
}
@GetMapping("/ai/rag")
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}真实商业项目还要加权限过滤。不能全库召回后再让模型“不要泄露”,因为模型不是权限系统。
public String askWithPermission(String question, CurrentUser user) {
SearchRequest searchRequest = SearchRequest.builder()
.query(question)
.topK(8)
.filterExpression("tenantId == '" + user.tenantId() + "' && status == 'active'")
.build();
List<Document> docs = vectorStore.similaritySearch(searchRequest);
String context = buildContext(docs);
return chatClient.prompt()
.system("只能根据资料回答,不能编造。")
.user("资料:\n" + context + "\n\n问题:" + question)
.call()
.content();
}如果权限过滤放在模型回答阶段,召回结果里可能已经包含用户无权看到的内容。即使模型最后不输出,也可能被 Prompt 注入诱导泄露。
阶段8:Tool Calling 全过程
Tool Calling 的核心不是“模型执行 Java 方法”,而是模型提出工具调用意图,后端负责校验和执行。
flowchart TD
A["用户问题"] --> B["ChatClient 携带工具定义"]
B --> C["模型判断需要调用哪个工具"]
C --> D["模型返回工具名和参数"]
D --> E["Spring AI 映射到 Java 方法"]
E --> F["后端做鉴权、参数校验、幂等和审计"]
F --> G["调用业务服务"]
G --> H["工具结果返回模型"]
H --> I["模型生成最终回答"]工具定义 Demo:
@Service
public class AssetTools {
private final AssetService assetService;
private final CurrentUserService currentUserService;
public AssetTools(AssetService assetService,
CurrentUserService currentUserService) {
this.assetService = assetService;
this.currentUserService = currentUserService;
}
@Tool(description = "根据资产编号查询当前用户有权限访问的资产状态")
public AssetStatusResult queryAssetStatus(String assetCode) {
CurrentUser user = currentUserService.current();
if (!user.hasAuthority("asset:read")) {
throw new AccessDeniedException("无权查询资产");
}
return assetService.queryStatusWithDataScope(assetCode, user);
}
}调用 Demo:
@RestController
public class AssetAiController {
private final ChatClient chatClient;
public AssetAiController(ChatClient.Builder builder, AssetTools assetTools) {
this.chatClient = builder
.defaultSystem("你是资产管理助手。查询资产必须通过工具,不允许编造资产状态。")
.defaultTools(assetTools)
.build();
}
@GetMapping("/ai/assets")
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}高风险写操作不能直接开放:
| 工具类型 | 风险 | 要求 |
|---|---|---|
| 查询资产 | 泄露数据 | 鉴权、数据权限、脱敏 |
| 创建工单 | 重复创建 | 幂等号、参数校验 |
| 发送短信 | 资费和骚扰 | 频率限制、模板白名单 |
| 删除数据 | 不可逆 | 通常不直接给模型调用 |
| 改权限 | 极高风险 | 禁止或人工审批 |
写操作安全 Demo:
@Tool(description = "创建采集异常工单,必须由用户确认后才能执行")
public CreateTicketResult createCollectErrorTicket(CreateTicketCommand command) {
CurrentUser user = currentUserService.current();
permissionService.check(user, "ticket:create");
idempotentService.check(command.requestId());
validateTicket(command);
return ticketService.create(command, user);
}模型可以提出“建议创建工单”,但是否真的创建,必须由后端权限、参数、幂等和用户确认决定。
阶段9:记忆和上下文治理
AI 记忆不是把所有聊天记录无限塞给模型。上下文越长,成本越高,延迟越高,噪声越多,也更容易泄露。
| 记忆类型 | 说明 | 风险 |
|---|---|---|
| 短期对话历史 | 当前会话的前几轮消息 | token 膨胀 |
| 摘要记忆 | 把历史压缩成摘要 | 摘要错误会污染后续回答 |
| 用户偏好 | 常用语言、输出习惯 | 不能保存敏感偏好 |
| 业务状态 | 当前订单、资产、任务 | 必须来自后端可信数据 |
上下文裁剪原则:
- 只放回答当前问题必要的信息。
- 敏感字段先脱敏。
- 多轮历史保留最近关键轮次。
- RAG Chunk 要有来源和权限。
- 工具返回值要裁剪,不要整对象塞给模型。
阶段10:生产治理
上线后不能只看“能不能回答”,要看完整指标:
| 指标 | 为什么重要 |
|---|---|
| TTFT | 首 token 等待,影响用户体感 |
| 总耗时 | 完整回答时间 |
| 输入/输出 token | 成本和延迟核心来源 |
| RAG 召回命中率 | 决定是否找到资料 |
| 引用命中率 | 决定回答是否有依据 |
| JSON 解析成功率 | 决定业务流程是否稳定 |
| 工具调用成功率 | 决定 Agent 是否可靠 |
| 权限拦截次数 | 发现越权尝试 |
| fallback 次数 | 发现模型或下游不可用 |
| 单用户/单租户成本 | 防止成本失控 |
日志建议记录:
requestId
userId / tenantId
scenario
promptVersion
modelName
knowledgeBaseVersion
retrievedChunkIds
toolNames
inputTokens / outputTokens
latencyMs
status
errorCode不要记录完整 Prompt、完整 RAG 原文、完整工具返回值和密钥。日志系统通常有更多人能访问,完整记录反而扩大泄露面。
阶段11:生产排查
AI 请求慢
flowchart TD
A["AI 请求慢"] --> B["看是首 token 慢还是总耗时慢"]
B --> C["入口排队和限流"]
C --> D["RAG 检索耗时"]
D --> E["Rerank 耗时"]
E --> F["工具调用耗时"]
F --> G["Prompt token 是否过长"]
G --> H["模型生成速度和供应商状态"]RAG 答错
flowchart TD
A["RAG 答错"] --> B["正确文档是否已入库"]
B --> C["Chunk 是否切分合理"]
C --> D["元数据和权限过滤是否正确"]
D --> E["TopK 是否召回正确片段"]
E --> F["Rerank 是否把正确片段排前"]
F --> G["Prompt 是否要求基于资料回答"]
G --> H["答案引用是否支持结论"]工具调用异常
flowchart TD
A["工具调用异常"] --> B["模型是否选错工具"]
B --> C["参数 schema 是否清晰"]
C --> D["参数校验是否失败"]
D --> E["权限或数据范围是否拒绝"]
E --> F["下游业务接口是否超时"]
F --> G["写操作幂等状态是否已成功"]成本突然升高
flowchart TD
A["AI 成本升高"] --> B["请求量是否上升"]
B --> C["输入 token 是否变长"]
C --> D["RAG TopK 是否过大"]
D --> E["历史消息是否无限追加"]
E --> F["模型是否切到更贵版本"]
F --> G["失败重试是否放大流量"]阶段12:商业场景验收
场景1:医疗数据采集异常助手
目标:用户输入采集任务失败日志,AI 给出排查建议。
链路:
flowchart TD
A["用户提交异常日志"] --> B["脱敏和截断"]
B --> C["识别错误码和系统模块"]
C --> D["RAG 检索采集规范和历史故障"]
D --> E["模型生成排查步骤"]
E --> F["返回建议和引用来源"]
F --> G["记录反馈用于评估"]必须注意:
- 日志里可能有 IP、账号、Token,要脱敏。
- 建议必须引用知识库或历史故障。
- 高风险操作例如重启、删除数据,只能建议人工确认。
场景2:数据资产问答
目标:用户问“某字段口径是什么”“某资产归属哪个系统”。
要求:
- RAG 检索前必须按租户、医院、部门、密级过滤。
- 回答必须带来源。
- 用户无权访问的资产不能被召回。
- 模型不能自己编造资产状态,状态必须来自工具或数据库。
场景3:智能工单分流
目标:根据用户描述自动分类、定优先级、建议处理人。
要求:
- 使用结构化输出。
- 分类结果要校验枚举。
- 高优先级工单要有人复核。
- 分类 Prompt 要版本化,变更后跑评估集。
阶段13:常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 前端直接调模型 | 绕过鉴权、限流、审计 | 统一走后端 AI 编排层 |
| Prompt 写死在代码各处 | 难评估、难回滚 | Prompt 版本管理 |
| RAG 只存文本和向量 | 无法权限过滤、引用、删除同步 | 保存完整元数据 |
| 全库检索后让模型保密 | 可能越权泄露 | 检索前按权限过滤 |
| 工具相信模型传的 userId | 越权访问 | 当前用户必须来自后端登录态 |
| 写工具无幂等号 | 重复创建、重复发送 | 幂等、二次确认、审计 |
| 完整工具结果给模型 | 泄露敏感字段,成本高 | 字段裁剪和脱敏 |
| 不做评估集 | 改 Prompt 靠感觉 | 固定样例回归 |
| 日志记录完整 Prompt | 扩大敏感数据泄露面 | 记录元数据和脱敏摘要 |
阶段14:面试标准回答
问:Spring AI 是什么?
标准回答:
Spring AI 是 Spring 生态里的 AI 应用工程化框架。它用 Spring Bean、自动配置、
ChatClient、Prompt、Advisor、EmbeddingModel、VectorStore和 Tool Calling,把模型调用、RAG、结构化输出、工具调用和可观测治理接入 Java 后端系统。它不是训练大模型的框架,而是让企业应用更稳定地使用大模型能力。
问:Spring AI 一次请求链路怎么走?
标准回答:
请求先进入 Controller,做鉴权、租户、限流和参数校验;业务层通过
ChatClient组装 Prompt;Advisor 链按需补充安全规则、会话记忆、RAG 检索资料或工具定义;底层ChatModel调用模型服务;模型返回后做结构化解析、引用校验、日志、token 和耗时记录,最后返回前端。生产中还要处理超时、重试、降级和安全审计。
问:Spring AI 怎么做 RAG?
标准回答:
离线阶段把文档解析、清洗、脱敏、切分 Chunk,补充来源、版本、租户、权限等元数据,用
EmbeddingModel向量化后写入VectorStore。在线阶段用户提问后先做身份和权限过滤,再把问题向量化,在用户有权访问的资料中召回 TopK,必要时 Rerank,把资料拼进 Prompt,由模型基于资料回答,并返回引用来源。
问:Tool Calling 为什么必须后端鉴权?
标准回答:
模型不是权限系统,它只能根据文本推断调用意图,不能可信判断用户身份和数据范围。用户可能通过自然语言诱导模型传入别人的 userId、tenantId 或资产编号。真正的用户身份必须来自后端登录态,工具方法执行前必须做参数校验、接口权限、数据权限、风险判断和审计,写操作还要幂等和二次确认。
最终验收题
能答清楚这些问题,才算真的学懂:
- 为什么 Spring AI 不是训练框架,而是应用工程化框架?
ChatClient和ChatModel为什么要分层?- Advisor 和普通业务 Service 的职责边界是什么?
- 为什么 RAG 入库必须保存元数据?
- 为什么文档和问题必须使用同一个 Embedding 模型?
- RAG 答错时你怎么区分是检索错还是生成错?
- Tool Calling 中模型和后端分别负责什么?
- 为什么写操作工具要幂等和二次确认?
- 结构化输出解析失败怎么处理?
- AI 请求变慢、成本升高、越权风险分别怎么排查?
关联知识点跳转
- Spring AI 学习总览
- Spring AI 从零到生产级掌握
- Spring AI 商业生产场景
- 快速入门
- ChatClient
- Prompt 与结构化输出
- RAG 知识库
- Tool Calling
- 生产化治理
- AI 应用架构
- RAG 流程
- AI 安全
- LLMOps
- AI 面试题
本章小结
Spring AI 的学习核心不是记 API,而是理解 AI 请求在后端如何被编排。ChatClient 负责统一调用入口,Advisor 负责链路增强,RAG 负责把企业知识带给模型,Tool Calling 负责连接业务能力,生产治理负责安全、成本、质量和排查。只有把这些环节串起来,才能从“会调模型”升级到“能做生产级 AI 应用”。
