很多开发者在集成AI对话功能时,都会遇到一个核心痛点:多轮对话中AI完全无法记住之前的沟通内容,每次对话都像首次交互,用户体验极差。而Spring AI作为Spring官方推出的AI应用开发框架,提供了一套完整、低侵入、高扩展的聊天记忆解决方案,彻底解决了大模型对话的无状态问题。
一、大模型对话的无状态本质与聊天记忆的核心逻辑
1.1 大模型为什么会“失忆”
大语言模型(LLM)的核心能力是基于输入的文本序列进行概率推理,生成符合语义的下一个token。它本身没有内置的持久化存储能力,每一次API调用都是完全独立的无状态请求,模型不会自动保存、读取上一次对话的任何内容。 举个通俗的例子:你第一次问“Java的三大特性是什么?”,AI给出了封装、继承、多态的回答;当你第二次问“那第一个特性的核心设计思想是什么?”,如果没有记忆机制,模型完全无法感知“第一个特性”对应的上下文,只能给出无意义的泛化回答。
1.2 聊天记忆的本质与核心三要素
聊天记忆的本质,是对用户与AI的历史对话消息进行有序管理、持久化存储、按需调度,并在每次对话请求时,将符合规则的历史内容注入到当前请求的Prompt中,让模型能够感知完整的上下文,实现连贯的多轮对话。 一套合格的聊天记忆体系,必须满足三个核心要素:
- 消息有序性:严格区分消息类型,保证用户提问与AI回复的时间顺序与成对关系,避免上下文错乱
- 会话隔离性:不同用户、不同会话的记忆数据完全隔离,杜绝数据串扰与越权访问
- 窗口可控性:可灵活控制历史消息的长度,避免内容过多导致模型token超限,保障请求稳定性与成本可控
二、Spring AI 聊天记忆的核心架构与组件
Spring AI的聊天记忆体系基于Spring的原生设计理念,采用分层架构,解耦了消息管理、持久化、请求拦截三大核心能力,具备极高的扩展性。
2.1 核心架构图
2.2 核心组件详解
Spring AI的聊天记忆能力围绕三个核心顶级接口展开,配合消息模型与拦截器机制,构成了完整的能力闭环。
2.2.1 Message 消息体系
Spring AI中所有对话内容都被封装为Message接口的实现类,核心实现分为三类,是记忆体系的最小数据单元:
SystemMessage:系统提示词,用于给模型设定角色、行为规则、输出边界,通常放在对话序列的最开头,单个会话一般仅保留一个UserMessage:用户输入的提问内容,对应对话中的用户侧消息AssistantMessage:AI模型生成的回复内容,对应对话中的AI侧消息
历史对话序列由UserMessage与AssistantMessage成对组成,严格按照交互时间排序,是模型感知上下文的核心依据。
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 核心环节原理拆解
- 拦截器调度:
ChatMemoryAdvisor基于Spring的AOP机制,在ChatClient的请求流程中插入了两个扩展点:请求发送前的前置处理,和响应返回后的后置处理,实现了记忆能力的完全无侵入集成,开发者无需修改业务代码即可接入。 - 会话隔离实现:通过
conversationId作为唯一标识,每个会话对应独立的消息列表,Spring AI在加载和保存消息时,严格按照conversationId进行过滤,完全杜绝了不同会话之间的数据串扰。 - 消息注入逻辑:在前置处理环节,
ChatMemoryAdvisor会将加载到的历史消息,按照时间顺序插入到当前用户消息的前面,保证模型能够按照交互顺序读取完整的上下文,同时保证SystemMessage始终在最开头的位置。 - 记忆持久化逻辑:在后置处理环节,
ChatMemoryAdvisor会自动将本次交互的UserMessage和AssistantMessage成对追加到ChatMemoryRepository中,无需开发者手动调用保存接口,保证了记忆数据的完整性。
7.3 易混淆概念区分
| 概念 | 核心职责 | 适用场景 |
| ChatMemory | 单个会话的内存级消息管理,负责当前会话的消息读写 | 单次请求的消息处理,会话级的内存操作 |
| ChatMemoryRepository | 全量会话的持久化管理,负责消息的加载、保存、清空 | 跨请求、跨实例的记忆持久化,分布式场景 |
| InMemoryChatMemory | 基于内存的ChatMemory实现,数据随应用重启丢失 | 本地测试、单机低并发场景 |
| MessageWindowChatMemory | 带滑动窗口的ChatMemory实现,自动控制消息数量 | 生产环境绝大多数业务场景,控制token成本 |
八、生产环境最佳实践与坑点规避
8.1 会话安全与隔离最佳实践
- 会话ID生成规范:采用UUID、雪花算法等非连续的唯一ID生成策略,禁止使用自增数字作为会话ID,避免被恶意遍历。
- 用户与会话绑定:生产环境中,必须将
conversationId与用户ID进行强绑定,在所有记忆操作前,校验当前登录用户是否有权限操作该会话ID,杜绝越权访问与数据泄露。 - 会话权限控制:禁止普通用户查询全量会话ID列表,该接口仅对管理员开放,且必须做分页与权限控制。
8.2 Token成本与稳定性最佳实践
- 强制设置滑动窗口:无论使用哪种记忆实现,都必须设置合理的消息数量上限,禁止无限制累加历史消息,根据使用的模型的上下文窗口,预留至少30%的token空间给当前请求与模型回复。
- 分级窗口策略:针对不同的业务场景设置不同的窗口大小,比如客服对话场景可以设置较大的窗口,普通问答场景设置较小的窗口,平衡体验与成本。
- Token数校验:在请求发送前,对全量Prompt的token数进行校验,如果超过模型的上限,自动触发历史消息的截断或总结,避免请求直接失败。
8.3 性能优化最佳实践
- 活跃会话缓存:针对高频访问的活跃会话,采用Redis进行缓存,缓存过期时间设置为30分钟,减少数据库的查询压力,提升请求响应速度。
- 批量操作优化:针对批量消息保存场景,采用编程式事务进行批量提交,减少数据库的IO次数,提升写入性能。
- 历史数据归档:设置定时任务,定期清理超过3个月未活跃的会话数据,或者将冷数据归档到离线存储,避免主表数据量过大导致的查询性能下降。
8.4 常见坑点规避
- SystemMessage重复累加:禁止将SystemMessage重复添加到历史消息中,否则会导致token的严重浪费,Spring AI的
ChatMemoryAdvisor会自动处理SystemMessage的位置,无需手动追加。 - 消息顺序错乱:必须保证历史消息的顺序是UserMessage与AssistantMessage成对出现,禁止手动修改消息的创建时间与顺序,否则会导致模型无法正确理解上下文。
- 敏感信息泄露:在保存历史消息前,必须对用户输入的手机号、身份证号、密码等敏感信息进行脱敏处理,禁止明文存储敏感信息到数据库中。
- 分布式环境数据一致性:分布式环境中,必须使用持久化的
ChatMemoryRepository实现,禁止使用InMemoryChatMemory,否则会导致不同实例之间的会话记忆不一致。
九、总结
Spring AI的聊天记忆体系,为Java开发者提供了一套优雅、低侵入、高扩展的多轮对话解决方案,完全屏蔽了底层的消息拼接、持久化、token控制等复杂逻辑,开发者仅需通过简单的配置,即可实现从本地测试到生产级落地的全流程能力。