告别 AI 对话 “失忆”!Spring AI 聊天记忆底层原理与全场景落地实战

简介: Spring AI提供优雅的聊天记忆解决方案,彻底解决大模型“失忆”痛点。其分层架构支持内存/MySQL等多存储,通过ChatMemory、ChatMemoryRepository和ChatMemoryAdvisor三大组件,实现会话隔离、消息有序、窗口可控,开箱即用,低侵入、高扩展。

很多开发者在集成AI对话功能时,都会遇到一个核心痛点:多轮对话中AI完全无法记住之前的沟通内容,每次对话都像首次交互,用户体验极差。而Spring AI作为Spring官方推出的AI应用开发框架,提供了一套完整、低侵入、高扩展的聊天记忆解决方案,彻底解决了大模型对话的无状态问题。

一、大模型对话的无状态本质与聊天记忆的核心逻辑

1.1 大模型为什么会“失忆”

大语言模型(LLM)的核心能力是基于输入的文本序列进行概率推理,生成符合语义的下一个token。它本身没有内置的持久化存储能力,每一次API调用都是完全独立的无状态请求,模型不会自动保存、读取上一次对话的任何内容。 举个通俗的例子:你第一次问“Java的三大特性是什么?”,AI给出了封装、继承、多态的回答;当你第二次问“那第一个特性的核心设计思想是什么?”,如果没有记忆机制,模型完全无法感知“第一个特性”对应的上下文,只能给出无意义的泛化回答。

1.2 聊天记忆的本质与核心三要素

聊天记忆的本质,是对用户与AI的历史对话消息进行有序管理、持久化存储、按需调度,并在每次对话请求时,将符合规则的历史内容注入到当前请求的Prompt中,让模型能够感知完整的上下文,实现连贯的多轮对话。 一套合格的聊天记忆体系,必须满足三个核心要素:

  1. 消息有序性:严格区分消息类型,保证用户提问与AI回复的时间顺序与成对关系,避免上下文错乱
  2. 会话隔离性:不同用户、不同会话的记忆数据完全隔离,杜绝数据串扰与越权访问
  3. 窗口可控性:可灵活控制历史消息的长度,避免内容过多导致模型token超限,保障请求稳定性与成本可控

二、Spring AI 聊天记忆的核心架构与组件

Spring AI的聊天记忆体系基于Spring的原生设计理念,采用分层架构,解耦了消息管理、持久化、请求拦截三大核心能力,具备极高的扩展性。

2.1 核心架构图

2.2 核心组件详解

Spring AI的聊天记忆能力围绕三个核心顶级接口展开,配合消息模型与拦截器机制,构成了完整的能力闭环。

2.2.1 Message 消息体系

Spring AI中所有对话内容都被封装为Message接口的实现类,核心实现分为三类,是记忆体系的最小数据单元:

  • SystemMessage:系统提示词,用于给模型设定角色、行为规则、输出边界,通常放在对话序列的最开头,单个会话一般仅保留一个
  • UserMessage:用户输入的提问内容,对应对话中的用户侧消息
  • AssistantMessage:AI模型生成的回复内容,对应对话中的AI侧消息

历史对话序列由UserMessageAssistantMessage成对组成,严格按照交互时间排序,是模型感知上下文的核心依据。

2.2.2 ChatMemory 会话记忆接口

ChatMemory是会话级记忆的核心接口,负责管理单个会话的全量消息,是记忆体系的内存操作入口,核心方法包括:

  • add(Message... messages):向当前会话追加消息
  • getMessages():获取当前会话的全量有序消息
  • clear():清空当前会话的所有消息
  • getConversationId():获取当前会话的唯一标识,用于会话隔离

Spring AI提供了默认的InMemoryChatMemory实现,基于ConcurrentHashMap实现内存级的消息管理,适合单机测试与低并发场景。

2.2.3 ChatMemoryRepository 持久化仓库接口

