Spring AI
文章介绍了人工智能(AI)的基本概念,并说明了AI相关术语,包括模型(Model)、大语言模型(LLM)、提示词(Prompt)和词元(Token)等核心概念。随后介绍了SpringAI框架,并提供了使用DeepSeek API的快速入门指南,包括环境要求、API申请和项目创建步骤,并展示了如何通过ChatModel和ChatClient接口与模型交互。最后介绍了SSE协议和Flux实现流式输出的
目录
基本概念
什么是 AI
AI:也就是 人工智能(Artificial Intelligence),顾名思义,就是让机器模拟人类智能的科学与技术
我们通过一个示例来对比理解:
普通计算机程序:像一台自动售货机。你按下特定的按钮(输入),它就给你一瓶特定的饮料(输出)。它的所有行为都是程序员预先设定好的规则
人工智能程序:更像是一个正在学习的孩子。你给它看很多猫和狗的图片(数据),并告诉它哪个是猫,哪个是狗。经过学习后,当你给它一张它从未从未见过的猫咪图片时,它也能识别出来。它自己从数据中学会了规律,而不是依赖硬编码的规则
因此,AI 的核心是从经验中学习,并根据所学做出决策或预测
而目前最主流、最引人注目的 AI 分支是 生成式人工智能,也就是现在常说的 AIGC(Artificial Intelligence Generated Content,人工智能生成内容)。它与传统 AI(主要用于分析数据,比如识别人脸)不同,它的目标是利用人工智能技术自动生成或创造出各类数字内容,比如写文章、报告、翻译、编程
为了更好的理解 AI,我们先来理解其中的一些常见术语
模型(Model)
模型(Model)是 AI系统的核心,它是通过算法在数据数据上训练后得到的结果。模型本质上是一个数学函数,它接收输入数据,并进行计算,然后产生输出。
我们常说的“调用一个AI”,实际上就是在使用这个“模型”。模型文件大小不一,可以从几MB到几十GB
可以把 AI 模型想象成一个 “虚拟大脑”。这个大脑通过在大量数据上进行“训练”或“学习”,掌握了一些技能和知识。而当被提问时,就需要运用这个大脑掌握的知识来解决问题
大语言模型 (LLM)
LLM( Large Language Model,大语言模型):一种基于深度学习的、使用海量文本数据训练的模型。它的主要任务是理解和生成人类语言。LLM是当前生成式AI热潮的代表。它们的特点是“大”,体现在训练数据量大、模型参数数量巨大
可以将其看做一个进行了超大规模训练的 “专家大脑” 。它通过学习互联网上几乎所有的文本,掌握了语言的语法、句法、事实知识以及上下文逻辑,拥有数百亿甚至数千亿个参数,并且因为它什么都学过,所以能应对各种各样的话题和任务
提示词 (Prompt)
提示词(Prompt):用户提供给AI模型的指令、问题或上下文信息。模型根据提示词来生成相应的回复。提示词的质量直接决定了 AI 回答的质量。
而设计和优化提示词的过程被称为“提示词工程”,是一门新兴的技能。
提示词就像是给AI这位“天才天才”下达的“工作订单”。订单越清晰、越具体,完成的工作质量就越高。
例如:
简单提示词:“法国的首都是哪里?” -> 模型回答:“巴黎。”
复杂提示词(角色扮演):“假设你是一位资深营养师,请为我(一位办公室久坐的上班族)设计一份为期一周的健康午餐食谱。” -> 模型会以营养师的口吻提供一份详细的食谱。
词元(Token)
词元(Token):是模型处理和理解的基本文本单位。它不是完全等同于一个英文单词或一个汉字。模型在处理前,会先将文本拆分成词元,同时,词元也是计费和衡量模型处理长度的基本单位。
英文中,单词
“unbelievable”可能会被拆分成三个词元[“un”, “believe”, “able”]中文中,“我喜欢编程”这句话,很可能会被拆分成四个词元[“我”, “喜”, “欢”, “编程”]
不同模型的分词规则不同,同一个词在不同模型中可能被拆分成不同词元
了解了 AI 的基本概念,接下来,我们来看 Spring AI 相关内容
Spring AI 是什么
Spring AI 是一个基于 Spring 生态系统的开源人工智能应用框架,它的核心目标是简化 AI 功能在 Java 应用程序中的集成过程,让 Java 开发者也能高效地构建生成式 AI 应用
官方文档:简介 :: Spring AI 参考文档 - Spring 框架
Spring AI 提供了作为开发 AI 应用基础的抽象。这些抽象具有多种实现,可以通过最少的代码更改轻松实现组件切换。
Spring AI 提供了一系列强大而实用的功能,使其成为一个功能完备的 AI 应用开发框架:
1. 统一的多模型支持:支持与众多主流的 AI 模型提供商进行交互,包括 OpenAI、Microsoft、Amazon、Google 和 Anthropic 等,无论是云端模型还是本地部署的模型(如通过 Ollama),都能通过一致的接口进行调用
2. 强大的数据集成能力:这是 Spring AI 的一大亮点。它内置了对向量数据库(如 Chroma、Pinecone、Redis 等)的支持
3. 与 Spring 生态无缝集成:作为 Spring 大家庭的一员,它能自然地与 Spring Boot、Spring Data 等其他知名项目协同工作
4. 简化的开发模式:允许 AI 模型根据需要请求执行客户端定义的函数,从而接入实时信息或触发具体动作
了解了相关概念后,我们就来上手体验一下 Spring AI
快速入门
环境要求
JDK 版本:JDK 17 或以上 (推荐 JDK 21),这是强制要求,因为 Spring Boot 3.x 本身就需要 JDK 17+
Spring Boot 版本:Spring Boot 3.2 或以上 ,具体版本可以是 3.3.3、3.4.3 或 3.5.0,选择一个稳定的3.x最新版本即可。
AI 服务凭证:有效的 API Key,需要一个来自 AI 服务提供商(如 OpenAI、DeepSeek、阿里百炼等)的账户和 API Key
在本篇文章中,我们以 DeepSeek 作为示例来进行学习
申请 API Key
我们访问 DeepSeek 官网:DeepSeek | 深度求索
进入 API 开放平台:

