SpringAI 1.0.0 正式版——利用Redis存储会话(ChatMemory)

官方文档:Chat Memory :: Spring AI Reference

1. 引言

SpringAI 1.0.0 改动了很多地方,本文根据官方的InMemoryChatMemoryRepository实现了自定义的RedisChatMemoryRepository,并使用MessageWindowChatMemory创建ChatMemory

2. 实现

2.1. 添加依赖

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

注意:SpringAI 1.0.0的maven依赖有所改变,artifactId变化了

2.2. 配置文件

server:port: 8080
spring:ai:openai:api-key: xxx     # 填自己的api-keybase-url: https://api.deepseek.comchat:options:model: deepseek-chattemperature: 0.7data:redis:host: localhostport: 6379password: 123456

正确配置redis连接即可

api-key可以填deepseek的(需要购买,1块钱能用挺久)

2.3. RedisChatMemoryRepository

RedisChatMemoryRepository用于存储会话数据

这里参考InMemoryChatMemoryRepository与【SpringAI 1.0.0】 ChatMemory 转换为 Redis 存储_springai如何将数据保存到redis-CSDN博客

package com.njust.repository;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.content.Media;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.MimeType;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;public class RedisChatMemoryRepository implements ChatMemoryRepository {private final StringRedisTemplate stringRedisTemplate;  // 用于操作 Redisprivate final ObjectMapper objectMapper;    // 用于序列化和反序列化private final String PREFIX ;       // 存储对话的 Redis Key 前缀private final String CONVERSATION_IDS_SET;  // 存储对话ID的 Redis Keypublic RedisChatMemoryRepository(StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {this(stringRedisTemplate, objectMapper, "chat:conversation:", "chat:all_conversation_ids");}public RedisChatMemoryRepository(StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper, String PREFIX) {this(stringRedisTemplate, objectMapper, PREFIX, "chat:all_conversation_ids");}public RedisChatMemoryRepository(StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper, String PREFIX, String CONVERSATION_IDS_SET) {this.stringRedisTemplate = stringRedisTemplate;this.objectMapper = objectMapper;this.PREFIX = PREFIX;this.CONVERSATION_IDS_SET = CONVERSATION_IDS_SET;}// 获取所有 conversationId(KEYS 命令匹配 chat:*)@Overridepublic List<String> findConversationIds() {// 使用ZSet存储对话ID(更高效)// 获取对话ID集合(按时间倒序排序,越晚创建的对话ID排在前面)Set<String> conversationIds = stringRedisTemplate.opsForZSet().reverseRange(CONVERSATION_IDS_SET, 0, -1);if (conversationIds == null || conversationIds.isEmpty()) {return List.of();}return new ArrayList<>(conversationIds);}// 根据 conversationId 获取 Message 列表@Overridepublic List<Message> findByConversationId(String conversationId) {// 参数验证if (conversationId == null || conversationId.isEmpty()) {throw new IllegalArgumentException("conversationId cannot be null or empty");}List<String> list = stringRedisTemplate.opsForList().range(PREFIX + conversationId, 0, -1);if (list == null || list.isEmpty()) {return List.of();}return list.stream().map(json -> {try {// return objectMapper.convertValue(json, Message.class);   // 直接反序列化Message会报错return deserializeMessage(json);    // 手动反序列化} catch (IOException e) {throw new RuntimeException(e);}}).collect(Collectors.toList());}// 保存整个 Message 列表到指定 conversationId@Overridepublic void saveAll(String conversationId, List<Message> messages) {// 参数验证if (conversationId == null || conversationId.isEmpty()) {throw new IllegalArgumentException("conversationId cannot be null or empty");}// 先清除原有的 conversation 数据stringRedisTemplate.delete(PREFIX + conversationId);if (messages == null || messages.isEmpty()) {return;}List<String> list = messages.stream().map(message -> {try {return objectMapper.writeValueAsString(message);} catch (JsonProcessingException e) {throw new RuntimeException("Failed to serialize Message", e);}}).collect(Collectors.toList());stringRedisTemplate.opsForList().rightPushAll(PREFIX + conversationId, list);// 更新对话ID集合stringRedisTemplate.opsForZSet().add(CONVERSATION_IDS_SET, conversationId, System.currentTimeMillis());}// 删除指定 conversationId 的数据@Overridepublic void deleteByConversationId(String conversationId) {if (conversationId == null || conversationId.isEmpty()) {throw new IllegalArgumentException("conversationId cannot be null or empty");}stringRedisTemplate.delete(PREFIX + conversationId);stringRedisTemplate.opsForZSet().remove(CONVERSATION_IDS_SET, conversationId);}// 手动反序列化 Messagepublic Message deserializeMessage(String json) throws IOException {// 解析 JSON 字符串为 JsonNodeJsonNode jsonNode = objectMapper.readTree(json);// 获取 messageType 字段值if (!jsonNode.has("messageType")) {throw new IllegalArgumentException("Missing or invalid messageType field");}String messageType = jsonNode.get("messageType").asText();// 获取 text 字段值String text = jsonNode.has("text") ? jsonNode.get("text").asText() : "";// 获取 metadata 字段值Map<String, Object> metadata = getMetadata(jsonNode);// 获取 media 字段值List<Media> mediaList = getMediaList(jsonNode);return switch (MessageType.valueOf(messageType)) {case SYSTEM -> new SystemMessage(text);case USER -> UserMessage.builder().text(text).media(mediaList).metadata(metadata).build();case ASSISTANT -> {List<AssistantMessage.ToolCall> toolCalls = getToolCalls(jsonNode);yield new AssistantMessage(text, metadata, toolCalls, mediaList);}default -> throw new IllegalArgumentException("Unknown message type: " + messageType);};}private Media deserializeMedia(ObjectMapper mapper, JsonNode mediaNode) throws IOException {Media.Builder builder = Media.builder();// Handle MIME typeif (mediaNode.has("mimeType")) {JsonNode mimeNode = mediaNode.get("mimeType");String type = mimeNode.get("type").asText();String subtype = mimeNode.get("subtype").asText();builder.mimeType(new MimeType(type, subtype));}// Handle data - could be either URL string or byte arrayif (mediaNode.has("data")) {String data = mediaNode.get("data").asText();if (data.startsWith("http://") || data.startsWith("https://")) {builder.data(new URL(data));} else {// Assume it's base64 encoded binary databyte[] bytes = Base64.getDecoder().decode(data);builder.data(bytes);}}// Handle dataAsByteArray if present (overrides data if both exist)if (mediaNode.has("dataAsByteArray")) {byte[] bytes = Base64.getDecoder().decode(mediaNode.get("dataAsByteArray").asText());builder.data(bytes);}// Handle optional fieldsif (mediaNode.has("id")) {builder.id(mediaNode.get("id").asText());}if (mediaNode.has("name")) {builder.name(mediaNode.get("name").asText());}return builder.build();}private Map<String, Object> getMetadata(JsonNode jsonNode) {if (jsonNode.has("metadata")) {return objectMapper.convertValue(jsonNode.get("metadata"), new TypeReference<>() {});}return new HashMap<>();}private List<Media> getMediaList(JsonNode jsonNode) throws IOException {List<Media> mediaList = new ArrayList<>();if (jsonNode.has("media")) {for (JsonNode mediaNode : jsonNode.get("media")) {mediaList.add(deserializeMedia(objectMapper, mediaNode));}}return mediaList;}private List<AssistantMessage.ToolCall> getToolCalls(JsonNode jsonNode) {if (jsonNode.has("toolCalls")) {return objectMapper.convertValue(jsonNode.get("toolCalls"), new TypeReference<>() {});}return Collections.emptyList();}
}

