【Spring AI Session】上下文压缩

随着对话不断进行,累积的事件数量会逐渐逼近甚至超过模型的上下文窗口限制。

上下文压缩(Compaction)正是为解决这一问题而,------它在保证对话连贯性的前提下,对会话事件历史进行精简,使其始终适配上下文窗口。

整个压缩机制由两个可组合的抽象概念驱动:

  • 触发器(Trigger):决定"何时"执行压缩。
  • 策略(Strategy):决定"如何"执行压缩,即对事件历史做怎样的处理。

一、核心入口:SessionService.compact()

所有压缩操作都通过 SessionService.compact() 方法触发。该方法是唯一的入口点,其执行逻辑分为两步:

  1. 评估触发器:判断当前是否满足压缩条件。
  2. 执行策略:若触发器触发,则调用指定的压缩策略,并将压缩后的结果写回存储库。
java 复制代码
// 示例:当对话轮次超过 20 轮时触发压缩,保留最后 10 个事件
CompactionResult result = service.compact(
    sessionId,
    new TurnCountTrigger(20),
    SlidingWindowCompactionStrategy.builder().maxEvents(10).build()
);

System.out.println(result.eventsRemoved());        // 被归档的事件数量(即 archivedEvents().size())
System.out.println(result.compactedEvents());      // 压缩后保留的事件列表
System.out.println(result.archivedEvents());       // 被归档的事件列表(并未删除)
System.out.println(result.tokensEstimatedSaved()); // 估算节省的 token 数量

// 无条件触发压缩:传入一个永远返回 true 的触发器
service.compact(sessionId, req -> true, SlidingWindowCompactionStrategy.builder().maxEvents(10).build());

归档而非删除

archivedEvents() 返回的事件会从活动上下文窗口 中移除,但并未真正删除。这些事件在数据库中保留,并标记为 SessionEvent.isArchived() == true。它们在提示构建时会被过滤掉(EventFilter.active() 会排除归档事件),但用户仍可通过召回存储 (Recall Storage)进行关键词搜索(EventFilter.keywordSearch(...))。压缩从不破坏对话历史

CAS 写入安全

DefaultSessionService.compact() 在获取事件列表之前会先读取事件日志的版本号。如果在读取和写入之间,有其他写入者修改了日志,compactEvents() 将返回 false,压缩操作会被静默跳过------因为并发写入者已经处理过该会话。若无任何事件被归档,则跳过写操作。这一机制对于生产环境的持久化后端至关重要。


二、压缩触发器(Compaction Triggers)

触发器实现 CompactionTrigger 接口(一个 @FunctionalInterface),它根据当前的 CompactionRequest 决定是否应该执行压缩。

1. TurnCountTrigger(轮次触发)

当会话中的完整对话轮次 超过指定阈值时触发。轮次根级别(branch == null)的 USER 事件 为计数单位,且仅统计非合成(non-synthetic)事件。

java 复制代码
new TurnCountTrigger(20);  // 当轮次 > 20 时触发

2. TokenCountTrigger(Token 数量触发)

当估算的总 Token 数量达到或超过指定阈值时触发。

java 复制代码
// 默认使用 JTokkitTokenCountEstimator
TokenCountTrigger.builder().threshold(4000).build();

// 使用自定义估算器(例如适配不同模型的分词器)
TokenCountTrigger.builder().threshold(4000).tokenCountEstimator(myEstimator).build();

3. CompositeCompactionTrigger(组合触发)

将多个触发器组合,采用 或(OR) 语义------只要任意一个触发器触发,即执行压缩。

java 复制代码
CompactionTrigger trigger = CompositeCompactionTrigger.anyOf(
    new TurnCountTrigger(20),
    TokenCountTrigger.builder().threshold(4000).build()
);

三、压缩策略(Compaction Strategies)

策略实现 CompactionStrategy 接口(@FunctionalInterface),定义了对事件历史的具体处理方式。每个策略接收一个 CompactionRequest,其中包含会话元数据和完整的事件列表。

