大家好,我是Java1234_小锋老师。
Spring AI 2.0 从入门到精通:架构解析与实战指南
一篇写给 Java 后端工程师的 Spring AI 2.0 全景手册。我会按照"它是什么 → 怎么用 → 怎么设计的"这条主线来组织内容,中间穿插架构图、流程图、可直接落地的代码片段,以及在生产环境踩过的一些经验。
一、写在前面:为什么是 Spring AI 2.0
最近两年,Java 生态里关于"如何把大模型搬进自家系统"这件事,一直处于一种半明半暗的状态。Python 那边 LangChain、LlamaIndex 已经卷出了花,而 Java 这边大多数团队还停留在自己写 HTTP 客户端、手动拼 Prompt、一遍遍处理 JSON 的阶段。
Spring AI 的出现就是为了把这件事做得更"Spring 一点"——也就是:约定优于配置、面向接口编程、可观测、可测试、可替换。
到了 2.0 版本,几个关键能力被正式确立下来:
- 统一的 ChatClient API,所有模型厂商一个写法;
- Tool Calling / Function Calling 完整支持,并和 Java 方法签名打通;
- Advisors 机制,让 RAG、记忆、日志、限流这些"横切关注点"可以像 Servlet Filter 一样组合;
- MCP(Model Context Protocol) 客户端与服务端能力,开始向 Agent 时代靠拢;
- Micrometer + OpenTelemetry 全链路可观测;
- Spring Boot 3.5+ / Java 17+ 作为基线,全面拥抱新特性。
如果说 1.x 是"试试看",那么 2.0 就是"可以放进生产了"。下面我会带你从最简单的 Hello World,一直走到一个完整的企业知识库问答系统。
二、Spring AI 2.0 是什么
2.1 它要解决什么问题
把一个大模型调用塞进 Spring Boot 项目,理论上一个 RestTemplate 就够用。但真正在做的时候,你会发现需要解决一堆"不那么 AI"的问题:
- 模型可替换:今天用 OpenAI,明天老板说要切阿里通义,再后天上私有化 Ollama,代码不能跟着改三遍。
- Prompt 工程化:模板、变量替换、版本管理、A/B 测试,全靠字符串拼接很快就会失控。
- 结构化输出:模型返回一段散文,但你的下游系统要
List<Order>,谁来负责解析、校验、重试? - 工具调用:模型说"帮我查一下今天的订单",怎么把这句话变成对
OrderService.queryToday()的真实调用? - 检索增强:业务数据是私有的,模型不可能知道,需要从向量库里捞出来再喂给它。
- 可观测:调用花了多少 Token?平均延迟多少?哪个 Prompt 命中率最高?没有这些数据,调优就是玄学。
Spring AI 2.0 把上面这些问题,一个一个都给了官方答案。
2.2 2.0 相比 1.x 的关键变化
| 维度 | 1.x 时代 | 2.0 时代 |
|---|---|---|
| 主入口 | ChatModel.call(...) 偏底层 | ChatClient 流畅链式 API |
| 工具调用 | @AiFunction 注解,限制较多 | @Tool 注解,支持 POJO 参数、可空值、嵌套对象 |
| Advisor | 概念存在但生态薄 | 官方提供 RAG、Memory、SafeGuard、Logging 等开箱即用 |
| MCP | 不支持 | 内建 spring-ai-starter-mcp-client / -server |
| 可观测 | 仅日志 | Micrometer Metrics + Tracing + Token 统计 |
| 向量库 | 5、6 种 | 15+ 种主流向量库官方 Starter |
| Java 版本 | Java 17 | Java 17,推荐 Java 21 |
| Spring Boot | 3.2+ | 3.5+ |
简单一句话:1.x 是"能用",2.0 是"好用"。
三、五分钟快速入门
3.1 环境与依赖
先确保本机:
- JDK 17 或更高(推荐 21);
- Maven 3.9+ 或 Gradle 8+;
- 一个可用的模型 API Key(这里以 OpenAI 为例,国内可换 DashScope、ZhipuAI,写法基本一致)。
新建一个 Spring Boot 3.5 工程,pom.xml 关键依赖:
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</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>
<!-- 选用 OpenAI 适配器;要换厂商,改这一个依赖即可 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
application.yml 中配置 API Key:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.7
小提示:把 Key 放到环境变量里,不要直接写到 yaml 提交到仓库,这是底线。
3.2 第一个 ChatClient 调用
@RestController
@RequiredArgsConstructor
public class HelloAiController {
private final ChatClient chatClient;
public HelloAiController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/ai/chat")
public String chat(@RequestParam String q) {
return chatClient.prompt()
.user(q)
.call()
.content();
}
}
启动应用,访问 http://localhost:8080/ai/chat?q=用一句话介绍 Spring AI,模型的回答就回来了。
注意几个细节:
ChatClient.Builder由 starter 自动装配进来,你只需要build();prompt().user(...).call().content()是最常用的"傻瓜链路";- 如果你换成阿里通义,只需把依赖换成
spring-ai-starter-model-dashscope,代码一行不动。
3.3 流式输出与 SSE
聊天场景一定要支持流式,不然用户盯着转圈会很难受。Spring AI 用 Reactor 的 Flux<String> 表达流:
@GetMapping(value = "/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String q) {
return chatClient.prompt()
.user(q)
.stream()
.content();
}
前端用 EventSource 监听,模型每吐一个 Token 就推一段,体验立刻就上来了。
四、整体架构与分层
4.1 架构总览
下面这张图是 Spring AI 2.0 的整体分层,从上到下分别是应用层、核心 API、增强能力、集成层和适配层:
逐层解释一下:
- Application Layer:你的业务代码,可能是 MVC 控制器、WebFlux、批处理任务,甚至是一个命令行工具;
- Core API:
ChatClient、ChatModel、EmbeddingModel、ImageModel等,是面向开发者的统一抽象; - Advanced Features:Advisors、Tool Calling、结构化输出、RAG、Memory、Observability,都是 2.0 重点打磨的部分;
- Integration Layer:向量库 API、文档读取与切分、MCP 协议、Prompt 模板,处于"管道"位置;
- Provider Adapters:把上面所有抽象,落到具体的 OpenAI、Anthropic、Ollama、PGVector、Milvus 等实现上。
这种分层带来的最大好处是——业务代码只依赖上两层,下面的实现可以随时换。
4.2 一次请求在内部的旅行
我们把"用户发了一个问题"这件事,画成一个时序图看一看:
整个调用链清晰、可插拔——每一个 Advisor 都可以独立测试,也可以独立替换。
五、核心技术深入
5.1 ChatClient:流畅的链式 API
ChatClient 是 2.0 推荐的主入口。它把"准备 Prompt → 调用模型 → 处理响应"这三步,封装成一条链路:
ChatResponse response = chatClient.prompt()
.system("你是一名资深 Java 架构师,回答尽量简洁。")
.user(u -> u.text("如何理解 {topic}?").param("topic", "Spring AI Advisor"))
.options(OpenAiChatOptions.builder()
.model("gpt-4o")
.temperature(0.3)
.build())
.call()
.chatResponse();
链式 API 的好处是"读起来像在描述需求"。每一步都是可选的,没有调用的就用默认值,符合最小惊讶原则。
构造 ChatClient 的几种典型姿势:
@Configuration
public class ChatClientConfig {
// 默认的 ChatClient
@Bean
ChatClient defaultChatClient(ChatClient.Builder builder) {
return builder.build();
}
// 给客服场景定制一个:带固定 system prompt + 默认 advisor
@Bean("supportChatClient")
ChatClient supportChatClient(ChatClient.Builder builder,
ChatMemory chatMemory) {
return builder
.defaultSystem("你是公司客服,请用礼貌的语气回答。")
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
}
不同业务场景用不同的 ChatClient,互不污染,这是我比较推崇的做法。
5.2 Prompt 与 PromptTemplate
Prompt 看似就是一段字符串,但生产环境里它的复杂度远超想象。Spring AI 提供了 PromptTemplate 来管理这些"带变量的字符串":
String template = """
请基于下面的产品资料,回答用户问题。
---
资料:
{context}
---
用户问题:{question}
要求:
1. 只使用资料中的信息;
2. 如果资料中没有,请回答"暂未收录"。
""";
PromptTemplate pt = new PromptTemplate(template);
Prompt prompt = pt.create(Map.of(
"context", retrievedDocs,
"question", "保修期是多久?"
));
String answer = chatClient.prompt(prompt).call().content();
更进一步,PromptTemplate 支持从 Resource 加载,方便把 Prompt 当成"配置"管理:
@Value("classpath:/prompts/qa-template.st")
private Resource qaTemplate;
PromptTemplate pt = new PromptTemplate(qaTemplate);
这样产品经理修改文案时,连应用都不用重新打包。
5.3 结构化输出(Structured Output)
模型默认返回散文,但绝大多数业务场景需要结构化数据。2.0 提供了 entity() 方法,直接把结果反序列化成 Java 对象:
public record TripPlan(String city, int days, List<String> highlights) {}
TripPlan plan = chatClient.prompt()
.user("帮我安排一个 3 天的杭州行程,列出 5 个亮点景点。")
.call()
.entity(TripPlan.class);
它的内部其实做了三件事:
- 用反射读出
TripPlan的 JSON Schema; - 自动在 Prompt 里追加 "请按下面的 JSON Schema 返回..." 的指令;
- 收到响应后用 Jackson 反序列化,失败会抛
RuntimeException,方便上层重试。
如果需要返回集合,用 ParameterizedTypeReference:
List<TripPlan> plans = chatClient.prompt()
.user("分别给上海、杭州、苏州各设计一个 2 天行程")
.call()
.entity(new ParameterizedTypeReference<List<TripPlan>>() {});
我个人很喜欢这个特性——它把"模型 → 业务对象"的鸿沟基本抹平了。
5.4 Tool / Function Calling
Function Calling 是大模型从"会聊天"走向"能干活"的关键一步。Spring AI 2.0 用 @Tool 注解,让普通的 Java 方法变成模型可调用的工具:
@Component
public class OrderTools {
private final OrderService orderService;
public OrderTools(OrderService orderService) {
this.orderService = orderService;
}
@Tool(description = "根据订单号查询订单详情")
public OrderVO getOrderById(
@ToolParam(description = "订单号,例如 NO20260101001") String orderId) {
return orderService.getById(orderId);
}
@Tool(description = "根据用户ID查询最近 N 天的订单")
public List<OrderVO> recentOrders(Long userId, int days) {
return orderService.recent(userId, days);
}
}
调用时把工具注册进去:
String reply = chatClient.prompt()
.user("帮我查一下订单号 NO20260101001 的发货状态")
.tools(orderTools)
.call()
.content();
整个调用过程的内部流转:
这套机制让"AI 能调用业务方法"变得几乎零成本。需要注意几点:
- 方法描述(
description)一定要写清楚,模型靠这个判断"该不该用、该怎么用"; - 参数最好用基本类型或简单 POJO,过度复杂的对象会让模型生成的参数容易出错;
- 工具方法要做好幂等和权限校验,永远不要相信模型生成的参数。
5.5 Advisors:横切关注点的"过滤器链"
Advisor 可以理解成 ChatClient 的 "Filter Chain"。它在请求进入模型前可以改写 Prompt,在响应返回后可以做加工。常见 Advisor 包括:
| Advisor | 作用 |
|---|---|
SimpleLoggerAdvisor | 打印请求/响应日志,调试神器 |
MessageChatMemoryAdvisor | 自动把历史消息拼到 Prompt 里 |
QuestionAnswerAdvisor | 自动做 RAG,从向量库捞 context 注入 |
SafeGuardAdvisor | 关键词拦截、敏感词替换 |
| 自定义 Advisor | 限流、审计、缓存、Token 控制等 |
注册方式很 Spring:
ChatClient client = builder
.defaultAdvisors(
new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(4).build())
.build()
)
.build();
写一个自己的 Advisor 也不难,比如做"请求计数":
public class CountingAdvisor implements CallAdvisor {
private final AtomicLong counter = new AtomicLong();
@Override
public ChatClientResponse adviseCall(ChatClientRequest request,
CallAdvisorChain chain) {
counter.incrementAndGet();
log.info("第 {} 次调用 LLM", counter.get());
return chain.nextCall(request);
}
@Override public String getName() { return "counting"; }
@Override public int getOrder() { return 0; }
}
写法和 Servlet Filter / Spring Interceptor 几乎是同一个心智模型,老 Spring 玩家上手没难度。
5.6 Embedding 与 Vector Store
要做 RAG,先得有 Embedding 和 Vector Store。Spring AI 的抽象很干净:
@Autowired EmbeddingModel embeddingModel;
@Autowired VectorStore vectorStore;
// 1. 算 Embedding
EmbeddingResponse resp = embeddingModel.embedForResponse(List.of("你好,世界"));
float[] vector = resp.getResults().get(0).getOutput();
// 2. 存进向量库
vectorStore.add(List.of(
new Document("Spring AI 是一个 Java 的 AI 框架。",
Map.of("source", "official-doc", "version", "2.0"))
));
// 3. 检索
List<Document> hits = vectorStore.similaritySearch(
SearchRequest.builder()
.query("什么是 Spring AI?")
.topK(3)
.similarityThreshold(0.7)
.build());
Vector Store 有 15+ 种官方实现,PG Vector 是我比较推荐的入门选择,因为:
- 直接复用 PostgreSQL,无需新增中间件;
- 支持精确过滤 + 向量混合检索;
- 运维成熟,备份恢复都有现成方案。
引入也只是改一行依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
5.7 RAG:基于检索的增强生成
RAG 的核心思想:模型不需要无所不知,只需要在被问到时,去合适的地方查一下,再基于查到的内容回答。它分两条流水线,一条索引,一条问答。整个流程如下图:
具体到代码,索引这边长这样:
@Service
@RequiredArgsConstructor
public class IndexingService {
private final VectorStore vectorStore;
public void indexPdf(Resource pdf) {
var reader = new PagePdfDocumentReader(pdf);
var splitter = new TokenTextSplitter(800, 80, 5, 10000, true);
List<Document> chunks = splitter.apply(reader.get());
vectorStore.add(chunks);
}
}
问答这边,最简单的写法是直接挂上 QuestionAnswerAdvisor:
String answer = chatClient.prompt()
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(4).build())
.build())
.user(question)
.call()
.content();
一行代码,RAG 就跑起来了。这背后其实做了非常多的事情:把问题转成 embedding、去向量库捞 topK、把命中文档塞进 system prompt、再调用模型。这个抽象的成熟度,是 2.0 给我最大的惊喜之一。
5.8 Memory:让对话拥有记忆
模型本身是无状态的,每次调用都是"失忆症患者"。ChatMemory 接口就是用来管理对话历史的:
@Bean
ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
}
@Bean
ChatClient chatClient(ChatClient.Builder builder, ChatMemory memory) {
return builder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
.build();
}
调用时通过 conversationId 区分不同会话:
String reply = chatClient.prompt()
.user(q)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
.call()
.content();
MessageWindowChatMemory 是基于"最近 N 条"的窗口策略,简单但够用。生产环境如果对话很长,建议改成"摘要 + 窗口"的混合策略,或者把历史落到 Redis、PostgreSQL 里持久化,避免重启就丢光。
5.9 MCP(Model Context Protocol)支持
MCP 是 Anthropic 牵头推动的协议,目标是给"AI 怎么调用外部工具"建立一套标准。Spring AI 2.0 内置了 MCP 客户端和服务端的 Starter:
<!-- 把现有 Spring 应用变成 MCP Server -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<!-- 让 ChatClient 能调用任意 MCP Server -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
服务端配置一下要暴露的工具:
spring:
ai:
mcp:
server:
name: order-mcp
version: 1.0.0
客户端连过去:
spring:
ai:
mcp:
client:
sse:
connections:
order:
url: http://internal.order-mcp.svc/mcp
注入并使用:
@Autowired
List<McpSyncClient> mcpClients;
String resp = chatClient.prompt()
.user("帮我看看上周的订单异常情况")
.toolCallbacks(mcpClients.stream()
.flatMap(c -> Arrays.stream(c.listTools().tools()))
.toList())
.call()
.content();
MCP 的最大价值在于"工具的解耦"——一个工具实现,可以被任何遵循 MCP 协议的 Agent / Client 复用,而不再绑死在某一个框架里。
5.10 Observability:可观测性是生产线的灯
Spring AI 2.0 把 Micrometer 和 OpenTelemetry 接得很彻底,几乎所有关键节点都打了点:
spring.ai.chat.client:ChatClient 调用次数、耗时;spring.ai.chat.model:模型层调用次数、耗时;gen_ai.client.token.usage:Prompt Token、Completion Token、Total Token;spring.ai.advisor:每个 Advisor 的耗时。
只需要引入 actuator 和 micrometer-prometheus 即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
接到 Grafana 后,可以画出 Token 消耗、平均延迟、模型错误率这些核心指标。没有这些指标,就不要谈 AI 上生产。
六、综合实战:构建一个企业知识库问答系统
理论再好,不落地都是耍流氓。下面我们用前面所有积木,搭一个最小可用的"企业知识库问答系统"。
6.1 需求与技术选型
需求:
- 上传 PDF/Markdown 文档,自动切分入库;
- 用户提问时,先检索后回答,回答末尾附引用;
- 支持多轮对话,按
sessionId隔离; - 关键指标可观测。
技术选型:
| 层 | 选型 |
|---|---|
| Web | Spring Boot 3.5 + Spring MVC |
| AI 框架 | Spring AI 2.0 |
| 模型 | OpenAI gpt-4o-mini(也可换通义千问) |
| Embedding | OpenAI text-embedding-3-small |
| 向量库 | PostgreSQL + pgvector |
| 业务库 | MySQL 8.0 |
| 监控 | Prometheus + Grafana |
6.2 数据库设计
按要求,库名以 db_ 开头,表名以 t_ 开头。这里业务库设计如下:
CREATE DATABASE db_kb_qa DEFAULT CHARACTER SET utf8mb4;
USE db_kb_qa;
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL UNIQUE,
password_md5 CHAR(32) NOT NULL COMMENT '密码 MD5',
role VARCHAR(16) NOT NULL DEFAULT 'USER',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB COMMENT = '用户表';
CREATE TABLE t_document (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(256) NOT NULL,
file_path VARCHAR(512) NOT NULL,
chunk_count INT NOT NULL DEFAULT 0,
upload_user BIGINT NOT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB COMMENT = '知识文档表';
CREATE TABLE t_chat_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
question TEXT NOT NULL,
answer MEDIUMTEXT NOT NULL,
prompt_token INT NOT NULL,
reply_token INT NOT NULL,
cost_ms INT NOT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_session (session_id),
INDEX idx_user (user_id)
) ENGINE = InnoDB COMMENT = '对话日志表';
-- 初始化一个管理员账号,原密码 123456,MD5 后入库
INSERT INTO t_user(username, password_md5, role)
VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', 'ADMIN');
application.yml 中数据库连接:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/db_kb_qa?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: "123456"
6.3 文档导入服务
@Service
@RequiredArgsConstructor
@Slf4j
public class DocumentImportService {
private final VectorStore vectorStore;
private final DocumentRepository documentRepo;
@Transactional
public Long importPdf(MultipartFile file, Long uploadUserId) throws IOException {
Path saved = saveToDisk(file);
var reader = new PagePdfDocumentReader(new FileSystemResource(saved));
var splitter = new TokenTextSplitter(800, 80, 5, 10000, true);
List<Document> chunks = splitter.apply(reader.get()).stream()
.peek(d -> d.getMetadata().put("source", file.getOriginalFilename()))
.toList();
vectorStore.add(chunks);
DocumentEntity entity = new DocumentEntity();
entity.setTitle(file.getOriginalFilename());
entity.setFilePath(saved.toString());
entity.setChunkCount(chunks.size());
entity.setUploadUser(uploadUserId);
documentRepo.save(entity);
log.info("导入文档 {} 完成,共 {} 个 chunk", file.getOriginalFilename(), chunks.size());
return entity.getId();
}
private Path saveToDisk(MultipartFile file) throws IOException {
Path dir = Paths.get("upload");
Files.createDirectories(dir);
Path target = dir.resolve(System.currentTimeMillis() + "_" + file.getOriginalFilename());
file.transferTo(target);
return target;
}
}
6.4 RAG 问答接口
@Service
@RequiredArgsConstructor
public class QaService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
private final ChatLogRepository chatLogRepo;
public QaResult ask(String sessionId, Long userId, String question) {
long start = System.currentTimeMillis();
ChatResponse response = chatClient.prompt()
.system("""
你是公司内部知识库问答助手。
请基于检索到的资料回答问题;如果资料中没有,请明确说明"暂未收录"。
回答末尾用 [来源] 标注引用文档名。
""")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(4)
.similarityThreshold(0.65)
.build())
.build())
.user(question)
.call()
.chatResponse();
String answer = response.getResult().getOutput().getText();
Usage usage = response.getMetadata().getUsage();
ChatLogEntity log = new ChatLogEntity();
log.setSessionId(sessionId);
log.setUserId(userId);
log.setQuestion(question);
log.setAnswer(answer);
log.setPromptToken(usage.getPromptTokens());
log.setReplyToken(usage.getCompletionTokens());
log.setCostMs((int) (System.currentTimeMillis() - start));
chatLogRepo.save(log);
return new QaResult(answer, usage.getTotalTokens(), log.getCostMs());
}
}
控制器层就是 thin wrapper:
@RestController
@RequestMapping("/qa")
@RequiredArgsConstructor
public class QaController {
private final QaService qaService;
@PostMapping
public QaResult ask(@RequestHeader("X-Session-Id") String sessionId,
@RequestAttribute("userId") Long userId,
@RequestBody @Valid QaRequest req) {
return qaService.ask(sessionId, userId, req.question());
}
public record QaRequest(@NotBlank String question) {}
}
6.5 调用链路图
把整个系统的关键调用串起来,画一张端到端的流程图:
到这里,一个具备 RAG、记忆、可观测性的企业级问答系统就成形了。所有零件都是 Spring AI 2.0 提供的标准抽象,没有任何"魔改"。
七、生产环境的几个建议
写到这里,分享一些我自己在项目里踩过的坑:
- Token 一定要有预算控制。给每个会话、每个用户设上限,避免某个 Bug 把你公司账单干爆。可以自定义一个
BudgetAdvisor在adviseCall里做拦截。 - Prompt 要放进版本管理。把 system prompt、模板都放在
resources/prompts/下,配合 Git,谁改了什么、什么时候改的,一目了然。 - 结果要做幂等缓存。同一个用户问同一个问题,没必要每次都调一次模型。可以基于 Prompt 的 hash 做 Redis 缓存,命中直接返回。
- 工具调用必须做权限校验。不要因为是模型调用过来的就放过权限检查,模型可不会管你哪个用户能看哪条数据。
- 流式响应要处理超时。SSE 在网络抖动下很容易"假活",给前端配上心跳和重连机制。
- Embedding 模型不要轻易换。一旦换了模型,旧向量基本作废,需要全量重建索引,工作量很大。
- 可观测性从第一天就接上。不要等出事了再补埋点,那时候黄花菜都凉了。
八、写在最后
Spring AI 2.0 不是"在 Spring Boot 里加几行调 OpenAI 的代码",而是把 AI 应用的常见模式(RAG、记忆、Agent、可观测)做成了一等公民。它最大的价值在于——让 Java 后端工程师不需要切换语言栈,就能写出接近一线水准的 AI 应用。
如果你已经熟悉 Spring 那一套(Bean、Starter、Filter、Actuator),那么 Spring AI 2.0 几乎是"零摩擦"的:你能感觉到熟悉的设计味道,每一个抽象都像是它本来就该长成这样。
下一步建议:
- 把本文 Demo 跑一遍,跑通一个最小闭环;
- 找一个团队内部的真实场景(FAQ、客服、运维助手),用 Spring AI 落一个 MVP;
- 持续关注 MCP 的演进,那是接下来 Agent 互联互通的关键。
工具已经够好了,剩下的就是把它用起来。
附录:常见问题速查
Q1:ChatClient 和 ChatModel 有什么区别? ChatModel 是底层调用接口,一次性发送 messages、拿回 response;ChatClient 是基于 ChatModel 之上的链式 API,多了 Advisor、Tool、StructuredOutput 等增强。日常开发优先用 ChatClient。
Q2:能不能不依赖云厂商,本地跑? 完全可以。换成 spring-ai-starter-model-ollama,本地起一个 Ollama,下载 llama3 或 qwen 系列模型,代码一行不改即可切换。
Q3:Spring AI 2.0 支持 WebFlux 吗? 支持。所有 stream() 调用返回 Flux,并且 starter 兼容 spring-boot-starter-webflux。
Q4:如何降低 RAG 的"幻觉"? 四个方向:① 提高 chunk 质量,避免过长或过短;② 给检索加 metadata 过滤;③ 在 system prompt 里强调"未检索到必须说不知道";④ 后置加一层 ModerationModel 或人工审核。
Q5:测试时不想真调模型怎么办? 可以注入一个 ChatModel 的 mock 实现,或者使用 Spring AI 提供的 TestChatModel,固定返回某段文本,便于在 CI 里跑用例。
本文示例代码均基于 Spring AI 2.0.0 与 Spring Boot 3.5.x 编写,部分 API 细节可能在后续小版本中微调,请以官方文档为准。