--Day05--天机AI助手智能体-路由工作流

--今日任务总览

  • 历史对话管理
  • 了解6种智能体架构的模型
  • 基于SpringAI实现路由工作流智能体
  • 基于多智能体之间的协调工作
  • 解决过程聊天记录的bug

--1.历史对话

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

--1.1异步设置聊天记录标题

--1.1.1功能分析

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

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

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

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

--1.1.2代码实现

ChatSessionService中新增方法:

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

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

/**
     * 异步更新会话信息 以及时间
     *
     * @param sessionId 会话id
     * @param title     新的会话标题
     * @param userId    用户id
     */
    @Override
    @Async
    public void update(String sessionId, String title, Long userId) {
        //查询符合条件的聊天会话列表
        List<ChatSession> list = 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.isNotEmpty(title))
            chatSession.setTitle(StrUtil.sub(title, 0, 100));
        //设置更新字段为当前时间
        chatSession.setUpdateTime(LocalDateTime.now());
        //更新库
        updateById(chatSession);
    }

改造chatserviceimpl#chat方法

private final ChatSessionService chatSessionService;
/**
     * 流式聊天
     *
     * @param question  问题
     * @param sessionId 会话id
     * @return 响应流
     */
    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        //获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        //大模型输出内容的缓存器,用于在输出中断后的数据存储
        StringBuilder outputBuilder = new StringBuilder();
        //生成请求id
        var requestId = IdUtil.fastSimpleUUID();
        //用户id
        var userId = UserContext.getUser();
        //更新会话时间
        chatSessionService.update(sessionId, question, userId);
        return chatClient.prompt()
                .system(promptSystemSpec -> promptSystemSpec
                        .text(systemPromptConfig.getChatSystemMessage().get())//设置系统提示语
                        .param("now", DateUtil.now()))//设置当前时间参数
                .advisors(advisor -> advisor
                        .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder().query(question).topK(3).build()))
                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))
                .toolContext(MapUtil.<String, Object>builder()//设置tool列表
                        .put(Constant.REQUEST_ID, requestId)//设置请求id参数
                        .put(Constant.USER_ID, userId)
                        .build())
                .user(question)
                .stream()
                .chatResponse() //返回 ChatResponse 流(结构化)
                .doFirst(() -> {//输出开始,标记正在输出
                    GENERATE_STATUS.put(sessionId, true);
                })
                .doOnComplete(() -> {//输出结束,清除标记
                    GENERATE_STATUS.remove(sessionId);
                })
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId))//错误时清除标记
                .doOnCancel(() -> {
                    //当输出被取消时,保存输出的内容到历史记录中
                    saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                //输出过程中判断是否正在输出,如果正在输出,则继续输出,否则结束输出
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
                .map(chatResponse -> {
                    //对于响应结果进行处理,如果是最后一条数据,就把此次消息id放到内存中
                    //主要用于存储消息数据到Redis中,可以根据消息id获取的请求id,再通过请求id就可以获取到参数列表
                    //从而解决,再历史聊天记录中,没有外参数的问题
                    var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
                    if (StrUtil.equals(Constant.STOP, finishReason)) {
                        var messageId = chatResponse.getMetadata().getId();
                        ToolResultHolder.put(messageId, Constant.REQUEST_ID, requestId);
                    }
                    //获取大模型的输出内容
                    String 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);//清除参数列表
                        //响应给前端的参数数据
                        ChatEventVO chatEventVO = ChatEventVO.builder()
                                .eventData(map)
                                .eventType(ChatEventTypeEnum.PARAM.getValue())
                                .build();
                        return Flux.just(chatEventVO, STOP_EVENT);
                    }
                    return Flux.just(STOP_EVENT);
                }));
    }
--1.1.3测试

--1.1.4优化

基于目前的代码,可以看出,新建会话是在打开界面时就完成了,但是如果用户一直刷新界面而不进行对话,那么就会创建很多没有标题的空对话,因此我们把保存会话的逻辑挪动到第一次开始聊天之后

在后续异步添加标题的地方, 顺便保存session对象

