通过测试用例掌握 ChatMemory 和 MessageChatMemoryAdvisor
本章通过 6 个完整的测试用例,带你掌握 Spring AI 的对话记忆机制。
你将学会如何使用 ChatMemory 接口和 MessageChatMemoryAdvisor 实现自动化的会话管理。
InMemoryChatMemoryMessageChatMemoryAdvisor 的自动记忆管理conversationId 实现会话隔离确保你已经:
创建文件:src/test/java/com/example/chatbot/knowledge/Test03_ChatMemory.java
package com.example.chatbot.knowledge;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.messages.Message;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;
/**
* 测试类:Spring AI 对话记忆机制
*
* 本测试类演示如何使用 ChatMemory 和 MessageChatMemoryAdvisor
*/
@SpringBootTest(classes = {
com.example.chatbot.AiChatbotApplication.class,
Test03_ChatMemory.TestConfiguration.class
})
@DisplayName("知识点3:对话记忆机制")
public class Test03_ChatMemory {
@Autowired
private ChatModel chatModel;
@Autowired
private ChatMemory chatMemory;
@Autowired
private ChatClient chatClient;
/**
* 测试配置类
* 配置 ChatMemory 和 ChatClient
*/
@Configuration
static class TestConfiguration {
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
@Bean
public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
return ChatClient.builder(chatModel)
.defaultAdvisors(
new org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor(chatMemory)
)
.build();
}
}
// 测试方法将在这里添加
}
我们在测试类中定义了一个内部配置类 TestConfiguration,
用于配置 ChatMemory 和 ChatClient,这样测试更加独立和清晰。
首先验证 AI 本身没有记忆,每次调用都是独立的。
/**
* 测试 3.1:验证 AI 默认没有记忆
*
* 演示:不使用 ChatMemory 时,AI 无法记住之前的对话
*/
@Test
@DisplayName("3.1 验证 AI 默认没有记忆")
public void test01_NoMemoryByDefault() {
System.out.println("\n========== 测试 3.1:AI 默认没有记忆 ==========");
// 第一次对话
String response1 = chatModel.call("我叫张三");
System.out.println("👤 第一次:我叫张三");
System.out.println("🤖 AI:" + response1);
// 第二次对话(独立调用)
String response2 = chatModel.call("我叫什么名字?");
System.out.println("\n👤 第二次:我叫什么名字?");
System.out.println("🤖 AI:" + response2);
System.out.println("\n💡 观察:AI 不记得之前的对话!");
System.out.println("✅ 测试通过");
}mvn test -Dtest=Test03_ChatMemory#test01_NoMemoryByDefault========== 测试 3.1:AI 默认没有记忆 ==========
👤 第一次:我叫张三
🤖 AI:你好,张三!很高兴认识你...
👤 第二次:我叫什么名字?
🤖 AI:抱歉,我不知道您的名字...
💡 观察:AI 不记得之前的对话!
✅ 测试通过call() 都是独立的学习如何配置 ChatMemory Bean,这是实现记忆功能的第一步。
/**
* 测试 3.2:配置 ChatMemory Bean
*
* 验证 ChatMemory 已正确注入
*/
@Test
@DisplayName("3.2 配置 ChatMemory Bean")
public void test02_ConfigureChatMemory() {
System.out.println("\n========== 测试 3.2:配置 ChatMemory Bean ==========");
// 验证 ChatMemory 已注入
assertNotNull(chatMemory, "ChatMemory 应该被正确注入");
System.out.println("✅ ChatMemory 已注入:" + chatMemory.getClass().getSimpleName());
// 验证是 InMemoryChatMemory 实现
assertTrue(chatMemory instanceof InMemoryChatMemory,
"应该是 InMemoryChatMemory 实现");
System.out.println("✅ 使用的是 InMemoryChatMemory 实现");
System.out.println("\n💡 ChatMemory 配置成功!");
System.out.println("✅ 测试通过");
}mvn test -Dtest=Test03_ChatMemory#test02_ConfigureChatMemory@Configuration
static class TestConfiguration {
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory(); // 使用内存存储
}
}ChatMemory 提供了以下核心方法:
add(conversationId, message) - 添加消息get(conversationId, lastN) - 获取历史clear(conversationId) - 清空历史
使用 MessageChatMemoryAdvisor 后,ChatClient 会自动管理对话历史,
无需手动添加 UserMessage 和 AssistantMessage。
/**
* 测试 3.3:使用 MessageChatMemoryAdvisor 实现自动记忆
*
* 演示:配置 Advisor 后,AI 自动拥有记忆功能
*/
@Test
@DisplayName("3.3 使用 MessageChatMemoryAdvisor 实现自动记忆")
public void test03_UseMessageChatMemoryAdvisor() {
System.out.println("\n========== 测试 3.3:MessageChatMemoryAdvisor 自动记忆 ==========");
String sessionId = "test-session-001";
// 第一轮对话
String response1 = chatClient
.prompt("我叫张三")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.call()
.content();
System.out.println("👤 第一轮:我叫张三");
System.out.println("🤖 AI:" + response1);
// 第二轮对话(带上 sessionId)
String response2 = chatClient
.prompt("我叫什么名字?")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.call()
.content();
System.out.println("\n👤 第二轮:我叫什么名字?");
System.out.println("🤖 AI:" + response2);
System.out.println("\n💡 观察:AI 记住了名字!");
System.out.println("📝 原理:MessageChatMemoryAdvisor 自动管理了历史");
System.out.println("✅ 测试通过");
}mvn test -Dtest=Test03_ChatMemory#test03_UseMessageChatMemoryAdvisor========== 测试 3.3:MessageChatMemoryAdvisor 自动记忆 ==========
👤 第一轮:我叫张三
🤖 AI:你好,张三!很高兴认识你...
👤 第二轮:我叫什么名字?
🤖 AI:你叫张三。
💡 观察:AI 记住了名字!
📝 原理:MessageChatMemoryAdvisor 自动管理了历史
✅ 测试通过
通过不同的 conversationId,可以为不同用户维护独立的对话历史。
/**
* 测试 3.4:使用 conversationId 实现会话隔离
*
* 演示:不同 conversationId 的历史互不干扰
*/
@Test
@DisplayName("3.4 使用 conversationId 实现会话隔离")
public void test04_ConversationIdIsolation() {
System.out.println("\n========== 测试 3.4:会话隔离 ==========");
// 会话1:用户A
String sessionA = "user-A";
String responseA1 = chatClient
.prompt("我叫张三")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionA))
.call()
.content();
System.out.println("👤 会话A:我叫张三");
System.out.println("🤖 AI:" + responseA1);
// 会话2:用户B
String sessionB = "user-B";
String responseB1 = chatClient
.prompt("我叫李四")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionB))
.call()
.content();
System.out.println("\n👤 会话B:我叫李四");
System.out.println("🤖 AI:" + responseB1);
// 在会话A中询问
String responseA2 = chatClient
.prompt("我叫什么?")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionA))
.call()
.content();
System.out.println("\n👤 会话A询问:我叫什么?");
System.out.println("🤖 AI:" + responseA2);
// 在会话B中询问
String responseB2 = chatClient
.prompt("我叫什么?")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionB))
.call()
.content();
System.out.println("\n👤 会话B询问:我叫什么?");
System.out.println("🤖 AI:" + responseB2);
System.out.println("\n💡 两个会话的记忆是独立的");
System.out.println("✅ 测试通过");
}mvn test -Dtest=Test03_ChatMemory#test04_ConversationIdIsolation========== 测试 3.4:会话隔离 ==========
👤 会话A:我叫张三
🤖 AI:你好,张三!...
👤 会话B:我叫李四
🤖 AI:你好,李四!...
👤 会话A询问:我叫什么?
🤖 AI:你叫张三。
👤 会话B询问:我叫什么?
🤖 AI:你叫李四。
💡 两个会话的记忆是独立的
✅ 测试通过虽然 MessageChatMemoryAdvisor 会自动管理记忆, 但有时我们需要手动查询某个会话的历史消息。
/**
* 测试 3.5:手动查询会话历史
*
* 演示:使用 ChatMemory.get() 查询历史消息
*/
@Test
@DisplayName("3.5 手动查询会话历史")
public void test05_QueryChatHistory() {
System.out.println("\n========== 测试 3.5:查询会话历史 ==========");
String sessionId = "test-session-002";
// 进行几轮对话
chatClient.prompt("我喜欢吃苹果")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.call().content();
chatClient.prompt("我还喜欢吃香蕉")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.call().content();
// 查询历史
List<Message> messages = chatMemory.get(sessionId, 100);
System.out.println("📝 会话历史(共 " + messages.size() + " 条消息):");
for (int i = 0; i < messages.size(); i++) {
Message msg = messages.get(i);
String role = msg.getMessageType().name();
String content = msg.getText();
System.out.println((i + 1) + ". [" + role + "] " +
(content.length() > 50 ? content.substring(0, 50) + "..." : content));
}
// 验证历史长度
assertEquals(4, messages.size(), "应该有4条消息(2轮对话)");
System.out.println("\n💡 可以通过 chatMemory.get() 查询历史");
System.out.println("✅ 测试通过");
}mvn test -Dtest=Test03_ChatMemory#test05_QueryChatHistory========== 测试 3.5:查询会话历史 ==========
📝 会话历史(共 4 条消息):
1. [USER] 我喜欢吃苹果
2. [ASSISTANT] 苹果是一种非常健康的水果...
3. [USER] 我还喜欢吃香蕉
4. [ASSISTANT] 香蕉也很不错...
💡 可以通过 chatMemory.get() 查询历史
✅ 测试通过提供"清空对话"功能,让用户可以重新开始。
/**
* 测试 3.6:清空会话历史
*
* 演示:使用 ChatMemory.clear() 清空历史
*/
@Test
@DisplayName("3.6 清空会话历史")
public void test06_ClearChatHistory() {
System.out.println("\n========== 测试 3.6:清空会话历史 ==========");
String sessionId = "test-session-003";
// 进行对话
chatClient.prompt("我叫王五")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.call().content();
System.out.println("👤 对话:我叫王五");
// 查询历史
List beforeClear = chatMemory.get(sessionId, 100);
System.out.println("📝 清空前历史长度: " + beforeClear.size());
assertTrue(beforeClear.size() > 0, "清空前应该有历史");
// 清空历史
chatMemory.clear(sessionId);
System.out.println("🗑️ 执行清空操作");
// 再次查询
List afterClear = chatMemory.get(sessionId, 100);
System.out.println("📝 清空后历史长度: " + afterClear.size());
assertEquals(0, afterClear.size(), "清空后应该没有历史");
// 验证 AI 不再记得
String response = chatClient.prompt("我叫什么名字?")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.call().content();
System.out.println("\n👤 清空后询问:我叫什么名字?");
System.out.println("🤖 AI:" + response);
System.out.println("\n💡 清空后 AI 不再记得之前的对话");
System.out.println("✅ 测试通过");
}mvn test -Dtest=Test03_ChatMemory#test06_ClearChatHistory========== 测试 3.6:清空会话历史 ==========
👤 对话:我叫王五
📝 清空前历史长度: 2
🗑️ 执行清空操作
📝 清空后历史长度: 0
👤 清空后询问:我叫什么名字?
🤖 AI:抱歉,我不知道您的名字...
💡 清空后 AI 不再记得之前的对话
✅ 测试通过到这里,你已经通过测试 3.1 ~ 3.6 跑通了“Advisor 自动拼接历史 + conversationId 会话隔离 + 手动查询/清空”的完整闭环。 这一节我们把它上升到“工程化选型”:当你的业务开始关注 成本、性能、隐私合规、多实例 时,ChatMemory 该怎么设计。
本项目以教学为主,页面代码以 InMemoryChatMemory 为主;但你仍然可以基于 ChatMemory 接口自己实现一套 Memory,满足你项目的策略需求。
| Memory Type | 特点 / 适合场景 | 典型用途 |
|---|---|---|
InMemoryChatMemory |
不裁剪:历史越聊越长(适合入门演示) | 教学演示、本地小工具、极小并发 |
Window Memory |
只保留最近 N 条消息,稳定控制 token、延迟和成本 | 客服问答、Copilot、短会话助手 |
Summary Memory |
把较早历史压缩成摘要,再拼接最近若干轮 | 长对话助手、复杂业务流程 |
Persistent Memory |
消息持久化到 Redis / MySQL / PostgreSQL,支持多实例共享 | 生产环境、水平扩容、重启不丢历史 |
Hybrid Memory |
窗口 + 摘要 + 持久化组合,兼顾体验、成本与可恢复性 | 企业级 Agent、复杂多轮业务系统 |
最关键的点是:ChatMemory 是按 conversationId 分区的。 你要保证同一个 conversationId 的消息按顺序追加、按需读取,并且能清空。 下面给出一个“最小可用”的自定义实现示例:用内存 Map 按会话存消息,并实现一个简单的“窗口上限”。
这份 chapter3.html 是教学页面,你可以把下面的代码拷到“演示项目”中执行。
如果你的演示项目是标准 Maven/Spring Boot 结构,我建议:
src/main/java/
com/example/chatbot/
memory/
SimpleWindowChatMemory.java
src/test/java/
com/example/chatbot/
knowledge/
Test03_ChatMemory.java
下面的单测不需要大模型,只依赖 spring-ai-core 里的 ChatMemory/Message/UserMessage 等基础类型。
在演示项目的 pom.xml 增加:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>1.0.0-M6</version>
</dependency>
如果你本地拉不到 1.0.0-M6(里程碑版本),再补一个 milestone 仓库(与 <dependencies> 同级):
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>package com.example.chatbot.memory;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
public class SimpleWindowChatMemory implements ChatMemory {
private final ConcurrentHashMap<String, Deque<Message>> store = new ConcurrentHashMap<>();
private final int maxMessages;
public SimpleWindowChatMemory(int maxMessages) {
this.maxMessages = maxMessages;
}
@Override
public void add(String conversationId, Message message) {
Deque<Message> q = store.computeIfAbsent(conversationId, k -> new ConcurrentLinkedDeque<>());
q.addLast(message);
trimToWindow(q);
}
@Override
public void add(String conversationId, List<Message> messages) {
if (messages == null || messages.isEmpty()) {
return;
}
Deque<Message> q = store.computeIfAbsent(conversationId, k -> new ConcurrentLinkedDeque<>());
for (Message m : messages) {
if (m != null) {
q.addLast(m);
}
}
trimToWindow(q);
}
private void trimToWindow(Deque<Message> q) {
if (maxMessages <= 0) {
q.clear();
return;
}
while (q.size() > maxMessages) {
q.pollFirst();
}
}
@Override
public List<Message> get(String conversationId, int lastN) {
Deque<Message> q = store.get(conversationId);
if (q == null || q.isEmpty()) {
return List.of();
}
int n = Math.min(lastN, q.size());
if (n <= 0) {
return List.of();
}
ArrayList<Message> all = new ArrayList<>(q);
return all.subList(all.size() - n, all.size());
}
@Override
public void clear(String conversationId) {
store.remove(conversationId);
}
}
一旦你有了 ChatMemory 实现,就可以像本章测试 3.3 那样交给 Advisor:
ChatMemory chatMemory = new SimpleWindowChatMemory(20);
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
这一节不再依赖大模型输出,而是直接验证自定义 ChatMemory 实现本身是否可靠。
重点检查 4 个能力:按会话隔离、窗口裁剪、按 lastN 读取、清空历史。
import com.example.chatbot.memory.SimpleWindowChatMemory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
private static void printMessages(String title, List<Message> messages) {
System.out.println(title + "(共 " + messages.size() + " 条)");
for (int i = 0; i < messages.size(); i++) {
System.out.println((i + 1) + ". " + messages.get(i).getText());
}
}
/**
* 测试 3.7:验证自定义窗口记忆可用性
*
* 演示:窗口长度 = 3 时,只保留最近 3 条消息
*/
@Test
@DisplayName("3.7 验证 SimpleWindowChatMemory 可用性")
public void test07_SimpleWindowChatMemory() {
System.out.println("\n========== 测试 3.7:SimpleWindowChatMemory ==========");
SimpleWindowChatMemory memory = new SimpleWindowChatMemory(3);
String sessionId = "window-session-001";
System.out.println("🛠️ 初始化完成:windowSize = 3, sessionId = " + sessionId);
memory.add(sessionId, new UserMessage("消息1:我喜欢苹果"));
printMessages("添加消息1后窗口内容", memory.get(sessionId, 10));
memory.add(sessionId, new UserMessage("消息2:我喜欢香蕉"));
printMessages("添加消息2后窗口内容", memory.get(sessionId, 10));
memory.add(sessionId, new UserMessage("消息3:我喜欢橙子"));
printMessages("添加消息3后窗口内容", memory.get(sessionId, 10));
memory.add(sessionId, new UserMessage("消息4:我喜欢西瓜"));
System.out.println("⚠️ 添加第4条后,应触发窗口裁剪,最早的消息1应被淘汰");
List<Message> messages = memory.get(sessionId, 10);
printMessages("📝 当前窗口内消息", messages);
assertEquals(3, messages.size(), "窗口大小为 3,只应保留最近 3 条消息");
assertEquals("消息2:我喜欢香蕉", messages.get(0).getText());
assertEquals("消息3:我喜欢橙子", messages.get(1).getText());
assertEquals("消息4:我喜欢西瓜", messages.get(2).getText());
System.out.println("✅ 验证通过:消息1 已被裁剪,只保留消息2/3/4");
List<Message> lastTwo = memory.get(sessionId, 2);
printMessages("🔎 查询最后 2 条消息", lastTwo);
assertEquals(2, lastTwo.size(), "按 lastN 查询时应该返回最后 2 条");
assertEquals("消息3:我喜欢橙子", lastTwo.get(0).getText());
assertEquals("消息4:我喜欢西瓜", lastTwo.get(1).getText());
System.out.println("✅ 验证通过:lastN = 2 时,返回消息3/4");
memory.clear(sessionId);
List<Message> afterClear = memory.get(sessionId, 10);
printMessages("🗑️ 清空后窗口内容", afterClear);
assertTrue(afterClear.isEmpty(), "清空后历史应为空");
System.out.println("✅ 窗口裁剪、按需读取、清空历史全部验证通过");
}mvn test -Dtest=Test03_ChatMemory#test07_SimpleWindowChatMemory========== 测试 3.7:SimpleWindowChatMemory ==========
🛠️ 初始化完成:windowSize = 3, sessionId = window-session-001
添加消息1后窗口内容(共 1 条)
1. 消息1:我喜欢苹果
添加消息2后窗口内容(共 2 条)
1. 消息1:我喜欢苹果
2. 消息2:我喜欢香蕉
添加消息3后窗口内容(共 3 条)
1. 消息1:我喜欢苹果
2. 消息2:我喜欢香蕉
3. 消息3:我喜欢橙子
⚠️ 添加第4条后,应触发窗口裁剪,最早的消息1应被淘汰
📝 当前窗口内消息(共 3 条)
1. 消息2:我喜欢香蕉
2. 消息3:我喜欢橙子
3. 消息4:我喜欢西瓜
✅ 验证通过:消息1 已被裁剪,只保留消息2/3/4
🔎 查询最后 2 条消息(共 2 条)
1. 消息3:我喜欢橙子
2. 消息4:我喜欢西瓜
✅ 验证通过:lastN = 2 时,返回消息3/4
🗑️ 清空后窗口内容(共 0 条)
✅ 窗口裁剪、按需读取、清空历史全部验证通过get(sessionId, lastN) 会返回最后 N 条clear() 后历史为空运行完整测试类,验证所有对话记忆功能:
mvn test -Dtest=Test03_ChatMemoryTests run: 7, Failures: 0, Errors: 0, Skipped: 0
✅ 所有测试通过!| 测试编号 | 测试名称 | 核心知识点 |
|---|---|---|
| 3.1 | 验证 AI 默认没有记忆 | AI 本身没有记忆 |
| 3.2 | 配置 ChatMemory Bean | InMemoryChatMemory 配置 |
| 3.3 | 使用 MessageChatMemoryAdvisor | 自动记忆管理 |
| 3.4 | 会话隔离 | conversationId 使用 |
| 3.5 | 查询会话历史 | chatMemory.get() 方法 |
| 3.6 | 清空会话历史 | chatMemory.clear() 方法 |
| 3.7 | 验证 SimpleWindowChatMemory 可用性 | 窗口裁剪、按需读取、清空历史 |
请完成下面的练习,巩固本章的“对话记忆”核心机制与工程化落地能力
⚠️ 提示:本章面试高频点集中在 conversationId、记忆窗口、持久化、以及多实例部署下的会话一致性。
ChatClient 发送 3 组对话(A/B/C),每组都包含“我叫谁 + 我叫什么”;
断言三组对话互不串话。建议把 conversationId 设为 userId 或 userId:deviceId。
ChatClient 下,不同 conversationId 的历史必须隔离;并发下也不能串话。@Test
public void multiConversationConcurrent() throws Exception {
String a = "userA";
String b = "userB";
String c = "userC";
ExecutorService pool = Executors.newFixedThreadPool(3);
Callable<String> taskA = () -> {
chatClient.prompt("我叫张三")
.advisors(x -> x.param(CHAT_MEMORY_CONVERSATION_ID_KEY, a))
.call().content();
return chatClient.prompt("我叫什么?")
.advisors(x -> x.param(CHAT_MEMORY_CONVERSATION_ID_KEY, a))
.call().content();
};
Callable<String> taskB = () -> {
chatClient.prompt("我叫李四")
.advisors(x -> x.param(CHAT_MEMORY_CONVERSATION_ID_KEY, b))
.call().content();
return chatClient.prompt("我叫什么?")
.advisors(x -> x.param(CHAT_MEMORY_CONVERSATION_ID_KEY, b))
.call().content();
};
Callable<String> taskC = () -> {
chatClient.prompt("我叫王五")
.advisors(x -> x.param(CHAT_MEMORY_CONVERSATION_ID_KEY, c))
.call().content();
return chatClient.prompt("我叫什么?")
.advisors(x -> x.param(CHAT_MEMORY_CONVERSATION_ID_KEY, c))
.call().content();
};
List<Future<String>> results = pool.invokeAll(List.of(taskA, taskB, taskC));
String rA = results.get(0).get();
String rB = results.get(1).get();
String rC = results.get(2).get();
assertTrue(rA.contains("张三"));
assertTrue(rB.contains("李四"));
assertTrue(rC.contains("王五"));
}chatMemory.clear(conversationId);再问一次“我叫什么”,
验证模型不再记得。附加:说明“清空”应该由后端做还是前端做。
@Test
public void clearConversationShouldWork() {
String cid = "clear-001";
chatClient.prompt("我叫张三")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, cid))
.call().content();
String remembered = chatClient.prompt("我叫什么名字?")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, cid))
.call().content();
assertTrue(remembered.contains("张三"));
chatMemory.clear(cid);
String afterClear = chatClient.prompt("我叫什么名字?")
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, cid))
.call().content();
// 清空后,模型不应再能从历史得出“张三”(断言避免过强)
assertFalse(afterClear.contains("张三"));
}userId 和 conversationId 作为一对进行传递;每次请求打印日志,
形成“谁在和 AI 说话”的可观测链路(为后续治理/审计做准备)。
String userId = "u001";
String cid = userId + ":web";
String text = chatClient.prompt("我叫张三")
.advisors(a -> {
a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, cid);
a.param("userId", userId); // 自定义参数(用于日志/审计)
})
.call().content();
log.info("chat userId={} conversationId={} replyPreview={}", userId, cid,
text == null ? "null" : (text.length() <= 50 ? text : text.substring(0, 50) + "..."));表字段:conversation_id, message, created_at, last_active_at
索引:idx_last_active_at
定时任务(每 5 分钟):
find conversation where now - last_active_at > 30min
delete messages by conversation_idconversationId。
conversationId(以及必要的 userId),不要手写“历史拼接”。userId 或 userId:deviceId。
conversationId 是对话历史的分区键/主键,用来实现会话隔离。chatMemory.get() 方法chatMemory.clear() 方法继续学习 第4章:项目二 · 带记忆的聊天API, 将本章学到的 ChatMemory 知识应用到实际项目中, 实现完整的会话管理、会话历史列表等功能!