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();
}