至此,我们的 RAG 系统已经完成了从“检索”到“验证”的完整闭环。但一个真正有生命的 AI 系统,不应该在对话结束后就“失忆”。
对话的结束,恰恰是进化的开始。

在前几个阶段,我们解决了效率、成本、精度和可信度的问题。而最后这一阶段,我们要解决的是“个性化”的问题。系统会在后台异步分析刚才的交互,提取用户的独特偏好(如“喜欢简洁”、“关注业务流程”),并将这些画像数据持久化存储。下次用户再来时,这些记忆将作为背景知识注入到“查询增强”阶段,让 AI 真正变得“越用越懂你”。这一阶段也负责写入语义缓存

传统的 RAG 系统往往是“无状态”的:每一轮对话都是独立的,AI 记不住用户的喜好,每次都像面对陌生人一样回答。

我们要构建的个性化闭环,核心在于引入长期记忆机制。其设计思想包含三个关键步骤:

  1. 异步洞察
    对话结束后,不阻塞用户,而是在后台启动一个“记忆分析师”(LLM)。它复盘刚才的问答,从中提炼出事实信息(如用户职业、所在地)和偏好信息(如回答风格、关注焦点)。
  2. 结构化沉淀:
    提取出的信息不是杂乱地存入日志,而是被结构化地更新到用户画像(User Profile)中。这里的关键是去重与合并:新发现的偏好要与旧记忆融合,而不是简单覆盖或重复堆砌。
  3. 动态注入
    记忆的价值在于使用。当用户再次发起提问时,系统会在“查询增强”阶段,自动从存储中加载该用户的画像,并将其作为System Prompt的一部分注入给模型。
    • 效果:AI 在回答前就已经知道:“这位用户喜欢简短的结论,不需要代码示例。”从而生成高度定制化的回答。

要实现这种跨会话的记忆能力,我们需要一个灵活且可靠的存储介质。在 Spring AI Alibaba 生态中,这一角色由 Store 接口承担。

Store 是 Spring AI 为非结构化/半结构化数据设计的统一存储抽象。它打破了传统关系型数据库对 Schema 的严格限制,采用了更贴近 AI 思维的组织方式:

  • Namespace(命名空间):类似于“文件夹”,用于逻辑隔离。例如 user_profilesorg_knowledge
  • Key(键):类似于“文件名”,唯一标识一条记忆,如 user_123_profile
  • Value(值):存储为 JSON 文档。这使得我们可以灵活地存储嵌套的画像数据(如包含偏好列表、历史摘要、身份标签的复杂对象),无需预先定义表结构。

虽然 Store 可以对接 Redis 或 MongoDB,但在 Java 企业级应用中,MySQL 往往是长期记忆的首选。

  • 数据可靠性:ACID 事务特性确保用户珍贵的画像数据绝不丢失。
  • JSON 原生支持:现代 MySQL 版本原生支持 JSON 类型字段,既能通过 Key 快速查找,又能利用 JSON 路径进行灵活的属性过滤(例如:“查找所有喜欢‘简洁风格’的用户”)。
  • 生态融合:Spring AI Alibaba 提供了 DatabaseStore 实现,只需改造一下,即可享受关系型数据库的稳健与 NoSQL 的灵活。

核心代码如下:

/**
 * <p>长期记忆进化 Hook</p>
 * 职责:在每轮对话结束后,分析交互内容,提取用户画像/偏好,并异步更新到长期存储 (Store)。
 **/
@Slf4j
@RequiredArgsConstructor
@HookPositions({HookPosition.AFTER_MODEL})
public class MemoryUpdaterHook extends ModelHook {

    private final ChatClient chatClient;
    private final SemanticCacheService semanticCacheService;
    // 定义 Store 的命名空间 key
    private static final String NAMESPACE_USER = "user_profiles";
    private static final String KEY_SUFFIX_PROFILE = "_profile";
    private static final String KEY_SUFFIX_PREFERENCES = "_preferences";

    private final ObjectMapper objectMapper = new ObjectMapper();

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

