多Agent编排模块深度解析

1.核心组件关系

创建了一个OrchestratorAgent用来管理多Agent,其中实现了process方法,process的主要流程:

1)获取用户上下文(UserContextProvider)

java 复制代码
 UserContext context = userContextProvider.getContext(token);
        if (context == null) {
            return "无法获取用户信息,请重新登录";
        }

        log.debug("用户上下文: userId={}, city={}, school={}", context.getUserId(), context.getCity(), context.getSchool());

以下是UserContextProvider的具体展开:

java 复制代码
/**
 * 用户上下文提供者
 * 从 JWT 解析用户信息,供 Orchestrator 和各 Agent 使用
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class UserContextProvider {

    private final UserMapper userMapper;
    private final JwtUtil jwtUtil;
    //获取用户token
    public UserContext getContext(HttpServletRequest request) {
        String token = extractToken(request);
        if (token == null) {
            log.warn("未获取到 Token");
            return null;
        }
        return getContext(token);
    }
    //根据token解析用户id,并且查询出用户的基本信息

    public UserContext getContext(String token) {
        try {
            if (token.startsWith("Bearer ")) {
                token = token.substring(7);
            }

            Long userId = jwtUtil.getUserId(token);
            if (userId == null) {
                log.warn("Token 解析失败,无法获取用户ID");
                return null;
            }

            User user = userMapper.selectById(userId);
            if (user == null) {
                log.warn("用户不存在: userId={}", userId);
                return null;
            }

            UserContext context = new UserContext();
            context.setUserId(userId);
            context.setNickname(user.getNickname());
            context.setCity(user.getCity());
            context.setSchool(user.getSchool());
            context.setSemesterStart(user.getSemesterStart() != null
                    ? user.getSemesterStart().toString() : "2025-03-03");
            context.setHeight(user.getHeight());
            context.setWeight(user.getWeight());
            context.setTastePreference(user.getTastePreference());
            context.setToken(token);

            log.debug("获取用户上下文: userId={}, city={}, school={}", userId, context.getCity(), context.getSchool());
            return context;

        } catch (Exception e) {
            log.error("解析用户上下文失败", e);
            return null;
        }
    }

    private String extractToken(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader;
        }
        return null;
    }
}

2)意图识别(IntentAnalyzer.analyze)

java 复制代码
 List<Task> tasks = intentAnalyzer.analyze(message, context, files);
        log.debug("意图分析结果: 共{}个任务", tasks.size());

        if (tasks.isEmpty()) {
            log.info("意图分析无结果,尝试ReAct推理模式");
            try {
                return reActExecutor.execute(message, context, sessionId, files);
            } catch (Exception e) {
                log.warn("ReAct执行失败,返回兜底: {}", e.getMessage());
                return "抱歉,我暂时无法理解您的问题,请换个说法试试?";
            }
        }

以下是具体的intentAnalyzer模块,基于Few-shot对用户输入的消息进行意图分析,具体流程

1)用户输入消息;

2)使用buildIntentPrompt()组装Prompt,包含多种Agent的类型定义,用户上下文,文件信息以及输出格式示例;

java 复制代码
 private String buildIntentPrompt(String message, UserContext context, Map<String, String> files) {
        StringBuilder prompt = new StringBuilder();
        prompt.append("你是青途AI助手的意图分析器。分析用户消息,识别用户想要执行的任务。\n\n");

        prompt.append("【可用Agent类型】(一个消息可能包含多个意图,需要分解为多个任务)\n");
        prompt.append("- weather: 天气查询(如'今天天气怎么样'、'明天要下雨吗'、'这周天气如何')\n");
        prompt.append("- expense: 记账创建/查询(如'记账花了25'、'今天花了多少'、'这周消费分析')\n");
        prompt.append("- course: 课程导入/查询(如'帮我记录课程'、'上传了课表'、'明天有什么课')\n");
        prompt.append("- profile: 个人信息修改/查询(如'帮我修改身高体重'、'我的资料是什么')\n");
        prompt.append("- note: 笔记生成/查询(如'帮我生成笔记'、'今天学了什么'、'查看我的笔记')\n");
        prompt.append("- calorie: 卡路里记录/查询(如'中午吃了牛肉面'、'今天摄入多少卡路里'、'饮食分析')\n");
        prompt.append("- search: 联网搜索(如'搜索学校新闻'、'查询最新消息'、'帮我查一下这个问题')\n");
        prompt.append("- school: 学校知识问答(如'学校有什么活动'、'图书馆开放时间'、'陕西理工大学通知')\n");
        prompt.append("- analysis: 数据分析(如'这个月消费怎么样'、'我的饮食健康吗'、'帮我分析学习情况')\n");
        prompt.append("- recommend: 个性化推荐(如'食堂推荐'、'根据我情况推荐菜品'、'推荐学习计划')\n");
        prompt.append("- remind: 提醒设置(如'提醒我明天上课'、'设置早起提醒'、'半小时后提醒我喝水')\n");
        prompt.append("- schedule: 日程管理(如'帮我安排这周的学习'、'空闲时间'、'帮我规划时间')\n");
        prompt.append("- document: 文档处理(如'上传了课件帮我总结'、'解读这份报告'、'帮我改作业')\n");
        prompt.append("- chat: 普通对话(问候、闲聊等,以上都不匹配时)\n\n");

        prompt.append("【用户上下文】\n");
        prompt.append("- 用户ID: ").append(context.getUserId()).append("\n");
        prompt.append("- 城市: ").append(context.getCity() != null ? context.getCity() : "未知").append("\n");
        prompt.append("- 学校: ").append(context.getSchool() != null ? context.getSchool() : "未知").append("\n\n");

        prompt.append("【文件信息】\n");
        if (files != null && !files.isEmpty()) {
            for (Map.Entry<String, String> entry : files.entrySet()) {
                prompt.append("- 文件名: ").append(entry.getKey());
                prompt.append(", 类型: ").append(getFileType(entry.getKey())).append("\n");
            }
        } else {
            prompt.append("- 无文件上传\n");
        }
        prompt.append("\n");

        prompt.append("【用户消息】\n").append(message).append("\n\n");

        prompt.append("【输出要求】\n");
        prompt.append("请输出JSON格式(不要输出任何其他内容):\n");
        prompt.append("{\n");
        prompt.append("  \"tasks\": [\n");
        prompt.append("    {\"agent\": \"agent类型\", \"action\": \"具体动作\", \"params\": {...}, \"description\": \"任务描述\", \"taskId\": \"任务序号(0,1,2...)\"}\n");
        prompt.append("  ]\n");
        prompt.append("}\n\n");

        prompt.append("【单任务示例】(无依赖):\n");
        prompt.append("用户: '帮我记账,花了25元' → {\"tasks\": [{\"agent\": \"expense\", \"action\": \"create\", \"params\": {\"amount\": 25, \"category\": \"饮食\"}, \"description\": \"记账25元\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '今天天气怎么样' → {\"tasks\": [{\"agent\": \"weather\", \"action\": \"query\", \"params\": {\"city\": \"汉中\"}, \"description\": \"查询天气\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '你好啊' → {\"tasks\": [{\"agent\": \"chat\", \"action\": \"chat\", \"params\": {}, \"description\": \"普通对话\", \"taskId\": \"0\"}]}\n\n");

        prompt.append("【多任务依赖示例】(有依赖关系的任务):\n");
        prompt.append("用户: '帮我查下天气,然后推荐穿搭' → {\"tasks\": [\n");
        prompt.append("  {\"agent\": \"weather\", \"action\": \"query\", \"params\": {}, \"description\": \"查询天气\", \"taskId\": \"0\", \"dependencies\": []},\n");
        prompt.append("  {\"agent\": \"recommend\", \"action\": \"outfit\", \"params\": {}, \"description\": \"根据天气推荐穿搭\", \"taskId\": \"1\", \"dependencies\": [\"0\"]}\n");
        prompt.append("]}\n");
        prompt.append("说明:第2个任务(recommend)依赖第1个任务(weather),需要等weather查完再执行\n\n");

        prompt.append("用户: '先查下我的消费记录,然后分析一下' → {\"tasks\": [\n");
        prompt.append("  {\"agent\": \"expense\", \"action\": \"query\", \"params\": {}, \"description\": \"查询消费记录\", \"taskId\": \"0\", \"dependencies\": []},\n");
        prompt.append("  {\"agent\": \"analysis\", \"action\": \"expense_analysis\", \"params\": {}, \"description\": \"分析消费数据\", \"taskId\": \"1\", \"dependencies\": [\"0\"]}\n");
        prompt.append("]}\n\n");

        prompt.append("用户: '查下明天天气和课程安排' → {\"tasks\": [\n");
        prompt.append("  {\"agent\": \"weather\", \"action\": \"forecast\", \"params\": {\"dayOffset\": 1}, \"description\": \"明天天气预报\", \"taskId\": \"0\", \"dependencies\": []},\n");
        prompt.append("  {\"agent\": \"course\", \"action\": \"query\", \"params\": {}, \"description\": \"查询课程表\", \"taskId\": \"1\", \"dependencies\": []}\n");
        prompt.append("]}\n");
        prompt.append("说明:天气和课程没有依赖关系,可以并行执行\n\n");

        prompt.append("用户: '分析一下我的饮食,然后推荐晚餐' → {\"tasks\": [\n");
        prompt.append("  {\"agent\": \"analysis\", \"action\": \"diet_analysis\", \"params\": {}, \"description\": \"分析饮食情况\", \"taskId\": \"0\", \"dependencies\": []},\n");
        prompt.append("  {\"agent\": \"recommend\", \"action\": \"food\", \"params\": {}, \"description\": \"推荐晚餐\", \"taskId\": \"1\", \"dependencies\": [\"0\"]}\n");
        prompt.append("]}\n\n");

        prompt.append("【依赖识别规则】:\n");
        prompt.append("- 有依赖:后面任务需要前面任务的结果,如'先...再...'、'...之后...'\n");
        prompt.append("- 无依赖:多个独立任务可以并行,如'...和...'、'...以及...'\n");
        prompt.append("- dependencies格式:[\"依赖的taskId\"],无依赖时为[]\n");
        prompt.append("- taskId从0开始,按执行顺序编号\n\n");

        prompt.append("【金额提取规则】:\n");
        prompt.append("- 数字直接提取:'13元' → amount:13, '25块' → amount:25, '50元' → amount:50\n");
        prompt.append("- 中文数字转换:'一'→1, '五'→5, '十'→10, '十三'→13, '二十五'→25\n");
        prompt.append("- 上下文推断类别:从关键词推断,如'吃饭'→'饮食', '坐车'→'交通', '买书'→'购物'\n\n");

        prompt.append("【身体数据提取规则】:\n");
        prompt.append("- 体重提取:'体重71' → weight:71, '71kg' → weight:71, '体重71公斤' → weight:71\n");
        prompt.append("- 身高提取:'身高175' → height:175, '175cm' → height:175, '身高175厘米' → height:175\n");
        prompt.append("- 只保留数字部分,单位自动忽略\n\n");

        prompt.append("【卡路里记录示例】:\n");
        prompt.append("用户: '中午吃了牛肉面' → {\"tasks\": [{\"agent\": \"calorie\", \"action\": \"create\", \"params\": {\"food\": \"牛肉面\", \"mealType\": \"lunch\"}, \"description\": \"记录午餐\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '记录一下晚餐,吃了炒饭' → {\"tasks\": [{\"agent\": \"calorie\", \"action\": \"create\", \"params\": {\"food\": \"炒饭\", \"mealType\": \"dinner\"}, \"description\": \"记录晚餐\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '早上吃了一个鸡蛋' → {\"tasks\": [{\"agent\": \"calorie\", \"action\": \"create\", \"params\": {\"food\": \"鸡蛋\", \"mealType\": \"breakfast\"}, \"description\": \"记录早餐\", \"taskId\": \"0\"}]}\n\n");

        prompt.append("【笔记生成示例】:\n");
        prompt.append("用户: '帮我生成今日课程的笔记' → {\"tasks\": [{\"agent\": \"note\", \"action\": \"generate\", \"params\": {\"type\": \"note\", \"includeTodayCourses\": true}, \"description\": \"生成今日课程笔记\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '生成学习笔记' → {\"tasks\": [{\"agent\": \"note\", \"action\": \"generate\", \"params\": {\"type\": \"note\"}, \"description\": \"生成学习笔记\", \"taskId\": \"0\"}]}\n\n");

        prompt.append("【资料修改示例】:\n");
        prompt.append("用户: '修改我的体重为71' → {\"tasks\": [{\"agent\": \"profile\", \"action\": \"update\", \"params\": {\"weight\": 71}, \"description\": \"修改体重\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '修改身高175,体重70' → {\"tasks\": [{\"agent\": \"profile\", \"action\": \"update\", \"params\": {\"height\": 175, \"weight\": 70}, \"description\": \"修改身高体重\", \"taskId\": \"0\"}]}\n\n");

        prompt.append("【新增意图示例】:\n");
        prompt.append("用户: '这个月消费怎么样?分析一下' → {\"tasks\": [{\"agent\": \"analysis\", \"action\": \"expense_analysis\", \"params\": {}, \"description\": \"消费分析\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '根据我的情况推荐食堂菜品' → {\"tasks\": [{\"agent\": \"recommend\", \"action\": \"food\", \"params\": {}, \"description\": \"食堂菜品推荐\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '提醒我明天上午9点上课' → {\"tasks\": [{\"agent\": \"remind\", \"action\": \"create\", \"params\": {\"time\": \"明天9点\", \"content\": \"上课\"}, \"description\": \"设置上课提醒\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '半小时后提醒我喝水' → {\"tasks\": [{\"agent\": \"remind\", \"action\": \"create\", \"params\": {\"delayMinutes\": 30, \"content\": \"喝水\"}, \"description\": \"设置喝水提醒\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '帮我安排这周的学习时间' → {\"tasks\": [{\"agent\": \"schedule\", \"action\": \"plan_week\", \"params\": {}, \"description\": \"安排本周学习\", \"taskId\": \"0\"}]}\n");
        prompt.append("用户: '上传了课件帮我总结' → {\"tasks\": [{\"agent\": \"document\", \"action\": \"summarize\", \"params\": {\"fileName\": \"课件.pdf\", \"fileType\": \"pdf\"}, \"description\": \"总结课件\", \"taskId\": \"0\"}]}\n");

        return prompt.toString();
    }

3)执行callLLM()方法,调用通义千问API,解析JSON响应:

java 复制代码
private String callLLM(String prompt) {
        String url = dashScopeConfig.getBaseUrl() + "/chat/completions";

        Map<String, Object> body = new HashMap<>();
        body.put("model", dashScopeConfig.getModel());
        body.put("messages", List.of(Map.of("role", "user", "content", prompt)));
        body.put("enable_search", false);
        body.put("temperature", 0.3);

        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + dashScopeConfig.getApiKey());
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
        String response = restTemplate.postForObject(url, entity, String.class);

        try {
            JsonNode root = objectMapper.readTree(response);
            JsonNode choices = root.path("choices");
            if (choices.isArray() && choices.size() > 0) {
                return choices.get(0).path("message").path("content").asText();
            }
        } catch (Exception e) {
            log.error("解析LLM响应失败", e);
        }
        return null;
    }

4)执行parseIntentResponse()方法,提取 tasks[] 数组,每个Task包含agent(Agent的类型),action(具体动作),params(参数),dependencies(依赖关系);

java 复制代码
@SuppressWarnings("unchecked")
    private List<Task> parseIntentResponse(String llmResponse, UserContext context) {
        List<Task> tasks = new ArrayList<>();
        Map<String, String> taskIdMap = new HashMap<>();
        Set<String> agentsNeedingRag = Set.of("school", "rag", "search", "chat", "document");

        if (llmResponse == null || llmResponse.isBlank()) {
            log.warn("LLM返回为空,使用默认chat任务");
            Task task = Task.of(INTENT_CHAT, "chat", Map.of("message", ""));
            task.setNeedRag(true);
            task.setIntentType(Task.INTENT_TYPE_SINGLE);
            tasks.add(task);
            return tasks;
        }

        try {
            String cleaned = cleanJsonResponse(llmResponse);

            int startIdx = cleaned.indexOf('{');
            int endIdx = cleaned.lastIndexOf('}');
            if (startIdx >= 0 && endIdx > startIdx) {
                String jsonStr = cleaned.substring(startIdx, endIdx + 1);
                Map<String, Object> parsed = objectMapper.readValue(jsonStr, Map.class);

                List<Map<String, Object>> taskList = (List<Map<String, Object>>) parsed.getOrDefault("tasks", new ArrayList<>());

                for (Map<String, Object> taskMap : taskList) {
                    String agent = (String) taskMap.getOrDefault("agent", INTENT_CHAT);
                    String action = (String) taskMap.getOrDefault("action", "chat");
                    Map<String, Object> params = (Map<String, Object>) taskMap.getOrDefault("params", new HashMap<>());
                    String description = (String) taskMap.getOrDefault("description", "");
                    String taskId = (String) taskMap.getOrDefault("taskId", "");

                    Task task = Task.of(agent, action, params, description);
                    taskIdMap.put(taskId, task.getTaskId());

                    task.setIntentType(taskList.size() == 1 ? Task.INTENT_TYPE_SINGLE : Task.INTENT_TYPE_MULTI);
                    task.setNeedRag(agentsNeedingRag.contains(agent.toLowerCase()));

                    tasks.add(task);
                }

                for (int i = 0; i < tasks.size(); i++) {
                    Task task = tasks.get(i);
                    Map<String, Object> taskMap = taskList.get(i);

                    Object depsObj = taskMap.get("dependencies");
                    if (depsObj instanceof List) {
                        List<String> depIds = new ArrayList<>();
                        for (Object depId : (List<?>) depsObj) {
                            String originalId = depId.toString();
                            String mappedId = taskIdMap.get(originalId);
                            if (mappedId != null) {
                                depIds.add(mappedId);
                            }
                        }
                        task.setDependencies(depIds);
                    }
                }

                if (context != null) {
                    for (Task task : tasks) {
                        Map<String, Object> params = task.getParameters();
                        params.put("_userId", context.getUserId());
                        params.put("_city", context.getCity());
                        params.put("_school", context.getSchool());
                        params.put("_semesterStart", context.getSemesterStart());
                    }
                }

                log.info("[Intent] 意图识别完成: tasks={}, intentTypes={}, ragFlags={}",
                        tasks.size(),
                        tasks.stream().map(Task::getIntentType).collect(Collectors.joining(",")),
                        tasks.stream().map(t -> t.isNeedRag() ? "RAG" : "DIRECT").collect(Collectors.joining(",")));
            }
        } catch (Exception e) {
            log.error("解析意图响应失败: {}", e.getMessage());
            Task task = Task.of(INTENT_CHAT, "chat", Map.of("message", llmResponse));
            task.setNeedRag(true);
            task.setIntentType(Task.INTENT_TYPE_SINGLE);
            tasks.add(task);
        }

        if (tasks.isEmpty()) {
            Task task = Task.of(INTENT_CHAT, "chat", Map.of("message", llmResponse));
            task.setNeedRag(true);
            task.setIntentType(Task.INTENT_TYPE_SINGLE);
            tasks.add(task);
        }

        return tasks;
    }

    private String cleanJsonResponse(String response) {
        if (response == null) return "";

        String cleaned = response.trim();

        cleaned = cleaned.replaceAll("```json\\s*", "");
        cleaned = cleaned.replaceAll("```\\s*", "");
        cleaned = cleaned.replaceAll("\\\\left\\{", "{");
        cleaned = cleaned.replaceAll("\\\\right\\}", "}");
        cleaned = cleaned.replaceAll("\\\\left\\[", "[");
        cleaned = cleaned.replaceAll("\\\\right\\]", "]");
        cleaned = cleaned.replaceAll("\\\\\"", "\"");
        cleaned = cleaned.replaceAll("\\\\n", "\n");
        cleaned = cleaned.replaceAll("\\\\t", "\t");

        return cleaned;
    }

2.1 如果是单任务,则直接路由到相应的子Agent(这里还会根据用户意图去判断是否需要RAG增强检索,这个我们后面说);

java 复制代码
if (tasks.size() == 1) {
            Task task = tasks.get(0);
            task.getParameters().put("sessionId", sessionId);

            if (task.isNeedRag()) {
                log.info("[Orchestrator] 路由到RAG Agent: agent={}, needRag={}", task.getAgent(), task.isNeedRag());
                ResultMessage result = ragAgent.execute("query", context, task.getParameters());
                return result.isSuccess() ? String.valueOf(result.getResult()) : "知识库查询失败";
            }

            switch (task.getAgent().toLowerCase()) {
                case "weather" -> {
                    ResultMessage result = weatherAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "天气查询失败";
                }
                case "expense" -> {
                    ResultMessage result = expenseAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "记账处理失败";
                }
                case "course" -> {
                    ResultMessage result = courseAgent.execute(task.getAction(), context, task.getParameters(), files);
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "课程处理失败";
                }
                case "profile" -> {
                    ResultMessage result = profileAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "资料处理失败";
                }
                case "note" -> {
                    ResultMessage result = noteAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "笔记处理失败";
                }
                case "calorie" -> {
                    ResultMessage result = calorieAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "卡路里处理失败";
                }
                case "analysis" -> {
                    log.info("[Orchestrator] 路由到分析Agent: action={}", task.getAction());
                    ResultMessage result = analysisAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "分析处理失败";
                }
                case "remind" -> {
                    log.info("[Orchestrator] 路由到提醒Agent: action={}", task.getAction());
                    ResultMessage result = remindAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "提醒设置失败";
                }
                case "schedule" -> {
                    log.info("[Orchestrator] 路由到日程Agent: action={}", task.getAction());
                    ResultMessage result = scheduleAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "日程处理失败";
                }
                case "recommend" -> {
                    log.info("[Orchestrator] 路由到推荐Agent: action={}", task.getAction());
                    ResultMessage result = recommendAgent.execute(task.getAction(), context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "推荐处理失败";
                }
                case "chat" -> {
                    ResultMessage result = chatAgent.execute("chat", context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "对话处理失败";
                }
                default -> {
                    log.warn("[Orchestrator] 未知Agent类型: {}, 尝试RAG兜底", task.getAgent());
                    ResultMessage result = ragAgent.execute("query", context, task.getParameters());
                    return result.isSuccess() ? String.valueOf(result.getResult()) : "处理失败";
                }
            }
        }

2.2 如果是多任务,则首先根据拓扑排序,判断各个任务之间的串/并行关系,然后进行执行并聚合结果;

2.2.1 首先实现拓扑排序并且判断有没有循环依赖:

java 复制代码
/**
 * 任务拓扑排序器
 * 基于 Kahn 算法实现有向无环图(DAG)的拓扑排序
 */