ChatMemoryRepository是持久化层的核心抽象接口,负责全量会话记忆的加载与持久化,是实现自定义存储的核心扩展点,核心方法包括:

  • getMessages(String conversationId):根据会话ID加载对应的历史消息序列
  • addMessage(String conversationId, Message message):向指定会话追加单条消息
  • addMessages(String conversationId, List<Message> messages):向指定会话批量追加消息
  • clear(String conversationId):清空指定会话的所有消息
  • listConversationIds():获取全量会话ID列表

该接口完全解耦了记忆逻辑与存储介质,开发者可以通过实现该接口,对接MySQL、Redis、MongoDB等任意存储系统,实现生产级的持久化能力。

2.2.4 ChatMemoryAdvisor 拦截器

ChatMemoryAdvisor是Spring AI记忆机制的核心调度器,基于Spring的AOP设计理念,实现了记忆能力与对话流程的无侵入集成:

  • 前置拦截:在对话请求发送给大模型之前,自动从ChatMemoryRepository中加载对应会话的历史消息,注入到当前请求的Prompt中
  • 后置拦截:在收到大模型的回复后,自动将当前用户消息与AI回复追加到ChatMemoryRepository中,完成记忆的持久化

开发者无需手动处理历史消息的拼接、存储,仅需通过配置即可实现完整的记忆能力,极大降低了开发成本。

三、环境搭建与基础依赖

3.1 项目基础环境

项目基于JDK 17开发,采用Maven进行项目管理,基础环境如下:

  • Spring Boot 3.4.5
  • Spring AI 1.2.0
  • MyBatis-Plus 3.5.7
  • MySQL 8.0
  • Lombok 1.18.30
  • FastJSON2 2.0.52
  • Guava 33.2.1-jre
  • SpringDoc OpenAPI 2.6.0

3.2 Maven 核心依赖配置

<?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.4.5</version>
       <relativePath/>
   </parent>
   <groupId>com.jam</groupId>
   <artifactId>spring-ai-chat-memory-demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>spring-ai-chat-memory-demo</name>
   <description>Spring AI Chat Memory Demo</description>
   
   <properties>
       <java.version>17</java.version>
       <spring-ai.version>1.2.0</spring-ai.version>
       <mybatis-plus.version>3.5.7</mybatis-plus.version>
       <fastjson2.version>2.0.52</fastjson2.version>
       <guava.version>33.2.1-jre</guava.version>
       <springdoc.version>2.6.0</springdoc.version>
   </properties>
   
   <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>
   
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.ai</groupId>
           <artifactId>spring-ai-starter-openai</artifactId>
       </dependency>
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
           <version>${mybatis-plus.version}</version>
       </dependency>
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <scope>runtime</scope>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>1.18.30</version>
           <scope>provided</scope>
       </dependency>
       <dependency>
           <groupId>com.alibaba.fastjson2</groupId>
           <artifactId>fastjson2</artifactId>
           <version>${fastjson2.version}</version>
       </dependency>
       <dependency>
           <groupId>com.google.guava</groupId>
           <artifactId>guava</artifactId>
           <version>${guava.version}</version>
       </dependency>
       <dependency>
           <groupId>org.springdoc</groupId>
           <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
           <version>${springdoc.version}</version>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-validation</artifactId>
       </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>
               <configuration>
                   <excludes>
                       <exclude>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                       </exclude>
                   </excludes>
               </configuration>
           </plugin>
       </plugins>
   </build>
</project>

3.3 应用配置文件

spring:
 application:
   name: spring-ai-chat-memory-demo
 datasource:
   driver-class-name: com.mysql.cj.jdbc.Driver
   url: jdbc:mysql://127.0.0.1:3306/ai_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
   username: root
   password: your_password
 ai:
   openai:
     api-key: your_api_key
     base-url: your_base_url
     chat:
       options:
         model: gpt-3.5-turbo
         temperature: 0.7
         max-tokens: 2048
mybatis-plus:
 configuration:
   map-underscore-to-camel-case: true
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 global-config:
   db-config:
     logic-delete-field: isDeleted
     logic-delete-value: 1
     logic-not-delete-value: 0
springdoc:
 api-docs:
   enabled: true
 swagger-ui:
   enabled: true
   path: /swagger-ui.html
server:
 port: 8080

四、快速上手:内存级聊天记忆实现