/**
     * 异步更新会话信息 以及时间
     *
     * @param sessionId 会话id
     * @param title     新的会话标题
     * @param userId    用户id
     */
    @Override
    @Async
    public void update(String sessionId, String title, Long userId) {
        //1.session由uuid生成保证唯一
        ChatSession session = lambdaQuery().eq(ChatSession::getSessionId, sessionId).one();
        String newTitle = title.substring(0, Math.min(title.length(), 20));
        //1.1第一次保存session对象
        if (ObjectUtil.isNull(session)) {
            //保存对话信息
            ChatSession chatSession = ChatSession.builder()
                    .userId(userId)
                    .sessionId(sessionId)
                    .title(newTitle)
                    //由于异步的原因,MybatisAutoFillInterceptor在使用ThreadLocal时获取不到创建者信息,所以这里手动设置
                    .creater(userId)
                    .updater(userId)
                    .build();
            save(chatSession);
            return;
        }
        //2.更新时间和标题
        ChatSession chatSession = ChatSession.builder()
                .id(session.getId())
                .updateTime(LocalDateTime.now())
                .title(newTitle)
                .build();
        updateById(chatSession);
    }

并将之前保存会话的逻辑注释掉

/**
     * 新建对话
     *
     * @param num 热门问题的数量
     * @return SessionVO实体
     */
    @Override
    public SessionVO createSession(Integer num) {
        SessionVO sessionVO = BeanUtil.toBean(sessionProperties, SessionVO.class);
        //随机获取examples
        sessionVO.setExamples(RandomUtil.randomEleList(sessionProperties.getExamples(), num));
        //随机生成sessionId
        sessionVO.setSessionId(IdUtil.fastSimpleUUID());
//        构建持久化对象,并持久化
//        ChatSession chatSession = ChatSession.builder()
//                .sessionId(sessionVO.getSessionId())
//                 .userId(UserContext.getUser())
//                .build();
//         save(chatSession);
        return sessionVO;
    }

--测试

可以看到虽然新建了会话但是还没有保存

进行会话后保存了,OK

--1.2查询历史对话

--1.2.1需求分析

页面效果如下:

--1.2.2接口分析

响应结构