@Slf4j
public class TaskTopologicalSort {

    /**
     * 对任务列表进行拓扑排序
     * @param tasks 任务列表
     * @return 排序后的任务列表(按依赖顺序)
     */
    public static List<Task> sort(List<Task> tasks) {
        if (tasks == null || tasks.isEmpty()) {
            return tasks;
        }

        if (tasks.size() == 1) {
            return tasks;
        }

        Map<String, Task> taskMap = tasks.stream()
                .collect(Collectors.toMap(Task::getTaskId, t -> t));

        Map<String, Integer> inDegree = new HashMap<>();
        Map<String, List<String>> adjacency = new HashMap<>();

        for (Task task : tasks) {
            inDegree.put(task.getTaskId(), 0);
            adjacency.put(task.getTaskId(), new ArrayList<>());
        }

        for (Task task : tasks) {
            List<String> deps = task.getDependencies();
            if (deps != null) {
                for (String dep : deps) {
                    if (taskMap.containsKey(dep)) {
                        adjacency.get(dep).add(task.getTaskId());
                        inDegree.merge(task.getTaskId(), 1, Integer::sum);
                    }
                }
            }
        }

        Queue<String> queue = new LinkedList<>();
        for (Map.Entry<String, Integer> entry : inDegree.entrySet()) {
            if (entry.getValue() == 0) {
                queue.offer(entry.getKey());
            }
        }

        List<Task> sorted = new ArrayList<>();
        while (!queue.isEmpty()) {
            String taskId = queue.poll();
            sorted.add(taskMap.get(taskId));

            for (String neighbor : adjacency.get(taskId)) {
                int newDegree = inDegree.get(neighbor) - 1;
                inDegree.put(neighbor, newDegree);
                if (newDegree == 0) {
                    queue.offer(neighbor);
                }
            }
        }

        if (sorted.size() != tasks.size()) {
            log.warn("拓扑排序检测到循环依赖,保持原顺序执行");
            return tasks;
        }

        log.debug("拓扑排序完成: {}", sorted.stream()
                .map(t -> t.getAgent() + ":" + t.getTaskId().substring(0, 8))
                .collect(Collectors.joining(" -> ")));

        return sorted;
    }

