Spring with AI (6): 记忆保持——会话与长期记忆

本文代码:
https://github.com/JunTeamCom/ai-demo/tree/release-6.0 (JDBC-MySQL版本的会话持久化)

https://github.com/JunTeamCom/ai-demo/tree/release-6.1 (VectorStore-Qrant版本的会话持久化)

本章讲解聊天内容的存储:短期记忆(即会话级别)、存储方式、长期记忆(用户级别),这是对前面两个Topic(OpenAI接入、RAG与向量数据库)的延伸。

1 注入聊天记忆类

参考上一章内容,也是通过Advisor类注入控制聊天内容存储:

  • MessageChatMemoryAdvisor
  • PromptChatMemoryAdvisor
  • VectorStoreChatMemoryAdvisor

前两者为短期记忆,不同之处是MessageChatMemoryAdvisor能够按角色存储会话(即用户和助手两种角色,不过部分大模型不支持),PromptChatMemoryAdvisor是把历史信息转换为大字符串、注入到提示词中。

VectorStoreChatMemoryAdvisor则是用向量数据库、记录历史消息(类似RAG)。

集成方式,是构建一个*MemoryAdvisor类,然后defaultAdvisors入参加入(方法入参为Advisor链)。

java 复制代码
@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder,
    ChatMemory chatMemory,
    VectorStore vectorStore) {
    // 设置顾问配置项
    return chatClientBuilder
        .defaultAdvisors(
            MessageChatMemoryAdvisor.builder(chatMemory)
                .build(),
            QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(SearchRequest.builder().build())
                .build())
        .build();
}

为了防止对话内容溢出,需要控制上下文最大条数:

java 复制代码
@Bean
ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
    return MessageWindowChatMemory.builder()
            .chatMemoryRepository(chatMemoryRepository)
            .maxMessages(50) // 最大条数50,即:提问25条、回答25条
            .build();
}

2 引入会话ID

2.1 Controller层引入

现实中我们当然不能把所有会话、都统一记忆;其实还是要通过会话ID进行区分(类似Session):

java 复制代码
    @PostMapping(path = "/ask", produces = "application/json")
    public ChatAnswer ask(
        @RequestHeader(name = "X_CONVERSATION_ID", defaultValue = "default") String conversationId,
        @RequestBody ChatQuestion chatQuestion) {
        return chatService.ask(chatQuestion, conversationId);
    }

2.2 Service层:引入会话ID前的简化

在修改Advisor、引入会话ID的同时,将上一章的Rules形式、改为Expression形式:

复制代码
package com.junteam.ai.demo.service.impl;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import com.junteam.ai.demo.model.ChatAnswer;
import com.junteam.ai.demo.model.ChatQuestion;
import com.junteam.ai.demo.service.ChatService;

import static org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor.FILTER_EXPRESSION;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;

@Service
public class OpenAIChatServiceImpl implements ChatService {

    private final ChatClient chatClient;

