导读:把 Spring AI 升到 2.1 之后,编译日志里会冒出一串"ChatMemory 已过时, 且标记为待删除"。替代品叫 Session Memory,2.1.0-RC1 刚带进来。我在临时项目里把迁移完整跑了一遍,这篇讲怎么改、改完数据存哪、多轮对话变长后怎么压缩,以及几个文档里不太显眼的坑。
适用版本:Spring AI 2.1.0-RC1(2026-10-23 发布)、Spring Boot 4.2.0-RC1、JDK 17。文中输出都来自本机实跑,模型用一个不联网的假 ChatModel 代替,只看记忆怎么拼进 prompt。
一、先说清楚:对话记忆是干什么的
大模型本身不记得上一句话,每次调用都是独立的。用户先问"订单 A123 什么时候发货",再问"那能改地址吗",模型并不知道"那"指的是哪个订单。要做多轮对话,程序就得每次把之前的问答一起发过去。
Spring AI 里负责这件事的组件,2.0 叫 ChatMemory:调用模型前,把这个会话的历史拼进请求;调用完,把新的一问一答存下来。历史可以放内存,也可以放数据库。
2.1 换了一套新实现,叫 Session Memory,干的还是同一件事,主要改进有三点:
- 历史只追加、不删除,出了问题能回查用户说过什么
- 对话太长时按"一问一答"整轮裁剪,不会把半轮对话发给模型
- 会话自带过期时间
对你有没有影响,看自己的情况:
二、哪些东西过时了
Spring AI 2.1 的 PR #7186 把整套聊天记忆 API 标成了 @Deprecated(since = "2.1.0", forRemoval = true),计划在 3.0.0 删除。升级后用老写法编译,会看到这样的警告(本机实测输出):
[WARNING] LegacyMemory.java:[14,9] org.springframework.ai.chat.memory.ChatMemory 已过时, 且标记为待删除
[WARNING] LegacyMemory.java:[15,43] org.springframework.ai.chat.memory.InMemoryChatMemoryRepository 已过时, 且标记为待删除
[WARNING] LegacyMemory.java:[14,33] org.springframework.ai.chat.memory.MessageWindowChatMemory 已过时, 且标记为待删除
[WARNING] LegacyMemory.java:[19,34] org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor 已过时, 且标记为待删除
触发这些警告的就是 2.0 时代最常见的写法:
// 2.0 写法:消息窗口记忆 + MessageChatMemoryAdvisor
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20)
.build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
完整的影响范围:
没过时的:VectorStoreChatMemoryAdvisor(向量检索式记忆)和 BaseChatMemoryAdvisor,Session 这边还没有语义召回的对应物。
⚠️ 警告只是提示,2.1 里老 API 的运行时行为没变,不改也能跑。
三、Session Memory 和 ChatMemory 差在哪
对照官方文档和 PR 的说明,两者最大的区别在于历史怎么存。先解释两个后面会反复出现的词:日志是存下来的全部历史;视图是这一次真正发给模型的那部分。新实现压缩时只缩小视图,不删除日志。
对后端来说,第二行和第三行最有用:日志不删,出了问题能回查用户到底说过什么;按轮切,工具调用和结果不会被拆散。
四、迁移步骤
1. 换依赖
把 BOM 升到 2.1.0-RC1,用 Session 的 JDBC starter 替换原来的 chat-memory starter:
<!-- 版本由 spring-ai-bom 2.1.0-RC1 管理 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-session-jdbc</artifactId>
</dependency>
<!-- 删掉:spring-ai-starter-model-chat-memory-repository-jdbc -->
这个 starter 带了 spring-ai-session、spring-ai-session-advisor、JDBC 存储和两个自动配置。只要容器里有一个 DataSource,就会自动建出 JdbcSessionRepository 和 SessionService 两个 Bean。
💡 Spring AI 2.x 依赖 Spring Boot 4.x,2.1.0-RC1 的源码 pom 里是 Boot 4.2.0-RC1,我的验证项目也用的这个版本。
2. 换 Advisor
advisor 可以理解成 ChatClient 的拦截器,类似 Spring MVC 的 HandlerInterceptor,在调用模型前后插一段逻辑,记忆就是靠它拼历史、存回复的。把 MessageChatMemoryAdvisor 换成 SessionAdvisors,它一次返回两个 advisor 的列表,直接塞给 defaultAdvisors:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.session.SessionService;
import org.springframework.ai.session.advisor.SessionAdvisors;
import org.springframework.ai.session.compaction.TurnCountTrigger;
import org.springframework.ai.session.compaction.WindowCompactionStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatConfig {
@Bean
ChatClient chatClient(ChatModel chatModel, SessionService sessionService) {
return ChatClient.builder(chatModel)
.defaultSystem("你是售后客服助手,回答要简短")
.defaultAdvisors(SessionAdvisors.builder(sessionService)
// 视图里超过 3 轮就压缩,只保留最近 2 轮(演示用,生产按 20/10 起步)
.compaction(new TurnCountTrigger(3),
WindowCompactionStrategy.builder().lastTurns(2).build())
.build())
.build();
}
}
两个 advisor 分工不同:SessionMemoryAdvisor 每次调模型前把历史拼进 prompt、调完把回复存下来;SessionTurnAdvisor 包在整次调用外面,负责首次建会话和调用结束后的压缩。官方文档说只注册前者会没有压缩,所以别拆开用,直接用 SessionAdvisors。
3. 每次调用带上会话 ID
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.session.advisor.SessionAdvisors;
import org.springframework.stereotype.Service;
@Service
public class CustomerChatService {
private final ChatClient chatClient;
public CustomerChatService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String chat(String userId, String conversationId, String question) {
// 会话 ID 带上用户 ID,避免不同用户传同一个 conversationId 串历史
String sessionId = userId + ":" + conversationId;
return chatClient.prompt()
.user(question)
.advisors(SessionAdvisors.session(sessionId))
.call()
.content();
}
}
会话不存在时,第一次调用会自动创建。建议会话 ID 拼上服务端拿到的用户 ID:前端传上来的 conversationId 不可信,不拼的话,别人猜到 ID 就能读到别人的历史。
4. 老代码里的 CONVERSATION_ID 不用改
如果你原来是这样传会话 ID 的:
// 2.0 写法,2.1 里常量挪到了 MemoryAdvisor,值没变
chatClient.prompt()
.user("第二句")
.advisors(a -> a.param(MemoryAdvisor.CONVERSATION_ID, "legacy-1"))
.call()
.content();
2.1 把 CONVERSATION_ID 从 ChatMemory 挪到了 MemoryAdvisor,字符串值还是 chat_memory_conversation_id,和 Session advisor 读的键是同一个。我实测用老写法传 ID,Session advisor 照样能拼出历史:
用老的 CONVERSATION_ID 键:4 条 [SYSTEM:你是售后客服助手,回答要简短][USER:第一句][ASSISTANT:第6次回复,收到2条消息][USER:第二句]
所以业务代码里大量 .param(ChatMemory.CONVERSATION_ID, id) 的项目,可以先只换 advisor,import 慢慢改。
五、实测:记忆是怎么拼进 prompt 的
为了不依赖真实大模型,我写了一个假的 ChatModel,把每次收到的 prompt 记下来,回复里写上"收到几条消息":
import java.util.ArrayList;
import java.util.List;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
// 不调真实大模型:记录每次收到的 prompt,回一句"收到 N 条消息"
public class EchoChatModel implements ChatModel {
final List<List<Message>> prompts = new ArrayList<>();
@Override
public ChatResponse call(Prompt prompt) {
prompts.add(prompt.getInstructions());
String reply = "第" + prompts.size() + "次回复,收到" + prompt.getInstructions().size() + "条消息";
return new ChatResponse(List.of(new Generation(new AssistantMessage(reply))));
}
}
测试里把它注册成 ChatModel Bean,数据库用 H2 内存库。同一个用户连问两句,再换一个用户问一句:
chatService.chat("u1001", "c1", "订单 A123 什么时候发货?");
chatService.chat("u1001", "c1", "那能改地址吗?");
chatService.chat("u2002", "c1", "我要退货");
模型收到的 prompt:
第二轮 prompt:4 条 [SYSTEM:你是售后客服助手,回答要简短][USER:订单 A123 什么时候发货?][ASSISTANT:第8次回复,收到2条消息][USER:那能改地址吗?]
另一个用户 prompt:2 条 [SYSTEM:你是售后客服助手,回答要简短][USER:我要退货]
两个用户都用了 conversationId = c1,因为会话 ID 拼了用户 ID,历史没有串。
再看不传会话 ID 会怎样:
chatClient.prompt().user("没带会话 ID").call().content();
直接抛异常,报错信息把正确写法都给了:
IllegalStateException: No session ID found in advisor context. Set SESSION_ID_CONTEXT_KEY on every request: .advisors(a -> a.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId))
官方文档给的理由是:如果有一个共用的默认会话,不同用户的历史会被悄悄合并。我在 2.1 上顺手试了老的 MessageChatMemoryAdvisor,不传 ID 同样报错(conversationId cannot be null),所以迁移前后这一点行为一致,只是新的报错信息更好懂。
⚠️ 系统提示词默认不存进会话(spring.ai.session.allow-system-messages 默认 false),每次调用由 defaultSystem 重新带上。上面输出里每轮都只有一条 SYSTEM,就是这个原因。不要为了"让模型记住人设"去打开它。
六、对话变长怎么办:按轮压缩
MessageWindowChatMemory 的 maxMessages(20) 是按条数硬截,Session 这边换成"触发器 + 策略":触发器决定什么时候压,策略决定留多少。上面配置的是"超过 3 轮就压缩,只留最近 2 轮",同一个会话连问 5 个问题,每轮后打印视图和日志的大小:
for (int i = 1; i <= 5; i++) {
chatService.chat("u3003", "c9", "第" + i + "个问题");
String id = "u3003:c9";
System.out.printf("第%d轮后:视图 %d 条,日志 %d 条消息,事件 %d 个%n", i,
sessionService.getActiveMessages(id).size(),
sessionService.getMessages(id).size(),
sessionService.getEvents(id).size());
}
实测输出:
第1轮后:视图 2 条,日志 2 条消息,事件 2 个
第2轮后:视图 4 条,日志 4 条消息,事件 4 个
第3轮后:视图 6 条,日志 6 条消息,事件 6 个
第4轮后:视图 4 条,日志 8 条消息,事件 9 个
第5轮后:视图 6 条,日志 10 条消息,事件 11 个
第 5 轮发给模型的 prompt:6 条 [SYSTEM:你是售后客服助手,回答要简短][USER:第3个问题][ASSISTANT:第3次回复,收到6条消息][USER:第4个问题][ASSISTANT:第4次回复,收到8条消息][USER:第5个问题]
几个要点:
- 第 4 轮结束时视图有 4 轮,超过 3,触发压缩,视图缩成最近 2 轮(4 条)
- 日志一条没删,还是 8 条消息;事件数多出 1 个,就是那个 COMPACTION 边界
- 第 5 轮发给模型的只有第 3、4 轮和本轮问题,第 1、2 轮已经不在 prompt 里
可选的触发器和窗口:
官方文档特别强调,不管用哪种窗口,切点都会挪到一轮的开头,不会出现保留了工具结果却丢了触发它的用户问题的情况。按条数截断时最怕的就是这个:只剩一条孤零零的 tool result,有的模型会直接拒绝请求。
💡 压缩默认在调用返回前同步执行。如果你的策略里带了摘要(CompactionSummarizer,会额外调一次模型),可以在 builder 上加 .asyncCompaction(true),让压缩在回复返回之后跑,不拖慢响应。
七、落到 MySQL:表结构和建表
JDBC 存储支持 PostgreSQL、MySQL、MariaDB、H2 四种,方言从数据库元数据自动识别。其他数据库(Oracle、SQL Server 等)启动时直接报 IllegalStateException,官方说 3.0.0 之前补方言。
H2 这类内嵌库会自动建表,我在测试里查到建出了 3 张表:
建出来的表:[AI_SESSION, AI_SESSION_EVENT, AI_SESSION_INDEX]
MySQL 默认不会自动建表(initialize-schema 默认 embedded)。开发环境可以打开:
spring:
ai:
session:
time-to-live: 30d
repository:
jdbc:
initialize-schema: always
生产环境我建议把脚本放进 Flyway 或 DBA 审核流程。脚本在 jar 里:spring-ai-session-jdbc-2.1.0-RC1.jar 的 org/springframework/ai/session/jdbc/schema-mysql.sql,核心是事件表(节选自 jar 内脚本):
CREATE TABLE IF NOT EXISTS AI_SESSION_EVENT (
seq BIGINT NOT NULL AUTO_INCREMENT,
id VARCHAR(255) COLLATE utf8mb4_bin NOT NULL PRIMARY KEY,
session_id VARCHAR(255) COLLATE utf8mb4_bin NOT NULL,
timestamp DATETIME(6) NOT NULL,
event_type VARCHAR(64) NOT NULL DEFAULT 'MESSAGE',
message_type VARCHAR(20),
message_content LONGTEXT,
payload LONGTEXT NOT NULL,
synthetic TINYINT(1) NOT NULL DEFAULT 0,
metadata LONGTEXT NOT NULL,
UNIQUE KEY uq_ai_session_event_seq (seq),
INDEX idx_ai_session_event_session_seq (session_id, seq),
INDEX idx_ai_session_event_session_type_seq (session_id, event_type, seq),
CONSTRAINT fk_ai_session_event_session
FOREIGN KEY (session_id) REFERENCES AI_SESSION (id) ON DELETE CASCADE
);
DBA 审脚本时大概会问到这几点,我先列出来:
⚠️ 最后一条要提前跟产品说清楚:切换上线后,用户之前的对话上下文就"断"了。确实需要保留的话,得自己写脚本把老表的消息用 sessionService.appendMessage 灌进新会话,这一步官方没提供。
八、过期会话要自己清
会话默认 60 天过期(spring.ai.session.time-to-live),但官方文档写明了不会自动清理,要自己定时调 deleteExpiredSessions。我在测试里把过期时间设成 2 小时验证了一下:
expiresAt 距现在约 119 分钟
现在清理:0 个
3 小时后清理:1 个
清理后还能查到吗:false
生产里加一个定时任务就行(启动类上要有 @EnableScheduling):
import java.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.session.SessionService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
// 过期会话不会自动清理,需要自己定时调用
@Component
public class SessionCleanupJob {
private static final Logger log = LoggerFactory.getLogger(SessionCleanupJob.class);
private final SessionService sessionService;
public SessionCleanupJob(SessionService sessionService) {
this.sessionService = sessionService;
}
@Scheduled(cron = "0 30 3 * * *")
public int cleanExpired() {
int removed = sessionService.deleteExpiredSessions(Instant.now());
log.info("清理过期 AI 会话 {} 个", removed);
return removed;
}
}
多实例部署时,这个任务要么加分布式锁,要么交给 XXL-JOB 这类调度平台只跑一个实例,不然几台机器同时删同一批数据,虽然结果一样但白白多打几次库。
九、速查表
十、常见问题
Q:现在是 RC 版,要不要马上迁?
我的建议是新项目直接用 Session,老项目先升级到 2.1 不改代码(老 API 还能用),等 2.1.0 正式版出来再按上面的速查表迁。用 Redis、MongoDB 做记忆存储的,等官方补齐 Session 存储再说。
Q:数据库是 Oracle / SQL Server / 达梦,能用吗?
内置方言只有 PostgreSQL、MySQL、MariaDB、H2,其他库启动报错。可以自己实现 JdbcSessionRepositoryDialect 接口,再传给 JdbcSessionRepository.builder().dialect(...)。这次没有试写方言,工作量取决于你的库对 JSON 和分页的支持程度。
Q:只想让某一次调用少带点历史?
用请求级过滤器,不用改全局配置:
chatClient.prompt()
.user("帮我总结一下")
.advisors(a -> a
.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId)
.param(SessionMemoryAdvisor.EVENT_FILTER_CONTEXT_KEY, EventFilter.lastN(5)))
.call()
.content();
lastN 窗口同样会扩到一轮的开头,不会拆开工具调用。
Q:敏感内容不想落库怎么办?
SessionMemoryAdvisor 有写入侧的 MessageFilter,被过滤的消息不会存,但这一次的 prompt 不受影响。注意工具调用和工具结果无论如何都会存,这是为了保证下一轮 prompt 合法。
参考链接
如果你正准备把项目升到 Spring AI 2.1,希望这篇能帮你少踩几个坑,觉得有用点个赞 👍 收藏 📌。
你们项目里的对话记忆存在哪,MySQL、Redis 还是向量库?迁移时遇到别的问题,欢迎评论区一起聊。
本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。
Introduction: After raising Spring AI to 2.1, a string of "ChatMemory is out of date and marked to be deleted" will appear in the compiler log. Alternatives are known as Session Memoory, 2.1.0-RC1 just brought in. I ran the entire migration in the interim project, how it was modified, where the data was stored, how it was compressed when the multi-wheel conversation grew, and the less visible pits in several documents.
Applicable version: Spring AI 2.1.0-RC1 (2026-10-23 publish), Spring Boot 4.2.0-RC1, JDK 17. The text output comes from the machine run, and the model is replaced by an unconnected chatModel, depending on how the memory goes into the programt.
First, let's be clear: what are the memories of dialogue?
LLM does not remember the last sentence, and every call is independent. Users ask "order A123 when to deliver" and then "can we change the address?" The model doesn't know which order. In order to have multiple rounds of dialogue, the procedure would have to be sent together with each previous question and answer session.
The component in Spring AI responsible for this matter, 2.0, is called ChatMemory: Spell this session's history into a request before calling the model; call it out and save a new question. History can be stored in memory or in databases.
2.1 There are three main improvements to the new set of achievements, known as Session Memoory, or the same thing:
- History only adds, doesn't delete, and there's a problem with checking what the user said.
- It's too long to trim the whole round with "a question and answer."
- The session expired.
It doesn't affect you.
What's out of date?
PR #7186 of Spring AI 2.1 marked the chat memory set API as @Deprecated(since = "2.1.0", for Removal = true) and planned to be deleted at 3.0.0. The upgrade is compiled in the old script, and you see the warning (this machine actually detects the output):
[WARNING] LegacyMemory.java: [14,9]
[WARNING] LegacyMemory.java: [15,43] [org. Springframework.ai.chat.memory.InMemorychamoryRepository]
[WARNING] LegacyMemory.java: [14,33]
[WARNING] LegacyMemory.java: [19,34] [19,34]
These warnings were triggered by the most common formulation of the 2.0 era:
/ 2.0 writing: MessageChatMemooryAdvisor
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20)
.build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
Full range of impacts:
It's not out of date: VictorStoreChatMemooryAdvisor and BaseChatMemooryAdvisor,Ssession have no semantic counterparts.
The warning is just a reminder that the behaviour of Li's old API in running has not changed and can run.
III. Where is the session memory and Chat Memory?
The biggest difference between official documents and PR is how history survives. Explain the two words that follow: the log is the whole history that's left; the view is the part that's actually sent to the model this time. Only narrows the view and does not delete the log when new compression is achieved.
For the back end, the second and third lines would be most useful: the logs would not be deleted, and questions could be asked about what the user had said; tool call and the results would not be broken down by the quartet.
IV. Steps for relocation
1. Replacement of dependency
Raise BOM to 2.1.0-RC1, replace Chat-Memory star:
<! -- Version managed by Spring-ai-bom 2.1.0-RC1->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-session-jdbc</artifactId>
</dependency>
Delete: Spring-ai-starter-model-chat-memoory-repository-jdbc-->
This starter brings Spring-ai-session, Spring-ai-session-advisor, JDBC storage and two automatic configurations. As long as there is one DataSource in the container, the JdbcesessionRepositiry and SessionService are built automatically.
The source pom of Spring AI 2.x, which relies on Spring Boot 4.x, 2.1.0-RC1, contains Boot 4.2.0-RC1, a version also used in my validation project.
2. Replacement of Advisor
It can be understood as a ChatCliant interceptor, like Spring MVC, Handler Interceptor, who inserts a logic before and after a call to a model by which memory is written and written back. Change MessageChatMemooryAdvisor to SsessionAdvisors, which returns to two lists at a time and plugs directly into defaultAdvisors:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.session.SessionService;
import org.springframework.ai.session.advisor.SessionAdvisors;
import org.springframework.ai.session.compaction.TurnCountTrigger;
import org.springframework.ai.session.compaction.WindowCompactionStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatConfig {
@Bean
ChatClient chatClient(ChatModel chatModel, SessionService sessionService) {
return ChatClient.builder(chatModel)
.defaultSystem.
.defaultAdvisors(SessionAdvisors.builder(sessionService)
/ / More than 3 rounds in view compress and only the most recent 2 rounds (demonstration, production starting at 20/10)
.compaction(new TurnCountTrigger(3),
WindowCompactionStrategy.builder().lastTurns(2).build())
.build())
.build();
}
}
Two divisions of labour are different: SessionMoryAdvisor puts history into prompt before each move, edits the response and saves it; and SessionTurnAdvisor packs outside of the whole call and is responsible for initial session and compression after call. The official document says that only the former will not be compressed, so don't unplug it and use SsessionAdvisors directly.
3. Each call belt session ID
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.session.advisor.SessionAdvisors;
import org.springframework.stereotype.Service;
@Service
public class CustomerChatService {
private final ChatClient chatClient;
public CustomerChatService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String chat(String userId, String conversationId, String question) {
/ Session id takes the user id and avoids passing the same conversion Id history to different users
String sessionId = userId + ":" + conversationId;
return chatClient.prompt()
.user(question)
.advisors(SessionAdvisors.session(sessionId))
.call()
.content();
}
}
The first call is automatically created when the session does not exist. Suggest that the session ID spell the user ID that the service gets: the transfer Id from the front end doesn't trust, if it doesn't spell, someone can read the other person's history.
4. CONTERSATION ID in the old code.
If this is how you used to pass the session ID:
/ 2.0 writing, 2.1 constants moved to Memory Advisor, unchanged
chatClient.prompt()
.user ( "second sentence")
.advisors(a -> a.param(MemoryAdvisor.CONVERSATION_ID, "legacy-1"))
.call()
.content();
2.1 The CONVERSATION ID was moved from ChatMemory to MemooryAdvisor, with a string value or cat memory conversion id, and the key read by Ssession Advisor. I've been working on the old I.D., the President advisor.
Using the old CONVERSATION ID key: 4 [SYSTEM: You're the after-sale customer service assistant, answer briefly] [USER: first sentence] [ASSISTANT: 6th reply, 2 messages received] [USER: second sentence]
So there's a lot of.param projects in the business code that can be changed slowly.
V. Factual: how memories spell prompt
In order not to rely on the real LLM, I wrote a fake ChatModel, wrote down every time I received the prompt, and wrote in the reply, "A few messages."
import java.util.ArrayList;
import java.util.List;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
/ Unrequired LLM: Record every receipt prompt, reply "Receive N messages."
public class EchoChatModel implements ChatModel {
final List<List<Message>> prompts = new ArrayList<>();
@Override
public ChatResponse call(Prompt prompt) {
prompts.add(prompt.getInstructions());
String reply = "No" + prompts.size() + "subsequent reply, received" + prompt.getInstruations(). size() + "List message";
return new ChatResponse(List.of(new Generation(new AssistantMessage(reply))));
}
}
It is registered in the test as ChatModel Bean, a database with an H2 repository. Ask the same user for two sentences, another user for:
Chat Service.chat.chat.
Chat Service. Chat. " );
Chat Service. Chat;
Prompt:
Second round prompt: 4 [SYSTEM: You're the after-sale customer service assistant, answer briefly] [USER: Order A123] When is the delivery? [ASSISTANT: 8th reply, 2 messages received] [USER: can that change the address? ]
Another user, prompt: 2.
Both users use conversionID = c1 because the session ID spell the user ID, no string in history.
And if you don't pass on the session, what happens to ID:
ChatClint.prompt().user("Id without session").call().content();
I'm going straight to the anomaly.
IllegalStateException: No session ID found in advisor context. Set SESSION_ID_CONTEXT_KEY on every request: .advisors(a -> a.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId))
The reason given by the official document is that if there is a shared default session, the history of different users will be quietly merged. I tried the old MessageChatMemooryAdvisor, without passing on ID, and so it was the same thing before and after the move, but the new information was better understood.
⚠️ The system prompt defaults on sessions (spring.ai.session.allow-system-messages default) with each call being reloaded by defaultSystem. That's why there's only one SYSTEM in every round of output. Don't open it for the "show the model to remember."
VI. HOW TO LONGER DIALOGUE: CLASSIFICATION BY ROLE
MessageWindowChatMemory's maxMessages(20) is hard by bar number, and the session is replaced by a "trigger + strategy": the trigger determines when to press and how much to keep. It is configured to "over 3 rounds, compress, leave the nearest 2 rounds", with the same session asking 5 questions, printing views and log sizes after each round:
for (int i = 1; i <= 5; i++) {
Chat Service. Chat;
String id = "u3003:c9";
System.out.printf("after %d: View %d, Log %d, Event %d %n", i,
sessionService.getActiveMessages(id).size(),
sessionService.getMessages(id).size(),
sessionService.getEvents(id).size());
}
Actual output:
After Round 1: View 2, Log 2, Message, Event 2
After Round 2: View 4, Log 4 messages, events 4
After Round 3: View 6, Log 6 messages, events 6
After Round 4: View 4, Log 8 messages, events 9
After Round 5: View 6, Log 10 messages, events 11
5 [SYSTEM: you're the after-sale customer's assistant and answer short] [USER: third question] [ASSISTANT: third reply, received six messages] [USER: fourth question] [ASSISTANT: fourth reply, received eight messages] [USER: fifth question]
Several points:
- At the end of round 4 the view is 4 rounds, exceeding 3, trigger compression, and the view is reduced to the nearest 2 rounds (4)
- Log without deletion or 8 messages; one more event, the COMPACTION border
- Round 5 to model only 3rd, 4th and current rounds, 1st and 2nd rounds are no longer in prompt
Optional triggers and windows:
The official document emphasizes, in particular, that, regardless of the window used, the cut points are moved to the beginning of the round, without the loss of the user problem that triggers the retention of the tool. This is the one thing that scares you most when you stop by number: there's only one single tool result, and some models refuse requests directly.
💡 The compression default is executed simultaneously before calling back. If you have a summary in your strategy, you can add.asyncCommation (true) to the builder so that compression can run back without slowing down the response.
MySQL: Table structure and tables
JDBC storage supports PostgreSQL, MySQL, MariaDB, H2 and dialects automatically recognize from database metadata. Other databases (Oracle, SQL Server, etc.) are directly reported Illegal StateException, officially 3.0.0 before the supplement.
H2 This kind of embedded library automatically forms, and I found in the test 3 forms:
Form: [AI SESSION, AI SESSION EVENT, AI SESSION INDEX]
MySQL default does not automatically build a table (initialize-schema default embedded). The development environment opens:
spring:
ai:
session:
time-to-live: 30d
repository:
jdbc:
initialize-schema: always
I suggest putting scripts in the Flyway or DBA audit process. Script in jar: Spring-ai-session-jdbc-2.1.0-RC1.jar org/ Springframework/ai/session/jdbc/schema-mysql.sql at the core of events (selected from inside jar):
CREATE TABLE IF NOT EXISTS AI_SESSION_EVENT (
seq BIGINT NOT NULL AUTO_INCREMENT,
id VARCHAR(255) COLLATE utf8mb4_bin NOT NULL PRIMARY KEY,
session_id VARCHAR(255) COLLATE utf8mb4_bin NOT NULL,
timestamp DATETIME(6) NOT NULL,
event_type VARCHAR(64) NOT NULL DEFAULT 'MESSAGE',
message_type VARCHAR(20),
message_content LONGTEXT,
payload LONGTEXT NOT NULL,
synthetic TINYINT(1) NOT NULL DEFAULT 0,
metadata LONGTEXT NOT NULL,
UNIQUE KEY uq_ai_session_event_seq (seq),
INDEX idx_ai_session_event_session_seq (session_id, seq),
INDEX idx_ai_session_event_session_type_seq (session_id, event_type, seq),
CONSTRAINT fk_ai_session_event_session
FOREIGN KEY (session_id) REFERENCES AI_SESSION (id) ON DELETE CASCADE
);
DBA will probably ask these questions in the script, so I'll start with:
The last one needs to be clear to the product in advance: the user's previous conversation with context is "breaked." If there is a real need to keep it, write your own script to fill in the old watch with a session service.appendMessage, a step not officially provided.
viii. Expiry sessions are self-cleaning.
Sessions are defaulted to expire at 60 days (spring.ai.session.time-to-live), but the official document states that it will not be self-cleaned and that it is necessary to adjust regularly. I set the expiry time in the test for two hours:
About 119 minutes from now.
Now clear: 0
Clean-up in 3 hours: 1
Can you find it after cleaning: false
A timed task for the production of Riga.
import java.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.session.SessionService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/ / Expiry sessions do not automatically clean up and need to call on their own time
@Component
public class SessionCleanupJob {
private static final Logger log = LoggerFactory.getLogger(SessionCleanupJob.class);
private final SessionService sessionService;
public SessionCleanupJob(SessionService sessionService) {
this.sessionService = sessionService;
}
@Scheduled(cron = "0 30 3 * * *")
public int cleanExpired() {
int removed = sessionService.deleteExpiredSessions(Instant.now());
log.info ("Clean Expired AI Sessions {}," removed);
return removed;
}
}
When multiple instances are deployed, the task is either to place the distribution lock on or to leave a movement control platform such as XXL-JOB with only one example, or several machines are able to delete the same data at the same time, albeit with the same result, but have been able to fill the library several times.
IX. Quick checklist
X. Common issues
Q: Now is the RC version. Should we move it now?
My suggestion is that the new project be used directly, and that the old project be upgraded to 2.1 without changing the code (old API is still working), and that the formal version of 2.1.0 be released and rescheduled as quickly as possible. Redis, MongoDB for memory storage until the official session is completed.
Q: The database is Oracle / SQL Server/ Dreamland, can it be used?
Only PostgreSQL, MySQL, MariaDB, H2, other libraries are reporting errors. The JdbcesessionRepositoryDialect interface can be implemented on its own and forwarded to JdbcsessionRepository.builder().dialect(...). This time there was no trial dialect, and the workload depends on the level of support your library has for JSON and the page break.
Q: Just one call with less history?
Use the request level filter without changing the global configuration:
chatClient.prompt()
.user.
.advisors(a -> a
.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId)
.param(SessionMemoryAdvisor.EVENT_FILTER_CONTEXT_KEY, EventFilter.lastN(5)))
.call()
.content();
The lastN window also extends to the beginning of the round and does not open tool call.
Q: What if sensitive content doesn't want to be stored?
SsessionMemory Advisor has written side of MessageFilter, filtered messages will not be stored, but this time the prompt will not be affected. Note that tool call and the tool results will in any case exist to ensure that the next round of prompt is legal.
References
If you're about to take the project up to Spring AI 2.1, I hope this piece will help you to make a few less pits, and I think it'll help.
Where's the conversation in your project, MySQL, Redis, or the vector bank? There were other problems with relocation, and the comment section would be welcome to discuss them.
Auto-fetched and translated by AI Atlas. All rights belong to the original author.