Skip to content

TIP

本页内容由本地原稿 04-记忆与线程.md 同步生成。 如果你要长期修改站点内容,优先回原稿修改,再执行 npm run docs:sync

第 4 章:记忆不是玄学,它依赖线程、状态和持久化

这章要解决什么问题

很多教程讲“记忆”时都很虚,像在讲人格和灵魂。
但落到工程里,记忆其实很现实。

你只要把下面三个角色搞清,就不容易迷糊:

  • threadId
  • RunnableConfig
  • Saver

这章的目标是:

把“记忆”从模糊体验,讲成一条清楚的工程链路。

1. 先用一句人话理解记忆

在 Spring AI Alibaba 里,记忆不是“模型天生记得你”。
它更像:

同一个会话标识下,框架把之前的状态和消息重新接回来。

所以请先记一句很关键的话:

同一个 threadId,才像同一个脑子。换了 threadId,就像换了一个新会话。

2. 先把三位主角分清

2.1 threadId

它是“这段会话是谁”的标识。

比如:

  • session-001
  • user-1001-chat
  • order-consulting-20260429

只要你在多次调用里使用同一个 threadId,框架就有机会把同一段会话接起来。

2.2 RunnableConfig

它不是一个普通配置类,而是一次运行时的上下文。

你常常会在里面放:

  • threadId
  • metadata
  • store
  • checkpoint 相关状态

2.3 Saver

它解决的是“状态往哪存”。
最常见的两种思路:

  • 存内存:MemorySaver
  • 存数据库或别的持久化介质:自定义 Saver

没有 saver,就算你给了 threadId,很多状态也无法稳定延续。

3. 把记忆链路画出来

看完这张图,你应该能立刻明白两件事:

  • threadId 是连接前后两轮的线
  • Saver 是状态真正落脚的地方

4. 最小记忆示例

我们先用官方示例里常见的 MemorySaver,做一个最小多轮记忆。

4.1 配置 Agent

java
@Bean
public MemorySaver memorySaver() {
    return new MemorySaver();
}

@Bean
public ReactAgent studyAgent(ChatModel chatModel, MemorySaver memorySaver) {
    return ReactAgent.builder()
            .name("memory_study_agent")
            .model(chatModel)
            .instruction("""
                    你是一个学习助教。
                    回答使用中文,并在多轮对话中保持上下文一致。
                    """)
            .saver(memorySaver)
            .build();
}

4.2 使用同一个 threadId

java
RunnableConfig config = RunnableConfig.builder()
        .threadId("session_001")
        .build();

AssistantMessage first = studyAgent.call("我叫小李,请记住。", config);
AssistantMessage second = studyAgent.call("我刚刚叫什么?", config);

System.out.println(first.getText());
System.out.println(second.getText());

这个例子背后最关键的点不是 call(...),而是:

java
RunnableConfig.builder().threadId("session_001").build();

同一个 threadId,是多轮记忆成立的前提。

这个调用方式在你本地官方示例里也能看到,参考:

  • AgentsExample.java(源码路径:D:/idea_space/spring-ai-alibaba/examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/AgentsExample.java
  • MemoryExample.java(源码路径:D:/idea_space/spring-ai-alibaba/examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/MemoryExample.java

5. 为什么 RunnableConfig 不只是 threadId

很多人第一次用多轮对话,只会写:

java
RunnableConfig.builder().threadId("session_001").build();

这当然没问题,但它只是第一层。

再往后,你还会往里面放:

  • metadata
  • store
  • 并行执行配置
  • checkpoint 相关配置

比如:

java
RunnableConfig config = RunnableConfig.builder()
        .threadId("session_001")
        .addMetadata("user_id", "u1001")
        .build();

这个 metadata 在后面做用户画像、长期记忆、权限隔离时都很有用。

6. 记忆不只是一段聊天记录

这是一个特别值得你现在就建立的认知。

很多人会把记忆理解成:

  • 把前几轮用户和模型的话拼起来

这只对了一半。

在 Agent 体系里,记忆至少可以分两层:

6.1 短期会话记忆

它关注:

  • 当前对话上下文
  • 这一轮之前说过什么
  • 最近的工具调用或系统状态

6.2 更长期的状态或业务记忆

它关注:

  • 用户偏好
  • 跨会话信息
  • 业务上下文
  • 某些图状态、checkpoint、store 数据

所以当你听到“记忆”这个词时,不要只想到聊天历史。
在 Spring AI Alibaba 里,它更像“运行状态管理”的总和。

7. 对照你们公司的工程,记忆是怎么落地的

你们的项目这块其实已经走得比“最小 demo”远了不少。

7.1 Saver 已经不是内存版,而是 MySQL 版

AiModelConfiguration.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/config/model/AiModelConfiguration.java),它会把 AgenMysqlSaver.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/config/hooks/AgenMysqlSaver.java) 注册进来。

