Spring AI 聊天记忆功能实战(二):自定义 Redis 聊天记忆外部存储实现
Spring AI 框架通过模块化的抽象设计允许我们将默认的内存存储替换为自定义的外部存储方式,从而获得更好的扩展性和数据持久性。本文将深入探讨如何通过自定义 Redis 存储实现聊天记忆的功能。
相较于内存存储,Redis 作为聊天记忆存储具有以下显著优势:
- 分布式支持:天然支持集群模式,适合微服务架构
- 数据持久化:提供 RDB 和 AOF 两种持久化方式,避免数据丢失
- 高可用性:通过 Sentinel 和 Cluster 实现自动故障转移
- 丰富数据结构:使用 List 或 Stream 结构可以更高效地管理消息序列
项目前期准备
通过上文可知,Spring AI 框架通过ChatMemoryRepository
与ChatMemory
抽象层实现聊天记忆及消息存储。那么 Spring AI 在 Chat Memory 上都做了哪些工作呢?
- 如果我们想要自定义一个外部存储方式,首先需要实现 ChatMemoryRepository 接口。
- 集成进 Spring AI 框架中,我们还需实现 自动配置 ChatMemoryRepository 的实现类 Bean 对象。
ChatMemoryRepository
接口提供了存储逻辑的统一抽象,这些方法会在模型交互时与 MessageWindowChatMemory
对象中产生调用:
java
public interface ChatMemoryRepository {
// 获取所有会话ID
List<String> findConversationIds();
// 获取指定会话ID的聊天消息
List<Message> findByConversationId(String conversationId);
// 存储整个会话ID的历史消息(替换式更新)
void saveAll(String conversationId, List<Message> messages);
// 清理指定会话ID中的聊天消息
void deleteByConversationId(String conversationId);
}
项目准备阶段,创建新的项目模块,分别是:
spring-ai-model-chat-memory-repository-redis:ChatMemoryRepository的核心实现模块,Redis外部存储库的具体实现。
spring-ai-autoconfigure-model-chat-memory-repository-redis:ChatMemoryRepository的自动配置模块,包含Spring Boot自动配置类。
spring-ai-starter-model-chat-memory-repository-redis:Starter模块,提供开箱即用的依赖管理。
接下来让我们进行逐一实现。
ChatMemoryRepository的Redis实现
Redis 客户端有很多,这里我们采用更 spring 的方式来创建客户端,导入 spring-data-redis 依赖,结合 spring 通过 RedisTemplate 模板类控制与 Redis 服务的数据交互。
xml
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
配置类定义
首先我们提供一个 Redis 客户端的 Config 配置类文件,配置写入数据时的需求参数,比如:管理 Redis 模板类、指定 key 前缀为"spring_ai_chat_memory:"、指定写入数据的 time-to-live 会话过期时间,参数可在应用配置中灵活调整,满足不同场景需求。
java
public class RedisChatMemoryRepositoryConfig {
private RedisTemplate<String, String> redisTemplate;
private String keyPrefix;
private long timeToLive;
// ......
}
实现类定义
在 spring-ai-model-chat-memory-repository-redis 模块中新建 RedisChatMemoryRepository
类文件,并实现 ChatMemoryRepository
接口,完成聊天消息的存储、查询与删除,一个非常简单的增删改逻辑,重写方法如下:
根据会话 ID 删除消息
直接调用 redisTemplate.delete
方法删除 Redis 中对应键的数据,从而实现聊天记忆中会话消息的删除。
java
@Override
public void deleteByConversationId(String conversationId) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
String key = config.getKeyPrefix() + conversationId;
redisTemplate.delete(key);
}
保存消息
调用 deleteByConversationId
方法删除该会话原有的所有消息。使用 redisTemplate 的opsForList().rightPushAll
方法将这些 JSON 字符串以列表格式有序存入。若配置了过期时间,为该键设置对应的过期时长,实现自动清理不再使用的会话数据。
java
@Override
public void saveAll(String conversationId, List<Message> messages) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
Assert.notNull(messages, "messages cannot be null");
Assert.noNullElements(messages, "messages cannot contain null elements");
String key = config.getKeyPrefix() + conversationId;
deleteByConversationId(conversationId);
List<String> messageJsons = messages.stream()
.map(message -> {
try {
message.getMetadata().put("timestamp", Instant.now().toString());
return objectMapper.writeValueAsString(message);
} catch (JsonProcessingException e) {
throw new RuntimeException("Error serializing message", e);
}
})
.toList();
redisTemplate.opsForList().rightPushAll(key, messageJsons);
if (config.getTimeToLive() > 0) {
redisTemplate.expire(key, config.getTimeToLive(), TimeUnit.SECONDS);
}
}
根据会话 ID 查询消息
根据传入的会话 ID 获取该键对应的所有消息字符串,并反序列化为 Message
对象,最终组成消息列表返回。
java
@Override
public List<Message> findByConversationId(String conversationId) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
String key = config.getKeyPrefix() + conversationId;
List<String> messageStrings = redisTemplate.opsForList().range(key, 0, -1);
if (messageStrings == null) {
logger.debug("No messages found for conversationId: " + conversationId);
return List.of();
}
List<Message> messages = new ArrayList<>();
for (String messageString : messageStrings) {
try {
JsonNode jsonNode = objectMapper.readTree(messageString);
messages.add(getMessage(jsonNode));
} catch (JsonProcessingException e) {
throw new RuntimeException("Error deserializing message", e);
}
}
return messages;
}
检索会话 ID
通过 ScanOptions
扫描所有匹配特定前缀的键,避免使用 KEYS
命令可能导致的阻塞风险。在遍历扫描结果时,对键进行分割处理,提取出会话 ID,最终返回包含所有会话 ID 的列表。
java
@Override
public List<String> findConversationIds() {
return redisTemplate.execute((RedisCallback<List<String>>) connection -> {
var keys = new HashSet<String>();
ScanOptions options =
ScanOptions.scanOptions()
.match(String.format("*%s*", config.getKeyPrefix()))
.count(Integer.MAX_VALUE)
.build();
try (Cursor<byte[]> cursor = connection.keyCommands().scan(options)) {
while (cursor.hasNext()) {
String[] key = new String(cursor.next(), StandardCharsets.UTF_8).split(":");
if (key.length > 0) {
keys.add(key[key.length - 1]);
}
}
}
return new ArrayList<>(keys);
});
}
通过自定义 RedisChatMemoryRepository
,我们已成功将 Redis 融入 Spring AI 的聊天记忆存储体系,实现了基于 Redis 的聊天记忆外部存储。
RedisChatMemoryRepository的自动配置实现
自动配置
在 spring-ai-autoconfigure-model-chat-memory-repository-redis 模块中新建 RedisChatMemoryRepositoryAutoConfiguration
类文件,利用 Spring 的自动配置机制,通过注解驱动的方式,实现 Redis 聊天记忆存储类的自动配置集成。
这里我们默认使用 Lettuce 作为 Redis 客户端,选择默认注入 "stringRedisTemplate" 模板类对象:
java
// 需确保在 RedisAutoConfiguration 配置完成后、ChatMemoryAutoConfiguration 配置前执行
@AutoConfiguration(after = RedisAutoConfiguration.class, before = ChatMemoryAutoConfiguration.class)
@ConditionalOnClass({RedisChatMemoryRepository.class, RedisTemplate.class})
@EnableConfigurationProperties({RedisChatMemoryRepositoryProperties.class})
public class RedisChatMemoryRepositoryAutoConfiguration {
// 设置为默认注入 "stringRedisTemplate" 模板类对象
@Bean
@ConditionalOnMissingBean
public RedisChatMemoryRepository redisChatMemoryRepository(
@Qualifier(RedisChatMemoryRepositoryProperties.DEFAULT_REDIS_TEMPLATE) RedisTemplate<String, String> redisTemplate,
RedisChatMemoryRepositoryProperties properties) {
return RedisChatMemoryRepository.builder()
.keyPrefix(properties.getKeyPrefix())
.timeToLive(properties.getTimeToLive())
.redisTemplate(redisTemplate)
.build();
}
}
Starter模块
最后,提供一个 RedisChatMemoryRepository 的 Starter模块,提供开箱即用的依赖管理。
在模块依赖项中填入我们先前创建好的新模块:
xml
<dependency>
<groupId>org.example</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-memory-repository-redis</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>spring-ai-model-chat-memory-repository-redis</artifactId>
<version>${project.parent.version}</version>
</dependency>
SpringBoot应用与大模型聊天记忆功能的交互、总结序列图如下:
实战案例实现
接下来我们使用MessageWindowChatMemory
+ 自定义RedisChatMemoryRepository
的方式实现一个AI聊天助手的功能。
RedisChatMemoryRepository
是我们自定义的一个 Redis 聊天记忆外部存储,实现是基于 spring-data-redis 模块,我们可通过 RedisTemplate
配置连接 Redis 服务。
1、导入模块
在上篇文章的案例项目中,我们添加自定义的 Starter模块 依赖:
xml
<dependency>
<groupId>org.example</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-redis</artifactId>
<version>${project.parent.version}</version>
</dependency>
2、配置参数
在 application.yml 中,添加配置 spring.ai
和RedisTemplate
连接配置参数:
yaml
spring:
application:
name: AI Assistant
ai:
openai:
api-key: ${OPENAI_API_KEY:NONE}
base-url: ${OPENAI_BASE_URL:NONE}
chat:
options:
model: DeepSeek-V3
completions-path: /v1/chat/completions
chat:
memory:
repository:
redis:
key-prefix: "my_chat_memory:"
time-to-live: "7d"
data:
redis:
host: "localhost"
port: 6379
database: 0
# username:
# password:
connect-timeout: 30s
timeout: 30s
lettuce:
pool:
min-idle: 0
max-idle: 10
max-active: 20
max-wait: -1ms
3、创建 Bean 实例
我们在文章上述实现中为 RedisChatMemoryRepository
提供了自动配置,可直接在应用中使用。
java
@Configuration
public class GeneralChatClientConfig {
private final RedisChatMemoryRepository redisChatMemoryRepository;
public GeneralChatClientConfig(RedisChatMemoryRepository redisChatMemoryRepository) {
this.redisChatMemoryRepository = redisChatMemoryRepository;
}
@Bean(name = "messageWindowChatMemoryWithRedis")
public MessageWindowChatMemory messageWindowChatMemoryWithRedis() {
int maxMessages = 20;
return MessageWindowChatMemory.builder()
.chatMemoryRepository(redisChatMemoryRepository) // default: new InMemoryChatMemoryRepository()
.maxMessages(maxMessages)
.build();
}
}
4、聊天记忆客户端
结合 MessageWindowChatMemory
与 PromptChatMemoryAdvisor
方式创建新的 ChatClient,配置如下:
java
@Bean(name = "deepseekV3ClientWithRedis")
public ChatClient deepseekV3ClientWithRedis(
@Value("${spring.ai.openai.base-url}") String baseUrl,
@Value("${spring.ai.openai.chat.options.model}") String modelName,
@Value("${spring.ai.openai.api-key}") String apiKey,
@Qualifier("messageWindowChatMemoryWithRedis") MessageWindowChatMemory messageWindowChatMemoryWithRedis) {
OpenAiApi build = OpenAiApi.builder().apiKey(apiKey).baseUrl(baseUrl).build();
OpenAiChatModel openAiChatModel =
OpenAiChatModel.builder()
.openAiApi(build)
.defaultOptions(OpenAiChatOptions.builder().model(modelName).build())
.build();
return ChatClient.builder(openAiChatModel)
.defaultAdvisors(PromptChatMemoryAdvisor.builder(messageWindowChatMemoryWithRedis).build())
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
调用 ChatClient
时,ChatMemoryAdvisor
将自动管理记忆存储。系统会根据指定的会话 ID 从记忆库检索历史对话。
我们在已有的 Controller 中添加一个 Redis 外部存储的 ChatClient 方法,注入配置好的 ChatClient,定义一个api实现大模型的 chat 调用,代码如下:
java
@RestController
@RequestMapping("/ai/v3/chat/")
@Slf4j
public class GeneralChatController {
private final ChatClient redisChatClient;
public GeneralChatController(@Qualifier("deepseekV3ClientWithRedis") ChatClient redisChatClient) {
this.redisChatClient = redisChatClient;
}
@PostMapping(value = "/t2", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
@ResponseBody
public Object chatWithRedis(@RequestBody String body) {
JSONObject entries = JSONUtil.parseObj(body);
String text = entries.getStr("text");
Boolean stream = entries.getBool("stream", false);
String conversationId = entries.getStr("conversationId");
log.info("开始对话聊天,会话ID:{}", conversationId);
var request =
redisChatClient
.prompt()
.system("你是乐观小王,回答问题简练精要。")
.advisors(advisor -> advisor.param(CONVERSATION_ID, conversationId))
.advisors(new SimpleLoggerAdvisor())
.user(text);
try {
log.info("开始生成回答,是否流式输出:{}", stream);
return stream ? request.stream().content() : request.call().content();
} catch (Exception e) {
log.error("对话聊天发生异常,会话ID:{}", conversationId, e);
throw e;
}
}
}
5、测试AI聊天功能
通过上述的简单配置和代码编写,我们已经实现了一个通用的带有记忆功能的AI聊天助手。
接下来让我们测试一下这个聊天助手的使用效果,设置几条 USER 的提问内容,具体内容如下:
http
# Openai Chat API
### 我的名字叫小明,你叫什么名字?
### 我是谁?
### 我们之间的第一句问话是什么?
POST http://localhost:10001/ai/v3/chat/t1
Content-Type: application/json
{
"text": "我的名字叫小明,你叫什么名字?",
"stream": false,
"conversationId": "bnA9f525-l7ae-5c66-ae21-vh53547c96cf"
}
聊天消息记录会被存储到 Redis 中,http顺序请求后的大模型返回结果 如图所示:

小总结
在探索 Spring AI 聊天记忆功能的过程中,我们自定义了基于 Redis 外部存储的实现。通过实现ChatMemoryRepository
抽象层,我们能够将会话数据存储到 Redis 中。
不过,当前的实现并非尽善尽美。对于ToolMessage
以及media
媒体文件等问答形式,我们的自定义实现尚未提供支持。从官方的开发动态和社区讨论来看,新版本的 Spring AI 有望带来对 Redis 外部存储更为完善的实现。