当前位置: 首页 > article >正文

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

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

项目地址:https://github.com/java-ai-tech/spring-ai-summary

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

前言

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

Spring AI Chat Memory 核心机制

架构概览 Architecture Overview

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

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

核心组件解析

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

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

依赖配置

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

Step 1: 创建 ChatClient 配置

@Configuration
public class ChatClientConfigs {@Beanpublic 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

@Service
public class ChatMemoryService {// 模拟一个会话 IDprivate static final String CONVERSATION_ID = "naming-20250528";@Autowiredprivate 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: 配置应用属性

# 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 (数据库存储)

依赖配置

<!-- 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: 数据库表结构

-- 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

@Service
public class ChatMemoryService {@Autowiredprivate ChatModel chatModel;@Autowiredprivate JdbcChatMemoryRepository chatMemoryRepository;private ChatMemory chatMemory;@PostConstructpublic 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. 存储用户消息到 memorythis.chatMemory.add(conversationId, userMessage);// 3. 从 memory 获取对话历史List<Message> messages = chatMemory.get(conversationId);// 4. 调用 ChatModel 生成响应ChatResponse response = chatModel.call(new Prompt(messages));// 5. 存储 AI 响应到 memorychatMemory.add(conversationId, response.getResult().getOutput());return response.getResult().getOutput().getText();}
}

核心机制

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

Step 3: 配置数据源

# 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 运行日志

第一轮对话:
用户: 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 数据库记录

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 |

实战测试验证

测试对话连续性

@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 应该记住用户的名字");
}

测试对话隔离性

@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 MemoryJDBC 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 参数限制消息窗口大小。

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

相关文档:

  • Spring AI Documentation
  • ChatMemory API Reference

相关文章:

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

一个面向 Java 开发者的 Sring-Ai 示例工程项目&#xff0c;该项目是一个 Spring AI 快速入门的样例工程项目&#xff0c;旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计&#xff0c;每个模块都专注于特定的功能领域&#xff0c;便于学习和…...

适应性Java用于现代 API:REST、GraphQL 和事件驱动

在快速发展的软件开发领域&#xff0c;REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名&#xff0c;不断适应这些现代范式的需求。随着不断发展的生态系统&#xff0c;Java 在现代 API 方…...

Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案

在大数据时代&#xff0c;海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构&#xff0c;在处理大规模数据抓取任务时展现出强大的能力。然而&#xff0c;随着业务规模的不断扩大和数据抓取需求的日益复杂&#xff0c;传统…...

pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)

目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 &#xff08;1&#xff09;输入单引号 &#xff08;2&#xff09;万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…...

论文阅读笔记——Muffin: Testing Deep Learning Libraries via Neural Architecture Fuzzing

Muffin 论文 现有方法 CRADLE 和 LEMON&#xff0c;依赖模型推理阶段输出进行差分测试&#xff0c;但在训练阶段是不可行的&#xff0c;因为训练阶段直到最后才有固定输出&#xff0c;中间过程是不断变化的。API 库覆盖低&#xff0c;因为各个 API 都是在各种具体场景下使用。…...

【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制

目录 节点的功能承载层&#xff08;GATT/Adv&#xff09;局限性&#xff1a; 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能&#xff0c;如 Configuration …...

关于easyexcel动态下拉选问题处理

前些日子突然碰到一个问题&#xff0c;说是客户的导入文件模版想支持部分导入内容的下拉选&#xff0c;于是我就找了easyexcel官网寻找解决方案&#xff0c;并没有找到合适的方案&#xff0c;没办法只能自己动手并分享出来&#xff0c;针对Java生成Excel下拉菜单时因选项过多导…...

Ubuntu Cursor升级成v1.0

0. 当前版本低 使用当前 Cursor v0.50时 GitHub Copilot Chat 打不开&#xff0c;快捷键也不好用&#xff0c;当看到 Cursor 升级后&#xff0c;还是蛮高兴的 1. 下载 Cursor 下载地址&#xff1a;https://www.cursor.com/cn/downloads 点击下载 Linux (x64) &#xff0c;…...

掌握 HTTP 请求:理解 cURL GET 语法

cURL 是一个强大的命令行工具&#xff0c;用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中&#xff0c;cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...

wpf在image控件上快速显示内存图像

wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像&#xff08;比如分辨率3000*3000的图像&#xff09;的办法&#xff0c;尤其是想把内存中的裸数据&#xff08;只有图像的数据&#xff0c;不包…...

android RelativeLayout布局

<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...

windows系统MySQL安装文档

概览&#xff1a;本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容&#xff0c;为学习者提供全面的操作指导。关键要点包括&#xff1a; 解压 &#xff1a;下载完成后解压压缩包&#xff0c;得到MySQL 8.…...

【Linux】自动化构建-Make/Makefile

前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具&#xff1a;make/makfile 1.背景 在一个工程中源文件不计其数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;mak…...

如何应对敏捷转型中的团队阻力

应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中&#xff0c;明确沟通敏捷转型目的尤为关键&#xff0c;团队成员只有清晰理解转型背后的原因和利益&#xff0c;才能降低对变化的…...

