1. 概述

在开发 AI 应用时,我们经常需要实现类似人类的对话交互。这就要求我们与 LLM 模型保持连续对话,而 Spring AI 通过其聊天记忆功能完美解决了这个问题。

本文将深入探讨 Spring AI 提供的多种聊天记忆实现方案,并通过示例展示如何将聊天记忆集成到聊天客户端中。

2. 聊天记忆

大语言模型(LLM)本质是无状态的,不会记住任何内容。 每次发送给 LLM 的提示都被视为独立查询,模型不会保留任何历史消息。

在 AI 应用中,保存对话历史至关重要,这能让 LLM 生成有意义的响应。聊天记忆正是为了填补这一空白而存在,它提供了:

  • 上下文理解 - 使 LLM 能基于完整对话生成响应
  • 个性化体验 - 根据聊天历史提供定制化回复
  • 持久化能力 - 根据实现方式,聊天记忆可跨多个会话持久保存

3. 聊天记忆存储库

Spring AI 提供了 ChatMemory 接口和多种开箱即用的实现,帮助我们轻松集成聊天记忆功能。

首先添加 Maven 依赖 spring-ai-starter-model-openai 以启用 OpenAI 集成。该依赖会自动引入 Spring AI 核心库:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
    <version>1.0.0</version>
</dependency>

创建聊天记忆时,需要提供 ChatMemoryRepository 的实现,负责将聊天消息持久化到存储:

ChatMemoryRepository chatMemoryRepository;

ChatMemory chatMemory = MessageWindowChatMemory.builder()
  .chatMemoryRepository(chatMemoryRepository)
  .maxMessages(10)
  .build();

Spring AI 提供了多种聊天记忆存储库,可根据项目技术栈选择。这里我们重点讨论两种常用实现。

3.1. 内存存储库

如果没有显式定义聊天记忆,Spring AI 默认使用内存存储。 它内部使用 ConcurrentHashMap 存储聊天消息,以会话 ID 为键,消息列表为值:

public final class InMemoryChatMemoryRepository implements ChatMemoryRepository {
    Map<String, List<Message>> chatMemoryStore = new ConcurrentHashMap();

    // 其他方法
}

内存存储库实现简单,适合不需要长期持久化的场景。 如果需要长期保存,就得考虑其他方案。

3.2. JDBC 存储库

JDBC 存储库用于将聊天消息持久化到关系型数据库。 Spring AI 内置支持多种数据库,包括 MySQL、PostgreSQL、SQL Server 和 HSQLDB。

若要在关系型数据库中存储聊天记忆,需添加 Maven 依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-model-chat-memory-repository-jdbc</artifactId>
    <version>1.0.0</version>
</dependency>

每种内置支持的数据库都有对应的方言实现,提供聊天记忆表的 CRUD 操作 SQL。初始化 JdbcChatMemoryRepository 时需指定方言:

JdbcChatMemoryRepositoryDialect dialect = ...; // 选择存储库方言
ChatMemoryRepository repository = JdbcChatMemoryRepository.builder()
  .jdbcTemplate(jdbcTemplate)
  .dialect(dialect)
  .build();

对于未内置支持的数据库,需实现 JdbcChatMemoryRepositoryDialect 接口,提供各 CRUD 操作的 SQL 语句:

public interface JdbcChatMemoryRepositoryDialect {
    String getSelectMessagesSql();

    String getInsertMessageSql();

    String getSelectConversationIdsSql();

    String getDeleteMessagesSql();
}

Spring AI 的方言实现使用标准 SQL,不依赖特定数据库厂商。因此可直接使用如 MysqlChatMemoryRepositoryDialect 等现成实现,无需自定义。

使用前需初始化数据库模式。对于支持的方言,Spring AI 提供了模式创建脚本,位于 classpath:org/springframework/ai/chat/memory/repository/jdbc

4. 将聊天记忆应用到聊天客户端

Spring AI 在 ChatMemoryAutoConfiguration 中提供了聊天记忆的自动配置。 如果选择内存存储库,无需显式定义任何内容,因为这是默认选项。

但如果要使用 JDBC 存储库,需要提供自己的 ChatMemoryRepository Bean 来覆盖默认的内存实现:

@Configuration
public class ChatConfig {
    @Bean
    public ChatMemoryRepository getChatMemoryRepository(JdbcTemplate jdbcTemplate) {
        return JdbcChatMemoryRepository.builder()
          .jdbcTemplate(jdbcTemplate)
          .dialect(new HsqldbChatMemoryRepositoryDialect())
          .build();
    }
}

