天机学堂AI助手智能体 - 多智能体协同工作

参考视频:黑马程序员SpringAI+DeepSeek大模型应用开发实战视频教程

1. 历史对话

历史对话功能是可以让用户查询之前的对话记录,最多查询30条,并且按照当天、最近30天、最近1年、1年以上分类,还支持更新标题、删除功能,如下:

img

1.1. 异步设置标题

1.1.1. 功能分析

在新建会话操作后,会话数据会记录到mysql数据库,但是,是没有标题数据的:

img

这是因为,在新建会话时,是没有标题数据的,那什么时候有标题数据呢?就是用户再发第一次消息时,就有了标题数据。

也就是,用户第一次发的问题,就是标题。

另外,更新时间字段,是每次用户提问时都要更新的。

1.1.2. 代码实现

ChatSessionService中新增方法:

java
    /**
     * 更新会话更新时间
     *
     * @param sessionId 会话ID,用于标识特定的聊天会话
     * @param title     新的会话标题,如果为空则不进行更新
     * @param userId    用户ID
     */
    void update(String sessionId, String title, Long userId);

编写接口实现类:(注意,这里采用了异步更新,主要是确保AI对话聊天时的用户体验)

java
    /**
     * 异步更新聊天会话的标题
     *
     * @param sessionId 会话ID,用于标识特定的聊天会话
     * @param title     新的会话标题,如果为空则不进行更新
     * @param userId    用户ID
     */
    @Async
    @Override
    public void update(String sessionId, String title, Long userId) {
        // 查询符合条件的聊天会话列表
        List<ChatSession> list = super.lambdaQuery()
                .eq(ChatSession::getSessionId, sessionId)
                .eq(ChatSession::getUserId, userId)
                .list();
        // 如果列表为空,直接返回,无需进一步处理
        if (CollUtil.isEmpty(list)) {
            return;
        }

        // 获取列表中的第一个聊天会话实例
        ChatSession chatSession = list.get(0);
        // 如果聊天会话的标题为空,并且新标题不为空,则更新标题
        if (StrUtil.isEmpty(chatSession.getTitle()) && !StrUtil.isEmpty(title)) {
            chatSession.setTitle(StrUtil.sub(title, 0, 100));
        }
        // 设置更新字段为updateTime为当前时间
        chatSession.setUpdateTime(LocalDateTimeUtil.now());
        // 更新数据库中的聊天会话信息
        super.updateById(chatSession);
    }
java
    private final ChatSessionService chatSessionService;

    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        // 获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        // 大模型输出内容的缓存器,用于在输出中断后的数据存储
        var outputBuilder = new StringBuilder();
        // 生成请求id
        var requestId = IdUtil.fastSimpleUUID();
        // 获取用户id
        var userId = UserContext.getUser();

        // 异步更新会话信息
        this.chatSessionService.update(sessionId, question, userId);
        
        // 创建RAG增强
        var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
                .searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
                .build();

        return this.chatClient.prompt()
                .system(promptSystem -> promptSystem
                        .text(this.systemPromptConfig.getChatSystemMessage().get()) // 设置系统提示语
                        .param("now", DateUtil.now()) // 设置当前时间的参数
                )
                // 设置RAG增强
                .advisors(qaAdvisor)
                .advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
                .toolContext(Map.of(Constant.REQUEST_ID, requestId, Constant.USER_ID, userId)) //通过工具上下文传递参数
                .user(question)
                .stream()
                .chatResponse()
                .doFirst(() -> GENERATE_STATUS.put(sessionId, true)) // 第一次输出内容时执行
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 出现异常时,删除标识
                .doOnComplete(() -> GENERATE_STATUS.remove(sessionId)) // 完成时执行,删除标识
                .doOnCancel(() -> {
                    // 当输出被取消时,保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                .takeWhile(response -> { // 通过返回值来控制Flux流是否继续,true:继续,false:终止
                    return GENERATE_STATUS.getOrDefault(sessionId, false);
                })
                .map(chatResponse -> {
                    var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
                    if (StrUtil.equals(Constant.STOP, finishReason)) {
                        var messageId = chatResponse.getMetadata().getId();
                        ToolResultHolder.put(messageId, Constant.REQUEST_ID, requestId);
                    }

                    // 获取大模型的输出的内容
                    var text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    // 封装响应对象
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                })
                .concatWith(Flux.defer(() -> {
                    // 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中
                    var map = ToolResultHolder.get(requestId);
                    if (CollUtil.isNotEmpty(map)) {
                        ToolResultHolder.remove(requestId); // 清除参数列表

                        // 响应给前端的参数数据
                        var chatEventVO = ChatEventVO.builder()
                                .eventData(map)
                                .eventType(ChatEventTypeEnum.PARAM.getValue())
                                .build();
                        return Flux.just(chatEventVO, STOP_EVENT);
                    }
                    return Flux.just(STOP_EVENT);
                }));
    }

1.1.3. 测试

新建会话是没有数据的:

img

进行聊天:

img

可以看到,数据写入了:

img

1.2. 查询历史会话

1.2.1. 需求分析

img

页面效果如下:

img

1.2.2. 接口文档

img

响应结构:

json
{
    "code": 200,
    "msg": "OK",
    "data": {
        "1年以上": [
            {
                "sessionId": "03b6491d3a1949c98cf0f8c37aa623fc",
                "title": "水水水水谁谁谁水水水水谁谁谁水水水水水水水水",
                "updateTime": "2023-02-26 15:45:31"
            }
        ],
        "最近1年": [
            {
                "sessionId": "53349594acff4a0fb92f71541491dc1b",
                "title": "帮我推荐课程",
                "updateTime": "2025-01-18 21:33:55"
            },
            {
                "sessionId": "695fdea704254c089da454133a1c17a8",
                "title": "你是谁",
                "updateTime": "2025-01-18 21:33:37"
            }
        ],
        "最近30天": [
            {
                "sessionId": "e380350f97174313898c214afb37d6d8",
                "title": "22222",
                "updateTime": "2025-02-25 13:44:44"
            }
        ],
        "当天": [
            {
                "sessionId": "fa046bdb4ffe48fba4915e490e1e0b0e",
                "title": "xxxxxx",
                "updateTime": "2025-02-26 15:44:01"
            }
        ]
    },
    "requestId": "bc8d535241104da7802e5d27f229d219"
}

1.2.3. 代码实现

1.2.3.1. 定义VO对象
java
package com.tianji.aigc.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatSessionVO {

    /**
     * 会话id
     */
    private String sessionId;

    /**
     * 会话标题
     */
    private String title;

    private LocalDateTime updateTime;

}
1.2.3.2. Controller
java
    /**
     * 查询历史会话列表
     */
    @GetMapping("/history")
    public Map<String, List<ChatSessionVO>> queryHistorySession() {
        return this.chatSessionService.queryHistorySession();
    }
1.2.3.3. Service
java
    /**
     * 查询历史会话列表
     */
    Map<String, List<ChatSessionVO>> queryHistorySession();
1.2.3.4. Impl
java
    @Override
    public Map<String, List<ChatSessionVO>> queryHistorySession() {
        var userId = UserContext.getUser();
        // 查询历史会话,限制返回条数
        var list = super.lambdaQuery()
                .eq(ChatSession::getUserId, UserContext.getUser())
                .isNotNull(ChatSession::getTitle)
                .orderByDesc(ChatSession::getUpdateTime)
                .last("LIMIT 30")
                .list();

        if (CollUtil.isEmpty(list)) {
            log.info("No chat sessions found for user: {}", userId);
            return Map.of();
        }


        // 转换为 ChatSessionVO 列表
        var chatSessionVOS = CollStreamUtil.toList(list, chatSession ->
                ChatSessionVO.builder()
                        .sessionId(chatSession.getSessionId())
                        .title(chatSession.getTitle())
                        .updateTime(chatSession.getUpdateTime())
                        .build()
        );

        final var TODAY = "当天";
        final var LAST_30_DAYS = "最近30天";
        final var LAST_YEAR = "最近1年";
        final var MORE_THAN_YEAR = "1年以上";

        // 当前时间
        var now = LocalDateTime.now().toLocalDate();

        // 按照更新时间分组
        return CollStreamUtil.groupByKey(chatSessionVOS, vo -> {
            // 计算两个日期之间的天数差
            long between = Math.abs(ChronoUnit.DAYS.between(vo.getUpdateTime().toLocalDate(), now));
            if (between == 0) {
                return TODAY;
            } else if (between <= 30) {
                return LAST_30_DAYS;
            } else if (between <= 365) {
                return LAST_YEAR;
            } else {
                return MORE_THAN_YEAR;
            }
        });
    }

1.2.4. 测试

img
img

点击历史对话,可以显示对话详情:

img

1.3. 删除历史会话

1.3.1. 需求分析

img

对于历史对话的删除,实现物理删除即可,但是要注意,Redis中的对话数据也要相应的删除。