基于Spring AI内置的InMemoryChatMemory,可以快速实现内存级的聊天记忆能力,无需额外的存储依赖,适合本地测试与功能验证。

4.1 ChatClient 配置类

package com.jam.demo.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.ChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* ChatClient配置类
* @author ken
*/

@Configuration
public class ChatClientConfig {

   /**
    * 配置内存级会话记忆
    * @return 会话记忆实例
    */

   @Bean
   public ChatMemory inMemoryChatMemory() {
       return new InMemoryChatMemory();
   }

   /**
    * 配置带记忆能力的ChatClient
    * @param builder ChatClient构建器
    * @param chatMemory 会话记忆实例
    * @return 带记忆能力的ChatClient实例
    */

   @Bean
   public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
       return builder
               .defaultAdvisors(ChatMemoryAdvisor.builder(chatMemory).build())
               .build();
   }
}

4.2 对话服务层实现

package com.jam.demo.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
* 内存级对话服务实现
* @author ken
*/

@Slf4j
@Service
@RequiredArgsConstructor
public class InMemoryChatService {

   private final ChatClient chatClient;

   /**
    * 带记忆的对话
    * @param conversationId 会话唯一标识
    * @param content 用户提问内容
    * @return AI回复内容
    */

   public String chatWithMemory(String conversationId, String content) {
       if (!StringUtils.hasText(conversationId)) {
           throw new IllegalArgumentException("会话ID不能为空");
       }
       if (!StringUtils.hasText(content)) {
           throw new IllegalArgumentException("提问内容不能为空");
       }

       return chatClient.prompt()
               .user(content)
               .conversationId(conversationId)
               .call()
               .content();
   }

   /**
    * 清空指定会话的记忆
    * @param conversationId 会话唯一标识
    */

   public void clearChatMemory(String conversationId) {
       if (!StringUtils.hasText(conversationId)) {
           throw new IllegalArgumentException("会话ID不能为空");
       }
       chatClient.prompt()
               .conversationId(conversationId)
               .call()
               .chatMemory()
               .clear();
       log.info("会话{}的记忆已清空", conversationId);
   }
}

4.3 对话接口实现

package com.jam.demo.controller;

import com.jam.demo.service.InMemoryChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
* 内存级对话接口
* @author ken
*/

@RestController
@RequestMapping("/api/chat/in-memory")
@RequiredArgsConstructor
@Tag(name = "内存级对话接口", description = "基于内存的带记忆对话能力")
public class InMemoryChatController {

   private final InMemoryChatService inMemoryChatService;

   /**
    * 带记忆的对话接口
    */

   @PostMapping("/send")
   @Operation(summary = "发送带记忆的对话请求", description = "同一个conversationId的请求会共享上下文记忆")
   public ResponseEntity<Map<String, String>> chat(
           @Parameter(description = "会话唯一标识", required = true) @RequestParam String conversationId,
           @Parameter(description = "用户提问内容", required = true) @RequestParam String content) {
       String result = inMemoryChatService.chatWithMemory(conversationId, content);
       return ResponseEntity.ok(Map.of("content", result));
   }

   /**
    * 清空会话记忆接口
    */

   @DeleteMapping("/clear")
   @Operation(summary = "清空指定会话的记忆", description = "清空后该会话的历史上下文将丢失")
   public ResponseEntity<Void> clearMemory(
           @Parameter(description = "会话唯一标识", required = true)
@RequestParam String conversationId) {
       inMemoryChatService.clearChatMemory(conversationId);
       return ResponseEntity.noContent().build();
   }
}

4.4 内存级实现的优缺点

  • 优点:集成简单、无额外存储依赖、请求延迟低、适合本地功能验证
  • 缺点:应用重启后记忆数据完全丢失、无法在分布式环境中共享会话、内存占用会随会话数量持续增长,仅适合测试场景,不可用于生产环境

五、生产级落地:基于MySQL的持久化聊天记忆实现

生产环境中,我们需要解决内存级实现的核心缺陷,通过实现Spring AI的ChatMemoryRepository接口,对接MySQL数据库,实现记忆数据的持久化,支持分布式环境部署,保障数据不丢失。

