Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成

一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和扩展。欢迎感兴趣的小伙伴们关注和 Star。

项目地址:github.com/java-ai-tec...

更多文章:mp.weixin.qq.com/s/72omFtMqi...

前言

在构建智能对话系统时,保持对话上下文的连贯性是提升用户体验的关键。Spring AI 框架提供了强大的 Chat Memory 机制,支持多种存储方式来持久化对话历史。本文将深入解析 Spring AI Chat Memory 的核心机制,并通过实际代码演示如何实现基于本地内存(Local)和数据库(JDBC)的两种存储方案。

Spring AI Chat Memory 核心机制

架构概览 Architecture Overview

Spring AI Chat Memory 采用分层架构设计:

java 复制代码
┌─────────────────────────────────────┐
│           ChatClient Layer          │
├─────────────────────────────────────┤
│         ChatMemory Advisor          │
├─────────────────────────────────────┤
│         ChatMemory Interface        │
├─────────────────────────────────────┤
│      ChatMemoryRepository Layer     │
├─────────────────────────────────────┤
│    Storage Layer (Local/JDBC)       │
└─────────────────────────────────────┘

核心组件解析

  1. ChatMemory 接口:提供统一的对话记忆管理抽象
  2. ChatMemoryRepository:负责底层存储操作
  3. MessageChatMemoryAdvisor:基于 Advisor 模式的透明化处理
  4. MessageWindowChatMemory:支持消息窗口限制的实现

实现方案一:Local Memory (本地内存存储)

依赖配置

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

Step 1: 创建 ChatClient 配置

java 复制代码
@Configuration
public class ChatClientConfigs {

    @Bean
    public ChatClient chatClient(OpenAiChatModel chatModel, ChatMemory chatMemory) {
        return ChatClient.builder(chatModel)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .defaultSystem("You are deepseek chat bot, you answer questions in a concise and accurate manner.")
                .build();
    }
}

关键点解析

  • MessageChatMemoryAdvisor:采用 Advisor 模式,自动处理消息的存储和检索
  • defaultAdvisors:为 ChatClient 配置默认的 advisor,使 memory 功能透明化

Step 2: 实现 ChatMemoryService

java 复制代码
@Service
public class ChatMemoryService {
    // 模拟一个会话 ID
    private static final String CONVERSATION_ID = "naming-20250528";

    @Autowired
    private ChatClient chatClient;

    /**
     * 基于 Advisor 模式的聊天方法
     * ChatClient 会自动处理消息的存储和检索
     * 
     * @param message 用户输入消息
     * @param conversationId 对话会话ID,如果为null则使用默认ID
     * @return AI的响应内容
     */
    public String chat(String message, String conversationId) {
        String answer = this.chatClient.prompt()
                .user(message)
                // 关键:通过 advisor 参数指定对话ID
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId == null ? CONVERSATION_ID : conversationId))
                .call()
                .content();
        return answer;
    }
}

核心机制

  • 通过 ChatMemory.CONVERSATION_ID 参数指定对话会话 ID
  • ChatClient 自动从 memory 中检索历史消息并添加到 prompt 中
  • 响应后自动将对话记录存储到 memory 中

Step 3: 配置应用属性

properties 复制代码
# application.properties
spring.application.name=spring-ai-chat-memory-local
server.port=8083
spring.profiles.active=deepseek

# DeepSeek API 配置
spring.ai.openai.api-key=${spring.ai.openai.api-key}
spring.ai.openai.chat.base-url=https://api.deepseek.com
spring.ai.openai.chat.completions-path=/v1/chat/completions
spring.ai.openai.chat.options.model=deepseek-chat

Local Memory 优缺点

优点

  • 配置简单,开箱即用
  • 响应速度快,无网络延迟
  • 适合开发和测试环境

缺点

  • 数据不持久化,重启后丢失
  • 不支持多实例间共享
  • 内存使用量随对话量增长

实现方案二:JDBC Memory (数据库存储)

依赖配置

xml 复制代码
<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>

Step 1: 数据库表结构

sql 复制代码
-- schema-mysql.sql
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
    conversation_id VARCHAR(36) NOT NULL,
    content TEXT NOT NULL,
    type VARCHAR(10) NOT NULL,
    `timestamp` TIMESTAMP NOT NULL,
    CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
);

CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX
ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp`);

表结构解析

  • conversation_id:对话会话标识,支持多会话隔离
  • content:消息内容
  • type:消息类型(用户、助手、系统、工具)
  • timestamp:时间戳,用于消息排序
  • 复合索引:优化按会话 ID 和时间的查询性能

Step 2: 实现 ChatMemoryService

java 复制代码
@Service
public class ChatMemoryService {

    @Autowired
    private ChatModel chatModel;

    @Autowired
    private JdbcChatMemoryRepository chatMemoryRepository;

    private ChatMemory chatMemory;

    @PostConstruct
    public void init() {
        this.chatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(chatMemoryRepository)
                .maxMessages(20)  // 限制消息窗口大小
                .build();
    }

    public String call(String message, String conversationId) {
        // 1. 创建用户消息
        UserMessage userMessage = new UserMessage(message);
        
        // 2. 存储用户消息到 memory
        this.chatMemory.add(conversationId, userMessage);
        
        // 3. 从 memory 获取对话历史
        List<Message> messages = chatMemory.get(conversationId);
        
        // 4. 调用 ChatModel 生成响应
        ChatResponse response = chatModel.call(new Prompt(messages));
        
        // 5. 存储 AI 响应到 memory
        chatMemory.add(conversationId, response.getResult().getOutput());
        
        return response.getResult().getOutput().getText();
    }
}

核心机制

  • MessageWindowChatMemory:支持消息窗口限制的内存实现
  • maxMessages:控制保留的最大消息数量,避免 token 超限
  • 手动管理消息的存储和检索流程

Step 3: 配置数据源

properties 复制代码
# application.properties
spring.application.name=spring-ai-chat-memory-jdbc
server.port=8083

# JDBC Memory Repository 配置
spring.ai.chat.memory.repository.jdbc.initialize-schema=always
spring.ai.chat.memory.repository.jdbc.schema=classpath:schema-@@platform@@.sql
spring.ai.chat.memory.repository.jdbc.platform=mysql

# MySQL 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/spring_ai_chat_memory?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=${spring.datasource.password}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

JDBC Memory 优缺点

优点

  • 数据持久化,支持服务重启
  • 支持多实例间共享对话历史
  • 可扩展性强,支持大规模应用
  • 支持复杂查询和数据分析

缺点

  • 配置相对复杂
  • 存在网络延迟
  • 需要维护数据库

运行效果演示

Local Memory 运行日志

vbnet 复制代码
第一轮对话:
用户: hello, my name is glmapper
AI: Hello glmapper! Nice to meet you. How can I help you today?

第二轮对话:
用户: do you remember my name?
AI: Yes, I remember! Your name is glmapper. Is there anything specific you'd like to discuss?

JDBC Memory 数据库记录

sql 复制代码
SELECT * FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = 'test-naming-202505281800';

| conversation_id           | content                    | type      | timestamp           |
|--------------------------|----------------------------|-----------|--------------------|
| test-naming-202505281800 | hello, my name is glmapper | USER      | 2025-01-20 10:30:15 |
| test-naming-202505281800 | Hello glmapper! Nice to... | ASSISTANT | 2025-01-20 10:30:16 |
| test-naming-202505281800 | do you remember my name?   | USER      | 2025-01-20 10:31:20 |
| test-naming-202505281800 | Yes, I remember! Your...  | ASSISTANT | 2025-01-20 10:31:21 |

实战测试验证

测试对话连续性

java 复制代码
@Test
@DisplayName("测试聊天记忆功能 - 上下文保持")
void testChatMemoryContextRetention() {
    String CONVERSATION_ID = "test-naming-202505281800";
    
    // 第一轮对话:自我介绍
    String firstMessage = "hello, my name is glmapper";
    String firstResponse = chatMemoryService.call(firstMessage, CONVERSATION_ID);
    
    // 第二轮对话:询问之前提到的信息
    String secondMessage = "do you remember my name?";
    String secondResponse = chatMemoryService.call(secondMessage, CONVERSATION_ID);
    
    // 验证AI是否记住了用户的名字
    assertTrue(secondResponse.contains("glmapper"), "AI 应该记住用户的名字");
}

测试对话隔离性

java 复制代码
@Test
@DisplayName("测试对话ID的非一致性")
void testConversationIdNonConsistency() {
    String CONVERSATION_ID1 = "test-naming-202505281801";
    String CONVERSATION_ID2 = "test-naming-202505281802";
    
    String message1 = "请记住这个数字:12345";
    String message2 = "刚才我说的数字是什么?";
    
    String response1 = chatMemoryService.call(message1, CONVERSATION_ID1);
    String response2 = chatMemoryService.call(message2, CONVERSATION_ID2);
    
    // 验证不同对话ID间的隔离性
    assertFalse(response2.contains("12345"), "不同对话ID应该相互隔离");
}

方案对比与选择建议

特性 Local Memory JDBC Memory
数据持久化
配置复杂度
性能
多实例共享
扩展性
适用场景 开发/测试 生产环境

选择建议

  • 开发/测试阶段:使用 Local Memory,快速验证功能
  • 生产环境:使用 JDBC Memory,确保数据可靠性
  • 高并发场景:考虑使用 Redis 等缓存方案
  • 企业级应用:JDBC + 数据库集群方案

常见问题与解决方案

Q1: 为什么 AI 记不住之前的对话?

A : 检查对话 ID 是否一致,确保在同一会话中使用相同的 conversationId

Q2: JDBC Memory 初始化失败?

A : 确认数据库连接正常,检查 spring.ai.chat.memory.repository.jdbc.initialize-schema=always 配置。

Q3: 对话历史过长导致 Token 超限?

A : 设置合适的 maxMessages 参数限制消息窗口大小。

java 复制代码
this.chatMemory = MessageWindowChatMemory.builder()
    .chatMemoryRepository(chatMemoryRepository)
    .maxMessages(10)  // 根据模型 token 限制调整
    .build();

最佳实践

  1. 会话 ID 管理 :使用 UUID 或有意义的业务标识,建议格式:user_{userId}_{timestamp}
  2. 消息窗口控制 :根据模型 token 限制合理设置 maxMessages(通常 10-20 条)
  3. 异常处理:实现 memory 操作的容错机制,避免单点故障
  4. 性能优化:使用数据库连接池,为高频查询字段建立索引
  5. 数据清理:定期清理过期对话数据,避免数据库膨胀
  6. 监控告警:监控 memory 操作的延迟和错误率

总结

Spring AI Chat Memory 提供了灵活的对话记忆管理能力,通过 Local 和 JDBC 两种存储方案,可以满足从开发测试到生产部署的不同需求。Local Memory 适合快速原型开发,而 JDBC Memory 则适合需要数据持久化的生产环境。

理解其核心机制和实现细节,有助于开发者根据实际场景选择合适的方案,构建出高质量的智能对话应用。


项目地址 : spring-ai-summary

相关文档:

相关推荐
哪吒编程35 分钟前
谷歌高调宣布,Gemini 2.5 Pro 0605发布,硬刚Claude Opus 4,国内直接使用
openai·gemini
保持学习ing1 小时前
Spring注解开发
java·深度学习·spring·框架
异常君2 小时前
Spring 中的 FactoryBean 与 BeanFactory:核心概念深度解析
java·spring·面试
cacyiol_Z2 小时前
在SpringBoot中使用AWS SDK实现邮箱验证码服务
java·spring boot·spring
新智元2 小时前
美 IT 业裁员狂飙 35%,「硅谷梦」彻底崩塌!打工人怒喷 PIP
人工智能·openai
新智元2 小时前
乔布斯挚友去世!胰腺癌再夺硅谷天才,曾写下苹果「创世代码」
人工智能·openai
新智元4 小时前
全球 30 名顶尖数学家秘密集会围剿 AI,当场破防!惊呼已接近数学天才
人工智能·openai
hstar95274 小时前
三十五、面向对象底层逻辑-Spring MVC中AbstractXlsxStreamingView的设计
java·后端·spring·设计模式·架构·mvc
楽码4 小时前
AI决策树:整理繁杂问题的简单方法
人工智能·后端·openai
土豆12504 小时前
告别“专属”编辑器:为什么 GitHub Copilot 是比 Cursor 更优的 AI 编程选择
llm·cursor·github copilot