目录
[1. 项目介绍](#1. 项目介绍)
[1.1 项目背景和目标](#1.1 项目背景和目标)
[1.2 核心功能](#1.2 核心功能)
[2. 界面设计](#2. 界面设计)
[3. 搭建环境](#3. 搭建环境)
[3.1 创建项目](#3.1 创建项目)
[3.2 添加依赖&写启动类](#3.2 添加依赖&写启动类)
[3.3 添加配置文件](#3.3 添加配置文件)
[4. 简单对话](#4. 简单对话)
[4.1 定义接口](#4.1 定义接口)
[4.2 代码实现](#4.2 代码实现)
[5. 对话记忆](#5. 对话记忆)
[5.1 Chat Memory](#5.1 Chat Memory)
[6. 会话历史](#6. 会话历史)
[6.1 定义会话列表](#6.1 定义会话列表)
[6.2 获取会话列表](#6.2 获取会话列表)
[6.3 获取会话记录](#6.3 获取会话记录)
[6.4 删除会话记录](#6.4 删除会话记录)
代码:spring-ai-first-term-practice/spring-ai-chat-bot · 巨蛇神/JavaEE进阶 - 码云 - 开源中国
1. 项目介绍
1.1 项目背景和目标
**时代背景:**现在大模型能力飞速提高,使用范围也逐渐扩大,为了跟上时代,必须要体验一把运用AI大模型的感觉,感受它的强大并提升自己的能力。此刻心想 "DeekSeek比较便宜,性能也出色,就选他了!"
技术背景: 在Spring AI 中引入的模型 默认 不具备上下文,因此同一个对话下你与AI交互时AI无法回答这样的问题,第一次输入:"我有100元,现在花了10元,还剩多少元?" 输出:"剩90元";第二次输入:"我又花了10元,还剩多少元?" 输出:"你好,我不知道你有多少钱,你可以先告诉我"。为了具备上下文,开发者可以添加上下文相关的advisor,或使用数据库保存每次输入输出信息。
产品目标:
- 提供流畅,自然的对话体验
- 支持多轮对话及上下文理解
- 记录和管理用户历史对话
1.2 核心功能
1. 对话
支持用户与机器人流畅对话。
实时响应用户输入,流式输出自然语言。
2. 多轮对话
能够理解和处理多轮对话
支持基于上下文的智能应答
3. 历史记录
自动保存用户与机器人的对话历史
支持 添加新对话、查看历史和删除历史对话
2. 界面设计
该项目只关注后端的智能体构建,前端代码在:巨蛇神/JavaEE进阶 - 码云 - 开源中国

3. 搭建环境
3.1 创建项目
创建项目:spring-ai-chat-bot
3.2 添加依赖&写启动类
添加pom依赖:
<!-- 加入spring boot --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> <!-- lookup parent from repository --> </parent>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> </dependencies>
添加bom:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>2.0.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
启动类:

3.3 添加配置文件
server: port: 8080 spring: application: name: spring-ai-chat-bot ai: openai: api-key: ${DEEPSEEK_API_KEY_SPRING_AI} #DeepSeek api-key base-url: https://api.deepseek.com chat: options: model: deepseek-flash #目前代表的是DeepSeek-V4.1-Flash temperature: 0.7 #温度越高,分子越容易发散——》AI生成的内容越不固定(后一次生成的内容和前一次不大相同) logging: level: org.springframework.ai.chat.client.advisor: debug pattern: console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" file: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
4. 简单对话
4.1 定义接口
请求
GET/POST /chat/stream
参数
prompt(String):用 户输入的消息内容
响应
Flux 流式返回机器人回复的内容 (text/html;charset=utf-8)
4.2 代码实现
1.配置ChatClient:
@Configuration public class CommonConfiguration { @Bean public ChatClient chatClient(ChatModel model){ return ChatClient.builder(model) .defaultSystem("你叫小蛇,是学生在学习英语的顶级魔王护,英语能力超雅思9分的高级学习助理") .defaultAdvisors(new SimpleLoggerAdvisor()) .build(); } }
2.流式返回:
@RequestMapping("/chat") @RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient = chatClient; } @RequestMapping(value = "/stream",produces = "text/html;charset=utf-8") public Flux<String> stream(String prompt){ return chatClient.prompt() .user(prompt) .stream() .content(); } }
直到这里,已经可以启动后端进行人机交互了。但是你会发现AI没有记忆!在同一会话中进行多轮对话,它会出现如下情况:

默认情况下大模型是没有记忆的,但是Spring AI 官方提供了实现对话记忆的接口,下面介绍。
5. 对话记忆
让AI记住上下文,就要给它每次人机交互的信息。Spring AI 官方定义了三种角色消息类型。
Spring AI角色消息类型
在对话系统,尤其是大语言模型应用中,SystemMessage、UserMessage和AssistantMessage 是三种核心角色消息类型,用于构建上下文感知的对话框架。
- SystemMessage:系统消息,用于设定对话背景、角色、行为准则等,在系统启动前设置。
- UserMessage:用户消息,用户输入的文本内容。
- AssistantMessage:助理消息,模型生成返回的信息。
例:
SystemMessage:"你叫小蛇,是学生在学习英语的顶级魔王护,英语能力超雅思9分的高级学习助理" UserMessage:"你是谁" AssistantMessage:"我是小蛇,你的英语学习“顶级魔王护”,也是你的高级学习助理!"
5.1 Chat Memory
大语言模型是无状态的,也就是说他不会记得以前的数据。为了解决这个问题,Spring AI 官方提供了对话内存功能,定义了ChatMemory接口,允许和方便 开发者创建、维护上下文。Spring AI内置了InMemoryChatMemoryRepository,它是spring ai默认提供的bean,它把记忆存储到内存中,重启服务之前的记忆就没了。
ChatMemory接口的抽象函数:|------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| void add(String conversationId, List<Message> messages); | 第一个 参数是会话ID,每个会话ID都要是独一无二的(常用UUID),不传会抛 IllegalArgumentException ;第二个参数是人机交互产生的信息,同一个会话中的信息在存储时带有的相同的ID。 |
| List<Message> get(String conversationId); | 根据ID返回会话所有信息,再由 MessageWindowChatMemoryzhe这类实现类在内部根据窗口大小获取信息和处理信息 |
| void clear(String conversationId); | 清空指定会话存储 |
实现上下文:
InMemoryChatMemoryRepository 不能单独使用,要配合 MessageWindowChatMemory:
第一步修改接口:
请求
GET/POST /chat/stream
参数
prompt(String): 用户输入 的消息内容
chatId (String) : 会话标识 ID, 由前端生成 , 不重复 , 新建会话时 , 创建新的 chatID响应
Flux 流式返回的机器人回复内容 (text/html;charset=utf-8)
java
@RequestMapping(value = "/stream",produces = "text/html;charset=utf-8")
public Flux<String> stream(String prompt,String chatId){ //前后端生成UUID都可以,这里前端生成并返回UUID
return chatClient.prompt()
.user(prompt)
.advisors(spec-> spec.param(ChatMemory.CONVERSATION_ID,chatId))//必须设置
.stream()
.content();
}
第二步加入advisors:
**方式一.**若手动创建InMemoryChatMemoryRepository bean,InMemoryChatMemoryRepository就要与MessageWindowChatMemory 搭配才能能添加到advisors里。如下图"第一步"可知,它俩搭配后转化为ChatMemory类型的数据。

方式二. 对于上面的手动创建或许有人在Spring AI官网:Chat Memory :: Spring AI Reference 看到下面这张图的写法也正常,这和上面一样引入的都是默认提供的 InM...Repository ,只不过隐式完成了"第一步"。

方式三. spring DI 自动注入ChatMemory,开发者不需要手动"第一步",spring已经隐式帮我们完成了。DI如下:

6. 会话历史
后端使用ChatMemory维护了一段上下文,人机可进行多轮有记忆对话。现在为前端实现会话列表,下图是前端实现的历史会话列表,一刷新就没了。

6.1 定义会话列表
总目标:1. 后端需实现 历史会话列表,前端接收后端信息,展示列表数据,点进会话看见以前的交互信息**;2.** 每个会话显示最后一轮用户发送的信息**;3.**当从历史会话列表删除会话记录,同时删除会话内的信息。
功能列表:
- 历史会话列表
- 根据会话ID,返回历史信息
- 删除历史会话
第一步:模仿ChatMemory,写一个包含add、get和clear方法的ChatHistoryRepository接口。
java
public interface ChatHistoryRepository {
void add(String chatId,String title);
List<ChatInfo> get();
void clear(String chatId);
}
第二步:实现实体类,定义会话列表信息

第三步:MemoryChatHistoryRepository实现ChatHistoryRepository接口
java
@Repository
public class MemoryChatHistoryRepository implements ChatHistoryRepository{
Map<String,String> chatInfos = new LinkedHashMap<>(); //有顺序的Map,返回给前端时元素的顺序与用户创建会话的顺序一致
/**
* 保存会话记录
* 如果首次创建会话,则新增数据
* 若会话已存在,则修改数据
*/
@Override
public void add(String chatId, String title) {
chatInfos.put(chatId,title);
}
/**
*返回会话列表
*/
@Override
public List<ChatInfo> get() {
return chatInfos.entrySet().stream()
//转化成ChatInfo类型
.map(entry->new ChatInfo(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
/**
* 根据会话ID删除会话
*/
@Override
public void clear(String chatId) {
chatInfos.remove(chatId);
}
}
第四步:修改ChatController
@Autowired private ChatHistoryRepository memoryChatHistoryRepository;
@RequestMapping(value = "/stream",produces = "text/html;charset=utf-8") public Flux<String> stream(String prompt,String chatId){ log.info("chatId:{},prompt:{}",chatId,prompt); //加入会话列表 memoryChatHistoryRepository.add(chatId,prompt); return chatClient.prompt() .user(prompt) .advisors(spec-> spec.param(ChatMemory.CONVERSATION_ID,chatId)) .stream() .content(); }
6.2 获取会话列表
请求
GET/POST /chat/getChatIds
参数
无
响应
会话 ID 列表 List<ChatInfo>
java
//获取会话列表
@RequestMapping("/getChatIds")
public List<ChatInfo> getChatIds(){
return memoryChatHistoryRepository.get();
}
6.3 获取会话记录
因为消息有四种类型:USER、SYSTEM 、ASSISTANT 和TOOL ,后端为了区分信息属于角色,需要实现实体类:MessageVO

第一步:实现MessageVO
java
@Data
public class MessageVO {
private String role;
private String content;
//判断角色
public MessageVO(Message message) {
switch (message.getMessageType()){
case USER ->{this.role = "user"; break;}
case ASSISTANT ->{this.role = "assistant"; break;}
case SYSTEM ->{this.role = "system"; break;}
case TOOL ->{this.role = "tool"; break;}
}
this.content=message.getText();
}
第二步:调用ChatMemory.get,前端接收
根据会话Id从ChatMemory获取会话信息记录
请求
GET/POST /chat/getChatHistory
参数
chatId (String) : 会话标识 ID
响应
会话历史记录 List<MessageVO>
java
//获取会话记录
@RequestMapping("/getChatHistory")
public List<MessageVO> getChatHistory(String chatId){
log.info("获取会话记录,chatId:{}",chatId);
List<Message> messages = chatMemory.get(chatId);
//List<Message>转化成List<MessageVO>
return messages.stream().map(MessageVO::new).collect(Collectors.toList());
}
6.4 删除会话记录
用户在前端删除会话列表记录时返回会话Id,后端根据会话id从会话列表中删除和对应Id的所有交互信息。
请求
GET/POST /chat/deleteChat
参数
chatId (String) : 会话标识 ID
响应
true/false
java
@RequestMapping("/deleteChat")
public Boolean deleteChat(String chatId){
log.error("删除会话记录,chatId:{}",chatId);
try {
memoryChatHistoryRepository.clear(chatId);
chatMemory.clear(chatId);
}catch(Exception e){
log.error("会话记录删除失败,chatId:{}",chatId);
return false;
}
return true;
}