跳到主要内容Spring AI 快速入门:核心接口与流式输出详解 | 极客日志JavaAIjava
Spring AI 快速入门:核心接口与流式输出详解
综述由AI生成Spring AI 是基于 Spring 生态的开源人工智能应用框架,旨在简化 Java 应用中 AI 功能的集成。文章涵盖 AI 基本概念如模型、LLM、提示词及词元,演示了基于 DeepSeek 的环境配置与项目搭建。重点解析了 ChatModel 与 ChatClient 两大核心接口的差异与应用场景,介绍 SystemMessage 等消息类型管理对话上下文。此外还讲解了结构化输出、SSE 流式传输原理及 Flux 响应式编程实现,并展示了 Advisors 拦截器机制在日志记录中的应用,帮助开发者高效构建生成式 AI 应用。
花里胡哨18 浏览 基本概念
什么是 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 提供了作为 开发 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
点击创建之后输入名称即可完成创建,但需要注意的是 API key 仅在创建时可见可复制
项目创建
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>
在 application.yml 中配置 API 密钥:
spring:
ai:
openai:
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,因为这两个设置的交互作用难以预测。
此时,就已经完成了 项目的创建 和 DeepSeek 的接入,接下来,我们通过编写接口来调用模型
接口编写
@RestController
@RequestMapping("/deepseek")
public class DeepSeekChatController {
@Autowired
private OpenAiChatModel deepSeekChatModel;
@GetMapping("/chat")
public String generate(String message) {
return deepSeekChatModel.call(message);
}
}
运行,并访问 http://127.0.0.1:8080/deepseek/chat?message=你是谁 进行测试。
上述,我们通过 ChatModel 完成了与模型的交互。
而在 Spring AI 框架中,ChatModel 和 ChatClient 是构建 对话式 AI 应用的两大核心接口,接下来,我们分别来看这两个接口
核心接口
ChatModel
ChatModel 直接与底层 AI 模型(如 GPT-4、Claude 等)通信,处理原始的请求和响应。
@Service
public class ChatService {
@Autowired
private ChatModel chatModel;
public String askQuestion(String question) {
UserMessage userMessage = new UserMessage(question);
Prompt prompt = new Prompt(List.of(userMessage));
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getContent();
}
}
ChatClient
ChatClient 在 ChatModel 之上提供了一层 流畅的 API,简化了常见的使用模式。
即 ChatClient 是对 ChatModel 的一层包装。
@Service
public class ChatService {
@Autowired
private ChatClient chatClient;
public String askQuestion(String question) {
return chatClient.call(question);
}
}
可以看到,ChatClient 的使用更加简洁直观。
ChatModel 与 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()
.content();
}
}
在 ChatClient 中,通过 defaultSystem 来设置 AI 模型的 默认系统消息,通过 ChatClient.Builder 链式调用设置的系统消息会作为对话的 "初始指令",注入到每次对话的上下文中,引导 AI 的回复风格或身份设定。
UserMessage
UserMessage 表示我们提出的具体问题或指令,上述输入的 "你是谁",就是 UserMessage
AssistantMessage
AssistantMessage:是 AI 模型给出的回复。
AssistantMessage 是实现 连贯多轮对话 的关键。每次 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 模型生成过程:
"春天"... (立即返回)
"春天来了"... (继续返回)
"春天来了,万物复苏"... (持续返回)
直到生成完整回答
Spring AI 主要通过 响应式编程 来实现流式输出,使用 stream() 方法生成 Flux 流。
@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)。
此时客户端不会关闭连接,会一直等待服务器发送过来新的数据流。
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 的常见取值有: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 秒后再尝试重连。
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++) {
String s = "event: foo\n";
s += "data: " + new Date() + "\n\n";
writer.write(s);
writer.flush();
Thread.sleep(1000L);
}
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></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 的使用流程:创建 → 转换 → 过滤 → 消费。
import reactor.core.publisher.Flux;
Flux<String> fixedFlux = Flux.just("Hello", "World", "!");
List<String> list = Arrays.asList("A", "B", "C");
Flux<String> fromCollection = Flux.fromIterable(list);
Flux<Integer> rangeFlux = Flux.range(1, 5);
Flux<Long> intervalFlux = Flux.interval(Duration.ofSeconds(1)).take(5);
Flux<String> arrayFlux = Flux.fromArray(new String[]{"X", "Y", "Z"});
Flux<String> emptyFlux = Flux.empty();
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Result");
Flux<String> futureFlux = Flux.fromFuture(future);
Flux<String> original = Flux.just("apple", "banana", "cherry");
Flux<String> uppercased = original.map(String::toUpperCase);
Flux<String> words = Flux.just("hello world", "spring ai");
Flux<String> splitWords = words.flatMap(word -> Flux.fromArray(word.split(" ")));
Flux<Object> objects = Flux.just("text1", "text2");
Flux<String> strings = objects.cast(String.class);
Flux<Integer> numbers = Flux.range(1, 4);
Flux<Integer> cumulativeSum = numbers.scan((acc, current) -> acc + current);
Flux<Integer> allNumbers = Flux.range(1, 10);
Flux<Integer> evenNumbers = allNumbers.filter(n -> n % 2 == 0);
Flux<String> withDuplicates = Flux.just("A", "B", "A", "C");
Flux<String> uniqueItems = withDuplicates.distinct();
Flux<String> limited = original.take(2);
Flux<String> skipped = original.skip(1);
Flux<Integer> sequence = Flux.range(1, 100);
Flux<Integer> firstPart = sequence.takeWhile(n -> n < 10);
Flux<Long> sampled = Flux.interval(Duration.ofMillis(100))
.sample(Duration.ofSeconds(1))
.take(3);
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!")
);
Mono<List<String>> listMono = data.collectList();
Mono<Integer> sum = numbers.reduce(0, Integer::sum);
Mono<Long> count = data.count();
Mono<Boolean> hasData = data.hasElements();
Mono<Void> completionSignal = data.then();
Advisors
Advisors 是 Spring AI 中的一种拦截器机制,允许我们在 AI 调用链的特定节点注入自定义逻辑。
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) {}
}
logging:
level:
org.springframework.ai.chat.client.advisor: debug
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online