Skip to content
DAILY QUOTE

“ ”

1.SpringAI入门

SpringAI整合了全球大多数大模型,而且对于大模型开发的三种技术架构都有比较好的封装和支持,开发起来非常方便。

不同模型能够接受的输入类型、输出类型不一定相同。SpringAI根据模型的输入和输出类型不同对模型进行了分类:

1.1. 快速入门

1.1.1.创建工程

创建一个新的SpringBoot工程,JDK版本是17:

原始pom.xml如下:

java
<?xml version="1.0" encoding="UTF-8"?>
<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.5.3</version>
       <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>spring-ai-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-ai-demo</name>
    <description>spring-ai-demo</description>
    <url/>
    <licenses>
       <license/>
    </licenses>
    <developers>
       <developer/>
    </developers>
    <scm>
       <connection/>
       <developerConnection/>
       <tag/>
       <url/>
    </scm>
    <properties>
       <java.version>17</java.version>
    </properties>
    <dependencies>
       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</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>

    <build>
       <plugins>
          <plugin>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
       </plugins>
    </build>

</project>

1.1.2.引入依赖

SpringAI完全失陪了SpringBoot的自动装配功能,而且给不同大模型提供了不同starter。

这里以Ollama为例。

首先,在项目pom.xml中添加spring-ai的版本信息

java
<spring-ai.version>1.0.0</spring-ai.version>

然后,添加spring-ai的依赖管理项:

java
<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-dpeepseek的依赖:

java
<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>

最后为了方便开发,在引入一个Lombok依赖:

java
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>

1.1.3.配置模型信息

以ollama为例,在application.yaml添加如下内容:

yaml
spring:  
  application:  
    name: heima-ai
  ai:  
  ollama:  
    base-url: http://localhost:11434  
    chat:  
      model: qwen2.5:3b

1.1.4.ChatClient

ChatClient中封装了与AI大模型对话的各种API,同时支持同步式或响应式交互。

不过在使用之前,我们需要先声明一个ChatClient

在config包下新建一个CommonConfiguration类:

完整代码如下:

java
@Configuration  
public class CommonConfiguration {  
    //参数中的model就是使用的模型
    @Bean  
    public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory){  
        return ChatClient  
                .builder(model) //创建ChatClient工厂
                .build();  //构建ChatClient实例
    }
}
  • ChatClient.builder:会得到一个ChatClient.Builder工厂对象,利用它可以自由选择模型、添加各种自定义配置。
  • OllamaChatModel:引入了ollama的starter,这里就可以自动注入模型对象。

1.1.5.同步调用

我们定义一个Controller,在其中接受用户发送的提示词,然后把提示词发送给大模型,交给大模型处理,拿到结果后返回。

代码如下:

java
@RequestMapping("/ai")  
@RestController  
@RequiredArgsConstructor  
public class ChatController {  
  
    private final ChatClient chatClient;  
  
    @RequestMapping("/chat")  
public String chat(@RequestParam(defaultValue = "你好~") String prompt) { 
    return chatClient  
            .prompt(prompt) //传入用户提示词  
            .call() //同步请求,会等待AI全部输出完才返回结果  
            .content(); //返回响应内容  
}
}

基于call()方法的调用属于同步调用,需要大模型返回所有响应结果后,才能返回给前端。需要等待较长时间。

启动项目,在浏览器中访问http://localhost:8080/ai/chat?prompt=你好

1.1.6.流式调用

同步调用需要等待很长时间页面才能看到结果,用户体验不好。为了解决这个问题,我们可以改进调用方式为流式调用。

在SpringAI中使用了WebFlux技术实现流式调用。

修改刚才ChatController中的chat方法:

java
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8") //告诉浏览器编码格式是utf-8 
public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt) {  
    return chatClient  
            .prompt(prompt)  
            .stream()  
            .content();  
}

1.1.7.System设定

如果我们希望AI按照我们给的设定工作,就需要给他设置System背景信息。

在SpringAI中,设置System信息非常方便,不需要再每次发送时封装到Message,而是创建ChatClient时指定即可。

我们修改SpringAIConfiguration中的代码,给ChatClient设定默认的System信息:

java
@Bean  
public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory){  
    return ChatClient  
            .builder(model)  
            .defaultSystem("你是可爱又迷人的反派角色,火箭队的武藏")  
            .build();  
}

1.2.推理模型

  • deepseek-reasoner:推理模型,目前版本是DeepSeek-R1-0528
  • deepseek-chat:普通模型,目前版本是DeepSeek-V3-0324

