【从零理解 Spring AI · 03】Spring AI 在 Spring 生态里的位置
写在开头
写完前两篇的概念铺垫,有性子急的朋友问我:概念我大概懂了,但我现在就能用 RestClient 发个 POST、拼个 JSON 把模型调通,为什么还要引入一个框架?
这个问题问得很好,而且答案不是"框架更高级"这种空话。
我先把"直接调 HTTP"这条路走一遍给你看它会在哪变丑,再讲 Spring AI 怎么解------你会发现它用的全是你在 Spring 里已经学过的老招式 :模板类、自动配置、可插拔拦截器。这也是我把这篇放在这么前面的原因:Spring AI 不是一门新技术,而是老 Spring 思想在新领域的一次落地。
一、直接调 HTTP,会在哪变丑
1.1 能跑通的"最小版本"
不引入任何框架,调一个兼容 OpenAI 接口规范的模型服务,大概长这样:
java
public String chat(String userMessage) {
Map<String, Object> body = Map.of(
"model", "some-model-name",
"messages", List.of(
Map.of("role", "system", "content", "你是一个严谨的技术顾问。"),
Map.of("role", "user", "content", userMessage)
)
);
RestClient restClient = RestClient.builder()
.baseUrl("https://api.some-provider.com")
.defaultHeader("Authorization", "Bearer " + apiKey)
.build();
Map<?, ?> resp = restClient.post()
.uri("/v1/chat/completions")
.contentType(MediaType.APPLICATION_JSON)
.body(body)
.retrieve()
.body(Map.class);
// 从响应里一层层挖出文本
List<Map<?, ?>> choices = (List<Map<?, ?>>) resp.get("choices");
Map<?, ?> message = (Map<?, ?>) choices.get(0).get("message");
return (String) message.get("content");
}
能跑。 第一次调通的时候还挺有成就感。但在真实项目里活下去之后,你会发现它开始到处漏风。
1.2 三处典型的漏风点
第一处:字符串强转的地狱。 上面那段 Map<?, ?> 的层层强转只是冰山一角。响应结构里还有用量统计、结束原因、多候选结果......每多关心一样,就多几层强转。写错一个字段名,运行时才炸。
第二处:模型服务的"方言"。 你写死的 choices[0].message.content,是某一种接口规范的结构。换一家服务商,字段名可能就不一样了;流式返回的数据格式更是每家一个花样。业务代码和某个具体服务商被焊死了。
第三处:横切能力没有落点。 对话记忆(每轮重带历史)、结构化输出(把文本转成对象)、日志审计------这些能力放在哪?写在业务代码里?那每个调用点都要重复一遍;写个工具类?那工具类会越长越大,最后变成一个私有 mini 框架。
这三处漏风点,正好对应 Spring AI 的三个核心设计。 接下来一个一个对。
二、Spring AI 的四层结构
先上全景图,后面逐层展开:

