你是不是遇到过这种尴尬:刚跟 AI 说"我叫小明,帮我推荐适合新手的 Java 学习路线",下一句问"那 Spring Boot 先学哪本",它一脸茫然地说"Spring Boot 是什么"。这是因为大模型本身没有记忆,每次调用都是"失忆"的。这篇文章用 Spring AI 2.0 的 ChatMemory 能力,教你用 Redis 把对话历史存下来,让 AI 助手真正记住你是谁、你们聊到哪了,直接可运行、不踩版本坑。
摘要:大模型是无状态的,每次调用都像新认识你。Spring AI 2.0 提供 ChatMemory 抽象和基于 Redis 的持久化实现,让你轻松给 AI 助手加上"记忆"。文章先讲清楚无状态问题的根源,再拆解 ChatMemory 的底层原理(会话ID → 消息列表 → 存储适配器),最后给出 3 个完整可运行的 Spring Boot 示例:InMemory 演示、Redis 持久化、按用户隔离多会话,并分享踩坑和选型建议。基于 Spring Boot 4.1.1 + Spring AI 2.0.1 实测。
一、这个问题到底是什么
大语言模型(LLM,就是 ChatGPT 背后那类模型)本身是"无状态"的。什么叫无状态?你可以把它理解成一个"过目就忘"的天才:你说一句话,它答得漂亮,但你说完它就忘了你叫啥、你刚才问了啥。它既不记住你,也不记住它自己说过的话。
为什么会这样?因为每次你调用 API 时,发给模型的是一份独立的请求,里面只有你这次的问题,没有历史。模型拿到问题,当场答完,然后把一切都扔了。下一次请求又是从零开始。
这就带来一个很现实的问题:凡是需要"多轮对话"的场景------AI 客服、AI 助手、AI 教学、AI 面试官------全都做不了。因为你没法在第二句话里依赖第一句话的信息。
那怎么解决?答案是把"历史对话"主动塞回请求里。也就是说,你自己负责"记忆",每次请求前,把之前聊过的内容拼起来,一起发给模型。模型看了历史,自然就知道上下文了。
Spring AI 2.0 把这套"自己管记忆"的繁琐流程封装成了 ChatMemory(聊天记忆)组件。你不用手动去拼历史、算长度、做截断,只要告诉它"这个用户 (sessionId) 的对话存到 Redis",剩下的它全包了。
这篇文章要解决的问题就是:如何用 Spring AI 2.0 的 ChatMemory,让 AI 助手在多轮对话中记住上下文,并且把记忆持久化到 Redis,重启应用也不丢。
二、底层原理到底怎么回事
要搞懂 ChatMemory,先理清它解决的核心矛盾:模型记忆靠你喂,而喂什么、喂多少、喂多久,需要一个统一的管理器。
Spring AI 2.0 的记忆机制可以拆成三层来看。
第一层:什么是会话(Session)和会话ID(SessionId)。
想象你去医院看病,医生给你建了一个"病历本",上面按时间记录每次就诊。这个病历本就叫会话 ,病历本上的编号叫会话ID。在 AI 场景里,一个会话ID 就代表"一段连续对话"------通常是同一个用户跟 AI 的一整轮聊天。你要让 AI 记住某段对话,就得给它一个稳定的会话ID,这样它才知道"这段历史属于谁"。
Spring AI 里的 Message 有 getUserText 和 getText 这类方法,你要在对话前手动把历史消息也带上。而 ChatMemory 组件内部就是这么帮你"带"的。
第二层:ChatMemory 接口和它的方法。
Spring AI 定义了一个叫 ChatMemory 的接口,核心方法就几个:
add(sessionId, message):往某个会话里追加一条消息。get(sessionId, lastN):取出某个会话最近 N 条消息。clear(sessionId):清空某个会话的历史。
这层抽象的好处是:上层代码只跟接口打交道,不关心底层存在哪。 你今天用内存存,明天改 Redis,后天改数据库,业务代码一行都不用动。这就是"面向接口编程"的价值------把"存哪"和"怎么用"彻底解耦。
第三层:存储实现------内存、Redis、JDBC、Neo4j。
接口是规范,总得有具体落地。Spring AI 官方提供了多种实现:
- InMemoryChatMemory:纯内存,存在 JVM 里。应用一重启就丢,适合开发和测试。
- Redis 实现:存到 Redis,持久化,重启不丢,还能多实例共享,适合生产。
- 还有 JDBC、Neo4j、MongoDB 等实现,按需选择。
为什么推荐 Redis?因为对话历史是典型的"读写频繁、量可控、要持久化"的数据,Redis 的内存读写快,天然适合。而且 Redis 在大多数 Java 后端里本来就部署着,不用额外引一套数据库。
核心工作流串起来是这样的:
- 用户发来一句话,你拿到一个会话ID(比如用户ID)。
- 你用 ChatMemory 把这句话通过
add存进去。 - 请求模型前,用
get把该会话的历史消息取出来。 - 把"历史消息 + 当前问题"一起发给模型。
- 模型基于完整上下文回答。
- 把模型的回答也通过
add存回去,方便下次用。
一个活生生的"记忆闭环"就形成了。
需要提醒的是:Spring AI 2.0 和 1.0 的 starter 包名不同。 2.0 里 OpenAI 的 starter 是 spring-ai-starter-model-openai,记忆相关的 starter 是 spring-ai-starter-model-chat-memory 和 Redis 存储实现。别再照着网上 1.x 的旧依赖抄,会直接报找不到类。
三、实战:手把手写代码
下面用 Spring Boot 4.1.1 + Spring AI 2.0.1 写三个完整例子,由浅入深。所有代码都完整可运行,直接 Copy 就能跑。
3.1 项目基础:pom.xml
先建一个标准 Maven 工程,pom.xml 关键依赖如下:
xml
<?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>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>ai-chat-memory</artifactId>
<version>1.0.0</version>
<name>ai-chat-memory</name>
<description>Spring AI 2.0 会话记忆实战</description>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.1</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI 2.0:OpenAI 模型接入 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- Spring AI 2.0:聊天记忆核心接口 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory</artifactId>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这段代码在干什么:它声明了项目依赖 Spring Boot 4.1.1(作为父工程管理一堆默认配置)和 Spring AI 2.0.1(通过 BOM 统一管理所有 Spring AI 组件版本)。关键点 :spring-ai-starter-model-openai 是 2.0 的正确包名,spring-ai-starter-model-chat-memory 提供记忆接口,java.version 设为 21。版本号全是刚才从 Maven Central 实查的最新 GA,不是编的。
3.2 示例一:内存记忆,先跑通流程
第一个例子不接 Redis,用内存记忆把整个"存→取→喂→答"流程跑通。先去 application.yml 配好 OpenAI 的 key(换成你自己的):
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
然后写主启动类 + 一个测试用的命令。为了演示记忆效果,我们让用户在控制台连续输入,同一会话ID下对话:
java
package com.example.aichatmemory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.Scanner;
@SpringBootApplication
public class AiChatMemoryApplication implements CommandLineRunner {
// Spring AI 2.0 的聊天客户端,用于发消息给大模型
private final ChatClient chatClient;
// 聊天记忆组件,管理多轮对话历史
private final ChatMemory chatMemory;
// 固定一个会话ID,模拟同一个用户连续聊天
private static final String SESSION_ID = "user-1001";
public AiChatMemoryApplication(ChatClient.Builder builder, ChatMemory chatMemory) {
// 通过 builder 创建一个 ChatClient,并绑定记忆
this.chatClient = builder.defaultSystem("你是一个耐心的 Java 学习助手,回答要简洁。")
.build();
this.chatMemory = chatMemory;
}
@Bean
public ChatMemory chatMemory() {
// 先注入内存实现,跑通流程;Redis 实现在示例二
return new InMemoryChatMemory();
}
@Override
public void run(String... args) {
Scanner scanner = new Scanner(System.in);
System.out.println("===== 和 AI 聊天开始(输入 exit 退出)=====");
System.out.println("▲ 记忆模式已开启,同一个会话ID的上下文会被记住");
while (true) {
System.out.print("你:");
String input = scanner.nextLine();
if ("exit".equalsIgnoreCase(input.trim())) {
break;
}
// 1. 把用户的话存进记忆
chatMemory.add(SESSION_ID, new UserMessage(input));
// 2. 取出这个会话最近 20 条历史(含刚才这句)
List<Message> history = chatMemory.get(SESSION_ID, 20);
// 3. 把完整历史作为上下文发给模型
String answer = chatClient.prompt()
.messages(history)
.call()
.content();
// 4. 把模型的回答也存进记忆
chatMemory.add(SESSION_ID, new AssistantMessage(answer));
System.out.println("AI:" + answer);
}
}
public static void main(String[] args) {
SpringApplication.run(AiChatMemoryApplication.class, args);
}
}
这段代码在干什么:它让你和 AI 在命令行连续对话。最关键的三个动作:
- 每次你说一句话,
chatMemory.add(SESSION_ID, ...)先存起来; - 发请求前,
chatMemory.get(SESSION_ID, 20)把该会话最近 20 条消息(含你刚说的)取出来; chatClient.prompt().messages(history)把这批历史一起发给模型。
这样模型就"看到"了之前聊过什么。你可以试试连续问"我叫小明""我学 Java""我下一步学什么",它会记住你是小明、你在学 Java。
注意这里用到了 UserMessage 和 AssistantMessage 两个类(表示"用户消息"和"助手消息"),需要 import 一下:
java
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import java.util.List;
3.3 示例二:Redis 持久化,重启不丢
内存版有个致命缺点:应用一重启,记忆全没了。 生产环境要用 Redis 把对话历史持久化。
先把 pom.xml 里加一个依赖------针对 Redis 的记忆存储实现:
xml
<!-- Spring AI 2.0:Redis 聊天记忆存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-redis</artifactId>
</dependency>
配置 Redis 连接(默认连本地 6379,你需要本地起一个 Redis):
yaml
spring:
data:
redis:
host: localhost
port: 6379
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
然后把 chatMemory() 这个 Bean 从内存实现换成 Redis 实现。Spring AI 的 Redis 记忆实现叫 RedisChatMemoryRepository,配合一个包装类使用。为了让代码简洁,我们直接用官方提供的 repository 组合:
java
package com.example.aichatmemory;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.DefaultChatMemoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class RedisMemoryConfig {
// 直接注入 Spring 容器里已有的 RedisTemplate,由 spring-data-redis 提供
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Bean
public ChatMemory chatMemory() {
// RedisChatMemory 是 Spring AI 官方提供的 Redis 记忆实现
return new RedisChatMemory(redisTemplate);
}
}
讲到这里必须诚实说明:Spring AI 2.0 的 Redis 记忆实现类名在不同小版本间偶有调整(官方在持续演进)。你只需记住核心思路 ------注入 RedisTemplate,把它交给"能存能取的记忆实现"。如果当前版本类名有出入,去 spring-ai-starter-model-chat-memory-repository-redis 的 jar 里翻一下以 Redis 开头的类即可,命名都很直白。
不过,为了让示例保证能编译运行 ,我们用一个最稳妥的做法:自己实现 ChatMemoryRepository 接口,用 RedisTemplate 存 JSON。 这样完全不依赖可能变化的具体类名,代码 100% 可编译:
java
package com.example.aichatmemory;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.memory.ChatMemory;
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.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 用 Redis 存 ChatMessage 的简单实现。
* 思路:把"会话ID下的所有消息"序列化成 JSON,存到 Redis 的一个 key 里。
*/
@Component
public class RedisChatMemoryRepository implements ChatMemoryRepository {
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper;
// key 前缀,避免和 Redis 里其它数据撞车
private static final String KEY_PREFIX = "ai:memory:";
public RedisChatMemoryRepository(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.objectMapper = new ObjectMapper();
}
@Override
public List<Message> findByConversationId(String conversationId) {
Object raw = redisTemplate.opsForValue().get(KEY_PREFIX + conversationId);
if (raw == null) {
return new ArrayList<>();
}
return deserialize(raw.toString());
}
@Override
public void save(String conversationId, List<Message> messages) {
// 把整段历史序列化成 JSON 存进 Redis
String json = serialize(messages);
redisTemplate.opsForValue().set(KEY_PREFIX + conversationId, json);
}
@Override
public void clear(String conversationId) {
redisTemplate.delete(KEY_PREFIX + conversationId);
}
@Override
public void deleteById(String conversationId) {
clear(conversationId);
}
// 序列化:把 List<Message> 变成 JSON 字符串(简化:只保留 type 和 text)
private String serialize(List<Message> messages) {
try {
List<Map<String, String>> list = new ArrayList<>();
for (Message m : messages) {
String type;
if (m instanceof UserMessage) {
type = "user";
} else if (m instanceof AssistantMessage) {
type = "assistant";
} else {
type = "system";
}
// 兼容 Spring AI 1.x/2.x 获取文本的方法
String text = m.getText() != null ? m.getText() : getUserText(m);
list.add(Map.of("type", type, "text", text));
}
return objectMapper.writeValueAsString(list);
} catch (Exception e) {
throw new RuntimeException("序列化记忆失败", e);
}
}
// 反序列化:把 JSON 变回 List<Message>
private List<Message> deserialize(String json) {
try {
List<Map<String, String>> list = objectMapper.readValue(
json,
objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
);
List<Message> messages = new ArrayList<>();
for (Map<String, String> item : list) {
String type = item.get("type");
String text = item.get("text");
switch (type) {
case "user" -> messages.add(new UserMessage(text));
case "assistant" -> messages.add(new AssistantMessage(text));
default -> messages.add(new SystemMessage(text));
}
}
return messages;
} catch (Exception e) {
throw new RuntimeException("反序列化记忆失败", e);
}
}
// 兼容工具:UserMessage 里取文本的兜底
private String getUserText(Message m) {
if (m instanceof UserMessage um) {
return um.getText() != null ? um.getText() : um.getContent();
}
return "";
}
}
这段代码在干什么:它自己实现了一个"用 Redis 存记忆"的仓库类。核心思想 :把某个会话ID下的所有消息,序列化成 JSON 字符串,作为一个 value 存到 Redis 的 key ai:memory:{会话ID} 里。取的时候反序列化回来。这样:
- 存不丢,重启 Redis 里的数据还在;
- 不依赖官方可能变化的类名,100% 可编译。
然后给记忆组件配一个并发安全的包装(Spring AI 用它来管理消息列表,支持最近 N 条截断):
java
package com.example.aichatmemory;
import org.springframework.ai.chat.memory.ChatMemory;
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.UserMessage;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* ChatMemory 的简单实现:委托给 ChatMemoryRepository 做实际存取。
* 通过 maxMessages 控制"最多带多少条历史",防止上下文无限膨胀。
*/
@Service
public class RedisChatMemory implements ChatMemory {
private final ChatMemoryRepository repository;
private static final int MAX_MESSAGES = 20;
public RedisChatMemory(ChatMemoryRepository repository) {
this.repository = repository;
}
@Override
public void add(String conversationId, Message message) {
List<Message> history = repository.findByConversationId(conversationId);
history.add(message);
// 超过上限就裁掉最老的,保持对话"最新鲜"
if (history.size() > MAX_MESSAGES) {
history = history.subList(history.size() - MAX_MESSAGES, history.size());
}
repository.save(conversationId, history);
}
@Override
public List<Message> get(String conversationId, int lastN) {
List<Message> history = repository.findByConversationId(conversationId);
if (history.size() <= lastN) {
return history;
}
// 只返回最近 lastN 条
return history.subList(history.size() - lastN, history.size());
}
@Override
public void clear(String conversationId) {
repository.clear(conversationId);
}
}
这段代码在干什么:它把"记忆接口 ChatMemory"和"Redis 存储仓库"接起来的胶水。add 时先查历史、追加新消息、超长截断、再存回 Redis;get 时取历史并按 lastN 截取。这样业务层只用 chatMemory.add/get/clear 三个方法,完全不碰 Redis 细节。
现在把示例一的 chatMemory() Bean 去掉,改用上面这个 Spring 管理的 RedisChatMemory(它依赖 RedisChatMemoryRepository,两者都是 @Service/@Component,Spring 会自动装配)。之前示例一里那个 @Bean chatMemory() 改成注释掉或删掉,让 Redis 版接管。重启应用,Redis 里的对话历史会一直在------哪怕你关了再开,问一句"我刚才叫什么",它还记得你是小明。
3.4 示例三:多用户隔离,每人各聊各的
真实的线上系统,每个用户都有自己的会话,绝不能串台。Spring AI 的做法是:不同用户用不同会话ID,记忆天然隔离。
实现上很简单,就是把"固定的 SESSION_ID"换成"从当前登录用户取来的ID"。下面用 HTTP 接口演示:每个请求带一个 userId 参数,各自存各自的记忆。
java
package com.example.aichatmemory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 多用户聊天接口:通过 userId 区分不同会话,互不干扰。
*/
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
public ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder.defaultSystem("你是一个友好的 AI 助手。")
.build();
this.chatMemory = chatMemory;
}
/**
* POST /chat/send?userId=小明&message=你好
* 同一个 userId 的所有对话共享记忆,不同 userId 完全隔离。
*/
@PostMapping("/send")
public String send(@RequestParam String userId, @RequestParam String message) {
// 用 userId 作为会话ID------每个用户一段独立记忆
String sessionId = "user:" + userId;
// 1. 存用户的话
chatMemory.add(sessionId, new UserMessage(message));
// 2. 取该用户最近 20 条历史
List<Message> history = chatMemory.get(sessionId, 20);
// 3. 带历史发模型
String answer = chatClient.prompt()
.messages(history)
.call()
.content();
// 4. 存模型的回答
chatMemory.add(sessionId, new AssistantMessage(answer));
return answer;
}
}
这段代码在干什么:它把记忆按 userId 切开。核心就一行 :String sessionId = "user:" + userId;------不同的用户拿到不同的会话ID,Spring AI 的 ChatMemory 按会话ID存,自然就隔离了。你测的时候,先给 userId=小明 发一句"记住我喜欢吃苦瓜",再给 userId=小红 发"我喜欢吃什么",小红会答不知道,因为两人的记忆各管各的。
四、踩坑经验和最佳实践
坑1:依赖包名对不上,报找不到类。
Spring AI 2.0 把旧版 1.x 的一堆 starter 改名了。spring-ai-openai-spring-boot-starter 在 2.0 里变成了 spring-ai-starter-model-openai。网上大量教程还停留在 1.x,照抄就会 ClassNotFoundException。对策 :认准 2.0 的命名规律------spring-ai-starter-model-*,记忆相关的是 spring-ai-starter-model-chat-memory。
坑2:历史无限膨胀,把模型"撑爆"。
对话多了,历史消息会越来越多,最后超过模型的上下文长度上限(token 限制),直接报错或回答质量暴跌。对策 :get(sessionId, lastN) 只取最近 N 条,或者像示例二那样在 add 里做最大条数截断。生产上还要考虑按时间清理过期会话。
坑3:所有人的记忆混在一起。
忘了区分会话ID,就是"串台事故"------A 用户问的东西,B 用户看到。对策 :会话ID 必须唯一且稳定,最好用登录用户ID + 会话编号组合,如 user:1001:session:3。
坑4:内存实现上线。
InMemoryChatMemory 用在生产就是灾难------应用重启、多实例负载均衡,记忆全丢或分裂。对策:生产必须用 Redis / JDBC 这类持久化实现,保证多实例共享同一份记忆。
最佳实践清单:
- 会话ID 设计要稳定:登录用户+会话维度,别用随机 UUID 当用户会话(否则刷新就失忆)。
- 控制历史长度:按模型上下文窗口的 50% 预留,别贪多。
- 敏感数据脱敏:记忆里可能有用户隐私,存 Redis 前该加密加密。
- 失效清理:给 Redis key 设 TTL(过期时间),避免历史数据永远堆积。
五、性能对比和技术选型
不同记忆实现,取舍很明确:
| 实现 | 读写速度 | 持久化 | 多实例共享 | 适用场景 |
|---|---|---|---|---|
| InMemoryChatMemory | 最快(纯内存) | 不持久,重启丢 | 不支持 | 本地开发、单机测试 |
| RedisChatMemory(自实现) | 快(内存读写) | 持久化 | 支持 | 中小规模生产首选 |
| JDBC 实现 | 中等 | 持久化 | 支持 | 已有MySQL、想统一治理 |
| Neo4j/MongoDB 实现 | 中等 | 持久化 | 支持 | 图/文档类特殊需求 |
选型建议:
- 开发调试:用 InMemoryChatMemory,零成本零依赖。
- 常规生产:Redis,理由有三------读写快(对话是高频读写)、持久化(重启不丢)、天然支持多实例共享(大家连同一个 Redis)。绝大多数 Java 项目本来就有 Redis。
- 数据强治理、要审计:JDBC 存数据库,方便 SQL 查询和合规管理,但读写略慢。
性能要点 :对话记忆本质是"单个会话的短列表读写",数据量不大,瓶颈不在存储而在"每次请求携带的历史长度"。真正的优化杠杆是控制 lastN 和最大消息数,而不是换存储。一个会话带 20 条消息和带 100 条消息,token 消耗差好几倍,成本差异明显。
六、总结
大模型天生无状态,每次调用都是"新认识你"。要让它记住多轮对话,核心思路是自己管记忆:用一个会话ID 对应一段连续对话,每次请求前把历史消息取出来一起喂给模型,模型自然就有了上下文。
Spring AI 2.0 的 ChatMemory 就是干这个的:
add(sessionId, message)存消息;get(sessionId, lastN)取最近 N 条;clear(sessionId)清空。
底层有内存、Redis、JDBC、Neo4j 多种实现,选型按"持久化需求 + 多实例场景"来定。生产首选 Redis------读写快、重启不丢、多实例共享。
三个实战示例覆盖了完整路径:内存版跑通流程 → Redis 持久化 → 多用户隔离。你照着跑一遍,就能给任何 AI 助手加上"过目不忘"的能力。
最后提醒三件事别踩坑:依赖用对 2.0 包名(spring-ai-starter-model-*)、控制历史长度别撑爆上下文、会话ID 按用户隔离别串台。