【Spring AI Session】核心概念

1. 概述

Spring AI Session 围绕会话(Session) 管理,定义了四个紧密关联的核心概念,它们共同描述了"一次完整的 AI 对话"如何被存储、组织和随时间演进。这四个概念分别是:

  • 会话(Session):对话的身份与生命周期容器。
  • 会话事件(SessionEvent):对话中的每一条消息(含元数据)。
  • 轮次(Turn):一次用户输入及其引发的所有后续响应的原子单元。
  • 合成摘要轮次(Synthetic Summary Turn):由压缩策略生成的两条合成消息,用于替换旧历史。

理解这些概念是正确使用 Spring AI Session 的基础。下面我们一一展开。


2. 会话(Session)

会话 代表一个用户与 AI 代理之间单次、连续对话 的标识和生命周期容器。它是一个不可变的值对象(immutable value object) ------仅持有元数据(metadata),不包含具体的事件列表 。事件日志单独存储在 SessionRepository 中,按需查询。

会话的字段

字段 描述
id 全局唯一会话标识符
userId 所属用户或代理标识,必填,用于隔离不同用户
createdAt 创建时间戳
expiresAt 过期时间点。默认从创建时刻起算,使用配置的默认 TTL(60 天);若为 null 则表示永不过期。构建器会拒绝设置过去的时间值
metadata 任意键值对,用于存放模型信息、标签等扩展数据

Session 只包含元数据

将事件列表从 Session 中剥离,使得 Session 对象可以轻量地在系统边界间传递,而压缩策略也能将事件列表作为显式参数接收,无需从 Session 对象中提取。这保持了 Session 的不可变性,也方便了后续的事件变更操作。

创建会话示例

java 复制代码
SessionService service = new DefaultSessionService(InMemorySessionRepository.builder().build());

Session session = service.create(
    CreateSessionRequest.builder()
        .id("my-session-id")                      // 可选,不指定则自动生成 UUID
        .userId("alice")
        .timeToLive(Duration.ofHours(2))          // 可选,默认 60 天
        .metadata("agentType", "research-assistant")
        .build()
);

3. 会话事件(SessionEvent)

会话事件 是对 Spring AI 中 Message 类型的不可变包装 ,它补充了 Message 本身所缺少的信息:身份标识、归属关系、时序顺序以及框架内部标记。

事件的字段

字段 描述
id 事件唯一标识(默认 UUID)
sessionId 所属会话 ID,用于归属和隔离
timestamp 时间戳(默认 Instant.now()),用于时间排序
message 原始的 Spring AI 消息对象,不复制其内容
metadata 框架标志,如 METADATA_SYNTHETIC(合成标记)和 METADATA_COMPACTION_SOURCE(压缩来源)
branch 点分隔的代理路径(例如 "orch.researcher"),null 表示根级事件

消息类型与合成标记

消息类型 isSynthetic() 含义
UserMessage(真实) false 真实用户输入
AssistantMessage(无工具调用) false 代理的文本回复
AssistantMessage(含工具调用) false 代理发起工具调用
ToolResponseMessage false 工具返回结果
UserMessage(合成) true 合成摘要轮次中的"用户"提示(影子提示)
AssistantMessage(合成) true 合成摘要轮次中的"助手"摘要文本

构建事件示例

java 复制代码
// 根级事件(无分支),所有代理可见,时间戳默认当前时间
SessionEvent event = SessionEvent.builder()
    .sessionId(sessionId)
    .message(new UserMessage("你好"))
    .build();

// 分支事件 ------ 归属于特定子代理
SessionEvent branched = SessionEvent.builder()
    .sessionId(sessionId)
    .message(new AssistantMessage("研究结果是..."))
    .branch("orch.researcher")
    .build();

// 携带自定义元数据
SessionEvent custom = SessionEvent.builder()
    .sessionId(sessionId)
    .message(new AssistantMessage("回复内容"))
    .metadata("model", "gpt-4o")
    .metadata("latencyMs", 230)
    .build();

// 测试时使用确定性时间戳
SessionEvent deterministic = SessionEvent.builder()
    .sessionId(sessionId)
    .timestamp(Instant.parse("2025-06-01T12:00:00Z"))
    .message(new UserMessage("Hello"))
    .build();

