对话记忆

什么是对话记忆

AI记忆

  • AI模型本身是无状态的,是不会保留历史交互信息。
  • 当需要与AI多轮交互时,如果无法保持上下文(对话记忆),则AI模型可能会出现理解错误的情况。
  • 这一特性也算是AI模型的一个局限点,只能应用系统自行实现上下文保持。
  • SpringAI主要通过记忆、存储、检索三块来实现与AI保持上下文。

记忆,并非历史记录

  • 对话记忆:
    • 主要是围绕与AI模型对话过程中用于维持上下文信息,可能只是对话过程中的部分信息。
    • 比如最近的多少条对话信息,或者是语意相近的部分对话信息。
  • 对话历史记录:则是完整的对话记录,包含用户与AI模型所有的对话信息。

SpringAI实现的记忆

快速使用

  • 摸你一下与AI多轮交互,使用SpringAI提供的ChatMemory实现维持上下文

  • 构建保留最近5条消息的ChatMemory,用于与AI交互过程中实现消息记忆

    1
    2
    3
    4
    5
    // 聊天记忆
    MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
    // 最大5条聊天记录
    .maxMessages(5)
    .build();
  • 通过SpringAI提供在与AI交互过程中自动从ChatMemory添加&检索记忆的Advisor

    1
    2
    3
    4
    5
    // 自动从 chatMemory 增加&检索记忆的 Advisor
    MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
    // 优先级高一些
    .order(Ordered.HIGHEST_PRECEDENCE + 10000)
    .build();
  • Advisor添加至ChatClient。这里使用的在构建时默认的Advisor

    1
    2
    3
    4
    ChatClient.builder(chatModel)
    // 添加Log Advisor用来输出提示词
    .defaultAdvisors(new LogExampleAdvisor(), chatMemoryAdvisor)
    .build();
  • 模拟一个聊天会话,先创建聊天会话。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    private final ChatClient chatMemoryClient;
    private final String system = """
    You are an assistant and need to proactively greet users.
    The user information is as follows:
    ```nickname: %s```
    """;
    public String createChat() {
    // 生成一个huihuaID
    String cid = UUID.randomUUID().toString();
    chatMemoryClient.prompt()
    .system(String.format(system, "Jan"))
    // 将会话ID添加到 advisor context
    .advisors(spec -> spec.params(Map.of(
    ChatMemory.CONVERSATION_ID, cid)))
    .call()
    .content();

    return cid;
    }
  • 模拟与AI聊天。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void chatMemoryExample(String userMsg, String cid) {
    chatMemoryClient.prompt()
    .user(userMsg)
    // 将会话ID添加到 advisor context
    .advisors(spec -> spec.params(Map.of(
    ChatMemory.CONVERSATION_ID, cid)))
    .call()
    .content();
    }
  • 从结果输出上看每次与AI交互的提示词都包含了之前的消息了。(但是体验上不尽人意)

    输出

遇见的问题

  • 虽然用上了“记忆”,但是在体验上并不好,AI模型对这些“记忆”消息似乎不太关注。

    • 比如先输入“将我接下来输入的内容全部翻译成中文”,AI会回复好的。
    • 再输入“Who are you?”,AI当成问题来处理了,而不是直接翻译。
  • 以为是使用上有问题,然后就去DeepseekChatGpt平台上使用了一下,发现也是如此。

    Deepseek

    ChatGpt

ChatMemory

  • ChatMemory是SpringAI提供的记忆抽象,里面抽象了记忆的基本操作方法,如添加、获取、清理等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public interface ChatMemory {
    /**
    * Save the specified message in the chat memory for the specified conversation.
    */
    default void add(String conversationId, Message message) {
    Assert.hasText(conversationId, "conversationId cannot be null or empty");
    Assert.notNull(message, "message cannot be null");
    this.add(conversationId, List.of(message));
    }
    /**
    * Save the specified messages in the chat memory for the specified conversation.
    */
    void add(String conversationId, List<Message> messages);
    /**
    * Get the messages in the chat memory for the specified conversation.
    */
    List<Message> get(String conversationId);
    /**
    * Clear the chat memory for the specified conversation.
    */
    void clear(String conversationId);
    }
  • SpringAI只提供了一个MessageWindowChatMemory的实现。上面的案例使用的就是这个记忆实现。

    • 其特性是固定容量(条数)的消息记忆。
    • 当消息超出容量限制时,自动移除最早的消息。
    • MessageWindowChatMemory的存储默认使用的内存存储。(记忆存储下面会记录)
  • 如果需要其他特性的记忆,就只能开发者自行实现了。比如按时间周期保留记忆。

