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

2. SpringAI 使用Redis完成会话记忆和会话历史功能

前言SpringAI默认提供的会话记忆功能是基于内存的。如果程序重新启动那么会话记忆和会话历史都会失。但是SpringAI也提供了会话记忆和会话历史的持久化做法只不过只是提供的接口具体需要用户自己实现。这里就使用Redis进行持久化。Maven依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.22/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependencyYaml配置spring: application: name: demo-ai ai: openai: base-url: https://api.deepseek.com/ api-key: chat: options: model: deepseek-reasoner temperature: 0.7 data: redis: host: localhost port: 6379这里需要配置openAI的相关配置base-url模型的地址api-keyapply for an API key调用远程模型时一般都是收费的如果是本地模型可以没有这个配置model选择的远程模型种类temperature温度意思就是回答的随机性值越大回答的随机性越大这和Transformer神经网络的推理模式有关根据前文推测出接下来的一个词语后把这个词语加入前文再次交给大模型处理推测下一个字然后不断重复前面的过程就可以生成大段的内容了。redis的相关配置portredis主机地址portredis端口passwordredis密码(如果有配置就写没配置就不用管)SpringBoot配置类Configuration public class CommonConfiguration { Bean(chatClient) public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) { return ChatClient.builder(model) .defaultSystem(你是可爱且热情、人见人爱花见花开的AI助手你的名字是墩墩请用墩墩的身份回答用户的问题) .defaultAdvisors( new SimpleLoggerAdvisor(), // 如果失去它系统就不再有会话记忆功能了不过这个会话记忆是在内存里的 // 重启项目就会丢失数据 MessageChatMemoryAdvisor.builder(chatMemory).build() ) .build(); } //参数在容器中自动获取无需显式注入 Bean public ChatMemoryRepository chatMemoryRepository(RedisChatMemoryRepositoryDialect dialect) { return new RedisChatMemoryRepository(dialect); } // 重新声明会话记忆类需要指定此时使用redis持久化的实现类 Bean public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages(20) .build(); } }配置类里涉及到的RedisChatMemoryRepositoryDialect需要自己实现。RedisChatMemoryRepositoryDialect的实现Slf4j Component public class RedisChatMemoryRepositoryDialect { Autowired private RedisTemplateString, Object redisTemplate; Autowired private ObjectMapper objectMapper; // Redis里存所有活跃会话ID的Set key方便查找所有会话 private static final String JANE_CONVERSATION_KEY chat:conversation_ids; // 每个会话消息列表的key前缀 private static final String JANE_MESSAGE_LIST_PREFIX chat:messages:; /** * 获取所有活跃会话ID * Redis数据结构Set无序且唯一 * 用于快速获取当前所有存在的会话ID */ public ListString findConversationIds() { SetObject members redisTemplate.opsForSet().members(JANE_CONVERSATION_KEY); return Optional.ofNullable(members) .filter(m - !m.isEmpty()) .map(m - m.stream().map(Object::toString).collect(Collectors.toList())) .orElse(Collections.emptyList()); } /** * 根据会话ID获取该会话的所有消息列表多轮对话历史反序列化 * Redis数据结构List有序 * 按消息顺序返回方便构造对话上下文 */ public ListMessage findByConversationId(String conversationId) { String key JANE_MESSAGE_LIST_PREFIX conversationId; Long size redisTemplate.opsForList().size(key); if(size null || size 0L){ return Collections.emptyList(); } ListObject range redisTemplate.opsForList().range(key, size-21, -1); ListMessage messages new ArrayList(); for(Object o:range){ String json JSON.toJSONString(o); try { // 从 JsonParser 中读取 JSON 数据并将其反序列化为 JsonNode树形结构对象 JsonNode jsonNode objectMapper.readTree(json); messages.add(getMessage(jsonNode)); } catch (JsonProcessingException e) { throw new RuntimeException(Error deserializing message, e); } } return messages; } /** * 将一个 JsonNode 转换成对应的 Message 子类实例。 * 根据 messageType 字段决定返回哪种 Message 类型并提取 text 和 metadata 字段。 * 额外会在 metadata 中添加当前时间戳。 * * param jsonNode 传入的 JSON 树节点包含 messageType、text、metadata 等字段 * return 对应类型的 Message 对象实例AssistantMessage、UserMessage、SystemMessage 或 ToolResponseMessage */ private Message getMessage(JsonNode jsonNode) { // 从 jsonNode 中获取 messageType 字段的文本内容默认为 USER 类型 String type Optional.ofNullable(jsonNode) .map(node - node.get(messageType)) // 取 messageType 字段节点 .map(JsonNode::asText) // 转为字符串 .orElse(MessageType.USER.getValue()); // 如果没有该字段默认是 USER 类型 // 根据字符串转换为枚举类型 MessageType MessageType messageType MessageType.valueOf(type.toUpperCase()); // 从 jsonNode 中获取 text 字段的内容 String textContent Optional.ofNullable(jsonNode) .map(node - node.get(text)) // 取 text 字段节点 .map(JsonNode::asText) // 转为字符串 // 如果 text 字段不存在根据消息类型返回默认值 // SYSTEM 和 USER 类型默认返回空字符串 其他类型返回 null .orElseGet(() - (messageType MessageType.SYSTEM || messageType MessageType.USER) ? : null); // 从 jsonNode 中获取 metadata 字段并转换为 MapString, Object MapString, Object metadata Optional.ofNullable(jsonNode) .map(node - node.get(metadata)) // 取 metadata 节点 .map(node - objectMapper.convertValue( // 用 Jackson ObjectMapper 转换成 Map node, new TypeReferenceMapString, Object() {})) .orElse(new HashMap()); // 如果没有 metadata 字段返回空 Map // 在 metadata 中加入当前时间戳key 是 timestamp值是当前 ISO 格式时间字符串 if(!metadata.containsKey(timestamp)){ metadata.put(timestamp, Instant.now().toString()); } // 根据不同的消息类型构造对应的 Message 子类实例并返回 return switch (messageType) { case ASSISTANT - new AssistantMessage(textContent, metadata); // 助手消息 case USER - UserMessage.builder().text(textContent).metadata(metadata).build(); // 用户消息 case SYSTEM - SystemMessage.builder().text(textContent).metadata(metadata).build(); // 系统消息 case TOOL - new ToolResponseMessage(List.of(), metadata); // 工具调用消息 }; } /** * 保存一批消息到指定会话中追加到消息列表末尾 * Redis数据结构List右侧追加 * 并且保证会话ID存在于会话ID集合中 */ public void saveAll(String conversationId, ListMessage messages) { if(CollectionUtils.isEmpty(messages)) return; String keyJANE_MESSAGE_LIST_PREFIXconversationId; deleteByConversationId(conversationId); redisTemplate.opsForSet().add(JANE_CONVERSATION_KEY, conversationId); ListMessage filteredMessages messages.stream() .filter(Objects::nonNull) .filter(m - m.getText() ! null m.getMessageType() ! null).toList(); ListMessage finalMessages new ArrayList(); for(Message message:filteredMessages){ String json JSON.toJSONString(message); try { JsonNode jsonNode objectMapper.readTree(json); finalMessages.add(getMessageWithTime(jsonNode,message.getMessageType(),message.getText())); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } redisTemplate.opsForList().rightPushAll(key, finalMessages.toArray()); int maxHistorySize 100; redisTemplate.opsForList().trim(key, -maxHistorySize, -1); } /** * 在saveall操作时统一添加系统时间 * param jsonNode * param messageType * param textContent * return */ private Message getMessageWithTime(JsonNode jsonNode,MessageType messageType,String textContent){ // 从 jsonNode 中获取 metadata 字段并转换为 MapString, Object MapString, Object metadata Optional.ofNullable(jsonNode) .map(node - node.get(metadata)) .map(node - objectMapper.convertValue( node, new TypeReferenceMapString, Object() {})) .orElse(new HashMap()); if(!metadata.containsKey(timestamp)){ metadata.put(timestamp, Instant.now().toString()); } // 根据不同的消息类型构造对应的 Message 子类实例并返回 return switch (messageType) { case ASSISTANT - new AssistantMessage(textContent, metadata); // 助手消息 case USER - UserMessage.builder().text(textContent).metadata(metadata).build(); // 用户消息 case SYSTEM - SystemMessage.builder().text(textContent).metadata(metadata).build(); // 系统消息 case TOOL - new ToolResponseMessage(List.of(), metadata); // 工具调用消息 }; } /** * 删除指定会话的所有消息以及会话ID集合中的对应ID * Redis数据结构删除List Set中元素 */ public void deleteByConversationId(String conversationId) { String key JANE_MESSAGE_LIST_PREFIX conversationId; redisTemplate.delete(key); redisTemplate.opsForSet().remove(JANE_CONVERSATION_KEY, conversationId); } }RedisChatMemoryRepository的实现Component public class RedisChatMemoryRepository implements ChatMemoryRepository { private final RedisChatMemoryRepositoryDialect dialect; public RedisChatMemoryRepository(RedisChatMemoryRepositoryDialect dialect) { this.dialect dialect; } /** * 查询所有的对话ID列表。 * * return 返回所有存在的对话ID集合。 */ Override public ListString findConversationIds() { return dialect.findConversationIds(); } /** * 根据对话ID查询该对话下的所有消息。 * * param conversationId 对话的唯一标识ID。 * return 返回该对话对应的消息列表。 */ Override public ListMessage findByConversationId(String conversationId) { return dialect.findByConversationId(conversationId); } /** * 保存指定对话ID对应的消息列表支持批量保存。 * * param conversationId 对话的唯一标识ID。 * param messages 需要保存的消息列表。 */ Override public void saveAll(String conversationId, ListMessage messages) { dialect.saveAll(conversationId, messages); } /** * 删除指定对话ID对应的所有消息。 * * param conversationId 需要删除的对话ID。 */ Override public void deleteByConversationId(String conversationId) { dialect.deleteByConversationId(conversationId); } }Controller层实现RequiredArgsConstructor RestController RequestMapping(/ai) public class ChatController { private final Qualifier(chatClient)ChatClient chatClient; private final ChatHistoryService chatHistoryService; private final ChatMemory chatMemory; // 再弄一个流式的但是这里一定要设置字符编码要不然是会乱码的 RequestMapping(value /chat, produces text/html;charsetUTF-8) public FluxString chatStream(String prompt, String chatId) { return chatClient.prompt(prompt) .user(prompt) .advisors(a - a.param(CONVERSATION_ID, chatId)) // 添加一个SpringAAOP环绕增强的配置 用作会话ID记忆这样每次会话的内容就不会串 .stream() .content(); } RequestMapping(/{type}) public ListString getChatIds(PathVariable String type) { return chatHistoryService.getChatIds(type); } RequestMapping(/{type}/{chatId}) public ListMessageVO getChatHistory(PathVariable String type, PathVariable String chatId) { ListMessage messages chatMemory.get(chatId); if (messages null) { return List.of(); } return messages.stream().map(MessageVO::new).toList(); } }Service层实现// 保存会话历史的接口 public interface ChatHistoryService { /** * 从redis中获取会话id列表 * return 会话ID列表 */ ListString getChatIds(); }Service RequiredArgsConstructor public class InMemoryChatHistoryServiceImpl implements ChatHistoryService { private final ChatMemoryRepository chatMemoryRepository; Override public ListString getChatIds(String type) { return chatMemoryRepository.findConversationIds(); } }Redis中的数据格式-会话记忆这里记录的就是每个会话当中的会话历史。这样一来即使服务器重启会话的历史数据也由于进行了持久化因此也不会丢失。