{
    "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
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.2controller
/**
     * 查询历史会话列表
     * @return 会话列表
     */
    @GetMapping("/history")
    public Map<String,List<ChatSessionVO>> queryHistorySession(){
        return chatSessionService.queryHistorySession();
    }
--1.2.3.3service和serviceimpl
/**
     * 查询历史会话
     * @return 历史会话列表
     */
    Map<String, List<ChatSessionVO>> queryHistorySession();
/**
     * 查询历史会话
     *
     * @return 历史会话列表
     */
    @Override
    public Map<String, List<ChatSessionVO>> queryHistorySession() {
        Long userId = UserContext.getUser();
        //查询历史对话,限制返回条数
        List<ChatSession> list = lambdaQuery().eq(ChatSession::getUserId, userId)
                .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 列表
        List<ChatSessionVO> chatSessionVOS = CollStreamUtil.toList(list, chatSession ->
                ChatSessionVO.builder()
                        .sessionId(chatSession.getSessionId())
                        .title(chatSession.getTitle())
                        .updateTime(chatSession.getUpdateTime())
                        .build());
        final String TODAY = "当天";
        final String LAST_30_DAYS = "最近30天";
        final String LAST_YEAR = "最近一年";
        final String MORE_THAN_YEAR = "一年以上";
        //当前时间
        LocalDateTime now = LocalDateTime.now();
        //按照更新时间分组
        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.3.4测试

--1.2.4bug解决

由于我们前面设置了,Redis里的消息在不被更新的情况下只会存在七天,那么这时我们通过查询历史对话详情的接口去查找历史对话记录时就会查找不到,导致用户查看不到这个一年之前的历史聊天记录,这时候怎么办呢?

在此之前我再更改一个逻辑,原来的查询历史聊天记录是取最早的1000条,这是不太友好的,所以我更改为--取最晚的100条

改造Redischatmemory的get方法

/**
     * 根据会话ID获取指定数量的最新消息
     *
     * @param conversationId 会话唯一标识符
     * @param lastN          需要获取的最后N条消息数量(N>0)
     * @return 包含消息对象的列表,若lastN<=0则返回空列表
     */
    @Override
    public List<Message> get(String conversationId, int lastN) {
        // 验证参数有效性,当lastN非正数时直接返回空结果
        if (lastN <= 0) {
            return List.of();
        }
        // 生成Redis键名用于存储会话消息
        var redisKey = this.getKey(conversationId);
        // 获取Redis列表操作对象
        var listOps = this.stringRedisTemplate.boundListOps(redisKey);
        // 从Redis列表中获取指定范围的元素(从最后一个元素回退到lastN位置)
        //var messages = listOps.range(0, lastN);
        var messages = listOps.range(-lastN, -1);
        // 将Redis返回的字符串列表转换为Message对象列表
        //空列表保护
        if (CollUtil.isEmpty(messages))
            return List.of();
        return CollStreamUtil.toList(messages, MessageUtil::toMessage);
    }

解决方案------查询历史对话详情时,Redis查不到时,去db回源,同时重建Redis(因为用户点开这个说明是近期要用),恢复当前会话的Redis列表,并恢复seq到db max seq

并把之前设置的Redis热窗口数更改到1000,保持一致

改造queryBySessionId方法

//单独注入Redischatmemory,重建缓存,避免走mq
    @Resource(name = "redisChatMemory")
    private ChatMemory redisChatMemory;
/**
     * 根据会话id查询消息列表
     *
     * @param sessionId 会话id
     * @return 消息列表
     */
    @Override
    public List<MessageVO> queryBySessionId(String sessionId) {
        //根据会话Id获取对话Id
        String conversationId = ChatService.getConversationId(sessionId);
        //从Redis中获取历史消息
        List<Message> messageList = chatMemory.get(conversationId, HISTORY_MESSAGE_COUNT);
        //如果Redis中查不到,回源db并重建当前会话
        if (CollUtil.isEmpty(messageList)) {
            Long userId = UserContext.getUser();
            List<ChatMessage> dbList = chatMessageService.lambdaQuery()
                    .eq(ChatMessage::getConversationId, conversationId)
                    .eq(ChatMessage::getUserId, userId)
                    .orderByDesc(ChatMessage::getSeq)
                    .last("limit " + HISTORY_MESSAGE_COUNT)
                    .list();
            if (CollUtil.isEmpty(dbList))
                return List.of();
            //取数据库最大seq
            Long maxSeq = dbList.get(0).getSeq();
            //反转成正序
            Collections.reverse(dbList);
            //meta_json还原为message
            messageList = CollStreamUtil.toList(dbList, m -> MessageUtil.toMessage(m.getMetaJson()));
            //回填Redis(仅走缓存,不触发mq)
            redisChatMemory.add(conversationId, messageList);
            //修复seq,避免seq从1开始
            String seqKey = "CHAT:SEQ:" + conversationId;
            stringRedisTemplate.opsForValue().set(seqKey, String.valueOf(maxSeq));
            stringRedisTemplate.expire(seqKey, Duration.ofDays(7));
        }
        //过滤并转换消息列表
        return StreamUtil.of(messageList)
                //过滤掉非用户消息和助手消息
                .filter(message ->
                        message.getMessageType() == MessageType.ASSISTANT
                                || message.getMessageType() == MessageType.USER)
                //转换成MessageVO对象
                .map(message -> {
                    //有参数要加上参数
                    if (message instanceof MyAssistantMessage) {
                        return MessageVO.builder()
                                .content(message.getText())
                                .type(MessageTypeEnum.valueOf(message.getMessageType().name()))
                                .params(((MyAssistantMessage) message).getParams())
                                .build();
                    }
                    return MessageVO.builder()
                            .content(message.getText())
                            .type(MessageTypeEnum.valueOf(message.getMessageType().name()))
                            .build();
                })
                .toList();
    }

--测试

好的没问题,那么我再测试一下,如果是带params结果的消息能否识别呢?

好的没问题,OK

新加聊天也没问题--那我们开始下一步

--1.3删除历史对话

--1.3.1需求分析

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

再注意:先删库再删缓存,否则会造成删库期间缓存重建

--1.3.2接口分析

请求参数只有一个sessionId

--1.3.3代码实现
/**
     * 删除历史会话
     * @param sessionId 会话id
     */
    @DeleteMapping("/history")
    public void deleteSession(@RequestParam("sessionId") String sessionId){
        chatSessionService.deleteSession(sessionId);
    }
/**
     * 删除会话
     * @param sessionId 会话id
     */
    void deleteSession(String sessionId);
/**
     * 删除会话
     *
     * @param sessionId 会话id
     */
    @Override
    @Transactional
    public void deleteSession(String sessionId) {
        //先删库--chatsession和chatmessage都要删--先删message
        Long userId = UserContext.getUser();
        LambdaQueryWrapper<ChatSession> lqw = Wrappers.<ChatSession>lambdaQuery()
                .eq(ChatSession::getSessionId, sessionId)
                .eq(ChatSession::getUserId, userId);
        LambdaQueryWrapper<ChatMessage> lqw2 = Wrappers.<ChatMessage>lambdaQuery()
                .eq(ChatMessage::getSessionId, sessionId)
                .eq(ChatMessage::getUserId, userId);
        chatMessageService.remove(lqw2);
        remove(lqw);
        //删除Redis中的数据,也是message和session都要删
        String conversationId = ChatService.getConversationId(sessionId);
        chatMemory.clear(conversationId);
    }
--1.3.4测试

OK没有了

--1.4更新历史会话标题

--1.4.1需求分析

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

点击编辑按钮:

修改标题并保存

--1.4.2接口分析

--1.4.3代码实现

经典的crud没啥说的

/**
     * 更新会话标题
     * @param sessionId 会话id
     * @param title 新的会话标题
     */
    @PutMapping("/history")
    public void updateSession(@RequestParam("sessionId") String sessionId,
                              @RequestParam("title") String title) {
        chatSessionService.updateSession(sessionId, title);
    }
/**
     * 更新会话标题
     * @param sessionId 会话id
     * @param title 新的会话标题
     */
    void updateSession(String sessionId, String title);
/**
     * 更新会话标题
     *
     * @param sessionId 会话id
     * @param title     新的会话标题
     */
    @Override
    public void updateSession(String sessionId, String title) {
        lambdaUpdate()
                .eq(ChatSession::getSessionId, sessionId)
                .eq(ChatSession::getUserId, UserContext.getUser())
                .set(ChatSession::getTitle, StrUtil.sub(title, 0, 20))
                .update();
    }
--1.4.4测试

没毛

--2.智能体架构模型

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

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

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

--2.1增强型智能体

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

模式说明:

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

目前实现的ai智能体就是这种模式

--2.2链式工作流智能体

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

模式说明:

  • 顺序分解:将任务拆解为 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 执行。

模式说明:

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

--2.4并行工作流智能体

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

模式说明:

  • 任务并行拆分:输入(In)同时分发给 LLM Call 1/2/3 并行处理,通过并发执行独立子任务提升效率。
  • 结果聚合策略:通过 Aggregator 模块整合多个LLM输出,用冗余计算换取结果可靠性。
  • 模块化隔离:各LLM节点无直接依赖关系(仅通过Aggregator连接),降低单点故障风险的容错设计。
  • 这种模式,一般会在两种场景中使用:
    • 将一个任务,拆分成多个子任务,并行执行,提升效率。
      • 例如:需要开发一个多维度内容审核系统,包括:是否含攻击性言论、关键数据是否准确、是否引用未授权内容等检测,这些检测可以并行执行,提升系统效率。
    • 将同一个任务,由不同的大模型执行多次,得到不同的输出,再聚合处理,以得到更准确的结果。
      • 例如:如医疗诊断辅助、金融风险评估

--2.5协调器工作流智能体

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

模式说明:

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

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

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

模式说明:

  • 生成阶段(Generator):产生初始解决方案(Solution)
  • 评估阶段(Evaluator):验证输出质量,可以选择Accepted(认可)或 Rejected+Feedback(拒绝+反馈)
  • 决策阶段:根据评估结果选择终结输出或重新生成
  • 举例:
  • 场景:
    • 在文学翻译领域,译者型语言模型(LLM)可能在初次尝试时未能完全把握文本中的细微差异,但评估型语言模型(LLM)能够对此提供具有建设性的反馈。
    • 在执行复杂的搜索任务时,往往需要通过多轮的搜索与分析来确保信息收集的全面性。在此过程中,评估者将根据已获取的信息质量及完整性,判断是否有必要展开进一步的搜索活动。

--2.7总结

模式名称

控制方式

延迟水平

可靠性

典型应用场景

开发复杂度

增强型智能体

直接输出

最低

简单问答、内容润色

简单

链式工作流智能体

线性顺序执行

中等

中高

分阶段任务(如大纲→内容→格式优化)

中等

路由工作流智能体

条件分支选择

低-中等

多领域处理(如客服分流转人工)

中等

并行工作流智能体

多模型并发执行

中等

可靠性敏感任务(如医疗诊断辅助)

较高

协调器工作流智能体

动态任务分解+调度

最高

复杂业务(如商业智能分析系统)

极高

评估优化工作流智能体

迭代优化+反馈修正

最高

极高

质量敏感场景(如法律文件生成)

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

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

--3.路由工作流智能体

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

--3.1实现流程

流程说明:

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

--3.2实现分析

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

--3.2.1定义类型枚举

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

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了:

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(String question) {
        return List.of();
    }

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

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

}
--3.2.3编写抽象类
package com.tianji.aigc.agent;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ReflectUtil;
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.client.advisor.AbstractChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import reactor.core.publisher.Flux;

import java.util.Map;
import java.util.Optional;
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是确保线程安全
    // TODO 目前的版本暂时用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();
        // 大模型输出内容的缓存器,用于在输出中断后的数据存储
        StringBuilder outputBuilder = new StringBuilder();

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

        return this.getChatClientRequest(sessionId, requestId, question)
                .stream()
                .chatResponse()
                .doFirst(() -> {
                    //输出开始,标记正在输出
                    GENERATE_STATUS.put(sessionId, true);
                })
                .doOnComplete(() -> {
                    //输出结束,清除标记
                    GENERATE_STATUS.remove(sessionId);
                })
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 错误时清除标记
                .doOnCancel(() -> {
                    // 当输出被取消时,保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(sessionId, outputBuilder.toString());
                })
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false)) // 只输出标记为true的流
                .map(chatResponse -> {
                    // 对于响应结果进行处理,如果是最后一条数据,就把此次消息id放到内存中
                    // 主要用于存储消息数据到 redis中,可以根据消息di获取的请求id,再通过请求id就可以获取到参数列表了
                    // 从而解决,在历史聊天记录中没有外参数的问题
                    var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
                    if (StrUtil.equals(Constant.STOP, finishReason)) {
                        var messageId = ((ChatResponseMetadata) ReflectUtil.getFieldValue(chatResponse, Constant.Chats.CHAT_RESPONSE_METADATA)).getId();
                        ToolResultHolder.put(messageId, Constant.REQUEST_ID, requestId);
                    }
                    // 获取大模型的输出的内容
                    String 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); // 清除参数列表
                        // 响应给前端的参数数据
                        ChatEventVO 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(question)).params(this.advisorParams(sessionId, requestId,question)))
                .tools(this.tools())
                .toolContext(this.toolContext(sessionId, requestId))
                .user(question);
    }

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

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

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

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

