Memory记忆可以让 Agent 记住之前的会话内容。对于 AI Agent,记忆至关重要,因为它让它们能够记住先前的交互、从反馈中学习并适应用户偏好。随着 Agent 处理更复杂的任务和大量用户交互,这种能力对于效率和用户满意度都变得至关重要。

短期记忆存储在 Graph 的状态中。

使用方法

在 Spring AI Alibaba 中,要向 Agent 添加短期记忆(会话级持久化),你需要在创建 Agent 时指定 checkpointer

配置短期记忆示例

import com.alibaba.cloud.ai.graph.agent.ReactAgent;

import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.alibaba.cloud.ai.graph.RunnableConfig;

// 配置 checkpointer
ReactAgent agent = ReactAgent.builder()
  .name("my_agent")
  .model(chatModel)
  .tools(getUserInfoTool)
  .saver(new MemorySaver())
  .build();

// 使用 thread_id 维护对话上下文
RunnableConfig config = RunnableConfig.builder()
  .threadId("1") // threadId 指定会话 ID
  .build();

agent.call("你好!我叫 Bob。", config);
agent.call("我叫什么?", config); // 输出Bob

在生产环境中

在生产环境中,使用数据库支持的 checkpointer,如RedisSaver、PostgresSaver、MysqlSaver、MongoSaver等

使用 Redis Checkpointer 示例

import com.alibaba.cloud.ai.graph.checkpoint.savers.RedisSaver;
import org.redisson.api.RedissonClient;

// 配置 Redis checkpointer
RedisSaver redisSaver = new RedisSaver(redissonClient);

ReactAgent agent = ReactAgent.builder()
  .name("my_agent")
  .model(chatModel)
  .tools(getUserInfoTool)
  .saver(redisSaver)
  .build();

记忆带来的上下文过长问题

保留所有对话历史是实现短期记忆最常见的形式。但较长的对话对历史可能会导致大模型 LLM 上下文窗口超限,导致上下文丢失或报错。

即使你在使用的大模型上下文长度足够大,大多数模型在处理较长上下文时的表现仍然很差。因为很多模型会被过时或偏离主题的内容"分散注意力"。同时,过长的上下文,还会带来响应时间变长、Token 成本增加等问题。

在 Spring AI ALibaba 中,ReactAgent 使用 messages 记录和传递上下文,其中包括指令(SystemMessage)和输入(UserMessage)。在 ReactAgent 中,消息(Message)在用户输入和模型响应之间交替,导致消息列表随着时间的推移变得越来越长。由于上下文窗口有限,许多应用程序可以从使用技术来移除或"忘记"过时信息中受益,即 “上下文工程”。

常见的解决方案包括:

  • 修剪消息。在调用 LLM 之前移除前 N 条或后 N 条消息
  • 删除消息。从 Graph 状态中永久删除消息
  • 总结消息。总结历史中较早的消息并用摘要替换它们
  • 自定义策略。自定义策略(例如消息过滤等)

这允许 Agent 在 reasoning-acting 循环中持续跟踪对话而不超过 LLM 的上下文窗口。

修剪消息

大多数 LLM 都有最大支持的上下文窗口(以 token 计)。

决定何时截断消息的一种方法是计算消息历史中的 token 数量,并在接近该限制时进行截断。

要在 Agent 中修剪消息历史,请使用 MessagesModelHook

MessageTrimmingHook 修剪消息示例

import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import org.springframework.ai.chat.messages.Message;
import java.util.ArrayList;
import java.util.List;

@HookPositions({HookPosition.BEFORE_MODEL})
public class MessageTrimmingHook extends MessagesModelHook {

  private static final int MAX_MESSAGES = 3;

  @Override
  public String getName() {
      return "message_trimming";
  }

  @Override
  public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
      if (previousMessages.size() <= MAX_MESSAGES) {
          return new AgentCommand(previousMessages); // 无需更改
      }