相关文章:

2. SpringAI 使用Redis完成会话记忆和会话历史功能

前言SpringAI默认提供的会话记忆功能是基于内存的。如果程序重新启动,那么会话记忆和会话历史都会失。但是SpringAI也提供了会话记忆和会话历史的持久化做法,只不过只是提供的接口,具体需要用户自己实现。这里就使用Redis进行持久化。Maven依…...

进军高端制造“俱乐部”:智石开PLM在复杂产品研发领域的突破性应用排名

在制造业的金字塔尖,高端制造领域因其产品结构极端复杂、研发协同跨学科、质量与合规要求严苛,向来被视为PLM技术与解决方案的终极“试金石”。过去,这块代表行业最高标准与价值的高地,长期被西门子、达索系统、PTC等国际巨头所垄…...

eVTOL动力电驱系统功率链路设计实战:效率、功率密度与可靠性的高空平衡之道

在电动垂直起降飞行器(eVTOL)朝着长航时、高载荷与高安全等级不断演进的今天,其核心动力电驱系统的功率管理已不再是简单的能量转换单元,而是直接决定了飞行器航程边界、动力响应与飞行安全的核心。一条设计精良的高压功率链路&am…...

CRT设置快捷键——密码登录

SecureCRT 快捷键设置用于密码登录,很是便捷,以下文档用于记录下。1、打开CRT,最上端菜单栏中,找到并点击Options,选择Globa Options 并点击(这是全局设置)。2、翻译过来:常规Genera…...