1.3.2. 接口文档

请求参数只有一个sessionId

img

1.3.3. 代码实现

1.3.3.1. Controller
java
    /**
     * 删除历史会话列表
     */
    @DeleteMapping("/history")
    public void deleteHistorySession(@RequestParam("sessionId") String sessionId) {
        this.chatSessionService.deleteHistorySession(sessionId);
    }
1.3.3.2. Service
java
    /**
     * 删除历史会话
     *
     * @param sessionId 会话id
     */
    void deleteHistorySession(String sessionId);
1.3.3.3. Impl
java
    @Override
    public void deleteHistorySession(String sessionId) {
        //删除数据库的数据
        var queryWrapper = Wrappers.<ChatSession>lambdaQuery()
                .eq(ChatSession::getSessionId, sessionId)
                .eq(ChatSession::getUserId, UserContext.getUser());
        super.remove(queryWrapper);

        //删除redis中的数据
        var conversationId = ChatService.getConversationId(sessionId);
        this.chatMemory.clear(conversationId);
    }

1.3.4. 测试

img
img

删除成功:

img

1.4. 练习:更新历史会话标题

1.4.1. 需求分析

历史会话的标题,默认是用户第一次发出的问题,后面用户可以修改标题,如下:

点击编辑按钮:

img

可以输入新的标题内容:

img

点击对钩保存数据:

img

1.4.2. 接口文档

参数有:sessionId 和 title 。

img

1.4.3. 代码实现

1.4.3.1. Controller
java
    /**
     * 更新历史会话标题
     */
    @PutMapping("/history")
    public void updateTitle(@RequestParam("sessionId") String sessionId,
                            @RequestParam("title") String title) {
        this.chatSessionService.updateTitle(sessionId, title);
    }
1.4.3.2. Service
java
    /**
     * 更新历史会话标题
     *
     * @param sessionId 会话id
     * @param title     标题
     */
    void updateTitle(String sessionId, String title);
1.4.3.3. Impl
java
    @Override
    public void updateTitle(String sessionId, String title) {
        //更新数据
        super.lambdaUpdate()
                // 设置更新条件, 更新字段为title(最多设置前100个字符),更新条件为sessionId和userId
                .set(ChatSession::getTitle, StrUtil.sub(title, 0, 100))
                .eq(ChatSession::getSessionId, sessionId)
                .eq(ChatSession::getUserId, UserContext.getUser())
                .update();
    }

1.4.4. 测试

准备修改:

img

修改成功:

img

2. 智能体架构模型

前面我们已经完成了天机AI助手智能体功能的开发,实际上我们实现的方式只是最为基础的一种模式,一般应用系统中的智能体架构有6种,分别是:

  • 增强型智能体
  • 链式工作流智能体
  • 路由工作流智能体
  • 并行工作流智能体
  • 协调器工作流智能体
  • 评估优化工作流智能体

下面,我们一起来了解下这6种架构模式,重点要关注:路由工作流智能体

2.1. 增强型智能体

增强型智能体的基本构建块是一个增强的LLM,其中包含检索、工具和记忆等增强功能。

来自:https://www.anthropic.com/engineering/building-effective-agents

📚 模式说明:

  • 输入输出系统:整个流程是线性的,就是从输入到大模型(LLM),再到输出。简单来说,就是 Input → LLM → Output。
  • 大模型(LLM):它就像个中央处理器,负责协调三个主要的功能模块——检索(Retrieval)、工具(Tools)和记忆(Memory),一起来完成信息处理。
  • 这种模型,适用于业务不是很复杂的场景。

之前我们实现的天机AI智能体就是采用这种模式。\text{\color{red}{之前我们实现的天机AI智能体就是采用这种模式。}}

2.2. 链式工作流智能体

工作流智能体模式,就是将任务分解为一系列步骤,其中每个LLM调用处理上一个步骤的输出,通过多步骤LLM调用分阶段处理复杂任务。

来自:https://www.anthropic.com/engineering/building-effective-agents

📚 模式说明:

  • 顺序分解:将任务拆解为 LLM Call 1LLM Call 2LLM Call 3 的固定步骤链。
  • 中间检查(Gate):在第一步输出后插入 Gate 决策点,实现中间验证阻断错误传播。
  • 上下文传递:Output 1 作为 LLM Call 2 的输入,Output 2 作为 LLM Call 3 的输入,通过链式传递上下文保持任务连贯性。
  • 这种模式适用于任务比较复杂,但处理流程固定的场景
  • 比如:写文章 → 生成大纲 → 校验大纲 → 依据大纲编写内容 → 对内容校验 → 最后输出。

2.3. 路由工作流智能体

路由工作流智能体,这种模式是将输入通过 LLM Call Router 对意图识别,再交由下游的 LLM 执行。

来自:https://www.anthropic.com/engineering/building-effective-agents

📚 模式说明:

  • 动态路径选择:Router 节点根据输入特征(如内容类型、用户意图)决定调用哪个LLM。
  • 集中式决策:所有输入需先经过Router(而非直接调用LLM),使用路由控制器降低业务系统之间的耦合度。
  • 可扩展性:图中预设3个LLM调用路径,实际上,可扩展更多分支,也就是说,路由模式可以灵活的增减处理模块,比较灵活。
  • 这种模式适用于复杂业务,并且后续的处理逻辑比较独立的场景。
  • 比如:天机AI助理,推荐课程、查询课程、购买课程,这都是独立的业务,可以用独立的智能体实现。

2.4. 并行工作流智能体

并行工作智能体,是值一个输入同时交给多个LLM去执行,再将这些大模型的输出进行汇总处理,再输出。

来自:https://www.anthropic.com/engineering/building-effective-agents

📚 模式说明:

  • 任务并行拆分:输入(In)同时分发给 LLM Call 1/2/3 并行处理,通过并发执行独立子任务提升效率。
  • 结果聚合策略:通过 Aggregator 模块整合多个LLM输出,用冗余计算换取结果可靠性。
  • 模块化隔离:各LLM节点无直接依赖关系(仅通过Aggregator连接),降低单点故障风险的容错设计。
  • 这种模式,一般会在两种场景中使用:
    • 将一个任务,拆分成多个子任务,并行执行,提升效率。

      • 例如:需要开发一个多维度内容审核系统,包括:是否含攻击性言论、关键数据是否准确、是否引用未授权内容等检测,这些检测可以并行执行,提升系统效率。
    • 将同一个任务,由不同的大模型执行多次,得到不同的输出,再聚合处理,以得到更准确的结果。

      • 例如:如医疗诊断辅助、金融风险评估

2.5. 协调器工作流智能体

协调器工作流智能体,这种模式是,由Orchestrator LLM作为智能调度中心,动态生成子任务列表,子任务可以是并行或串行执行,结果由Synthesizer进行聚合输出。

img

📚 模式说明:

  • 动态任务分解:Orchestrator 担任智能调度的核心角色,能够利用大型语言模型(LLM)动态地生成子任务列表。
  • 异构模型协同工作:通过并行或串行方式调用多个大型语言模型,并依据各个子任务的具体需求选择最适合的模型执行,以此实现高效的资源分配策略。
  • 结果智能化整合:Synthesizer 采用语义融合技术,确保来自不同模型的结果在最终输出时达到高度的一致性和连贯性。
  • 这种工作流程适合处理复杂且细节不确定的任务(如编程时根据实际情况修改文件)。它看起来像并行处理,但更灵活:任务不是预先定义好的,而是由协调器根据进展动态分配和调整。
  • img

2.6. 评估优化工作流智能体

评估优化工作流智能体,是这一种 生成 → 评估 → 反馈 循环反馈的机制。

img

📚 模式说明:

  • 生成阶段(Generator):产生初始解决方案(Solution)

  • 评估阶段(Evaluator):验证输出质量,可以选择Accepted(认可)或 Rejected+Feedback(拒绝+反馈)

  • 决策阶段:根据评估结果选择终结输出或重新生成

  • 举例:img

  • 场景

    • 在文学翻译领域,译者型语言模型(LLM)可能在初次尝试时未能完全把握文本中的细微差异,但评估型语言模型(LLM)能够对此提供具有建设性的反馈。
    • 在执行复杂的搜索任务时,往往需要通过多轮的搜索与分析来确保信息收集的全面性。在此过程中,评估者将根据已获取的信息质量及完整性,判断是否有必要展开进一步的搜索活动。

2.7. 总结

模式名称控制方式延迟水平可靠性典型应用场景开发复杂度
增强型智能体直接输出最低简单问答、内容润色简单
链式工作流智能体线性顺序执行中等中高分阶段任务(如大纲→内容→格式优化)中等
路由工作流智能体条件分支选择低-中等多领域处理(如客服分流转人工)中等
并行工作流智能体多模型并发执行中等可靠性敏感任务(如医疗诊断辅助)较高
协调器工作流智能体动态任务分解+调度最高复杂业务(如商业智能分析系统)极高
评估优化工作流智能体迭代优化+反馈修正最高极高质量敏感场景(如法律文件生成)

