(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#
查看12道真题和解析