    /**
     * 检查是否存在循环依赖
     * @param tasks 任务列表
     * @return true 如果存在循环依赖
     */
    public static boolean hasCycle(List<Task> tasks) {
        if (tasks == null || tasks.isEmpty()) {
            return false;
        }

        Map<String, Task> taskMap = tasks.stream()
                .collect(Collectors.toMap(Task::getTaskId, t -> t));

        Set<String> visited = new HashSet<>();
        Set<String> recursionStack = new HashSet<>();

        for (Task task : tasks) {
            if (hasCycleUtil(task.getTaskId(), taskMap, visited, recursionStack)) {
                return true;
            }
        }

        return false;
    }

    private static boolean hasCycleUtil(String taskId, Map<String, Task> taskMap,
                                        Set<String> visited, Set<String> recursionStack) {
        if (recursionStack.contains(taskId)) {
            return true;
        }
        if (visited.contains(taskId)) {
            return false;
        }

        visited.add(taskId);
        recursionStack.add(taskId);

        Task task = taskMap.get(taskId);
        if (task != null && task.getDependencies() != null) {
            for (String dep : task.getDependencies()) {
                if (taskMap.containsKey(dep)) {
                    if (hasCycleUtil(dep, taskMap, visited, recursionStack)) {
                        return true;
                    }
                }
            }
        }

        recursionStack.remove(taskId);
        return false;
    }
}

