news 2026/5/1 8:48:45

Spring AI 多轮对话记忆(ChatMemory)保姆级教程:从内存版到 Redis 持久化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 多轮对话记忆(ChatMemory)保姆级教程:从内存版到 Redis 持久化

一、关键点分析

  1. 核心痛点:大模型 API 本身是无状态的,每次请求都是独立的。网页版 ChatGPT 能“记住”是因为在每次请求时把历史消息都塞进了上下文。

  2. 手动实现的麻烦:需要自己维护List<Message>,并且要处理会话 ID、上下文窗口超限、服务重启丢历史等问题。

  3. Spring AI 的解决方案:通过ChatMemory+Advisor自动管理对话历史。

    • MessageWindowChatMemory(内存版,按条数裁剪)

    • MessageChatMemoryAdvisor(注入到ChatClient的调用链中)

  4. 控制消息数量maxMessages参数,权衡记忆长度与 Token 消耗。

  5. 生产级持久化

    • 架构分层:ChatMemoryRepository(纯存储) +MessageWindowChatMemory(包装、裁剪)

    • 以 Redis 为例:实现RedisChatMemoryRepository,注入StringRedisTemplate,存储为 List,设置 TTL。

    • 注册为 Bean 后,Controller 代码与内存版完全一致。

  6. 会话管理:提供clear(conversationId)接口。

  7. Token 预算问题

    • 简单策略:限制保留条数。

    • 精确策略:按字符数/Token 数截断(示例TokenBudgetChatMemory)。

    • 高级策略:摘要压缩(后续 Agent 涉及)。




模型本身是无状态的——每次 API 请求对它来说都是全新的,上一次聊了什么它完全不知道。
“但我在 ChatGPT 网页上聊天,它明明能记住前面的内容啊?”
对,那是因为网页前端每次都把历史记录一起发给了模型,不是模型自己有记忆,而是历史消息被塞进了这次请求的上下文里。
Spring AI 的ChatMemory就是帮你做这件事的——自动管理对话历史,每次发请求时自动带上前面的消息。

二、不用 ChatMemory 时,多轮对话要怎么做

先看看手动实现多轮对话是什么样的:

@RestController @RequestMapping("/manual-chat") public class ManualChatController { private final ChatClient chatClient; // 手动维护每个会话的历史(演示用,生产不推荐) private final Map<String, List<Message>> sessions = new ConcurrentHashMap<>(); public ManualChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @PostMapping public String chat(@RequestBody ChatRequest request) { // 获取或创建该会话的历史 List<Message> history = sessions.computeIfAbsent(request.conversationId(), id -> { List<Message> list = new ArrayList<>(); list.add(new SystemMessage("你是一个 Java 技术助手")); return list; }); // 追加用户消息 history.add(new UserMessage(request.message())); // 带完整历史调用模型 String reply = chatClient.prompt() .messages(history) .call() .content(); // 把模型回复也追加进历史 history.add(new AssistantMessage(reply)); return reply; } record ChatRequest(String conversationId, String message) {} }

能实现,但有明显痛点:

  • 历史列表需要调用方自己维护,接口无状态,每次请求都要传完整历史。

  • 上下文窗口有限,对话一长,总 Token 数超出限制就会报错。