SpringAI中默认采用的是deepseek-chat模型,所以看不到推理过程。

1.2.1.修改模型

有两种办法可以修改模型:

  • 全局配置:在application.yml配置文件中修改模型,作用域全局
  • 局部配置:在ChatClient中修改模型,作用域局部

application.yml的配置刚才已经说过,接下来我们以ChatClient的局部配置为例来介绍。

修改SpringAIConfiguration中的ChatClient的配置:

Java
package com.itheima.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringAIConfiguration {

    @Bean
    public ChatClient chatClient(DeepSeekChatModel chatModel){
        return ChatClient.builder(chatModel)
                .defaultOptions(ChatOptions.builder().model("deepseek-reasoner").build())
                .defaultSystem("你是可爱又迷人的反派角色,火箭队的武藏。回答中可以用表情~")
                .build();
    }
}

1.2.2.输出推理过程

重启测试后,会发现输出的结果依然是没有思考推理过程。怎么回事?

这是因为DeepSeek返回的思考内容与普通内容不是在一起的,而是分为两个字段返回:

在SpringAI中,普通模型返回的结果都是AssistantMessage类型,而DeepSeek模型返回的结果是DeepSeekAssistantMessage类型,官方给出的解决方案是这样的:

Java
public void deepSeekReasonerExample() {
    DeepSeekChatOptions promptOptions = DeepSeekChatOptions.builder()
            .model(DeepSeekApi.ChatModel.DEEPSEEK_REASONER.getValue())
            .build();
    Prompt prompt = new Prompt("9.11 and 9.8, which is greater?", promptOptions);
    ChatResponse response = chatModel.call(prompt);

    // Get the CoT content generated by deepseek-reasoner, only available when using deepseek-reasoner model
    DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) response.getResult().getOutput();
    String reasoningContent = deepSeekAssistantMessage.getReasoningContent();
    String text = deepSeekAssistantMessage.getText();
}

所以,我们可以模仿这种方式。修改ChatController中的代码:

Java
package com.itheima.ai.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
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;

    @RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
    public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt) {
        return chatClient
                .prompt(prompt)
                .stream()
                .chatResponse()
                .mapNotNull(this::handleReasonerMessage); // 处理推理结果
    }

    private String handleReasonerMessage(ChatResponse response) {
        // 获取消息,转为DeepSeekAssistantMessage
        DeepSeekAssistantMessage message = (DeepSeekAssistantMessage) response.getResult().getOutput();
        // 获取推理结果
        String reasoningContent = message.getReasoningContent();
        if (reasoningContent != null && !reasoningContent.isBlank()) {
            // 如果推理结果存在,则将其包裹上<think></think>标签,方便前端处理
            return "<think>" + reasoningContent + "</think>";
        }
        // 没有推理结果,直接返回文本
        return message.getText();
    }
}

1.2.3.ollama推理输出

本地使用deepseek-r1:1.5b,使用.enableThinking()让模型开启思考过程,DeepSeek R1 本身支持推理,但 Spring AI 发请求时最好明确加上 think=true。不加的话,有些 Ollama 版本/模型配置可能只返回最终答案,不返回 thinking 字段。 具体代码如下:

java
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")  
public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt) {  
    return chatClient  
            .prompt(prompt)  
            .options(OllamaChatOptions.builder()  
                    .model("deepseek-r1:1.5b").enableThinking().build())  
            .stream()  
            .chatResponse() //获取全部chatresponse响应对象,而不是文本内容
            .flatMap(response->{  
                String thinking = response.getResult().getMetadata().get("thinking"); //Ollama 的推理内容不是放在正文里,而是放在响应的metadata里
                String answer = response.getResult().getOutput().getText();  
                if (thinking != null && !thinking.isBlank()) {  
                    return Flux.just("\n" + thinking);  
                } 
                if (answer != null && !answer.isBlank()) {  
                    return Flux.just(answer);  
                }  
                return Flux.empty();  
            });  
}

1.3.日志功能

1.3.1.Advisor

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

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

  • SimpleLoggerAdvisor:日志记录的Advisor
  • MessageChatMemoryAdvisor:会话记忆的Advisor
  • QuestionAnswerAdvisor:实现RAG的Advisor

当然,我们也可以自定义Advisor,具体可以参考: [SpringAI](Advisors API :: Spring AI Reference)

1.3.2.添加日志Advisor

首先需要修改SpringAIConfiguration,给ChatClient添加日志Advisor:

java
@Bean  
public ChatClient chatClient(OllamaChatModel chatModel){  
    return ChatClient.builder(chatModel)  
            .defaultOptions(OllamaChatOptions.builder()  
                    .model("deepseek-r1:1.5b").enableThinking().build())  
            .defaultSystem("你是可爱又迷人的反派角色,火箭队的武藏。回答中可以用表情~")  
            .defaultAdvisors(  
                    SimpleLoggerAdvisor.builder().build() // 添加日志记录advisor  
            )  
            .build();  
}

1.3.3.修改日志级别

接下来,再application.yaml中添加日志配置,更新日志级别:

java
logging:  
  level:  
    org.springframework.ai: debug #AI对话的日志级别  
    com.example.ai: debug #本项目的日志级别

重启项目,再次聊天就能看到AI对话的日志信息了。

1.4.对接前端

Nginx运行

Bash
# 启动Nginx
start nginx.exe
# 停止
nginx.exe -s stop

启动后,访问http://localhost:5173即可看到页面:

1.5.会话记忆

大语言模型是无状态的,意味着他们不会保留以前交互的信息。如果想让大模型知道之前聊了什么,就需要在每次与大模型交互时携带会话的历史信息,也就是会话的上下文。

我们可以把用户与LLM所有会话历史保存下来,不过LLM的上下文是有限制的,因此,当历史会话过多时,没有办法把所有历史回话都拼接到上下文中,也就是说LLM的记忆会受限。

两个概念上的差异:

  • 会话历史:会话完整记录,包含用户与LLM之间交互的所有消息。
  • 会话记忆:每次会话时携带在上下文中的部分信息。用于让LLM感知聊天的历史。

SpringAI只提供了会话记忆功能(并非会话历史),我们只需要简单配置就能使用了。包含两部分:

  • ChatMemory:会话记忆管理,管理会话上下文。
  • ChatMemoryRepository:会话记忆存储管理,实现会话记忆的读写操作。

注意: ChatMemory 负责记忆管理和裁剪规则。 ChatMemoryRepository 负责把裁剪后的记忆存起来。

1.5.1.ChatMemory

ChatMemory负责管理会话记忆,也就是决定会话历史中的哪一部分作为会话记忆。其接口声明如下:

java
package org.springframework.ai.chat.memory;  
  
import java.util.List;  
import org.springframework.ai.chat.messages.Message;  
import org.springframework.util.Assert;  
  
public interface ChatMemory {  
    String DEFAULT_CONVERSATION_ID = "default";  
    String CONVERSATION_ID = "chat_memory_conversation_id";  
  
    default void add(String conversationId, Message message) {  
        Assert.hasText(conversationId, "conversationId cannot be null or empty");  
        Assert.notNull(message, "message cannot be null");  
        this.add(conversationId, List.of(message));  
    }  
  
    void add(String conversationId, List<Message> messages);  
  
    List<Message> get(String conversationId);  
	
    void clear(String conversationId);  
}

所有的会话记忆都是与conversationId有关联的,也就是会话Id,将来不同会话id的记忆自然是分开管理的。

ChatMemory有一个默认的实现:MessageWindowChatMemory,即固定窗口大小的会话记忆。它会设定一个会话记忆的窗口,并设定该窗口允许的最大值。当消息数超过最大值时,将删除较旧的消息,保留新消息。默认窗口大小为 20。

ChatMemory只负责管理会话记忆,而不是读写记忆。真正读写会话记忆还要靠ChatMemoryRepository来实现。

1.5.2.ChatMemoryRepository

ChatMemoryRepository是SpringAI提供的会话记忆存储接口,强调一下,这个不是会话历史。因为它每次保存会话都会删除旧的会话。

ChatMemoryRepository有很多种实现方式,也就是说你可以用不同的方式来存储会话记忆。例如:

  • InMemoryChatMemoryRepository:基于内存存储,底层是ConcurrentHashMap,默认方案
  • JdbcChatMemoryRepository:基于JDBC在关系数据库中存储,支持多种数据库
  • CassandraChatMemoryRepository:基于Apache Cassandra 存储消息。

默认方案是InMemoryChatMemoryRepository,也就是把会话记忆存储在内存中,

JdbcChatMemoryRepository为例,分为四步:

  1. 引入依赖

依赖如下:

java
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>
  1. 准备SQL脚本