    @Override
    public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {

        return CompletableFuture.supplyAsync(() -> {
            try {
                // 1. 获取用户 ID
                String userId = (String) config.metadata("user_id").orElse(null);
                if (userId == null || userId.isBlank()) {
                    log.debug("未找到 user_id,跳过记忆更新");
                    return Map.of();
                }

                // 2. 获取 Store 实例
                Store store = config.store();
                if (store == null) {
                    log.warn("RunnableConfig 中未配置 Store,无法更新长期记忆");
                    return Map.of();
                }

                // 3. 提取本轮关键上下文 (最后一条用户消息 + 最后一条 AI 回复)
                List<Message> messages = (List<Message>) state.value("messages").orElse(new ArrayList<>());
                if (messages.size() < 2) {
                    return Map.of();
                }
                Message lastUserMsg = getLastMessageOfType(messages, MessageType.USER);
                Message lastAiMsg = getLastMessageOfType(messages, MessageType.ASSISTANT);
                if (lastUserMsg == null || lastAiMsg == null) {
                    return Map.of();
                }

                // 写入语义缓存
                String lastUserMsgText = lastUserMsg.getText();
                String lastAiMsgText = lastAiMsg.getText();
                try {
                    semanticCacheService.put(lastUserMsgText, lastAiMsgText);
                    log.debug("语义缓存已更新: {}", lastUserMsgText.substring(0, Math.min(20, lastUserMsgText.length())));
                } catch (Exception e) {
                    log.error("写入语义缓存失败,但不影响主流程", e);
                }

                // 4. 加载现有记忆
                Map<String, Object> currentProfile = loadCurrentProfile(store, userId);

                // 5. 调用 LLM 分析是否需要更新记忆
                MemoryUpdatePlan updatePlan = analyzeMemoryNeeds(
                        userId,
                        lastUserMsgText,
                        lastAiMsgText,
                        currentProfile
                );
                if (updatePlan.isEmpty()) {
                    return Map.of();
                }

                // 6. 执行更新
                applyMemoryUpdate(store, userId, updatePlan);

                log.info("长期记忆已更新 (User: {}): 新增{}条偏好,更新{}项画像",
                        userId, updatePlan.newPreferences.size(), updatePlan.profileUpdates.size());

                return Map.of();

            } catch (Exception e) {
                log.error("长期记忆更新失败,不影响主流程", e);
                return Map.of();
            }
        });
    }

    /**
     * 执行更新:将计划写入 Store
     */
    private void applyMemoryUpdate(Store store, String userId, MemoryUpdatePlan plan) {

        // 1. 更新基础画像 (Profile)
        if (!plan.profileUpdates.isEmpty()) {
            Optional<StoreItem> existingOpt = store.getItem(List.of(NAMESPACE_USER), userId + KEY_SUFFIX_PROFILE);
            Map<String, Object> currentData = existingOpt.map(StoreItem::getValue).orElse(new HashMap<>());

            // 合并新数据
            currentData.putAll(plan.profileUpdates);
            currentData.put("updated_at", System.currentTimeMillis());

            store.putItem(StoreItem.of(List.of(NAMESPACE_USER), userId + KEY_SUFFIX_PROFILE, currentData));
        }

        // 2. 更新偏好列表 (Preferences) - 采用追加策略,避免重复
        if (!plan.newPreferences.isEmpty()) {
            Optional<StoreItem> existingOpt = store.getItem(List.of(NAMESPACE_USER), userId + KEY_SUFFIX_PREFERENCES);
            List<String> currentPrefs = new ArrayList<>();

            if (existingOpt.isPresent()) {
                Object items = existingOpt.get().getValue().get("items");
                if (items instanceof List) {
                    currentPrefs = (List<String>) items;
                }
            }

            // 去重追加
            for (String newPref : plan.newPreferences) {
                if (!currentPrefs.contains(newPref)) {
                    currentPrefs.add(newPref);
                }
            }

            Map<String, Object> prefData = new HashMap<>();
            prefData.put("items", currentPrefs);
            prefData.put("updated_at", System.currentTimeMillis());

            store.putItem(StoreItem.of(List.of(NAMESPACE_USER), userId + KEY_SUFFIX_PREFERENCES, prefData));
        }
    }
    