四层结构:易变的推到最底层,稳定的留在上面
你的业务代码
↓
ChatClient ------ 你每天打交道的入口
↓
Advisor 链 ------ 记忆、检索、工具调用、日志
↓
ChatModel 适配层 ------ 把统一调用翻译成各家的请求格式
↓
各家模型服务
一句话概括这个设计:把"经常变的东西"(哪家模型、什么协议、什么参数名)压到最底层,把"稳定的东西"(你的业务意图、能力编排)留在最上层。
三、三层抽象各自管什么
3.1 ChatModel:协议适配层
ChatModel 是最底层的接口,它定义了一个统一的方法形态:
java
public interface ChatModel extends Model<ChatRequest, ChatResponse> {
ChatResponse call(ChatRequest request);
// 另有 stream(...) 用于流式
}
每一家模型服务商,对应一个 ChatModel 实现------OpenAI 规范的、其他厂商的、本地部署的,各自把"统一的调用"翻译成"自家的 HTTP 报文"。
你平时不会直接碰这一层,它的价值体现在"换模型"那一刻:换实现、换配置,上面的代码不动。
3.2 ChatClient:业务侧的流式 API
ChatClient 是你每天写代码面对的东西 ,定位类似 RestClient 之于 HTTP 调用、JdbcTemplate 之于数据库:
java
String answer = chatClient.prompt()
.system("你是一个严谨的技术顾问,回答要给出依据。")
.user("Spring 的依赖注入有哪几种方式?")
.call()
.content();
对比第一章那一坨 Map 强转------同样的功能,代码量少一个数量级,且全程类型安全。
它把"组装请求 → 发送 → 解析响应 → 提取文本"这套固定动作封装成了链式 API,你只声明变了的部分:这次问什么、这次带哪些能力。
3.3 Advisor:可插拔的能力链
Advisor 是这套设计里最妙的一环,如果你写过 Servlet Filter 或 Spring MVC 的 HandlerInterceptor,可以瞬间理解:
请求进来时依次穿过一串拦截器,每个拦截器可以在"发给模型之前"加工请求(比如把历史补进去、把检索资料塞进去),在"模型返回之后"加工响应(比如记日志、做审计)。
prompt() ──→ [Advisor 1] ──→ [Advisor 2] ──→ ChatModel ──→ 响应原路返回
记忆、检索增强、工具调用、日志......在 Spring AI 里全部是 Advisor。这就是第一篇说的"把三套机制标准化"的具体落点:
- 想加记忆?挂一个记忆 Advisor;
- 想接知识库?挂一个检索 Advisor;
- 不想要了?摘掉即可,业务代码一行不改。
能力变成了可插拔的环,而不是散落在业务代码里的 if-else。
四、自动配置:和 Spring Boot 的老配合
Spring AI 对 Spring Boot 生态的融入方式,也是你最熟悉的那套:Starter + 自动配置 + 属性绑定。
4.1 引入 starter
以兼容 OpenAI 接口规范的服务为例(DeepSeek、通义等多数国内厂商都兼容这套规范):
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
注意 artifactId 的命名是
spring-ai-starter-model-*。早期里程碑版本的spring-ai-openai-spring-boot-starter已经过时,网上旧教程里如果看到那个名字,直接按新命名替换。
4.2 写配置
yaml
spring:
ai:
openai:
api-key: ${API_KEY} # 密钥不要硬编码,放环境变量
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
就这么多。 启动应用后,Spring Boot 自动装配会做好三件事:
- 按配置创建好对应的
ChatModel实现的 Bean; - 创建一个
ChatClient.BuilderBean,预绑定了刚才的模型; - 你在任何地方注入
ChatClient.Builder就能用。
4.3 在业务代码里使用
java
@Service
public class AssistantService {
private final ChatClient chatClient;
// 直接注入 Builder,构建一次,全局复用
public AssistantService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个严谨的技术顾问,回答要给出依据。")
.build();
}
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
没有手动 new、没有连接池管理、没有报文解析。 这和你在 Spring Boot 里用 JdbcTemplate、RedisTemplate 的体验完全一致------写 AI 代码的姿势,就是写普通 Spring 代码的姿势。
五、"换模型不改代码"的真实含义
这个宣传语需要校准一下预期,它成立,但有边界。
5.1 成立的部分
换模型时,你改的是依赖和配置:
yaml
# 换一家服务商:改 base-url、api-key、model 名,换对应 starter 依赖
业务代码里的 chatClient.prompt().user(...).call().content() 一个字不用动,因为 ChatClient 这层抽象是稳定的。
5.2 有边界的部分
抽象统一的是"接口",统一不了"能力"。
不同模型对高级特性的支持程度不一样:有的对工具调用支持得好,有的对长上下文更从容,有的结构化输出更稳定。换了模型之后:
- 接口层面的代码不用改 ✅
- 但效果可能变化------原来好用的提示词,新模型可能需要微调
- 个别特性(如某些参数)可能不可用,需要条件化配置
所以准确的预期是:切换成本从"重写"降到"调整配置 + 回归验证",而不是零。 这已经很值了,但知道边界在哪,才不会被宣传语带偏。
六、这篇的四个要点
- 直接调 HTTP 的三大漏风点------字符串强转地狱、厂商方言焊死、横切能力没有落点。
- 四层结构 ------业务代码 / ChatClient / Advisor 链 / ChatModel 适配层;易变的压到底层,稳定的留在上层。
- Advisor 是能力可插拔的关键------记忆、检索、工具、日志全是链上的环;写过 Filter 的人零成本理解。
- 接入体验就是标准 Spring Boot 姿势 ------Starter + 配置 + 注入
ChatClient.Builder;"换模型"= 换依赖换配置,业务代码不动,但能力差异仍需回归验证。
延伸思考
这篇为了讲清结构,所有示例都是"一次调用拿一个字符串"的最简形态。真实业务里有个高频需求是对话记忆------上一轮说过的话这一轮还记得。它就是挂在 Advisor 链上的第一个"环"。
不过在看记忆怎么配之前,得先把 ChatClient 的日常用法过一遍:system 提示词怎么设、完整的响应对象怎么拿、什么时候用 ChatModel 而不是 ChatClient。这就是下一篇的内容。
下一篇
《ChatClient:把"调模型"变成一行代码》 ------把 prompt().call().content() 这条链逐段拆开:每一环在做什么、默认配置怎么设、拿到完整响应对象有什么用、以及 ChatClient 和 ChatModel 的选用标准。
这篇是整个专栏使用频率最高的一篇------后面所有的能力(记忆、检索、工具)都挂在这条链上,链本身必须先熟。
本专栏按 Spring AI 当前版本讲解。