SQL
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
     `id` BIGINT(19) NOT NULL AUTO_INCREMENT,
     `conversation_id` VARCHAR(36) NOT NULL COLLATE 'utf8mb4_general_ci',
     `content` TEXT NOT NULL COLLATE 'utf8mb4_general_ci',
     `type` VARCHAR(10) NOT NULL COLLATE 'utf8mb4_general_ci',
     `timestamp` TIMESTAMP NOT NULL,
     PRIMARY KEY (`id`) USING BTREE,
     INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`) USING BTREE,
     CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
);

需要将脚本放在项目的resource目录中,例如:

  1. 配置

application.yml中添加相关配置:

yaml
spring:
  application:
    name: spring-ai-demo
  ai:
    deepseek:
      api-key: ${DEEPSEEK_API_KEY} # 获取 DeepSeek API Key
      chat:
        options:
          model: deepseek-chat # 模型名称,默认为 deepseek-chat,可以不配
    chat:
      memory:
        repository:
          jdbc:
            initialize-schema: always # 自动建表
            schema: classpath:sql/schema-mysql.sql # 建表脚本
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-ai-demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&useSSL=false
    username: root
    password: xxx
logging:
  level:
    com.itheima: debug
    org.springframework.ai: debug
  1. 使用 最后,只需要在自定义ChatMemory时配置即可。修改CommonConfiguration,添加配置:
java
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) {
    return MessageWindowChatMemory.builder()
            .chatMemoryRepository(chatMemoryRepository)
            .maxMessages(20)
            .build();
}

1.5.3.添加会话记忆Advisor

有了ChatMemory之后,会话记忆就可以交给Spring管理了。Spring底层还是通过AOP方式来实现的,通过MessageChatMemoryAdvisor拦截请求,把消息写入ChatMemory

所以,还需要在ChatClient中配置MessageChatMemoryAdvisor

然后添加MessageChatMemoryAdvisorChatClient

java
@Bean  
public ChatClient chatClient(OllamaChatModel chatModel, ChatMemory chatMemory){  
    return ChatClient.builder(chatModel)  
            .defaultOptions(OllamaChatOptions.builder()  
                    .model("deepseek-r1:1.5b").enableThinking().build())  
            .defaultSystem("你是可爱又迷人的反派角色,火箭队的武藏。回答中可以用表情~")  
            .defaultAdvisors(  
                    SimpleLoggerAdvisor.builder().build(), // 添加日志记录advisor  
                    MessageChatMemoryAdvisor.builder(chatMemory).build() //会话记忆advisor  
            )  
            .build();  
}

1.5.4.添加会话id

ChatMemory的会话记忆管理是基于conversationId的,用conversationId来区分不同的会话。

所以,为了区分不同的会话,我们还需要再发送请求时携带会话id:

java
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")  
public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt,  
                         @RequestParam("chatId")String chatId) {  
    return chatClient  
            .prompt(prompt)  
            .advisors(as->as.param(ChatMemory.CONVERSATION_ID,chatId))  
            .stream()  
            .chatResponse()  
            .flatMap(response->{  
                String thinking = response.getResult().getMetadata().get("thinking");  
                String answer = response.getResult().getOutput().getText();  
                if (thinking != null && !thinking.isBlank()) {  
                    return Flux.just("\n" + thinking);  
                }  
                if (answer != null && !answer.isBlank()) {  
                    return Flux.just(answer);  
                }  
                return Flux.empty();  
            });  
}

1.5.5. ChatMemoryRepository分类

Spring AI 里常见的 ChatMemoryRepository 可以按存储方式分成三类:内存版、JDBC 版、Cassandra 版。

1.内存版 InMemoryChatMemoryRepository:数据存在 Java 后端进程内存里。

  • 特点:
    • 不需要数据库
    • 使用最简单
    • 后端一重启,记忆全部丢失
    • 不能多实例共享
    • 适合学习、测试、Demo
  • 典型场景:
    • 本地练习 Spring AI
    • 临时对话上下文
    • 不关心服务重启后的记忆
  • 生命周期: 后端进程还活着+conversationId相同 = 能继续记住 后端重启 = 全部清空

2.JDBC 版 JdbcChatMemoryRepository:数据存在关系型数据库里,比如 MySQL、PostgreSQL。

  • 特点:
    • 需要配置 DataSource
    • 需要数据库表
    • 后端重启后记忆还在
    • 多个后端实例可以共享同一个数据库
    • 适合普通业务项目 一般需要建表配置:
java
  spring:
    ai:
      chat:
        memory:
          repository:
            jdbc:
              initialize-schema: always
              schema: classpath:sql/schema-mysql.sql
  • 典型场景:

    • 单体项目或普通 Web 项目
    • 希望重启后还能保留上下文记忆
    • 用 MySQL/PostgreSQL 保存记忆
  • 注意: JDBC 保存的是 ChatMemory 记忆,不等于完整聊天历史。 如果设置了窗口大小,旧消息仍可能被裁剪。

    3.Cassandra 版 CassandraChatMemoryRepository:数据存在 Cassandra 这种分布式 NoSQL 数据库里。

  • 特点:

    • 适合大规模、高并发、分布式场景
    • 可以横向扩展
    • 写入能力强
    • 部署和维护成本高
    • 普通项目一般用不上
  • 典型场景:

    • 大规模 AI 聊天系统
    • 多节点部署
    • 海量 conversationId
    • 需要高可用和分布式存储
  • 不适合:

    • 本地学习
    • 小型项目
    • 单机应用
    • 普通管理系统

2.会话管理

这里有几个概念我们要区分清楚:

  • 会话记录:用户有几次会话,每次会话是什么,包含:

    • 会话id:也就是conversationId

    • 创建时间:会话创建的时间

    • 标题:可以根据会话内容让AI提取出标题

    • 所属用户:如果存在多用户的话,可以加上用户id信息

    • ... :其它业务相关字段

  • 会话历史:每次会话完整历史记录,包含用户与LLM之间交互的所有消息。有两类:

    • userMessage:用户提问的消息

    • assistantMessage:AI返回的消息

  • 会话记忆:每次会话时携带在上下文中的部分信息。用于让LLM回忆之前聊天内容。

需要注意的是,在SpringAI中是没有会话历史(ChatHistory)的,只有会话记忆(ChatMemory)。

会话记忆是会话历史的一部分,存在以下问题:

  • 默认只保留最近20条消息,旧消息会被清除

  • 会话记忆中不保留推理模型的推理内容

如果想做到“完整聊天记录”,要自己设计数据库和业务接口,同时不能把ChatMemoryRepository当历史记录来用,因为它的职责只是给ChatMemory存消息。

  • ChatMemory:会话记忆,在其中管理会话记忆,但要改进实现,存储时不再只存20条,而是全部存储

  • ChatMemoryRepository:会话记忆的存储,我们可以存储到MySQL、MongoDB等任何地方,但是要改为增量存储,而不是覆盖旧消息。

2.1.会话记录管理

我们需要创建数据库表记录会话id等信息,并提供查询用户会话记录、删除记录等功能。

2.1.1.创建表

每次会话都有自己的唯一标识,也就是会话id(conversationId)。

会话不仅仅有id信息,在某些业务中,会话还会跟用户有关联,还跟业务有关联,所以要记录的信息就比较多:

  • chatId:会话id

  • title:标题

  • userId:本次会话关联的用户

  • type:业务类型,我们后续的案例有3个需要用到会话记忆,所以这里可以是:chat、service、pdf

    • chat:多模态聊天机器人

    • service:黑马智能客服

    • pdf:个人知识库chatPdf

  • createTime:会话创建时间

因此我们要创建一个来表示会话记录:

SQL
CREATE TABLE `spring_ai_chat_record` (
        `id` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '会话id' COLLATE 'utf8mb4_general_ci',
        `title` VARCHAR(150) NULL DEFAULT '' COLLATE 'utf8mb4_general_ci',
        `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户id',
        `type` VARCHAR(50) NOT NULL DEFAULT '0' COMMENT 'chat:聊天机器人;service:智能客服;pdf:个人知识库' COLLATE 'utf8mb4_general_ci',
        `create_time` TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '会话创建时间',
        PRIMARY KEY (`id`) USING BTREE,
        INDEX `create_time` (`create_time`) USING BTREE
)
COMMENT='会话历史记录'
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB
;