创建 API Key:

点击创建之后输入名称即可完成创建,但需要注意的是 API key 仅在创建时可见可复制
然后需要进行充值(充值前需要进行实名认证):

只是学习使用,1块就够了
项目创建
Spring AI 专门为 OpenAI 及兼容 API 服务设计了 spring-ai-openai-spring-boot-starter,用于快速集成大模型语言能力到 Spring Boot 应用中:

正常创建 Maven 项目(注意JDK 和 Spring Boot 版本),并添加 Spring AI 依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
依赖版本可参考:https://docs.spring.io/spring-ai/reference/getting-started.html
在 application.yml 中配置 API 密钥:
spring:
ai:
openai:
#DeepSeek
api-key: 申请的 API Key
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
temperature: 0.7
其中:
spring.ai.openai.base-url:要连接的 URL
spring.ai.openai.api-key:申请的 DeepSeek API 密钥
spring.ai.openai.chat.options.model:要使用的 DeepSeek LLM 模型
spring.ai.openai.chat.options.temperature:用于控制模型生成文本的随机性和创造性:低温 (接近 0.0) → 保守、确定、可预测;高温 (接近 2.0) → 冒险、多样、富有想象力。也就是说,temperature 值越低,相同的提问得到的结果越类似。此外,不建议在同一个补全请求中同时修改 temperature 和 top_p,因为这两个设置的交互作用难以预测。
配置项可参考:https://docs.spring.io/spring-ai/reference/api/chat/deepseek-chat.html
此时,就已经完成了 项目的创建 和 DeepSeek 的接入,接下来,我们通过编写接口来调用模型
接口编写
@RestController
@RequestMapping("/deepseek")
public class DeepSeekChatController {
@Autowired
private OpenAiChatModel deepSeekChatModel;
@GetMapping("/chat")
public String generate(String message) {
return deepSeekChatModel.call(message);
}
}
运行,并访问 127.0.0.1:8080/deepseek/chat?message=你是谁 进行测试

