AI大模型基础和SpringAI1.0.1的接入和基本使用
前言
目前大模型应用开发比较常见的框架是基于Python语言的LangChain,但是对于Java程序员平时使用Spring生态
开发的大量应用来说,适配性就稍微差了些。
而Spring公司推出的SpringAI框架,充分利用了Spring框架中AOP、IOC的能力,可以与现有的Java项目无缝合,
非常方便。
但是Spring AI要求的JDK版本至少是JDK17,SpringBoot也必须是3.x的版本才可以,JDK8是不支持使用Spring AI的。
大模型调用基础知识
以DeepSeek官方给出的文档为例:
# Please install OpenAI SDK first: `pip3 install openai`
from openai import OpenAI
# 1.初始化OpenAI客户端,要指定两个参数:api_key、base_url
client = OpenAI(api_key="<DeepSeek API Key>", base_url="https://api.deepseek.com")
# 2.发送http请求到大模型,参数比较多
response = client.chat.completions.create(
model="deepseek-chat", # 2.1.选择要访问的模型
messages=[ # 2.2.发送给大模型的消息
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "Hello"},
],
stream=False # 2.3.是否以流式返回结果
)
print(response.choices[0].message.content)
接口说明
- 请求方式:通常是POST,因为要传递JSON风格的参数
- 请求路径:与平台有关
- DeepSeek官方平台:https://api.deepseek.com
- 阿里云百炼平台:https://dashscope.aliyuncs.com/compatible-mode/v1
- 本地ollama部署的模型:http://localhost:11434
- 安全校验:开放平台都需要提供API_KEY来校验权限,本地ollama则不需要
- 请求参数:参数很多,比较常见的有:
- model:要访问的模型名称
- messages:发送给大模型的消息,是一个数组
- stream:true,代表响应结果流式返回;false,代表响应结果一次性返回,但需要等待
- temperature:取值范围[0:2),代表大模型生成结果的随机性,越小随机性越低。DeepSeek-R1不支持
注意,这里请求参数中的messages是一个消息数组,而且其中的消息要包含两个属性:
- role:消息对应的角色
- content:消息内容
其中消息的内容,也被称为提示词(Prompt),也就是发送给大模型的指令。
提示词角色
通常消息的角色有三种:
| 角色 | 描述 | 示例 |
|---|---|---|
| system | 优先于user指令之前的指令,也就是给大模型设定角色和任务背景的系统指令 | 你是一个乐于助人的编程助手,你以小团团的风格来回答用户的问题。 |
| user | 终端用户输入的指令(类似于你在ChatGPT聊天框输入的内容) | 写一首关于Java编程的诗 |
| assistant | 由大模型生成的消息,可能是上一轮对话生成的结果 | 注意,用户可能与模型产生多轮对话,每轮对话模型都会生成不同结果。 |
其中System类型的消息非常重要!影响了后续AI会话的行为模式。
比如,我们会发现,当我们询问这些AI对话产品“你是谁”这个问题的时候,每一个AI的回答都不一样。
而回答的模式就是遵循System的设定来回答。
会话记忆问题
这里还有一个问题:
我们为什么要把历史消息都放入Messages中,形成一个数组呢?
这是因为大模型是没有记忆的,因此我们调用API接口与大模型对话时,每一次对话信息都不会保留,多次对话之间都是独立的,没有关联的。
但是大家可能发现了,我们使用的AI对话产品却能够记住每一轮对话信息,根据这些信息进一步回答,这是怎么回事呢?
答案就是Messages数组。
我们只需要每一次发送请求时,都把历史对话中每一轮的User消息、Assistant消息都封装到Messages数组中,一起发送给大模型,这样大模型就会根据这些历史对话信息进一步回答,就像是拥有了记忆一样。
这里还有一个问题:
我们为什么要把历史消息都放入Messages中,形成一个数组呢?
这是因为大模型是没有记忆的,因此我们调用API接口与大模型对话时,每一次对话信息都不会保留,多次对话之间都是独立的,没有关联的。
但是大家可能发现了,我们使用的AI对话产品却能够记住每一轮对话信息,根据这些信息进一步回答,这是怎么回事呢?
答案就是Messages数组。
我们只需要每一次发送请求时,都把历史对话中每一轮的User消息、Assistant消息都封装到Messages数组中,一起发送给大模型,这样大模型就会根据这些历史对话信息进一步回答,就像是拥有了记忆一样。