接着引入MyBatisPlus的依赖:

XML
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.10.1</version>
</dependency>

2.1.2.创建实体类

java
@Data 
@EqualsAndHashCode(callSuper = false) 
@Accessors(chain = true) //返回自己,方便链式调用
@TableName("spring_ai_chat_record")
public class SpringAiChatRecord implements Serializable {  
    private static final long serialVersionUID = 1L;  
  
    /**  
     * 会话id  
     */    @TableId(value = "id", type = IdType.INPUT)  
    private String id;  
  
    /**  
     * 会话标题  
     */  
    private String title;  
  
    /**  
     * 用户id  
     */    private Long userId;  
  
    /**  
     * chat:聊天机器人;service:智能客服;pdf:个人知识库  
     */  
    private String type;  
  
    /**  
     * 会话创建时间  
     */  
    private LocalDateTime createTime;  
}

2.1.3.编写mapper

java
public interface SpringAiChatRecordMappe extends BaseMapper<SpringAiChatRecord> {  
    @Select("SELECT id FROM spring_ai_chat_record WHERE type = #{type} and user_id = #{userId} ORDER BY create_time DESC")  
    List<String> findConversationIds(@Param("type") String type, @Param("userId") Long userId);  
}

2.1.4.编写service

java
@Service  
public class SpringAiChatRecordServiceImpl extends ServiceImpl<SpringAiChatRecordMapper, SpringAiChatRecord> implements ISpringAiChatRecordService {  
    @Override  
    public void saveRecord(String type, String conversionId) {  
        //1.判断记录是否存在  
        Long count=this.lambdaQuery()  
                .eq(SpringAiChatRecord::getId,conversionId)  
                .count();  
        if(count!=null&&count>0){  
            //记录已存在,结束  
            return;  
        }  
        //2.保存记录  
        SpringAiChatRecord record=new SpringAiChatRecord();  
        record.setType(type);  
        record.setId(conversionId);  
        //TODO userId暂时写死,后续从sessionId获取  
        record.setUserId(1L);  
        //TODO 会话标题暂时用会话id,后续可以根据内容生成  
        record.setTitle(conversionId);  
        record.setCreateTime(LocalDateTime.now());  
        save(record);  
    }  
  