      // 保留第一条消息和最后几条消息
      Message firstMsg = previousMessages.get(0);
      int keepCount = previousMessages.size() % 2 == 0 ? 3 : 4;
      List<Message> recentMessages = previousMessages.subList(
          previousMessages.size() - keepCount,
          previousMessages.size()
      );

      List<Message> trimmedMessages = new ArrayList<>();
      trimmedMessages.add(firstMsg);
      trimmedMessages.addAll(recentMessages);

      // 使用 REPLACE 策略替换消息列表,只保留需要的消息
      return new AgentCommand(trimmedMessages, UpdatePolicy.REPLACE);
  }
}

// 使用
ReactAgent agent = ReactAgent.builder()
  .name("my_agent")
  .model(chatModel)
  .tools(tools)
  .hooks(new MessageTrimmingHook())
  .saver(new MemorySaver())
  .build();

RunnableConfig config = RunnableConfig.builder()
  .threadId("1")
  .build();

agent.call("你好,我叫 bob", config);
agent.call("写一首关于猫的短诗", config);
agent.call("现在对狗做同样的事情", config);
AssistantMessage finalResponse = agent.call("我叫什么名字?", config);

System.out.println(finalResponse.getText());
// 输出:你的名字是 Bob。你之前告诉我的。

删除消息

你可以从 Graph 状态中删除消息以管理消息历史。

这在你想要删除特定消息或清除整个消息历史时很有用。

要从 Graph 状态中删除消息,你可以在 Hook 中返回新的消息列表:

MessageDeletionHook 删除消息示例

import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import org.springframework.ai.chat.messages.Message;
import java.util.List;

@HookPositions({HookPosition.AFTER_MODEL})
public class MessageDeletionHook extends MessagesModelHook {

  @Override
  public String getName() {
      return "message_deletion";
  }

  @Override
  public AgentCommand afterModel(List<Message> previousMessages, RunnableConfig config) {
      if (previousMessages.size() > 2) {
          // 删除最早的两条消息,只保留剩余的消息
          List<Message> remainingMessages = previousMessages.subList(2, previousMessages.size());
          return new AgentCommand(remainingMessages, UpdatePolicy.REPLACE);
      }

      return new AgentCommand(previousMessages);
  }
}

删除所有消息

ClearAllMessagesHook 删除所有消息示例

import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import org.springframework.ai.chat.messages.Message;
import java.util.ArrayList;
import java.util.List;

@HookPositions({HookPosition.AFTER_MODEL})
public class ClearAllMessagesHook extends MessagesModelHook {

  @Override
  public String getName() {
      return "clear_all_messages";
  }

  @Override
  public AgentCommand afterModel(List<Message> previousMessages, RunnableConfig config) {
      // 删除所有消息,返回空列表
      return new AgentCommand(new ArrayList<>(), UpdatePolicy.REPLACE);
  }
}

警告:删除消息时,确保生成的消息历史有效。检查你使用的 LLM 提供商的限制。例如:

  • 某些提供商期望消息历史以 user 消息开始
  • 大多数提供商要求带有工具调用的 assistant 消息后跟相应的 tool 结果消息

DeleteOldMessagesHook 删除旧消息示例

import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import org.springframework.ai.chat.messages.Message;
import java.util.List;

@HookPositions({HookPosition.AFTER_MODEL})
public class DeleteOldMessagesHook extends MessagesModelHook {

  @Override
  public String getName() {
      return "delete_old_messages";
  }

  @Override
  public AgentCommand afterModel(List<Message> previousMessages, RunnableConfig config) {
      if (previousMessages.size() > 2) {
          // 删除最早的两条消息,只保留剩余的消息
          List<Message> remainingMessages = previousMessages.subList(2, previousMessages.size());
          return new AgentCommand(remainingMessages, UpdatePolicy.REPLACE);
      }

      return new AgentCommand(previousMessages);
  }
}

ReactAgent agent = ReactAgent.builder()
  .name("my_agent")
  .model(chatModel)
  .systemPrompt("请简洁明了。")
  .hooks(new DeleteOldMessagesHook())
  .saver(new MemorySaver())
  .build();