安卓基础(Java 和 Gradle 版本)

1. 设置项目的 JDK 版本 方法1&#xff1a;通过 Project Structure File → Project Structure... (或按 CtrlAltShiftS) 左侧选择 SDK Location 在 Gradle Settings 部分&#xff0c;设置 Gradle JDK 方法2&#xff1a;通过 Settings File → Settings... (或 CtrlAltS)…...

华为OD机试-最短木板长度-二分法(A卷,100分)

此题是一个最大化最小值的典型例题&#xff0c; 因为搜索范围是有界的&#xff0c;上界最大木板长度补充的全部木料长度&#xff0c;下界最小木板长度&#xff1b; 即left0,right10^6; 我们可以设置一个候选值x(mid)&#xff0c;将木板的长度全部都补充到x&#xff0c;如果成功…...

【前端异常】JavaScript错误处理:分析 Uncaught (in promise) error

在前端开发中&#xff0c;JavaScript 异常是不可避免的。随着现代前端应用越来越多地使用异步操作&#xff08;如 Promise、async/await 等&#xff09;&#xff0c;开发者常常会遇到 Uncaught (in promise) error 错误。这个错误是由于未正确处理 Promise 的拒绝&#xff08;r…...

在 Spring Boot 项目里,MYSQL中json类型字段使用

前言&#xff1a; 因为程序特殊需求导致&#xff0c;需要mysql数据库存储json类型数据&#xff0c;因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...

「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案

在移动互联网营销竞争白热化的当下&#xff0c;推客小程序系统凭借其裂变传播、精准营销等特性&#xff0c;成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径&#xff0c;助力开发者打造具有市场竞争力的营销工具。​ 一、系统核心功能架构&…...

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准

城市路内停车管理常因行道树遮挡、高位设备盲区等问题&#xff0c;导致车牌识别率低、逃费率高&#xff0c;传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法&#xff0c;正成为破局关键。该设备安装于车位侧方0.5-0.7米高度&#xff0c;直接规避树枝遮…...

通过 Ansible 在 Windows 2022 上安装 IIS Web 服务器

拓扑结构 这是一个用于通过 Ansible 部署 IIS Web 服务器的实验室拓扑。 前提条件&#xff1a; 在被管理的节点上安装WinRm 准备一张自签名的证书 开放防火墙入站tcp 5985 5986端口 准备自签名证书 PS C:\Users\azureuser> $cert New-SelfSignedCertificate -DnsName &…...

libfmt: 现代C++的格式化工具库介绍与酷炫功能

libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库&#xff0c;提供了高效、安全的文本格式化功能&#xff0c;是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全&#xff1a…...

在 Spring Boot 中使用 JSP

jsp&#xff1f; 好多年没用了。重新整一下 还费了点时间&#xff0c;记录一下。 项目结构&#xff1a; pom: <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://ww…...

Chrome 浏览器前端与客户端双向通信实战

Chrome 前端&#xff08;即页面 JS / Web UI&#xff09;与客户端&#xff08;C 后端&#xff09;的交互机制&#xff0c;是 Chromium 架构中非常核心的一环。下面我将按常见场景&#xff0c;从通道、流程、技术栈几个角度做一套完整的分析&#xff0c;特别适合你这种在分析和改…...

LangFlow技术架构分析

&#x1f527; LangFlow 的可视化技术栈 前端节点编辑器 底层框架&#xff1a;基于 &#xff08;一个现代化的 React 节点绘图库&#xff09; 功能&#xff1a; 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...

elementUI点击浏览table所选行数据查看文档

项目场景&#xff1a; table按照要求特定的数据变成按钮可以点击 解决方案&#xff1a; <el-table-columnprop"mlname"label"名称"align"center"width"180"><template slot-scope"scope"><el-buttonv-if&qu…...

嵌入式常见 CPU 架构

架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集&#xff0c;单周期执行&#xff1b;低功耗、CIP 独立外设&#xff1b;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel&#xff08;原始…...

LOOI机器人的技术实现解析:从手势识别到边缘检测

LOOI机器人作为一款创新的AI硬件产品&#xff0c;通过将智能手机转变为具有情感交互能力的桌面机器人&#xff0c;展示了前沿AI技术与传统硬件设计的完美结合。作为AI与玩具领域的专家&#xff0c;我将全面解析LOOI的技术实现架构&#xff0c;特别是其手势识别、物体识别和环境…...

Qt 事件处理中 return 的深入解析

Qt 事件处理中 return 的深入解析 在 Qt 事件处理中&#xff0c;return 语句的使用是另一个关键概念&#xff0c;它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别&#xff1a;不同层级的事件处理 方…...

Vue ③-生命周期 || 脚手架

生命周期 思考&#xff1a;什么时候可以发送初始化渲染请求&#xff1f;&#xff08;越早越好&#xff09; 什么时候可以开始操作dom&#xff1f;&#xff08;至少dom得渲染出来&#xff09; Vue生命周期&#xff1a; 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...