注意:我们不需要显式定义 ChatMemory 的 Bean,因为它已在 ChatMemoryAutoConfiguration 中定义。

在 Spring Boot 中创建 ChatService

@Component
@SessionScope
public class ChatService {
    private final ChatClient chatClient;
    private final String conversationId;

    public ChatService(ChatModel chatModel, ChatMemory chatMemory) {
        this.chatClient = ChatClient.builder(chatModel)
          .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
          .build();
        this.conversationId = UUID.randomUUID().toString();
    }

    public String chat(String prompt) {
        return chatClient.prompt()
          .user(userMessage -> userMessage.text(prompt))
          .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
          .call()
          .content();
    }
}

在构造函数中,Spring Boot 会自动注入 ChatMemory 实现。我们通过 MemoryChatMemoryAdvisor 用它初始化 ChatClient

定义 chat 方法接收提示,将消息发送到聊天模型。同时添加会话 ID 作为聊天顾问参数,基于当前会话唯一标识对话。

⚠️ 重要提示:必须用 @SessionScope 注解服务,使其实例能在多个请求间保持。

5. 与 OpenAI 集成

在演示中,我们将聊天记忆与 OpenAI 集成,观察 Spring AI 如何调用 OpenAI API,并采用内存 HSQL 数据库作为持久化存储。

application.yml 中添加配置,设置 OpenAI API 密钥、数据库连接,并在应用启动时初始化模式:

spring:
  ai:
    openai:
      api-key: "sk-1234567890abcdef"  # 替换为你的实际 API 密钥

  datasource:
    url: jdbc:hsqldb:mem:chatdb
    driver-class-name: org.hsqldb.jdbc.JDBCDriver
    username: sa
    password:

  sql:
    init:
      mode: always
      schema-locations: classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-hsqldb.sql

现在配置已完成。创建 REST 接口以便调用之前定义的 ChatService

@RestController
public class ChatController {
    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @PostMapping("/chat")
    public ResponseEntity<String> chat(@RequestBody @Valid ChatRequest request) {
        String response = chatService.chat(request.getPrompt());
        return ResponseEntity.ok(response);
    }
}

ChatRequest 是一个简单的 DTO,包含字符串形式的提示:

public class ChatRequest {
    @NotNull
    private String prompt;

    // getter 和 setter
}

6. 测试运行

现在可以向 REST 接口发送请求了。我们将使用 Postman 发送请求,并用 HTTP 工具包拦截 Spring Boot 应用与 OpenAI 之间的 HTTP 请求,观察内部工作原理。

6.1. 第一次请求

在 Postman 中调用接口请求一个笑话,检查响应:

第一次请求截图

在 HTTP 工具包中观察拦截的请求,会看到发送到 OpenAI 的 HTTP 请求:

{
  "messages": [
    {
      "content": "Tell me a joke",
      "role": "user"
    }
  ],
  "model": "gpt-4o-mini",
  "stream": false,
  "temperature": 0.7
}

这是一个非常简单的请求,使用用户角色发送我们的提示内容。

6.2. 第二次请求

现在发送另一个请求,观察差异:

第二次请求截图

这次查看拦截的 OpenAI HTTP 请求,会发现 Spring AI 不仅发送了当前提示,还包含了之前的提示和响应:

{
  "messages": [
    {
      "content": "Tell me a joke",
      "role": "user"
    },
    {
      "content": "Why did the scarecrow win an award? \n\nBecause he was outstanding in his field!",
      "role": "assistant"
    },
    {
      "content": "Tell me another one",
      "role": "user"
    }
  ],
  "model": "gpt-4o-mini",
  "stream": false,
  "temperature": 0.7
}

在这个例子中,我们看到 Spring AI 将整个聊天历史发送给了聊天模型。 这种方式帮助模型保持完整对话上下文,使交互更自然流畅。

7. 总结

本文介绍了 Spring AI 如何通过聊天记忆功能在多次聊天请求中维护对话历史,从而提升对话体验。

我们探讨了不同的记忆存储库实现,演示了如何将聊天记忆与 Spring AI 和 OpenAI 集成,并深入分析了 Spring AI 聊天记忆与 OpenAI 协同工作的底层机制。

完整源代码可在 GitHub 获取。