构建器默认值id 随机 UUID,timestampInstant.now()metadata 为空,branchnullsessionIdmessage 为必填项


4. 轮次(Turn)

轮次 是对话中的原子单元,定义如下:

一个 UserMessage 及其之后、直到下一个 UserMessage 之前的所有后续事件(包括助手回复、工具调用、工具结果)共同组成一个轮次。

需要轮次而非按消息计数

  • 确保压缩(compaction)不会把工具调用 与其对应的工具结果拆散。
  • 避免删除某条助手回复却保留引发该回复的用户问题,从而破坏对话的因果连贯性。

示例

复制代码
轮次 1: [用户] "什么是 Spring AI?"  → [助手] "Spring AI 是..."
轮次 2: [用户] "它能用工具吗?"      → [助手(工具调用)] → [工具结果] → [助手] "是的,可以..."
轮次 3: [用户] "给我举个例子"       → [助手] "例如..."

轮次计数规则

轮次数通过 CompactionRequest.currentTurnCount() 计算,仅统计

  • 非合成isSynthetic == false
  • 根级branch == null
  • 类型为 UserMessage 的事件

合成事件和子代理分支上的 UserMessage 会被排除,以防止多代理场景下的轮次数虚高,避免 TurnCountTrigger 过早触发压缩。


5. 合成摘要轮次(Synthetic Summary Turn)

当压缩策略(如 RecursiveSummarizationCompactionStrategy)对会话进行压缩时,它会将归档的事件替换为两条合成事件,这两条事件共同构成一个完整的对话轮次:

复制代码
[用户 / 合成] "请总结我们到目前为止的对话。"
[助手 / 合成] "用户询问了主题 X。助手解释了......"
[用户 / 真实] "下一个真正的问题"
[助手 / 真实] "下一个真正的回复"

这种模式模仿了 OpenAI Agents SDK 的影子提示(shadow-prompt)模式,确保下游模型始终看到有效的用户↔助手交替,不会出现连续两条用户消息或助手消息。

所有压缩策略的共同行为

  • 将合成事件视为不透明数据,在处理前将其与真实事件分离。
  • 保留 所有合成事件,并始终将它们放在压缩输出的最前面

手动构建合成摘要轮次

java 复制代码
Instant now = Instant.now();
List<SessionEvent> summaryTurn = List.of(
    SessionEvent.builder()
        .sessionId(sessionId)
        .timestamp(now)
        .message(new UserMessage("请总结我们到目前为止的对话。"))
        .metadata(SessionEvent.METADATA_SYNTHETIC, true)
        .metadata(SessionEvent.METADATA_COMPACTION_SOURCE, "recursive-summarization")
        .build(),
    SessionEvent.builder()
        .sessionId(sessionId)
        .timestamp(now)
        .message(new AssistantMessage("用户询问了 X。助手解释了..."))
        .metadata(SessionEvent.METADATA_SYNTHETIC, true)
        .metadata(SessionEvent.METADATA_COMPACTION_SOURCE, "recursive-summarization")
        .build()
);

两条事件使用相同的时间戳,确保它们被视为一个原子单元。


6. 架构设计要点

6.1 核心组件关系图