模式选型建议,根据业务需求选择:

  • 简单任务 → 增强型智能体 / 链式工作流智能体
  • 多分支处理 → 路由模式
  • 高实时性 → 并行化(需任务可拆分)
  • 超复杂任务 → 协调器工作流智能体
  • 超高可靠性 → 评估优化工作流智能体

3. 路由工作流智能体

根据前面的分析,我们的天机AI助理,比较适合用路由工作流模式,接下来,我们将把之前的增强型智能体,改造成路由工作流模式。

3.1. 实现流程

img

📚 流程说明:

  • 我们把原来的单一智能体改成了5个智能体一起协同工作。
  • 当用户提出问题时,首先会发送给【意图分析智能体】,它会判断用户是想让我们推荐课程、查询课程信息还是购买课程。
  • 一旦明确了用户的意图,就会根据不同的需求调用相应的智能体来完成任务,比如推荐课程或购买课程等。
  • 这样做的好处是每个智能体都有明确的任务分工,并且只有在需要时才会调用特定的工具,不需要所有智能体都配备全套工具。这样一来,整个系统变得更加灵活高效了。

3.2. 实现分析

我们已经知道,接下来要做的事情就是要将单一的智能体,改造成5个智能体协同工作,每个智能体必然会有一些部分代码是重复的,所以需要定义个interface Agent,用来定义Agent的标准方法,并且也需要提供一个抽象类实现,将通用的业务实现写到这个抽象类中。

3.2.1. 定义类型枚举

不同的智能体,是需要通过类型来区分的,比较好的一种方式就是定义类型枚举。

java
package com.tianji.aigc.enums;

import cn.hutool.core.util.EnumUtil;
import lombok.Getter;

/**
 * 智能体类型
 */
@Getter
public enum AgentTypeEnum {
    ROUTE("ROUTE", "路由智能体"),
    RECOMMEND("RECOMMEND", "课程推荐智能体"),
    CONSULT("CONSULT", "课程咨询智能体"),
    BUY("BUY", "课程购买智能体"),
    KNOWLEDGE("KNOWLEDGE", "知识讲解智能体");

    private final String agentName;
    private final String desc;

    AgentTypeEnum(String agentName, String desc) {
        this.agentName = agentName;
        this.desc = desc;
    }

    @Override
    public String toString() {
        return this.name();
    }


    /**
     * 通过智能体的名称查找枚举
     */
    public static AgentTypeEnum agentNameOf(String agentName) {
        return EnumUtil.getBy(AgentTypeEnum::getAgentName, agentName);
    }

}

3.2.2. 定义Agent接口

我们可以想一下,每个智能体都有什么相关的方法,就把他们抽象出来,形成一个Agent interface,子类只需要实现接口即可。

应该有的方法:

  • process (普通对话)
  • processStream (流式对话)
  • getAgentType (获取智能体类型)
  • stop (停止方法)
  • systemMessage (获取系统提示词方法)

以上这些都是基本的操作方法。实际上,对于一个智能体而言,与大模型或Tools交互,还需要一些设定,比如toolContext、advisors等,所以还需要额外的加一个方法:

  • tools (工具集)
  • toolContext (工具上下文参数)
  • advisors (Advisor列表)
  • advisorParams (Advisor参数列表)
  • systemMessageParams (系统提示词中的参数列表)

所以,基于上面的分析,就可以定义Agent interface了:

java
package com.tianji.aigc.agent;

import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.vo.ChatEventVO;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import reactor.core.publisher.Flux;

import java.util.List;
import java.util.Map;

/**
 * AI代理接口,定义处理聊天事件和会话的核心能力
 */
public interface Agent {

    /**
     * 表示空参数的预定义数组
     */
    Object[] EMPTY_OBJECTS = new Object[0];

    /**
     * 处理流式请求(如流式回答)
     *
     * @param question  用户输入的问题
     * @param sessionId 会话唯一标识
     * @return 包含中间结果的反应式事件流(Flux)
     */
    Flux<ChatEventVO> processStream(String question, String sessionId);

    /**
     * 处理标准请求(非流式)
     *
     * @param question  用户输入的问题
     * @param sessionId 会话唯一标识
     * @return 最终处理结果字符串
     */
    String process(String question, String sessionId);

    /**
     * 获取智能体类型标识
     *
     * @return 代理类型枚举值(如:ROUTE、RECOMMEND等)
     */
    AgentTypeEnum getAgentType();

    /**
     * 停止指定会话的处理
     *
     * @param sessionId 需要终止的会话ID
     */
    void stop(String sessionId);

    /**
     * 获取系统提示信息模板,默认为空字符串,子类可以覆盖重写该方法以返回自定义的系统提示信息。
     *
     * @return 系统提示的文本模板
     */
    default String systemMessage() {
        return "";
    }


    /**
     * 获取工具列表,默认返回空数组。子类需根据需求覆盖此方法。
     */
    default Object[] tools() {
        return EMPTY_OBJECTS;
    }

    /**
     * 创建并返回一个工具上下文的空Map对象。
     *
     * @param sessionId 会话标识符
     * @param requestId 请求标识符
     * @return 默认返回一个空的Map对象,子类可以覆盖重写该方法以返回自定义的工具上下文。
     */
    default Map<String, Object> toolContext(String sessionId, String requestId) {
        return Map.of();
    }

    /**
     * Advisor列表,默认返回空对象
     */
    default List<Advisor> advisors() {
        return List.of();
    }

    /**
     * 创建并返回一个Advisor的空Map对象。
     *
     * @param sessionId 会话标识符
     * @param requestId 请求标识符
     * @return 默认返回一个空的Map对象,子类可以覆盖重写该方法以返回自定义的工具上下文。
     */
    default Map<String, Object> advisorParams(String sessionId, String requestId) {
        return Map.of();
    }

    /**
     * 获取系统提示信息模板的参数,默认为空Map,子类可以覆盖重写该方法以返回自定义的系统提示信息参数。
     */
    default Map<String, Object> systemMessageParams() {
        return Map.of();
    }

}

3.2.3. 编写抽象类

java
package com.tianji.aigc.agent;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.tianji.aigc.config.ToolResultHolder;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.ChatEventTypeEnum;
import com.tianji.aigc.service.ChatService;
import com.tianji.aigc.service.ChatSessionService;
import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import reactor.core.publisher.Flux;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public abstract class AbstractAgent implements Agent {

    @Resource
    private ChatSessionService chatSessionService;
    @Resource
    private ChatClient chatClient;
    @Resource
    private ChatMemory chatMemory;

    // 输出结束的标记
    public static final ChatEventVO STOP_EVENT = ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build();

    // 存储大模型的生成状态,这里采用ConcurrentHashMap是确保线程安全
    // 目前的版本暂时用Map实现,如果考虑分布式环境的话,可以考虑用redis来实现
    public static final Map<String, Boolean> GENERATE_STATUS = new ConcurrentHashMap<>();

    @Override
    public String process(String question, String sessionId) {
        // 获取用户id
        var userId = UserContext.getUser();
        var requestId = this.generateRequestId();

        //更新会话时间
        this.chatSessionService.update(sessionId, question, userId);

        return this.getChatClientRequest(sessionId, requestId, question)
                .call()
                .content();
    }

    public Flux<ChatEventVO> processStream(String question, String sessionId) {
        // 获取用户id
        var userId = UserContext.getUser();
        var requestId = this.generateRequestId();
        // 大模型输出内容的缓存器,用于在输出中断后的数据存储
        var outputBuilder = new StringBuilder();
        // 获取对话id
        var conversationId = ChatService.getConversationId(sessionId);

        //更新会话时间
        this.chatSessionService.update(sessionId, question, userId);

        return this.getChatClientRequest(sessionId, requestId, question)
                .stream()
                .chatResponse()
                .doFirst(() -> GENERATE_STATUS.put(sessionId, true)) // 第一次输出内容时执行
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 出现异常时,删除标识
                .doOnComplete(() -> GENERATE_STATUS.remove(sessionId)) // 完成时执行,删除标识
                .doOnCancel(() -> {
                    // 当输出被取消时,保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                .takeWhile(response -> { // 通过返回值来控制Flux流是否继续,true:继续,false:终止
                    return GENERATE_STATUS.getOrDefault(sessionId, false);
                })
                .map(chatResponse -> {
                    var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
                    if (StrUtil.equals(Constant.STOP, finishReason)) {
                        var messageId = chatResponse.getMetadata().getId();
                        ToolResultHolder.put(messageId, Constant.REQUEST_ID, requestId);
                    }

                    // 获取大模型的输出的内容
                    var text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    // 封装响应对象
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                })
                .concatWith(Flux.defer(() -> {
                    // 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中
                    var map = ToolResultHolder.get(requestId);
                    if (CollUtil.isNotEmpty(map)) {
                        ToolResultHolder.remove(requestId); // 清除参数列表

                        // 响应给前端的参数数据
                        var chatEventVO = ChatEventVO.builder()
                                .eventData(map)
                                .eventType(ChatEventTypeEnum.PARAM.getValue())
                                .build();
                        return Flux.just(chatEventVO, STOP_EVENT);
                    }
                    return Flux.just(STOP_EVENT);
                }));
    }

    private ChatClient.ChatClientRequestSpec getChatClientRequest(String sessionId, String requestId, String question) {
        return this.chatClient.prompt()
                .system(promptSystem -> promptSystem.text(this.systemMessage()).params(this.systemMessageParams()))
                .advisors(advisor -> advisor.advisors(this.advisors()).params(this.advisorParams(sessionId, requestId)))
                .tools(this.tools())
                .toolContext(this.toolContext(sessionId, requestId))
                .user(question);
    }

    /**
     * 保存停止输出的记录
     *
     * @param conversationId 对话id
     * @param content   大模型输出的内容
     */
    private void saveStopHistoryRecord(String conversationId, String content) {
        this.chatMemory.add(conversationId, new AssistantMessage(content));
    }

    private String generateRequestId() {
        return IdUtil.fastSimpleUUID();
    }

    @Override
    public Map<String, Object> advisorParams(String sessionId, String requestId) {
        var conversationId = ChatService.getConversationId(sessionId);
        return Map.of(ChatMemory.CONVERSATION_ID, conversationId);
    }

    @Override
    public void stop(String sessionId) {
        GENERATE_STATUS.remove(sessionId);
    }
}

