package com.vincent.rsf.server.ai.service.diagnosis; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.vincent.rsf.server.ai.constant.AiMcpConstants; import com.vincent.rsf.server.ai.constant.AiSceneCode; import com.vincent.rsf.server.ai.dto.GatewayChatMessage; import com.vincent.rsf.server.ai.dto.GatewayChatRequest; import com.vincent.rsf.server.ai.model.AiChatMessage; import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult; import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor; import com.vincent.rsf.server.ai.model.AiPromptContext; import com.vincent.rsf.server.ai.service.AiModelRouteRuntimeService; import com.vincent.rsf.server.ai.service.AiTextCompletionService; import com.vincent.rsf.server.ai.service.mcp.AiMcpRegistryService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @Service public class AiDiagnosisMcpRuntimeService { @Resource private AiMcpRegistryService aiMcpRegistryService; @Resource private AiDiagnosticToolService aiDiagnosticToolService; @Resource private AiTextCompletionService aiTextCompletionService; @Resource private ObjectMapper objectMapper; /** * 根据当前问题、上下文消息和可用模型,解析出本轮真正要执行的 MCP 工具结果。 * 先做工具过滤,再做启发式或模型规划选择,最后顺序执行工具。 */ public List resolveToolResults(Long tenantId, AiPromptContext context, List contextMessages, AiModelRouteRuntimeService.RouteCandidate plannerCandidate) { List tools = filterTools(aiMcpRegistryService.listTools(tenantId, null), context.getSceneCode()); if (tools.isEmpty()) { return fallbackResults(context); } List selectedTools = selectTools(context, contextMessages, plannerCandidate, tools); if (selectedTools.isEmpty()) { return fallbackResults(context); } List output = new ArrayList<>(); for (AiMcpToolDescriptor tool : selectedTools) { try { AiDiagnosticToolResult result = aiMcpRegistryService.executeTool(tenantId, tool, context); if (result != null && result.getSummaryText() != null && !result.getSummaryText().trim().isEmpty()) { output.add(result); } } catch (Exception e) { output.add(new AiDiagnosticToolResult() .setToolCode(tool.getToolCode()) .setMountCode(tool.getMountCode()) .setMcpToolName(tool.getMcpToolName()) .setToolName(tool.getToolName()) .setSeverity("WARN") .setSummaryText("MCP工具执行失败:" + e.getMessage())); } } if (output.isEmpty()) { return fallbackResults(context); } return output; } /** * 判断当前请求是否值得启用 MCP。 * 诊断场景默认启用,普通聊天则按关键词和业务问题特征触发。 */ public boolean shouldUseMcp(AiPromptContext context) { if (context == null) { return false; } if (AiSceneCode.SYSTEM_DIAGNOSE.equals(context.getSceneCode())) { return true; } String question = context.getQuestion(); if (question == null || question.trim().isEmpty()) { return false; } String normalized = question.toLowerCase(); return normalized.contains("mcp") || normalized.contains("工具") || normalized.contains("license") || question.contains("许可证") || normalized.contains("task") || normalized.contains("device") || normalized.contains("site") || normalized.contains("loc") || question.contains("库存") || question.contains("库位") || question.contains("货位") || question.contains("物料") || question.contains("任务") || question.contains("设备") || question.contains("站点") || question.contains("巷道"); } /** * 诊断场景在没有选出具体工具时,退回到内置工具全量收集模式; * 普通聊天则直接返回空结果,避免无关工具污染回答。 */ private List fallbackResults(AiPromptContext context) { if (context != null && AiSceneCode.SYSTEM_DIAGNOSE.equals(context.getSceneCode())) { return aiDiagnosticToolService.collect(context); } return new ArrayList<>(); } /** * 按场景和启用状态过滤可用工具目录。 */ private List filterTools(List tools, String sceneCode) { List output = new ArrayList<>(); for (AiMcpToolDescriptor tool : tools) { if (tool == null || !Integer.valueOf(1).equals(tool.getEnabledFlag())) { continue; } if (sceneCode != null && tool.getSceneCode() != null && !tool.getSceneCode().trim().isEmpty() && !sceneCode.equals(tool.getSceneCode())) { continue; } output.add(tool); } return output; } /** * 普通聊天优先使用启发式工具选择,诊断场景优先尝试模型规划。 */ private List selectTools(AiPromptContext context, List contextMessages, AiModelRouteRuntimeService.RouteCandidate plannerCandidate, List tools) { List heuristic = heuristicSelect(tools, context == null ? null : context.getQuestion()); if (context != null && !AiSceneCode.SYSTEM_DIAGNOSE.equals(context.getSceneCode()) && !heuristic.isEmpty()) { return heuristic; } List byModel = selectToolsByModel(context, contextMessages, plannerCandidate, tools); if (!byModel.isEmpty()) { return byModel; } return heuristic; } /** * 使用一个轻量规划请求,让模型在现有工具目录中选出最需要调用的工具。 */ private List selectToolsByModel(AiPromptContext context, List contextMessages, AiModelRouteRuntimeService.RouteCandidate plannerCandidate, List tools) { if (plannerCandidate == null || plannerCandidate.getRuntimeConfig() == null) { return new ArrayList<>(); } try { GatewayChatRequest request = new GatewayChatRequest(); request.setSessionId(context == null ? null : context.getSessionId()); request.setModelCode(plannerCandidate.getAttemptModelCode()); request.setRouteCode("system_diagnose_planner"); request.setAttemptNo(0); request.setChatUrl(plannerCandidate.getRuntimeConfig().getChatUrl()); request.setApiKey(plannerCandidate.getRuntimeConfig().getApiKey()); request.setModelName(plannerCandidate.getRuntimeConfig().getModelName()); request.setSystemPrompt(buildPlannerSystemPrompt()); for (AiChatMessage item : contextMessages) { GatewayChatMessage message = new GatewayChatMessage(); message.setRole(item.getRole()); message.setContent(item.getContent()); request.getMessages().add(message); } GatewayChatMessage plannerMessage = new GatewayChatMessage(); plannerMessage.setRole("user"); plannerMessage.setContent(buildPlannerUserPrompt(context, tools)); request.getMessages().add(plannerMessage); String response = aiTextCompletionService.complete(request); return mapSelection(parseToolNames(response), tools); } catch (Exception ignore) { return new ArrayList<>(); } } /** * 生成专用于工具规划的 system prompt。 */ private String buildPlannerSystemPrompt() { return "你是WMS诊断工具调度器。你只能从提供的 MCP 工具目录中选择最需要调用的工具。" + "只输出 JSON,不要 markdown,不要解释。输出格式必须是 " + "{\"tools\":[{\"name\":\"工具名\",\"reason\":\"简短原因\"}]}" + "。工具名必须来自目录,最多选择4个;如果无需工具,返回 {\"tools\":[]}。"; } /** * 将问题和工具目录整理成规划模型可直接消费的输入。 */ private String buildPlannerUserPrompt(AiPromptContext context, List tools) { List parts = new ArrayList<>(); parts.add("问题:"); parts.add(context == null || context.getQuestion() == null ? "" : context.getQuestion()); parts.add(""); parts.add("可用工具目录:"); for (AiMcpToolDescriptor tool : tools) { parts.add("- " + tool.getMcpToolName() + " | " + safe(tool.getToolName()) + " | " + safe(tool.getDescription()) + " | " + safe(tool.getToolPrompt())); } return String.join("\n", parts); } /** * 解析规划模型返回的 JSON,提取被选中的工具名列表。 */ private List parseToolNames(String response) { List output = new ArrayList<>(); if (response == null || response.trim().isEmpty()) { return output; } String normalized = unwrapJson(response); try { JsonNode root = objectMapper.readTree(normalized); JsonNode toolsNode = root.path("tools"); if (!toolsNode.isArray()) { return output; } for (JsonNode item : toolsNode) { String name = item.path("name").asText(""); if (!name.trim().isEmpty()) { output.add(name.trim()); } } } catch (Exception ignore) { } return output; } /** * 将模型返回的工具名映射回本地工具描述符,并按返回顺序去重截断。 */ private List mapSelection(List names, List tools) { Map byName = new LinkedHashMap<>(); for (AiMcpToolDescriptor tool : tools) { byName.put(tool.getMcpToolName(), tool); } Set seen = new LinkedHashSet<>(); List output = new ArrayList<>(); for (String name : names) { if (seen.contains(name)) { continue; } AiMcpToolDescriptor tool = byName.get(name); if (tool != null) { output.add(tool); seen.add(name); } if (output.size() >= 4) { break; } } return output; } /** * 基于中文业务关键词与工具描述打分,选出最可能命中的工具。 */ private List heuristicSelect(List tools, String question) { List output = new ArrayList<>(); if (tools.isEmpty()) { return output; } String normalized = normalize(question); List scoredTools = new ArrayList<>(); for (AiMcpToolDescriptor tool : tools) { int score = matchScore(tool, normalized); if (score > 0) { scoredTools.add(new ScoredTool(tool, score)); } } scoredTools.sort(Comparator.comparingInt(ScoredTool::getScore).reversed()); for (ScoredTool scoredTool : scoredTools) { output.add(scoredTool.getTool()); if (output.size() >= 4) { return output; } } for (AiMcpToolDescriptor tool : tools) { if (!output.contains(tool)) { output.add(tool); } if (output.size() >= 3) { break; } } return output; } /** * 计算单个工具与当前问题的命中分数。 */ private int matchScore(AiMcpToolDescriptor tool, String question) { if (question == null || question.trim().isEmpty()) { return 0; } return countMatches(question, tool.getToolCode(), tool.getToolName(), tool.getDescription(), tool.getToolPrompt()); } /** * 统计问题文本与一组候选字段的片段命中次数。 */ private int countMatches(String question, String... values) { int score = 0; for (String value : values) { for (String piece : buildMatchFragments(value)) { if (piece.length() >= 2 && question.contains(piece)) { score++; } } } return score; } /** * 将工具名、描述等文本拆成适合中文业务查询匹配的片段集合。 */ private List buildMatchFragments(String value) { List fragments = new ArrayList<>(); if (value == null || value.trim().isEmpty()) { return fragments; } String[] pieces = normalize(value).split("[^a-z0-9\\u4e00-\\u9fa5]+"); for (String piece : pieces) { if (piece.length() < 2) { continue; } fragments.add(piece); if (piece.matches("[\\u4e00-\\u9fa5]+")) { if (piece.endsWith("摘要") || piece.endsWith("概况") || piece.endsWith("概览") || piece.endsWith("状态")) { fragments.add(piece.substring(0, piece.length() - 2)); } for (int size = 2; size <= Math.min(4, piece.length()); size++) { for (int i = 0; i <= piece.length() - size; i++) { fragments.add(piece.substring(i, i + size)); } } } } return fragments; } /** * 对比对文本做小写归一化,便于统一匹配。 */ private String normalize(String value) { return value == null ? "" : value.toLowerCase(); } /** * 尽量从模型输出中剥离出可解析的 JSON 片段。 */ private String unwrapJson(String response) { String normalized = response.trim(); if (normalized.startsWith("```")) { int firstBrace = normalized.indexOf('{'); int lastBrace = normalized.lastIndexOf('}'); if (firstBrace >= 0 && lastBrace > firstBrace) { return normalized.substring(firstBrace, lastBrace + 1); } } int firstBrace = normalized.indexOf('{'); int lastBrace = normalized.lastIndexOf('}'); if (firstBrace >= 0 && lastBrace > firstBrace) { return normalized.substring(firstBrace, lastBrace + 1); } return normalized; } /** * 为 prompt 组装阶段提供空值安全字符串。 */ private String safe(String value) { return value == null ? "" : value; } private static class ScoredTool { private final AiMcpToolDescriptor tool; private final int score; /** * 保存工具及其匹配得分。 */ private ScoredTool(AiMcpToolDescriptor tool, int score) { this.tool = tool; this.score = score; } /** * 返回被打分的工具。 */ public AiMcpToolDescriptor getTool() { return tool; } /** * 返回工具匹配得分。 */ public int getScore() { return score; } } }