> 技术文档 > SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发


4 SpringAI入门:对话机器人

4.1 快速入门

4.1.1 创建工程

  • 创建一个新的SpringBoot工程,勾选Web、MySQL驱动、Ollama:

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

  • 工程结构:

    • application.properties改成application.yaml
    • 后面将chatrobot文件夹重命名为ai

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

  • 初始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类:

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

  • 代码:

    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,在其中接收用户发送的提示词,然后把提示词发送给大模型,交给大模型处理,拿到结果后返回;

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

  • 代码:

    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=你好

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

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();}
  • 重启测试,再次访问:

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

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实例}
  • 再次询问“你是谁?”

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

    • 以上的回答由DeepSeek的深度思考内+最终回答组成。

4.2 日志功能

  • 默认情况下,应用于AI的交互时不记录日志的,我们无法得知SpringAI组织的提示词到底长什么样,这样不方便我们调试。

4.2.1 Advisor

  • SpringAI基于AOP机制实现与大模型对话过程的增强、拦截、修改等功能,所有的增强通知都需要实现Advisor接口;

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

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

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

    • 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对话的日志信息了;

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

4.3 对接前端

  • 有两个前端的源代码:

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

4.3.1 npm运行

  • 进入spring-ai-protal文件夹(该文件夹要放在非中文目录下),然后执行cmd命令:

    # 安装依赖npm install# 运行程序npm run dev
  • 启动后,访问http://localhost:5173即可看到页面:

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

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即可看到页面:

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

  • 点击第一个卡片“AI聊天”进入对话机器人页面:

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

4.4 会话记忆功能

  • 目前的AI聊天机器人是没有记忆功能的;

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

  • 前面讲过,让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();}
  • 然后添加MessageChatMemoryAdvisorChatClient

    @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为例,页面上的会话历史:

    SpringAI+DeepSeek大模型应用开发——4 SpringAI入门:对话机器人_spring ai chatbot开发

  • 在ChatMemory中,会记录一个会话中的所有消息,记录方式是以conversationId为key,以List为value,根据这些历史消息,大模型就能继续回答问题,这就是所谓的会话记忆

  • 会话历史,就是每一个会话的conversationId,将来根据conversationId再去查询List

    • 比如上图中,有3个不同的会话历史,就会有3个conversationId,管理会话历史,就是记住这些conversationId,当需要的时候查询出conversationId的列表;
  • 注意,在接下来业务中,以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实现方案不能共存,只能选择其一。