--3.3路由智能体

--3.3.1 系统提示词

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

# 角色
天机AI意图分析师

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

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

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

## 示例
输入:20岁本科想学Java → RECOMMEND  
输入:现在要下单 → BUY  
输入:你好 → 您好!有什么可以帮您?  
输入:今天天气 → 抱歉我只处理课程相关问题
--3.3.2读取配置

改造之前的AIProperties代码

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中增加配置:

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

加载配置

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编写路由智能体
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;

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

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

--3.4课程推荐智能体

--3.4.1系统提示词

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

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

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

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

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

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

加载配置.....

--3.4.3编写课程推荐智能体
package com.tianji.aigc.agent;

import cn.hutool.core.map.MapUtil;
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.QuestionAnswerAdvisor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
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;
    /**
     * 获取智能体类型标识
     *
     * @return 代理类型枚举值(如:ROUTE、RECOMMEND等)
     */
    @Override
    public AgentTypeEnum getAgentType() {
        return AgentTypeEnum.RECOMMEND;
    }

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

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

    /**
     * 创建并返回一个工具上下文的空Map对象。
     *
     * @param sessionId 会话标识符
     * @param requestId 请求标识符
     * @return 默认返回一个空的Map对象,子类可以覆盖重写该方法以返回自定义的工具上下文。
     */
    @Override
    public Map<String, Object> toolContext(String sessionId, String requestId) {
        var userId = UserContext.getUser();
        return MapUtil.<String,Object>builder()//设置tool列表
                .put(Constant.USER_ID,userId)//设置用户id参数
                .put(Constant.REQUEST_ID,requestId)//设置请求id参数
                .build();
    }

    /**
     * Advisor列表,默认返回空对象
     */
    @Override
    public List<Advisor> advisors(String question) {
        var searchRequest  = SearchRequest.builder().query(question).topK(3).build();
        return List.of(new QuestionAnswerAdvisor(vectorStore,searchRequest));
    }


}
--3.4.4测试用例
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 = "930f82c7fd1b4d8aa9dc761bb92f7840";
        UserContext.setUser(2L);
        Flux<ChatEventVO> flux = recommendAgent.processStream(question, sessionId);
        flux.subscribe(System.out::println);
        //阻塞主线程,防止主线程结束,子线程终止
        Thread.sleep(1000000);
    }
}