5.1 数据库表结构设计

CREATE TABLE `ai_chat_memory` (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `conversation_id` varchar(64) NOT NULL COMMENT '会话唯一标识',
 `message_type` varchar(16) NOT NULL COMMENT '消息类型:SYSTEM/USER/ASSISTANT',
 `content` text NOT NULL COMMENT '消息内容',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标识:0-未删除,1-已删除',
 PRIMARY KEY (`id`),
 UNIQUE KEY `uk_conversation_id_create_time` (`conversation_id`,`create_time`),
 KEY `idx_conversation_id` (`conversation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI对话记忆表';

5.2 实体类定义

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDateTime;

/**
* 对话记忆实体类
* @author ken
*/

@Data
@TableName("ai_chat_memory")
@Schema(description = "对话记忆实体")
public class ChatMemoryEntity {

   @Schema(description = "主键ID")
   @TableId(type = IdType.AUTO)
   private Long id;

   @Schema(description = "会话唯一标识")
   @TableField("conversation_id")
   private String conversationId;

   @Schema(description = "消息类型:SYSTEM/USER/ASSISTANT")
   @TableField("message_type")
   private String messageType;

   @Schema(description = "消息内容")
   @TableField("content")
   private String content;

   @Schema(description = "创建时间")
   @TableField(value = "create_time", fill = FieldFill.INSERT)
   private LocalDateTime createTime;

   @Schema(description = "更新时间")
   @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
   private LocalDateTime updateTime;

   @Schema(description = "逻辑删除标识")
   @TableField("is_deleted")
   @TableLogic
   private Integer isDeleted;
}

5.3 Mapper接口定义

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.ChatMemoryEntity;
import org.apache.ibatis.annotations.Mapper;

/**
* 对话记忆Mapper接口
* @author ken
*/

@Mapper
public interface ChatMemoryMapper extends BaseMapper<ChatMemoryEntity> {
}

5.4 自定义持久化ChatMemoryRepository实现

package com.jam.demo.repository;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import com.jam.demo.entity.ChatMemoryEntity;
import com.jam.demo.mapper.ChatMemoryMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.List;

/**
* 基于MySQL的对话记忆持久化仓库实现
* @author ken
*/

@Slf4j
@Repository
@RequiredArgsConstructor
public class JdbcChatMemoryRepository implements ChatMemoryRepository {

   private final ChatMemoryMapper chatMemoryMapper;
   private final TransactionTemplate transactionTemplate;

   @Override
   public List<Message> getMessages(String conversationId) {
       if (!StringUtils.hasText(conversationId)) {
           return Lists.newArrayList();
       }

       List<ChatMemoryEntity> entityList = chatMemoryMapper.selectList(
               new LambdaQueryWrapper<ChatMemoryEntity>()
                       .eq(ChatMemoryEntity::getConversationId, conversationId)
                       .orderByAsc(ChatMemoryEntity::getCreateTime)
       );

       if (CollectionUtils.isEmpty(entityList)) {
           return Lists.newArrayList();
       }

       List<Message> messageList = Lists.newArrayList();
       for (ChatMemoryEntity entity : entityList) {
           MessageType messageType = MessageType.valueOf(entity.getMessageType());
           switch (messageType) {
               case SYSTEM -> messageList.add(new SystemMessage(entity.getContent()));
               case USER -> messageList.add(new UserMessage(entity.getContent()));
               case ASSISTANT -> messageList.add(new AssistantMessage(entity.getContent()));
               default -> log.warn("不支持的消息类型:{}", messageType);
           }
       }
       return messageList;
   }

   @Override
   public void addMessage(String conversationId, Message message) {
       if (!StringUtils.hasText(conversationId) || ObjectUtils.isEmpty(message)) {
           return;
       }

       ChatMemoryEntity entity = buildChatMemoryEntity(conversationId, message);
       chatMemoryMapper.insert(entity);
       log.debug("会话{}新增消息成功,消息类型:{}", conversationId, message.getMessageType());
   }

   @Override
   public void addMessages(String conversationId, List<Message> messages) {
       if (!StringUtils.hasText(conversationId) || CollectionUtils.isEmpty(messages)) {
           return;
       }

       transactionTemplate.execute(status -> {
           List<ChatMemoryEntity> entityList = Lists.newArrayList();
           for (Message message : messages) {
               entityList.add(buildChatMemoryEntity(conversationId, message));
           }
           for (ChatMemoryEntity entity : entityList) {
               chatMemoryMapper.insert(entity);
           }
           return Boolean.TRUE;
       });
       log.debug("会话{}批量新增{}条消息成功", conversationId, messages.size());
   }

   @Override
   public void clear(String conversationId) {
       if (!StringUtils.hasText(conversationId)) {
           return;
       }

       chatMemoryMapper.delete(
               new LambdaQueryWrapper<ChatMemoryEntity>()
                       .eq(ChatMemoryEntity::getConversationId, conversationId)
       );
       log.info("会话{}的记忆已清空", conversationId);
   }

   @Override
   public List<String> listConversationIds() {
       List<ChatMemoryEntity> entityList = chatMemoryMapper.selectList(
               new LambdaQueryWrapper<ChatMemoryEntity>()
                       .select(ChatMemoryEntity::getConversationId)
                       .distinct()
       );

       return entityList.stream()
               .map(ChatMemoryEntity::getConversationId)
               .toList();
   }

   /**
    * 构建对话记忆实体
    * @param conversationId 会话ID
    * @param message 消息对象
    * @return 对话记忆实体
    */

   private ChatMemoryEntity buildChatMemoryEntity(String conversationId, Message message) {
       ChatMemoryEntity entity = new ChatMemoryEntity();
       entity.setConversationId(conversationId);
       entity.setMessageType(message.getMessageType().name());
       entity.setContent(message.getText());
       return entity;
   }
}

5.5 持久化ChatClient配置

package com.jam.demo.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.ChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
* 持久化ChatClient配置类
* @author ken
*/

@Configuration
public class PersistentChatClientConfig {

   /**
    * 配置带滑动窗口的持久化会话记忆
    * @param chatMemoryRepository 持久化仓库
    * @return 会话记忆实例
    */

   @Bean
   @Primary
   public ChatMemory persistentChatMemory(ChatMemoryRepository chatMemoryRepository) {
       return MessageWindowChatMemory.builder()
               .maxMessages(20)
               .chatMemoryRepository(chatMemoryRepository)
               .build();
   }

   /**
    * 配置带持久化记忆能力的ChatClient
    * @param builder ChatClient构建器
    * @param persistentChatMemory 持久化会话记忆
    * @return 带持久化记忆能力的ChatClient实例
    */

   @Bean
   @Primary
   public ChatClient persistentChatClient(ChatClient.Builder builder, ChatMemory persistentChatMemory) {
       return builder
               .defaultAdvisors(ChatMemoryAdvisor.builder(persistentChatMemory).build())
               .build();
   }
}

5.6 持久化对话服务实现

package com.jam.demo.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

/**
* 持久化对话服务实现
* @author ken
*/

@Slf4j
@Service
@RequiredArgsConstructor
public class PersistentChatService {

   private final ChatClient persistentChatClient;
   private final ChatMemoryRepository chatMemoryRepository;

   /**
    * 带持久化记忆的对话
    * @param conversationId 会话唯一标识
    * @param content 用户提问内容
    * @return AI回复内容
    */

   public String chatWithPersistentMemory(String conversationId, String content) {
       if (!StringUtils.hasText(conversationId)) {
           throw new IllegalArgumentException("会话ID不能为空");
       }
       if (!StringUtils.hasText(content)) {
           throw new IllegalArgumentException("提问内容不能为空");
       }

       return persistentChatClient.prompt()
               .user(content)
               .conversationId(conversationId)
               .call()
               .content();
   }

   /**
    * 清空指定会话的持久化记忆
    * @param conversationId 会话唯一标识
    */

   public void clearChatMemory(String conversationId) {
       if (!StringUtils.hasText(conversationId)) {
           throw new IllegalArgumentException("会话ID不能为空");
       }
       chatMemoryRepository.clear(conversationId);
   }

   /**
    * 获取指定会话的历史消息
    * @param conversationId 会话唯一标识
    * @return 历史消息列表
    */

   public List<Message> getChatHistory(String conversationId) {
       if (!StringUtils.hasText(conversationId)) {
           throw new IllegalArgumentException("会话ID不能为空");
       }
       return chatMemoryRepository.getMessages(conversationId);
   }

   /**
    * 获取所有会话ID列表
    * @return 会话ID列表
    */

   public List<String> listAllConversations() {
       return chatMemoryRepository.listConversationIds();
   }
}

5.7 持久化对话接口实现

package com.jam.demo.controller;

import com.jam.demo.service.PersistentChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.messages.Message;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

/**
* 持久化对话接口
* @author ken
*/

@RestController
@RequestMapping("/api/chat/persistent")
@RequiredArgsConstructor
@Tag(name = "持久化对话接口", description = "基于MySQL的带记忆对话能力,支持分布式部署")
public class PersistentChatController {

   private final PersistentChatService persistentChatService;

   /**
    * 带持久化记忆的对话接口
    */

   @PostMapping("/send")
   @Operation(summary = "发送带持久化记忆的对话请求", description = "同一个conversationId的请求会共享持久化上下文记忆,应用重启不丢失")
   public ResponseEntity<Map<String, String>> chat(
           @Parameter(description = "会话唯一标识", required = true) @RequestParam String conversationId,
           @Parameter(description = "用户提问内容", required = true) @RequestParam String content) {
       String result = persistentChatService.chatWithPersistentMemory(conversationId, content);
       return ResponseEntity.ok(Map.of("content", result));
   }

   /**
    * 清空会话记忆接口
    */

   @DeleteMapping("/clear")
   @Operation(summary = "清空指定会话的持久化记忆", description = "清空后该会话的历史上下文将从数据库中删除")
   public ResponseEntity<Void> clearMemory(
           @Parameter(description = "会话唯一标识", required = true)
@RequestParam String conversationId) {
       persistentChatService.clearChatMemory(conversationId);
       return ResponseEntity.noContent().build();
   }

   /**
    * 获取会话历史消息接口
    */

   @GetMapping("/history")
   @Operation(summary = "获取指定会话的历史消息", description = "查询该会话的所有历史对话消息")
   public ResponseEntity<List<Message>> getChatHistory(
           @Parameter(description = "会话唯一标识", required = true) @RequestParam String conversationId) {
       List<Message> history = persistentChatService.getChatHistory(conversationId);
       return ResponseEntity.ok(history);
   }

   /**
    * 获取所有会话ID接口
    */

   @GetMapping("/conversations")
   @Operation(summary = "获取所有会话ID列表", description = "查询系统中所有的会话ID")
   public ResponseEntity<List<String>> listAllConversations() {
       List<String> conversations = persistentChatService.listAllConversations();
       return ResponseEntity.ok(conversations);
   }
}

六、高级特性:滑动窗口与总结式记忆

大模型的上下文窗口有固定的token上限,无限制累加历史消息会导致token超限、请求失败、成本飙升,Spring AI提供了两种核心解决方案,分别应对不同的业务场景。

6.1 滑动窗口记忆

滑动窗口记忆是最常用的token控制方案,核心逻辑是仅保留最近的N轮对话,当消息数量超过设定的阈值时,自动删除最早的历史消息,保证消息总数始终在可控范围内。 Spring AI内置的MessageWindowChatMemory已经实现了该能力,在之前的持久化配置中,我们通过maxMessages(20)设置了最大保留20条消息,也就是10轮完整的对话,完全满足绝大多数业务场景的需求。

滑动窗口记忆的核心配置

@Bean
public ChatMemory messageWindowChatMemory(ChatMemoryRepository chatMemoryRepository) {
   return MessageWindowChatMemory.builder()
           .maxMessages(20)
           .chatMemoryRepository(chatMemoryRepository)
           .build();
}

6.2 总结式记忆

总结式记忆适用于需要保留长周期上下文的业务场景,核心逻辑是当历史消息超过阈值时,不直接删除最早的消息,而是通过大模型对历史消息进行核心信息总结,用简短的总结文本替换原来的长历史消息,既保留了完整的上下文语义,又严格控制了token数量

总结式记忆实现示例

package com.jam.demo.memory;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
* 总结式记忆处理器
* @author ken
*/

@Slf4j
@Component
@RequiredArgsConstructor
public class SummarizingChatMemoryHandler {

   private final ChatClient chatClient;
   private final ChatMemoryRepository chatMemoryRepository;

   private static final int MAX_MESSAGES_BEFORE_SUMMARY = 30;
   private static final String SUMMARY_PROMPT = "请对以下对话历史进行核心信息总结,保留关键的上下文语义,去除冗余内容,总结内容不超过500字。对话历史:\n%s";

   /**
    * 处理会话的历史消息总结
    * @param conversationId 会话ID
    */

   public void summarizeHistoryIfNeeded(String conversationId) {
       List<Message> messageList = chatMemoryRepository.getMessages(conversationId);
       if (CollectionUtils.isEmpty(messageList) || messageList.size() < MAX_MESSAGES_BEFORE_SUMMARY) {
           return;
       }

       StringBuilder historyBuilder = new StringBuilder();
       for (Message message : messageList) {
           historyBuilder.append(message.getMessageType().name())
                   .append(": ")
                   .append(message.getText())
                   .append("\n");
       }

       String summary = chatClient.prompt()
               .user(String.format(SUMMARY_PROMPT, historyBuilder.toString()))
               .call()
               .content();

       chatMemoryRepository.clear(conversationId);
       chatMemoryRepository.addMessage(conversationId, new SystemMessage("历史对话总结:" + summary));

       int startIndex = Math.max(0, messageList.size() - 10);
       List<Message> recentMessages = messageList.subList(startIndex, messageList.size());
       chatMemoryRepository.addMessages(conversationId, recentMessages);

       log.info("会话{}的历史消息总结完成,原消息数量:{},总结后保留消息数量:{}",
               conversationId, messageList.size(), recentMessages.size() + 1);
   }
}

七、带记忆对话的全流程底层原理

7.1 全流程执行时序图

7.2 核心环节原理拆解

  1. 拦截器调度ChatMemoryAdvisor基于Spring的AOP机制,在ChatClient的请求流程中插入了两个扩展点:请求发送前的前置处理,和响应返回后的后置处理,实现了记忆能力的完全无侵入集成,开发者无需修改业务代码即可接入。
  2. 会话隔离实现:通过conversationId作为唯一标识,每个会话对应独立的消息列表,Spring AI在加载和保存消息时,严格按照conversationId进行过滤,完全杜绝了不同会话之间的数据串扰。
  3. 消息注入逻辑:在前置处理环节,ChatMemoryAdvisor会将加载到的历史消息,按照时间顺序插入到当前用户消息的前面,保证模型能够按照交互顺序读取完整的上下文,同时保证SystemMessage始终在最开头的位置。
  4. 记忆持久化逻辑:在后置处理环节,ChatMemoryAdvisor会自动将本次交互的UserMessage和AssistantMessage成对追加到ChatMemoryRepository中,无需开发者手动调用保存接口,保证了记忆数据的完整性。

7.3 易混淆概念区分

概念 核心职责 适用场景
ChatMemory 单个会话的内存级消息管理,负责当前会话的消息读写 单次请求的消息处理,会话级的内存操作
ChatMemoryRepository 全量会话的持久化管理,负责消息的加载、保存、清空 跨请求、跨实例的记忆持久化,分布式场景
InMemoryChatMemory 基于内存的ChatMemory实现,数据随应用重启丢失 本地测试、单机低并发场景
MessageWindowChatMemory 带滑动窗口的ChatMemory实现,自动控制消息数量 生产环境绝大多数业务场景,控制token成本

八、生产环境最佳实践与坑点规避

8.1 会话安全与隔离最佳实践

  1. 会话ID生成规范:采用UUID、雪花算法等非连续的唯一ID生成策略,禁止使用自增数字作为会话ID,避免被恶意遍历。
  2. 用户与会话绑定:生产环境中,必须将conversationId与用户ID进行强绑定,在所有记忆操作前,校验当前登录用户是否有权限操作该会话ID,杜绝越权访问与数据泄露。
  3. 会话权限控制:禁止普通用户查询全量会话ID列表,该接口仅对管理员开放,且必须做分页与权限控制。

8.2 Token成本与稳定性最佳实践

  1. 强制设置滑动窗口:无论使用哪种记忆实现,都必须设置合理的消息数量上限,禁止无限制累加历史消息,根据使用的模型的上下文窗口,预留至少30%的token空间给当前请求与模型回复。
  2. 分级窗口策略:针对不同的业务场景设置不同的窗口大小,比如客服对话场景可以设置较大的窗口,普通问答场景设置较小的窗口,平衡体验与成本。
  3. Token数校验:在请求发送前,对全量Prompt的token数进行校验,如果超过模型的上限,自动触发历史消息的截断或总结,避免请求直接失败。

8.3 性能优化最佳实践

  1. 活跃会话缓存:针对高频访问的活跃会话,采用Redis进行缓存,缓存过期时间设置为30分钟,减少数据库的查询压力,提升请求响应速度。
  2. 批量操作优化:针对批量消息保存场景,采用编程式事务进行批量提交,减少数据库的IO次数,提升写入性能。
  3. 历史数据归档:设置定时任务,定期清理超过3个月未活跃的会话数据,或者将冷数据归档到离线存储,避免主表数据量过大导致的查询性能下降。

8.4 常见坑点规避

  1. SystemMessage重复累加:禁止将SystemMessage重复添加到历史消息中,否则会导致token的严重浪费,Spring AI的ChatMemoryAdvisor会自动处理SystemMessage的位置,无需手动追加。
  2. 消息顺序错乱:必须保证历史消息的顺序是UserMessage与AssistantMessage成对出现,禁止手动修改消息的创建时间与顺序,否则会导致模型无法正确理解上下文。
  3. 敏感信息泄露:在保存历史消息前,必须对用户输入的手机号、身份证号、密码等敏感信息进行脱敏处理,禁止明文存储敏感信息到数据库中。
  4. 分布式环境数据一致性:分布式环境中,必须使用持久化的ChatMemoryRepository实现,禁止使用InMemoryChatMemory,否则会导致不同实例之间的会话记忆不一致。

九、总结

Spring AI的聊天记忆体系,为Java开发者提供了一套优雅、低侵入、高扩展的多轮对话解决方案,完全屏蔽了底层的消息拼接、持久化、token控制等复杂逻辑,开发者仅需通过简单的配置,即可实现从本地测试到生产级落地的全流程能力。

目录
相关文章
|
4天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
3923 8
|
15天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
11584 131
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
3天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
1403 5
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
15天前
|
人工智能 IDE API
2026年国内 Codex 安装教程和使用教程:GPT-5.4 完整指南
Codex已进化为AI编程智能体,不仅能补全代码,更能理解项目、自动重构、执行任务。本文详解国内安装、GPT-5.4接入、cc-switch中转配置及实战开发流程,助你从零掌握“描述需求→AI实现”的新一代工程范式。(239字)
7885 139
|
5天前
|
人工智能 自然语言处理 数据挖掘
零基础30分钟搞定 Claude Code,这一步90%的人直接跳过了
本文直击Claude Code使用痛点,提供零基础30分钟上手指南:强调必须配置“工作上下文”(about-me.md+anti-ai-style.md)、采用Cowork/Code模式、建立标准文件结构、用提问式提示词驱动AI理解→规划→执行。附可复制模板与真实项目启动法,助你将Claude从聊天工具升级为高效执行系统。
|
4天前
|
人工智能 定位技术
Claude Code源码泄露:8大隐藏功能曝光
2026年3月,Anthropic因配置失误致Claude Code超51万行源码泄露,意外促成“被动开源”。代码中藏有8大未发布功能,揭示其向“超级智能体”演进的完整蓝图,引发AI编程领域震动。(239字)
2283 9
|
4天前
|
云安全 供应链 安全
Axios投毒事件:阿里云安全复盘分析与关键防护建议
阿里云云安全中心和云防火墙第一时间响应
1179 0

热门文章

最新文章