随着对话不断进行,累积的事件数量会逐渐逼近甚至超过模型的上下文窗口限制。
上下文压缩(Compaction)正是为解决这一问题而,------它在保证对话连贯性的前提下,对会话事件历史进行精简,使其始终适配上下文窗口。
整个压缩机制由两个可组合的抽象概念驱动:
- 触发器(Trigger):决定"何时"执行压缩。
- 策略(Strategy):决定"如何"执行压缩,即对事件历史做怎样的处理。
一、核心入口:SessionService.compact()
所有压缩操作都通过 SessionService.compact() 方法触发。该方法是唯一的入口点,其执行逻辑分为两步:
- 评估触发器:判断当前是否满足压缩条件。
- 执行策略:若触发器触发,则调用指定的压缩策略,并将压缩后的结果写回存储库。
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 摘要归档事件 | 是 | 需要最大限度保留语义信息,且可接受计算成本 |
所有策略均遵循轮次边界保护 和分支感知原则,确保压缩后的上下文在语义上依然连贯、完整。归档事件永久保留,可追溯,不会丢失任何历史信息。
通过灵活组合触发器和策略,我们变可以精细控制压缩的时机与方式,使长对话应用始终保持高效与可靠。