Spring AI Alibaba 1.x 系列【64】 ReactAgent 长期记忆

文章目录

  • [1. 概述](#1. 概述)
    • [1.1 短期记忆(Short-Term Memory, STM)](#1.1 短期记忆(Short-Term Memory, STM))
    • [1.2 长期记忆(Long-Term Memory, LTM)](#1.2 长期记忆(Long-Term Memory, LTM))
    • [1.3 Spring AI Alibaba 记忆架构](#1.3 Spring AI Alibaba 记忆架构)
  • [2. Store 存储层](#2. Store 存储层)
    • [2.1 Store 接口](#2.1 Store 接口)
    • [2.2 BaseStore 抽象类](#2.2 BaseStore 抽象类)
    • [2.3 Store 实现类](#2.3 Store 实现类)
    • [2.4 StoreItem](#2.4 StoreItem)
  • [3. ReactAgent 集成案例](#3. ReactAgent 集成案例)
    • [3.1 存储配置](#3.1 存储配置)
    • [3.2 方式一:通过记忆工具实现](#3.2 方式一:通过记忆工具实现)
      • [3.2.1 获取记忆工具](#3.2.1 获取记忆工具)
      • [3.2.2 搜索记忆](#3.2.2 搜索记忆)
      • [3.2.3 保存记忆](#3.2.3 保存记忆)
      • [3.2.4 删除记忆](#3.2.4 删除记忆)
      • [3.2.5 构建 Agent](#3.2.5 构建 Agent)
      • [3.2.6 单元测试](#3.2.6 单元测试)
    • [3.3 方式二:通过 Hook 自动管理记忆](#3.3 方式二:通过 Hook 自动管理记忆)
      • [3.3.1 实现 Hook](#3.3.1 实现 Hook)
      • [3.3.2 构建 Agent](#3.3.2 构建 Agent)
      • [3.3.3 单元测试](#3.3.3 单元测试)
    • [3.4 结合【短期记忆 + 长期记忆 】](#3.4 结合【短期记忆 + 长期记忆 】)
    • [3.5 跨会话记忆共享](#3.5 跨会话记忆共享)
    • [3.6 用户偏好自动学习](#3.6 用户偏好自动学习)

1. 概述

智能体(Agent)用的记忆系统 可以让 AI 能像人一样:

  • 记得刚聊过什么(短期记忆)
  • 记住用户长期习惯、偏好(长期记忆)

1.1 短期记忆(Short-Term Memory, STM)

短期记忆 是智能体在处理当前任务 /单一会话周期内保持的信息,核心是保障当前交互的连贯性与实时推理能力。例如,开启一个会话中的对话历史、系统指令、中间推理步骤、工具返回结果等。

在很早之前,我们介绍过 Spring AI Alibaba 通过 CheckpointSaver 实现短期记忆 持久化,提供 MemoryMySQLRedisMongoPostgresOracle、文件等存储,同时支持支持在工具、Hook、模型拦截器中读写会话记忆、裁剪消息、过滤敏感内容、动态修改提示词,灵活定制 Agent 会话行为。

1.2 长期记忆(Long-Term Memory, LTM)

长期记忆是智能体跨会话、跨时间、永久持久化保存的信息,核心是实现个性化和持续学习,如用户偏好、重要事实、学习到的经验等。

常用技术架构:

  • 向量数据库 + RAG:文本向量化存储,语义检索召回
  • 知识图谱:结构化存储实体与关系,支持复杂关联推理

1.3 Spring AI Alibaba 记忆架构

两大记忆核心

  • 短期记忆 :按 threadId(对话线程 ID)管理,存储对话历史、消息状态、上下文
  • 长期记忆 :按 namespace/key(命名空间 + 键)存储,存储用户画像、偏好、习惯、持久化数据

ModelHook / 拦截器自动化机制:

  • beforeModel:调用 AI 前,自动从短期 + 长期记忆里读取内容,把记忆注入到提示词
  • afterModelAI 回答后,自动把新的对话内容学习并保存,更新短期记忆,重要信息自动沉淀到长期记忆

记忆工具

  • saveMemory:主动把内容存到长期记忆
  • getMemory:主动从长期记忆读取内容

2. Store 存储层

Spring AI Alibaba 长期记忆以 JSON 文档结构 存入 Store 存储层。

存储结构设计:

  • namespace :命名空间,类比文件夹,可按用户 ID、组织 ID、业务场景做层级隔离
  • key:记忆唯一标识,类比文件名
  • 支持层次化记忆组织 ,可通过内容过滤器实现跨命名空间检索

依赖核心类:

  • Store:记忆存储
  • StoreItem:记忆数据载体

2.1 Store 接口

多智能体系统中长期记忆存储的接口,提供持久化、跨会话的管理能力,支持分层命名空间和结构化数据存储。这与专注于短期图状态持久化的 CheckpointSaver(检查点保存器)不同。

核心特性

  • 分层命名空间:使用嵌套命名空间组织数据
  • 结构化数据 :存储基于 Map 的复杂数据结构
  • 搜索与过滤:按命名空间、键模式和内容查询数据
  • 分页:支持通过偏移量/限制处理大型结果集
  • 跨会话:数据在不同执行会话间持久化

子类:

Store 接口方法说明表:

方法名 作用描述 入参说明 返回值 异常说明
putItem 在指定命名空间存储数据项,存在同命名空间+同Key则覆盖更新 StoreItem:待存储的数据项 void 入参为 null 或非法时抛出 IllegalArgumentException
getItem 根据分层命名空间与Key查询数据项 List<String> namespace:分层命名空间路径 String key:数据项唯一键 Optional<StoreItem>:存在则返回数据项,不存在返回空Optional 命名空间/Key 为 null 或非法时抛出 IllegalArgumentException
deleteItem 根据命名空间与Key删除指定数据项 List<String> namespace:分层命名空间路径 String key:数据项唯一键 boolean:删除成功返回true,数据不存在返回false 命名空间/Key 为 null 或非法时抛出 IllegalArgumentException
searchItems 按自定义条件检索存储的数据项 StoreSearchRequest:搜索条件请求对象 StoreSearchResult:匹配的搜索结果集 搜索请求对象为 null 时抛出 IllegalArgumentException
listNamespaces 按条件查询所有可用命名空间列表 NamespaceListRequest:命名空间查询请求参数 List<String>:命名空间路径集合 请求参数为 null 时抛出 IllegalArgumentException
clear 清空存储中所有数据,操作不可逆 void
size 获取当前存储内数据项总数量 long:数据条目总数
isEmpty 判断当前存储是否为空 boolean:为空返回true,否则返回false

在指定命名空间中使用给定键存储一个数据项:

java 复制代码
	/**
	 * 在指定命名空间中使用给定键存储一个数据项。
	 * 如果具有相同命名空间和键的数据项已存在,则会进行更新。
	 * @param item 要存储的数据项
	 * @throws IllegalArgumentException 当数据项为 null 或无效时抛出
	 */
	void putItem(StoreItem item);

从指定命名空间中根据键删除数据项:

java 复制代码
	/**
	 * 从指定命名空间中根据键删除数据项。
	 * @param namespace 分层命名空间路径
	 * @param key 数据项键
	 * @return 如果数据项被删除则返回 true,如果不存在则返回 false
	 * @throws IllegalArgumentException 当命名空间或键为 null/无效时抛出
	 */
	boolean deleteItem(List<String> namespace, String key);

从指定命名空间中根据键获取数据项:

java 复制代码
	/**
	 * 从指定命名空间中根据键获取数据项。
	 * @param namespace 分层命名空间路径
	 * @param key 数据项键
	 * @return 如果找到数据项则返回包含该数据项的 Optional 对象,否则返回空
	 * @throws IllegalArgumentException 当命名空间或键为 null/无效时抛出
	 */
	Optional<StoreItem> getItem(List<String> namespace, String key);

根据提供的搜索条件搜索数据项:

java 复制代码
	/**
	 * 根据提供的搜索条件搜索数据项。
	 * @param searchRequest 搜索参数
	 * @return 包含匹配数据项的搜索结果
	 * @throws IllegalArgumentException 当搜索请求为 null 时抛出
	 */
	StoreSearchResult searchItems(StoreSearchRequest searchRequest);

根据提供的条件列出可用的命名空间:

java 复制代码
	/**
	 * 根据提供的条件列出可用的命名空间。
	 * @param namespaceRequest 命名空间列表查询参数
	 * @return 命名空间路径列表
	 * @throws IllegalArgumentException 当命名空间请求为 null 时抛出
	 */
	List<String> listNamespaces(NamespaceListRequest namespaceRequest);

清空存储库中的所有数据项:

java 复制代码
	/**
	 * 清空存储库中的所有数据项。
	 * <p>
	 * <strong>警告:</strong>此操作不可撤销,将删除所有已存储的数据。
	 * </p>
	 */
	void clear();

获取存储库中的数据项总数:

java 复制代码
	/**
	 * 获取存储库中的数据项总数。
	 * @return 已存储的数据项数量
	 */
	long size();

检查存储库是否为空:

java 复制代码
	/**
	 * 检查存储库是否为空。
	 * @return 如果存储库不包含任何数据项则返回 true,否则返回 false
	 */
	boolean isEmpty();

使用示例:

java 复制代码
// 存储用户偏好
StoreItem preferences = StoreItem.of(
    List.of("users", "user123", "preferences"),
    "ui_settings",
    Map.of("theme", "dark", "language", "en-US")
);
store.putItem(preferences);

// 获取数据
Optional<StoreItem> item = store.getItem(
    List.of("users", "user123", "preferences"),
    "ui_settings"
);

// 搜索
StoreSearchRequest searchRequest = StoreSearchRequest.builder()
    .namespace("users")
    .query("preferences")
    .limit(10)
    .build();
StoreSearchResult result = store.searchItems(searchRequest);

2.2 BaseStore 抽象类

Store 接口实现的抽象基类,提供通用的参数校验与工具方法。该类为 Store 接口的具体实现类提供基础支撑,保证统一的参数校验行为和通用工具方法,减少重复代码。

参数校验方法:

方法名称 访问权限 功能说明 校验规则
validatePutItem protected 校验存储数据项入参合法性 校验 StoreItem 非空、命名空间非空、Key 非空且非空白
validateGetItem protected 校验查询数据项入参合法性 校验命名空间非空、Key 非空且非空白
validateDeleteItem protected 校验删除数据项入参合法性 校验命名空间非空、Key 非空且非空白
validateSearchItems protected 校验搜索数据项入参合法性 校验搜索请求对象 StoreSearchRequest 非空
validateListNamespaces protected 校验查询命名空间入参合法性 校验命名空间查询请求对象 NamespaceListRequest 非空

Key 编解码工具方法:

方法名称 访问权限 功能说明 实现逻辑
createStoreKey protected 拼接命名空间+Key,生成 Base64 编码全局唯一存储键 封装 namespace、key 为 Map → JSON 序列化 → Base64 编码
parseStoreKey protected 解析 Base64 存储键,还原命名空间与 Key Base64 解码 → JSON 反序列 → 拆分出命名空间和键

命名空间匹配工具方法:

方法名称 访问权限 功能说明
startsWithPrefix protected 判断目标命名空间是否匹配指定前缀命名空间层级

搜索过滤与匹配方法:

方法名称 访问权限 功能说明
matchesSearchCriteria protected 综合匹配:命名空间前缀、文本关键词、自定义过滤条件,判断数据项是否符合搜索规则

排序比较工具方法:

方法名称 访问权限 功能说明
createComparator protected 根据搜索请求的排序字段、升降序,构建 StoreItem 比较器
compareByField private 按指定字段(创建时间/更新时间/Key/命名空间/自定义字段)对比两条数据项大小

校验 putItem 方法的参数合法性:

java 复制代码
	/**
	 * 校验 putItem 方法的参数合法性
	 * @param item 待校验的数据项
	 */
	protected void validatePutItem(StoreItem item) {
		if (item == null) {
			throw new IllegalArgumentException("数据项不能为空");
		}
		if (item.getNamespace() == null) {
			throw new IllegalArgumentException("命名空间不能为空");
		}
		if (item.getKey() == null || item.getKey().trim().isEmpty()) {
			throw new IllegalArgumentException("键名不能为 null 或空字符串");
		}
	}

校验 getItem 方法的参数合法性:

java 复制代码
	/**
	 * 校验 getItem 方法的参数合法性
	 * @param namespace 命名空间
	 * @param key 数据项键
	 */
	protected void validateGetItem(List<String> namespace, String key) {
		if (namespace == null) {
			throw new IllegalArgumentException("命名空间不能为空");
		}
		if (key == null) {
			throw new IllegalArgumentException("键名不能为 null");
		}
		if (key.trim().isEmpty()) {
			throw new IllegalArgumentException("键名不能为空字符串");
		}
	}

2.3 Store 实现类

Store 接口所有实现类:

实现类名称 存储介质 核心实现原理 适用场景 持久化能力 生产可用 特点说明
FileSystemStore 本地文件系统 按分层命名空间映射为目录结构,数据序列化为 JSON 文件 存储;加读写锁保证线程安全 单机部署、无中间件、本地持久化、开发/小型项目 支持磁盘持久化,重启数据不丢失 可用 无需额外数据库/中间件,跨会话持久化,兼容分层命名空间,自动清理空目录
DatabaseStore 关系型数据库 基于 JDBC 通用数据源,自建数据表;命名空间/结构数据序列化为 JSON 存入字段,兼容 MySQL、PostgreSQL、H2 企业级项目、需要 ACID 事务、多实例共享存储 数据库持久化,重启数据不丢失 推荐 标准 SQL 实现,自动建表,兼容多数据库,支持事务、高可靠可集群部署
InMemoryStore 内存 基于 ConcurrentHashMap 纯内存缓存存储 单元测试、本地开发、临时轻量应用、无需持久化 无,应用重启数据全部丢失 不推荐 无依赖、性能极高、开箱即用,只适合临时会话与测试
MongoLikeInMemoryStore 内存(模拟MongoDB) ConcurrentHashMap 模拟 MongoDB 文档型存储行为 开发测试、不想引入 MongoDB 依赖、适配Mongo风格业务 无,重启数据丢失 不推荐 接口行为对齐 MongoDB,仅本地模拟,生产需替换为真实 MongoDB 客户端
RedisLikeInMemoryStore 内存(模拟Redis) ConcurrentHashMap 模拟 Redis KV 缓存行为 开发测试、本地调试、暂不部署 Redis 环境 无,重启数据丢失 不推荐 模拟 Redis 读写语义,轻量化无依赖,生产需替换为真实 Redis 客户端

2.4 StoreItem

StoreItem 表示存储在 Store 中的数据项,支持分层命名空间。每个 StoreItem 包含命名空间路径、键、值以及创建和最后更新的时间戳。命名空间用于对数据进行分层组织,而键则是该命名空间内的唯一标识。

字段说明:

字段名 类型 中文说明 示例
namespace List<String> 分层命名空间路径,用于层级化组织数据 ["users", "user123", "preferences"]
key String 同一命名空间下的唯一键 profiletheme
value Map<String, Object> 数据项内容,结构化键值对格式 {name:John, mode:dark}
createdAt long 数据创建时间戳(毫秒) 1735689600000
updatedAt long 数据最后更新时间戳(毫秒) 1735689900000

使用示例:

java 复制代码
public class StoreItemExample {
    public static void main(String[] args) {
        // 创建一个简单数据项
        StoreItem item = StoreItem.of(
            List.of("users", "user123"),
            "profile",
            Map.of("name", "John Doe", "email", "john@example.com")
        );

        // 创建带有嵌套命名空间的数据项
        StoreItem preferences = StoreItem.of(
            List.of("users", "user123", "settings", "ui"),
            "theme",
            Map.of("mode", "dark", "fontSize", 14)
        );

        // 打印结果
        System.out.println("简单数据项:");
        System.out.println(item);
        
        System.out.println("\n嵌套命名空间数据项:");
        System.out.println(preferences);
    }
}

3. ReactAgent 集成案例

3.1 存储配置

这里使用 MySQL 数据库存储长期记忆,需要使用 DatabaseStore 在源码中可以看到它依赖于 DataSource

建表语句:

MySQL 建表 SQL

sql 复制代码
  CREATE TABLE IF NOT EXISTS ai_memory_store (
    id VARCHAR(255) PRIMARY KEY,
    namespace TEXT,
    key_name VARCHAR(500),
    value_json TEXT,
    created_at DATETIME,
    updated_at DATETIME
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

项目集成 MySQL 就不赘述了,直接配置一个 DatabaseStore

java 复制代码
@Configuration
public class MemoryConfig {

    @Bean
    public Store memoryStore(DataSource dataSource) {
        return new DatabaseStore(dataSource, "ai_memory_store");
    }
}

3.2 方式一:通过记忆工具实现

Agent 通过工具显式调用 Store 完成记忆的保存和读取。LLM 根据用户意图自主决定何时调用记忆工具。

3.2.1 获取记忆工具

从长期记忆存储中检索已保存的信息。Agent 使用此工具获取用户画像、偏好、历史对话等持久化数据。

java 复制代码
@Component
public class GetMemoryTool {

    /**
     * 获取记忆请求参数
     *
     * @param namespace 命名空间列表,层次化组织数据
     * @param key       数据项的唯一键名
     */
    public record Request(List<String> namespace, String key) {}

    /**
     * 获取记忆响应结果
     *
     * @param message 操作结果消息(成功/失败描述)
     * @param value   检索到的数据,未找到时为空Map
     */
    public record Response(String message, Map<String, Object> value) {}

    /**
     * 创建获取记忆工具的ToolCallback实例
     *
     * <p>工具执行流程:</p>
     * <ol>
     *   <li>从ToolContext获取RunnableConfig</li>
     *   <li>从RunnableConfig获取Store实例</li>
     *   <li>调用Store.getItem检索数据</li>
     *   <li>返回检索结果或失败消息</li>
     * </ol>
     *
     * @return 配置好的ToolCallback实例,可被Agent直接调用
     */
    public ToolCallback toolCallback() {
        BiFunction<Request, ToolContext, Response> function = (request, context) -> {
            // 从工具上下文获取运行配置
            Optional<RunnableConfig> configOpt = ToolContextHelper.getConfig(context);
            if (configOpt.isEmpty()) {
                return new Response("无法获取配置,获取失败", Map.of());
            }

            // 从配置中获取存储实例
            Store store = configOpt.get().store();
            if (store == null) {
                return new Response("未配置记忆存储,获取失败", Map.of());
            }

            // 从存储中检索数据项
            Optional<StoreItem> itemOpt = store.getItem(request.namespace(), request.key());
            if (itemOpt.isPresent()) {
                Map<String, Object> value = itemOpt.get().getValue();
                return new Response("找到记忆", value);
            }

            return new Response("未找到记忆", Map.of());
        };

        // 构建并返回工具回调
        return FunctionToolCallback.builder("getMemory", function)
                .description("从长期记忆中获取信息。参数:namespace=命名空间列表,key=键名")
                .inputType(Request.class)
                .build();
    }

3.2.2 搜索记忆

在长期记忆存储中搜索匹配的信息。Agent 使用此工具模糊查找相关记忆,支持关键词搜索和命名空间过滤。

java 复制代码
@Component
public class SearchMemoryTool {

    /**
     * 搜索记忆请求参数
     *
     * @param namespace 命名空间列表,限制搜索范围
     * @param query     搜索关键词,用于模糊匹配
     * @param limit     返回结果数量限制,默认10
     */
    public record Request(List<String> namespace, String query, int limit) {}

    /**
     * 搜索记忆响应结果
     *
     * @param message 操作结果消息,包含找到的记忆数量
     * @param results 搜索结果列表,每个元素为一条记忆的value
     */
    public record Response(String message, List<Map<String, Object>> results) {}

    /**
     * 创建搜索记忆工具的ToolCallback实例
     *
     * <p>工具执行流程:</p>
     * <ol>
     *   <li>从ToolContext获取RunnableConfig</li>
     *   <li>从RunnableConfig获取Store实例</li>
     *   <li>构建StoreSearchRequest并调用Store.searchItems</li>
     *   <li>提取所有匹配项的value并返回结果列表</li>
     * </ol>
     *
     * @return 配置好的ToolCallback实例,可被Agent直接调用
     */
    public ToolCallback toolCallback() {
        BiFunction<Request, ToolContext, Response> function = (request, context) -> {
            // 从工具上下文获取运行配置
            Optional<RunnableConfig> configOpt = ToolContextHelper.getConfig(context);
            if (configOpt.isEmpty()) {
                return new Response("无法获取配置,搜索失败", List.of());
            }

            // 从配置中获取存储实例
            Store store = configOpt.get().store();
            if (store == null) {
                return new Response("未配置记忆存储,搜索失败", List.of());
            }

            // 构建搜索请求并执行搜索
            List<StoreItem> items = store.searchItems(
                    StoreSearchRequest.builder()
                            .namespace(request.namespace())
                            .query(request.query())
                            .limit(request.limit() > 0 ? request.limit() : 10)
                            .build()
            ).getItems();

            // 提取匹配项的value
            List<Map<String, Object>> results = items.stream()
                    .map(StoreItem::getValue)
                    .toList();

            return new Response("搜索完成,找到 " + results.size() + " 条记忆", results);
        };

        // 构建并返回工具回调
        return FunctionToolCallback.builder("searchMemory", function)
                .description("搜索长期记忆。参数:namespace=命名空间列表,query=搜索关键词,limit=返回数量限制")
                .inputType(Request.class)
                .build();
    }
}

3.2.3 保存记忆

将信息持久化保存到长期记忆存储中。Agent 使用此工具保存用户画像、偏好、重要对话等需要跨会话保留的数据。

java 复制代码
@Component
public class SaveMemoryTool {

    /**
     * 保存记忆请求参数
     *
     * @param namespace 命名空间列表,层次化组织数据
     * @param key       数据项的唯一键名
     * @param value     要保存的数据内容,Map结构
     */
    public record Request(List<String> namespace, String key, Map<String, Object> value) {}

    /**
     * 保存记忆响应结果
     *
     * @param message 操作结果消息(成功/失败描述)
     * @param value   已保存的数据内容
     */
    public record Response(String message, Map<String, Object> value) {}

    /**
     * 创建保存记忆工具的ToolCallback实例
     *
     * <p>工具执行流程:</p>
     * <ol>
     *   <li>从ToolContext获取RunnableConfig</li>
     *   <li>从RunnableConfig获取Store实例</li>
     *   <li>构建StoreItem并调用Store.putItem保存</li>
     *   <li>返回保存结果或失败消息</li>
     * </ol>
     *
     * @return 配置好的ToolCallback实例,可被Agent直接调用
     */
    public ToolCallback toolCallback() {
        BiFunction<Request, ToolContext, Response> function = (request, context) -> {
            // 从工具上下文获取运行配置
            Optional<RunnableConfig> configOpt = ToolContextHelper.getConfig(context);
            if (configOpt.isEmpty()) {
                return new Response("无法获取配置,保存失败", Map.of());
            }

            // 从配置中获取存储实例
            Store store = configOpt.get().store();
            if (store == null) {
                return new Response("未配置记忆存储,保存失败", Map.of());
            }

            // 构建存储项并保存到数据库
            StoreItem item = StoreItem.of(request.namespace(), request.key(), request.value());
            store.putItem(item);

            return new Response("成功保存到长期记忆", request.value());
        };

        // 构建并返回工具回调
        return FunctionToolCallback.builder("saveMemory", function)
                .description("保存信息到长期记忆。参数:namespace=命名空间列表,key=键名,value=要保存的数据")
                .inputType(Request.class)
                .build();
    }
}

3.2.4 删除记忆

从长期记忆存储中移除已保存的信息。Agent 使用此工具清理过期数据、纠正错误记忆或删除敏感信息。

java 复制代码
Component
public class DeleteMemoryTool {

    /**
     * 删除记忆请求参数
     *
     * @param namespace 命名空间列表,定位数据存储位置
     * @param key       要删除的数据项的唯一键名
     */
    public record Request(List<String> namespace, String key) {}

    /**
     * 删除记忆响应结果
     *
     * @param message 操作结果消息(成功删除/未找到/失败)
     * @param value   空Map,删除操作不返回数据
     */
    public record Response(String message, Map<String, Object> value) {}

    /**
     * 创建删除记忆工具的ToolCallback实例
     *
     * <p>工具执行流程:</p>
     * <ol>
     *   <li>从ToolContext获取RunnableConfig</li>
     *   <li>从RunnableConfig获取Store实例</li>
     *   <li>调用Store.deleteItem删除指定数据项</li>
     *   <li>返回删除结果(成功/未找到)</li>
     * </ol>
     *
     * @return 配置好的ToolCallback实例,可被Agent直接调用
     */
    public ToolCallback toolCallback() {
        BiFunction<Request, ToolContext, Response> function = (request, context) -> {
            // 从工具上下文获取运行配置
            Optional<RunnableConfig> configOpt = ToolContextHelper.getConfig(context);
            if (configOpt.isEmpty()) {
                return new Response("无法获取配置,删除失败", Map.of());
            }

            // 从配置中获取存储实例
            Store store = configOpt.get().store();
            if (store == null) {
                return new Response("未配置记忆存储,删除失败", Map.of());
            }

            // 执行删除操作
            boolean deleted = store.deleteItem(request.namespace(), request.key());
            if (deleted) {
                return new Response("成功删除记忆", Map.of());
            }

            return new Response("未找到要删除的记忆", Map.of());
        };

        // 构建并返回工具回调
        return FunctionToolCallback.builder("deleteMemory", function)
                .description("从长期记忆中删除信息。参数:namespace=命名空间列表,key=键名")
                .inputType(Request.class)
                .build();
    }
}

3.2.5 构建 Agent

创建带记忆工具的 Agent

java 复制代码
@Configuration
public class MemoryAgentConfig {

    @Bean("memoryToolAgent")
    public ReactAgent memoryToolAgent(DashScopeChatModel dashScopeChatModel,
                                       SaveMemoryTool saveMemoryTool,
                                       GetMemoryTool getMemoryTool) {

        ToolCallback[] tools = new ToolCallback[]{
                saveMemoryTool.toolCallback(),
                getMemoryTool.toolCallback()
        };

        return ReactAgent.builder()
                .name("memory_tool_agent")
                .description("带长期记忆工具的智能助手")
                .systemPrompt("""
                        你是一个智能助手,具有长期记忆能力。
                        可用工具:
                        - saveMemory: 保存信息到长期记忆
                        - getMemory: 从长期记忆获取信息
                        """)
                .model(dashScopeChatModel)
                .tools(tools)
                .saver(new MemorySaver())
                .enableLogging(true)
                .build();
    }
}    

3.2.6 单元测试

单元测试类:

java 复制代码
    @Test
    void testMemoryTools() throws Exception {
        RunnableConfig config = RunnableConfig.builder()
                .threadId("test_tools_thread")
                .addMetadata("user_id", "user_001")
                .store(memoryStore)
                .build();

        // 保存记忆
        Optional<OverAllState> saveResult = memoryToolAgent.invoke(
                "请帮我记住:我最喜欢的颜色是蓝色。使用 saveMemory 工具保存,namespace=['user_preferences'], key='favorite_color', value={'color': '蓝色'}。",
                config
        );

        assertTrue(saveResult.isPresent());
        printLastMessage(saveResult.get());

        // 获取记忆
        Optional<OverAllState> getResult = memoryToolAgent.invoke(
                "我最喜欢的颜色是什么?使用 getMemory 工具获取,namespace=['user_preferences'], key='favorite_color'。",
                config
        );

        assertTrue(getResult.isPresent());
        AssistantMessage response = getLastAssistantMessage(getResult.get());
        System.out.println("获取记忆响应: " + response.getText());

        // 验证记忆已保存
        Optional<StoreItem> savedItem = memoryStore.getItem(List.of("user_preferences"), "favorite_color");
        assertTrue(savedItem.isPresent());
        assertEquals("蓝色", savedItem.get().getValue().get("color"));
    }

输出结果:

text 复制代码
获取记忆响应: 你最喜欢的颜色是 **蓝色**! 😊

数据库:

3.3 方式二:通过 Hook 自动管理记忆

Hook 拦截模型调用,自动加载画像注入消息、自动学习偏好保存存储,实现无感知的长期记忆。

3.3.1 实现 Hook

在模型调用前后自动加载和保存长期记忆:

  • beforeModel: 从长期记忆加载用户画像,注入到系统消息中
  • afterModel: 从对话中提取用户偏好,保存到长期记忆

实现记忆管理 Hook

java 复制代码
@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class MemoryHook extends ModelHook {

    private static final String USER_PROFILES_NAMESPACE = "user_profiles";
    private static final String USER_PREFERENCES_NAMESPACE = "user_preferences";

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

    /**
     * 模型调用前:加载用户画像并注入到系统消息
     */
    @Override
    public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
        Optional<Object> userIdOpt = config.metadata("user_id");
        if (userIdOpt.isEmpty()) {
            return CompletableFuture.completedFuture(Map.of());
        }

        String userId = (String) userIdOpt.get();
        Store store = config.store();

       if (store == null) {
            return CompletableFuture.completedFuture(Map.of());
        }

        // 加载用户画像
        Optional<StoreItem> profileOpt = store.getItem(List.of(USER_PROFILES_NAMESPACE), userId);
        if (profileOpt.isEmpty()) {
            return CompletableFuture.completedFuture(Map.of());
        }

        Map<String, Object> profile = profileOpt.get().getValue();
        String userContext = buildUserContext(profile);

        // 更新消息列表
        List<Message> messages = (List<Message>) state.value("messages").orElse(new ArrayList<>());
        List<Message> newMessages = injectUserContext(messages, userContext);

        return CompletableFuture.completedFuture(Map.of("messages", newMessages));
    }

    /**
     * 模型调用后:从对话中提取并保存用户偏好
     */
    @Override
    ublic CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
        Optional<Object> userIdOpt = config.metadata("user_id");
        if (userIdOpt.isEmpty()) {
            return CompletableFuture.completedFuture(Map.of());
        }

        String userId = (String) userIdOpt.get();
        Store store = config.store();

        if (store == null) {
            return CompletableFuture.completedFuture(Map.of());
        }

        // 提取用户偏好
        List<Message> messages = (List<Message>) state.value("messages").orElse(new ArrayList<>());
        extractAndSavePreferences(messages, userId, store);

        return CompletableFuture.completedFuture(Map.of());
    }

    /**
     * 构建用户上下文信息
     */
    private String buildUserContext(Map<String, Object> profile) {
        StringBuilder sb = new StringBuilder("【用户信息】\n");

        if (profile.containsKey("name")) {
            sb.append("姓名: ").append(profile.get("name")).append("\n");
        }
        if (profile.containsKey("age")) {
            sb.append("年龄: ").append(profile.get("age")).append("\n");
        }
        if (profile.containsKey("occupation")) {
            sb.append("职业: ").append(profile.get("occupation")).append("\n");
        }
        if (profile.containsKey("email")) {
            sb.append("邮箱: ").append(profile.get("email")).append("\n");
        }
        if (profile.containsKey("preferences")) {
            Object prefs = profile.get("preferences");
            if (prefs instanceof List) {
                sb.append("偏好: ").append(String.join(", ", (List<String>) prefs)).append("\n");
            }
        }

        return sb.toString();
    }

    /**
     * 将用户上下文注入到消息列表中
     */
    private List<Message> injectUserContext(List<Message> messages, String userContext) {
        List<Message> newMessages = new ArrayList<>();

        // 查找是否已存在 SystemMessage
        SystemMessage existingSystemMessage = null;
        int systemMessageIndex = -1;

        for (int i = 0; i < messages.size(); i++) {
            Message msg = messages.get(i);
            if (msg instanceof SystemMessage) {
                existingSystemMessage = (SystemMessage) msg;
                systemMessageIndex = i;
                break;
            }
        }

        // 更新或创建 SystemMessage
        SystemMessage enhancedSystemMessage;
        if (existingSystemMessage != null) {
            enhancedSystemMessage = new SystemMessage(
                    existingSystemMessage.getText() + "\n\n" + userContext
            );
        } else {
            enhancedSystemMessage = new SystemMessage(userContext);
        }

        // 构建新的消息列表
        if (systemMessageIndex >= 0) {
            for (int i = 0; i < messages.size(); i++) {
                if (i == systemMessageIndex) {
                    newMessages.add(enhancedSystemMessage);
                } else {
                    newMessages.add(messages.get(i));
                }
            }
        } else {
            newMessages.add(enhancedSystemMessage);
            newMessages.addAll(messages);
        }

        return newMessages;
    }

    /**
     * 从对话中提取用户偏好并保存
     */
    private void extractAndSavePreferences(List<Message> messages, String userId, Store store) {
        // 加载现有偏好
        Optional<StoreItem> prefsOpt = store.getItem(
                List.of(USER_PREFERENCES_NAMESPACE), userId + "_learned"
        );

        List<String> preferences = new ArrayList<>();
        if (prefsOpt.isPresent()) {
            Map<String, Object> prefsData = prefsOpt.get().getValue();
            Object items = prefsData.getOrDefault("items", new ArrayList<>());
            if (items instanceof List) {
                preferences.addAll((List<String>) items);
            }
        }

        // 提取新偏好
        for (Message msg : messages) {
            String content = msg.getText();
            if (content.contains("喜欢") || content.contains("偏好") || content.contains("爱好")) {
                if (!preferences.contains(content)) {
                    preferences.add(content);

                    // 保存更新后的偏好
                    Map<String, Object> prefsData = new HashMap<>();
                    prefsData.put("items", preferences);
                    prefsData.put("updatedAt", System.currentTimeMillis());

                    StoreItem item = StoreItem.of(
                            List.of(USER_PREFERENCES_NAMESPACE),
                            userId + "_learned",
                            prefsData
                    );
                    store.putItem(item);
                }
            }
        }
    }
}

3.3.2 构建 Agent

创建带自动记忆管理的 Agent

java 复制代码
    @Bean("autoMemoryAgent")
    public ReactAgent autoMemoryAgent(DashScopeChatModel dashScopeChatModel) {

        ModelHook memoryHook = new MemoryHook();

        return ReactAgent.builder()
                .name("auto_memory_agent")
                .description("自动管理长期记忆的智能助手")
                .systemPrompt("你是一个智能助手,会自动记住用户的信息和偏好。")
                .model(dashScopeChatModel)
                .hooks(List.of(memoryHook))
                .saver(new MemorySaver())
                .enableLogging(true)
                .build();
    }

3.3.3 单元测试

自动加载用户画像测试:

java 复制代码
    @Test
    void testAutoLoadUserProfile() throws Exception {
        RunnableConfig config = RunnableConfig.builder()
                .threadId("test_auto_load_thread")
                .addMetadata("user_id", "user_001")
                .store(memoryStore)
                .build();

        Optional<OverAllState> result = autoMemoryAgent.invoke(
                "请介绍一下我的个人信息。",
                config
        );

        assertTrue(result.isPresent());
        AssistantMessage response = getLastAssistantMessage(result.get());
        System.out.println("自动加载响应: " + response.getText());

        // 验证响应包含用户画像信息
        assertTrue(response.getText().contains("张三") ||
                   response.getText().contains("软件工程师") ||
                   response.getText().contains("28"),
                "Agent应该使用长期记忆中的用户信息");
    }

3.4 结合【短期记忆 + 长期记忆 】

组合使用:

  • 短期记忆(MemorySaver):维护当前会话对话上下文
  • 长期记忆(MemoryStore):持久化用户画像、职业、偏好等静态信息
  • 通过 ModelHook 统一拼接两类记忆注入提示词

核心效果:Agent 同时记得「本次聊天内容」+「用户永久资料」

协作流程:

复制代码
                    短期+长期记忆协作流程

  第一次对话
  ─────────────
  用户: "我今天在做一个Spring项目"
         │
         ▼
  ┌─────────────┐
  │ MemorySaver │ ←── 短期记忆:记录对话内容
  │ (短期记忆)   │     messages=[用户:Spring项目, Agent:好的...]
  └─────────────┘

  第二次对话
  ─────────────
  用户: "根据我的职业和今天的工作,给我一些建议"
         │
         ▼
  ┌─────────────────────────────────────────────┐
  │         MemoryHook.beforeModel              │
  │  1. 从config获取user_id                     │
  │  2. 从MySQL加载用户画像(长期记忆)           │
  │     → {name="李工程师", occupation="软件工程"}│
  │  3. 注入到SystemMessage                     │
  └─────────────────────────────────────────────┘
         │
         ▼
  ┌─────────────────────────────────────────────┐
  │         LLM推理                              │
  │  输入包含:                                  │
  │  - SystemMessage: "用户李工程师,软件工程师"   │ ←长期记忆
  │  - 历史 Message: "今天做Spring项目"          │ ←短期记忆
  └─────────────────────────────────────────────┘
         │
         ▼
  Agent回复: "作为软件工程师做Spring项目,建议..."
            ↑
            同时使用了长期记忆(职业)和短期记忆(Spring项目)

Agent 配置(同时启用两种记忆):

java 复制代码
ReactAgent agent = ReactAgent.builder()
    .saver(new MemorySaver())       // ← 短期记忆:保存对话历史
    .hooks(List.of(memoryHook))     // ← 长期记忆:Hook自动加载画像
    .store(databaseStore)           // ← 长期记忆:MySQL存储实例
    .build();

MemoryHook.beforeModel(加载长期记忆):

java 复制代码
public CompletableFuture<Map<String, Object>> beforeModel(...) {
    // 1. 从长期记忆加载用户画像
    Store store = config.store();
    Optional<StoreItem> profileOpt = store.getItem(
        List.of("user_profiles"), userId);

    // 2. 注入到SystemMessage
    String userContext = "用户信息:姓名=" + profile.get("name");
    SystemMessage enhancedMessage = new SystemMessage(
        existingSystemMessage.getText() + "\n\n" + userContext);

    // 3. 返回更新后的消息列表
    return CompletableFuture.completedFuture(Map.of("messages", newMessages));
}

实际效果示例:

java 复制代码
// 第一次对话
agent.invoke("我今天在做一个Spring项目", config);
// → 短期记忆保存: messages=[用户:Spring项目, Agent:好的]

// 第二次对话
agent.invoke("根据我的职业和工作给我建议", config);
// → Hook.beforeModel 加载长期记忆(职业=软件工程师)
// → MemorySaver 加载短期记忆(Spring项目)
// → LLM 综合两者回复: "作为软件工程师做Spring,建议..."

3.5 跨会话记忆共享

长期记忆与 threadId 会话解绑,同一用户不同会话可共享记忆:

  1. 不同 threadId 绑定同一个 user_id
  2. 共用同一个 MemoryStore
  3. 任意会话均可读取/写入该用户的长期记忆

长期记忆本身就是跨会话的,可以直接测试:

java 复制代码
    @Test
    void testMemoryAcrossSessions() throws Exception {
        // 会话1:保存信息
        RunnableConfig session1 = RunnableConfig.builder()
                .threadId("session_morning")
                .addMetadata("user_id", "user_cross")
                .store(memoryStore)
                .build();

        memoryToolAgent.invoke(
                "记住我的密码是 secret123。使用 saveMemory 保存,namespace=['credentials'], key='user_cross_password', value={'password': 'secret123'}。",
                session1
        );

        // 会话2:不同线程,同一用户,获取信息
        RunnableConfig session2 = RunnableConfig.builder()
                .threadId("session_afternoon")
                .addMetadata("user_id", "user_cross")
                .store(memoryStore)
                .build();

        Optional<OverAllState> result = memoryToolAgent.invoke(
                "我的密码是什么?使用 getMemory 获取,namespace=['credentials'], key='user_cross_password'。",
                session2
        );

        assertTrue(result.isPresent());
        AssistantMessage response = getLastAssistantMessage(result.get());
        System.out.println("跨会话响应: " + response.getText());

        // 验证长期记忆在不同会话间持久化
        assertTrue(response.getText().contains("secret123"),
                "长期记忆应该在不同会话间持久化");
    }

3.6 用户偏好自动学习

基于 afterModel 钩子,监听对话内容,自动提取用户喜好并存入长期记忆:

  1. 截每轮对话结束后的消息
  2. 简单关键词/NLP 提取用户偏好
  3. 自动更新 MemoryStore 用户偏好档案
  4. 后续对话自动加载偏好做个性化应答

相关推荐
道可云1 小时前
道可云荣登半导体AI智能体应用第一梯队,打造研发全链路新范式
人工智能·半导体
w_t_y_y2 小时前
知识体系——MCP(四)自定义mcp server和client
人工智能
quan26312 小时前
20260529,日常开发-数据库主从问题
java·mysql·主从·延迟
山川湖海2 小时前
AI时代快速学编程语言的陷阱(以Python为例)
大数据·人工智能·python
悟乙己2 小时前
因果机器学习DML效果与应用场景探索
人工智能·机器学习
z小猫不吃鱼2 小时前
13 Scaling Law 入门:模型规模、数据规模和计算量是什么关系?
人工智能·深度学习·机器学习
JacksonMx2 小时前
@Transactional 最佳实践
java·spring boot·spring·性能优化
Sincerelyplz2 小时前
【AI会议纪要实践】mapReduce、RAG 与结构化输出
java·后端·agent
一叶清辉2 小时前
CS336 Assignment 1 BPE分词器训练初版(朴素版基础上优化)及后续优化方向分析
人工智能