Token 计费说明

所有四种策略在估算归档事件的 Token 成本时,都使用统一的事件格式化器 。工具调用(tool calls)和工具响应(tool responses)会以其完整的格式化表示参与计数,而不仅仅是 getText() 的内容(对这两种类型来说,getText() 通常为空)。

消息类型 格式化表示
UserMessage / AssistantMessage / SystemMessage User: <text> / Assistant: <text> / System: <text>
AssistantMessage 含工具调用 Assistant [tool calls: name(args), ...]
ToolResponseMessage Tool [responses: name -> data, ...]

这种设计确保了 CompactionResult.tokensEstimatedSaved() 能准确反映被移除事件的完整 Token 成本,尤其适用于工具调用密集的对话轮次。

注意:RecursiveSummarizationCompactionStrategy 在构建发送给 LLM 的摘要提示时,也使用同样的格式化器,并提供了 eventFormatter 构建选项以便覆盖默认行为(详见后文)。


1. SlidingWindowCompactionStrategy(滑动窗口策略)

保留最后 N 个真实事件(real events) 。简单、可预测,无需调用 LLM。合成摘要事件(synthetic summary events)会被始终保留 并置于最前面,且不计入 maxEvents 配额。

java 复制代码
// 保留最后 20 个真实事件
SlidingWindowCompactionStrategy.builder().maxEvents(20).build();

// 使用自定义 Token 估算器
SlidingWindowCompactionStrategy.builder().maxEvents(20).tokenCountEstimator(myEstimator).build();

算法流程
#mermaid-svg-Oapb9oWaMSeHcf6d{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-Oapb9oWaMSeHcf6d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Oapb9oWaMSeHcf6d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Oapb9oWaMSeHcf6d .error-icon{fill:#552222;}#mermaid-svg-Oapb9oWaMSeHcf6d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Oapb9oWaMSeHcf6d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Oapb9oWaMSeHcf6d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Oapb9oWaMSeHcf6d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Oapb9oWaMSeHcf6d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Oapb9oWaMSeHcf6d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Oapb9oWaMSeHcf6d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Oapb9oWaMSeHcf6d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Oapb9oWaMSeHcf6d .marker.cross{stroke:#333333;}#mermaid-svg-Oapb9oWaMSeHcf6d svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Oapb9oWaMSeHcf6d p{margin:0;}#mermaid-svg-Oapb9oWaMSeHcf6d .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Oapb9oWaMSeHcf6d .cluster-label text{fill:#333;}#mermaid-svg-Oapb9oWaMSeHcf6d .cluster-label span{color:#333;}#mermaid-svg-Oapb9oWaMSeHcf6d .cluster-label span p{background-color:transparent;}#mermaid-svg-Oapb9oWaMSeHcf6d .label text,#mermaid-svg-Oapb9oWaMSeHcf6d span{fill:#333;color:#333;}#mermaid-svg-Oapb9oWaMSeHcf6d .node rect,#mermaid-svg-Oapb9oWaMSeHcf6d .node circle,#mermaid-svg-Oapb9oWaMSeHcf6d .node ellipse,#mermaid-svg-Oapb9oWaMSeHcf6d .node polygon,#mermaid-svg-Oapb9oWaMSeHcf6d .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Oapb9oWaMSeHcf6d .rough-node .label text,#mermaid-svg-Oapb9oWaMSeHcf6d .node .label text,#mermaid-svg-Oapb9oWaMSeHcf6d .image-shape .label,#mermaid-svg-Oapb9oWaMSeHcf6d .icon-shape .label{text-anchor:middle;}#mermaid-svg-Oapb9oWaMSeHcf6d .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Oapb9oWaMSeHcf6d .rough-node .label,#mermaid-svg-Oapb9oWaMSeHcf6d .node .label,#mermaid-svg-Oapb9oWaMSeHcf6d .image-shape .label,#mermaid-svg-Oapb9oWaMSeHcf6d .icon-shape .label{text-align:center;}#mermaid-svg-Oapb9oWaMSeHcf6d .node.clickable{cursor:pointer;}#mermaid-svg-Oapb9oWaMSeHcf6d .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Oapb9oWaMSeHcf6d .arrowheadPath{fill:#333333;}#mermaid-svg-Oapb9oWaMSeHcf6d .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Oapb9oWaMSeHcf6d .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Oapb9oWaMSeHcf6d .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Oapb9oWaMSeHcf6d .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Oapb9oWaMSeHcf6d .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Oapb9oWaMSeHcf6d .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Oapb9oWaMSeHcf6d .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Oapb9oWaMSeHcf6d .cluster text{fill:#333;}#mermaid-svg-Oapb9oWaMSeHcf6d .cluster span{color:#333;}#mermaid-svg-Oapb9oWaMSeHcf6d div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Oapb9oWaMSeHcf6d .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Oapb9oWaMSeHcf6d rect.text{fill:none;stroke-width:0;}#mermaid-svg-Oapb9oWaMSeHcf6d .icon-shape,#mermaid-svg-Oapb9oWaMSeHcf6d .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Oapb9oWaMSeHcf6d .icon-shape p,#mermaid-svg-Oapb9oWaMSeHcf6d .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Oapb9oWaMSeHcf6d .icon-shape .label rect,#mermaid-svg-Oapb9oWaMSeHcf6d .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Oapb9oWaMSeHcf6d .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Oapb9oWaMSeHcf6d .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Oapb9oWaMSeHcf6d :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入事件列表
分离合成事件
合成事件始终保留,置于输出最前
取最后 maxEvents 个真实事件
将切割点向前对齐到最近的根级别 USER 事件