运行结果:

能正确推荐课程了

--3.5课程购买(预下单)智能体

--3.5.1系统提示词
# 角色说明

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

## 技能要求

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

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

修改aiproperties--修改application.yml----在systemPromptConfig中加载配置

.......

--3.5.3编写课程购买智能体
package com.tianji.aigc.agent;

import cn.hutool.core.map.MapUtil;
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;

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

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

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

    /**
     * 创建并返回一个工具上下文的空Map对象。
     *
     * @param sessionId 会话标识符
     * @param requestId 请求标识符
     * @return 默认返回一个空的Map对象,子类可以覆盖重写该方法以返回自定义的工具上下文。
     */
    @Override
    public Map<String, Object> toolContext(String sessionId, String requestId) {
        var userId = UserContext.getUser();
        return MapUtil.<String, Object>builder() // 设置tool列表
                .put(Constant.USER_ID, userId) // 设置用户id参数
                .put(Constant.REQUEST_ID, requestId) // 设置请求id参数
                .build();
    }
}
--3.5.4测试用例
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 = "930f82c7fd1b4d8aa9dc761bb92f7840";
        UserContext.setUser(2L);
        Flux<ChatEventVO> flux = buyAgent.processStream(question, sessionId);
        flux.subscribe(System.out::println);

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

}