  • 没有持久化,服务重启历史就丢了。

ChatMemory解决的就是这些问题。

三、ChatMemory 基础用法(内存版)

Spring AI 内置了基于AdvisorChatMemory支持。

@RestController @RequestMapping("/memory-chat") public class MemoryChatController { private final ChatClient chatClient; private final MessageWindowChatMemory chatMemory; public MemoryChatController(ChatClient.Builder builder) { // 保留最近 10 条消息 this.chatMemory = MessageWindowChatMemory.builder().maxMessages(10).build(); this.chatClient = builder .defaultSystem("你是一个 Java 技术助手") .build(); } @GetMapping public String chat( @RequestParam String message, @RequestParam(defaultValue = "default") String conversationId) { return chatClient.prompt() .user(message) .advisors(MessageChatMemoryAdvisor.builder(chatMemory) .conversationId(conversationId) .build()) .call() .content(); } }

测试效果:

# 第一轮 curl "http://localhost:8080/memory-chat?message=我叫大王&conversationId=user123" # 模型回复:你好,大王!有什么可以帮你的? # 第二轮(同一个 conversationId) curl "http://localhost:8080/memory-chat?message=你还记得我叫什么吗&conversationId=user123" # 模型回复:记得,你叫大王。 # 换一个 conversationId(新会话,不记得之前的内容) curl "http://localhost:8080/memory-chat?message=你还记得我叫什么吗&conversationId=user456" # 模型回复:抱歉,我不知道你的名字,你可以告诉我吗?
  • MessageWindowChatMemory默认基于内存存储,重启应用后记忆会丢失。生产环境可替换为持久化实现(如 Redis、数据库)。

四、控制保留的消息数量

默认保留最近 20 条消息,你可以自定义:

@RestController @RequestMapping("/long-chat") public class LongChatController { private final ChatClient chatClient; private final MessageWindowChatMemory chatMemory; public LongChatController(ChatClient.Builder builder) { // 保留最近 20 条消息 this.chatMemory = MessageWindowChatMemory.builder().maxMessages(20).build(); this.chatClient = builder .defaultSystem("你是一个 Java 技术助手") .build(); } @GetMapping public String chat(...) { // 与上例完全相同,只是 maxMessages 改了 return chatClient.prompt() .user(message) // 1.1.x 新 API:按 conversationId 构建 Advisor .advisors(MessageChatMemoryAdvisor.builder(chatMemory) .conversationId(conversationId) .build()) .call() .content(); } }

消息数量的权衡:

  • 太少:模型忘得快,早期说的内容就不记得了。

  • 太多:每次发送的 Token 增多,费用上升,超出上下文窗口也会报错。

一般客服/聊天场景保留 10~20 条够用;如果是长文档处理,需要配合 RAG 或用长上下文模型。

五、持久化存储(生产环境必备)

MessageWindowChatMemory只在内存里,服务一重启历史就没了,生产环境不能用。

Spring AI 1.1.x 把存储层和裁剪逻辑拆成了两层:

  • ChatMemoryRepository:纯存储接口,只管读写全量消息,不做任何裁剪。

