Spring AI Alibaba ------记忆管理(Memory)
💡 核心结论:一句话先记住 如果说 Short-term Memory(短期记忆) 是 Agent 的"系统内存(RAM)",聊完天、关掉当前对话(Thread)就清空;那么 Long-term Memory(长期记忆) 就是 Agent 的"专属硬盘(数据库)",哪怕你换台电脑重新登录,它依然记得你"不喝香菜、只写 Java"。
在 Spring AI Alibaba 中,想要打造一个拥有"超强记忆力"的专属 AI 助理,你必须把短期记忆(MemorySaver)和长期记忆(MemoryStore)双剑合璧!
🛑 一、 核心概念:长期记忆是怎么存的? 大白话: 长期记忆在系统里是以 JSON 文档的形式存着的。为了不乱套,官方搞了两个概念:
- Namespace(命名空间): 相当于文件夹 。比如你可以建一个叫
users的文件夹,专门存用户画像。 - Key(键): 相当于文件名 。比如叫
user_123。
💻 1. 基础热身:MemoryStore 怎么用?(数据库操作)
Java
import com.alibaba.cloud.ai.graph.store.stores.MemoryStore;
import com.alibaba.cloud.ai.graph.store.StoreItem;
import java.util.*;
// 1. 建一个存储库(生产环境请换成真实的数据库 Redis/MySQL 等实现)
MemoryStore store = new MemoryStore();
String userId = "my-user";
String applicationContext = "chitchat";
// ⭐ 定义文件夹路径:[用户ID, 业务场景]
List<String> namespace = List.of(userId, applicationContext);
// 2. 准备要存的数据
Map<String, Object> memoryData = new HashMap<>();
memoryData.put("rules", List.of("用户喜欢简短直接的语言", "用户只说中文和Java"));
memoryData.put("my-key", "my-value");
// 3. 存进去!(存入文件夹 namespace,文件名为 a-memory)
StoreItem item = StoreItem.of(namespace, "a-memory", memoryData);
store.putItem(item);
// 4. 精准读取
Optional<StoreItem> retrievedItem = store.getItem(namespace, "a-memory");
// 5. 模糊搜索(根据内容找记忆,甚至按向量相似度排序!)
List<StoreItem> items = store.searchItems(namespace, Map.of("my-key", "my-value"));
🛠️ 二、 玩法一:让 Agent 的"双手"去读写记忆(Tools 显式操作)
大白话: 这是最直接的办法------给大模型配两把"工具(Tools)",一把叫"查档案",一把叫"写档案"。当大模型觉得需要时,它会自己动手去翻。
💻 2. 让 Agent 自己去【读】长期记忆(抄作业区):
Java
// 1. 定义工具的输入输出
public record GetMemoryRequest(List<String> namespace, String key) {}
public record MemoryResponse(String message, Map<String, Object> value) {}
// 2. 搞一个存储库,塞点假数据
MemoryStore store = new MemoryStore();
Map<String, Object> userData = Map.of("name", "张三", "language", "中文");
store.putItem(StoreItem.of(List.of("users"), "user_123", userData));
// 3. ⭐ 打造"查档案"工具
BiFunction<GetMemoryRequest, ToolContext, MemoryResponse> getUserInfoFunction = (request, context) -> {
// 从上下文里掏出全局仓库
RunnableConfig runnableConfig = (RunnableConfig) context.getContext().get("config");
Optional<StoreItem> itemOpt = runnableConfig.store().getItem(request.namespace(), request.key());
if (itemOpt.isPresent()) {
return new MemoryResponse("找到用户信息", itemOpt.get().getValue());
}
return new MemoryResponse("未找到用户", Map.of());
};
ToolCallback getUserInfoTool = FunctionToolCallback.builder("getUserInfo", getUserInfoFunction)
.description("查询用户信息").inputType(GetMemoryRequest.class).build();
// 4. 挂载给 Agent
ReactAgent agent = ReactAgent.builder()
.name("memory_agent").model(chatModel)
.tools(getUserInfoTool) // 装备工具
.saver(new MemorySaver()) // 开启短期记忆
.build();
// 5. 跑起来!让它去查
RunnableConfig config = RunnableConfig.builder()
.threadId("session_001") // 当前聊天 ID
.store(store) // ⭐ 关键:把仓库挂在运行时配置上
.build();
agent.invoke("查询用户信息,namespace=['users'], key='user_123'", config);
💻 3. 让 Agent 自己去【写】长期记忆(抄作业区):
Java
// 1. 定义写入工具的输入
public record SaveMemoryRequest(List<String> namespace, String key, Map<String, Object> value) {}
// 2. ⭐ 打造"写档案"工具
BiFunction<SaveMemoryRequest, ToolContext, MemoryResponse> saveUserInfoFunction = (request, context) -> {
RunnableConfig runnableConfig = (RunnableConfig) context.getContext().get("config");
Store store = runnableConfig.store();
// 把 AI 传进来的数据直接怼进数据库
store.putItem(StoreItem.of(request.namespace(), request.key(), request.value()));
return new MemoryResponse("成功保存用户信息", request.value());
};
ToolCallback saveUserInfoTool = FunctionToolCallback.builder("saveUserInfo", saveUserInfoFunction)
.description("保存用户信息").inputType(SaveMemoryRequest.class).build();
ReactAgent agent = ReactAgent.builder()
.name("save_memory_agent").model(chatModel)
.tools(saveUserInfoTool).saver(new MemorySaver()).build();
// 3. 跑起来!命令它记住你
RunnableConfig config = RunnableConfig.builder().threadId("session_001").store(new MemoryStore()).build();
agent.invoke("我叫张三,请保存我的信息。使用 saveUserInfo 工具,namespace=['users'], key='user_123', value={'name': '张三'}", config);
🧠 三、 玩法二:大管家偷偷喂料(ModelHook 自动加载,最优雅 ⭐⭐⭐⭐⭐)
大白话: 用 Tool 让大模型自己查,费 Token 也费脑子。最高级的做法是:每次大模型开口前,系统管家(Hook)先去查档案,查完后悄悄拼接在系统提示词(System Prompt)里塞给它。大模型会觉得:"哎?我怎么天然就知道这哥们的喜好?"
💻 4. 使用 ModelHook 全自动管理记忆(抄作业区):
Java
// 1. 创建拦截器/钩子
ModelHook memoryInterceptor = new ModelHook() {
@Override public String getName() { return "memory_interceptor"; }
@Override public HookPosition[] getHookPositions() {
return new HookPosition[]{HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL};
}
// ⭐ 在大模型思考前,硬核塞记忆
@Override
public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
String userId = (String) config.metadata("user_id").orElse(null);
if (userId == null) return CompletableFuture.completedFuture(Map.of());
// 去长期记忆里扒出这个人的画像
Optional<StoreItem> itemOpt = config.store().getItem(List.of("user_profiles"), userId);
if (itemOpt.isPresent()) {
Map<String, Object> profile = itemOpt.get().getValue();
String userContext = String.format("用户信息:姓名=%s, 年龄=%s, 偏好=%s",
profile.get("name"), profile.get("age"), profile.get("preferences"));
// 把这堆信息,偷偷加进原有的聊天记录(Messages)的系统人设里
List<Message> messages = (List<Message>) state.value("messages").orElse(new ArrayList<>());
List<Message> newMessages = new ArrayList<>();
// (此处省略遍历找原 SystemMessage 的细节逻辑,总之就是把 userContext 拼接到 SystemMessage 里面去,或者新建一个放在最前面)
newMessages.add(new SystemMessage(userContext));
newMessages.addAll(messages);
// 狸猫换太子,把加了记忆的记录传给大模型
return CompletableFuture.completedFuture(Map.of("messages", newMessages));
}
return CompletableFuture.completedFuture(Map.of());
}
// (可选) afterModel 里还可以写逻辑:对话结束后,自动总结并存入长期记忆
};
// 2. 挂载 Hook
ReactAgent agent = ReactAgent.builder().name("memory_agent").model(chatModel)
.hooks(memoryInterceptor).saver(new MemorySaver()).build();
// 3. 准备数据并运行
MemoryStore memoryStore = new MemoryStore();
memoryStore.putItem(StoreItem.of(List.of("user_profiles"), "user_001", Map.of("name", "王小明", "age", 28)));
RunnableConfig config = RunnableConfig.builder().threadId("session_001").addMetadata("user_id", "user_001").store(memoryStore).build();
// AI 虽然没见过你,但 Hook 已经告诉它了!
agent.invoke("请介绍一下我的信息。", config);
🤝 四、 终极体:长短期记忆双管齐下 (跨会话不迷路)
大白话: 如果你想做一个生产级的完美 Agent。你必须:
- 用
MemorySaver+ 相同的threadId来保证用户在这一次聊天框里上下文不丢(短期)。 - 用
MemoryStore+ 相同的user_id来保证用户就算第二天新开了一个聊天框,它还记得用户的身份和职业(长期)。
💻 5. 长短期记忆双拼代码:
Java
// (Hook 的定义与上文类似,主要是在 beforeModel 中拼接长期记忆)
ModelHook combinedMemoryHook = new ModelHook() { /*...*/ };
// 核心配置:同时注入 hook(管长期) 和 saver(管短期)
ReactAgent agent = ReactAgent.builder()
.name("combined_memory_agent")
.model(chatModel)
.hooks(combinedMemoryHook) // ⭐ 长期记忆的搬运工
.saver(new MemorySaver()) // ⭐ 短期记忆的保管箱
.build();
// 提前在库里存好老底(长期)
MemoryStore memoryStore = new MemoryStore();
memoryStore.putItem(StoreItem.of(List.of("profiles"), "user_002", Map.of("name", "李工程师", "occupation", "软件工程师")));
RunnableConfig config = RunnableConfig.builder()
.threadId("combined_thread") // 短期记忆靠这个认人
.addMetadata("user_id", "user_002") // 长期记忆靠这个认人
.store(memoryStore).build();
// 聊第一句(此时它用了长期记忆"李工程师",并把当前聊天放进短期记忆)
agent.invoke("我今天在做一个 Spring 项目。", config);
// 聊第二句(此时它既知道你是谁(长期),又知道你在干嘛(短期)!)
agent.invoke("根据我的职业和今天的工作,给我一些建议。", config);
🎯 终极秒记口诀 一次聊天短期记,Saver 搞定省力气; 要想档案永不丢,Store 上阵做登记。 Tools 读写随手用,手动存取很给力; 想要优雅显神奇,Hook 拦截偷喂你; 长短结合跨会话,最强助理就是你!