#mermaid-svg-oZeNLmDb12SEuq2d{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oZeNLmDb12SEuq2d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oZeNLmDb12SEuq2d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oZeNLmDb12SEuq2d .error-icon{fill:#552222;}#mermaid-svg-oZeNLmDb12SEuq2d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oZeNLmDb12SEuq2d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oZeNLmDb12SEuq2d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oZeNLmDb12SEuq2d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oZeNLmDb12SEuq2d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oZeNLmDb12SEuq2d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oZeNLmDb12SEuq2d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oZeNLmDb12SEuq2d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oZeNLmDb12SEuq2d .marker.cross{stroke:#333333;}#mermaid-svg-oZeNLmDb12SEuq2d svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oZeNLmDb12SEuq2d p{margin:0;}#mermaid-svg-oZeNLmDb12SEuq2d g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-oZeNLmDb12SEuq2d g.classGroup text .title{font-weight:bolder;}#mermaid-svg-oZeNLmDb12SEuq2d .cluster-label text{fill:#333;}#mermaid-svg-oZeNLmDb12SEuq2d .cluster-label span{color:#333;}#mermaid-svg-oZeNLmDb12SEuq2d .cluster-label span p{background-color:transparent;}#mermaid-svg-oZeNLmDb12SEuq2d .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oZeNLmDb12SEuq2d .cluster text{fill:#333;}#mermaid-svg-oZeNLmDb12SEuq2d .cluster span{color:#333;}#mermaid-svg-oZeNLmDb12SEuq2d .nodeLabel,#mermaid-svg-oZeNLmDb12SEuq2d .edgeLabel{color:#131300;}#mermaid-svg-oZeNLmDb12SEuq2d .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-oZeNLmDb12SEuq2d .label text{fill:#131300;}#mermaid-svg-oZeNLmDb12SEuq2d .labelBkg{background:#ECECFF;}#mermaid-svg-oZeNLmDb12SEuq2d .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-oZeNLmDb12SEuq2d .classTitle{font-weight:bolder;}#mermaid-svg-oZeNLmDb12SEuq2d .node rect,#mermaid-svg-oZeNLmDb12SEuq2d .node circle,#mermaid-svg-oZeNLmDb12SEuq2d .node ellipse,#mermaid-svg-oZeNLmDb12SEuq2d .node polygon,#mermaid-svg-oZeNLmDb12SEuq2d .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oZeNLmDb12SEuq2d .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d g.clickable{cursor:pointer;}#mermaid-svg-oZeNLmDb12SEuq2d g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-oZeNLmDb12SEuq2d g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-oZeNLmDb12SEuq2d .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-oZeNLmDb12SEuq2d .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-oZeNLmDb12SEuq2d .dashed-line{stroke-dasharray:3;}#mermaid-svg-oZeNLmDb12SEuq2d .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-oZeNLmDb12SEuq2d #compositionStart,#mermaid-svg-oZeNLmDb12SEuq2d .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #compositionEnd,#mermaid-svg-oZeNLmDb12SEuq2d .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #dependencyStart,#mermaid-svg-oZeNLmDb12SEuq2d .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #dependencyStart,#mermaid-svg-oZeNLmDb12SEuq2d .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #extensionStart,#mermaid-svg-oZeNLmDb12SEuq2d .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #extensionEnd,#mermaid-svg-oZeNLmDb12SEuq2d .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #aggregationStart,#mermaid-svg-oZeNLmDb12SEuq2d .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #aggregationEnd,#mermaid-svg-oZeNLmDb12SEuq2d .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #lollipopStart,#mermaid-svg-oZeNLmDb12SEuq2d .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d #lollipopEnd,#mermaid-svg-oZeNLmDb12SEuq2d .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-oZeNLmDb12SEuq2d .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-oZeNLmDb12SEuq2d .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oZeNLmDb12SEuq2d .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oZeNLmDb12SEuq2d .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oZeNLmDb12SEuq2d :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} uses
持有(通过Repository)
1
0..*
Session
+String id
+String userId
+Instant createdAt
+Instant expiresAt
+Map<String,Object> metadata
SessionEvent
+UUID id
+String sessionId
+Instant timestamp
+Message message
+Map<String,Object> metadata
+String branch
+boolean isSynthetic()
+boolean isArchived()
<<interface>>
SessionService
+create(CreateSessionRequest) : Session
+getEvents(sessionId) : List<SessionEvent>
+appendMessage(sessionId, Message) : void
+compactEvents(sessionId, expectedVersion, strategy) : CompactionResult
+delete(sessionId) : void
+deleteExpiredSessions(Instant) : int
DefaultSessionService
-SessionRepository repository
<<interface>>
SessionRepository
+save(session)
+findById(id)
+appendEvent(sessionId, event)
+compactEvents(sessionId, archived, retained, version)
+getEventVersion(sessionId)
+delete(id)
+deleteExpired(instant)
SessionMemoryAdvisor
-SessionService sessionService
-CompactionTrigger trigger
-CompactionStrategy strategy
ChatClient
+prompt()

6.2 为何 Session 不直接携带事件?

  • 避免可变引用:若事件列表挂在 Session 内部,任何需要修改列表的操作(如压缩、归档)都必须持有对 Session 内部可变字段的引用,破坏不可变性。
  • 职责分离 :Session 仅作为元数据,所有事件变更通过 SessionRepository 的专用方法(appendEventcompactEvents)完成。SessionService 将事件列表作为显式参数处理,逻辑更清晰。