RunnableConfig config = RunnableConfig.builder()
  .threadId("1")
  .build();

// 第一次调用
agent.call("你好!我是 bob", config);
// 输出:[('human', "你好!我是 bob"), ('assistant', '你好 Bob!很高兴见到你...')]

// 第二次调用
agent.call("我叫什么名字?", config);
// 输出:[('human', "我叫什么名字?"), ('assistant', '你的名字是 Bob...')]

总结消息

如上所示,修剪或删除消息的问题在于你可能会丢失消息队列淘汰的信息。因此,一些应用程序受益于使用聊天模型总结消息历史的更复杂方法。

要在 Agent 中总结消息历史,可以使用自定义 Hook:

MessageSummarizationHook 总结消息示例

import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import java.util.ArrayList;
import java.util.List;

@HookPositions({HookPosition.BEFORE_MODEL})
public class MessageSummarizationHook extends MessagesModelHook {

  private final ChatModel summaryModel;
  private final int maxTokensBeforeSummary;
  private final int messagesToKeep;

  public MessageSummarizationHook(
      ChatModel summaryModel,
      int maxTokensBeforeSummary,
      int messagesToKeep
  ) {
      this.summaryModel = summaryModel;
      this.maxTokensBeforeSummary = maxTokensBeforeSummary;
      this.messagesToKeep = messagesToKeep;
  }

  @Override
  public String getName() {
      return "message_summarization";
  }

  @Override
  public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
      // 估算 token 数量(简化版)
      int estimatedTokens = previousMessages.stream()
          .mapToInt(m -> m.getText().length() / 4)
          .sum();

      if (estimatedTokens < maxTokensBeforeSummary) {
          return new AgentCommand(previousMessages);
      }

      // 需要总结
      int messagesToSummarize = previousMessages.size() - messagesToKeep;
      if (messagesToSummarize <= 0) {
          return new AgentCommand(previousMessages);
      }

      List<Message> oldMessages = previousMessages.subList(0, messagesToSummarize);
      List<Message> recentMessages = previousMessages.subList(messagesToSummarize, previousMessages.size());

      // 生成摘要
      String summary = generateSummary(oldMessages);

      // 创建摘要消息
      SystemMessage summaryMessage = new SystemMessage(
          "## 之前对话摘要:
" + summary
      );

      // 构建新的消息列表:摘要消息 + 保留的最近消息
      List<Message> newMessages = new ArrayList<>();
      newMessages.add(summaryMessage);
      newMessages.addAll(recentMessages);

      // 使用 REPLACE 策略替换消息列表
      return new AgentCommand(newMessages, UpdatePolicy.REPLACE);
  }

  private String generateSummary(List<Message> messages) {
      StringBuilder conversation = new StringBuilder();
      for (Message msg : messages) {
          conversation.append(msg.getMessageType())
                    .append(": ")
                    .append(msg.getText())
                    .append("
");
      }

      String summaryPrompt = "请简要总结以下对话:

" + conversation;

      ChatResponse response = summaryModel.call(
          new Prompt(new UserMessage(summaryPrompt))
      );

      return response.getResult().getOutput().getText();
  }
}

// 使用
ChatModel summaryModel = // ... 用于总结的模型(可以是更便宜的模型)

MessageSummarizationHook summarizationHook = new MessageSummarizationHook(
  summaryModel,
  4000,  // 在 4000 tokens 时触发总结
  20     // 总结后保留最后 20 条消息
);

ReactAgent agent = ReactAgent.builder()
  .name("my_agent")
  .model(chatModel)
  .hooks(summarizationHook)
  .saver(new MemorySaver())
  .build();

RunnableConfig config = RunnableConfig.builder()
  .threadId("1")
  .build();

agent.call("你好,我叫 bob", config);
agent.call("写一首关于猫的短诗", config);
agent.call("现在对狗做同样的事情", config);
AssistantMessage finalResponse = agent.call("我叫什么名字?", config);

System.out.println(finalResponse.getText());
// 输出:你的名字是 Bob!
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