在上一篇文章《用 SpringAIAlibab 让高频问题实现毫秒级响应》中,我们利用 Spring AI Alibaba 的 Hook 机制,成功让 Agent 拥有了相似问题命中缓存时跳过整个 RAG 链路的效果。这大幅减少了 LLM 调用次数,使相似问题能够毫秒级响应。
但随着对话轮数的增加,我们陷入了一个新的尴尬困境:对话越智能,记忆越沉重。全量保留的历史记录不仅导致 Token 成本直线上升,更可能触碰大模型的上下文窗口上限;更糟糕的是,过长的上下文中夹杂的大量过时"噪音",反而分散了模型的注意力,导致回答质量下降。
如何在保留关键记忆与控制成本之间找到平衡?难道为了省钱就必须让 Agent"失忆"吗?
当然不。答案在于"上下文工程"中的核心策略:自适应摘要压缩。
今天,我们将深入探讨如何让 Agent 学会"遗忘"的艺术:从手写一个基础的 MessageSummarizationHook 开始,逐步演进到 Spring AI Alibaba 官方提供的生产级 SummarizationHook,给上下文"瘦身",让 Agent 在轻装上阵的同时,依然铭记核心价值。
随着 AI Agent 应用的深入,我们常常面临一个尴尬的困境:对话越智能,记忆越沉重。
记忆可以让 Agent 记住之前的会话内容。对于 AI Agent,记忆至关重要,因为它让它们能够记住先前的交互、从反馈中学习并适应用户偏好。随着 Agent 处理更复杂的任务和大量用户交互,这种能力对于效率和用户满意度都变得至关重要。
短期记忆让你的应用程序能够在单个线程或会话中记住先前的交互。
Spring AI Alibaba 将短期记忆作为 Agent 状态的一部分进行管理。
通过将这些存储在 Graph 的状态中,Agent 可以访问给定对话的完整上下文,同时保持不同对话之间的分离。状态使用 checkpointer 持久化到数据库(或内存),以便可以随时恢复线程。短期记忆在调用 Agent 或完成步骤(如工具调用)时更新,并在每个步骤开始时读取状态。
但是,在长轮次对话中,历史记录不断累积,不仅导致 Token 成本直线上升,更可能触碰大模型的上下文窗口上限。更糟糕的是,过长的上下文往往包含大量过时或无关的"噪音",反而分散了模型的注意力,导致回答质量下降。
如何在保留关键记忆与控制成本之间找到平衡?答案在于**"上下文工程"**中的核心策略:自适应摘要压缩。
本文将带你从手写实现出发,逐步演进到 Spring AI Alibaba 官方提供的 SummarizationHook,深入理解这一机制的设计思想与落地实践。
为什么需要"有损"记忆?
传统的短期记忆通常是将所有历史消息原封不动地存入状态(State)并传递给模型。这种全量保留策略在对话初期非常有效,但随着轮数增加,弊端尽显:
- 成本失控:每次请求都在为几年前的寒暄付费。
- 性能瓶颈:超长上下文显著增加首字延迟(TTFT)。
- 模型迷失:LLM 在处理过长文本时,容易出现"中间丢失"现象,忽略关键指令。
理想的记忆机制应当像人类一样:模糊掉具体的措辞,但提炼出事实、意图和待办事项。这就是我们要实现的"Token 自适应压缩"。
第一阶段:手写实现 MessageSummarizationHook
在 Spring AI Alibaba 的 ReactAgent 架构中,Hook 是拦截和处理消息流的绝佳位置。我们可以在模型调用前(BEFORE_MODEL)介入,动态调整发送给 LLM 的消息列表。
以下是一个基于社区实践的手写实现,它展示了核心逻辑:
java
@Slf4j
@RequiredArgsConstructor
@HookPositions({HookPosition.BEFORE_MODEL})
public class MessageSummarizationHook extends MessagesModelHook {
private static final String HISTORY_SUMMARY_KEY = "conversation_summary";
private final ChatModel summaryModel;
private final int maxTokensBeforeSummary;
private final int messagesToKeep;
@Override
public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
// 1. 估算 Token (简化算法:字符数 / 4)
int estimatedTokens = previousMessages.stream()
.mapToInt(m -> m.getText().length() / 4)
.sum();
if (estimatedTokens < maxTokensBeforeSummary) {
return new AgentCommand(previousMessages);
}
log.info("检测到上下文过长 ({} tokens),触发消息摘要机制...", estimatedTokens);
// 2. 确定需要被摘要的消息范围:保留最近 N 条,前面的全部总结
int messagesToSummarizeCount = previousMessages.size() - messagesToKeep;
if (messagesToSummarizeCount <= 0) {
return new AgentCommand(previousMessages);
}
List<Message> oldMessages = previousMessages.subList(0, messagesToSummarizeCount);
List<Message> recentMessages = previousMessages.subList(messagesToSummarizeCount, previousMessages.size());
// 3. 提取上一轮的摘要(如果有)
String previousSummary = extractPreviousSummary(previousMessages);
// 4. 生成新摘要(增量融合)
String newSummary = generateSummary(oldMessages, previousSummary);
// 5. 构建新的消息列表:摘要 SystemMessage + 最近原始消息
List<Message> newMessages = new ArrayList<>();
newMessages.add(new SystemMessage("## 之前对话摘要:\n" + newSummary));
newMessages.addAll(recentMessages);
// 6. 返回 REPLACE 命令
return new AgentCommand(newMessages, UpdatePolicy.REPLACE);
}
// ... generateSummary 和 extractPreviousSummary 方法略 ...
}
核心设计思路
- 阈值检测:实时估算 Token,超限则触发。
- 切片压缩 :保留最近 NN 轮详细对话,压缩早期历史。
- 增量融合:结合旧摘要与新对话片段,生成连贯的新摘要,避免记忆断片。
- 无感替换 :通过
UpdatePolicy.REPLACE将"瘦身"后的上下文注入 Agent。
这个实现虽然有效,但作为生产级代码,它还存在一些不足:
- Token 估算粗糙:简单的字符除法不够精确。
- 缺乏配置化:阈值、保留轮数硬编码或在构造函数中传递,不够灵活。
- 重复造轮子:每个项目都要写一遍类似的逻辑。
幸运的是,Spring AI Alibaba 社区听到了开发者的呼声,正式推出了官方的 SummarizationHook。
第二阶段:拥抱官方 SummarizationHook
Spring AI Alibaba 引入了生产级的 SummarizationHook,位于 com.alibaba.cloud.ai.graph.agent.hook.summarization 包下。它不仅封装了上述所有逻辑,还提供了更强大的配置能力和更精准的 Token 控制。
官方实现的核心优势
相比手写版本,官方 SummarizationHook 带来了以下提升:
- 精准的 Token 计算:内部集成了更准确的 Token 估算策略(支持 TikToken 等),不再依赖简单的字符除法。
- 灵活的配置项:支持通过 Builder 模式或配置文件轻松定制阈值、保留消息数、摘要 Prompt 模板等。
- 健壮的错误处理:内置了完善的异常捕获与降级策略,防止因摘要服务波动导致整个对话中断。
- 无缝集成 Graph :作为官方组件,它与
ReactAgent和Graph工作流的集成更加丝滑,支持自动状态管理。
如何使用官方 SummarizationHook
使用官方 Hook 非常简单,只需几行代码即可完成配置并注册到 Agent 中。
通过 Builder 模式构建 SummarizationHook 实例,指定摘要模型、触发阈值和保留策略:
java
import com.alibaba.cloud.ai.graph.agent.hook.summarization.SummarizationHook;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
SummarizationHook summarizationHook = SummarizationHook.builder()
.model(summaryChatModel) // 指定摘要模型
.maxTokens(4000) // 触发阈值:超过 4000 tokens 开始压缩
.messagesToKeep(5) // 保留最近 5 轮原始消息
.summaryPrompt(""" // 自定义摘要 Prompt (可选)
你是一个专业的对话记录员。
### 之前的对话摘要
{previous_summary}
### 新增的对话片段
{conversation_text}
### 任务
请合并生成一份连贯、精炼的新摘要。保留关键事实、用户偏好和待办事项。
去除寒暄和重复信息,字数控制在 800 字以内。
""")
.build();
官方 SummarizationHook 的执行流程与我们手写的逻辑一致,但在细节上更加严谨:
- 拦截请求 :在
BEFORE_MODEL阶段拦截消息流。 - 精确计数:使用模型对应的 Tokenizer 计算当前上下文的真实 Token 数。
- 智能切片 :若超限,自动计算切片点,保留最近的
messagesToKeep条消息。 - 增量摘要:
- 检查历史中是否存在已有的摘要(通常标记为特定的 System Message)。
- 若有,则将"旧摘要" + "被切片的旧消息"合并输入给摘要模型。
- 若无,则直接总结旧消息。
- 重构上下文:用新生成的摘要 System Message 替换掉被切片的旧消息,拼接保留的近期消息。
- 放行请求:将精简后的消息列表传递给主模型进行推理。
除非你有极其特殊的定制需求(如非标准的摘要存储格式、特殊的切片算法),否则强烈建议在生产环境中直接使用官方的 SummarizationHook。它不仅经过了社区的充分测试,还能让你从重复的样板代码中解放出来,专注于业务逻辑的实现。
让 Agent 学会"遗忘"的艺术
在 AI Agent 的演进道路上,"记性好"不代表"记得多",而在于"记得准"。
从手写的 MessageSummarizationHook 到官方成熟的 SummarizationHook,我们看到的不仅仅是一个组件的进化,更是 Spring AI Alibaba 生态对**上下文工程(Context Engineering)**理解的深化。
通过自适应压缩,我们让 Agent 学会了像人类一样整理记忆:遗忘琐碎的细节,铭记核心的价值。这不仅解决了技术上的长度限制问题,更是提升 Agent 长期交互体验、控制运营成本的关键一步。
未来,随着多模态记忆和向量检索的结合,这种"有损压缩"策略将与"无损检索"相辅相成,构建出更加智能、高效的下一代 Agent 记忆系统。而 Spring AI Alibaba 正通过一个个像 SummarizationHook 这样的标准化组件,让这一切变得触手可及。