春秋云境CVE-2019-13396

1.阅读靶场介绍这里我们得到的是文件包含的提示想到include2.启动靶场得到上面的照片然后第一感觉就是看url是否存在include这类的参数这里发现没有那我们接下来就是去登入后台了3.bp启动这里我们发现响应体出现include的参数直接尝试../../../../../../../flag读取旗帜如下图所…...

深度解析:Redis 预扣减与 RabbitMQ 异步解耦,如何完美平衡延迟与一致性?

🚀 深度解析:Redis 预扣减与 RabbitMQ 异步解耦,如何完美平衡延迟与一致性?💡 核心导读: 在高并发架构中,“延迟(Latency)” 和 “一致性(Consistency&#x…...

2026 年 Java 后端面试题,吃透 20 套专题技术栈

前言小编分享的这份 2026 年 Java 备战面试题总计有 1000 多道面试题,包含了 MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Java 并发编程、Java 基础、Spring、微服务、Linux、Spring Boot 、Spring Cloud、RabbitMQ、kafka 等 20 个专题技…...

期货程序化交易断线重连与订单状态同步

免责声明:本文基于个人使用体验,与任何厂商无商业关系。内容仅供技术交流参考,不构成投资建议。 一、前言 实盘运行中网络断线、进程重启后,需要重连并同步账户与订单状态,避免重复下单或漏单。做了多年期货程序化&am…...

德希科技水质监测仪厂家

一、核心技术与研发优势国内专业水质监测设备生产企业以自主研发为核心,将电化学、光学传感与智能控制技术深度融合,研发团队针对水环境监测痛点持续优化核心部件,设备在长期稳定性、抗干扰能力与维护便捷性上形成明显优势。一体化集成设计被…...

KingbaseES集群运维案例之--主备发生故障,主库能正常使用,备库无法启用

KingbaseES集群运维案例之–主备发生故障,主库能正常使用,备库无法启用 案例:主备发生故障,主库能正常使用,备库无法启用 文章目录KingbaseES集群运维案例之--主备发生故障,主库能正常使用,备库…...

白菜遗传转化

白菜遗传转化主要采用农杆菌介导法,以带柄子叶或花序为外植体,转化效率最高可达13.5%,常用于抗病和品质改良。主流方法比较 方法 外植体 优点 缺点 效率 子叶法 7–10天无菌苗带柄子叶 再生稳定、操作简便 需组培 8–…...

ORACLE-ADG

需要理解一下如下的一些概念,以下是19c,我的一个测试库的环境主库SERVICEprimaryINSTANCE_NAME(SQL)ORACLE_SID(instance)SID_NAME(listener)SERVICE_NAME(tns)DB_UNIQUE_NAME(pfile)FAL_CLIENT(pfile)SERVICE(pfile*XDB)cnpcdb_namecnpc---------------…...

小鼠T细胞激活试剂盒技术原理与应用

一、引言T淋巴细胞作为适应性免疫应答的核心细胞,在抗感染、抗肿瘤及免疫调节中发挥不可替代的作用。T细胞的活化是启动免疫应答的首要步骤,其过程受到严格的双信号调控。在过继性免疫治疗和基础免疫学研究中,如何在体外高效激活并扩增功能性…...

想找专业AI智能获客公司?这几个数字揭秘行业佼佼者!

引言在当今竞争激烈的商业环境中,企业面临着获客成本高、转化难、人效低等诸多挑战。专业的AI智能获客公司成为众多企业寻求突破的关键。本文将通过几个关键数字,揭秘行业中的佼佼者,为企业选择合适的AI智能获客伙伴提供参考。多客智能&#…...

无信号的井下场景,手持slam三维扫描难点在哪?

在无信号的地下空间,手持SLAM三维扫描,会面临以下难点: 1.无外部定位,完全靠自身算法 地下没有信号,设备只能靠自身惯导与视觉,一旦算法不稳,很容易漂移、重定位失败。 2.地下场景往往光线暗、…...

算法设计与分析-习题5.2

目录 1.应用快速排序将序列E,X,A,M,P,L,E按照字母顺序排序并画出相应的递归调用树。 2.对于本节描述的划分过程: a.请证明,如果两个扫描指针停下来以后指向的是同一个元素&#xf…...

DebugFS 文件系统

debugfs 是 Linux 内核提供的专用调试文件系统,核心定位是「为内核开发者 / 调试人员提供一个简单、统一的接口,用于暴露内核 / 硬件的调试信息、状态和配置」,你之前问到的 /sys/kernel/debug/dw/hdmi 就是 debugfs 最典型的应用场景。一、核…...

第二届大数据分析与人工智能应用学术会议(BDAIA2025)EI检索通知

【检索通知】BDAIA2025已被EI Compendex检索发布时间: 2026-03-11转发尊敬的投稿作者:您好! 我们很高兴通知您,第二届大数据分析与人工智能应用学术会议(BDAIA2025)已经成功实现EI Compendex检索,作者们可自…...

【第一篇】未来真AI记忆:道术分离分层耦合框架(AGI 长记忆核心架构)

【第一篇】未来真AI记忆:道术分离分层耦合框架(AGI 长记忆核心架构) 发布格式:CSDN 技术博客 / 人工智能 / 大模型 / 记忆系统 作者:华夏之光永存 本文主体定级:终极 未来真AI记忆:道术分离分层…...

卸载OpenClaw之linux安装方式

当然此方法适用于直接在linux上安装OpenClaw的方式,实际上我们应该避免直接在linux服务器上安装OpenClaw,可以采用docker的形式直接在linux上安装的话,风险够大,卸载麻烦。。。兼容性问题OpenClaw可能对特定Linux发行版或内核版本…...

收藏!2026年Java招聘面试两极分化,小白/程序员必看备考指南

2026年Java招聘面试的“两极分化”态势愈发明显,成为所有Java从业者(尤其是小白和初入职场的程序员)必须正视的现状:常规Java开发岗位需求持续缩减,招聘门槛同步抬高,竞争愈发激烈;而高含金量的…...

【Unity-AI开发篇】| Unity-MCP最新指南

原文连接...

试验台铁地板的应用场景与适配要求

试验台铁地板的应用场景铁地板(铸铁平台)因其高稳定性、耐磨性和抗变形能力,广泛应用于以下场景:精密测量与检测:用于三坐标测量机、激光跟踪仪等设备的安装基座,确保测量精度。机械加工与装配:…...

【笔试真题】- 顺丰-2026.03.15-第二套

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围在线刷题 bishipass.com 顺丰-2026.03.15-第二套 顺丰-2026.03.15-第二套 这套第二套整体偏基础,第一题是读完规则后直接按“行状态”收口,第二题虽然看起来像构造,实质只是在算可达…...

django基于深度学习的音乐推荐系统

第一章 音乐推荐系统开发背景与核心目标 在数字音乐产业蓬勃发展的当下,各大音乐平台汇聚了千万级别的歌曲资源,涵盖流行、摇滚、古典、民谣等多种曲风。但用户面临“选择过载”困境——难以从海量曲库中快速找到契合自身听觉偏好的音乐,传统…...

基于Python的服装品类趋势及消费者洞察数据分析可视化系统

目录数据收集与预处理趋势分析模型构建可视化系统开发消费者洞察模块系统部署与优化项目技术支持可定制开发之功能创新亮点源码获取详细视频演示 :文章底部获取博主联系方式!同行可合作数据收集与预处理 服装品类数据可从电商平台API(如淘宝…...

探索基于事件触发机制的多智能体系统事件触发控制及Matlab数值仿真

基于事件触发机制的多智能体系统事件触发控制,Matlab数值仿真实验。在多智能体系统(MAS)的研究领域,事件触发控制逐渐崭露头角,成为优化系统性能、减少资源消耗的重要手段。与传统的时间驱动控制不同,事件触…...

口岸边检智能化,筑牢国门安全防线

口岸边检是国家门户的重要防线,承担着人员出入境核验、打击非法出入境等重要职责,其工作效率与安全性,直接关系到国门安全与涉外交流的顺畅。传统的边检模式,依赖人工核验证件,不仅劳动强度大,还容易因人为…...

Unity Shader 实战:从零掌握 PBR 基于物理的渲染

一、什么是 PBR? PBR(Physically Based Rendering,基于物理的渲染)是现代游戏、影视行业的主流渲染方案。 与传统的 Blinn-Phong 光照相比,PBR 的核心区别在于: 对比项传统光照(Blinn-Phong&…...

全志H618

全志H618是一款很常见的芯片,主要用在电视盒子、开发板和智能家居小主机上。它主打低功耗和高性价比,在够用的性能下实现了非常好的能效比。 下面为你整理了它的核心参数和实际表现:参数类别具体规格CPU四核 ARM Cortex-A53,最高主…...