3.3. 路由智能体

3.3.1. 系统提示词

智能体的提示词依然是存放在nacos中,并且支持热更新。

img
plain
# 角色
天机AI意图分析师

## 能力
1. 识别用户意图并匹配对应编号:
   - RECOMMEND(课程推荐)
   - BUY(课程购买)
   - CONSULT(课程咨询)
   - KNOWLEDGE(知识讲解)
2. 特殊场景处理:
   - 识别关键词触发意图:
     - BUY: 确认购买/下单/是的确认
     - RECOMMEND: 包含年龄/学历/兴趣信息
   - 识别问候语并礼貌回应:你好/您好
3. 非相关提问时礼貌拒答

## 约束
精准识别,避免误判

## 输出
- 匹配意图时返回编号
- 问候语场景返回「您好!有什么可以帮您?」
- 无匹配时用自然语言回复

## 示例
输入:20岁本科想学Java → RECOMMEND  
输入:现在要下单 → BUY  
输入:这个课程多少钱 → CONSULT
输入:java是什么 → KNOWLEDGE
输入:你好 → 您好!有什么可以帮您?  
输入:今天天气 → 抱歉我只处理课程相关问题

3.3.2. 读取配置

java
package com.tianji.aigc.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.prompt")
public class AIProperties {

    private System system; // 系统提示语,用于课程推荐、购买业务

    @Data
    public static class System {
        private Chat chat; // 系统提示语,用于课程推荐、购买业务
        private Chat routeAgent; // 路由智能体系统提示词

        @Data
        public static class Chat {
            private String dataId;
            private String group = "DEFAULT_GROUP";
            private long timeoutMs = 20000L; // 读取的超时时间,单位毫秒
        }
    }
}

application.yml中增加配置:

yaml
tj:
  ai:
    prompt:
      system:
        chat:
          data-id: system-chat-message.txt
          group: DEFAULT_GROUP
          timeout-ms: 20000
        route-agent:
          data-id: route-agent-system-message.txt

加载配置:

java
package com.tianji.aigc.config;
// 省略一些代码........
public class SystemPromptConfig {

    // 省略一些代码........

    // 使用原子引用,保证线程安全
    private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> routeAgentSystemMessage = new AtomicReference<>();
    
    @PostConstruct // 初始化时加载配置
    public void init() {
        // 读取配置文件
        loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
        loadConfig(aiProperties.getSystem().getRouteAgent(), routeAgentSystemMessage);
    }
// 省略一些代码........
}

3.3.3. 编写智能体

java
package com.tianji.aigc.agent;

import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.enums.AgentTypeEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
 * 路由智能体
 */
@Component
@RequiredArgsConstructor
public class RouteAgent extends AbstractAgent {

    private final SystemPromptConfig systemPromptConfig;

    @Override
    public String systemMessage() {
        return this.systemPromptConfig.getRouteAgentSystemMessage().get();
    }

    @Override
    public AgentTypeEnum getAgentType() {
        return AgentTypeEnum.ROUTE;
    }

}

3.3.4. 测试用例

java
package com.tianji.aigc.agent;

import cn.hutool.core.lang.Assert;
import com.tianji.aigc.enums.AgentTypeEnum;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RouteAgentTest {

    @Resource
    private RouteAgent routeAgent;

    @Test
    public void testChat(){
        Assert.equals(this.routeAgent.process("最新有哪些课程", "1"), AgentTypeEnum.RECOMMEND.getAgentName());
        Assert.equals(this.routeAgent.process("下单购买这个课程", "1"), AgentTypeEnum.BUY.getAgentName());
        Assert.equals(this.routeAgent.process("这个课程是多少钱", "1"), AgentTypeEnum.CONSULT.getAgentName());
        Assert.equals(this.routeAgent.process("java是什么", "1"), AgentTypeEnum.KNOWLEDGE.getAgentName());
    }

}

3.4. 推荐智能体

3.4.1. 系统提示词

img
plain
# 在线教育客服&讲师指南

## 核心职责
分步精准推荐:信息采集 → 课程匹配 → 执行推荐

## 强制流程
1. **信息采集(必须优先)**
   - 必须收集三项核心数据:
     ▪ 年龄(数字)
     ▪ 最高学历(初中/高中/本科/硕士等)
     ▪ 编程基础(无经验/基础语法/项目经验)
   - 任一信息缺失时:立即停止推荐,礼貌追问直至信息完整

2. **课程匹配
   - 强制:要通过课程id查询课程之后再输出
   - 匹配逻辑:
     1) 精准匹配(年龄+学历+兴趣)
     2) 向下兼容课程(如学历达标但年龄较小)
     3) 关联领域Top3课程

3. **推荐执行
   - 每次推荐必须包含:
     ▪ 数据关联说明(例:"针对25岁本科学历...")
     ▪ 课程适配点(例:"包含实战项目模块...")
   - 禁止推荐未经数据验证的课程

## 关键规则
- 阻断机制:未收齐三项数据前禁用推荐功能
- 数据校验:发现矛盾数据(如"12岁硕士学历")需确认
- 异常处理:无匹配时提供「人工咨询」入口
- 必须要输出课程id、价格、介绍等信息

3.4.2. 读取配置

java
package com.tianji.aigc.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.prompt")
public class AIProperties {

    private System system; // 系统提示语,用于课程推荐、购买业务

    @Data
    public static class System {
        private Chat chat; // 系统提示语,用于课程推荐、购买业务
        private Chat routeAgent; // 路由智能体系统提示词
        private Chat recommendAgent; // 推荐智能体系统提示词

        @Data
        public static class Chat {
            private String dataId;
            private String group = "DEFAULT_GROUP";
            private long timeoutMs = 20000L; // 读取的超时时间,单位毫秒
        }
    }
}

application.yml中增加配置:

yaml
tj:
  ai:
    prompt:
      system:
        chat:
          data-id: system-chat-message.txt
          group: DEFAULT_GROUP
          timeout-ms: 20000
        route-agent:
          data-id: route-agent-system-message.txt
        recommend-agent:
          data-id: recommend-agent-system-message.txt

加载配置:

java
package com.tianji.aigc.config;
// 省略一些代码........
public class SystemPromptConfig {

    // 省略一些代码........

    // 使用原子引用,保证线程安全
    private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> routeAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> recommendAgentSystemMessage = new AtomicReference<>();
    
    @PostConstruct // 初始化时加载配置
    public void init() {
        // 读取配置文件
        loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
        loadConfig(aiProperties.getSystem().getRouteAgent(), routeAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getRecommendAgent(), recommendAgentSystemMessage);
    }
// 省略一些代码........
}

3.4.3. 编写智能体

java
package com.tianji.aigc.agent;

import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.tools.CourseTools;
import com.tianji.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class RecommendAgent extends AbstractAgent {

    private final SystemPromptConfig systemPromptConfig;
    private final VectorStore vectorStore;
    private final CourseTools courseTools;

    @Override
    public String systemMessage() {
        return this.systemPromptConfig.getRecommendAgentSystemMessage().get();
    }

    @Override
    public AgentTypeEnum getAgentType() {
        return AgentTypeEnum.RECOMMEND;
    }

    @Override
    public List<Advisor> advisors() {
        // 创建RAG增强
        var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
                .searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
                .build();
        return List.of(qaAdvisor);
    }

    @Override
    public Object[] tools() {
        return new Object[]{courseTools};
    }

    @Override
    public Map<String, Object> toolContext(String sessionId, String requestId) {
        var userId = UserContext.getUser();
        return Map.of(
                Constant.USER_ID, userId, // 设置用户id参数
                Constant.REQUEST_ID, requestId  // 设置请求id参数
        );
    }

}

3.4.4. 测试用例

java
package com.tianji.aigc.agent;

import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.core.publisher.Flux;

@SpringBootTest
class RecommendAgentTest {

    @Resource
    private RecommendAgent recommendAgent;

    @Test
    public void processStream() throws InterruptedException {
        String question = "推荐课程,20岁,本科,对产品运营感兴趣";
        String sessionId = "123";
        UserContext.setUser(123L);
        Flux<ChatEventVO> flux = recommendAgent.processStream(question, sessionId);
        flux.subscribe(System.out::println);

        // 阻塞主线程,防止主线程结束,子线程终止
        Thread.sleep(100000);
    }

}

运行结果如下:

bash
ChatEventVO(eventData=在, eventType=1001)
ChatEventVO(eventData=为您, eventType=1001)
ChatEventVO(eventData=推荐, eventType=1001)
ChatEventVO(eventData=课程之前,我, eventType=1001)
ChatEventVO(eventData=需要确认几个信息, eventType=1001)
ChatEventVO(eventData=:您目前有, eventType=1001)
ChatEventVO(eventData=产品运营方面的工作, eventType=1001)
ChatEventVO(eventData=经验吗?如果有, eventType=1001)
ChatEventVO(eventData=,请告诉我大概有多少, eventType=1001)
ChatEventVO(eventData=时间的经验。这, eventType=1001)
ChatEventVO(eventData=将帮助我更, eventType=1001)
ChatEventVO(eventData=精准地为您推荐, eventType=1001)
ChatEventVO(eventData=合适的课程。
根据, eventType=1001)
ChatEventVO(eventData=您的情况,我, eventType=1001)
ChatEventVO(eventData=将考虑以下两个, eventType=1001)
ChatEventVO(eventData=课程:

第一个是, eventType=1001)
ChatEventVO(eventData=“高级产品运营, eventType=1001)
ChatEventVO(eventData=策略与实践”,, eventType=1001)
ChatEventVO(eventData=这个课程适合2, eventType=1001)
ChatEventVO(eventData=0岁以上,具有, eventType=1001)
ChatEventVO(eventData=大专及以上学历,, eventType=1001)
ChatEventVO(eventData=至少1年的产品, eventType=1001)
ChatEventVO(eventData=运营工作经验的专业人士, eventType=1001)
ChatEventVO(eventData=。您可以深入学习, eventType=1001)
ChatEventVO(eventData=高级的产品管理技巧, eventType=1001)

..........省略一些输出..........

ChatEventVO(eventData=具体需求或疑问, eventType=1001)
ChatEventVO(eventData=,欢迎继续咨询, eventType=1001)
ChatEventVO(eventData=!, eventType=1001)
ChatEventVO(eventData={courseInfo_1880521847886917634=CourseInfo(id=1880521847886917634, name=互联网产品运营实战, price=199.99, validDuration=12, usePeople=本课程适合20至35岁之间的学员,要求具备大专及以上学历,并且需要有一定的市场营销或产品管理基础知识。学员应对互联网行业有浓厚的兴趣,愿意深入学习如何管理和优化互联网产品。, detail=本课程旨在帮助学员掌握互联网产品运营的核心技能。通过系统化的学习,学员将了解如何从零开始构建一个成功的产品,并有效地进行市场推广和用户增长。课程内容涵盖产品生命周期管理、用户需求分析、竞品分析、数据分析与优化、用户增长策略等方面。我们将结合实际案例进行讲解,让学员能够更好地理解和应用所学知识。此外,课程还会教授如何使用各种工具(如Google Analytics、Mixpanel等)进行数据监测和分析,从而提升产品的用户体验和市场表现。通过本课程的学习,学员将能够在实际工作中灵活运用这些技能,推动产品的持续改进和优化。), courseInfo_1880522777017528321=CourseInfo(id=1880522777017528321, name=高级产品运营策略与实践, price=0.0, validDuration=9999, usePeople=年龄20岁以上,具有大专及以上学历,至少1年的产品运营工作经验。适合希望深化自己的运营技能,掌握更高级的产品管理技巧的专业人士。, detail=课程采用理论与实践相结合的教学方式,通过一系列模拟项目让学员亲身体验从战略规划到执行落地的全过程。学习完成后,学员不仅能熟练应用所学知识解决实际问题,还能为所在企业带来创新性的解决方案。)}, eventType=1003)
ChatEventVO(eventData=null, eventType=1002)

3.5. 课程购买智能体

3.5.1. 系统提示词

img
plain
# 角色说明

作为在线教育平台的资深客服代表兼讲师,你的职责包括协助学员购买课程,并提供相关支持。

## 技能要求

### 课程购买流程
1. **判断购买意图**:当学员表示想要购买课程时,首先确认会话中是否已明确提及具体的课程名称或系统已为学员推荐了特定课程。
2. **直接预下单**:
   - 如果学员已经明确了具体课程名或系统已有推荐,则调用`prePlaceOrder`接口,基于已知信息直接进入预下单流程。
3. **引导推荐流程**:
   - 若无明确课程或未进行推荐,需引导学员进入课程推荐流程,帮助其找到合适的课程。
4. **询问具体需求**:
   - 当学员表达购买意愿但未指明具体课程时,主动询问其感兴趣的课程名称。
5. **支持多课程购买**:
   - 确保能够处理单门或多门课程的购买请求。

## 注意事项
- 始终关注学员的具体需求,确保提供的服务精准且高效。

3.5.2. 读取配置

java
package com.tianji.aigc.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.prompt")
public class AIProperties {

    private System system; // 系统提示语,用于课程推荐、购买业务

    @Data
    public static class System {
        private Chat chat; // 系统提示语,用于课程推荐、购买业务
        private Chat routeAgent; // 路由智能体系统提示词
        private Chat recommendAgent; // 推荐智能体系统提示词
        private Chat buyAgent; // 购买智能体系统提示词

        @Data
        public static class Chat {
            private String dataId;
            private String group = "DEFAULT_GROUP";
            private long timeoutMs = 20000L; // 读取的超时时间,单位毫秒
        }
    }
}

application.yml中增加配置:

yaml
tj:
  ai:
    prompt:
      system:
        chat:
          data-id: system-chat-message.txt
          group: DEFAULT_GROUP
          timeout-ms: 20000
        route-agent:
          data-id: route-agent-system-message.txt
        recommend-agent:
          data-id: recommend-agent-system-message.txt
        buy-agent:
          data-id: buy-agent-system-message.txt

加载配置:

java
package com.tianji.aigc.config;
// 省略一些代码........
public class SystemPromptConfig {

    // 省略一些代码........

    // 使用原子引用,保证线程安全
    private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> routeAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> recommendAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> buyAgentSystemMessage = new AtomicReference<>();
    
    @PostConstruct // 初始化时加载配置
    public void init() {
        // 读取配置文件
        loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
        loadConfig(aiProperties.getSystem().getRouteAgent(), routeAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getRecommendAgent(), recommendAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getBuyAgent(), buyAgentSystemMessage);
    }
// 省略一些代码........
}

3.5.3. 编写智能体

java
package com.tianji.aigc.agent;

import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.tools.OrderTools;
import com.tianji.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@RequiredArgsConstructor
public class BuyAgent extends AbstractAgent {

    private final SystemPromptConfig systemPromptConfig;
    private final OrderTools orderTools;

    @Override
    public String systemMessage() {
        return this.systemPromptConfig.getBuyAgentSystemMessage().get();
    }

    @Override
    public AgentTypeEnum getAgentType() {
        return AgentTypeEnum.BUY;
    }

    @Override
    public Object[] tools() {
        return new Object[]{orderTools};
    }

    @Override
    public Map<String, Object> toolContext(String sessionId, String requestId) {
        var userId = UserContext.getUser();
        return Map.of(
                Constant.USER_ID, userId, // 设置用户id参数
                Constant.REQUEST_ID, requestId  // 设置请求id参数
        );
    }
}

3.5.4. 测试用例

java
package com.tianji.aigc.agent;

import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.core.publisher.Flux;

@SpringBootTest
class BuyAgentTest {

    @Resource
    private BuyAgent buyAgent;

    @Test
    public void processStream() throws InterruptedException {
        String question = "下单购买,课程id为:1589905661084430337";
        String sessionId = "123";
        UserContext.setUser(123L);
        Flux<ChatEventVO> flux = buyAgent.processStream(question, sessionId);
        flux.subscribe(System.out::println);

        // 阻塞主线程,防止主线程结束,子线程终止
        Thread.sleep(100000);
    }

}

测试结果:

bash
ChatEventVO(eventData=您, eventType=1001)
ChatEventVO(eventData=已成功预下单, eventType=1001)
ChatEventVO(eventData=课程ID为1, eventType=1001)
ChatEventVO(eventData=5899, eventType=1001)
ChatEventVO(eventData=0566, eventType=1001)
ChatEventVO(eventData=1084, eventType=1001)
ChatEventVO(eventData=4303, eventType=1001)
ChatEventVO(eventData=37的课程, eventType=1001)
ChatEventVO(eventData=。此订单详情, eventType=1001)
ChatEventVO(eventData=如下:

-, eventType=1001)
ChatEventVO(eventData= 课程数量:1, eventType=1001)
ChatEventVO(eventData=
- 总, eventType=1001)
ChatEventVO(eventData=金额:19, eventType=1001)
ChatEventVO(eventData=9元
-, eventType=1001)
ChatEventVO(eventData= 折扣金额, eventType=1001)
ChatEventVO(eventData=:0元(, eventType=1001)
ChatEventVO(eventData=未使用优惠券, eventType=1001)
ChatEventVO(eventData=)
- 实付, eventType=1001)
ChatEventVO(eventData=金额:19, eventType=1001)
ChatEventVO(eventData=9元

您的, eventType=1001)
ChatEventVO(eventData=订单号为:, eventType=1001)
ChatEventVO(eventData=1905, eventType=1001)
ChatEventVO(eventData=5221, eventType=1001)
ChatEventVO(eventData=7666, eventType=1001)
ChatEventVO(eventData=1401, eventType=1001)
ChatEventVO(eventData=602。, eventType=1001)
ChatEventVO(eventData=

请确认信息无, eventType=1001)
ChatEventVO(eventData=误后,进行, eventType=1001)
ChatEventVO(eventData=支付完成购买。, eventType=1001)
ChatEventVO(eventData=若有任何疑问或, eventType=1001)
ChatEventVO(eventData=需要进一步的帮助,请, eventType=1001)
ChatEventVO(eventData=随时联系我们。感谢, eventType=1001)
ChatEventVO(eventData=您选择我们的课程, eventType=1001)
ChatEventVO(eventData=!, eventType=1001)
ChatEventVO(eventData={prePlaceOrder=PrePlaceOrder(count=1, totalAmount=199.0, discountAmount=0.0, couponName=, payAmount=199.0, courseIds=[1589905661084430337], orderId=1905522176661401602, couponId=null)}, eventType=1003)
ChatEventVO(eventData=null, eventType=1002)

3.6. 练习:课程咨询智能体

3.6.1. 系统提示词

img
plain
# 角色说明

作为在线教育平台的资深客服代表兼讲师,你的主要职责是为学员提供关于课程的咨询服务。

## 技能:课程咨询

### 课程推荐与信息查询
- 当学员询问课程内容时,根据知识库匹配合适的课程,并获取课程ID以查询详细信息。确保回复全面且具有引导性,鼓励学员报名购买。
- 若未能找到相关课程,请礼貌通知学员未检索到相关内容,并建议联系人工客服(电话:010-12345678)。
- 对于课程有效期的咨询,将当前时间{now}与课程有效期相加后告知学员具体日期;若有效期为999天,则视为永久有效。

### 注意事项
- 所有推荐课程必须源自知识库,严禁编造。
- 确保回答逻辑清晰、内容详尽无遗漏。
- 仅限回答与课程和IT知识点相关的问题。如遇无关问题,应告知学员无法作答,并引导其提出与课程或IT相关的疑问。
- 学员询问课程ID时,解释无法直接提供课程ID,并邀请他们探讨其他感兴趣的话题。

3.6.2. 读取配置

java
package com.tianji.aigc.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.prompt")
public class AIProperties {

    private System system; // 系统提示语,用于课程推荐、购买业务

    @Data
    public static class System {
        private Chat chat; // 系统提示语,用于课程推荐、购买业务
        private Chat routeAgent; // 路由智能体系统提示词
        private Chat recommendAgent; // 推荐智能体系统提示词
        private Chat buyAgent; // 购买智能体系统提示词
        private Chat consultAgent; // 咨询智能体系统提示词

        @Data
        public static class Chat {
            private String dataId;
            private String group = "DEFAULT_GROUP";
            private long timeoutMs = 20000L; // 读取的超时时间,单位毫秒
        }
    }
}

application.yml中增加配置:

yaml
tj:
  ai:
    prompt:
      system:
        chat:
          data-id: system-chat-message.txt
          group: DEFAULT_GROUP
          timeout-ms: 20000
        route-agent:
          data-id: route-agent-system-message.txt
        recommend-agent:
          data-id: recommend-agent-system-message.txt
        buy-agent:
          data-id: buy-agent-system-message.txt
        consult-agent:
          data-id: consult-agent-system-message.txt

加载配置:

java
package com.tianji.aigc.config;
// 省略一些代码........
public class SystemPromptConfig {

    // 省略一些代码........

    // 使用原子引用,保证线程安全
    private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> routeAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> recommendAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> buyAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> consultAgentSystemMessage = new AtomicReference<>();
    
    @PostConstruct // 初始化时加载配置
    public void init() {
        // 读取配置文件
        loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
        loadConfig(aiProperties.getSystem().getRouteAgent(), routeAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getRecommendAgent(), recommendAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getBuyAgent(), buyAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getConsultAgent(), consultAgentSystemMessage);
    }
// 省略一些代码........
}

3.6.3. 编写智能体

java
package com.tianji.aigc.agent;

import cn.hutool.core.date.DateUtil;
import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.tools.CourseTools;
import com.tianji.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

/**
 * 课程咨询智能体
 */
@Component
@RequiredArgsConstructor
public class ConsultAgent extends AbstractAgent {

    private final SystemPromptConfig systemPromptConfig;
    private final VectorStore vectorStore;
    private final CourseTools courseTools;

    @Override
    public String systemMessage() {
        return this.systemPromptConfig.getConsultAgentSystemMessage().get();
    }

    @Override
    public AgentTypeEnum getAgentType() {
        return AgentTypeEnum.CONSULT;
    }

    @Override
    public List<Advisor> advisors() {
        // 创建RAG增强
        var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
                .searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
                .build();
        return List.of(qaAdvisor);
    }

    @Override
    public Object[] tools() {
        return new Object[]{courseTools};
    }

    @Override
    public Map<String, Object> toolContext(String sessionId, String requestId) {
        var userId = UserContext.getUser();
        return Map.of(
                Constant.USER_ID, userId, // 设置用户id参数
                Constant.REQUEST_ID, requestId  // 设置请求id参数
        );
    }

    @Override
    public Map<String, Object> systemMessageParams() {
        return Map.of("now", DateUtil.now());
    }
}

3.6.4. 测试用例

java
package com.tianji.aigc.agent;

import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.core.publisher.Flux;

@SpringBootTest
class ConsultAgentTest {

    @Resource
    private ConsultAgent consultAgent;

    @Test
    public void processStream() throws InterruptedException {
        String question = "课程多少钱,课程id为:1589905661084430337";
        String sessionId = "123";
        UserContext.setUser(123L);
        Flux<ChatEventVO> flux = consultAgent.processStream(question, sessionId);
        flux.subscribe(System.out::println);

        // 阻塞主线程,防止主线程结束,子线程终止
        Thread.sleep(100000);
    }

}

测试结果:

bash
ChatEventVO(eventData=您, eventType=1001)
ChatEventVO(eventData=所查询的课程, eventType=1001)
ChatEventVO(eventData= "可能是史上最全, eventType=1001)
ChatEventVO(eventData=的微服务技术, eventType=1001)
ChatEventVO(eventData=栈课程" 的, eventType=1001)
ChatEventVO(eventData=价格为19, eventType=1001)
ChatEventVO(eventData=9元。此, eventType=1001)
ChatEventVO(eventData=课程由黑马名师, eventType=1001)
ChatEventVO(eventData=授课,非常适合有一定, eventType=1001)
ChatEventVO(eventData=Java开发基础并, eventType=1001)
ChatEventVO(eventData=熟练使用SpringBoot, eventType=1001)
ChatEventVO(eventData=、MyBatis, eventType=1001)
ChatEventVO(eventData=等基础框架的学习, eventType=1001)
ChatEventVO(eventData=者。鉴于其, eventType=1001)
ChatEventVO(eventData=详尽的内容和, eventType=1001)
ChatEventVO(eventData=优质的教学资源,, eventType=1001)
ChatEventVO(eventData=这确实是一个值得, eventType=1001)
ChatEventVO(eventData=拥有的课程。, eventType=1001)
ChatEventVO(eventData=希望您能喜欢, eventType=1001)
ChatEventVO(eventData=并从中受益!, eventType=1001)
ChatEventVO(eventData=如果您有更多问题, eventType=1001)
ChatEventVO(eventData=或需要进一步的帮助, eventType=1001)
ChatEventVO(eventData=,请随时联系我们。, eventType=1001)
ChatEventVO(eventData=, eventType=1001)
ChatEventVO(eventData={courseInfo_1589905661084430337=CourseInfo(id=1589905661084430337, name=可能是史上最全的微服务技术栈课程, price=199.0, validDuration=9999, usePeople=有一定的Java开发基础,熟练使用了SpringBoot、MyBatis等基础框架, detail=可能是史上最全的微服务技术栈课程,由黑马名师授课,你值的拥有)}, eventType=1003)
ChatEventVO(eventData=null, eventType=1002)

3.7. 练习:知识讲解智能体

3.7.1. 系统提示词

img
plain
# 角色说明

作为在线教育平台的资深客服代表兼讲师,你的主要职责是解答学员关于IT相关知识点的疑问,并提供详细讲解和示例。

## 技能要求

### 知识讲解
- 针对学员提出的IT知识点问题,进行详细的解析并给出实际案例辅助理解。

## 限制条件

- 仅限回答与课程内容及IT知识点相关的问题。如果学员提出与课程或IT知识无关的问题,请告知其你只能回答相关问题,并鼓励他们提出课程或IT领域的疑问。

3.7.2. 读取配置

java
package com.tianji.aigc.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.prompt")
public class AIProperties {

    private System system; // 系统提示语,用于课程推荐、购买业务

    @Data
    public static class System {
        private Chat chat; // 系统提示语,用于课程推荐、购买业务
        private Chat routeAgent; // 路由智能体系统提示词
        private Chat recommendAgent; // 推荐智能体系统提示词
        private Chat buyAgent; // 购买智能体系统提示词
        private Chat knowledgeAgent; // 知识讲解智能体系统提示词

        @Data
        public static class Chat {
            private String dataId;
            private String group = "DEFAULT_GROUP";
            private long timeoutMs = 20000L; // 读取的超时时间,单位毫秒
        }
    }
}

application.yml中增加配置:

yaml
tj:
  ai:
    prompt:
      system:
        chat:
          data-id: system-chat-message.txt
          group: DEFAULT_GROUP
          timeout-ms: 20000
        route-agent:
          data-id: route-agent-system-message.txt
        recommend-agent:
          data-id: recommend-agent-system-message.txt
        buy-agent:
          data-id: buy-agent-system-message.txt
        knowledge-agent:
          data-id: knowledge-agent-system-message.txt

加载配置:

java
package com.tianji.aigc.config;
// 省略一些代码........
public class SystemPromptConfig {

    // 省略一些代码........

    // 使用原子引用,保证线程安全
    private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> routeAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> recommendAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> buyAgentSystemMessage = new AtomicReference<>();
    private final AtomicReference<String> knowledgeAgentSystemMessage = new AtomicReference<>();
    
    @PostConstruct // 初始化时加载配置
    public void init() {
        // 读取配置文件
        loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
        loadConfig(aiProperties.getSystem().getRouteAgent(), routeAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getRecommendAgent(), recommendAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getBuyAgent(), buyAgentSystemMessage);
        loadConfig(aiProperties.getSystem().getKnowledgeAgent(), knowledgeAgentSystemMessage);
    }
// 省略一些代码........
}

3.7.3. 编写智能体

java
package com.tianji.aigc.agent;

import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.enums.AgentTypeEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class KnowledgeAgent extends AbstractAgent {

    private final SystemPromptConfig systemPromptConfig;

    @Override
    public String systemMessage() {
        return this.systemPromptConfig.getKnowledgeAgentSystemMessage().get();
    }

    @Override
    public AgentTypeEnum getAgentType() {
        return AgentTypeEnum.KNOWLEDGE;
    }

}

3.7.4. 测试用例

java
package com.tianji.aigc.agent;

import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.core.publisher.Flux;

@SpringBootTest
class KnowledgeAgentTest {

    @Resource
    private KnowledgeAgent knowledgeAgent;

    @Test
    public void processStream() throws InterruptedException {
        String question = "简要说明,java什么";
        String sessionId = "123";
        UserContext.setUser(123L);
        Flux<ChatEventVO> flux = knowledgeAgent.processStream(question, sessionId);
        flux.subscribe(System.out::println);

        // 阻塞主线程,防止主线程结束,子线程终止
        Thread.sleep(100000);
    }

}

测试结果:

bash
ChatEventVO(eventData=Java, eventType=1001)
ChatEventVO(eventData=是一种, eventType=1001)
ChatEventVO(eventData=广泛, eventType=1001)
ChatEventVO(eventData=使用的计算机编程语言, eventType=1001)
ChatEventVO(eventData=,拥有跨平台, eventType=1001)
ChatEventVO(eventData=、面向对象、, eventType=1001)
ChatEventVO(eventData=泛型编程的, eventType=1001)

.........省略一些输出...........

ChatEventVO(eventData=当程序运行时, eventType=1001)
ChatEventVO(eventData=,它将在控制, eventType=1001)
ChatEventVO(eventData=台上打印出", eventType=1001)
ChatEventVO(eventData=Hello, World!", eventType=1001)
ChatEventVO(eventData=。, eventType=1001)
ChatEventVO(eventData=null, eventType=1002)

3.8. 多智能体协调工作

前面已经实现了多个智能体,这些智能体都是独立运行,接下来我们就需要把他们整合起来,一起协调工作,完成天机AI助理。

3.8.1. 编写AgentServiceImpl实现类

java
package com.tianji.aigc.service.impl;

import cn.hutool.extra.spring.SpringUtil;
import com.tianji.aigc.agent.AbstractAgent;
import com.tianji.aigc.agent.Agent;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.enums.ChatEventTypeEnum;
import com.tianji.aigc.service.ChatService;
import com.tianji.aigc.vo.ChatEventVO;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "tj.ai", name = "chat-type", havingValue = "ROUTE")
public class AgentServiceImpl implements ChatService {

    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        // 先通过路由智能体,分析用户的意图,再执行后面的逻辑
        var result = this.findAgentByType(AgentTypeEnum.ROUTE).process(question, sessionId);
        var agentTypeEnum = AgentTypeEnum.agentNameOf(result);

        var agent = this.findAgentByType(agentTypeEnum);
        if (agent == null) {
            // 找不到对应的智能体,直接返回结果
            var chatEventVO = ChatEventVO.builder()
                    .eventType(ChatEventTypeEnum.DATA.getValue())
                    .eventData(result)
                    .build();
            return Flux.just(chatEventVO, AbstractAgent.STOP_EVENT);
        }
        // 执行智能体的逻辑
        return agent.processStream(question, sessionId);
    }

    /**
     * 根据代理类型查找对应的Agent实例
     *
     * @param agentTypeEnum 要查找的代理类型
     * @return 与给定类型匹配的Agent实例,如果未找到或类型为null则返回null
     */
    private Agent findAgentByType(AgentTypeEnum agentTypeEnum) {
        if (agentTypeEnum == null) {
            return null;
        }
        var beans = SpringUtil.getBeansOfType(Agent.class);
        // 遍历所有Agent Bean查找匹配类型
        for (var agent : beans.values()) {
            if (agentTypeEnum == agent.getAgentType()) {
                return agent;
            }
        }
        return null;
    }

    /**
     * 停止生成
     *
     * @param sessionId 会话ID
     */
    @Override
    public void stop(String sessionId) {
        this.findAgentByType(AgentTypeEnum.ROUTE).stop(sessionId);
    }
}

application.yml配置文件中,增强条件配置:

yaml
tj:
  ai:
    chat-type: ROUTE # ROUTE / ENHANCE / APP

3.8.2. 改造ChatServiceImpl

在原ChatServiceImpl中添加条件:

img

3.8.3. 改造SpringAIConfig

在SpringAIConfig中,就不需要设置默认的Tool了,需要改造下,如下:

java
    /**
     * 配置 ChatClient
     */
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
                                 Advisor loggerAdvisor,
                                 Advisor messageChatMemoryAdvisor,
                                 CourseTools courseTools, // 课程工具
                                 OrderTools orderTools // 预下单工具
    ) {  // 日志记录器
        return chatClientBuilder
                .defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor) //添加 Advisor 功能增强
                // .defaultTools(courseTools, orderTools) //添加默认工具
                .build();
    }

3.8.4. 测试

测试时,最好使用 qwen-max-latest模型,这样会准确一些。

img
img
img
img

4. bug解决

4.1. bug说明

前面已经实现了多智能体的协调工作,在查询历史记录时,会这样显示:

img
img

完整的记录如下:

json
{
    "code": 200,
    "msg": "OK",
    "data": [
        {
            "type": "USER",
            "content": "课程推荐"
        },
        {
            "type": "ASSISTANT",
            "content": "RECOMMEND",
            "params": {}
        },
        {
            "type": "USER",
            "content": "课程推荐"
        },
        {
            "type": "ASSISTANT",
            "content": "为了给您提供最合适的课程推荐,请您先提供以下三项核心数据:\n1. 您的年龄\n2. 您的最高学历(初中/高中/本科/硕士等)\n3. 您的编程基础(无经验/基础语法/项目经验)\n\n这些信息将帮助我为您精准匹配最适合您的课程。期待您的回复!",
            "params": {}
        },
        {
            "type": "USER",
            "content": "20岁,本科,对java感兴趣,没有编程经验"
        },
        {
            "type": "ASSISTANT",
            "content": "RECOMMEND",
            "params": {}
        },
        {
            "type": "USER",
            "content": "20岁,本科,对java感兴趣,没有编程经验"
        },
        {
            "type": "ASSISTANT",
            "content": "针对20岁本科学历且对Java感兴趣但无编程经验的您,我为您精准匹配了以下课程:\n\n### 课程名称:Java开发零基础入门\n- **课程ID**:1880533253575225346\n- **适学人群**:适合18至28岁之间,具备大专及以上学历,无需任何编程基础的学员。\n- **课程价格**:0.01元\n- **有效期**:9999天\n- **课程介绍**:\n  - 学员将学习如何编写基本的Java代码,理解面向对象编程的核心概念,并掌握常用的Java开发环境(如Eclipse、IntelliJ IDEA)。\n  - 课程内容包括Java语法基础、变量与数据类型、控制结构、类与对象、异常处理等。\n  - 我们还将深入探讨如何进行项目管理和团队协作,帮助学员全面提升Java开发的能力。\n  - 通过大量的实战案例和项目练习,学员可以快速掌握Java开发的核心方法,并在实际工作中灵活运用。\n  - 完成本课程后,学员将具备独立编写简单Java应用程序的能力。\n\n此课程针对您的条件非常契合,无需编程基础即可开始学习。希望这个推荐对您有所帮助!如果有其他疑问或需要进一步的咨询,请随时告诉我。",
            "params": {
                "courseInfo_1880533253575225346": {
                    "id": "1880533253575225346",
                    "name": "Java开发零基础入门",
                    "price": 0.01,
                    "validDuration": 9999,
                    "usePeople": "本课程适合18至28岁之间的学员,要求具备大专及以上学历,无需任何编程基础。学员应具有对软件开发的浓厚兴趣,希望在Java开发领域有所发展。",
                    "detail": "学员将学习如何编写基本的Java代码,理解面向对象编程的核心概念,并掌握常用的Java开发环境(如Eclipse、IntelliJ IDEA)。课程内容包括Java语法基础、变量与数据类型、控制结构、类与对象、异常处理等。我们还将深入探讨如何进行项目管理和团队协作,帮助学员全面提升Java开发的能力。通过大量的实战案例和项目练习,学员可以快速掌握Java开发的核心方法,并在实际工作中灵活运用。完成本课程后,学员将具备独立编写简单Java应用程序的能力。"
                }
            }
        },
        {
            "type": "USER",
            "content": "下单购买"
        },
        {
            "type": "ASSISTANT",
            "content": "BUY",
            "params": {}
        },
        {
            "type": "USER",
            "content": "下单购买"
        },
        {
            "type": "ASSISTANT",
            "content": "您已成功预下单!以下是您购买课程的详细信息:\n\n- **订单ID**:1905550033890586625\n- **课程名称**:Java开发零基础入门\n- **原价**:0.01元\n- **优惠金额**:无\n- **实际支付金额**:0.01元\n\n请您在支付页面完成付款,以确保课程能够顺利开通。如有任何疑问或需要进一步的帮助,请随时联系我。祝您学习愉快!",
            "params": {
                "prePlaceOrder": {
                    "count": 1,
                    "totalAmount": 0.01,
                    "discountAmount": 0,
                    "couponName": "",
                    "payAmount": 0.01,
                    "courseIds": [
                        "1880533253575225346"
                    ],
                    "orderId": "1905550033890586625"
                }
            }
        }
    ],
    "requestId": "9ee824ef3061471d9cf519ac87251e22"
}

可以看到,由路由智能体输出的内容也被记录了下来,实际上,是不应该显示出来的,这个是内部的实现,不能让用户看到。

4.2. bug解决

📚 解决方案:

  我们将创建一个自定义的Advisor,名为RecordOptimizationAdvisor。该Advisor的主要职责是监控大型语言模型生成的输出内容。如果检测到输出中包含特定智能体的名称(例如:BUY、RECOMMEND等),则会从Redis缓存中移除最近两条记录。

  另外,为了确保流程正确执行,必须将RecordOptimizationAdvisor配置在MessageChatMemoryAdvisor之前。这样可以保证在大模型生成响应并将其存储至Redis之后,再由RecordOptimizationAdvisor进行相应的清理操作。此设置对于历史记录数据的一致性和准确性至关重要。

第一步:编写RecordOptimizationAdvisor

java
package com.tianji.aigc.advisor;

import cn.hutool.core.map.MapUtil;
import com.tianji.aigc.enums.AgentTypeEnum;
import com.tianji.aigc.memory.MyChatMemoryRepository;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;

/**
 * 记录优化
 */
public class RecordOptimizationAdvisor implements BaseAdvisor {

    private final MyChatMemoryRepository myChatMemoryRepository;

    public RecordOptimizationAdvisor(MyChatMemoryRepository myChatMemoryRepository) {
        this.myChatMemoryRepository = myChatMemoryRepository;
    }

    @Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
        return chatClientRequest;
    }

    @Override
    public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
        // 获取大模型的响应内容
        var chatResponse = chatClientResponse.chatResponse();
        // 获取大模型的响应内容,判断内容是否是智能体的名称,如果是,优化记录,否则无需优化
        assert chatResponse != null;
        var text = chatResponse.getResult().getOutput().getText();
        var agentType = AgentTypeEnum.agentNameOf(text);
        if (null != agentType) {
            // 需要优化记录
            var conversationId = MapUtil.getStr(chatClientResponse.context(), ChatMemory.CONVERSATION_ID);
            this.myChatMemoryRepository.optimization(conversationId);
        }

        return chatClientResponse;
    }

    @Override
    public int getOrder() {
        return Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER - 100;
    }
}

为了消除idea为空的警告,增加package-info.java文件:

java
@NonNullApi
@NonNullFields
package com.tianji.aigc.advisor;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

**第二步,**创建MyChatMemoryRepository接口,定义optimization方法:

java
package com.tianji.aigc.memory;

public interface MyChatMemoryRepository {

    /**
     * 根据对话ID优化对话记录,删除最后的2条消息,因为这条消息是从路由智能体存储的,请求由后续的智能体处理
     * 为了确保历史消息的完整性,所以需要将中间转发的消息清理掉
     *
     * @param conversationId 对话的唯一标识符
     */
    void optimization(String conversationId);

}

**第三步,**在RedisChatMemoryRepository中实现optimization方法:

java
    /**
     * 根据对话ID优化对话记录,删除最后的2条消息,因为这条消息是从路由智能体存储的,请求由后续的智能体处理
     * 为了确保历史消息的完整性,所以需要将中间转发的消息清理掉
     *
     * @param conversationId 对话的唯一标识符
     */
    public void optimization(String conversationId) {
        var redisKey = this.getKey(conversationId);
        var listOps = this.stringRedisTemplate.boundListOps(redisKey);
        // 从Redis列表右侧弹出2个元素
        listOps.rightPop(2);
    }

**第四步,**在SpringAIConfig中增加配置,使Advisor生效:

java
    /**
     * 配置 ChatClient
     */
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
                                 Advisor loggerAdvisor,
                                 Advisor messageChatMemoryAdvisor,
                                 Advisor recordOptimizationAdvisor, // 记录优化
                                 CourseTools courseTools, // 课程工具
                                 OrderTools orderTools // 预下单工具
    ) {  // 日志记录器
        return chatClientBuilder
                .defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor, recordOptimizationAdvisor) //添加 Advisor 功能增强
                // .defaultTools(courseTools, orderTools) //添加默认工具
                .build();
    }


    /**
     * 优化对话历史记录
     */
    @Bean
    public Advisor recordOptimizationAdvisor(MyChatMemoryRepository myChatMemoryRepository) {
        return new RecordOptimizationAdvisor(myChatMemoryRepository);
    }

**第五步,**测试:

json
{
    "code": 200,
    "msg": "OK",
    "data": [
        {
            "type": "USER",
            "content": "你好"
        },
        {
            "type": "ASSISTANT",
            "content": "您好!有什么可以帮您?",
            "params": {}
        },
        {
            "type": "USER",
            "content": "课程推荐"
        },
        {
            "type": "ASSISTANT",
            "content": "您好!有什么可以帮您呢?",
            "params": {}
        }
    ],
    "requestId": "85fe6cff6b4a40acae388601c679fb89"
}

可以看到,中间的记录已经被删除。


Q.E.D.
天机学堂AI助手智能体 - 平台智能体、通用文本模型与语音
天机学堂AI助手智能体 - 购买课程与知识库