--测试结果

--3.6课程咨询智能体

--3.6.1老三样

系统提示词

# 角色说明

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

## 技能:课程咨询

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

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

改aiproperties,改application.yml,改systemPromptConfig

--3.6.2编写课程咨询智能体
package com.tianji.aigc.agent;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
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.QuestionAnswerAdvisor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
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(String question) {
        var searchRequest = SearchRequest.builder().query(question).topK(3).build();
        return List.of(new QuestionAnswerAdvisor(vectorStore, searchRequest));
    }

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

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

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

测试用例

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 = "930f82c7fd1b4d8aa9dc761bb92f7840";
        UserContext.setUser(2L);
        Flux<ChatEventVO> flux = consultAgent.processStream(question, sessionId);
        flux.subscribe(System.out::println);

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

}

测试结果

--3.7知识讲解智能体

--3.7.1老三样
# 角色说明

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

## 技能要求

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

## 限制条件

- 仅限回答与课程内容及IT知识点相关的问题。如果学员提出与课程或IT知识无关的问题,请告知其你只能回答相关问题,并鼓励他们提出课程或IT领域的疑问。
--3.7.2编写知识讲解智能体
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.3测试

测试用例

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 = "930f82c7fd1b4d8aa9dc761bb92f7840";
        UserContext.setUser(2L);
        Flux<ChatEventVO> flux = knowledgeAgent.processStream(question, sessionId);
        flux.subscribe(System.out::println);

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

}

结果

--3.8多智能体协调工作

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

--3.8.1 编写AgentServiceImpl实现类
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 reactor.core.publisher.Flux;

import java.util.Map;
@Service
@RequiredArgsConstructor
public class AgentServiceImpl implements ChatService {
    /**
     * 智能问答
     *
     * @param question  问题
     * @param sessionId 会话id
     * @return 响应流
     */
    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        //先通过路由智能体,分析用户的意图,再执行后面的逻辑
        String result = findAgentByType(AgentTypeEnum.ROUTE).process(question, sessionId);
        AgentTypeEnum agentTypeEnum = AgentTypeEnum.agentNameOf(result);
        Agent agent = findAgentByType(agentTypeEnum);
        if (agent == null) {
            //找不到对应的智能体,直接返回结果
            ChatEventVO chatEventVO = ChatEventVO.builder()
                    .eventData(result)
                    .eventType(ChatEventTypeEnum.DATA.getValue())
                    .build();
            return Flux.just(chatEventVO, AbstractAgent.STOP_EVENT);
        }
        //执行智能体的逻辑并返回结果
        return agent.processStream(question, sessionId);
    }

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

    /**
     * 通过智能体类型,找到对应的智能体实例
     *
     * @param agentTypeEnum 要查找的代理类型
     * @return 与给定类型匹配的Agent示例, 如果未找到或类型为null则返回null
     */
    private Agent findAgentByType(AgentTypeEnum agentTypeEnum) {
        if (agentTypeEnum == null) return null;
        Map<String, Agent> beans = SpringUtil.getBeansOfType(Agent.class);
        //遍历所有AgentBean查找匹配类型
        for (Agent agent : beans.values()) {
            if (agentTypeEnum == agent.getAgentType())
                return agent;
        }
        return null;
    }
}
--3.8.2注释原来的Chatserviceimpl