2.2.2 接下来实现任务的串/并行判断和执行:

java 复制代码
 List<Task> sortedTasks = TaskTopologicalSort.sort(tasks);
        log.debug("拓扑排序后任务顺序: {}", sortedTasks.stream()
                .map(t -> t.getAgent() + ":" + t.getAction())
                .collect(Collectors.joining(" -> ")));

        Map<String, CompletableFuture<ResultMessage>> taskFutures = new LinkedHashMap<>();

        for (Task task : sortedTasks) {
            task.getParameters().put("sessionId", sessionId);
            CompletableFuture<ResultMessage> future = executeTaskWithDependencies(task, context, files, correlationId, taskFutures);
            taskFutures.put(task.getTaskId(), future);
        }

        List<CompletableFuture<ResultMessage>> futures = new ArrayList<>(taskFutures.values());
        List<ResultMessage> results = waitForResults(futures, correlationId);

        String response = resultAggregator.aggregate(results, message);

        long duration = System.currentTimeMillis() - startTime;
        log.info("Orchestrator处理完成: correlationId={}, 任务数={}, 耗时={}ms", correlationId, tasks.size(), duration);

        return response;
    }

    private CompletableFuture<ResultMessage> executeTaskWithDependencies(
            Task task,
            UserContext context,
            Map<String, String> files,
            String correlationId,
            Map<String, CompletableFuture<ResultMessage>> taskFutures) {

        CompletableFuture<ResultMessage> future = new CompletableFuture<>();

        CompletableFuture.runAsync(() -> {
            try {
                List<String> deps = task.getDependencies();
                if (deps != null && !deps.isEmpty()) {
                    log.debug("等待任务依赖完成: taskId={}, dependencies={}", task.getTaskId(), deps);
                    for (String depId : deps) {
                        CompletableFuture<ResultMessage> depFuture = taskFutures.get(depId);
                        if (depFuture != null) {
                            try {
                                depFuture.get(RESULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
                            } catch (Exception e) {
                                log.warn("依赖任务执行失败或超时: depId={}", depId);
                            }
                        }
                    }
                }

                ResultMessage result = executeTaskDirectly(task, context, files);
                future.complete(result);
            } catch (Exception e) {
                log.error("任务执行异常: taskId={}", task.getTaskId(), e);
                future.completeExceptionally(e);
            }
        }, orchestratorExecutor);

        return future;
    }

    private CompletableFuture<ResultMessage> executeTaskAsync(Task task, UserContext context, Map<String, String> files, String correlationId) {
        CompletableFuture<ResultMessage> future = new CompletableFuture<>();

        TaskMessage message = new TaskMessage();
        message.setTaskId(task.getTaskId());
        message.setAgent(task.getAgent());
        message.setAction(task.getAction());
        message.setUserId(context.getUserId());
        message.setUserName(context.getNickname());
        message.setCity(context.getCity());
        message.setSchool(context.getSchool());
        message.setSemesterStart(context.getSemesterStart());
        message.setParams(task.getParameters());
        message.setCorrelationId(correlationId);
        message.setFiles(files);

        pendingResults.put(task.getTaskId(), 

3) 结果聚合

java 复制代码
/**
 * 结果聚合器
 * 收集各 Agent 返回的结果,生成友好回复
 */
@Slf4j
@Component
public class ResultAggregator {

    public String aggregate(List<ResultMessage> results, String originalMessage) {
        if (results == null || results.isEmpty()) {
            return "处理完成,但没有返回结果。";
        }

        StringBuilder response = new StringBuilder();

        for (ResultMessage result : results) {
            String partial = formatResult(result);
            if (partial != null && !partial.isBlank()) {
                if (response.length() > 0) {
                    response.append("\n\n");
                }
                response.append(partial);
            }
        }

        return response.toString();
    }

    private String formatResult(ResultMessage result) {
        if (result == null) {
            return null;
        }

        if (!result.isSuccess() && !result.isFallback()) {
            return formatError(result);
        }

        return switch (result.getAgent().toLowerCase()) {
            case "weather" -> formatWeatherResult(result);
            case "expense" -> formatExpenseResult(result);
            case "course" -> formatCourseResult(result);
            case "profile" -> formatProfileResult(result);
            case "note" -> formatNoteResult(result);
            case "chat" -> formatChatResult(result);
            case "search" -> formatSearchResult(result);
            default -> formatDefaultResult(result);
        };
    }

    private String formatWeatherResult(ResultMessage result) {
        @SuppressWarnings("unchecked")
        Map<String, Object> data = (Map<String, Object>) result.getResult();
        if (data == null) return null;

        Object forecastsObj = data.get("forecasts");
        if (forecastsObj instanceof List) {
            return formatForecastResult(data);
        }

        String message = (String) data.getOrDefault("message", "");
        if (!message.isBlank()) {
            return message;
        }

        String city = (String) data.getOrDefault("city", "未知");
        String temp = String.valueOf(data.getOrDefault("temp", "--"));
        String text = (String) data.getOrDefault("text", "");
        String summary = (String) data.getOrDefault("summary", "");

        StringBuilder sb = new StringBuilder();
        sb.append("【").append(city).append("天气】\n");
        sb.append("温度: ").append(temp).append("°C ").append(text).append("\n");
        if (!summary.isBlank()) {
            sb.append(summary);
        }
        return sb.toString();
    }

    @SuppressWarnings("unchecked")
    private String formatForecastResult(Map<String, Object> data) {
        String city = (String) data.getOrDefault("city", "未知");
        List<DailyForecast> allForecasts = (List<DailyForecast>) data.get("forecasts");
        int dayOffset = 0;
        Object offsetObj = data.get("dayOffset");
        if (offsetObj != null) {
            try {
                dayOffset = Integer.parseInt(offsetObj.toString());
            } catch (NumberFormatException ignored) {}
        }

        List<DailyForecast> forecasts = allForecasts;
        if (dayOffset > 0 && forecasts.size() > dayOffset) {
            forecasts = forecasts.subList(dayOffset, forecasts.size());
        }

        StringBuilder sb = new StringBuilder();
        String prefix = dayOffset > 0 ? (dayOffset == 1 ? "明天" : (dayOffset == 2 ? "后天" : dayOffset + "天后")) : "";
        if (!prefix.isEmpty()) {
            sb.append("【").append(city).append(prefix).append("天气】\n");
        } else {
            sb.append("【").append(city).append("未来").append(forecasts.size()).append("天天气】\n");
        }

        for (DailyForecast f : forecasts) {
            sb.append(f.getDateDisplay());
            sb.append(":").append(f.getWeatherEmoji());
            sb.append(f.getTextDay());
            sb.append(" ").append(f.getTempLow()).append("-").append(f.getTempHigh()).append("°C");
            sb.append(" 紫外线:").append(f.getUvIndex());
            sb.append("\n");
        }
        return sb.toString();
    }

    private String formatExpenseResult(ResultMessage result) {
        @SuppressWarnings("unchecked")
        Map<String, Object> data = (Map<String, Object>) result.getResult();
        if (data == null) return null;

        if (result.isFallback()) {
            @SuppressWarnings("unchecked")
            Map<String, Object> mockExpense = (Map<String, Object>) data.get("mockExpense");
            if (mockExpense != null) {
                return "⚠️ 记账服务暂时不可用,模拟记录:消费 " + mockExpense.get("amount") + " 元(" + mockExpense.get("category") + ")" + mockExpense.get("note");
            }
            return "⚠️ " + data.getOrDefault("message", "记账失败");
        }

        String message = (String) data.getOrDefault("message", "");
        if (!message.isBlank()) {
            return message;
        }

        Object expenseId = data.get("expenseId");
        Object amount = data.get("amount");
        Object category = data.get("category");

        if (expenseId != null) {
            return "✅ 已为您记账:" + category + " " + amount + " 元";
        }
        return "✅ 记账完成";
    }

    private String formatCourseResult(ResultMessage result) {
        @SuppressWarnings("unchecked")
        Map<String, Object> data = (Map<String, Object>) result.getResult();
        if (data == null) return null;

        if (result.isFallback()) {
            return "⚠️ " + data.getOrDefault("message", "课程导入失败");
        }

        String message = (String) data.getOrDefault("message", "");
        if (!message.isBlank()) {
            return message;
        }

        Object imported = data.get("coursesImported");
        if (imported != null) {
            return "✅ 已导入 " + imported + " 门课程到您的课表";
        }
        return "✅ 课程处理完成";
    }

    private String formatProfileResult(ResultMessage result) {
        @SuppressWarnings("unchecked")
        Map<String, Object> data = (Map<String, Object>) result.getResult();
        if (data == null) return null;

        if (result.isFallback()) {
            return "⚠️ " + data.getOrDefault("message", "资料修改失败");
        }

        String message = (String) data.getOrDefault("message", "");
        if (!message.isBlank()) {
            return message;
        }

        Object updated = data.get("updatedFields");
        if (updated != null) {
            return "✅ 已更新个人资料:" + updated;
        }
        return "✅ 资料修改完成";
    }

    private String formatNoteResult(ResultMessage result) {
        @SuppressWarnings("unchecked")
        Map<String, Object> data = (Map<String, Object>) result.getResult();
        if (data == null) return null;

        if (result.isFallback()) {
            return "⚠️ " + data.getOrDefault("message", "笔记生成失败");
        }

        Object noteId = data.get("noteId");
        Object title = data.get("title");
        Object content = data.get("content");

        if (noteId != null && title != null) {
            return "✅ 已为您生成笔记:「" + title + "」\n" + content;
        }

        String contentStr = content != null ? content.toString() : "";
        if (!contentStr.isBlank()) {
            return "📝 笔记内容:\n" + contentStr;
        }

        return "✅ 笔记生成完成";
    }

    private String formatChatResult(ResultMessage result) {
        Object data = result.getResult();
        if (data == null) return null;
        return data.toString();
    }

    private String formatSearchResult(ResultMessage result) {
        Object data = result.getResult();
        if (data == null) return null;
        return data.toString();
    }

    private String formatError(ResultMessage result) {
        String error = result.getErrorMessage();
        if (error == null || error.isBlank()) {
            return "⚠️ " + result.getAgent() + " 执行失败";
        }
        return "⚠️ " + result.getAgent() + " 失败:" + error;
    }

    private String formatDefaultResult(ResultMessage result) {
        Object data = result.getResult();
        if (data == null) {
            return "✅ " + result.getAgent() + " 处理完成";
        }
        if (data instanceof String) {
            return (String) data;
        }
        if (data instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> mapData = (Map<String, Object>) data;
            String message = (String) mapData.getOrDefault("message", "");
            if (!message.isBlank()) {
                return message;
            }
        }
        return "✅ " + result.getAgent() + " 处理完成";
    }
}

4)兜底处理

当IntentAnalyzer执行失败时,会触发ReAct推理循环进行兜底,所谓的ReAct,就是让Agent边行动,边思考,对应的其余思想还有Plan-and-Execute,二者的区别在于,ReAct是一边行动一边思考,而Plan-and-Execute是先做计划,然后整体执行。**那么为什么不选择后者呢?**我的想法是,因为这是一个面向大学生的生活助手,边行动边思考能够解决大部分的问题,针对业务场景来说,不需要完成多么繁重的任务或者工程问题,解决的都是些日常的琐事,因此无需使用Plan-and-Execute。

ReAct的主要执行流程就是先Think,然后Action,最后Observe,然后重复循环,最终输出答案,当然,肯定不能无限循环下去,我设计的是最多循环五次。以下是prompt模板:

你是青途智伴AI助手,使用ReAct推理模式。

【可用工具】

  • weather: 查询天气

  • expense: 记账

...

【输出格式】(JSON)

{

"thought": "思考过程",

"action": "weather/chat/search...",

"params": {...},

"result": "工具返回结果"

}

2.消息队列的设计

引入消息队列的目的:

1)实现业务的异步解耦,编排器无需一直等待子Agent的执行完成;

