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 javax.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<AiDiagnosticToolResult> resolveToolResults(Long tenantId,
|
AiPromptContext context,
|
List<AiChatMessage> contextMessages,
|
AiModelRouteRuntimeService.RouteCandidate plannerCandidate) {
|
List<AiMcpToolDescriptor> tools = filterTools(aiMcpRegistryService.listTools(tenantId, null), context.getSceneCode());
|
if (tools.isEmpty()) {
|
return fallbackResults(context);
|
}
|
List<AiMcpToolDescriptor> selectedTools = selectTools(context, contextMessages, plannerCandidate, tools);
|
if (selectedTools.isEmpty()) {
|
return fallbackResults(context);
|
}
|
List<AiDiagnosticToolResult> 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<AiDiagnosticToolResult> fallbackResults(AiPromptContext context) {
|
if (context != null && AiSceneCode.SYSTEM_DIAGNOSE.equals(context.getSceneCode())) {
|
return aiDiagnosticToolService.collect(context);
|
}
|
return new ArrayList<>();
|
}
|
|
/**
|
* 按场景和启用状态过滤可用工具目录。
|
*/
|
private List<AiMcpToolDescriptor> filterTools(List<AiMcpToolDescriptor> tools, String sceneCode) {
|
List<AiMcpToolDescriptor> 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<AiMcpToolDescriptor> selectTools(AiPromptContext context,
|
List<AiChatMessage> contextMessages,
|
AiModelRouteRuntimeService.RouteCandidate plannerCandidate,
|
List<AiMcpToolDescriptor> tools) {
|
List<AiMcpToolDescriptor> heuristic = heuristicSelect(tools, context == null ? null : context.getQuestion());
|
if (context != null && !AiSceneCode.SYSTEM_DIAGNOSE.equals(context.getSceneCode()) && !heuristic.isEmpty()) {
|
return heuristic;
|
}
|
List<AiMcpToolDescriptor> byModel = selectToolsByModel(context, contextMessages, plannerCandidate, tools);
|
if (!byModel.isEmpty()) {
|
return byModel;
|
}
|
return heuristic;
|
}
|
|
/**
|
* 使用一个轻量规划请求,让模型在现有工具目录中选出最需要调用的工具。
|
*/
|
private List<AiMcpToolDescriptor> selectToolsByModel(AiPromptContext context,
|
List<AiChatMessage> contextMessages,
|
AiModelRouteRuntimeService.RouteCandidate plannerCandidate,
|
List<AiMcpToolDescriptor> 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<AiMcpToolDescriptor> tools) {
|
List<String> 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<String> parseToolNames(String response) {
|
List<String> 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<AiMcpToolDescriptor> mapSelection(List<String> names, List<AiMcpToolDescriptor> tools) {
|
Map<String, AiMcpToolDescriptor> byName = new LinkedHashMap<>();
|
for (AiMcpToolDescriptor tool : tools) {
|
byName.put(tool.getMcpToolName(), tool);
|
}
|
Set<String> seen = new LinkedHashSet<>();
|
List<AiMcpToolDescriptor> 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<AiMcpToolDescriptor> heuristicSelect(List<AiMcpToolDescriptor> tools, String question) {
|
List<AiMcpToolDescriptor> output = new ArrayList<>();
|
if (tools.isEmpty()) {
|
return output;
|
}
|
String normalized = normalize(question);
|
List<ScoredTool> 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<String> buildMatchFragments(String value) {
|
List<String> 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;
|
}
|
}
|
}
|