Spring AI 初步集成(2)-添加记忆

0 阅读5分钟

添加记忆

大语言模型(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.gif

目前所有的请求都共用同一份Chat Memory,这意味着不同的用户之间的Chat Memory是共享的。如果我们希望每个对话有独立的Chat Memory,我们可以在每次对话时传入一个conversationId来区分不同的对话。

修改 src/main/java/com/example/canaan/service/AiService.javasrc/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

memory-init-script-path.png 因为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,以便在应用程序重启后仍然对话仍然可用。通过这些步骤,我们可以在多次交互中保持上下文或状态,从而使应用程序更加智能和人性化。