SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发
4 SpringAI入门:对话机器人
4.1 快速入门
4.1.1 创建工程
-
创建一个新的SpringBoot工程,勾选Web、MySQL驱动、Ollama:
-
工程结构:
- 将
application.properties
改成application.yaml
; - 后面将
chatrobot
文件夹重命名为ai
;
- 将
-
初始pom.xml内容:
<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.4</version> <relativePath/> </parent> <groupId>com.shisan</groupId> <artifactId>chat-robot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>chat-robot</name> <description>chat-robot</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0-M6</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>
4.1.2 引入依赖
-
SpringAI完全适配了SpringBoot的自动装配功能,而且给不同的大模型提供了不同的starter,比如:
-
Anthropic:
org.springframework.ai spring-ai-anthropic-spring-boot-starter
-
Azure OpenAI:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId></dependency>
-
DeepSeek:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId></dependency>
-
Hugging Face:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-huggingface-spring-boot-starter</artifactId></dependency>
-
Ollama:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId></dependency>
-
OpenAI:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId></dependency>
-
-
可以根据自己选择的平台来选择引入不同的依赖,此处以Ollama为例;
- 因为在
4.1.1 创建工程
中已经勾选了依赖,所以下面的步骤可以忽略;
- 因为在
-
在项目pom.xml中添加spring-ai的版本信息:
<spring-ai.version>1.0.0-M6</spring-ai.version>
-
然后,添加spring-ai的依赖管理项:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>
-
最后,引入spring-ai-ollama的依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId></dependency>
-
为了方便后续开发,再手动引入一个Lombok依赖==(该步骤不可忽略)==:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version></dependency>
- 注意:千万不要用start.spring.io提供的lombok,有bug!!
-
完整依赖如下:
<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.4</version> <relativePath/> </parent> <groupId>com.shisan</groupId> <artifactId>chat-robot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>chat-robot</name> <description>chat-robot</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0-M6</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>
4.1.3 配置模型信息
-
在配置文件中配置模型的参数信息,以Ollama为例:
spring: application: name: chart-robot ai: ollama: base-url: http://localhost:11434 # ollama服务地址, 这就是默认值 chat: model: deepseek-r1:14b # 模型名称 options: temperature: 0.8 # 模型温度,影响模型生成结果的随机性,越小越稳定
4.1.4 ChatClient
-
ChatClient
中封装了与AI大模型对话的各种API,同时支持同步式或响应式交互; -
在使用之前,需要声明一个
ChatClient
; -
在
com.shisan.ai.config
包下新建一个CommonConfiguration
类: -
代码:
package com.shisan.ai.config;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.ollama.OllamaChatModel;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class CommonConfiguration { // 注意参数中的model就是使用的模型,这里用了Ollama,也可以选择OpenAIChatModel @Bean public ChatClient chatClient(OllamaChatModel model) { return ChatClient.builder(model) // 创建ChatClient工厂 .build(); // 构建ChatClient实例 }}
ChatClient.builder
:会得到一个ChatClient.Builder
工厂对象,利用它可以自由选择模型、添加各种自定义配置;OllamaChatModel
:如果引入了ollama的starter,这里就可以自动注入OllamaChatModel
对象。同理,OpenAI
也是一样的用法。
4.1.5 同步调用
-
接定义一个Controller,在其中接收用户发送的提示词,然后把提示词发送给大模型,交给大模型处理,拿到结果后返回;
-
代码:
package com.shisan.ai.controller;import lombok.RequiredArgsConstructor;import org.springframework.ai.chat.client.ChatClient;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RequiredArgsConstructor@RestController@RequestMapping(\"/ai\")public class ChatController { private final ChatClient chatClient; // 请求方式和路径不要改动,将来要与前端联调 @RequestMapping(\"/chat\") public String chat(@RequestParam(String prompt) { return chatClient .prompt(prompt) // 传入user提示词 .call() // 同步请求,会等待AI全部输出完才返回结果 .content(); //返回响应内容 }}
- 注意,基于
call()
方法的调用属于同步调用,需要所有响应结果全部返回后才能返回给前端;
- 注意,基于
-
启动项目,在浏览器中访问:
http://localhost:8080/ai/chat?prompt=你好
;
4.1.6 流式调用
-
同步调用需要等待很长时间页面才能看到结果,用户体验不好。为了解决这个问题,可以改进调用方式为流式调用;
- 在SpringAI中使用了WebFlux技术实现流式调用;
-
修改
ChatController
中的chat方法:// 注意看返回值,是Flux,也就是流式结果,另外需要设定响应类型和编码,不然前端会乱码@RequestMapping(value = \"/chat\", produces = \"text/html;charset=UTF-8\")public Flux<String> chat(@RequestParam(String prompt) { return chatClient .prompt(prompt) .stream() // 流式调用 .content();}
-
重启测试,再次访问:
4.17 System设定
-
可以发现,当我们询问AI你是谁的时候,它回答自己是DeepSeek-R1,这是大模型底层的设定。如果我们希望AI按照新的设定工作,就需要给它设置System背景信息;
-
在SpringAI中,设置System信息非常方便,不需要在每次发送时封装到Message,而是创建ChatClient时指定即可;
-
修改
CommonConfiguration
中的代码,给ChatClient
设定默认的System信息:@Beanpublic ChatClient chatClient(OllamaChatModel model) { return ChatClient .builder(model) // 创建ChatClient工厂实例 .defaultSystem(\"你是邓超,请以邓超的口吻回答用户的问题。\") .defaultAdvisors(new SimpleLoggerAdvisor()) .build(); // 构建ChatClient实例}
-
再次询问“你是谁?”
- 以上的回答由DeepSeek的深度思考内+最终回答组成。
4.2 日志功能
- 默认情况下,应用于AI的交互时不记录日志的,我们无法得知SpringAI组织的提示词到底长什么样,这样不方便我们调试。
4.2.1 Advisor
-
SpringAI基于AOP机制实现与大模型对话过程的增强、拦截、修改等功能,所有的增强通知都需要实现Advisor接口;
-
Spring提供了一些Advisor的默认实现,来实现一些基本的增强功能:
SimpleLoggerAdvisor
:日志记录的Advisor;MessageChatMemoryAdvisor
:会话记忆的Advisor;QuestionAnswerAdvisor
:实现RAG的Advisor;
-
当然,也可以自定义Advisor,具体可以参考:Advisors API :: Spring AI Reference。
4.2.2 添加日志Advisor
-
需要修改
CommonConfiguration
,给ChatClient
添加日志Advisor:@Beanpublic ChatClient chatClient(OllamaChatModel model) { return ChatClient .builder(model) // 创建ChatClient工厂实例 .defaultSystem(\"你是邓超,请以邓超的口吻回答用户的问题。\") .defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志 .build(); // 构建ChatClient实例}
4.2.3 修改日志级别
-
在
application.yaml
中添加日志配置,修改日志级别:logging: level: org.springframework.ai.chat.client.advisor: debug # AI对话的日志级别 com.shisan.ai: debug # 本项目的日志级别
-
重启项目,再次聊天就能在IDEA的运行控制台中看到AI对话的日志信息了;
4.3 对接前端
-
有两个前端的源代码:
4.3.1 npm运行
-
进入
spring-ai-protal
文件夹(该文件夹要放在非中文目录下),然后执行cmd命令:# 安装依赖npm install# 运行程序npm run dev
-
启动后,访问
http://localhost:5173
即可看到页面:
4.3.2 Nginx运行
-
若不关心源码,进入
spring-ai-nginx
文件夹(该文件夹要放在非中文目录下),然后执行cmd命令:# 启动Nginxstart nginx.exe# 停止nginx.exe -s stop
-
启动后,访问
http://localhost:5173
即可看到页面。
4.3.3 解决CORS问题
-
前后端在不同端口,存在跨域问题,因此需要在服务端解决cors问题;
-
在
com.shisan.ai.config
包中添加一个MvcConfiguration
类:package com.shisan.ai.config;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.CorsRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class MvcConfiguration implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(\"/**\") .allowedOrigins(\"*\") .allowedMethods(\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\") .allowedHeaders(\"*\") .exposedHeaders(\"Content-Disposition\"); }}
-
重启服务,如果服务端接口正确,那么应该就可以聊天了;
- 注意:
- 前端访问服务端的默认路径是:
http://localhost:8080
; - 聊天对话的接口是:POST /ai/chat;
- 请确保服务端接口也是这样。
- 前端访问服务端的默认路径是:
- 注意:
4.3.4 测试
-
启动前端后,访问 http://localhost:5173即可看到页面:
-
点击第一个卡片“AI聊天”进入对话机器人页面:
4.4 会话记忆功能
-
目前的AI聊天机器人是没有记忆功能的;
-
前面讲过,让AI有会话记忆的方式就是把每一次历史对话内容拼接到Prompt中,一起发送过去,这种方式比较挺麻烦;
-
但如果使用了SpringAI,并不需要自己拼接,SpringAI自带了会话记忆功能,可以把历史会话保存下来,下一次请求AI时会自动拼接,非常方便。
4.4.1 ChatMemory
-
会话记忆功能同样是基于AOP实现,Spring提供了一个
MessageChatMemoryAdvisor
的通知,我们可以像之前添加日志通知一样添加到ChatClient
即可; -
不过,要注意的是,
MessageChatMemoryAdvisor
需要指定一个ChatMemory
实例,也就是会话历史保存的方式; -
ChatMemory
接口声明如下:public interface ChatMemory { // TODO: consider a non-blocking interface for streaming usages default void add(String conversationId, Message message) { this.add(conversationId, List.of(message)); } // 添加会话信息到指定conversationId的会话历史中 void add(String conversationId, List<Message> messages); // 根据conversationId查询历史会话 List<Message> get(String conversationId, int lastN); // 清除指定conversationId的会话历史 void clear(String conversationId);}
- 可以看到,所有的会话记忆都是与
conversationId
有关联的,也就是会话Id,将来不同会话Id的记忆自然是分开管理的;
- 可以看到,所有的会话记忆都是与
-
目前,在SpringAI中有两个ChatMemory的实现:
InMemoryChatMemory
:会话历史保存在内存中;CassandraChatMemory
:会话保存在Cassandra数据库中(需要引入额外依赖,并且绑定了向量数据库,不够灵活);
-
目前选择用
InMemoryChatMemory
来实现。
4.4.2 添加会话记忆Advisor
-
在
CommonConfiguration
中注册ChatMemory
对象:@Beanpublic ChatMemory chatMemory() { return new InMemoryChatMemory();}
-
然后添加
MessageChatMemoryAdvisor
到ChatClient
:@Beanpublic ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) { return ChatClient.builder(model) // 创建ChatClient工厂实例 .defaultSystem(\"你是邓超,请以邓超的口吻回答用户的问题。\") .defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志 .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); // 构建ChatClient实例}
-
目前
com/shisan/ai/config/CommonConfiguration.java
的完整代码如下:package com.shisan.ai.config;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;import org.springframework.ai.chat.memory.ChatMemory;import org.springframework.ai.chat.memory.InMemoryChatMemory;import org.springframework.ai.ollama.OllamaChatModel;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class CommonConfiguration { //注册ChatMemory对象,开启会话记忆功能 @Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } // // 注意参数中的model就是使用的模型,这里用了Ollama,也可以选择OpenAIChatModel// @Bean// public ChatClient chatClient(OllamaChatModel model) {// return ChatClient// .builder(model) // 创建ChatClient工厂// .build(); // 构建ChatClient实例// } @Bean public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) { return ChatClient .builder(model) // 创建ChatClient工厂实例 .defaultSystem(\"你是邓超,请以邓超的口吻回答用户的问题。\") .defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志 .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) // 添加默认的Advisor,记录会话记录 //或者可以有下面的写法:// .defaultAdvisors(// new SimpleLoggerAdvisor(),// new MessageChatMemoryAdvisor(chatMemory)// ) .build(); // 构建ChatClient实例 }}
-
现在聊天会话已经有记忆功能了,不过现在的会话记忆还是不完善的,接下来的章节还会继续补充。
4.5 会话历史
-
注意区分会话历史与会话记忆:
- 会话记忆:是指让大模型记住每一轮对话的内容,不至于前一句刚问完,下一句就忘了;
- 会话历史:是指要记录总共有多少不同的对话;
-
以DeepSeek为例,页面上的会话历史:
-
在ChatMemory中,会记录一个会话中的所有消息,记录方式是以
conversationId
为key,以List
为value,根据这些历史消息,大模型就能继续回答问题,这就是所谓的会话记忆; -
而会话历史,就是每一个会话的
conversationId
,将来根据conversationId
再去查询List
;- 比如上图中,有3个不同的会话历史,就会有3个
conversationId
,管理会话历史,就是记住这些conversationId
,当需要的时候查询出conversationId
的列表;
- 比如上图中,有3个不同的会话历史,就会有3个
-
注意,在接下来业务中,以
chatId
来代指conversationId
。
4.5.1 管理会话id(会话历史)
-
由于会话记忆是以
conversationId
来管理的,也就是**会话id(以后简称为chatId)。**将来要查询会话历史,其实就是查询历史中有哪些chatId; -
因此,为了实现查询会话历史记录,必须记录所有的chatId,需要定义一个管理会话历史的标准接口;
-
定义一个
com.shisan.ai.repository
包,然后新建一个ChatHistoryRepository
接口:package com.shisan.ai.repository;import java.util.List;public interface ChatHistoryRepository { /** * 保存会话记录 * @param type 业务类型,如:chat、service、pdf * @param chatId 会话ID */ void save(String type, String chatId); /** * 获取会话ID列表 * @param type 业务类型,如:chat、service、pdf * @return 会话ID列表 */ List getChatIds(String type);}
-
在这个包下继续创建一个实现类
InMemoryChatHistoryRepository
:package com.shisan.ai.repository;import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;import java.util.Map;@Component@RequiredArgsConstructorpublic class InMemoryChatHistoryRepository implements ChatHistoryRepository { private Map<String, List<String>> chatHistory = new HashMap<>(); @Override public void save(String type, String chatId) { /*if (!chatHistory.containsKey(type)) { chatHistory.put(type, new ArrayList()); } List 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 chatIds = chatHistory.get(type); return chatIds == null ? List.of() : chatIds;*/ return chatHistory.getOrDefault(type, List.of()); }}
-
注意:
- 目前业务比较简单,因此简单采用内存保存type与chatId关系;
- 将来也可以根据业务需要把会话id持久化保存到Redis、MongoDB、MySQL等数据库;
- 如果业务中有user的概念,还需要记录userId、chatId、time等关联关系。
4.5.2 保存会话id
-
接下来,修改
ChatController
中的chat方法,做到以下3点:- 添加一个请求参数:chatId,每次前端请求AI时都需要传递chatId;
- 每次处理请求时,将chatId存储到ChatRepository;
- 每次发请求到AI大模型时,都传递自定义的chatId;
package com.shisan.ai.controller;import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;import com.shisan.ai.repository.ChatHistoryRepository;import lombok.RequiredArgsConstructor;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.chat.memory.ChatMemory;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;@RequiredArgsConstructor@RestController@RequestMapping(\"/ai\")public class ChatController { private final ChatClient chatClient; private final ChatMemory chatMemory; private final ChatHistoryRepository chatHistoryRepository; //同步调用 // 请求方式和路径不要改动,将来要与前端联调// @RequestMapping(\"/chat\")// public String chat(@RequestParam(defaultValue = \"讲个笑话\") String prompt) {// return chatClient// .prompt(prompt) // 传入user提示词// .call() // 同步请求,会等待AI全部输出完才返回结果// .content(); //返回响应内容// } //流式调用 // 注意看返回值,是Flux,也就是流式结果,另外需要设定响应类型和编码,不然前端会乱码 @RequestMapping(value = \"/chat\", produces = \"text/html;charset=UTF-8\") public Flux<String> chat(String prompt, String chatId) { //1、保存会话id chatHistoryRepository.save(\"chat\", chatId); //2、请求模型 return chatClient .prompt(prompt) .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)) .stream() .content(); }}
-
注意,这里传递chatId给Advisor的方式是通过AdvisorContext,也就是以key-value形式存入上下文:
chatClient.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
- 其中的*
CHAT_MEMORY_CONVERSATION_ID_KEY
*是AbstractChatMemoryAdvisor
中定义的常量key,将来MessageChatMemoryAdvisor
执行的过程中就可以拿到这个chatId了。
- 其中的*
4.5.3 查询会话历史
-
定义一个新的Controller,专门实现回话历史的查询。包含两个接口:
- 根据业务类型查询会话历史列表(将来有3个不同业务,需要分别记录历史。可以自己扩展成按userId记录,根据UserId查询);
- 根据chatId查询指定会话的历史消息;
- 其中,查询会话历史消息,也就是Message集合。但是由于Message并不符合页面的需要,所以需要自己定义一个VO;
-
定义一个
com.shisan.ai.entity.vo
包,在其中定义一个MessageVO
类:package com.shisan.ai.entity.vo;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.ai.chat.messages.Message;@NoArgsConstructor@Datapublic class MessageVO { private String role; private String content; public MessageVO(Message message) { this.role = switch (message.getMessageType()) { case USER -> \"user\"; case ASSISTANT -> \"assistant\"; case SYSTEM -> \"system\"; default -> \"\"; }; this.content = message.getText(); }}
-
在
com.shisan.ai.controller
包下新建一个ChatHistoryController
:package com.shisan.ai.controller;import com.shisan.ai.entity.vo.MessageVO;import com.shisan.ai.repository.ChatHistoryRepository;import lombok.RequiredArgsConstructor;import org.springframework.ai.chat.memory.ChatMemory;import org.springframework.ai.chat.messages.Message;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;@RequiredArgsConstructor@RestController@RequestMapping(\"/ai/history\")public class ChatHistoryController { private final ChatHistoryRepository chatHistoryRepository; private final ChatMemory chatMemory; /** * 查询会话历史列表 * @param type 业务类型,如:chat,service,pdf * @return chatId列表 */ @GetMapping(\"/{type}\") public List<String> getChatIds(@PathVariable(\"type\") String type) { return chatHistoryRepository.getChatIds(type); } /** * 根据业务类型、chatId查询会话历史 * @param type 业务类型,如:chat,service,pdf * @param chatId 会话id * @return 指定会话的历史消息 */ @GetMapping(\"/{type}/{chatId}\") public List<MessageVO> getChatHistory(@PathVariable(\"type\") String type, @PathVariable(\"chatId\") String chatId) { List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE); if(messages == null) { return List.of(); } return messages.stream().map(MessageVO::new).toList(); }}
-
重启服务,现在AI聊天机器人就具备会话记忆和会话历史功能了!
4.6 完善会话记忆
- 目前,会话记忆是基于内存,重启服务就没了;
- 如果要持久化保存,这里提供了3种办法:
- 依然是基于InMemoryChatMemory,但是在项目停机时,或者使用定时任务实现自动持久化;
- 自定义基于Redis的ChatMemory;
- 基于SpringAI官方提供的CassandraChatMemory,同时会自动启用CassandraVectorStore。
4.6.1 定义可序列化的Message
-
前面的两种方案,都面临一个问题,SpringAI中的Message类未实现Serializable接口,也没提供public的构造方法,因此无法基于任何形式做序列化。所以必须定义一个可序列化的Message类,方便后续持久化。定义一个
com.shisan.ai.entity.po
包,新建一个Msg
类:package com.shisan.ai.entity.po;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.ai.chat.messages.*;import java.util.List;import java.util.Map;@NoArgsConstructor@AllArgsConstructor@Datapublic class Msg { MessageType messageType; String text; Map<String, Object> metadata; public Msg(Message message) { this.messageType = message.getMessageType(); this.text = message.getText(); this.metadata = message.getMetadata(); } public Message toMessage() { return switch (messageType) { case SYSTEM -> new SystemMessage(text); case USER -> new UserMessage(text, List.of(), metadata); case ASSISTANT -> new AssistantMessage(text, metadata, List.of(), List.of()); default -> throw new IllegalArgumentException(\"Unsupported message type: \" + messageType); }; }}
-
这个类中有两个关键方法:
- 构造方法:实现将SpringAI的Message转为我们的Msg的功能;
- toMessage方法:实现将我们的Msg转为SpringAI的Message;
-
4.6.2 方案一:定期持久化
-
接下来,将SpringAI提供的
InMemoryChatMemory
中的数据持久化到本地磁盘,并且在项目启动时加载; -
不仅如此,将
ChatHistoryRepository
也持久化了; -
注意:
- 本方案中,采用Spring的生命周期方法,在项目启动时加载持久化文件,在项目停机时持久化数据;
- 也可以考虑使用定时任务完成持久化,项目启动加载的方案;
-
修改
com.shisan.ai.repository.InMemoryChatHistoryRepository
类,添加持久化功能:package com.shisan.ai.repository;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.core.type.TypeReference;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.ObjectWriter;import com.itheima.ai.entity.po.Msg;import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.ai.chat.memory.ChatMemory;import org.springframework.ai.chat.memory.InMemoryChatMemory;import org.springframework.ai.chat.messages.Message;import org.springframework.core.io.FileSystemResource;import org.springframework.stereotype.Component;import java.io.IOException;import java.io.PrintWriter;import java.lang.reflect.Field;import java.nio.charset.StandardCharsets;import java.util.*;@Slf4j@Component@RequiredArgsConstructorpublic class InMemoryChatHistoryRepository implements ChatHistoryRepository { private Map<String, List<String>> chatHistory; private final ObjectMapper objectMapper; private final ChatMemory chatMemory; @Override public void save(String type, String chatId) { /*if (!chatHistory.containsKey(type)) { chatHistory.put(type, new ArrayList()); } List 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 chatIds = chatHistory.get(type); return chatIds == null ? List.of() : chatIds;*/ return chatHistory.getOrDefault(type, List.of()); } @PostConstruct private void init() { // 1.初始化会话历史记录 this.chatHistory = new HashMap<>(); // 2.读取本地会话历史和会话记忆 FileSystemResource historyResource = new FileSystemResource(\"chat-history.json\"); FileSystemResource memoryResource = new FileSystemResource(\"chat-memory.json\"); if (!historyResource.exists()) { return; } try { // 会话历史 Map<String, List<String>> chatIds = this.objectMapper.readValue(historyResource.getInputStream(), new TypeReference<>() { }); if (chatIds != null) { this.chatHistory = chatIds; } // 会话记忆 Map<String, List<Msg>> memory = this.objectMapper.readValue(memoryResource.getInputStream(), new TypeReference<>() { }); if (memory != null) { memory.forEach(this::convertMsgToMessage); } } catch (IOException ex) { throw new RuntimeException(ex); } } private void convertMsgToMessage(String chatId, List<Msg> messages) { this.chatMemory.add(chatId, messages.stream().map(Msg::toMessage).toList()); } @PreDestroy private void persistent() { String history = toJsonString(this.chatHistory); String memory = getMemoryJsonString(); FileSystemResource historyResource = new FileSystemResource(\"chat-history.json\"); FileSystemResource memoryResource = new FileSystemResource(\"chat-memory.json\"); try ( PrintWriter historyWriter = new PrintWriter(historyResource.getOutputStream(), true, StandardCharsets.UTF_8); PrintWriter memoryWriter = new PrintWriter(memoryResource.getOutputStream(), true, StandardCharsets.UTF_8) ) { historyWriter.write(history); memoryWriter.write(memory); } catch (IOException ex) { log.error(\"IOException occurred while saving vector store file.\", ex); throw new RuntimeException(ex); } catch (SecurityException ex) { log.error(\"SecurityException occurred while saving vector store file.\", ex); throw new RuntimeException(ex); } catch (NullPointerException ex) { log.error(\"NullPointerException occurred while saving vector store file.\", ex); throw new RuntimeException(ex); } } private String getMemoryJsonString() { Class<InMemoryChatMemory> clazz = InMemoryChatMemory.class; try { Field field = clazz.getDeclaredField(\"conversationHistory\"); field.setAccessible(true); Map<String, List<Message>> memory = (Map<String, List<Message>>) field.get(chatMemory); Map<String, List<Msg>> memoryToSave = new HashMap<>(); memory.forEach((chatId, messages) -> memoryToSave.put(chatId, messages.stream().map(Msg::new).toList())); return toJsonString(memoryToSave); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } } private String toJsonString(Object object) { ObjectWriter objectWriter = this.objectMapper.writerWithDefaultPrettyPrinter(); try { return objectWriter.writeValueAsString(object); } catch (JsonProcessingException e) { throw new RuntimeException(\"Error serializing documentMap to JSON.\", e); } }}
4.6.3 方案二:自定义ChatMemory
-
接下来,基于Redis来实现自定义ChatMemory;
-
首先,在项目中引入spring-data-redis的starter依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>
-
在
com.shisan.ai.repository
包中新建一个RedisChatMemory
类:package com.shisan.ai.repository;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import com.itheima.ai.entity.po.Msg;import lombok.RequiredArgsConstructor;import org.springframework.ai.chat.memory.ChatMemory;import org.springframework.ai.chat.messages.Message;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import java.util.List;@RequiredArgsConstructor@Componentpublic class RedisChatMemory implements ChatMemory { private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper; private final static String PREFIX = \"chat:\"; @Override public void add(String conversationId, List<Message> messages) { if (messages == null || messages.isEmpty()) { return; } List<String> list = messages.stream().map(Msg::new).map(msg -> { try { return objectMapper.writeValueAsString(msg); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }).toList(); redisTemplate.opsForList().leftPushAll(PREFIX + conversationId, list); } @Override public List<Message> get(String conversationId, int lastN) { List<String> list = redisTemplate.opsForList().range(PREFIX + conversationId, 0, lastN); if (list == null || list.isEmpty()) { return List.of(); } return list.stream().map(s -> { try { return objectMapper.readValue(s, Msg.class); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }).map(Msg::toMessage).toList(); } @Override public void clear(String conversationId) { redisTemplate.delete(PREFIX + conversationId); }}
-
同时,为了保证会话历史持久化,再定义一个RedisChatHistory类,用于实现会话历史持久化:
package com.shisan.ai.repository;import lombok.RequiredArgsConstructor;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import java.util.Collections;import java.util.List;import java.util.Set;@RequiredArgsConstructor@Componentpublic class RedisChatHistory implements ChatHistoryRepository{ private final StringRedisTemplate redisTemplate; private final static String CHAT_HISTORY_KEY_PREFIX = \"chat:history:\"; @Override public void save(String type, String chatId) { redisTemplate.opsForSet().add(CHAT_HISTORY_KEY_PREFIX + type, chatId); } @Override public List<String> getChatIds(String type) { Set<String> chatIds = redisTemplate.opsForSet().members(CHAT_HISTORY_KEY_PREFIX + type); if(chatIds == null || chatIds.isEmpty()) { return Collections.emptyList(); } return chatIds.stream().sorted(String::compareTo).toList(); }}
-
注意:
- 使用Redis方案时,需要将之前内存方案定义的ChatMemory、ChatHistoryRepository从Spring容器中移除;
- 由于使用的是Redis的Set结构,无序的,因此要确保chatId是单调递增的。
4.6.4 方案三:Cassandra
-
SpringAI官方提供了CassandraChatMemory,但是是跟CassandraVectorStore绑定的,不太灵活;
-
首先,需要安装一个Cassandra访问,使用Docker安装:
docker run -d --name cas -p 9042:9042 cassandra
-
在项目中添加cassandra依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-cassandra-store-spring-boot-starter</artifactId></dependency>
-
配置Cassandra地址:
spring: cassandra: contact-points: 192.168.150.101:9042 local-datacenter: datacenter1
-
基于Cassandra的ChatMemory已经实现了,其它不变。
-
**注意:**多种ChatMemory实现方案不能共存,只能选择其一。