ChatMemoryRepository

  • ChatMemoryRepository是SpringAI抽象的记忆存储接口。

  • 以会话ID对聊天记忆进行存储、检索、删除等操作。下面是其源码

    ChatMemoryRepository

InMemoryChatMemoryRepository

  • InMemoryChatMemoryRepository是SpringAI默认自动配置的记忆存储。

  • 该记忆存储基于ConcurrentHashMap实现的内存存储方式。

  • MessageWindowChatMemory中默认使用的也是该存储方式。

    MessageWindowChatMemory

JdbcChatMemoryRepository

  • JdbcChatMemoryRepository是SpringAI提供的关系型数据库记忆存储。

  • 需要额外引入starter

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
    </dependency>
  • JdbcChatMemoryRepository提供了三个配置

    属性 说明 默认值
    spring.ai.chat.memory.repository.jdbc.initialize-schema 控制初始化 Schema 的时机。可选值:embeddedalwaysnever embedded
    spring.ai.chat.memory.repository.jdbc.schema 用于初始化的 Schema 脚本位置。支持 classpath: URL 及平台占位符。 classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-@@platform@@.sql
    spring.ai.chat.memory.repository.jdbc.platform 用于替换始化脚本中 @@platform@@ 占位符,指定平台标识。默认通过DataSource自动识别。 auto-detected
  • JdbcChatMemoryRepository的工作流程

    • 创建JdbcChatMemoryRepositorySchemaInitializer进行脚本初始化,创建SPRING_AI_CHAT_MEMORY表。

    • Mysql为例,下面是Mysql建表的脚本源码

      1
      2
      3
      4
      5
      6
      7
      8
      CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
      `conversation_id` VARCHAR(36) NOT NULL,
      `content` TEXT NOT NULL,
      `type` ENUM('USER', 'ASSISTANT', 'SYSTEM', 'TOOL') NOT NULL,
      `timestamp` TIMESTAMP NOT NULL,

      INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`)
      );
    • 在脚本初始化过程中会根据上面的三个配置选择初始化时机、脚本文件、以及数据库平台。

    • 根据DataSource创建方言JdbcChatMemoryRepositoryDialect,用于获取操作数据库表的Sql。

    • 构建JdbcChatMemoryRepository,在进行记忆操作时是通过方言Dialect获取Sql并使用Jdbc执行。

  • 自行模拟一个JdbcChatMemoryRepository

    • 需求:

      • 自行定义一个字段更丰富的存储表。(还是基于Mysql
      • 不使用脚本初始化过程。
      • 自行定义方言Dialect提供操作存储表的Sql。
      • 自定义JdbcChatMemoryRepository
    • 实现:

      • 定义存储表结构

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        CREATE TABLE `AI_CHAT_MEMORY` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
        `conversation_id` bigint(20) NOT NULL COMMENT '会话ID',
        `message` TEXT NOT NULL COMMENT '消息内容',
        `message_type` ENUM('USER', 'ASSISTANT', 'SYSTEM', 'TOOL') NOT NULL COMMENT '消息类型',
        `sort` TIMESTAMP NOT NULL COMMENT '排序',
        `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除,0否1是',
        `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
        `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
        PRIMARY KEY (`id`),
        INDEX `idx_conversation_id` (`conversation_id`)
        ) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='AI聊天记忆表';
      • 配置。手动创建表,关闭脚本初始化过程。

        1
        spring.ai.chat.memory.repository.jdbc.initialize-schema=never
      • 自行定义方言

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        public class AIChatMemoryRepositoryDialect implements JdbcChatMemoryRepositoryDialect {
        @Override
        public String getSelectMessagesSql() {
        return """
        SELECT
        message, message_type
        FROM
        AI_CHAT_MEMORY
        WHERE
        conversation_id = ?
        ORDER BY
        sort
        """;
        }
        @Override
        public String getInsertMessageSql() {
        return """
        INSERT INTO
        AI_CHAT_MEMORY (conversation_id, message, message_type, sort)
        VALUES (?, ?, ?, ?)
        """;
        }
        @Override
        public String getSelectConversationIdsSql() {
        // 没什么意义,暂不实现
        return "";
        }
        @Override
        public String getDeleteMessagesSql() {
        // 用逻辑删除
        return "UPDATE AI_CHAT_MEMORY SET is_deleted = 1 WHERE conversation_id = ?";
        }
        }
      • 配置JdbcChatMemoryRepository

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        @Bean
        public JdbcChatMemoryRepository jdbcChatMemoryRepository(JdbcTemplate jdbcTemplate) {
        return JdbcChatMemoryRepository.builder()
        .jdbcTemplate(jdbcTemplate)
        // 使用自定义的方言
        .dialect(new AIChatMemoryRepositoryDialect())
        .build();
        }
        @Bean
        public ChatClient jdbcChatMemoryClient(DeepSeekChatModel chatModel,
        JdbcChatMemoryRepository jdbcChatMemoryRepository) {
        // 聊天记忆
        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
        // 最大5条聊天记录
        .maxMessages(5)
        // 使用JDBC存储
        .chatMemoryRepository(jdbcChatMemoryRepository)
        .build();
        // 自动从 chatMemory 增加&检索记忆的 Advisor
        MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
        // 优先级高一些
        .order(Ordered.HIGHEST_PRECEDENCE + 10000)
        .build();
        return ChatClient.builder(chatModel)
        // 添加Log Advisor用来输出提示词
        .defaultAdvisors(new LogExampleAdvisor(), chatMemoryAdvisor)
        .build();
        }
      • 执行案例

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        public class JdbcChatMemoryExample {

        private final ChatClient jdbcChatMemoryClient;

        public String createChat() {
        // 生成会话ID
        return System.currentTimeMillis() + "";
        }

        public void chatMemoryExample(String userMsg, String cid) {
        jdbcChatMemoryClient.prompt()
        .user(userMsg)
        .advisors(spec -> spec.params(Map.of(
        ChatMemory.CONVERSATION_ID, cid)))
        .call()
        .content();
        }
        }
      • 案例运行结果

        存储结果