由于ChatService接口,已经有2个实现类,现在我们测试Agent版的实现,所以,需要把原ChatServiceImpl@Service注解去掉。

--3.8.3改造SpringAiConfig

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

    /**
     * 配置 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测试

--4.bug解决

--4.1bug说明

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

redis中也会记录

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

--4.2bug解决

不要给路由智能体添加会话记忆功能即可

--4.2.1新建一个不带MessageChatMemoryAdvisord的chatClient

在SpringAIConfig里新增一个专用Bean

/**
     * 路由智能体专用chatClient,不含会话记录
     * @param builder        ChatClient.Builder
     * @param loggerAdvisor 日志记录器
     * @return ChatClient
     */
    @Bean("routeChatClient")
    public ChatClient routeChatClient(ChatClient.Builder builder, Advisor loggerAdvisor) {
        return builder
                .defaultAdvisors(loggerAdvisor) // 不加 messageChatMemoryAdvisor
                .build();
    }
--4.2.2让AbstractAgent可被子类切换ChatClient

把 AbstractAgent 里 getChatClientRequest 改成用一个可覆写的方法:

先增加一个可复写的方法

protected ChatClient getChatClient() { return this.chatClient; }

然后更改getChatClientRequest方法

chatClient 改成 getChatClient()

private ChatClient.ChatClientRequestSpec getChatClientRequest(String sessionId, String requestId, String question) {
        return getChatClient().prompt()
                .system(promptSystem -> promptSystem.text(this.systemMessage()).params(this.systemMessageParams()))
                .advisors(advisor ->
                        advisor.advisors(this.advisors(question)).params(this.advisorParams(sessionId, requestId, question)))
                .tools(this.tools())
                .toolContext(this.toolContext(sessionId, requestId))
                .user(question);
    }
--4.2.3在Route智能体的具体实现里复写采用无记忆的Client
@Resource(name = "routeChatClient")
private ChatClient routeChatClient;

@Override
protected ChatClient getChatClient() {
    return routeChatClient;
}

--此时我们发现,当触发流式取消时,还是会落入历史,那么我们在取消时判断一下是不是route,然后跳过,不过我们发现,前端等待输出时是不能按取消按钮的,所以这点好像可以忽视,不过保险起见还是做一下,因为也可能有超时、断连、或后续前端改动触发 doOnCancel,加这条保护不会有副作用,且能避免路由内容落历史

--4.2.4修改AbstractAgent的保存停止输出记录的代码
/**
     * 保存停止输出的记录
     *
     * @param sessionId 会话id
     * @param content   大模型输出的内容
     */
    private void saveStopHistoryRecord(String sessionId, String content) {
        if (getAgentType() == AgentTypeEnum.ROUTE) return;
        String conversationId = ChatService.getConversationId(sessionId);
        this.chatMemory.add(conversationId, new AssistantMessage(content));
    }
--4.2.5测试

测试时发现,会出userid丢失的bug,因为 UserContext 在 reactive 线程里丢了
saveStopHistoryRecord 里又调用 ChatService.getConversationId(sessionId),它用 UserContext.getUser() 拼 conversationId,结果变成 "null_xxx",在 RabbitPersistChatMemory.buildEvent 里 Long.parseLong("null") 就出异常了

改动:

  • 不要在异步/取消回调里依赖 UserContext,提前算好 conversationId 传进去。
public Flux<ChatEventVO> processStream(String question, String sessionId) {
    var userId = UserContext.getUser();
    var conversationId = userId + "_" + sessionId;

    return this.getChatClientRequest(sessionId, requestId, question)
            ...
            .doOnCancel(() -> saveStopHistoryRecord(conversationId, outputBuilder.toString()));
}

private void saveStopHistoryRecord(String conversationId, String content) {
    if (getAgentType() == AgentTypeEnum.ROUTE) return;
    this.chatMemory.add(conversationId, new AssistantMessage(content));
}

OK,历史记录很干净,并且中断保存也正常了

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