这说明你们不是简单地“把状态放内存里”,而是做了持久化。

7.2 会话消息和图状态是分开存的

看下面几个实体:

  • AiChatRecordMessage.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/pojo/entity/AiChatRecordMessage.java
  • GraphThread.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/pojo/entity/GraphThread.java
  • GraphCheckpoint.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/pojo/entity/GraphCheckpoint.java

它们对应的表分别是:

  • ai_chat_record_message
  • graph_thread
  • graph_checkpoint

这三个表的职责可以先粗略理解成:

作用
ai_chat_record_message存聊天消息本身,比如 user / assistant / tool 内容
graph_thread存逻辑线程,也就是会话主线索
graph_checkpoint存图运行状态快照,比如节点位置和状态数据

这是一种非常典型的工程化思路:

聊天记录是一回事,Agent 的图状态又是另一回事。

7.3 AgenMysqlSaver 实际做了什么

AgenMysqlSaver.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/config/hooks/AgenMysqlSaver.java),你会发现它大致在做这些事:

  • 根据 threadId 查找已有 checkpoint
  • ai_chat_record_message 里把历史消息读出来
  • graph_checkpoint 里恢复图状态
  • 运行后把新的 checkpoint 继续写回去
  • 如果是新线程,还会往 graph_thread 里登记

如果你用一句特别直白的话来形容它:

它就是把“这个 Agent 上次思考到哪、说到哪”重新捡回来。

7.4 聊天消息服务又负责什么

AiChatRecordMessageServiceImpl.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/service/impl/AiChatRecordMessageServiceImpl.java)。

你会看到它会按 conversationId 查询历史消息,并区分:

  • messageType
  • content
  • toolContent
  • reasoningContent

这说明你们项目里的“记忆”并不是只存一句最终回答,而是把多种消息形态都考虑进去了。

8. 工程里为什么要把“聊天消息”和“checkpoint”分开

这是一个非常有工程价值的问题。

原因很简单:

  • 聊天消息更偏“业务对话记录”
  • checkpoint 更偏“运行时状态恢复”

如果把它们硬塞成一种数据结构,后面会很难处理:

  • 会话回放
  • 故障恢复
  • 状态调试
  • 多节点执行
  • Graph 续跑

分开之后,系统会更清楚:

  • 用户说过什么
  • 模型答过什么
  • Agent 执行到哪一步

9. 初学者最容易搞混的点

9.1 以为只要同一个用户,就一定有记忆

不一定。
真正直接起作用的,通常还是你运行时传进去的 threadId

9.2 以为有 threadId 就一定能记住

也不一定。
如果没有合适的 Saver,或者你的状态没有正确持久化,记忆链路还是会断。

9.3 以为记忆只和聊天有关

不对。
在 Agent 世界里,记忆还经常和:

  • 工具调用上下文
  • 工作流状态
  • store 中的业务数据

一起发生。

10. 本章小结

你现在最该记住的,不是某个类名,而是这条关系:

text
threadId 决定“是不是同一段会话”
RunnableConfig 决定“这次运行带了什么上下文”
Saver 决定“这些状态最后存到哪里”

你只要把这三句话吃透,再去看你们项目里的数据库记忆方案,就不会觉得它神秘。

后面等我们进入 RAG,你会发现“记忆”和“知识”又是两种完全不同的增强能力。
一个解决“记住你刚才说过什么”,一个解决“知道你没说但系统文档里写过什么”。

Built with VitePress. Deployed on Cloudflare Pages.