其他记忆存储

  • 除了上面两种记忆存储,SpringAI还提供了NoSql数据库存储以及图数据库存储的实现,这里先暂时简单了解一下,不做深入学习。

  • CassandraChatMemoryRepository

    • 基于Apache Cassandra实现的记忆存储

    • 需要添加以下依赖

      1
      2
      3
      4
      <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-starter-model-chat-memory-repository-cassandra</artifactId>
      </dependency>
    • 使用方式与JdbcChatMemoryRepository一致。

  • Neo4jChatMemoryRepository

    • 基于图数据库实现的记忆存储

    • 需要添加以下依赖

      1
      2
      3
      4
      <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-starter-model-chat-memory-repository-neo4j</artifactId>
      </dependency>
  • 对于这两种记忆存储,以后机会再进一步深入学习。

ChatMemoryAdvisor

  • ChatMemoryAdvisor是SpringAI提供的在与模型交互过程中自动添加/检索记忆的Advisor

  • 在上面的几个实用案例中都是使用的MessageChatMemoryAdvisor来自动管理记忆。

  • SpringAI提供三种不同的ChatMemoryAdvisor,如下

    • MessageChatMemoryAdvisor:指定ChatMemory来管理会话记忆。在与模型交互时从记忆库检索历史记忆,将其作为Message集合注入到Prompt

      MessageChatMemoryAdvisor

    • PromptChatMemoryAdvisor:也是指定ChatMemory来管理会话记忆。不同的是将历史记忆以纯文本形式追加至系统(SystemMessage)提示词中,这里是可以自定义模版来追加。(用法与MessageChatMemoryAdvisor一致的)

      PromptChatMemoryAdvisor

      追加模版

    • VectorStoreChatMemoryAdvisor:指定 VectorStore 实现管理会话记忆。通过从向量存储检索历史记忆,以纯文本形式追加至系统(SystemMessage)消息。(后续学习了向量数据库后再进一步学习向量记忆,理论上有效减少记忆增加的Tokens

总结

  • AI模型是无状态的,不具备对话记忆的能力;记忆是对话过程中用于维持上下文,非所有的历史记录。
  • SpringAI主要是通过ChatMemoryChatMemoryAdvisor来管理记忆。
  • SpringAI提供了多种记忆存储,默认是内存存储,其他扩展的如JDBC、图数据库等存储。

最后

  • 一个成熟的AI应用,其记忆存储肯定是一个较为复杂的解决方案。所以这里只是简单了解了SpringAI是如何来实现对话记忆的。
  • 对于SpringAI的基础使用我认为学习到这里算是结束了,到这里可以使用SpringAI快速的开发简单的AI应用,减少一些基础封装的过程。
  • 接下来将学习MCP/RAG/向量等进一步提升AI应用能力的技术使用。
  • 所有案例的源码,都会提交在GitHub上。本次案例包:com.spring.ai.example.memory