本文代码:
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链)。- @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();
- }
复制代码 为了防止对话内容溢出,需要控制上下文最大条数:- @Bean
- ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
- return MessageWindowChatMemory.builder()
- .chatMemoryRepository(chatMemoryRepository)
- .maxMessages(50) // 最大条数50,即:提问25条、回答25条
- .build();
- }
复制代码 2 引入会话ID
2.1 Controller层引入
现实中我们当然不能把所有会话、都统一记忆;其实还是要通过会话ID进行区分(类似Session):- @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);
- }
- }
复制代码 相应的,提示词模板修改:- 你是一个有用的助手,负责回答有关{countryTitle}的历史地理风俗问题。
复制代码 2.3 Service层引入会话ID
修改Service接口和实现类:- 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);
- }
复制代码- 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方式与其他相比,需要再引入驱动:- <dependency>
- <groupId>com.mysql</groupId>
- mysql-connector-j</artifactId>
- </dependency>
复制代码 3、找到相关初始SQL,再数据库执行SQL:
classpath rg/springframework/ai/chat/memory/repository/jdbc/schema-mysql.sql
- 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 数据库配置
配置文件添加数据库地址:- 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等)- @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即可;
这不需要额外的数据源,是简洁方便的方式:- 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();
- }
- }
- ···
复制代码 来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |