(V3)SpringAI——会话记忆对齐+多AI总结

前言

SpringAI框架初步集成:SpringAI的初步用法

SpringAI框架会话记忆优化:SpringAI——会话记忆优化版本

虽然之前已经集成SpringAI框架,实现基于Redis的会话记忆,并实现五轮+摘要的优化。但是和算法端同学对接后,发现有一些流程需要进一步优化,因为原来记忆优化的版本,是每五轮进行一次总结,并且删除原始五轮会话,进行一次总结。

这样的效果就是:

  1. 第五轮时候开始优化,原始五轮会话记忆丢失,并生成第一轮摘要
  2. 第八轮时候,只有最近三轮原始对话记忆,以及原来五轮进行总结生成的一轮摘要
  3. 第十轮时候,再次优化,将最近五轮原始会话记忆删除,并再次生成一轮摘要
  4. 第十一轮,情况就成为了:只有一轮原始会话+两轮会话摘要。

那么问题就出现了:

  1. 这样的会话记忆压缩形式,本质是减缓会话膨胀,但是随着会话轮数的增加,摘要轮数依旧会有增加。
  2. 会话记忆总结期间,通过消息队列解耦会话总结,再删除会话记忆,这样就会导致有一小段时间的记忆断层。
  3. 同时所有的摘要仅仅针对当前五轮会话,也就是说,即使是摘要,也无法进行有效的总结。
  4. 同时这并不满足算法端之前提出的五轮会话+一轮摘要的情况。

与算法端进一步对齐信息差后,解决方案也显而易见:

  1. 五轮会话,指的是最近的五轮原始会话,而不是每五轮一次优化。
  2. 一轮摘要,指的是携带原来的摘要,以及这五轮会话,进行进一步的摘要总结,而不是删除前五轮会话
  3. 本质上来说,这种形式类似于滑动窗口,第五轮后,每一轮淘汰一轮原始会话,同时进行摘要总结。
  4. 同时不仅仅是摘要总结,对于数据看报,也需要对用户的其他特征进行总结,比如标签、所有会话流程摘要、危险等级、危险词。

那么接下来就是对于流程的优化

1. 优化记忆形式

  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#
全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务