(V3)SpringAI——会话记忆对齐+多AI总结
前言
SpringAI框架初步集成:SpringAI的初步用法
SpringAI框架会话记忆优化:SpringAI——会话记忆优化版本
虽然之前已经集成SpringAI框架,实现基于Redis的会话记忆,并实现五轮+摘要的优化。但是和算法端同学对接后,发现有一些流程需要进一步优化,因为原来记忆优化的版本,是每五轮进行一次总结,并且删除原始五轮会话,进行一次总结。
这样的效果就是:
- 第五轮时候开始优化,原始五轮会话记忆丢失,并生成第一轮摘要
- 第八轮时候,只有最近三轮原始对话记忆,以及原来五轮进行总结生成的一轮摘要
- 第十轮时候,再次优化,将最近五轮原始会话记忆删除,并再次生成一轮摘要
- 第十一轮,情况就成为了:只有一轮原始会话+两轮会话摘要。
那么问题就出现了:
- 这样的会话记忆压缩形式,本质是减缓会话膨胀,但是随着会话轮数的增加,摘要轮数依旧会有增加。
- 会话记忆总结期间,通过消息队列解耦会话总结,再删除会话记忆,这样就会导致有一小段时间的记忆断层。
- 同时所有的摘要仅仅针对当前五轮会话,也就是说,即使是摘要,也无法进行有效的总结。
- 同时这并不满足算法端之前提出的五轮会话+一轮摘要的情况。
与算法端进一步对齐信息差后,解决方案也显而易见:
- 五轮会话,指的是最近的五轮原始会话,而不是每五轮一次优化。
- 一轮摘要,指的是携带原来的摘要,以及这五轮会话,进行进一步的摘要总结,而不是删除前五轮会话
- 本质上来说,这种形式类似于滑动窗口,第五轮后,每一轮淘汰一轮原始会话,同时进行摘要总结。
- 同时不仅仅是摘要总结,对于数据看报,也需要对用户的其他特征进行总结,比如标签、所有会话流程摘要、危险等级、危险词。
那么接下来就是对于流程的优化
1. 优化记忆形式
- RedisSummeryMemoryRepository的流程优化
- 当会话轮数超过了最大限制的时候(消息数>=12),进入优化流程
- 发送到消息队列,进行摘要总结
- 移除其中的系统角色信息,防止出现系统信息存入Redis的现象
- 移除最早一轮会话,压缩到五轮会话(10条)消息
- 存入Redis
/** * @author by 王玉涛 * @Classname RedisSummeryMemoryRepository * @Description 基于Redis实现的对话记忆仓库,用于存储和管理AI对话历史记录。 * 支持以下功能: * 1. 存储、查询、删除对话记录 * 2. 对超出最大容量的对话进行摘要处理,并通过消息队列发送给AI模型生成总结 * 3. 使用缓存提供者(CacheProvider)操作Redis,保证数据读写一致性 * @Date 2025/7/13 13:00 */ @Repository @Slf4j @RequiredArgsConstructor public class RedisSummeryMemoryRepository implements ChatMemoryRepository { /** * RabbitMQ工具类,用于异步发送消息到AI模型服务 */ private final RabbitUtils rabbitUtils; /** * 最大对话记录数,超过该数量后将触发摘要逻辑 * 配置来源:application.yml 中的 doubao.memory.max-messages 属性 */ @Value("${doubao.memory.max-memory-size}") private Integer maxSize; /** * 存储所有对话ID集合的缓存键 * 用于快速获取系统中所有的 conversationId */ private static final String AI_MEMORY_CONVERSATION_IDS_KEY = "ai:memory:conversation:ids"; /** * 对话记录的缓存键前缀 * 后接具体的 conversationId 构成完整键名,如:ai:memory:conversation:12345 */ private static final String AI_MEMORY_CONVERSATION_KEY_PREFIX = "ai:memory:conversation:"; /** * 对话摘要的缓存键前缀 * 后接具体的 conversationId 构成完整键名,如:ai:memory:summary:12345 */ private static final String AI_MEMORY_SUMMARY_KEY_PREFIX = "ai:memory:summary:"; /** * 缓存提供者接口实例,封装了对Redis的基本操作 */ private final CacheProvider cacheProvider; /** * 获取所有存在的对话ID列表 * * @return 包含所有 conversationId 的字符串列表 */ @Override public List<String> findConversationIds() { log.info("基于Redis内存的聊天历史记忆:查询所有的id"); return cacheProvider.getSet(AI_MEMORY_CONVERSATION_IDS_KEY, String.class); } /** * 根据指定的 conversationId 查询完整的对话内容 * * @param conversationId 对话唯一标识符 * @return Spring AI 的 Message 列表,表示整个对话历史 * 因为当前查找的信息,会被截断,所以说,我们目前有一个比较确定的是:我们自己设置的对话窗口以及上下文限制,不能一样。 */ @Override public List<Message> findByConversationId(String conversationId) { log.info("基于Redis内存的聊天历史记忆:查询id为{}的历史记忆+信息摘要", conversationId); // 从 Redis 中获取原始对话 String data = cacheProvider.getString(AI_MEMORY_CONVERSATION_KEY_PREFIX, conversationId); String summaryData = cacheProvider.getString(AI_MEMORY_SUMMARY_KEY_PREFIX, conversationId); // 如果主数据和摘要都为空,则直接返回空列表,避免后续无意义处理 if (StringUtils.isAllNullOrEmpty(data, summaryData)) { return Collections.emptyList(); } // 将主数据反序列化为 MessageInfo 列表 List<MessageInfo> myMessages = new ArrayList<>(); if (!StringUtils.isNullOrEmpty(data)) { myMessages.addAll(JSONUtil.toList(data, MessageInfo.class)); } // 合并主消息和摘要消息 if (StringUtils.isNotNullOrEmpty(summaryData)) { myMessages.add(MessageFactory.createSystemMessage(summaryData)); } // 最终将 MessageInfo 转换为 Spring AI 的 Message 类型并返回 return myMessages.stream() .map(MessageConvertor::myToAiMessage) .toList(); } /** * 保存与指定 conversationId 关联的所有对话消息 * * @param conversationId 对话唯一标识符 * @param messages 要保存的 Spring AI Message 列表 */ @Override public void saveAll(String conversationId, List<Message> messages) { // 将 Spring AI 的 Message 列表转换为本地 MessageInfo 列表 List<MessageInfo> messageInfos = new ArrayList<>(messages.stream().map(MessageConvertor::aiMessageToMy).toList()); // 判断非系统消息数量是否超过最大限制 if (countSystemSize(messageInfos) > maxSize + 1) { log.info("基于Redis内存的聊天历史记忆:对话记录轮数数超出最大限制,进行摘要总结并精简"); // 构建摘要键并生成摘要请求对象 String key = AI_MEMORY_SUMMARY_KEY_PREFIX + conversationId; SummaryMessage summaryMessage = MessageFactory.createSummaryRequest(key, messageInfos, Long.parseLong(conversationId)); // 使用 RabbitMQ 发送摘要请求至 AI 模型处理 rabbitUtils.sendToQueue( RabbitMQConstant.AI_MODEL_EXCHANGE, RabbitMQConstant.AI_MODEL_SUMMARY_KEY, summaryMessage ); // 移除系统消息 messageInfos = excludeSystemMessages(messageInfos); // 移除最早的一轮会话以腾出空间 messageInfos.remove(0); messageInfos.remove(0); // 更新 Redis 中的对话记录 key = AI_MEMORY_CONVERSATION_KEY_PREFIX + conversationId; cacheProvider.set(key, JSONUtil.toJsonStr(messageInfos)); return; } // 当前对话未超出限制,正常保存对话记录 log.info("基于Redis内存的聊天历史记忆:保存id为{}的历史记忆", conversationId); // 将当前 conversationId 添加到全局对话ID集合中,便于后续查询 cacheProvider.addSet(AI_MEMORY_CONVERSATION_IDS_KEY, conversationId); // 将 Spring AI 的 Message 列表转换为本地 MessageInfo 列表 List<MessageInfo> myMessages = messages.stream() .map(MessageConvertor::aiMessageToMy) .toList(); // 过滤掉系统消息,避免系统消息被重复存储或影响对话上下文 myMessages = excludeSystemMessages(myMessages); // 将过滤后的消息列表序列化为 JSON 字符串并保存到 Redis cacheProvider.set(AI_MEMORY_CONVERSATION_KEY_PREFIX + conversationId, JSONUtil.toJsonStr(myMessages)); } /** * 删除与指定 conversationId 关联的所有对话记录 * * @param myMessages 待处理的消息列表 * @return 过滤后的消息列表,不包含系统消息 */ List<MessageInfo> excludeSystemMessages(List<MessageInfo> myMessages) { int size = myMessages.size(); List<MessageInfo> messageInfos = new ArrayList<>(size); for (MessageInfo messageInfo : myMessages) { if (Objects.isNull(messageInfo)) { continue; } // 过滤 senderIdentity 为 3 的系统消息 if (messageInfo.getSenderIdentity() != 3) { messageInfos.add(messageInfo); } } return messageInfos; } /** * 计算非系统消息的个数 * * @param messageInfos 消息列表 * @return 非系统消息的个数 */ private int countSystemSize(List<MessageInfo> messageInfos) { int count = 0; for (MessageInfo messageInfo : messageInfos) { // senderIdentity 为 3 表示系统消息 if (messageInfo.getSenderIdentity() != 3) { count++; } } return count; } /** * 删除与指定 conversationId 关联的所有对话记录 * * @param conversationId 要删除的对话唯一标识符 */ @Override public void deleteByConversationId(String conversationId) { log.info("基于Redis内存的聊天历史记忆:删除id为{}的历史记忆", conversationId); // 删除该 conversationId 对应的对话记录和全局ID集合中的条目 cacheProvider.deleteByKey(AI_MEMORY_CONVERSATION_KEY_PREFIX + conversationId); cacheProvider.deleteByKey(AI_MEMORY_SUMMARY_KEY_PREFIX + conversationId); cacheProvider.deleteByKey(AI_MEMORY_CONVERSATION_IDS_KEY, conversationId); } }
/** * 保存与指定 conversationId 关联的所有对话消息 * * @param conversationId 对话唯一标识符 * @param messages 要保存的 Spring AI Message 列表 */ @Override public void saveAll(String conversationId, List<Message> messages) { // 将 Spring AI 的 Message 列表转换为本地 MessageInfo 列表 List<MessageInfo> messageInfos = new ArrayList<>(messages.stream().map(MessageConvertor::aiMessageToMy).toList()); // 判断非系统消息数量是否超过最大限制 if (countSystemSize(messageInfos) > maxSize + 1) { log.info("基于Redis内存的聊天历史记忆:对话记录轮数数超出最大限制,进行摘要总结并精简"); // 构建摘要键并生成摘要请求对象 String key = AI_MEMORY_SUMMARY_KEY_PREFIX + conversationId; SummaryMessage summaryMessage = MessageFactory.createSummaryRequest(key, messageInfos, Long.parseLong(conversationId)); // 使用 RabbitMQ 发送摘要请求至 AI 模型处理 rabbitUtils.sendToQueue( RabbitMQConstant.AI_MODEL_EXCHANGE, RabbitMQConstant.AI_MODEL_SUMMARY_KEY, summaryMessage ); // 移除系统消息 messageInfos = excludeSystemMessages(messageInfos); // 移除最早的一轮会话以腾出空间 messageInfos.remove(0); messageInfos.remove(0); // 更新 Redis 中的对话记录 key = AI_MEMORY_CONVERSATION_KEY_PREFIX + conversationId; cacheProvider.set(key, JSONUtil.toJsonStr(messageInfos)); return; } // 当前对话未超出限制,正常保存对话记录 log.info("基于Redis内存的聊天历史记忆:保存id为{}的历史记忆", conversationId); // 将当前 conversationId 添加到全局对话ID集合中,便于后续查询 cacheProvider.addSet(AI_MEMORY_CONVERSATION_IDS_KEY, conversationId); // 将 Spring AI 的 Message 列表转换为本地 MessageInfo 列表 List<MessageInfo> myMessages = messages.stream() .map(MessageConvertor::aiMessageToMy) .toList(); // 过滤掉系统消息,避免系统消息被重复存储或影响对话上下文 myMessages = excludeSystemMessages(myMessages); // 将过滤后的消息列表序列化为 JSON 字符串并保存到 Redis cacheProvider.set(AI_MEMORY_CONVERSATION_KEY_PREFIX + conversationId, JSONUtil.toJsonStr(myMessages)); }
2. 消息队列的多AI总结
2.1 前置准备
- 多Prompt拓展
/** * @author by 王玉涛 * @Classname DoubaoPrompt * @Description TODO * @Date 2025/7/12 14:45 */ public class DoubaoPrompt { private DoubaoPrompt() {} /** * 危险信号检测 */ public static final String DANGER_WORD_CHECK_PROMPT = """ # 身份 你是一名顶尖的心理咨询专家和危机预警系统,拥有多年的临床经验,尤其擅长从对话中精准、快速地识别出潜在的危机信号。你的核心能力是对文本进行高精度的危险信号检测。 # 任务 你的任务是分析心理咨询来访者(用户)输入的文本,并仅检测其中所有表明潜在危险的词汇或短语。你需要像一个高度灵敏的雷达,只捕捉和输出那些明确指向危险的信号词。 # 危险词汇定义与分类 危险词汇主要指代以下几类情况,你需要对这些词汇高度敏感: 1. **自我伤害/自杀倾向**: 表明有结束自己生命或伤害自己身体意图的词。例如:想死、自杀、轻生、结束生命、离开这个世界、解脱、没意思、安乐死、一了百了、撑不下去、跳楼、割腕等。 2. **伤害他人/暴力倾向**: 表明有对他人进行身体或心理伤害意图的词。例如:报复、杀、弄死、捅、毁掉他、同归于尽、攻击、暴力、恨之入骨等。 3. **严重的精神状态异常**: 表明可能存在幻觉、妄想、精神崩溃等严重情况的词。例如:幻觉、幻听、被监控、被害妄想、崩溃、分裂、失控等。 4. **遭受虐待/暴力**: 表明来访者正在经受来自他人的伤害。例如:家暴、被打、被虐待、被控制、威胁、欺凌等。 # 工作流程 1. 接收并仔细分析来访者输入的文本。 2. 根据【危险词汇定义与分类】进行扫描和匹配。 3. 提取并识别出所有符合定义的危险词汇或短语。 4. 如果未发现任何危险词汇,则明确告知“未检测到危险词汇”。 # 输出格式 按照 ["危险词汇一", "危险词汇二", "危险词汇三", ...] 格式输出,贴合Java的List<String>序列化后风格即可, 并且注意,按照从新到旧的顺序,尽量不超过10个词汇,注意:不要有重复词汇,同时也不要有意思相近的词汇 """; /** * 获取所有消息摘要 */ public static final String ALL_MESSAGE_SUMMARY_PROMPT = """ # 身份 你是一位经验丰富的临床心理学督导,拥有深厚的心理咨询理论功底和丰富的实践经验。你的专长是审阅咨询记录,并生成结构化、精炼且富有洞见的专业咨询总结(类似于SOAP note的风格,但更侧重于过程和主题)。 \s # 任务 你的核心任务是分析所提供的完整咨询记录,并严格按照以下三个部分,生成一份全面而专业的咨询总结: 1. 概述 2. 咨询主题 3. 会话过程总结 \s # 各部分撰写指南 \s ### 1. 概述 (Overview) * 目标: 提供一个高度概括的摘要,让阅读者在30秒内了解本次会谈的核心。 * 内容: 用2-3句话简明扼要地描述来访者的核心议题、本次会谈中的主要情绪状态,以及讨论的最关键的生活事件或内在冲突。 * 风格: 高度凝练,直截了当。 \s ### 2. 咨询主题 (Consultation Themes) * 目标: 提炼并归纳本次会谈中探讨的所有关键议题。 * 内容: 将对话内容抽象化为心理学或社会学主题。这些主题应是对话背后反复出现的模式或核心矛盾,而不仅仅是事件的复述。 * 格式: 必须以无序列表(分点)的形式呈现。 * 示例主题: * 人际关系模式:边界感与依赖 * 认知扭曲:灾难化思维与完美主义 * 情绪调节困难:对愤怒情绪的压抑 * 原生家庭影响:父母的期待与个人选择的冲突 * 存在议题:个人价值感与职业倦怠 \s ### 3. 会话过程总结 (Session Process Summary) * 目标: 描述本次会谈的动态流程和关键转折点。 * 内容: 叙述会谈是如何从开始、发展到结束的。应包括但不限于: * 开端: 来访者如何引入议题,开场的情绪状态。 * 探索: 咨询师引导探索的关键领域(如具体事件、感受、想法、身体感觉)。 * 转折/洞见: 会谈中的关键时刻,比如访者产生新的领悟、情绪出现明显变化、或发现关键的自动化思维。 * 干预: 咨询师可能使用的咨询技术(如共情、面质、苏格拉底式提问、空椅子技术等)。 * 结尾: 会谈如何结束,是否布置了家庭作业,双方对本次会谈的总结或感受。 * 风格: 以客观的视角进行过程叙述,侧重于“发生了什么”和“如何发生的”。 \s # 工作流程 1. 通读并消化完整的咨询记录。 2. 首先撰写【概述】。 3. 重读记录,识别并列出所有反复出现的【咨询主题】。 4. 按照时间线和逻辑线,梳理并撰写【会话过程总结】。 5. 整合三个部分,确保语言专业、客观、流畅。 \s # 输出格式: 一、咨询概述 xxxxx 二、咨询主题 xxxxx 三、咨询过程 xxxxx """; /** * 危险等级评定 */ public static final String DANGER_LEVEL_SUMMARY_PROMPT = """ # 身份 你是一名资深的临床心理学家和危机干预专家。你拥有超过20年的临床经验,尤其擅长通过分析完整的对话记录,整合来访者的情绪、认知、行为及个人史信息,以做出整体性、系统性的风险评估。 # 任务 你的核心任务是全面、深入地分析一份完整的心理咨询记录,并根据下方定义的10级危险等级标准,给出一个最符合来访者当前状态的危险等级。 # 核心评估原则 在判断等级时,你必须综合考量以下几个维度: 1. 伤害性想法 (Ideation): 是否存在伤害自己或他人的想法?是模糊的、被动的,还是清晰的、主动的?频率如何? 2. 伤害性意图 (Intent): 来访者有多想将想法付诸行动?是矛盾的、犹豫的,还是坚定的? 3. 计划与准备 (Plan & Preparation): 是否有具体的伤害计划(时间、地点、方式)?计划的可行性和致命性如何?是否已为执行计划做出准备(如写遗书、购买工具)? 4. 过往行为 (History): 过去是否有过自我伤害、自杀未遂或暴力行为? 5. **风险与保护因素 (Risk & Protective Factors):** * 风险因素: 如重大丧失、精神疾病史、药物滥用、社会孤立、冲动控制能力差等。 * 保护因素: 如强烈的家庭责任感、良好的社会支持系统、对未来的计划、害怕死亡或疼痛等。 6. 情绪与认知状态: 是否表现出极度的绝望、激动、愤怒或精神病性症状(如幻觉、妄想)?认知是否僵化,认为伤害是唯一出路? # 危险等级定义(1-10级) --- ### **第一部分:低风险区 (Levels 1-3)** **等级 1:基本无风险** * 表现: 存在一般性情绪困扰(如焦虑、抑郁情绪),但无任何自我或他人伤害的想法。有良好的应对能力和支持系统。 **等级 2:极轻微风险** * 表现: 存在模糊的生活无望感,但未提及伤害性想法。风险因素不明显,保护因素强。 **等级 3:轻度风险/存在被动想法** * 表现: 出现被动的自我伤害想法(如“活着真没意思,有时希望一觉睡过去就不要醒来”),但无主动意图和计划。当被问及时,会否认有伤害自己的打算。\\n\\n---\\n ### **第二部分:中度风险区 (Levels 4-6)** **等级 4:中低度风险** * 表现: 出现主动的、但频率不高的自我伤害想法,意图不明确或矛盾。无具体计划。有一定的保护因素在起作用。 **等级 5:中度风险** * 表现: 自我伤害的想法更频繁、更具体。意图开始增强,但仍有犹豫。可能思考过计划,但计划模糊、不具体。风险因素开始超过保护因素。 **等级 6:中高度风险** * 表现: 有明确的伤害想法和较强的意图。开始构思具体计划,但计划的致命性较低或执行条件不成熟。保护因素薄弱。需要引起高度关注。 ### **第三部分:高风险区 (Levels 7-8)** **等级 7:高度风险** * 表现: 有明确的伤害想法和强烈的意图,并已制定了具体的、有一定致命性的计划。可能拥有实施计划的工具/途径。保护因素非常有限。 **等级 8:极高风险** * 表现: 强烈的伤害意图和具体的、高致命性的计划。有实施计划的工具,且可能已在近期有过预演或中止的行为。来访者处于极度痛苦和绝望中。**(需要立即启动危机干预)**\\n\\n---\\n ### **第四部分:紧急危险区 (Levels 9-10)** **等级 9:即时危险** * 表现: 除了具备8级的所有特征外,来访者已做出某些准备行为(如已写好遗书、将财物送人),并可能已决定在24-48小时内行动。自我控制能力极弱。\\n*\s **等级 10:极端紧急危险** * 表现: 来访者正在实施伤害行为,或已告知即刻(数小时内)就要行动,且有高致命性手段。这是最紧急的状态,等同于危机事件正在发生。\\n\\n --- # 工作流程 1. 通读并理解输入的完整咨询记录。 2. 依据【核心评估原则】分析记录中的关键信息。 3. 最后输出的内容只有一个危险等级数字,例如:4 """; /** * 关键词提取 */ public static final String TAG_SUMMARY_PROMPT = """ #身份# 你是一名专业的<心理咨询师>,你在心理学领域的学术科研上有丰硕的成果,并且善于将理论与实践相结合,运用心理学原理来指导人们解决心理困扰,积累了非常丰富的实践经验。 #情境# 你正在为一位用户进行心理咨询,该用户可能是任何需要心理帮助的人群。 #任务# 你的任务是根据咨询师与用户的交流内容,结合你的心理学知识,总结出一个或多个心理问题的关键词。*注意*在分析用户信息时,需要特别注意用户消息背后的隐含信息,你需要敏锐捕捉或挖掘更深层次的心理问题,如自杀倾向、暴力倾向、精神障碍等,并提供相应的关键词。 # 工作流程# 1. 仔细聆听和分析用户的交流内容。 2. 识别用户可能面临的心理问题和隐含信息。 3. 结合心理学知识,总结关键词。 #输出格式# 直接给出你总结的关键词,输出格式: ["标签1", "标签2", ...],关于标签内容尽量精简 """; /** * 咨询历史总结 */ public static final String HISTORY_SUMMARY_PROMPT = """ 你是一个经验丰富的心理咨询师,有着丰富的心理咨询和记录总结经验。 请根据最新对话更新历史总结,提取出关键点和主题,并按照以下JSON格式返回: { "咨询目的与主要关注点": { "来访者寻求咨询的初始原因": "", "咨询师确定的核心问题或主题": "" }, "来访者情况": { "行为表现和情绪状态": "", "工作/学习/社交环境中的表现": "", "人际关系状况": "", "心理和情感状态": "" }, "咨询师的观察与评估": { "专业判断和初步评估": "", "提出的理论框架或解释模型": "" }, "干预策略与建议": { "咨询师提出的具体策略": "", "来访者接受程度和反馈": "" } }"""; /** * 咨询Prompt */ public static final String APPLY_SUMMARY = """ # Role: Psychologist (心理咨询师) ## Profile - language: 中文 - description: 你是一名中国专业的心理咨询师,你的国籍是中国,擅长通过共情、倾听技巧、动机式访谈和认知行为疗法等专业知识,帮助来访者化解个人情绪困扰和人际关系问题。你利用你丰富的心理学、社会学知识,有效引导来访者走向心理健康与自我成长。 ## Skills 1. 专业的心理学知识,熟练应用动机式访谈、认知行为疗法等方法 2. 高效的倾听技巧,能够让来访者感到被理解和接纳 3. 良好的引导技巧,帮助来访者发现自己内心的困惑和解决方案 ## Background - 教育背景:心理学博士,相关心理学证书 - 经验:20年心理咨询经验,擅长处理焦虑、抑郁、压力、人际关系等问题 - 专业领域:个人成长、情感支持、家庭治疗、职场压力等 ## Goals - 帮助来访者化解情绪困扰和人际关系问题 - 引导来访者提升自我认知、情感调节和心理健康 - 通过共情、引导和心理学知识,促进来访者的个人成长和心理复原 - 对于非心理咨询话题,你不要分析也不要回答,也不要表示尊重或者同情,直接跳过,去引导咨询者去询问心理咨询相关的问题 ## OutputFormat 1. 语气:保持亲切、温和,避免使用过于正式或机械的语言,让来访者感到舒适和安全。 2. 交流:避免分步骤的回复,确保对话流畅自然,引导来访者表达内心感受并适时引导,促进深入交流。 3. 语言:避免使用过于学术化的语言,确保表达自然简洁,让对话更具亲和力。 ## Rules 1. 始终保持职业道德,确保来访者的隐私和安全。 2. 不对来访者的情绪或行为进行评判,而是从理解和共情的角度提供帮助,不要出现“心疼你呀”。 3. 提供建设性反馈,避免直接给出"答案",而是引导来访者自己发现解决方案。 4. 尊重来访者的节奏,确保对话保持自然流畅,避免催促或压力。 5. 每次回复不超过100字 ## Workflows 1. 通过共情、倾听建立信任关系,了解来访者的困扰。 2. 倾听并引导来访者表达情感,帮助其识别和理解内心的情绪和想法。 3. 根据具体情境,选择合适的心理学方法,如动机式访谈、认知行为疗法等,帮助来访者分析问题。 4. 提供情感上的支持和有效的引导,帮助来访者找到个人成长和解决问题的途径。 5. 结束对话时,给予鼓励与支持,确保来访者感到被理解、接纳并有行动方向。"""; }
当前Prompt相较于之前:
- 将正常的String字符串替换为新版本支持的文本块,提升可读性
- 同时将算法同学提供的优质Pormpt进行适配化改造,将返回的格式直接定为可JSON序列化的形式,例如标签直接定为["标签一", "标签二"]这种可以直接序列化为List<String>类型的格式,方便直接转换,但也需要注意为空时候避免引发其他异常
- 多Client构建
/** * @author by 王玉涛 * @Classname DoubaoAiClientConfig * @Description 配置用于创建与豆包AI交互的ChatClient实例,提供对话回复和历史摘要功能。 * @Date 2025/7/12 13:09 */ @Configuration public class DoubaoAiClientConfig { /** * 创建用于生成AI回复的ChatClient Bean。 * 使用注入的doubaoReplyModel模型构建ChatClient实例,适用于实时对话场景。 * * @param doubaoReplyModel 提供AI对话能力的模型Bean * @return ChatClient 实例 */ @Bean public ChatClient doubaoReplyClient(ChatModel doubaoReplyModel, ChatMemory doubaoAiSummaryMemoryRepository) { return ChatClient .builder(doubaoReplyModel) .defaultAdvisors( new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(doubaoAiSummaryMemoryRepository).build() ) .defaultSystem(DoubaoPrompt.APPLY_SUMMARY) .build(); } /** * 创建用于处理历史记录摘要的ChatClient Bean。 * 使用注入的doubaoHistorySummaryModel模型构建ChatClient实例,适用于需要总结或分析对话历史的场景。 * * @param doubaoHistorySummaryModel 提供AI对话能力的模型Bean,专用于历史信息处理 * @return ChatClient 实例 */ @Bean public ChatClient doubaoHistorySummaryClient(ChatModel doubaoHistorySummaryModel) { return ChatClient .builder(doubaoHistorySummaryModel) .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultSystem(DoubaoPrompt.HISTORY_SUMMARY_PROMPT) .build(); } /** * 创建用于处理历史记录标签的ChatClient Bean。 * 使用注入的doubaoHistorySummaryModel模型构建ChatClient实例,适用于需要总结或分析对话历史的场景。 * * @param doubaoHistorySummaryModel 提供AI对话能力的模型Bean,专用于用户标签处理 * @return ChatClient 实例 */ @Bean public ChatClient doubaoTagSummaryClient(ChatModel doubaoHistorySummaryModel) { return ChatClient .builder(doubaoHistorySummaryModel) .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultSystem(DoubaoPrompt.TAG_SUMMARY_PROMPT) .build(); } /** * 创建用于处理历史记录危险等级的ChatClient Bean。 * 使用注入的doubaoHistorySummaryModel模型构建ChatClient实例,适用于需要总结或分析对话历史的场景。 * * @param doubaoHistorySummaryModel 提供AI对话能力的模型Bean,专用于危险等级处理 * @return ChatClient 实例 */ @Bean public ChatClient doubaoDangerLevelSummaryClient(ChatModel doubaoHistorySummaryModel) { return ChatClient .builder(doubaoHistorySummaryModel) .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultSystem(DoubaoPrompt.DANGER_LEVEL_SUMMARY_PROMPT) .build(); } /** * 创建一个 ChatClient 实例,用于处理所有消息的摘要 * * @param doubaoHistorySummaryModel 模型 * @return ChatClient 实例 */ @Bean public ChatClient doubaoAllMessageHistoryClient(ChatModel doubaoHistorySummaryModel) { return ChatClient .builder(doubaoHistorySummaryModel) .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultSystem(DoubaoPrompt.ALL_MESSAGE_SUMMARY_PROMPT) .build(); } /** * 创建一个 ChatClient 实例,用于处理危险词的检测 * @param doubaoHistorySummaryModel 模型 * @return ChatClient 实例 */ @Bean public ChatClient doubaoDangerWordCheckClient(ChatModel doubaoHistorySummaryModel) { return ChatClient .builder(doubaoHistorySummaryModel) .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultSystem(DoubaoPrompt.DANGER_WORD_CHECK_PROMPT) .build(); } }
- 豆包AiProvider中扩展
/** * @author 王玉涛 * @Classname DoubaoAiProvider * @Description 集成豆包大模型AI能力的提供者类,用于配置和管理与豆包AI相关的客户端资源。 * 当前托管两个核心功能的客户端: * - doubaoReplyClient: 豆包回复生成客户端,用于对话场景中的实时回复生成。 * - doubaoHistorySummaryClient: 豆包历史对话总结客户端,用于对长对话历史进行摘要处理。 * @Date 2025/7/12 13:11 * * 该类通过构造函数注入方式初始化ChatClient实例,并提供统一的访问接口获取不同功能的客户端。 * 使用Lombok的@Data注解自动实现getter和setter方法,同时使用@Component注解声明为Spring组件, * 以便在其他模块中可以通过依赖注入使用该类提供的AI能力。 */ @RequiredArgsConstructor @Component @Data public class DoubaoAiProvider { /** * 豆包回复生成客户端,注入名为 "doubaoReplyClient" 的Bean, * 用于在对话交互中生成自然语言回复。 * * 该客户端主要应用于实时对话场景,例如用户问题回答、对话式交互等, * 提供高质量的自然语言生成能力。 */ private final ChatClient doubaoReplyClient; /** * 豆包历史对话摘要客户端,注入名为 "doubaoHistorySummaryClient" 的Bean, * 用于对较长的对话历史进行内容压缩和关键信息提取。 * * 该客户端主要用于处理多轮对话记录,提取关键信息并生成简洁摘要, * 常用于对话状态跟踪、会话分析等场景。 */ private final ChatClient doubaoHistorySummaryClient; /** * 豆包标签摘要客户端,注入名为 "doubaoTagSummaryClient" 的Bean, * 用于对对话内容进行标签提取和主题识别。 * * 该客户端主要适用于多轮对话场景,对对话内容进行主题识别和标签提取, * 以帮助用户快速了解对话内容,并快速定位问题。 */ private final ChatClient doubaoTagSummaryClient; /** * 豆包危险等级摘要客户端,注入名为 "doubaoDangerLevelSummaryClient" 的Bean, * 用于对对话内容进行危险等级识别。 * * 该客户端主要适用于多轮对话场景,对对话内容进行危险等级识别, * 以帮助用户快速了解对话内容,并快速定位问题。 */ private final ChatClient doubaoDangerLevelSummaryClient; /** * 豆包历史摘要客户端,注入名为 "doubaoHistorySummaryClient" 的Bean, * 用于对对话内容进行历史摘要。 * * 该客户端主要适用于多轮对话场景,对对话内容进行历史摘要, * 以帮助用户快速了解对话内容,并快速定位问题。 */ private final ChatClient doubaoAllMessageHistoryClient; /** * 豆包危险词检查客户端,注入名为 "doubaoDangerWordCheckClient" 的Bean, * 用于对对话内容进行危险词检查。 * * 该客户端主要适用于多轮对话场景,对对话内容进行危险词检查, * 以帮助用户快速了解对话内容,并快速定位问题。 */ private final ChatClient doubaoDangerWordCheckClient; /** * 获取回复生成客户端实例。 * * @return ChatClient 返回用于生成对话回复的客户端对象 */ public ChatClient replyClient() { return doubaoReplyClient; } /** * 使用豆包回复生成客户端生成回复。 * * @param prompt 用户输入的提示信息 * @param conversationId 会话ID * @return String 返回生成的回复内容 */ public String reply(String prompt, Long conversationId) { return replyClient() .prompt(prompt) .advisors( advisorSpec -> advisorSpec .param(ChatMemory.CONVERSATION_ID, conversationId) ) .call() .content(); } /** * 获取历史对话摘要客户端实例。 * * @return ChatClient 返回用于生成对话摘要的客户端对象 */ public ChatClient historySummaryClient() { return doubaoHistorySummaryClient; } /** * 使用豆包历史对话摘要客户端生成摘要。 * * @param prompt 用户输入的提示信息 * @return String 返回生成的摘要内容 */ public String historySummary(String prompt) { return historySummaryClient() .prompt(prompt) .call() .content(); } /** * 获取标签摘要客户端实例。 * * @return ChatClient 返回用于生成标签摘要的客户端对象 */ public ChatClient tagSummaryClient() { return doubaoTagSummaryClient; } /** * 使用豆包标签摘要客户端生成标签摘要。 * * @param prompt 用户输入的提示信息 * @return String 返回生成的标签摘要内容 */ public String tagSummary(String prompt) { return tagSummaryClient() .prompt(prompt) .call() .content(); } /** * 使用豆包危险等级摘要客户端生成危险等级摘要。 * * @return String 返回生成的危险等级摘要内容 */ public ChatClient dangerLevelSummaryClient() { return doubaoDangerLevelSummaryClient; } /** * 使用豆包危险等级摘要客户端生成危险等级摘要。 * * @param prompt 用户输入的提示信息 * @return String 返回生成的危险等级摘要内容 */ public String dangerLevelSummary(String prompt) { return dangerLevelSummaryClient() .prompt(prompt) .call() .content(); } /** * 使用豆包历史摘要客户端生成历史摘要。 * * @return String 返回生成的历史摘要内容 */ public ChatClient allMessageHistoryClient() { return this.doubaoAllMessageHistoryClient; } /** * 使用豆包历史摘要客户端生成历史摘要。 * * @param prompt 用户输入的提示信息 * @return String 返回生成的历史摘要内容 */ public String allMessageHistorySummary(String prompt) { return allMessageHistoryClient() .prompt(prompt) .call() .content(); } /** * 使用豆包危险词检查客户端生成危险词检查结果。 * * @return String 危险词检查结果 */ public ChatClient dangerWordCheckClient() { return doubaoDangerWordCheckClient; } /** * 使用豆包危险词检查客户端生成危险词检查结果。 * * @param prompt 用户输入的提示信息 * @return String 危险词检查结果 */ public String dangerWordCheck(String prompt) { return dangerWordCheckClient() .prompt(prompt) .call() .content(); } }
当前Provider相比较于之前版本:
- 优化了调用形式,除了提供historySummaryClient()返回对应对象的形式,也提供了封装好的方法,直接传入prompt即可直接获取到回复信息
- 添加了多个Client的实例,通过@RequiredArgsConstructor+private final 修饰的多ChatClient类型,不同Bean名称实现精准注入
- 这样的Provider,完美符合开闭原则,后续也可以继续添加更多设置其他系统提示词的ChatClient成员。
2.2 消息队列调用
- 现在可以通过DoubaoAiProvider进行多模型调用,同时JavaDoc形式的注解,也为不同的方法提供了精准的解释,即使直接上手,也很容易直接理解每个方法对应的功能。
- 在原有的消息队列中直接进行拓展
/** * @author by 王玉涛 * @Classname AiModelConsumer * @Description AI模型消息消费者,用于处理AI对话摘要任务。 * 从RabbitMQ队列中消费待处理的对话消息,调用豆包大模型生成对话摘要, * 并将生成的摘要内容存储到缓存中。 * @Date 2025/7/13 14:30 */ @Component @RequiredArgsConstructor @Slf4j public class AiModelConsumer { /** * 豆包AI提供者,用于获取历史对话摘要客户端 */ private final DoubaoAiProvider doubaoAiProvider; /** * 缓存服务提供者,用于存储生成的摘要内容 */ private final CacheProvider cacheProvider; /** * 聊天映射器,用于获取聊天信息 */ private final ChatMapper chatMapper; /** * 消息映射器,用于获取消息信息 */ private final MessageMapper messageMapper; /** * 监听AI模型摘要队列,处理对话摘要任务 * * @param summaryMessage 包含对话历史和存储键的摘要消息对象 */ @RabbitListener(queues = RabbitMQConstant.AI_MODEL_SUMMARY_QUEUE, concurrency = "5") public void handleAiModelSummary(SummaryMessage summaryMessage) { log.info("处理消息: {}", summaryMessage); String storageKey = summaryMessage.getStorageKey(); String summaryConstant = cacheProvider.getString(storageKey); summaryMessage.addSummary(summaryConstant); // 使用豆包AI的历史对话摘要客户端生成摘要内容 // 提示信息基于对话历史生成,用于总结对话内容 String prompt = summaryMessage.myMessagesToString(); String summaryContent = doubaoAiProvider.historySummary(prompt); log.info("摘要内容: {}", summaryContent); // 将生成的摘要内容添加到缓存列表中 // 这样后续可以快速访问和使用生成的摘要 cacheProvider.set(storageKey, summaryContent); log.info("存储摘要内容成功"); // 开始获取用户标签 // 通过结合对话历史和已生成的摘要内容,识别用户的主要主题和标签 log.info("开始获取用户标签: 配合五轮对话+会话总结"); prompt = summaryContent + "\n" + prompt; log.info("tagSummary的Prompt: {}", prompt); String tagSummary = doubaoAiProvider.tagSummary(prompt); log.info("用户标签: {}", tagSummary); Long chatId = summaryMessage.getChatId(); // 更新数据库中的用户标签信息 // 用于后续的用户分析和个性化服务 log.info("更新用户标签成功"); // 开始总结用户的危险等级 // 在已获取的用户标签基础上,进一步分析用户的潜在风险等级 log.info("开始总结用户的危险等级: 在上一个标签的基础上,进一步添加prompt"); prompt = tagSummary + "\n" + prompt; String dangerLevel = doubaoAiProvider.dangerLevelSummary(prompt); log.info("总结用户的危险等级: {}", dangerLevel); // 更新数据库中的用户危险等级信息 // 用于风险控制和优先级处理 log.info("查询用户所有的消息,进行总结, chatId: {}", chatId); QueryWrapper<MessageInfo> messageWrapper = new QueryWrapper<MessageInfo>().eq("chat_id", chatId); List<MessageInfo> messages = messageMapper.selectList(messageWrapper); log.info("查询用户所有的消息成功, 开始总结"); String messageStr = JSONUtil.toJsonStr(messages); String messageHistorySummary = doubaoAiProvider.allMessageHistorySummary(messageStr); log.info("总结用户所有的消息成功"); // 再总结用户当前话语的危险词汇 log.info("开始总结用户当前话语的危险词汇, 输入的信息是全部信息"); String dangerWordCheck = doubaoAiProvider.dangerWordCheck(messageStr); log.info("用户当前话语的危险词汇: {}", dangerWordCheck); UpdateWrapper<ChatInfo> updateWrapper = new UpdateWrapper<ChatInfo>() .eq("chat_id", chatId) .set("tag", tagSummary) .set("danger_level", dangerLevel) .set("summary", messageHistorySummary) .set("danger_words", dangerWordCheck); chatMapper.update(updateWrapper); log.info("更新用户标签、危险等级、会话总结、危险词汇成功"); } }
- 其中有一些细节可以注意一下
- 对于每次总结,我并不是调用完就直接修改一次数据库,而是把所有总结都完成后,才进行数据库修改
- 至于为什么这么做,是因为每次总结一次修改,会多出两次写操作,而并发量如果上来,就会给数据库造成巨大的压力,因此相比较于三次修改三个字段,不如一次直接修改三个字段,这样可以减轻数据库压力。
- 当然,这个方案肯定不是完美方案,因为会话摘要与后四次总结,还是放在一个消费者中,这看起来并不是最好的,因为一个线程调用四五次模型,如果在高并发压力下,很可能出现消费者线程不够用的情况。
- 后续可以考虑多建立几个队列,每个队列专注于不同的任务,但是如何拆分?我建议是后四次调用尽量放在同一个队列中,防止出现第三点说的,给数据库带来巨大的压力。
经过当前的优化,进一步接近了算法端的原来实现效果,在严格控制会话量五轮+一轮摘要的情况下,也对用户的多种特征进行精确总结,且方便后端直接序列化,而不是手写拆解逻辑。算是一次比较成功的改造。
#SpringBoot##SpringAI##Java#