6.3 归档而非删除

压缩不会删除 被移出活动上下文的事件,而是通过 compactEvents 将它们标记为 已归档archived = true)。完整的历史记录全部保留在日志中。

  • 活动上下文 (即 SessionMemoryAdvisor 注入到提示中的内容)使用 EventFilter.active() 视图(excludeArchived = true)。
  • 回忆存储(Recall Storage) 搜索(如 EventFilter.keywordSearch(...))则跨越全部日志,包括已归档事件。

这正是 MemGPT 回忆模式 的基础:代理即使在上下文压缩后,仍能检索到任何先前的交流内容。

6.4 乐观并发控制

SessionRepository 提供 getEventVersion(sessionId),返回一个单调递增的版本号,每次 appendEventcompactEvents 都会递增。

调用者流程:

  1. 读取当前版本号。
  2. 获取事件列表。
  3. 执行压缩或追加操作时,将预期版本号作为条件传入 compactEvents(sessionId, archivedEvents, retainedEvents, expectedVersion)
  4. 若期间有其他写入者修改了日志,CAS 返回 false,调用者将其视为无操作(不重试)。

持久化实现(JDBC、Redis)应映射为数据库乐观锁列或 Redis WATCH

6.5 事件顺序

事件始终按照**插入顺序(即逻辑对话顺序)**返回,而非 墙上时钟时间。

JDBC 实现使用自增的 seq 列来保证顺序,即使合成摘要的时间戳晚于被替换的事件,它们也能被放置在正确的位置(位于被保留的旧事件之前)。


7. 会话生命周期(Session Lifecycle)

SessionRepository SessionService App SessionRepository SessionService App #mermaid-svg-L5XKsngAzYhdo5es{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-L5XKsngAzYhdo5es .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-L5XKsngAzYhdo5es .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-L5XKsngAzYhdo5es .error-icon{fill:#552222;}#mermaid-svg-L5XKsngAzYhdo5es .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-L5XKsngAzYhdo5es .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-L5XKsngAzYhdo5es .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-L5XKsngAzYhdo5es .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-L5XKsngAzYhdo5es .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-L5XKsngAzYhdo5es .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-L5XKsngAzYhdo5es .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-L5XKsngAzYhdo5es .marker{fill:#333333;stroke:#333333;}#mermaid-svg-L5XKsngAzYhdo5es .marker.cross{stroke:#333333;}#mermaid-svg-L5XKsngAzYhdo5es svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-L5XKsngAzYhdo5es p{margin:0;}#mermaid-svg-L5XKsngAzYhdo5es .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-L5XKsngAzYhdo5es text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-L5XKsngAzYhdo5es .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-L5XKsngAzYhdo5es .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-L5XKsngAzYhdo5es .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-L5XKsngAzYhdo5es .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-L5XKsngAzYhdo5es #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-L5XKsngAzYhdo5es .sequenceNumber{fill:white;}#mermaid-svg-L5XKsngAzYhdo5es #sequencenumber{fill:#333;}#mermaid-svg-L5XKsngAzYhdo5es #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-L5XKsngAzYhdo5es .messageText{fill:#333;stroke:none;}#mermaid-svg-L5XKsngAzYhdo5es .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-L5XKsngAzYhdo5es .labelText,#mermaid-svg-L5XKsngAzYhdo5es .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-L5XKsngAzYhdo5es .loopText,#mermaid-svg-L5XKsngAzYhdo5es .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-L5XKsngAzYhdo5es .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-L5XKsngAzYhdo5es .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-L5XKsngAzYhdo5es .noteText,#mermaid-svg-L5XKsngAzYhdo5es .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-L5XKsngAzYhdo5es .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-L5XKsngAzYhdo5es .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-L5XKsngAzYhdo5es .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-L5XKsngAzYhdo5es .actorPopupMenu{position:absolute;}#mermaid-svg-L5XKsngAzYhdo5es .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-L5XKsngAzYhdo5es .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-L5XKsngAzYhdo5es .actor-man circle,#mermaid-svg-L5XKsngAzYhdo5es line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-L5XKsngAzYhdo5es :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} create(request)save(session)sessionsessionappendMessage(sessionId, userMsg)appendEvent(event)okappendMessage(sessionId, asstMsg)appendEvent(event)okgetMessages(sessionId)findById(sessionId)eventsList<Message>delete(sessionId)delete(sessionId)ok