上述,我们通过 ChatModel 完成了与模型的交互
而在 Spring AI 框架中,ChatModel 和 ChatClient 是构建 对话式AI 应用的两大核心接口,接下来,我们分别来看这两个接口
核心接口
ChatModel
ChatMode 直接与底层 AI 模型(如 GPT-4、Claude 等)通信,处理原始的请求和响应
因此,ChatModel 更底层,使用更灵活
@Service
public class ChatService {
@Autowired
private ChatModel chatModel; // 例如 OpenAiChatModel
public String askQuestion(String question) {
// 1. 构造消息
UserMessage userMessage = new UserMessage(question);
// 2. 创建 Prompt
Prompt prompt = new Prompt(List.of(userMessage));
// 3. 调用并获得完整响应
ChatResponse response = chatModel.call(prompt);
// 4. 从响应中提取内容
return response.getResult().getOutput().getContent();
}
}
ChatClient
ChatClient 在 ChatModel 之上提供了一层流畅的 API,简化了常见的使用模式

即 ChatClient 是对 ChatMode 的一层包装
因此,ChatClient 更高级,也更简洁
@Service
public class ChatService {
@Autowired
private ChatClient chatClient;
public String askQuestion(String question) {
// 一行搞定
return chatClient.call(question);
}
}
可以看到,ChatClient 的使用更加简洁直观
ChatMode 与 ChatClient 对比:
| 维度 | ChatModel | ChatClient |
|---|---|---|
| 抽象层级 | 底层,接近原始模型 | 高层,面向业务使用 |
| 返回值 | ChatResponse(包含丰富元数据的完整响应对象) | ChatResponse(直接获得内容的纯文本)或流式响应 |
| 使用方法 | 需要手动构造 Prompt 对象 | 提供流式的 builder 模式 |
| 控制粒度 | 精细控制 | 快捷简便 |
消息类型
在 Spring AI 中,所有消息类型都实现了 org.springframework.ai.chat.messages.Message 接口,系统中的消息被设计用来模拟一个多轮对话中的不同参与者
| 消息类型 | 对应角色 | 核心作用 |
|---|---|---|
| SystemMessage | 系统 / 导演 | 设定 AI 的背景、角色、行为和回复风格。通常在对话开始时提供,为整个会话定下基调。 |
| UserMessage | 用户 / 提问者 | 代表人机交互中的人类一方,是驱动对话前进的源泉。 |
| AssistantMessage | 助理 / AI 本身 | 代表 AI 在之前轮次中做出的回复。是多轮对话连贯性的保障。 |
| FunctionMessage | 函数 / 工具 | 代表 AI 通过函数调用获得的额外信息或操作结果。 |
| ToolMessage | 工具 | 功能与 ToolMessage 完全相同,是 ToolMessage 的别名。 |
| MediaMessage | 多媒体 | 表示除文本外的其他类型消息数据,例如图像。 |
其中,最常使用的是 SystemMessage、UserMessage 和 AssistantMessage
SystemMessage
SystemMessage 通常用于设定 AI 助手的身份、性格、行为准则和对话规则,一般位于对话的开头,为整个对话设定基调
例如,我们可以为其进行角色预设:
@RequestMapping("/chat")
@RestController
public class ChatClientController {
private ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("你叫小小鱼,是一款专业的智能答疑AI助手,擅长Java和Python,以友好的态度来回答问题")
.build();
}
@GetMapping("/call")
public String generation(String userInput) {
return this.chatClient.prompt()
.user(userInput) // 用户输入
.call() // 调用 API
.content(); // 返回响应
}
}
在 ChatClient 中,通过 defaultSystem 来设置 AI 模型的默认系统消息,通过 ChatClient.Builder 链式调用设置的系统消息会作为对话的 "初始指令",注入到每次对话的上下文中,引导 AI 的回复风格或身份设定
此时,我们访问接口,再次询问其身份:

