* Spring AI持久化会话记忆(ChatMemory)
Spring AI 多轮对话记忆(ChatMemory)保姆级教程:从内存版到 Redis 持久化:
https://blog.csdn.net/weixin_55772633/article/details/160626801?spm=1011.2415.3001.5331
在 AI 应用开发中,多轮对话的记忆能力至关重要。Spring AI 提供了 ChatMemory 抽象,但默认的基于内存的实现仅适用于学习测试。在生产环境中,我们需要可靠、高性能的持久化存储方案。本文将带你从零实现一个基于 Redis 的 ChatMemoryRepository,解决序列化、查询等核心问题,并最终完成一个可用的会话记忆组件。
一、为什么需要自定义存储?
Spring AI 官方的 ChatMemory 默认实现是 InMemoryChatMemory,数据存于 JVM 堆内存,存在以下缺陷:
-
无法持久化,应用重启后对话丢失
-
不支持分布式部署,多实例间内存不共享
-
内存占用随会话量线性增长,无法支撑生产规模
因此我们需要更换存储方案。可选的有 MySQL、MongoDB 或 Redis。本文选择 Redis,原因如下:
-
读写速度快,适合高频对话场景
-
数据结构丰富,List 天然适合存储聊天记录
-
使用简单,Spring Data Redis 集成方便
遗憾的是,Spring AI 官方并未提供 Redis 的实现,需要我们自己动手。
二、理解核心接口:ChatMemoryRepository
要自定义存储,首先要实现 org.springframework.ai.chat.memory.ChatMemoryRepository 接口:
public interface ChatMemoryRepository {
List<String> findConversationIds();
List<Message> findByConversationId(String conversationId);
void saveAll(String conversationId, List<Message> messages);
void deleteByConversationId(String conversationId);
}
该接口定义了会话消息的 CRUD 操作。我们采用 Redis 的 List 结构来存储每个会话的消息列表,key 为 chat:memory:{conversationId},每条消息作为一个 JSON 字符串存放到列表中。
Spring AI 1.1.x 把存储层和裁剪逻辑拆成了两层:
-
ChatMemoryRepository:纯存储接口,只管读写全量消息,不做任何裁剪。 -
MessageWindowChatMemory:包装Repository,对外暴露ChatMemory,负责按条数裁剪窗口。
三、导入Redis相关配置
3.1 添加依赖
redis依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
常用工具依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.34</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.54</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
3.2 配置 Redis 连接
spring:
data:
redis:
host: localhost
password: xxx
port: 6379
timeout: 5000
database: 7
key-prefix: studyAi
3.3 配置Redis配置类
package com.edu.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import java.time.Duration;
/**
* Redis配置类
* 负责配置RedisTemplate序列化方式、缓存管理器以及自定义key前缀策略
*/
@Configuration
@EnableCaching
public class RedisConfig {
// 从配置文件中读取Redis key的统一前缀
@Value("${spring.data.redis.key-prefix}")
private String keyPrefix;
/**
* 配置RedisTemplate
* - key使用带前缀的String序列化
* - value使用Jackson JSON序列化
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// key采用带统一前缀的key序列化器
template.setKeySerializer(prefixRedisSerializer());
// hash的key也采用带统一前缀的key序列化器
template.setHashKeySerializer(prefixRedisSerializer());
// value序列化方式采用jackson
template.setValueSerializer(valueSerializer());
// hash的value序列化方式采用jackson
template.setHashValueSerializer(valueSerializer());
template.afterPropertiesSet();
return template;
}
/**
* 配置Spring Cache缓存管理器
* - 使用非锁定的RedisCacheWriter
* - 自定义缓存key前缀拼接规则
* - value采用Jackson JSON序列化
* - 默认永不过期
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith(name -> {
if (name.endsWith(":")) {
return keyPrefix.concat(":").concat(name);
}
return keyPrefix.concat(":").concat(name).concat(":");
})
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()));
redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
//过期时间设置为 Duration#ZERO永远不过期
redisCacheConfiguration.entryTtl(Duration.ZERO);
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
/**
* 创建带统一前缀的key序列化器
*/
private RedisSerializer<String> prefixRedisSerializer() {
return new PrefixRedisSerializer(keyPrefix + ":");
}
/**
* 自定义前缀序列化器
* 在序列化时自动为key添加前缀,实现多应用共用Redis时的命名空间隔离
*/
static class PrefixRedisSerializer implements RedisSerializer<String> {
/**
* 委托给默认的String序列化器处理实际的字节转换
*/
private final RedisSerializer<String> delegate = RedisSerializer.string();
/**
* key前缀,如 "app:"
*/
private final String prefix;
public PrefixRedisSerializer(String prefix) {
this.prefix = prefix;
}
@Override
public byte[] serialize(String s) throws SerializationException {
return delegate.serialize(prefix + s);
}
@Override
public String deserialize(byte[] bytes) throws SerializationException {
return delegate.deserialize(bytes);
}
}
/**
* 使用Jackson序列化器
*
* @return
*/
private RedisSerializer<Object> valueSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
}
四、自定义 RedisChatMemoryRepository(简易版本)
4.1自定义 RedisChatMemoryRepository
package com.edu.chatMemory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
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.UserMessage;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 基于Redis的聊天记忆存储实现
* 将对话消息以List结构存储在Redis中,支持按会话ID进行增删查操作
* 每个会话设置TTL自动过期,避免无限占用内存
*/
public class RedisChatMemoryRepository implements ChatMemoryRepository {
/**
* Redis key前缀,完整key格式为 chat:memory:{conversationId}
*/
private static final String KEY_PREFIX = "chat:memory:";
/**
* 会话记录过期天数
*/
private static final int TTL_DAYS = 3;
/**
* JSON序列化工具,忽略未知属性以增强兼容性
*/
private static final ObjectMapper MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final StringRedisTemplate stringRedisTemplate;
public RedisChatMemoryRepository(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 获取所有会话ID
* 注意:使用KEYS命令扫描,生产环境数据量大时应替换为SCAN
*/
@Override
public List<String> findConversationIds() {
Set<String> keys = stringRedisTemplate.keys(KEY_PREFIX + "*");
if (CollectionUtils.isEmpty(keys)) {
return new ArrayList<>();
}
return keys.stream()
.map(key -> key.substring(KEY_PREFIX.length()))
.toList();
}
/**
* 根据会话ID查询所有消息
* 从Redis List中读取原始数据,反序列化为对应的Message子类(UserMessage/AssistantMessage)
*/
@Override
public List<Message> findByConversationId(String conversationId) {
String key = KEY_PREFIX + conversationId;
List<String> rawMessages = stringRedisTemplate.opsForList().range(key, 0, -1);
if (CollectionUtils.isEmpty(rawMessages)) {
return new ArrayList<>();
}
List<Message> messages = new ArrayList<>();
rawMessages.forEach(raw -> {
try {
MessageRecord record = MAPPER.readValue(raw, MessageRecord.class);
if (MessageType.USER.getValue().equals(record.role())) {
messages.add(new UserMessage(record.content()));
} else if (MessageType.ASSISTANT.getValue().equals(record.role())) {
messages.add(new AssistantMessage(record.content()));
}
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to deserialize chat message", e);
}
});
return messages;
}
/**
* 全量保存会话消息
* 采用"先删后写"策略,确保每次都是完整覆盖而非追加,防止消息重复
* 写入后刷新TTL过期时间
*/
@Override
public void saveAll(String conversationId, List<Message> messages) {
String key = KEY_PREFIX + conversationId;
stringRedisTemplate.delete(key);
List<String> jsonList = messages.stream()
.map(msg -> {
try {
return MAPPER.writeValueAsString(new MessageRecord(msg.getMessageType().getValue(), msg.getText()));
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize chat message", e);
}
})
.toList();
if (!jsonList.isEmpty()) {
stringRedisTemplate.opsForList().rightPushAll(key, jsonList);
}
stringRedisTemplate.expire(key, TTL_DAYS, TimeUnit.DAYS);
}
/**
* 删除指定会话的所有消息记录
*/
@Override
public void deleteByConversationId(String conversationId) {
stringRedisTemplate.delete(KEY_PREFIX + conversationId);
}
/**
* 消息记录DTO,用于Redis序列化/反序列化,role标识消息类型(user/assistant)
*/
record MessageRecord(String role, String content) {
}
}
4.2注册 ChatMemory Bean
package com.edu.config;
import com.edu.chatMemory.RedisChatMemoryRepository;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory(StringRedisTemplate stringRedisTemplate) {
RedisChatMemoryRepository redisChatMemoryRepository = new RedisChatMemoryRepository(stringRedisTemplate);
// 底层走 Redis 持久化,上层限制最多保留 20 条消息
return MessageWindowChatMemory.builder()
.chatMemoryRepository(redisChatMemoryRepository)
.maxMessages(20)
.build();
}
}
4.3注册advisors
@Autowired
private ChatMemory redisChatMemory;
@Override
public Flux<ChatEventVO> streamChatJson(String question, String sessionId) {
return chatClient
.prompt()
.advisors(MessageChatMemoryAdvisor.builder(redisChatMemory)
.conversationId(sessionId)
.build())
.system(x->x.text(aiPromptResources.getSystemChatMessage())
.param("now", LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
.user(question)
.stream()
.chatResponse()
.map(chatResponse -> {
var text = chatResponse.getResult().getOutput().getText();
return ChatEventVO.builder()
.eventType(ChatEventTypeEnum.DATA.getValue())
.eventData(text)
.build();
})
.doFirst(() -> GENERATE_STATUS.put(sessionId, true)) // 开始生成时置为 true
.doOnError(throwable -> GENERATE_STATUS.remove(sessionId))
.doOnComplete(() -> GENERATE_STATUS.remove(sessionId))
.takeWhile(response -> GENERATE_STATUS.getOrDefault(sessionId, false)) // 关键:控制流是否继续
.concatWith(Flux.just(ChatEventVO.builder()
.eventType(ChatEventTypeEnum.STOP.getValue()) //结束标识
.build()));
}
4.4测试
聊天如下:

Redis存储如下:(正常存储)

4.5解决“停止生成”导致会话记忆丢失的 Bug
在构建 AI 对话应用时,我们通常会实现两个核心功能:
-
停止生成:允许用户中途打断 AI 的长文本回复,提升交互体验。
-
会话记忆:将多轮对话持久化到 Redis,实现跨请求的上下文记忆。
然而,当这两个功能同时启用时,一个隐蔽的 Bug 浮现了:用户点击“停止生成”后,AI 已经输出的内容不会被保存到会话记忆中。
4.5.1重现步骤
-
在
RedisChatMemoryRepository.saveAll方法上打断点。 -
在
ChatServiceImpl.chat方法(流式调用入口)上也打断点。 -
发起一次对话请求,例如用户输入:“你好,请介绍一下自己”。
-
第一次进入
saveAll→ 这是保存用户输入的消息(正常)。放行,Redis 中出现用户消息。 -
流式输出开始,
ChatServiceImpl断点命中多次,AI 逐字返回内容(例如“您好,我是在线教育平台的客服代表……”)。 -
在输出过程中,调用
/stop接口。 -
放行后续断点,观察到:
-
前端不再有新的数据返回(停止生效)。
-
Redis 中没有任何 AI 回复的消息,只有之前保存的用户问题。
-
结论:只要执行了停止操作,本次 AI 生成的回复就永久丢失了,不会存入会话记忆。
4.5.2原因分析
通过源码追踪,我们发现:
-
Spring AI 的
MessageWindowChatMemory在每次完整对话结束后(即 Flux 正常完成时),会自动调用add方法将用户消息和 AI 回复一起存入ChatMemoryRepository。 -
当用户主动中断(
/stop)时,底层的Flux被取消(cancel),Spring AI 不会触发保存逻辑。这可能是当前版本的设计行为(未处理取消场景),或是一个待完善的边缘情况。
简单来说:正常结束 → 自动保存;异常取消 → 不保存。而我们的停止生成就是通过取消 Flux 实现的,因此造成了记忆丢失。
4.5.3解决方案
既然框架不处理取消场景,那我们就自己动手,在 Flux 取消时手动保存已生成的内容。
技术要点
-
使用
Flux.doOnCancel()注册取消时的回调。 -
在流式处理的
map操作中,动态拼接已输出的文本(存于StringBuilder)。 -
取消时,调用
chatMemory.add()手动将 AI 回复(即使不完整)存入 Redis。
关键:
doOnCancel()必须放在takeWhile()等可能中断流的操作之前,否则取消事件无法被捕获。
4.5.4改造后的代码
@Autowired
private ChatMemory redisChatMemory;
@Override
public Flux<ChatEventVO> streamChatJson(String question, String sessionId) {
// 大模型输出内容的缓存器,用于在输出中断后的数据存储
var outputBuilder = new StringBuilder();
return chatClient
.prompt()
.advisors(MessageChatMemoryAdvisor.builder(redisChatMemory)
.conversationId(sessionId)
.build())
.system(x->x.text(aiPromptResources.getSystemChatMessage())
.param("now", LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
.user(question)
.stream()
.chatResponse()
.map(chatResponse -> {
var text = chatResponse.getResult().getOutput().getText();
outputBuilder.append(text); // 累积文本
return ChatEventVO.builder()
.eventType(ChatEventTypeEnum.DATA.getValue())
.eventData(text)
.build();
})
.doFirst(() -> GENERATE_STATUS.put(sessionId, true)) // 开始生成时置为 true
.doOnError(throwable -> GENERATE_STATUS.remove(sessionId))
.doOnComplete(() -> GENERATE_STATUS.remove(sessionId))
.doOnCancel(() -> {
// 当输出被取消时,保存输出的内容到历史记录中
redisChatMemory.add(sessionId, new AssistantMessage(outputBuilder.toString()));
})
.takeWhile(response -> GENERATE_STATUS.getOrDefault(sessionId, false)) // 关键:控制流是否继续
.concatWith(Flux.just(ChatEventVO.builder()
.eventType(ChatEventTypeEnum.STOP.getValue()) //结束标识
.build()));
}
4.6主动停止时,redis缓存的内存记录比实际输出多
如果遇到这个问题,可见本文5.5章节,此处不再赘述!!!
五、自定义 RedisChatMemoryRepository(复杂参数版本)
首先我们需要了解下Message接口的子类:

可以看到,有4个具体的子类,分别对应着4种消息:
- AssistantMessage 大模型生成的消息
- SystemMessage 系统消息
- ToolResponseMessage 工具响应消息
- UserMessage 用户消息
虽然,有4种消息,实际上我们只需要定义一个类来对应这4种消息即可,只需要通过messageType就行。
5.1创建实体类
package com.edu.memory;
import lombok.Data;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.content.Media;
import java.util.List;
import java.util.Map;
/**
* 自定义消息DTO,用于在Redis中统一存储各类型消息
* 作为Spring AI Message体系与JSON序列化之间的中间转换对象
*/
@Data
public class MyMessage {
/** 消息类型标识,对应 MessageType 枚举值(SYSTEM/USER/ASSISTANT/TOOL) */
private String messageType;
/** 消息元数据,存储附加键值信息 */
private Map<String, Object> metadata = Map.of();
/** 多媒体附件列表,用于携带图片等媒体内容(主要用于USER消息) */
private List<Media> media = List.of();
/** AI助手调用的工具列表(仅ASSISTANT消息使用) */
private List<AssistantMessage.ToolCall> toolCalls = List.of();
/** 消息文本内容 */
private String textContent;
/** 工具执行结果列表(仅TOOL消息使用) */
private List<ToolResponseMessage.ToolResponse> toolResponses = List.of();
/** 扩展参数 */
private Map<String, Object> params = Map.of();
}
5.2编写MessageUtil工具类来实现序列化和反序列化方法:
package com.edu.memory;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.ai.chat.messages.*;
/**
* 消息转换工具类,提供消息对象与JSON字符串之间的转换功能,主要用于Redis存储格式转换
*/
public class MessageUtil {
/**
* 将Message对象转换为Redis存储格式的JSON字符串
*
* @param message 需要转换的原始消息对象
* @return 符合Redis存储规范的JSON字符串
*/
public static String toJson(Message message) {
var myMessage = BeanUtil.toBean(message, MyMessage.class);
// 设置消息内容
myMessage.setTextContent(message.getText());
if (message instanceof AssistantMessage assistantMessage) {
myMessage.setToolCalls(assistantMessage.getToolCalls());
}
if (message instanceof ToolResponseMessage toolResponseMessage) {
myMessage.setToolResponses(toolResponseMessage.getResponses());
}
return JSONUtil.toJsonStr(myMessage);
}
/**
* 将Redis存储的JSON字符串反序列化为对应的Message对象
*
* @param json Redis存储的JSON格式消息数据
* @return 对应类型的Message对象
* @throws RuntimeException 当无法识别的消息类型时抛出异常
*/
public static Message toMessage(String json) {
var myMessage = JSONUtil.toBean(json, MyMessage.class);
var messageType = MessageType.valueOf(myMessage.getMessageType());
switch (messageType) {
case SYSTEM -> {
//return new SystemMessage(myMessage.getTextContent());
return SystemMessage.builder()
.text(myMessage.getTextContent())
.build();
}
case USER -> {
return UserMessage.builder()
.text(myMessage.getTextContent())
.metadata(myMessage.getMetadata())
.media(myMessage.getMedia())
.build();
}
case ASSISTANT -> {
return AssistantMessage.builder()
.content(myMessage.getTextContent())
.media(myMessage.getMedia())
.toolCalls(myMessage.getToolCalls())
.build();
}
case TOOL -> {
return ToolResponseMessage.builder()
.responses(myMessage.getToolResponses())
.metadata(myMessage.getMetadata())
.build();
}
}
throw new RuntimeException("Message data conversion failed.");
}
}
5.3自定义 RedisChatMemoryRepository
package com.edu.chatMemory;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.stream.StreamUtil;
import cn.hutool.core.util.StrUtil;
import com.edu.memory.MessageUtil;
import org.apache.logging.log4j.util.Strings;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.List;
import java.util.Set;
/**
* 基于Redis实现的ChatMemoryRepository
*/
public class RedisChatMemoryRepository implements ChatMemoryRepository {
// 默认redis中key的前缀
public static final String DEFAULT_PREFIX = "chat:memory:";
private StringRedisTemplate stringRedisTemplate;
public RedisChatMemoryRepository(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public List<String> findConversationIds() {
Set<String> keys = this.stringRedisTemplate.keys(DEFAULT_PREFIX + "*");
if (null == keys) {
return List.of();
}
return StreamUtil.of(keys)
.map(key -> StrUtil.replace(key, DEFAULT_PREFIX, Strings.EMPTY))
.toList();
}
@Override
public List<Message> findByConversationId(String conversationId) {
var redisKey = this.getKey(conversationId);
var listOps = this.stringRedisTemplate.boundListOps(redisKey);
// 获取列表中所有的数据
var messages = listOps.range(0, -1);
return CollStreamUtil.toList(messages, MessageUtil::toMessage);
}
@Override
public void saveAll(String conversationId, List<Message> messages) {
// 注意:messages 是全量的消息列表
var redisKey = this.getKey(conversationId);
var listOps = this.stringRedisTemplate.boundListOps(redisKey);
//将原有消息全部删除
this.deleteByConversationId(conversationId);
// 保存数据到redis
messages.forEach(message -> listOps.rightPush(MessageUtil.toJson(message)));
}
@Override
public void deleteByConversationId(String conversationId) {
var redisKey = this.getKey(conversationId);
this.stringRedisTemplate.delete(redisKey);
}
private String getKey(String conversationId) {
return DEFAULT_PREFIX + conversationId;
}
}
5.4注册 ChatMemory Bean
package com.edu.config;
import com.edu.chatMemory.RedisChatMemoryRepository;
import com.edu.chatMemory.RedisChatSimpleMemoryRepository;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory(StringRedisTemplate stringRedisTemplate) {
//RedisChatSimpleMemoryRepository redisChatMemoryRepository = new RedisChatSimpleMemoryRepository(stringRedisTemplate);
RedisChatMemoryRepository redisChatMemoryRepository = new RedisChatMemoryRepository(stringRedisTemplate);
// 底层走 Redis 持久化,上层限制最多保留 20 条消息
return MessageWindowChatMemory.builder()
.chatMemoryRepository(redisChatMemoryRepository)
.maxMessages(20)
.build();
}
}
5.5小问题注意事项

问题描述:主动停止时,redis缓存的内存记录比实际输出多。也就是说,当调用取消(cancel)时,doOnCancel 中会将 outputBuilder.toString() 保存到 redisChatMemory 中,但这个 outputBuilder 累积的文本可能比实际输出给客户端的要多。
观察代码

问题原因:outputBuilder.append(text) 发生在 takeWhile 判断之前,当主动取消(cancel)时,最后一个 chunk 的文本已被追加到 outputBuilder,但由于 takeWhile 的条件变为 false(如 GENERATE_STATUS 被外部设为 false 或取消导致流终止),该 chunk 实际上并没有被发射给客户端。最终 doOnCancel 保存了包含这个未发送 chunk 的完整 outputBuilder,导致 Redis 中存储的内容比客户端实际收到的多。
解决方案:将文本追加操作移到 takeWhile 之后,确保只有确认要发射的 chunk 才被累积到 outputBuilder 中。同时建议在 doOnComplete 中也保存历史记录(适用于优雅停止场景),并保持 doOnCancel 的保存逻辑。


AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)