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,本文不展开迁移步骤。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。