UserMessage
UserMessage 表示我们提出的具体问题或指令,上述输入的 "你是谁",就是 UserMessage
AssistantMessage
AssistentMessage:是AI模型给出的回复

AssistentMessage 是实现 连贯多轮对话 的关键。每次 AI 回复后,可以将这个回复作为 AssistantMessage 保存下来,并在下一次请求时将其作为历史上下文的一部分发送给 AI
输出格式
结构化输出
若想要从 LLM 接收结构化输出,Spring AI 支持将 ChatModel/ChatClient 方法的返回类型从 Spring 更改为其他类型
通过 entity() 方法将模型输出转化为自定义实体
例如:
@RequestMapping("/chat")
@RestController
public class ChatClientController {
private ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.build();
}
@GetMapping("/entity")
public String entity(String userInput) {
Recipe entity = this.chatClient.prompt()
.user(String.format("请帮我生成%s的菜谱", userInput))
.call()
.entity(Recipe.class);
return entity.toString();
}
record Recipe(String dis, List<String> ingredients) {}
}

流式输出
首先我们对比来看什么是流式输出:
传统输出(非流式):等待全部生成完成后才一次性返回,用户长时间等待 → 突然显示完整答案。像寄送一封平信,写完所有内容才寄出,对方一次性收到整封信。
流式输出:边生成边返回,立即推送部分结果,几乎立即开始显示 → 逐字逐句增长。像打电话一样,对方一边说话,你一边就能听到。
流式输出过程:
用户提问: "请写一篇关于春天的短文"
AI 模型生成过程:
"春天"... (立即返回)
"春天来了"... (继续返回)
"春天来了,万物复苏"... (持续返回)
直到生成完整回答SSE 协议
Spring AI 主要通过 响应式编程 来实现流式输出,使用 stream() 方法生成 Flux<String> 流
@RequestMapping("/chat")
@RestController
public class ChatClientController {
private ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.build();
}
@GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
public Flux<String> stream(String userInput) {
return this.chatClient.prompt()
.user(userInput)
.stream()
.content();
}
record Recipe(String dis, List<String> ingredients) {}
}
但是,我们思考这样一个问题:由于 HTTP 协议本身设计为无状态的请求-响应模式,也就是严格来说,无法做到服务器主动推送消息到客户端,那么我们要如何实现服务器的流式响应呢?
我们可以通过 SSE(Server-Sent Events,服务器发送事件)来实现流式传输,允许服务器主动向浏览器推送数据流
SSE 协议介绍
SSE 是一种基于 HTTP 的轻量级实时通信协议,浏览器通过内置的 EventSource API 接收并处理这些实时事件
服务器向客户端声明:接下来发送的是 流消息(streaming)、
此时客户端不会关闭连接,会一直等待服务器发送过来新的数据流
SSE 核心特点:
1. 单向通信:数据流只能从服务器推送到客户端。客户端不能通过这个连接向服务器发送数据(除了最初的建立连接请求)。
2. 基于 HTTP/HTTPS:SSE 使用标准的 HTTP 协议,这意味着它可以轻松地穿越大多数防火墙和代理服务器,无需特殊的配置。
3. 长连接:客户端发起一个普通的 HTTP 请求,但服务器会保持这个连接处于打开状态,而不是在发送一次响应后就关闭它。
4. 文本数据流:服务器通过这个持久的连接,持续地向客户端发送遵循特定格式的文本数据流。
5. 自动重连:SSE 协议内建了重连机制。如果连接意外断开,浏览器会自动尝试重新连接到服务器。
SSE 数据格式
服务器向浏览器发送 SSE 数据,需要设置必须的 HTTP 头信息
Content-Type: text/event-stream;charset=utf-8
Connection: keep-alive
整个数据流由一系列消息组成,每条消息(message)由一行或多行文本构成,每行文本以一个字段名开头,后跟一个冒号和一个空格,然后是字段的值,每条消息以一个空行(即两个连续的换行符 \n\n)结束
每一行格式:[field]: value\n
field 的常见取值有:data、event、id、retry
data
data:消息主体,是最重要的字段,用于承载消息的实际内容,如果一个消息包含多个 data 行,客户端会将它们用换行符 (\n) 连接起来,形成一个完整的数据字符串。可用于传递 JSON 字符串、纯文本、XML 等任何文本数据
示例:
data: 这是一条简单的消息\n\n
data: Hello\n
data: World\n
data: !\n\n
event
event:事件类型,用于指定消息的自定义类型,若提供了此字段,客户端将触发对该特定事件名的监听器;否则,将触发通用的 onmessage 事件,可用于对不同类型的消息进行分类处理
示例:
event: userJoined
data: Alice
id
id:事件id,用于为消息设置一个唯一的 ID(字符串),如果连接中断,当客户端重新连接时,会在 HTTP 请求头 Last-Event-ID 中自动发送最后一个接收到的 ID。可以用于实现消息的幂等性和断点续传。
示例:
id: msg-123
data: 这是一条重要消息
retry
retry:重连时间,表示建议浏览器在连接断开后再次尝试连接之前应等待的毫秒数,由于这不是一个强制命令,浏览器可能会忽略它。用于避免在服务器出现故障时,客户端过于频繁地重试。
示例:告诉浏览器,如果连接失败,请等待 10 秒后再尝试重连
retry: 10000
我们通过一个简单的示例来看 SSE 协议的使用
SSE 使用示例
后端接口:
@Slf4j
@RequestMapping("/sse")
@RestController
public class SseController {
@RequestMapping("/end")
public void end(HttpServletResponse response) throws IOException, InterruptedException {
log.info("发起请求: event");
response.setContentType("text/event-stream;charset=utf-8");
PrintWriter writer = response.getWriter();
for (int i = 0; i < 10; i++) {
// 事件 foo 事件
String s = "event: foo\n";
s += "data: " + new Date() + "\n\n";
writer.write(s);
writer.flush();
Thread.sleep(1000L);
}
//定义end事件, 表示当前流传输结束
writer.write("event: end\ndata: EOF\n\n");
writer.flush();
}
}
前端实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE</title>
</head>
<body>
<div id="sse"></div>
<script>
let eventSource = new EventSource("/sse/end");
eventSource.addEventListener("foo", function(event) {
console.log(event);
document.getElementById("sse").innerHTML = event.data;
});
eventSource.addEventListener("end", function(event) {
console.log("连接关闭")
eventSource.close();
});
</script>
</body>
</html>
运行,观察控制台打印的日志:

可以看到成功传输消息,并在消息传输完毕后关闭连接
而在 Spring 中,可以通过 WebFlux 优雅地实现 SSE 协议,也就是我们之前使用的 Flux,它是 WebFlux 中的核心组件,我们来看 Flux 的使用和常见操作
Flux
Flux 的使用流程:创建 → 转换 → 过滤 → 消费
创建 Flux:
import reactor.core.publisher.Flux;
// 1.1 从固定值创建
Flux<String> fixedFlux = Flux.just("Hello", "World", "!");
// 1.2 从集合创建
List<String> list = Arrays.asList("A", "B", "C");
Flux<String> fromCollection = Flux.fromIterable(list);
// 1.3 数值范围
Flux<Integer> rangeFlux = Flux.range(1, 5); // 1,2,3,4,5
// 1.4 动态生成
Flux<Long> intervalFlux = Flux.interval(Duration.ofSeconds(1)).take(5);
// 1.5 从数组创建
Flux<String> arrayFlux = Flux.fromArray(new String[]{"X", "Y", "Z"}));
// 1.6 空流
Flux<String> emptyFlux = Flux.empty();
// 1.7 从 Future 创建(适配传统异步API)
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Result");
Flux<String> futureFlux = Flux.fromFuture(future);
转化操作:
// 2.1 map - 一对一转换
Flux<String> original = Flux.just("apple", "banana", "cherry");
Flux<String> uppercased = original.map(String::toUpperCase); // APPLE, BANANA, CHERRY
// 2.2 flatMap - 一对多转换(异步展平)
Flux<String> words = Flux.just("hello world", "spring ai");
Flux<String> splitWords = words.flatMap(word ->
Flux.fromArray(word.split(" "))
);
// 2.3 cast - 类型转换
Flux<Object> objects = Flux.just("text1", "text2");
Flux<String> strings = objects.cast(String.class);
// 2.4 scan - 累积计算
Flux<Integer> numbers = Flux.range(1, 4);
Flux<Integer> cumulativeSum = numbers.scan((acc, current) -> acc + current); // 1,3,6,10
过滤操作:
// 3.1 filter - 条件过滤
Flux<Integer> allNumbers = Flux.range(1, 10);
Flux<Integer> evenNumbers = allNumbers.filter(n -> n % 2 == 0); // 2,4,6,8,10
// 3.2 distinct - 去重
Flux<String> withDuplicates = Flux.just("A", "B", "A", "C");
Flux<String> uniqueItems = withDuplicates.distinct()); // A,B,C
// 3.3 take - 取前 N 个
Flux<String> limited = original.take(2); // apple, banana
// 3.4 skip - 跳过前 N 个
Flux<String> skipped = original.skip(1); // banana, cherry
// 3.5 takeWhile / skipWhile - 条件取/跳
Flux<Integer> sequence = Flux.range(1, 100);
Flux<Integer> firstPart = sequence.takeWhile(n -> n < 10); // 1,2,3,...,9
// 3.6 sample - 采样(定期取最新元素)
Flux<Long> sampled = Flux.interval(Duration.ofMillis(100)))
.sample(Duration.ofSeconds(1)))
.take(3); // 每隔1秒取样,共取3次
消费操作:
// 4.1 subscribe - 最基本的消费方式
Flux<String> data = Flux.just("one", "two", "three");
data.subscribe(
item -> System.out.println("Received: " + item),
error -> System.err.println("Error: " + error)),
,
() -> System.out.println("Completed!"))
);
// 4.2 collectList - 收集所有元素到 List
Mono<List<String>> listMono = data.collectList());
// 4.3 blockFirst / blockLast - 阻塞获取(仅用于测试)
// String first = data.blockFirst();
// 4.4 reduce - 归约操作
Mono<Integer> sum = numbers.reduce(0, Integer::sum));
// 4.5 count - 计数
Mono<Long> count = data.count();
// 4.6 hasElement - 检查是否有元素
Mono<Boolean> hasData = data.hasElements();
// 4.7 then - 忽略元素,只在完成后触发
Mono<Void> completionSignal = data.then();
Advisors
Advisors 是 Spring AI 中的一种拦截器机制,允许我们在 AI 调用链的特定节点注入自定义逻辑。
Advisors 在两个关键的时机点介入:
Before Call(调用前):在请求发送到 AI 模型之前执行,主要用于修改提示词
After Call(调用后):在收到 AI 响应后、返回给客户端之前执行
其执行流程为:
用户输入 → Advisor1.before() → Advisor2.before() → AI 模型调用 → Advisor2.after() → Advisor1.after() → 最终响应
在 Spring AI 中内置了一些 Advisor,如 SimpleLoggerAdvisor,其主要功能是进行日志记录,只需要将其添加到 Advisor 链中,就可以自动记录 Advisor 的聊天请求和响应:
@RequestMapping("/chat")
@RestController
public class ChatClientController {
private ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("你叫小小鱼,是一款专业的智能答疑AI助手,擅长Java和Python,以友好的态度来回答问题")
.build();
}
@GetMapping("/advisor")
public String advisor(String userInput) {
return this.chatClient.prompt()
.advisors(new SimpleLoggerAdvisor())
.user(userInput)
.call()
.content();
}
record Recipe(String dis, List<String> ingredients) {}
}
我们将日志级别配置为 debug 来观察:
logging:
level:
org.springframework.ai.chat.client.advisor: debug

观察打印的日志内容:

成功打印了对应的请求和响应日志
更多推荐



所有评论(0)