Spring AI 2.1.0-RC1 把 Session Memory 放进了正式发布线:会话改成 append-only 的事件日志,模型看到的是日志上的活跃视图;压缩只追加边界事件,不改写旧记录。本地用 Spring Boot 4.2.0-RC1 与 spring-ai-session 的内存仓库(不调外部模型)复现了窗口切点的 turn 边界行为:五条消息里先有一轮「用户 + 带 tool call 的助手 + ToolResponse」,再有一轮普通问答;对活跃视图做 lastEvents(3) 无条件压缩时,按事件数本会留下末尾的 ToolResponse 与第二轮,实测整轮工具对话被盖住,活跃视图只剩第二轮两条,全量日志从 5 条增到 6 条并多出一条 COMPACTION 边界。
同一次 RC1 的 Tool Call Confirmation 写在之前那篇讲混工具回复整段暂停的文章里,本文只覆盖 Session Memory 与 turn-aware 压缩这一条线。
发布依据
GitHub Release v2.1.0-RC1 于 2026-10-23T14:56:34Z 发布(Asia/Shanghai 2026-10-23 22:56),New Features 写明 Add session memory: an event log with turn-aware compaction #7161,以及 Deprecate ChatMemory in favor of session memory #7186。标签 v2.1.0-RC1 指向提交 7782e72d4b8e4394e0a5cb6ee17315f24386f5aa。落地提交 b2eca788e6(作者时间 2026-10-14T21:06:18Z)的提交说明是 “Add session memory: an event log with turn-aware compaction”;弃用 ChatMemory 的提交是 308e42477b。行为细则以官方文档 Session Memory 与 Compaction 为准(文档抬头仍标 2.1-SNAPSHOT)。
父 POM 用 Spring Boot 4.2.0-RC1,BOM 为 org.springframework.ai:spring-ai-bom:2.1.0-RC1。运行环境是 JDK 27、Maven 3.10.0。演示只依赖 spring-ai-session 与 spring-ai-model,用 InMemorySessionRepository,未接 JDBC,也未配置任何 ChatModel。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.2.0-RC1</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.1.0-RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-session</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-model</artifactId>
</dependency>
会话怎么建
文档 Quick Start 与 jar 里的 DefaultSessionService 一致:仓库用 InMemorySessionRepository(文档写明适合测试与演示,重启会丢、跨实例不共享),再 create 指定 session id,用 appendMessage 写入消息事件。
SessionService sessionService = DefaultSessionService.builder()
.sessionRepository(InMemorySessionRepository.builder().build())
.build();
String sessionId = "demo-weather";
sessionService.create(CreateSessionRequest.builder().id(sessionId).build());
sessionService.appendMessage(sessionId, new UserMessage("北京天气怎么样"));
AssistantMessage.ToolCall toolCall = new AssistantMessage.ToolCall(
"call_1", "function", "get_weather", "{\"city\":\"北京\"}");
sessionService.appendMessage(sessionId,
AssistantMessage.builder().toolCalls(List.of(toolCall)).build());
sessionService.appendMessage(sessionId,
ToolResponseMessage.builder()
.responses(List.of(new ToolResponseMessage.ToolResponse(
"call_1", "get_weather", "晴,25C")))
.build());
sessionService.appendMessage(sessionId, new UserMessage("那明天呢"));
sessionService.appendMessage(sessionId, new AssistantMessage("明天也是晴,26C"));
压缩入口是 SessionService.compact(id, trigger, strategy)。文档写明:trigger 看何时,strategy 看活跃视图里留下什么;结果记成一条 COMPACTION 边界事件,可跟摘要消息;日志本身不删不改。本次 trigger 写成恒真,strategy 用 WindowCompactionStrategy.builder().lastEvents(3).build(),对应文档 Compaction 表里 “Cut lands mid-turn / lastEvents(3)” 一类切点,并落到带 ToolResponse 的工具轮上。
CompactionResult result = sessionService.compact(sessionId, request -> true,
WindowCompactionStrategy.builder().lastEvents(3).build());
压缩前后对照
启动日志确认运行在 Boot 4.2.0-RC1 上,并打印了本次未调用模型 API。
:: Spring Boot :: (v4.2.0-RC1)
=== Spring AI 2.1.0-RC1 Session Memory compaction demo ===
NOTE: no ChatModel / no remote model API called
压缩前全量日志 5 条 MESSAGE,活跃消息同样 5 条:用户、带 get_weather tool call 的助手、ToolResponse、第二轮用户与助手。
--- BEFORE compact: full log (5 events) ---
[MESSAGE] User: 北京天气怎么样
[MESSAGE] Assistant [tool calls: get_weather({"city":"北京"})]
[MESSAGE] Tool [responses: get_weather -> 晴,25C]
[MESSAGE] User: 那明天呢
[MESSAGE] Assistant: 明天也是晴,26C
--- BEFORE compact: active messages (5) ---
USER / 北京天气怎么样
ASSISTANT / toolCalls=get_weather
TOOL / ToolResponse ids=call_1
USER / 那明天呢
ASSISTANT / 明天也是晴,26C
CompactionResult 显示 unchanged=false、strategy=window、coveredEventCount=3。被盖住的正是整轮工具对话(用户 + tool call 助手 + ToolResponse);保留的是第二轮两条。文档写明切点会推到下一轮起点(cut moves forward to the next turn start),保证工具结果不会脱离发起该轮的用户消息:若严格按末尾 3 个事件切,会把 ToolResponse 与第二轮一起留下;实现把切点前移后,工具轮整段进入 covered。
--- CompactionResult ---
unchanged=false
strategy=window
coveredEventCount=3
covered=
User: 北京天气怎么样
Assistant [tool calls: get_weather({"city":"北京"})]
Tool [responses: get_weather -> 晴,25C]
preserved=
User: 那明天呢
Assistant: 明天也是晴,26C
pinned=
summary.size=0
压缩后全量日志 6 条:原 5 条 MESSAGE 仍在,末尾多出 COMPACTION,payload 含 pinnedEventIds、preservedEventIds、coveredEventCount 等。活跃视图事件与活跃消息都只剩第二轮两条;活跃视图里已看不到 ToolResponse。
--- AFTER compact: full log (6 events) ---
[MESSAGE] User: 北京天气怎么样
[MESSAGE] Assistant [tool calls: get_weather({"city":"北京"})]
[MESSAGE] Tool [responses: get_weather -> 晴,25C]
[MESSAGE] User: 那明天呢
[MESSAGE] Assistant: 明天也是晴,26C
[COMPACTION] COMPACTION boundary payloadKeys=[pinnedEventIds, strategy, coveredEventCount, preservedEventIds, tokensEstimatedSaved, summaryEventIds]
--- AFTER compact: active view events (2) ---
[MESSAGE] User: 那明天呢
[MESSAGE] Assistant: 明天也是晴,26C
--- MEASURED ---
tool_response_absent_from_active_view=true
active_is_exactly_turn2_user_assistant=true
covered_includes_tool_response=true
full_log_grew_with_compaction_boundary=true
full_log_event_count_before=5
full_log_event_count_after=6
和 ChatClient、摘要的关系
文档把 ChatClient 集成放在 spring-ai-session-advisor:SessionMemoryAdvisor 与 SessionTurnAdvisor 通过 SessionAdvisors 挂上;通话结束后 SessionTurnAdvisor 会调用 compact。生产侧还有 spring-ai-session-jdbc / spring-ai-starter-model-session-jdbc。本次为了把窗口切点测干净,直接打 SessionService.compact,未挂 SessionAdvisors,也未配置 ChatModelSummarizer,所以 summary.size=0,活跃视图里没有摘要轮。文档还写明:带 summarizer 时摘要调用直打 ChatModel、不经 advisor、不写入对话;摘要失败则视图保持不变。ChatMemory 弃用与迁移说明见提交 308e42477b 与同日文档提交 cba03cdd21,本文不展开迁移步骤。
一起交流
分享你的思考,让讨论更进一步。