2)流量削峰,平滑的处理高峰期;

3)故障隔离,其中某一个Agent出现故障不会影响其余Agent的执行;

4)可靠性保证:生产端开启确认机制,消费端开始手都ACK,以及死信队列可以保证消息不丢失。

2.1 队列配置

针对不同的Agent,设计了不同的消息队列,实现物理隔离

java 复制代码
// 交换机定义
public static final String EXCHANGE_AGENT = "agent.exchange";  // 业务交换机
public static final String EXCHANGE_RESULTS = "agent.results.exchange"; // 结果交换机
public static final String DLX_EXCHANGE = "dlx.exchange";  // 死信交换机

// 队列定义 (每个Agent独立队列)
public static final String QUEUE_WEATHER = "agent.weather";
public static final String QUEUE_EXPENSE = "agent.expense";
public static final String QUEUE_COURSE = "agent.course";
// ... 共13个Agent队列

同时配置了死信队列,当任务消费失败重试次数超过三次时,会进入死信队列,后续可由人工审核/定时任务重试/告警等解决方案处理:

java 复制代码
// 死信交换机
@Bean
public DirectExchange deadLetterExchange() {
    return new DirectExchange(DLX_EXCHANGE);
}

// 为每个业务队列创建死信队列
@Bean
public Queue dlqWeather() {
    return QueueBuilder.durable("agent.weather" + DLQ_SUFFIX).build();
}
相关推荐
心中有国也有家5 小时前
CANN 学习新范式:cann-learning-hub 如何让昇腾入门不再「劝退」
人工智能·经验分享·笔记·学习·算法
一只大袋鼠5 小时前
SpringBoot 入门学习笔记(三)Web 开发下篇
spring boot·笔记·学习
承渊政道5 小时前
Linux系统学习【进程概念从入门到深入理解】
linux·服务器·笔记·学习·ubuntu·系统架构·bash
Roselind_Yi6 小时前
池化对比:CNN池化 VS Java线程池
java·人工智能·经验分享·笔记·深度学习·神经网络·cnn
心中有国也有家6 小时前
hixl:昇腾分布式推理的「快递专线」
人工智能·经验分享·笔记·分布式·学习·算法
玄米乌龙茶12314 小时前
LLM成长笔记(二):数据处理与工具链
笔记
tq108615 小时前
因果本是叙事
笔记
晓梦林15 小时前
Baji1靶场学习笔记
笔记·学习
xian_wwq16 小时前
【学习笔记】大模型备案到底要交什么材料
笔记·学习