(轮次边界保护)
输出: 合成事件 + 保留的真实事件


2. TurnWindowCompactionStrategy(轮次窗口策略)

保留最后 N 个完整对话轮次 。与滑动窗口不同,该策略绝不会在轮次中间切割 ------它总是归档完整的 用户↔助手 交换对。

java 复制代码
// 保留最后 10 个完整轮次
TurnWindowCompactionStrategy.builder().maxTurns(10).build();

// 使用自定义 Token 估算器
TurnWindowCompactionStrategy.builder().maxTurns(10).tokenCountEstimator(myEstimator).build();

算法流程
#mermaid-svg-Q5hWosRyTQ3TZbig{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-Q5hWosRyTQ3TZbig .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Q5hWosRyTQ3TZbig .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Q5hWosRyTQ3TZbig .error-icon{fill:#552222;}#mermaid-svg-Q5hWosRyTQ3TZbig .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Q5hWosRyTQ3TZbig .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Q5hWosRyTQ3TZbig .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Q5hWosRyTQ3TZbig .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Q5hWosRyTQ3TZbig .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Q5hWosRyTQ3TZbig .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Q5hWosRyTQ3TZbig .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Q5hWosRyTQ3TZbig .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Q5hWosRyTQ3TZbig .marker.cross{stroke:#333333;}#mermaid-svg-Q5hWosRyTQ3TZbig svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Q5hWosRyTQ3TZbig p{margin:0;}#mermaid-svg-Q5hWosRyTQ3TZbig .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Q5hWosRyTQ3TZbig .cluster-label text{fill:#333;}#mermaid-svg-Q5hWosRyTQ3TZbig .cluster-label span{color:#333;}#mermaid-svg-Q5hWosRyTQ3TZbig .cluster-label span p{background-color:transparent;}#mermaid-svg-Q5hWosRyTQ3TZbig .label text,#mermaid-svg-Q5hWosRyTQ3TZbig span{fill:#333;color:#333;}#mermaid-svg-Q5hWosRyTQ3TZbig .node rect,#mermaid-svg-Q5hWosRyTQ3TZbig .node circle,#mermaid-svg-Q5hWosRyTQ3TZbig .node ellipse,#mermaid-svg-Q5hWosRyTQ3TZbig .node polygon,#mermaid-svg-Q5hWosRyTQ3TZbig .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Q5hWosRyTQ3TZbig .rough-node .label text,#mermaid-svg-Q5hWosRyTQ3TZbig .node .label text,#mermaid-svg-Q5hWosRyTQ3TZbig .image-shape .label,#mermaid-svg-Q5hWosRyTQ3TZbig .icon-shape .label{text-anchor:middle;}#mermaid-svg-Q5hWosRyTQ3TZbig .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Q5hWosRyTQ3TZbig .rough-node .label,#mermaid-svg-Q5hWosRyTQ3TZbig .node .label,#mermaid-svg-Q5hWosRyTQ3TZbig .image-shape .label,#mermaid-svg-Q5hWosRyTQ3TZbig .icon-shape .label{text-align:center;}#mermaid-svg-Q5hWosRyTQ3TZbig .node.clickable{cursor:pointer;}#mermaid-svg-Q5hWosRyTQ3TZbig .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Q5hWosRyTQ3TZbig .arrowheadPath{fill:#333333;}#mermaid-svg-Q5hWosRyTQ3TZbig .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Q5hWosRyTQ3TZbig .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Q5hWosRyTQ3TZbig .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q5hWosRyTQ3TZbig .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Q5hWosRyTQ3TZbig .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q5hWosRyTQ3TZbig .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Q5hWosRyTQ3TZbig .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Q5hWosRyTQ3TZbig .cluster text{fill:#333;}#mermaid-svg-Q5hWosRyTQ3TZbig .cluster span{color:#333;}#mermaid-svg-Q5hWosRyTQ3TZbig div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Q5hWosRyTQ3TZbig .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Q5hWosRyTQ3TZbig rect.text{fill:none;stroke-width:0;}#mermaid-svg-Q5hWosRyTQ3TZbig .icon-shape,#mermaid-svg-Q5hWosRyTQ3TZbig .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q5hWosRyTQ3TZbig .icon-shape p,#mermaid-svg-Q5hWosRyTQ3TZbig .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Q5hWosRyTQ3TZbig .icon-shape .label rect,#mermaid-svg-Q5hWosRyTQ3TZbig .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q5hWosRyTQ3TZbig .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Q5hWosRyTQ3TZbig .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Q5hWosRyTQ3TZbig :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入事件列表
剥离合成事件,始终保留并置于最前
收集第一个 USER 事件之前的序言事件
将剩余事件按轮次分组

(每个轮次始于一个 USER 事件)
归档最旧的轮次,直到剩余轮次数 ≤ maxTurns
输出: 合成事件 + 序言 + 保留的轮次


3. TokenCountCompactionStrategy(Token 数量策略)

从最新到最旧遍历事件,保留一个连续的、总 Token 数不超过预算的事件后缀

java 复制代码
// 保持在 4000 个 Token 以内
TokenCountCompactionStrategy.builder().maxTokens(4000).build();

// 使用自定义估算器
TokenCountCompactionStrategy.builder().maxTokens(4000).tokenCountEstimator(myEstimator).build();

算法流程
#mermaid-svg-yCqThWpwAcYW89WB{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-yCqThWpwAcYW89WB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yCqThWpwAcYW89WB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yCqThWpwAcYW89WB .error-icon{fill:#552222;}#mermaid-svg-yCqThWpwAcYW89WB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yCqThWpwAcYW89WB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yCqThWpwAcYW89WB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yCqThWpwAcYW89WB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yCqThWpwAcYW89WB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yCqThWpwAcYW89WB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yCqThWpwAcYW89WB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yCqThWpwAcYW89WB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yCqThWpwAcYW89WB .marker.cross{stroke:#333333;}#mermaid-svg-yCqThWpwAcYW89WB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yCqThWpwAcYW89WB p{margin:0;}#mermaid-svg-yCqThWpwAcYW89WB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yCqThWpwAcYW89WB .cluster-label text{fill:#333;}#mermaid-svg-yCqThWpwAcYW89WB .cluster-label span{color:#333;}#mermaid-svg-yCqThWpwAcYW89WB .cluster-label span p{background-color:transparent;}#mermaid-svg-yCqThWpwAcYW89WB .label text,#mermaid-svg-yCqThWpwAcYW89WB span{fill:#333;color:#333;}#mermaid-svg-yCqThWpwAcYW89WB .node rect,#mermaid-svg-yCqThWpwAcYW89WB .node circle,#mermaid-svg-yCqThWpwAcYW89WB .node ellipse,#mermaid-svg-yCqThWpwAcYW89WB .node polygon,#mermaid-svg-yCqThWpwAcYW89WB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yCqThWpwAcYW89WB .rough-node .label text,#mermaid-svg-yCqThWpwAcYW89WB .node .label text,#mermaid-svg-yCqThWpwAcYW89WB .image-shape .label,#mermaid-svg-yCqThWpwAcYW89WB .icon-shape .label{text-anchor:middle;}#mermaid-svg-yCqThWpwAcYW89WB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yCqThWpwAcYW89WB .rough-node .label,#mermaid-svg-yCqThWpwAcYW89WB .node .label,#mermaid-svg-yCqThWpwAcYW89WB .image-shape .label,#mermaid-svg-yCqThWpwAcYW89WB .icon-shape .label{text-align:center;}#mermaid-svg-yCqThWpwAcYW89WB .node.clickable{cursor:pointer;}#mermaid-svg-yCqThWpwAcYW89WB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yCqThWpwAcYW89WB .arrowheadPath{fill:#333333;}#mermaid-svg-yCqThWpwAcYW89WB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yCqThWpwAcYW89WB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yCqThWpwAcYW89WB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yCqThWpwAcYW89WB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yCqThWpwAcYW89WB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yCqThWpwAcYW89WB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yCqThWpwAcYW89WB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yCqThWpwAcYW89WB .cluster text{fill:#333;}#mermaid-svg-yCqThWpwAcYW89WB .cluster span{color:#333;}#mermaid-svg-yCqThWpwAcYW89WB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yCqThWpwAcYW89WB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yCqThWpwAcYW89WB rect.text{fill:none;stroke-width:0;}#mermaid-svg-yCqThWpwAcYW89WB .icon-shape,#mermaid-svg-yCqThWpwAcYW89WB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yCqThWpwAcYW89WB .icon-shape p,#mermaid-svg-yCqThWpwAcYW89WB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yCqThWpwAcYW89WB .icon-shape .label rect,#mermaid-svg-yCqThWpwAcYW89WB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yCqThWpwAcYW89WB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yCqThWpwAcYW89WB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yCqThWpwAcYW89WB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入事件列表
分离合成事件,其 Token 成本先从预算中扣除
从最新到最旧遍历真实事件,累加 Token 成本