    public OpenAIChatServiceImpl(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @Value("classpath:/promptTemplates/questionPromptTemplate.st")
    Resource questionPromptTemplate;

    @Override
    public ChatAnswer ask(ChatQuestion chatQuestion, String conversationId) {
        var countryTitleMatch = String.format(
            "countryTitle == '%s'",
            chatQuestion.title());

        return chatClient
            .prompt()
            .system(systemSpec -> systemSpec
                .text(questionPromptTemplate)
                .param("countryTitle", chatQuestion.title()))
            .advisors(advisorSpec -> advisorSpec
                .param(FILTER_EXPRESSION, countryTitleMatch)
            .user(chatQuestion.question())
            .call()
            .entity(ChatAnswer.class);
    }
}

相应的,提示词模板修改:

plaintext 复制代码
你是一个有用的助手,负责回答有关{countryTitle}的历史地理风俗问题。

2.3 Service层引入会话ID

修改Service接口和实现类:

java 复制代码
package com.junteam.ai.demo.service;

import com.junteam.ai.demo.model.ChatAnswer;
import com.junteam.ai.demo.model.ChatQuestion;

public interface ChatService {
    ChatAnswer ask(ChatQuestion question, String conversationId);
}
java 复制代码
package com.junteam.ai.demo.service.impl;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import com.junteam.ai.demo.model.ChatAnswer;
import com.junteam.ai.demo.model.ChatQuestion;
import com.junteam.ai.demo.service.ChatService;

import static org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor.FILTER_EXPRESSION;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;

@Service
public class OpenAIChatServiceImpl implements ChatService {

    private final ChatClient chatClient;

    public OpenAIChatServiceImpl(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @Value("classpath:/promptTemplates/questionPromptTemplate.st")
    Resource questionPromptTemplate;

    @Override
    public ChatAnswer ask(ChatQuestion chatQuestion, String conversationId) {
        var countryTitleMatch = String.format(
            "countryTitle == '%s'",
            chatQuestion.title());

        return chatClient
            .prompt()
            .system(systemSpec -> systemSpec
                .text(questionPromptTemplate)
                .param("countryTitle", chatQuestion.title()))
            .advisors(advisorSpec -> advisorSpec
                .param(FILTER_EXPRESSION, countryTitleMatch)
                .param(CONVERSATION_ID, conversationId)) // 引入会话ID
            .user(chatQuestion.question())
            .call()
            .entity(ChatAnswer.class);
    }
}

需要注意的是:
会话内容是存储在内存中的,服务一旦重启、那么所有历史内容都会清空。

如果需要持久化存储、并且多服务节点时能共享会话内容,需要使用向量数据库等手段存储。

3 持久化会话数据

从单体应用,迈向无状态的分布式服务集群,会话数据必须持久化(即将临时存储的内容------如内存数据等,通过文件或数据库等长期存储)。

这既可以通过ChatMemoryRepository实现,也可以直接使用VectorStoreChatMemoryAdvisor。

3.1 ChatMemoryRepository实现持久化

具体来说,ChatMemoryRepository主要有三种实现方式(都是使用数据库):

  • CassandraChatMemoryRepository
  • JdbcChatMemoryRepository
  • Neo4jChatMemoryRepository

分别对应Cassandra文档数据库、JDBC关系型数据库、Neo4j图数据库(相关概念不再赘述,搜索引擎即可搜到准确简明的概念说明)。

如果要使用,引入相关的Starter、注入ChatMemoryRepository、配置数据库链接地址即可。

下面以JDBC-MySQL为例、进行扩展。

3.1.1 依赖引入

1、引入Starter:

org.springframework.ai: spring-ai-starter-model-chat-memory-repository-jdbc

2、JDBC方式与其他相比,需要再引入驱动:

xml 复制代码
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
</dependency>

3、找到相关初始SQL,再数据库执行SQL:

classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-mysql.sql

mysql 复制代码
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`)
);

3.1.2 数据库配置

配置文件添加数据库地址:

yaml 复制代码
spring:
  # 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${DB_SERVER}:3306/chat_db
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

3.1.3 JAVA代码适配

JAVA代码适配、全部通过ChatMemoryRepository@Bean注入(AiConfig类);

JDBC也特殊一点,需要自定义数据库方言(比如MySQL/PostgreSQL/Oracle/SQLServer等)

java 复制代码
@Bean
ChatMemoryRepository chatMemoryRepository(DataSource dataSource) {
    return JdbcChatMemoryRepository.builder()
        .dialect(new MysqlChatMemoryRepositoryDialect())
        .dataSource(dataSource)
        .build();
}

Service实现类不需要进行变更。

3.2 VectorStoreChatMemoryAdvisor实现持久化

spring-ai-advisors-vector-store依赖里已经包含相关Advisor,直接修改AiConfig即可;

这不需要额外的数据源,是简洁方便的方式:

java 复制代码
package com.junteam.ai.demo.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {
    @Bean
    ChatClient chatClient(ChatClient.Builder chatClientBuilder,
        VectorStore vectorStore) {
        // 设置顾问配置项
        return chatClientBuilder
            .defaultAdvisors(
                VectorStoreChatMemoryAdvisor
                    .builder(vectorStore)
                    .build(),
                QuestionAnswerAdvisor
                    .builder(vectorStore)
                    .searchRequest(SearchRequest.builder().build())
                    .build())
            .build();
    }
}
···
相关推荐
梦想很大很大3 小时前
从 0 到 1 实现 AI Agent(02):设计可扩展的 Tool 调用系统
人工智能·llm·agent
冬奇Lab3 小时前
一天一个开源项目(第61篇):knowledge_graph - 把任意文本转成知识图谱
人工智能·llm
泯仲5 小时前
项目实践|ETL Pipeline 完整解析:从多源文档到向量库的全链路实现
数据仓库·agent·etl·rag
数据智能老司机5 小时前
Transformers 权威指南——用于图像生成的 Transformer
llm
老李的勺子6 小时前
Agent 记忆失效的 5 种方式:完整排查复盘
python·llm
数据智能老司机6 小时前
Transformers 权威指南——从声音到 Token 再返回声音:音频领域中的 Transformer
llm
阿里云大数据AI技术6 小时前
Mem0 + Elasticsearch:构建 AI 记忆系统
人工智能·llm
编写人生7 小时前
Agent构建:声明式优于硬编码
ai
VIP_CQCRE7 小时前
Nano Banana Images API 集成指南
ai