Spring AI的接入与基本使用
下面的配置基于Spring AI 1.0.1 正式版和Spring Boot 3.5.5。
Maven依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
第一步,直接引入Spring AI中关于openai的起步依赖,这样一来,项目中就可以使用Spring AI接入本地或者各类远程的AI大模型了。
Yaml配置
spring:
application:
name: demo-ai
ai:
openai:
base-url: https://api.deepseek.com/
api-key:
chat:
options:
model: deepseek-reasoner
temperature: 0.7
这里需要配置openAI的相关配置:
-
base-url:模型的地址
-
api-key:apply for an API key,调用远程模型时一般都是收费的,如果是本地模型可以没有这个配置
-
model:选择的远程模型种类
-
temperature:温度,意思就是回答的随机性,值越大,回答的随机性越大,这和Transformer神经网络的推理模式有关,根据前文推测出接下来的一个词语后,把这个词语加入前文,再次交给大模型处理,推测下一个字,然后不断重复前面的过程,就可以生成大段的内容了。
这里就以配置Deepseek远程模型为例,后续改成其它远程大模型比如Open AI都是兼容的。
Deepseek远程模型地址和APIKey获取方式
Deepseek API文档当中可以看到官方提供的base-url和两个常见的model。
Deepseek API文档地址:
https://api-docs.deepseek.com/zh-cn/

Deepseek的ApiKey获取地址:
https://platform.deepseek.com/api_keys
不过这个需要进行充值才可以创建自己的ApiKey。

增加Configuration配置类配置SpringAI的必需Bean
这里就先展示最基本的ChatClient配置,有了它就可以在代码中使用大模型的基本功能了。
@Configuration
public class CommonConfiguration {
@Bean("chatClient")
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem("你是可爱且热情、人见人爱,花见花开的AI助手,你的名字是墩墩,请用墩墩的身份回答用户的问题!")
.build();
}
}
defaultSystem可以修改一些系统的默认配置,给AI设定初始的角色,回答问题的范围等等
提供Controller层对外暴露的接口
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final @Qualifier("chatClient")ChatClient chatClient;
// 非流式的接口,这种情况要等AI大模型完成全部回答再一次性返回结果
@GetMapping("/chat/simple")
@CrossOrigin
public String chat(String prompt) {
return chatClient.prompt(prompt)
.user(prompt)
.call()
.content();
}
// 流式的接口,这种情况就是大模型生成一个字就返回一个字,和我们平时用deepSeek问问题是一样的效果
// 但是这里一定要设置字符编码,要不然是会乱码的
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chatStream(String prompt) {
return chatClient.prompt(prompt)
.user(prompt)
.stream()
.content();
}
}
效果展示

非流式的接口和流式接口返回的结果都是差不多的,只不过一个是一次性返回,一个是一个字一个字往外蹦。
SpringAI 日志配置
默认情况下,应用于AI的交互时不记录日志的,我们无法得知SpringAI组织的提示词到底长什么样,有没有问题。
这样不方便我们调试。
这里就需要用到SpringAI的Advisor。
Advisor的原理
SpringAI基于AOP机制实现与大模型对话过程的增强、拦截、修改等功能。所有的增强通知都需要实现Advisor接口。

SpringAI提供了一些Advisor的默认实现,来实现一些基本的增强功能:

- SimpleLoggerAdvisor:日志记录的Advisor
- MessageChatMemoryAdvisor:会话记忆的Advisor
- QuestionAnswerAdvisor:实现RAG的Advisor
当然,我们也可以自定义Advisor,具体可以参考官方文档:
https://docs.spring.io/spring-ai/reference/1.0/api/advisors.html#_implementing_an_advisor
添加日志Advisor
在配置类中给ChatClient添加日志Advisor。
@Configuration
public class CommonConfiguration {
@Bean("chatClient")
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem("你是可爱且热情、人见人爱,花见花开的AI助手,你的名字是墩墩,请用墩墩的身份回答用户的问题!")
.defaultAdvisors(
new SimpleLoggerAdvisor() // 添加默认的Advisor,记录日志
)
.build();
}
}
在application.yaml中添加日志配置:
logging:
level:
org.springframework.ai.chat.client.advisor: DEBUG
com.xxx.demo.ai: DEBUG
效果展示