7.1 完整代码示例

java 复制代码
SessionService service = new DefaultSessionService(InMemorySessionRepository.builder().build());

// 1. 创建
Session session = service.create(
    CreateSessionRequest.builder()
        .userId("alice")
        .build()
);

// 2. 追加事件(便捷方法自动包装 Message 为 SessionEvent)
service.appendMessage(session.id(), new UserMessage("什么是 Spring AI?"));
service.appendMessage(session.id(), new AssistantMessage("Spring AI 是..."));

// 3. 获取消息列表(直接传给 LLM)
List<Message> history = service.getMessages(session.id());

// 4. 获取事件列表(用于过滤、检查、压缩)
List<SessionEvent> events = service.getEvents(session.id());

// 5. 删除单个会话
service.delete(session.id());

// 6. 删除所有过期会话(由调度器调用)
int removed = service.deleteExpiredSessions(Instant.now());

已删除的会话会完全从仓库移除,不存在墓碑状态。

7.2 过期清理

会话不会自动扫描过期------必须显式调用 deleteExpiredSessions(Instant)。建议通过定时任务触发:

java 复制代码
@Scheduled(fixedRate = 3_600_000) // 每小时执行一次
void sweepExpiredSessions() {
    int removed = sessionService.deleteExpiredSessions(Instant.now());
    log.info("已清理 {} 个过期会话", removed);
}

deleteExpiredSessions 会查找所有 expiresAt 早于给定时刻的会话,逐个删除,并返回删除数量。


8. 包结构一览

复制代码
org.springframework.ai.session          (Java 包)
├── Session.java                        -- 不可变元数据对象
├── SessionEvent.java                   -- 消息包装器
├── SessionService.java                 -- 生命周期 + 压缩 API
├── SessionRepository.java              -- 持久化 SPI
├── CreateSessionRequest.java           -- 会话创建参数构建器
├── EventFilter.java                    -- 事件检索的组合条件
├── DefaultSessionService.java          -- 默认服务实现
├── InMemorySessionRepository.java      -- 基于 ConcurrentHashMap 的仓库(测试用)
│
├── advisor/
│   └── SessionMemoryAdvisor.java       -- ChatClient 通知器(含自动压缩)
│
├── compaction/
│   ├── CompactionRequest.java          -- (会话, 事件列表, 事件数, 轮次数)
│   ├── CompactionResult.java           -- 压缩后事件 + 归档事件 + 度量信息
│   ├── CompactionStrategy.java         -- 策略 SPI
│   ├── CompactionTrigger.java          -- 触发器 SPI
│   ├── CompositeCompactionTrigger.java -- OR 组合触发器
│   ├── TurnCountTrigger.java           -- 按轮次数触发
│   ├── TokenCountTrigger.java          -- 按 token 数触发
│   ├── SlidingWindowCompactionStrategy.java   -- 滑动窗口保留最近 N 条
│   ├── TurnWindowCompactionStrategy.java      -- 按轮次窗口保留
│   ├── TokenCountCompactionStrategy.java      -- 按 token 预算裁剪
│   └── RecursiveSummarizationCompactionStrategy.java -- 递归摘要合成
│
└── tool/
    └── SessionEventTools.java          -- @Tool 注解的对话搜索(回忆存储)

9. 总结

Spring AI Session 通过元数据会话 + 独立事件日志的设计,实现了高效、可扩展的对话状态管理。核心机制包括:

  • 不可变 Session 保证轻量传递。
  • SessionEvent 为每条消息补充身份、时间、分支等关键信息。
  • 轮次(Turn) 作为原子单元,保证工具调用等复合交互不被割裂。
  • 合成摘要轮次 优雅地压缩历史,同时维持用户↔助手的交替模式。
  • 归档而非删除 使得回忆搜索成为可能。
  • 乐观并发事件排序 确保多线程环境下的数据一致性。

理解这些概念后,你可以轻松地将 Spring AI Session 集成到自己的应用中,实现有记忆、可压缩、可回忆的智能对话系统。