    private Map<String, Object> loadCurrentProfile(Store store, String userId) {

        Map<String, Object> merged = new HashMap<>();

        // 加载基础画像
        Optional<StoreItem> profileOpt = store.getItem(List.of(NAMESPACE_USER), userId + KEY_SUFFIX_PROFILE);
        profileOpt.ifPresent(item -> merged.putAll(item.getValue()));

        // 加载偏好
        Optional<StoreItem> prefOpt = store.getItem(List.of(NAMESPACE_USER), userId + KEY_SUFFIX_PREFERENCES);
        if (prefOpt.isPresent()) {
            Object prefs = prefOpt.get().getValue().get("items");
            if (prefs instanceof List) {
                merged.put("preferences", prefs);
            }
        }
        return merged;
    }
}

回顾整个系列,我们利用 Spring AI 的 HookInterceptor 机制,像搭积木一样构建了一个具备全链路智能的企业级 RAG 系统。这五个阶段环环相扣,缺一不可:

阶段 核心组件 解决的问题 价值主张
1. 语义缓存 Cache Hook 响应速度 让高频问题毫秒级响应,大幅降低 Token 成本和延迟。
2. 摘要压缩 Summarization Hook 上下文长度 让长对话轻装上阵,在有限的窗口内保留核心信息,节省成本。
3. 工具精排 Tool Interceptor 检索质量 让检索结果去伪存真,通过“宽召回 + 模型精排”剔除噪音,只喂给模型最相关的文档。
4. 答案验证 Validation Interceptor 内容可信度 让最终输出可信可靠,通过“生成 - 验证 - 修正”的自我修正循环,杜绝幻觉和遗漏。
5. 个性化记忆 Memory Hook 用户体验 让 AI 越用越懂你,通过异步画像提取与长期存储,实现千人千面的定制化服务。

这不仅仅是五个独立的功能,而是一个有机整体

  • 缓存加速了常规问答,释放资源给复杂问题。
  • 摘要确保了长对话中缓存和检索的准确性。
  • 精排为验证环节提供了高质量的“事实依据”。
  • 验证保证了输出给用户的每一个字都经得起推敲。
  • 记忆则贯穿始终,让上述所有环节都带着“用户的视角”去执行。

实现如此复杂的“感知 - 记忆 - 增强”闭环,在传统开发模式下往往意味着巨大的工程成本。但在 Spring AI Alibaba 的加持下,这一切变得前所未有的简单:

  1. 原生组件支持
    我们用到的 Hook(用于缓存、摘要、记忆)和 Interceptor(用于精排、验证),正是 Spring AI 标准规范的一部分。Spring AI Alibaba 完美实现了这些接口。这意味着,构建全链路智能所需的“积木”都已经现成存在,你只需要关注业务逻辑,无需重复造轮子。

  2. 无缝生态整合
    它深度集成了阿里云的大模型服务(通义千问)、向量检索服务(DashVector)以及关系型数据库(MySQL)。从模型调用到向量存储,再到用户画像的持久化,所有组件都在同一个 Spring 生态内无缝流转。配置一个 DatabaseStore,就能立刻拥有基于 MySQL 的长期记忆能力。

  3. Java 友好
    对于庞大的 Java 开发者群体而言,最大的优势在于零学习曲线

    • 你依然使用熟悉的 Spring Boot 启动应用。
    • 你依然使用熟悉的方式去管理 Bean。
    • 你依然使用 AOP 思想处理切面逻辑。

    Spring AI Alibaba 将复杂的 AI 工作流编排和多智能体协作抽象成了标准的 Java 接口。这让 Java 团队能够以最小的迁移成本,构建出优秀的 AI 应用。

    从“能回答问题”到“回答得好”,再到“懂你所想”,我们完成了一次 AI 应用架构的进化之旅。

    通过语义缓存、摘要压缩、工具精排、答案验证、个性化记忆这五道防线,我们不仅构建了一个高效、精准、可信的 RAG 系统,更打造了一个具有持续进化能力的智能体。

    在 Spring AI Alibaba 的赋能下,Java 开发者不再是大模型时代的旁观者。利用熟悉的工具和强大的生态,我们完全有能力构建出既具备企业级稳健性,又拥有极致用户体验的下一代 AI 应用。

    现在,你的 AI 已经准备好了。它不仅聪明,而且懂你。

Logo

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

更多推荐