这样一来控制台就可以看到AI会话的日志,就可以方便调试了。
SpringAI原生会话记忆功能
由于将会话记忆持久化的篇幅会多一些,这里就讲解最容易的会话记忆实现方式,主要看看思路。
这里依旧是使用上面提到的Advisor去对会话过程进行增强,从而达到会话记忆的功能。
配置会话记忆Advisor
@Configuration
public class CommonConfiguration {
@Bean("chatClient")
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem("你是可爱且热情、人见人爱,花见花开的AI助手,你的名字是墩墩,请用墩墩的身份回答用户的问题!")
.defaultAdvisors(
new SimpleLoggerAdvisor() // 添加默认的Advisor,记录日志
// 有了下面这句话,会话就有了记忆功能
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
}
此时,会话记忆信息是保存在内存当中的,如果重启了系统,会话记忆依旧是会丢的。
会话历史的实现-内存方式
有了单个会话记忆之后还需要有会话历史的功能
比如deepseek这里就是会话历史的功能

定义接口
// 保存会话历史的接口
public interface ChatHistoryService {
/**
* 保存会话历史记录
* @param type 会话业务类型 如:chat(聊天)
* @param chatId 会话ID
*/
void save(String type, String chatId);
/**
* 根据业务类型获取会话id列表
* @param type 会话业务类型 如:chat(聊天)
* @return 会话ID列表
*/
List<String> getChatIds(String type);
}
定义实现类
@Component
@RequiredArgsConstructor
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {
private Map<String, List<String>> chatHistory;
@Override
public void save(String type, String chatId) {
/*if (!chatHistory.containsKey(type)) {
chatHistory.put(type, new ArrayList<>());
}
List<String> chatIds = chatHistory.get(type);*/
List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());
if (chatIds.contains(chatId)) {
return;
}
chatIds.add(chatId);
}
@Override
public List<String> getChatIds(String type) {
/*List<String> chatIds = chatHistory.get(type);
return chatIds == null ? List.of() : chatIds;*/
return chatHistory.getOrDefault(type, List.of());
}
}
Controller层
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final @Qualifier("chatClient")ChatClient chatClient;
private final ChatHistoryService chatHistoryService;
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chatStream(String prompt, String chatId) {
// 1.保存会话id
chatHistoryService.save(ServiceTypeEnum.CHAT.getType(), chatId);
// 2.请求模型
return chatClient.prompt(prompt)
.user(prompt)
// 添加一个SpringAOP环绕增强的配置 用作会话ID记忆,这样每次会话的内容就不会串
.advisors(a -> a.param(CONVERSATION_ID, chatId))
.stream()
.content();
}
@RequestMapping("/history/{type}")
public List<String> getChatIds(@PathVariable String type) {
return chatHistoryService.getChatIds(type);
}
@RequestMapping("/history/{type}/{chatId}")
public List<MessageVO> getChatHistory(@PathVariable String type, @PathVariable String chatId) {
List<Message> messages = chatMemory.get(chatId);
if (messages == null) {
return List.of();
}
return messages.stream().map(MessageVO::new).toList();
}
}
枚举
@Getter
@AllArgsConstructor
public enum ServiceTypeEnum {
/**
* 聊天
*/
CHAT("chat");
private final String type;
}
消息VO类
@Data
@NoArgsConstructor
public class MessageVO {
private String role;
private String content;
public MessageVO(Message message) {
MessageType messageType = message.getMessageType();
switch (messageType) {
case USER:
this.role = "user";
break;
case ASSISTANT:
this.role = "assistant";
break;
default:
this.role = "";
break;
}
this.content = message.getText();
}
}
更多推荐



所有评论(0)