(使用统一格式化器)
当累加成本即将超出剩余预算时停止
得到连续后缀,再向前调整到根级别 USER 事件

(轮次边界保护)
输出: 合成事件 + 调整后的保留事件

该策略不会跳过单个超大事件,因为那样会产生不连续的空隙,破坏对话连贯性。它总是形成连续的后缀。


4. RecursiveSummarizationCompactionStrategy(递归摘要策略)

基于 LLM 的策略 ,使用 ChatClient 对被归档的事件生成摘要。摘要会以合成的用户+助手轮次 形式存入会话,使得后续的压缩操作可以在此基础上继续------形成滚动式、递归的压缩历史

java 复制代码
RecursiveSummarizationCompactionStrategy strategy =
    RecursiveSummarizationCompactionStrategy.builder(chatClient)
        .maxEventsToKeep(10)           // 活动窗口大小(保留的真实事件数)
        .overlapSize(2)                // 从活动窗口中提取送给摘要提示的事件数(必须 < maxEventsToKeep,默认 2)
        .systemPrompt("...")           // 可选的自定义系统提示
        .shadowPrompt("...")           // 可选的自定义用户阴影提示
        .tokenCountEstimator(myEst)    // 自定义估算器(默认 JTokkitTokenCountEstimator)
        .eventFormatter(myFormatter)   // 可选的自定义事件格式化器(见下文)
        .build();