主要的部分都写上注释了,应该比较好理解

需要注意的是反序列化Message需要手动进行

2.4. 注册Bean

package com.njust.config;import com.fasterxml.jackson.databind.ObjectMapper;
import com.njust.repository.ChatHistoryRepository;
import com.njust.repository.RedisChatHistoryRepository;
import com.njust.repository.RedisChatMemoryRepository;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;@Configuration
public class CommonConfiguration {@Beanpublic ChatMemoryRepository chatMemoryRepository(StringRedisTemplate stringRedisTemplate) {// 默认情况下,如果尚未配置其他存储库,则 Spring AI 会自动配置ChatMemoryRepository类型的 beanInMemoryChatMemoryRepository可以直接在应用程序中使用。// 这里手动创建内存聊天记忆存储库return new RedisChatMemoryRepository(stringRedisTemplate, new ObjectMapper());}@Beanpublic ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {// 注册聊天上下文记忆机制return MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).maxMessages(20)   // 聊天记忆条数.build();}@Bean// 通过OpenAI平台注入deepseek模型public ChatClient deepseekChatClient(OpenAiChatModel openAiChatModel, ChatMemory chatMemory) {return ChatClient.builder(openAiChatModel).defaultSystem("你是南京理工大学计算机科学与工程学院的一名研究生,你的名字叫小兰").defaultAdvisors(new SimpleLoggerAdvisor(),  // 配置日志AdvisorMessageChatMemoryAdvisor.builder(chatMemory).build()    // 绑定上下文记忆).build();}
}

这里用MessageWindowChatMemory创建ChatMemory,用于限制上下文记忆条数

2.5. Controller

package com.njust.controller;import com.njust.repository.ChatHistoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.content.Media;
import org.springframework.util.MimeType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;import java.util.List;
import java.util.Objects;import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;// @RequiredArgsConstructor 的作用是:为所有 final 字段或带有 @NotNull 注解的字段自动生成构造函数,实现简洁、安全的依赖注入
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {private final ChatClient deepseekChatClient;@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")public Flux<String> chat(@RequestParam("prompt") String prompt,@RequestParam("chatId") String chatId) {return deepseekChatClient.prompt().user(prompt).advisors(a -> a.param(CONVERSATION_ID, chatId)).stream().content();}
}

3. 效果

redis中存储的内容为:

如果想存储某个用户的会话id,也可以存储在redis中,自己写个方法,用zset存,在controller中的chat方法内,将chatId作为member存入zset,score为时间戳,这样就能根据创建时间排序了。每次会话都更新下score,这样最新发送消息订单会话就能排在最上面,和deepseek一样。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/83793.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RFC8489-STUN

0. 学习参考 RFC5389 中文翻译 中文RFC RFC文档 RFC翻译 RFC中文版 RFC 5389&#xff1a;NAT 的会话遍历实用程序 &#xff08;STUN&#xff09; --- RFC 5389: Session Traversal Utilities for NAT (STUN) 1. RFC 3489的演变 自 RFC 3489 发布以来的经验发现&#xff0c;…

开始在本地部署自己的 Gitea 服务器

0.简介 在软件开发和团队协作中&#xff0c;代码管理是至关重要的环节。笔者一直使用gitblit管理自己的仓库。然鹅&#xff0c;这个软件已经很久没有更新了。经过多方考察&#xff0c;发现Gitea 是一款轻量级的开源代码托管平台&#xff0c;具有易于部署、资源占用少、功能丰富…

Xsens-AAA工作室品质,为动画师准备

每一帧都讲述着一个故事&#xff0c;当动作真实呈现时&#xff0c;故事便鲜活起来。我们打造并改进了 Xsens Animate&#xff0c;助力专业人士突破数字动画的界限。 通过升级后的 Xsens Animate&#xff0c;您可以获得女性和男性解剖模型以及更精确的运动引擎&#xff0c;从一…

嵌入(Embedding)技术的实现原理与应用场景解析

嵌入&#xff08;Embedding&#xff09;技术的实现原理与应用场景解析 引言&#xff1a;从One-Hot到语义空间 在自然语言处理的演进历程中&#xff0c;嵌入技术&#xff08;Embedding&#xff09;的诞生标志着一个重要转折点——它让离散的符号表示突破了维度诅咒&#xff0c…

金仓数据库征文-金仓KES数据同步优化实践:逻辑解码与增量同步

目录 一.同步场景与方案选型 二.什么是KES 三.同步环境配置 1.前置条件验证 2.逻辑解码配置 四.同步实施与问题排查 1.结构映射规则 2.增量数据捕获 3.数据一致性校验 五.性能调优实践 1.同步线程优化 2.批量提交优化 3.资源监控指标 六.典型场景解决方案 1.双向…

开源语义分割工具箱mmsegmentation基于Lovedata数据集训练模型

开源语义分割工具箱mmsegmentation安装环境 文章目录 1、下载数据集2、整理数据集3、下载预训练模型4、测试5、训练模型参考官方数据处理步骤 https://github.com/open-mmlab/mmsegmentation/blob/main/docs/zh_cn/user_guides/2_dataset_prepare.md#loveda 数据集类别标签:…

Python概率统计可视化——概率分布、假设检验与分子运动模型

Python概率统计可视化——概率分布、假设检验与分子运动模型 前言 概率统计作为描述不确定性和随机现象的数学工具&#xff0c;广泛应用于物理学、生物学、经济学等领域。然而&#xff0c;抽象的概率分布和统计推断过程往往难以直观理解。可视化技术通过将概率密度、假设检验逻…

NLP学习路线图(二十二): 循环神经网络(RNN)

在自然语言处理&#xff08;NLP&#xff09;的广阔天地中&#xff0c;序列数据是绝对的核心——无论是流淌的文本、连续的语音还是跳跃的时间序列&#xff0c;都蕴含着前后紧密关联的信息。传统神经网络如同面对一幅打散的拼图&#xff0c;无法理解词语间的顺序关系&#xff0c…

禅道5月更新速览 | 新增交付物配置功能,支持建立跨执行任务依赖关系,研发效能平台上线

禅道体验又升级啦&#xff01;禅道5月新功能合集来啦&#xff0c;研发效能平台与大家见面啦&#xff01; 我们将继续坚持&#xff0c;月月有大招&#xff0c;迭代不停歇&#xff0c;快来更新禅道&#xff0c;体验全新的项目管理工具吧~ ​

【PDF PicKiller】PDF批量删除固定位置图片工具,默认解密,可去一般图、背景图、水印图!

PDF批量删除固定位置图片工具 PDF PicKiller <center>PDF PicKiller [Download](https://github.com/Peaceful-World-X/PDF-PicKiller)&#x1f929; 工具介绍&#x1f973; 主要功能&#x1f92a; 软件使用&#x1f92a; 参数解释&#x1f92a; 关键代码&#x1f929; 项…

kubeadm安装k8s

1、环境准备 1.1、升级系统内核 参考另一篇文章&#xff1a;https://blog.csdn.net/u012533920/article/details/148457715?spm1011.2415.3001.5331 1.2、设置Hostname cat <<EOF > /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhos…

Vue基础(14)_列表过滤、列表排序

Array.prototype.filter()【ES5】 filter() 方法创建给定数组一部分的浅拷贝&#xff0c;其包含通过所提供函数实现的测试的所有元素。 语法&#xff1a; filter(callbackFn) filter(callbackFn, thisArg) 参数&#xff1a; callbackFn(回调函数)&#xff1a;为数组中的每个元…

ComfyUI 中如何使用 Depth ControlNet SD1.5

目录 SD1.5 Depth ControlNet 简介 Depth ControlNet 主要特点 SD1.5 Depth ControlNet工作流准备工作 1. 安装必要插件 方式一:使用 ComfyUI Manager(推荐) 方式二:通过 git 安装必要插件 方式三:手动安装(不推荐) 2.1 下载工作流所需模型 2.2 模型存放位置 SD…

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)

文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…

mysql的分页场景下,页数越大查询速度越慢的解决方法

一 问题描述 select * from table limit 0,10扫描满足条件的10行&#xff0c;返回10行&#xff0c; 但当limit 99989,10的时候数据读取就很慢,limit 99989,10的意思扫描满足条件的99999行&#xff0c;扔掉前面的99989行&#xff0c;返回最后的10行&#xff0c;这样速度就会很慢…

MDP的 Commands模块

文章目录 Isaac Lab Commands 模块详细指南&#x1f4cb; 模块概述&#x1f3d7;️ 模块架构&#x1f3aa; 命令类型详解1. &#x1f6ab; 空命令 (NullCommand)2. &#x1f3c3; 速度命令 (VelocityCommand)&#x1f3b2; 均匀分布速度命令 (UniformVelocityCommand)&#x1f…

全流程开源!高德3D贴图生成系统,白模一键生成真实感纹理贴图

导读 MVPainter 随着3D生成从几何建模迈向真实感还原&#xff0c;贴图质量正逐渐成为决定3D资产视觉表现的核心因素。我们团队自研的MVPainter系统&#xff0c;作为业内首个全流程开源的3D贴图生成方案&#xff0c;仅需一张参考图与任意白模&#xff0c;即可自动生成对齐精确…

Levenberg-Marquardt算法详解和C++代码示例

Levenberg-Marquardt&#xff08;LM&#xff09;算法是非线性最小二乘问题中常用的一种优化算法&#xff0c;它融合了高斯-牛顿法和梯度下降法的优点&#xff0c;在数值计算与SLAM、图像配准、机器学习等领域中应用广泛。 一、Levenberg-Marquardt算法基本原理 1.1 问题定义 …

理解网络协议

1.查看网络配置 : ipconfig 2. ip地址 : ipv4(4字节, 32bit), ipv6, 用来标识主机的网络地址 3.端口号(0~65535) : 用来标识主机上的某个进程, 1 ~ 1024 知名端口号, 如果是服务端的话需要提供一个特定的端口号, 客户端的话是随机分配一个端口号 4.协议 : 简单来说就是接收数据…

如何计算光伏工程造价预算表?

在光伏工程的推进过程中&#xff0c;造价预算表的编制是至关重要的环节&#xff0c;传统的光伏工程造价预算编制方法&#xff0c;往往依赖人工收集数据、套用定额&#xff0c;再进行繁琐的计算与汇总&#xff0c;不仅效率低下&#xff0c;而且容易出现人为误差&#xff0c;导致…