添加记忆
大语言模型(LLMs)是无状态的,这意味着它们不会保留关于先前交互的信息。当你希望在多次交互中保持上下文或状态时,这可能会成为一个限制。为了解决这个问题,Spring AI提供了保存上下文功能,定义为Chat Memory,允许你在与LLM的多次交互中存储和检索信息。
但是在此之前,我们需要理解,Chat Memory并不等于Chat History。
- Chat Memory:大型语言模型在整个对话过程中保留并用于保持语境感知的信息。
- Chat History:整个对话历史,包括用户与模型之间交换的所有消息,通常用于审计或回顾。
添加Chat Memory
Spring AI会自动配置一个ChatMemory Bean,我们可以在应用程序中直接使用。默认情况下,它使用内存中的存储库(InMemoryChatMemoryRepository)来存储消息,并使用MessageWindowChatMemory实现来管理对话历史。
修改 src/main/java/com/example/canaan/config/ai/ChatConfiguration.java 文件,在构建ChatClient的实例时添加Chat Memory配置:
package com.chestnut.canaan.config.ai;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatConfiguration {
@Resource
private ChatMemory chatMemory;
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.defaultSystem("you are a friendly assistant!").build();
}
}
在这个配置中,我们使用了 MessageChatMemoryAdvisor,它会将ChatMemory与ChatClient关联起来。这样,每次调用ChatClient时,都会自动使用Chat Memory。因为我们没有额外的配置,所以默认使用InMemoryChatMemoryRepository 来存储消息。InMemoryChatMemoryRepository会将消息存在ConcurrentHashMap中。每次使用chatClient的相关方法时,Chat Memory会自动更新。
测试效果:
目前所有的请求都共用同一份Chat Memory,这意味着不同的用户之间的Chat Memory是共享的。如果我们希望每个对话有独立的Chat Memory,我们可以在每次对话时传入一个conversationId来区分不同的对话。
修改 src/main/java/com/example/canaan/service/AiService.java和src/main/java/com/example/canaan/service/impl/AiServiceImpl.java 文件,添加一个 conversationId 参数:
package com.chestnut.canaan.service;
import reactor.core.publisher.Flux;
public interface AiService {
Flux<String> chat(String input,String conversationId);
}
package com.chestnut.canaan.service.impl;
import com.chestnut.canaan.service.AiService;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class AiServiceImpl implements AiService {
@Resource
private ChatClient chatClient;
@Override
public Flux<String> chat(String input,String conversationId) {
return chatClient.prompt()
.advisors(it -> it.param(ChatMemory.CONVERSATION_ID, conversationId))
.user(input).stream().content();
}
}
现在,为了快速检测结果,我们修改一下控制器 src/main/java/com/example/canaan/controller/AiController.java,使其支持传入 conversationId 参数:
package com.chestnut.canaan.controller;
import com.chestnut.canaan.service.AiService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/ai")
public class AiController {
@Resource
private AiService aiService;
@GetMapping("/chat")
public Flux<String> chat(String input,String conversationId) {
return aiService.chat(input,conversationId);
}
}
现在,我们可以通过访问 /ai/chat?input=问题&conversationId=对话ID 来与模型进行交互。每个不同的 conversationId 将会有独立的Chat Memory。
记忆持久化
目前,Chat Memory是存储在内存中的,这意味着当应用程序重启时,所有的Chat Memory都会丢失。为了持久化Chat Memory,我们可以使用数据库或其他持久化存储。
Spring AI提供了ChatMemoryRepository抽象来存储Chat Memory,并提供了内置存储库及其使用方法。如有需要,我们也可以实现自己的存储库。以下是几种内置存储库:
- InMemoryChatMemoryRepository:默认的内存存储库。
- JdbcChatMemoryRepository:使用JDBC将消息存储在关系型数据库中。支持多种数据库,开箱即用,适用于需要持久存储Chat Memory的应用程序。
- CassandraChatMemoryRepository: 使用Apache Cassandra来存储消息。适用于需要持久存储Chat Memory的应用程序,尤其适用于对可用性、持久性、扩展性有要求,以及需要利用生存时间(TTL)功能的场景。
- Neo4j ChatMemoryRepository:使用Neo4j将Chat Memory作为节点和关系存储在属性图数据库中。适用于希望利用Neo4j的图功能来持久化Chat Memory的应用程序。
使用JDBC存储Chat Memory
我们将使用JDBC存储Chat Memory。数据库可以是MySQL、PostgreSQL等关系型数据库。以下是使用MySQL的步骤:
首先,我们需要添加依赖项。在 build.gradle 文件的dependencies中添加以下依赖:
dependencies {
// JDBC memory repository 核心依赖
implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc'
// MySQL JDBC驱动
runtimeOnly 'com.mysql:mysql-connector-j'
}
其次,我们需要新建一个数据库,名称随意,我取名assists:
CREATE DATABASE `assists` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
配置数据库连接。在 src/main/resources/application.yml 文件中添加以下配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${PERSONAL_SERVER_IP}:3306/assists
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
ai:
chat:
memory:
repository:
jdbc:
platform: mariadb
initialize-schema: always
其中spring.datasource.*配置的是数据库相关信息
spring.ai.chat.memory.repository.jdbc.platform使用mariadb而不是mysql,是因为默认的数据表初始化文件放在依赖包classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-@@platform@@.sql位置,但是我们可以看到,在此目录下并不存在数据表初始化文件schema-mysql.sql
因为mariadb兼容MySQL,所以我们可以使用mariadb的初始化脚本。或者我们也可以通过
spring.ai.chat.memory.repository.jdbc.schema手动指定脚本位置。
默认情况下,初始化存储Chat Memory表SPRING_AI_CHAT_MEMORY仅对嵌入式数据库(H2、HSQL、Derby等)生效,所以我们需要使用
spring.ai.chat.memory.repository.jdbc.initialize-schema来让初始化脚本对MySQL生效。
因为Spring AI 会自动配置一个ChatMemory Bean,所以我们不需要手动配置ChatMemory。Spring AI会根据spring.ai.chat.memory.repository.jdbc的配置来创建一个JdbcChatMemoryRepository实例,并以此为基础创建一个ChatMemory实例。并且由于有了jdbc配置,InMemoryChatMemoryRepository将不会被创建。
最后,启动服务,在数据库assists下面应该已经出现了数据表SPRING_AI_CHAT_MEMORY。
重新调用接口/ai/chat?input=问题&conversationId=对话ID,你会发现Chat Memory已经被存储在数据库中了。每次调用接口时,Chat Memory都会被更新。
总结
在本章中,我们了解了如何为Spring AI应用程序添加Chat Memory。我们了解了Chat Memory的概念,并通过配置ChatClient来启用Chat Memory功能。我们还知道了如何使用JDBC存储Chat Memory,以便在应用程序重启后仍然对话仍然可用。通过这些步骤,我们可以在多次交互中保持上下文或状态,从而使应用程序更加智能和人性化。