技术栈:Spring Boot 3.4.5 + Spring AI 1.0.0(GA)+ 通义千问(OpenAI 兼容接口)
一、先抛个问题
很多人第一次看 Spring AI 的 demo 会懵:
java
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return chatClient.prompt().user(message).call().content();
}
"这不就是调了个通义千问的 API 吗?有什么意义?"
这个质疑是对的。 就"发一句、收一句"这种场景,Spring AI 跟你自己用 RestTemplate/OkHttp 调一次 HTTP 几乎没区别。
它的价值不在"调一次 API",而在于:真实 AI 应用很快会超出一问一答,那时候自己手写会非常痛苦。 下面用能跑的例子说明。
二、核心区别:一张表
| 能力 | 直接调 API(裸 HTTP) | Spring AI |
|---|---|---|
| 基础对话 | 自己拼 JSON 请求、解析响应 | .call().content() |
| 多轮对话记忆 | 自己维护消息列表、估算 token、裁剪历史 | ChatMemory 接入 |
| 结构化输出(回复转 Java 对象) | 自己写 prompt 要 JSON、ObjectMapper 解析、处理格式错乱 |
.entity(Xxx.class) |
| 工具调用(让 AI 调你的代码) | 手动解析 tool_calls、执行、回填、再请求一轮 |
@Tool + .tools(...) |
| RAG 知识库 | 自己做文档切分、向量化、检索、拼 prompt | QuestionAnswerAdvisor |
| 流式输出(打字机效果) | 自己处理 SSE 流式解析 | .stream() |
| 切换模型(通义 → OpenAI → 本地 Ollama) | 改一堆请求代码和参数 | 改 application.yml |
| 日志 / 监控 / 重试 | 全部自己实现 | 内置(Observability) |
一句话:"调 API" 是地基,Spring AI 的价值是在地基上能快速盖楼。
三、Demo 1:结构化输出 ------ 省掉解析的活
接口 :GET /study-plan?topic=Spring AI
java
@GetMapping("/study-plan")
public StudyPlan studyPlan(@RequestParam String topic) {
return chatClient.prompt()
.user(u -> u.text("为「{topic}」这个主题,生成一个适合初学者的学习计划")
.param("topic", topic))
.call()
.entity(StudyPlan.class); // ← 关键就这一行
}
java
public record StudyPlan(String topic, List<String> steps, String summary) {}
返回的直接是对象,不是文本:
json
{
"topic": "Spring AI",
"steps": ["搭建 Spring Boot 项目", "接入 ChatClient", "..."],
"summary": "..."
}
裸调 API 你得自己干 :写 prompt 要求返回 JSON → ObjectMapper 解析 → 处理模型偶尔不守格式的情况。
Spring AI 干了:自动注入"请按这个格式返回"的说明 + 解析 + 映射成对象。
四、Demo 2:工具调用 ------ 让 AI 反过来调你的代码
接口 :GET /assistant?message=现在几点了?顺便查下 iPhone 库存
java
@GetMapping("/assistant")
public String assistant(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.tools(new AssistantTools()) // ← 把工具交给模型
.call()
.content();
}
java
public class AssistantTools {
@Tool(description = "获取服务器当前的日期和时间")
public String getCurrentDateTime() {
return LocalDateTime.now().format(...);
}
@Tool(description = "查询指定商品的当前库存数量")
public String getStock(@ToolParam(description = "商品名称") String product) {
// 这里可以查真实数据库
}
}
执行流程(模型本身不知道时间、更不知道你的库存):
- 模型看到你提供的两个工具;
- 自己决定 要调用它们,并从用户的话里提取参数
iPhone; - Spring AI 真正执行你的 Java 方法,把结果回填给模型;
- 模型用真实数据组织回答:「现在是 2026-06-09 14:30,iPhone 当前库存 42 件」。
这就是 AI 能"操作你系统"的入口 ------ 查库、下单、发邮件都靠它。
这也是裸调 API 最繁琐的部分:要手动解析
tool_calls、回填、再发一轮请求。
五、Demo 3:对话记忆 ------ 让 AI 记住上下文
接口 :GET /memory-chat?conversationId=u1&message=...
java
public ChatMemoryController(ChatClient.Builder builder) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20) // 只保留最近 20 条,防止历史无限增长把 token 撑爆
.build();
this.chatClient = builder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
@GetMapping("/memory-chat")
public String chat(@RequestParam(defaultValue = "default") String conversationId,
@RequestParam String message) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) // ← 用会话 id 隔离
.call()
.content();
}
演示"它记住了" (同一个 conversationId=u1):
?conversationId=u1&message=我叫小明→ 好的,小明?conversationId=u1&message=我叫什么名字?→ 你叫小明(记住了上一轮)- 换
?conversationId=u2&message=我叫什么名字?→ 我不知道(另一段会话,互不干扰)
| 关键点 | 作用 |
|---|---|
MessageWindowChatMemory |
只保留最近 N 条,防止 token 爆掉 |
MessageChatMemoryAdvisor |
每次请求自动把历史拼进 prompt、再把本轮问答存回去 |
conversationId |
多用户/多会话隔离的钥匙(真实项目常用 userId / sessionId) |
裸调 API:得自己维护消息 List、自己决定留几条、每次手动拼进请求体。
⚠️ 默认内存存储,重启即清空 ;生产换
JdbcChatMemoryRepository/ Redis,接口不变。
六、Demo 4:RAG 知识库 ------ 让 AI 回答它本不知道的私有知识
接口 :GET /rag?question=...
第一步,把"私有知识"灌进向量库(启动时自动执行):
java
@Component
public class KnowledgeBaseLoader implements ApplicationRunner {
private final VectorStore vectorStore; // 构造注入
public void run(ApplicationArguments args) {
vectorStore.add(List.of(
new Document("光途科技的员工每年享有 15 天带薪年假,入职满 3 年后增加到 20 天。"),
new Document("光途科技的报销:登录 OA 提交发票,审批后 3 个工作日到账。")
// ...更多文档
)); // add 时自动调 embedding 模型把文本转成向量
}
}
第二步,提问时挂上 QuestionAnswerAdvisor:
java
@GetMapping("/rag")
public String rag(@RequestParam String question) {
return chatClient.prompt()
.advisors(QuestionAnswerAdvisor.builder(vectorStore).build()) // ← RAG 入口
.user(question)
.call()
.content();
}
对照演示(故意用虚构公司"光途科技",模型训练时不可能知道它):
GET /chat?message=光途科技的年假有几天?→ 瞎编GET /rag?question=光途科技的年假有几天?→ 15 天,满 3 年 20 天(从知识库检索到了)
RAG 四步:
- 启动时:文档 → 调 embedding 转成向量 → 存进向量库
- 提问时:把问题也转成向量,在库里找语义最相近的几条
- 把检索到的资料拼进 prompt:
参考以下资料回答:{资料}\n问题:{问题} - 模型基于真实资料作答 ------ 第 2~4 步全由
QuestionAnswerAdvisor自动完成
⚠️ 踩坑提醒:
QuestionAnswerAdvisor在独立依赖spring-ai-advisors-vector-store里,必须单独引,否则 import 标红、编译不过。demo 用的
SimpleVectorStore是内存版、重启清空;生产换 PGVector / Redis / Milvus,只换VectorStore这个 Bean,/rag一行不用动。
一句话:RAG = 检索你的私有资料 + 喂给模型"开卷考试",从而回答它本不知道的领域知识,还能减少瞎编。
七、学习步骤(建议路线)
阶段 0:环境准备
- JDK 17+,Spring Boot 3.x
- 申请一个模型 API Key(通义千问 / 智谱 / DeepSeek 都行,都有 OpenAI 兼容接口)
- API Key 用环境变量,别写死在配置里
阶段 1:跑通基础对话
- 引入
spring-ai-bom+spring-ai-starter-model-openai - 配
application.yml(base-url 指向兼容端点 + model) - 写一个
/chat接口,理解ChatClient的prompt().user().call().content()
阶段 2:结构化输出
- 定义一个 record,用
.entity(Xxx.class)拿到对象 - 理解"AI 输出 → 程序数据"这一步的价值
阶段 3:工具调用(Function Calling)
- 写带
@Tool的方法,用.tools(...)注册 - 理解"AI → 反向调用你的代码"
阶段 4:多轮对话记忆
- 接入
ChatMemory,让对话能记住上下文 - 理解 token 与历史裁剪
阶段 5:RAG 知识库
- 文档切分 → 向量化 → 存向量库(如 PGVector / Redis)
- 用
QuestionAnswerAdvisor让 AI 基于你的资料回答
阶段 6:进阶
- 流式输出(SSE)、Prompt 模板、Advisor 机制、可观测性、多模型编排
八、一句话总结
直接调 API 解决的是"怎么把一句话发给模型 ";
Spring AI 解决的是"怎么把模型变成应用的一部分" ------ 让它的输出能被程序使用,让它能调用程序的能力。
前者是 HTTP 客户端,后者是 AI 应用开发框架。