    @Override  
    public List<String> findConversationIds(String type) {  
        //TODO  
        return this.getBaseMapper().findConversationIds(type,1L);  
    }  
}

2.1.5.保存记录

修改ChatController中的逻辑,在对话时保存会话记录:

java
    private final ChatClient chatClient;  
    private final ISpringAiChatRecordService recordService;  
  
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")  
public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt,  
                         @RequestParam("chatId")String chatId) {  
    //保存会话记录  
    recordService.saveRecord("chat",chatId);  
  
    return chatClient  
            .prompt(prompt)  
            .advisors(as->as.param(ChatMemory.CONVERSATION_ID,chatId))  
            .stream()  
            .chatResponse()  
            .flatMap(response->{  
                String thinking = response.getResult().getMetadata().get("thinking");  
                String answer = response.getResult().getOutput().getText();  
                if (thinking != null && !thinking.isBlank()) {  
                    return Flux.just("\n" + thinking);  
                }  
                if (answer != null && !answer.isBlank()) {  
                    return Flux.just(answer);  
                }  
                return Flux.empty();  
            });  
}

2.2.会话历史

会话历史的查询。包含两个接口:

  • 根据业务类型查询会话历史列表(我们将来有3个不同业务,需要分别记录历史。大家的业务可能是按userId记录,根据UserId查询)

  • 根据chatId查询指定会话的历史消息

2.2.1.消息VO

其中,查询会话历史消息,也就是Message集合。但是由于Message并不符合页面的需要,我们需要自己定义一个VO.

定义一个com.itheima.ai.entity.vo包,在其中定义一个MessageVO类:

java
@NoArgsConstructor  
@Data  
public class MessageVO {  
    private String role;  
    private String content;  
  
    public MessageVO(Message message){  
        switch (message.getMessageType()){  
            case USER:  
                role="user";  
                break;  
            case ASSISTANT:  
                role="assistant";  
                break;  
            default:  
                role="";  
                break;  
        }  
        this.content=message.getText();  
    }  
}

2.2.2.会话历史接口

新建一个ChatHistoryController

java
@RestController  
@RequestMapping("/ai/history")  
@RequiredArgsConstructor  
public class ChatHistoryController {  
  
    private final ChatHistoryRepository chatHistoryRepository;  
    private final ISpringAiChatRecordService recordService;  
    private final ChatMemoryRepository chatMemoryRepository;  
  
    @GetMapping("/{type}")  
    public List<String> getChatIds(@PathVariable("type") String type){  
        return recordService.findConversationIds(type);  
    }  
  
    @GetMapping("/{type}/{chatId}")  
    public List<MessageVO> getChatHistory(@PathVariable("type")String type,@PathVariable("chatId")String chatId){  
          
        return chatMemoryRepository.findByConversationId(chatId).stream().map(MessageVO::new).toList();  
    }  
}

重启服务,现在AI聊天机器人就具备会话记忆和会话历史功能了。