构建校验overlapSize 必须 >= 0 且严格小于 maxEventsToKeep,否则抛出 IllegalArgumentException

LLM 调用失败处理 :若 LLM 返回空或空白摘要,策略会记录 WARN 级别日志并跳过本次压缩,事件历史保持不变。可通过注册失败回调来作出程序化响应:

java 复制代码
RecursiveSummarizationCompactionStrategy strategy =
    RecursiveSummarizationCompactionStrategy.builder(chatClient)
        .maxEventsToKeep(10)
        .onSummarizationFailure(req -> {
            log.error("Compaction failed for session {}", req.session().id());
            // 重试、告警、增加指标等
        })
        .build();

自定义事件格式化器 :策略在构建摘要提示时使用统一格式化器(见 [Token 计费说明](#Token 计费说明)),因此工具调用名称、参数和响应数据对 LLM 均可见。可通过 eventFormatter 覆盖以实现领域特定渲染或多语言摘要:

java 复制代码
RecursiveSummarizationCompactionStrategy strategy =
    RecursiveSummarizationCompactionStrategy.builder(chatClient)
        .maxEventsToKeep(10)
        .eventFormatter(event -> switch (event.getMessageType()) {
            case TOOL -> "Tool result: " + event.getMessage().getText();
            default   -> RecursiveSummarizationCompactionStrategy.formatEvent(event);
        })
        .build();

算法流程
#mermaid-svg-qR8UaHtumwt51vFS{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-qR8UaHtumwt51vFS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qR8UaHtumwt51vFS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qR8UaHtumwt51vFS .error-icon{fill:#552222;}#mermaid-svg-qR8UaHtumwt51vFS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qR8UaHtumwt51vFS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qR8UaHtumwt51vFS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qR8UaHtumwt51vFS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qR8UaHtumwt51vFS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qR8UaHtumwt51vFS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qR8UaHtumwt51vFS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qR8UaHtumwt51vFS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qR8UaHtumwt51vFS .marker.cross{stroke:#333333;}#mermaid-svg-qR8UaHtumwt51vFS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qR8UaHtumwt51vFS p{margin:0;}#mermaid-svg-qR8UaHtumwt51vFS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qR8UaHtumwt51vFS .cluster-label text{fill:#333;}#mermaid-svg-qR8UaHtumwt51vFS .cluster-label span{color:#333;}#mermaid-svg-qR8UaHtumwt51vFS .cluster-label span p{background-color:transparent;}#mermaid-svg-qR8UaHtumwt51vFS .label text,#mermaid-svg-qR8UaHtumwt51vFS span{fill:#333;color:#333;}#mermaid-svg-qR8UaHtumwt51vFS .node rect,#mermaid-svg-qR8UaHtumwt51vFS .node circle,#mermaid-svg-qR8UaHtumwt51vFS .node ellipse,#mermaid-svg-qR8UaHtumwt51vFS .node polygon,#mermaid-svg-qR8UaHtumwt51vFS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qR8UaHtumwt51vFS .rough-node .label text,#mermaid-svg-qR8UaHtumwt51vFS .node .label text,#mermaid-svg-qR8UaHtumwt51vFS .image-shape .label,#mermaid-svg-qR8UaHtumwt51vFS .icon-shape .label{text-anchor:middle;}#mermaid-svg-qR8UaHtumwt51vFS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qR8UaHtumwt51vFS .rough-node .label,#mermaid-svg-qR8UaHtumwt51vFS .node .label,#mermaid-svg-qR8UaHtumwt51vFS .image-shape .label,#mermaid-svg-qR8UaHtumwt51vFS .icon-shape .label{text-align:center;}#mermaid-svg-qR8UaHtumwt51vFS .node.clickable{cursor:pointer;}#mermaid-svg-qR8UaHtumwt51vFS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qR8UaHtumwt51vFS .arrowheadPath{fill:#333333;}#mermaid-svg-qR8UaHtumwt51vFS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qR8UaHtumwt51vFS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qR8UaHtumwt51vFS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qR8UaHtumwt51vFS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qR8UaHtumwt51vFS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qR8UaHtumwt51vFS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qR8UaHtumwt51vFS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qR8UaHtumwt51vFS .cluster text{fill:#333;}#mermaid-svg-qR8UaHtumwt51vFS .cluster span{color:#333;}#mermaid-svg-qR8UaHtumwt51vFS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qR8UaHtumwt51vFS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qR8UaHtumwt51vFS rect.text{fill:none;stroke-width:0;}#mermaid-svg-qR8UaHtumwt51vFS .icon-shape,#mermaid-svg-qR8UaHtumwt51vFS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qR8UaHtumwt51vFS .icon-shape p,#mermaid-svg-qR8UaHtumwt51vFS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qR8UaHtumwt51vFS .icon-shape .label rect,#mermaid-svg-qR8UaHtumwt51vFS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qR8UaHtumwt51vFS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qR8UaHtumwt51vFS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qR8UaHtumwt51vFS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入事件列表
分离合成事件与真实事件
计算原始切割点:最新的 maxEventsToKeep 个真实事件构成活动窗口
将切割点向前对齐到最近的轮次边界
提取: 先前的合成摘要 + 待归档事件 + 重叠事件(overlap)
将上述内容喂给 LLM 生成摘要
用新的合成摘要轮次替换所有被归档事件

(USER shadow + ASSISTANT summary)
输出: 摘要轮次 + 活动窗口

递归特性 :先前合成的摘要中的 ASSISTANT 文本会被作为 === PRIOR SUMMARY === 上下文再次喂给 LLM,因此每次摘要都建立在前一次的基础上,无需从头开始。


四、共通安全机制:轮次边界保护与分支感知

所有四种策略都共享一个由 CompactionUtils.snapToTurnStart 实施的安全规则:保留窗口的起始位置始终对齐到根级别(branch == null)的 USER 事件

如果原始切割点落在某一轮次的中间(例如助手回复或工具响应之后),该切割点会向前推进到下一个符合条件的 USER 事件。这可以避免保留工具结果或助手回复的同时,却丢失了发起该轮次的用户消息。
#mermaid-svg-sIaY4FIXHzhFTm4Z{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-sIaY4FIXHzhFTm4Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sIaY4FIXHzhFTm4Z .error-icon{fill:#552222;}#mermaid-svg-sIaY4FIXHzhFTm4Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sIaY4FIXHzhFTm4Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sIaY4FIXHzhFTm4Z .marker.cross{stroke:#333333;}#mermaid-svg-sIaY4FIXHzhFTm4Z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sIaY4FIXHzhFTm4Z p{margin:0;}#mermaid-svg-sIaY4FIXHzhFTm4Z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sIaY4FIXHzhFTm4Z .cluster-label text{fill:#333;}#mermaid-svg-sIaY4FIXHzhFTm4Z .cluster-label span{color:#333;}#mermaid-svg-sIaY4FIXHzhFTm4Z .cluster-label span p{background-color:transparent;}#mermaid-svg-sIaY4FIXHzhFTm4Z .label text,#mermaid-svg-sIaY4FIXHzhFTm4Z span{fill:#333;color:#333;}#mermaid-svg-sIaY4FIXHzhFTm4Z .node rect,#mermaid-svg-sIaY4FIXHzhFTm4Z .node circle,#mermaid-svg-sIaY4FIXHzhFTm4Z .node ellipse,#mermaid-svg-sIaY4FIXHzhFTm4Z .node polygon,#mermaid-svg-sIaY4FIXHzhFTm4Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sIaY4FIXHzhFTm4Z .rough-node .label text,#mermaid-svg-sIaY4FIXHzhFTm4Z .node .label text,#mermaid-svg-sIaY4FIXHzhFTm4Z .image-shape .label,#mermaid-svg-sIaY4FIXHzhFTm4Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-sIaY4FIXHzhFTm4Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sIaY4FIXHzhFTm4Z .rough-node .label,#mermaid-svg-sIaY4FIXHzhFTm4Z .node .label,#mermaid-svg-sIaY4FIXHzhFTm4Z .image-shape .label,#mermaid-svg-sIaY4FIXHzhFTm4Z .icon-shape .label{text-align:center;}#mermaid-svg-sIaY4FIXHzhFTm4Z .node.clickable{cursor:pointer;}#mermaid-svg-sIaY4FIXHzhFTm4Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sIaY4FIXHzhFTm4Z .arrowheadPath{fill:#333333;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sIaY4FIXHzhFTm4Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sIaY4FIXHzhFTm4Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sIaY4FIXHzhFTm4Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sIaY4FIXHzhFTm4Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sIaY4FIXHzhFTm4Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sIaY4FIXHzhFTm4Z .cluster text{fill:#333;}#mermaid-svg-sIaY4FIXHzhFTm4Z .cluster span{color:#333;}#mermaid-svg-sIaY4FIXHzhFTm4Z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sIaY4FIXHzhFTm4Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sIaY4FIXHzhFTm4Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-sIaY4FIXHzhFTm4Z .icon-shape,#mermaid-svg-sIaY4FIXHzhFTm4Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sIaY4FIXHzhFTm4Z .icon-shape p,#mermaid-svg-sIaY4FIXHzhFTm4Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sIaY4FIXHzhFTm4Z .icon-shape .label rect,#mermaid-svg-sIaY4FIXHzhFTm4Z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sIaY4FIXHzhFTm4Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sIaY4FIXHzhFTm4Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sIaY4FIXHzhFTm4Z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 切割后
切割前
切割点落在 a3
切割点移至
u1 (USER)
a1 (ASSISTANT)
u2 (USER)
a2 (ASSISTANT)
a3 (ASSISTANT)
cut_before
u3 (USER)
a3 (ASSISTANT)
u1 (USER)
a1 (ASSISTANT)
u2 (USER)
a2 (ASSISTANT)
u3 (USER)
a3 (ASSISTANT)

多智能体会话中的分支感知

在多智能体(multi-agent)会话中,UserMessage 事件可能出现在命名分支上(例如 branch="orch.researcher"),而不仅限于根级别。分支上的 UserMessage 是子智能体的内部提示,属于轮次内部事件,并非轮次边界

一个根轮次可能包含整个子智能体的交互过程:

复制代码
[branch=null]  USER:      "What's the weather in Paris?"      ← 真实轮次开始
[branch=null]  ASSISTANT: [tool call: delegate_to_agent]
[branch="sub"] USER:      "Fetch weather for Paris"           ← 子智能体内部提示
[branch="sub"] ASSISTANT: [tool call: get_weather]
[branch="sub"] TOOL:      {temp: "22C"}
[branch="sub"] ASSISTANT: "It's 22°C in Paris"
[branch=null]  ASSISTANT: "The weather in Paris is 22°C"

snapToTurnStart跳过所有分支事件 (无论消息类型),只有在遇到 branch == null 的 USER 事件(即 SessionEvent.isRootEvent() == true)时才会停止。这避免了切割点落在子智能体提示上,从而导致根轮次的用户消息被错误地留在归档窗口中。


五、总结

策略 核心逻辑 是否调用 LLM 适用场景
滑动窗口 保留最后 N 个事件 简单、固定大小窗口,快速且可预测
轮次窗口 保留最后 N 个完整轮次 需要确保轮次完整,避免截断用户提问
Token 数量 保留不超过预算的连续后缀 严格控制 Token 预算,需要精确容量
递归摘要 用 LLM 摘要归档事件 需要最大限度保留语义信息,且可接受计算成本

所有策略均遵循轮次边界保护分支感知原则,确保压缩后的上下文在语义上依然连贯、完整。归档事件永久保留,可追溯,不会丢失任何历史信息。

通过灵活组合触发器和策略,我们变可以精细控制压缩的时机与方式,使长对话应用始终保持高效与可靠。