  • MessageWindowChatMemory:包装Repository,对外暴露ChatMemory,负责按条数裁剪窗口。

4.1 添加依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

4.2 配置 Redis 连接

spring: data: redis: host: localhost port: 6379 database: 0

4.3 自定义 RedisChatMemoryRepository

package com.studying.chatMemory; 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.RedisTemplate; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; public class RedisChatMemoryRepository implements ChatMemoryRepository { private static final String KEY_PREFIX = "chat:memory:"; private static final int TTL_DAYS = 3; private static final ObjectMapper MAPPER = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private final RedisTemplate<String, Object> redisTemplate; public RedisChatMemoryRepository(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public List<String> findConversationIds() { // 实现不推荐在生产环境使用,"KEYS *" 命令可能导致性能问题 Set<String> keys = redisTemplate.keys(KEY_PREFIX + "*"); if (CollectionUtils.isEmpty(keys)) { return new ArrayList<>(); } return keys.stream() .map(key -> key.substring(KEY_PREFIX.length())) .toList(); } @Override public List<Message> findByConversationId(String conversationId) { String key = KEY_PREFIX + conversationId; List<Object> rawMessages = redisTemplate.opsForList().range(key, 0, -1); if (CollectionUtils.isEmpty(rawMessages)) { return new ArrayList<>(); } List<Message> messages = new ArrayList<>(); rawMessages.stream() .forEach(raw -> { MessageRecord record = MAPPER.convertValue(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())); } }); return messages; } @Override public void saveAll(String conversationId, List<Message> messages) { String key = KEY_PREFIX + conversationId; // 先清除旧数据,再全量写入,避免重复追加 redisTemplate.delete(key); List<MessageRecord> recordList = messages.stream() .map(msg -> new MessageRecord(msg.getMessageType().getValue(), msg.getText())) .toList(); redisTemplate.opsForList().rightPushAll(key, recordList.toArray()); redisTemplate.expire(key, TTL_DAYS, TimeUnit.DAYS); } @Override public void deleteByConversationId(String conversationId) { redisTemplate.delete(KEY_PREFIX + conversationId); } record MessageRecord(String role, String content) { } }

4.4 注册 ChatMemory Bean

@Configuration public class ChatMemoryConfig { @Bean public ChatMemory chatMemory(RedisTemplate<String, Object> redisTemplate) { RedisChatMemoryRepository repository = new RedisChatMemoryRepository(redisTemplate); // 底层走 Redis 持久化,上层限制最多保留 20 条消息 return MessageWindowChatMemory.builder() .chatMemoryRepository(repository) .maxMessages(20) .build(); } }

4.5 Controller 使用(与内存版完全一样)

@RestController @RequestMapping("/redis-chat") public class RedisChatController { private final ChatClient chatClient; private final ChatMemory chatMemory; public RedisChatController(ChatClient.Builder builder, ChatMemory chatMemory) { this.chatMemory = chatMemory; this.chatClient = builder .defaultSystem("你是一个 Java 技术助手") .build(); } @GetMapping public String chat( @RequestParam String message, @RequestParam(defaultValue = "default") String conversationId) { return chatClient.prompt() .user(message) .advisors(MessageChatMemoryAdvisor.builder(chatMemory) .conversationId(conversationId) .build()) .call() .content(); } }

测试效果:服务重启后历史依然保留(数据存在 Redis 里)

# 第一轮 curl "http://localhost:8080/redis-chat?message=我叫大王&conversationId=user123" # 重启服务后再发第二轮 curl "http://localhost:8080/redis-chat?message=你还记得我叫什么吗&conversationId=user123" # 模型回复:记得,你叫大王。

六、会话管理:清除历史

用户退出登录、开启新对话时,需要清除历史:

@RestController @RequestMapping("/session") public class SessionController { private final ChatMemory chatMemory; public SessionController(ChatMemory chatMemory) { this.chatMemory = chatMemory; } @DeleteMapping("/{conversationId}") public void clearHistory(@PathVariable String conversationId) { chatMemory.clear(conversationId); } }

七、完整的多轮对话 Controller

@RestController @RequestMapping("/api/conversation") public class ConversationController { private final ChatClient chatClient; private final ChatMemory chatMemory; public ConversationController(ChatClient.Builder builder, ChatMemory chatMemory) { this.chatMemory = chatMemory; this.chatClient = builder .defaultSystem(""" 你是一个智能助手。 记住用户告诉你的所有信息,在后续对话中灵活运用。 回答简洁,除非用户要求详细解释。 """) .build(); } @PostMapping("/message") public MessageResponse sendMessage(@RequestBody MessageRequest request) { String reply = chatClient.prompt() .user(request.message()) .advisors(MessageChatMemoryAdvisor.builder(chatMemory) .conversationId(request.conversationId()) .build()) .call() .content(); return new MessageResponse(reply, request.conversationId()); } @DeleteMapping("/{conversationId}") public void clearConversation(@PathVariable String conversationId) { chatMemory.clear(conversationId); } record MessageRequest(String conversationId, String message) {} record MessageResponse(String reply, String conversationId) {} }

八、上下文窗口和 Token 预算

多轮对话最容易踩的坑是Token 超限。每个模型都有最大上下文长度限制(比如 DeepSeek-V3 是 128K Token)。历史消息越多,每次请求的 Token 数就越多。

策略一:限制保留消息数(最简单,已介绍)

策略二:按 Token 数限制(更精确)

实现一个按 Token 预算裁剪的ChatMemory

public class TokenBudgetChatMemory implements ChatMemory { private static final int CHARS_PER_TOKEN = 4; // 粗估:4个字符≈1 Token private final int maxTokenBudget; private final ConcurrentHashMap<String, List<Message>> store = new ConcurrentHashMap<>(); public TokenBudgetChatMemory(int maxTokenBudget) { this.maxTokenBudget = maxTokenBudget; } @Override public void add(String conversationId, List<Message> messages) { store.computeIfAbsent(conversationId, k -> new ArrayList<>()).addAll(messages); } @Override public List<Message> get(String conversationId) { List<Message> all = store.getOrDefault(conversationId, List.of()); if (all.isEmpty()) return List.of(); List<Message> result = new ArrayList<>(); int tokenCount = 0; for (int i = all.size() - 1; i >= 0; i--) { int msgTokens = all.get(i).getText().length() / CHARS_PER_TOKEN; if (tokenCount + msgTokens > maxTokenBudget) break; result.add(all.get(i)); tokenCount += msgTokens; } Collections.reverse(result); return result; } @Override public void clear(String conversationId) { store.remove(conversationId); } }

使用:

ChatMemory tokenBudgetMemory = new TokenBudgetChatMemory(2000); // 预算 2000 Token

策略三:摘要压缩(高级)

定期把历史消息压缩成摘要,用摘要替代原始历史,大幅减少 Token 占用。这个方案比较复杂,适合长期对话场景,后续 Agent 模块会涉及。

八、总结

  • 模型无状态,多轮对话需要自行管理历史消息。

  • Spring AI 的ChatMemory通过Advisor机制自动帮你做这件事。

  • 内存版切换到Redis 持久化只需要实现ChatMemoryRepository并注入MessageWindowChatMemoryBean,Controller 代码零更改。

  • 注意控制消息数量Token 预算,防止超出模型上下文窗口。

  • 提供clear接口让用户能够主动清除对话历史。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 8:39:52

fVDB框架解析:3D空间智能的深度学习革命

1. 从现实世界3D数据构建空间智能的深度学习框架fVDB解析在构建能够理解和操作物理世界的生成式AI时&#xff0c;空间智能是最关键的瓶颈之一。想象一下&#xff0c;要让AI像人类一样在三维空间中自如地导航、交互和创造&#xff0c;首先需要解决的是如何让机器"看见"…

作者头像 李华
网站建设 2026/5/1 8:39:33

番茄小说下载器:3种界面模式与跨平台部署完整指南

番茄小说下载器&#xff1a;3种界面模式与跨平台部署完整指南 【免费下载链接】Tomato-Novel-Downloader 番茄小说下载器不精简版 项目地址: https://gitcode.com/gh_mirrors/to/Tomato-Novel-Downloader 番茄小说下载器是一款功能强大的Rust开源工具&#xff0c;专门用…

作者头像 李华
网站建设 2026/5/1 8:38:27

AI智能体工作流引擎:从零构建多智能体协同系统

1. 项目概述&#xff1a;从零构建一个AI智能体工作流引擎 最近在开源社区里&#xff0c; aiagentflow/aiagentflow 这个项目引起了我的注意。乍一看这个名字&#xff0c;你可能会觉得它又是一个跟风大模型的玩具项目&#xff0c;但当我真正深入去研究它的代码结构和设计理念时…

作者头像 李华
网站建设 2026/5/1 8:35:44

D2VLM:视频语言模型的分解学习框架解析

1. 项目背景与核心价值视频理解领域正面临一个关键挑战&#xff1a;如何让AI系统像人类一样理解动态视觉内容中的时序关系和语义信息。传统视频语言模型通常将视频视为整体进行处理&#xff0c;忽略了视频内容本身具有的多层次时空分解特性。D2VLM&#xff08;Decomposed Video…

作者头像 李华
网站建设 2026/5/1 8:35:26

基于微信小程序的校园失物招领管理系统【uniapp+springboot+vue】

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

作者头像 李华