diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/config/MCPMigrationProperties.java b/src/main/java/com/chinaweal/youfool/devops/ai/config/MCPMigrationProperties.java new file mode 100644 index 0000000..5b89a16 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/config/MCPMigrationProperties.java @@ -0,0 +1,157 @@ +package com.chinaweal.youfool.devops.ai.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * MCP迁移配置属性 + * + * 控制从"假MCP"(硬编码工具调用)迁移到"真MCP"(动态工具调用)的设置 + * + * @author AI开发团队 + * @since 2025-08-17 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "ai.mcp.migration") +public class MCPMigrationProperties { + + /** + * 是否启用MCP迁移功能 + */ + private boolean enabled = true; + + /** + * 是否使用真正的MCP系统 + * true: 使用TrueMCPServer和动态工具调用 + * false: 使用旧的硬编码MCP系统 + */ + private boolean useTrueMcp = true; + + /** + * 是否启用回退到旧系统 + * 当新系统失败时是否回退到旧的硬编码系统 + */ + private boolean fallbackToLegacy = true; + + /** + * 是否启用比较模式 + * 同时运行新旧系统并记录比较结果 + */ + private boolean comparisonMode = false; + + /** + * 比较模式下的日志记录设置 + */ + private ComparisonLogging comparisonLogging = new ComparisonLogging(); + + /** + * 提供商选择策略配置 + */ + private ProviderSelection providerSelection = new ProviderSelection(); + + /** + * 性能监控配置 + */ + private PerformanceMonitoring performanceMonitoring = new PerformanceMonitoring(); + + @Data + public static class ComparisonLogging { + /** + * 是否启用比较日志记录 + */ + private boolean enabled = true; + + /** + * 日志详细级别: basic, detailed, full + */ + private String detailLevel = "basic"; + + /** + * 是否记录响应时间差异 + */ + private boolean logTimeDifferences = true; + + /** + * 是否记录质量分数差异 + */ + private boolean logQualityDifferences = true; + + /** + * 是否记录工具调用差异 + */ + private boolean logToolCallDifferences = true; + } + + @Data + public static class ProviderSelection { + /** + * 提供商选择策略: intelligent, round-robin, fixed, load-balanced + */ + private String strategy = "intelligent"; + + /** + * 主要提供商 + */ + private String primary = "qwen"; + + /** + * 备用提供商列表 + */ + private String[] fallback = {"claude", "openai"}; + + /** + * 是否启用自动故障转移 + */ + private boolean autoFailover = true; + + /** + * 故障转移超时时间(毫秒) + */ + private int failoverTimeoutMs = 5000; + + /** + * 中文查询优先使用的提供商 + */ + private String chineseProvider = "qwen"; + + /** + * 复杂推理优先使用的提供商 + */ + private String complexReasoningProvider = "claude"; + } + + @Data + public static class PerformanceMonitoring { + /** + * 是否启用性能监控 + */ + private boolean enabled = true; + + /** + * 响应时间阈值(毫秒),超过此值记录警告 + */ + private int responseTimeThresholdMs = 10000; + + /** + * 是否记录Token使用统计 + */ + private boolean trackTokenUsage = true; + + /** + * 是否记录工具调用成功率 + */ + private boolean trackToolCallSuccessRate = true; + + /** + * 是否记录用户体验改进 + */ + private boolean trackUserExperienceImprovement = true; + + /** + * 统计数据保存天数 + */ + private int retentionDays = 30; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/controller/OpenAICompatibleController.java b/src/main/java/com/chinaweal/youfool/devops/ai/controller/OpenAICompatibleController.java index 6f99977..1343ec5 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/controller/OpenAICompatibleController.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/controller/OpenAICompatibleController.java @@ -1,10 +1,15 @@ package com.chinaweal.youfool.devops.ai.controller; +import com.chinaweal.youfool.devops.ai.client.MCPClient; +import com.chinaweal.youfool.devops.ai.config.MCPMigrationProperties; import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerRequest; import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerResponse; import com.chinaweal.youfool.devops.ai.dto.llm.ChatMessage; import com.chinaweal.youfool.devops.ai.dto.llm.ChatRequest; import com.chinaweal.youfool.devops.ai.dto.llm.ChatResponse; +import com.chinaweal.youfool.devops.ai.dto.llm.FunctionTool; +import com.chinaweal.youfool.devops.ai.provider.LLMProvider; +import com.chinaweal.youfool.devops.ai.provider.ProviderManager; import com.chinaweal.youfool.devops.ai.service.AIAnswerServiceMCP; import com.chinaweal.youfool.devops.ai.service.QwenChatService; import com.chinaweal.youfool.devops.util.ErrorLogUtils; @@ -23,8 +28,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -46,6 +50,9 @@ public class OpenAICompatibleController { private final QwenChatService qwenChatService; private final AIAnswerServiceMCP aiAnswerServiceMCP; + private final ProviderManager providerManager; + private final MCPClient mcpClient; + private final MCPMigrationProperties migrationProperties; /** * 工单ID正则表达式 - 匹配常见的工单ID格式 @@ -60,17 +67,17 @@ public class OpenAICompatibleController { }; /** - * OpenAI兼容的聊天完成接口 - 增强版支持MCP工具调用 + * OpenAI兼容的聊天完成接口 - 真正的MCP系统 */ @PostMapping("/chat/completions") - @Operation(summary = "OpenAI兼容聊天完成", description = "与OpenAI chat/completions API兼容的接口,智能判断是否使用MCP工具") + @Operation(summary = "OpenAI兼容聊天完成", description = "与OpenAI chat/completions API兼容的接口,使用真正的MCP动态工具调用") public ResponseEntity> chatCompletions( @Valid @RequestBody ChatRequest request, HttpServletRequest httpRequest) { try { - log.info("Received OpenAI-compatible chat request from IP: {}, user: {}", - getClientIpAddress(httpRequest), request.getUserId()); + log.info("Received OpenAI-compatible chat request from IP: {}, user: {}, migration.useTrueMcp: {}", + getClientIpAddress(httpRequest), request.getUserId(), migrationProperties.isUseTrueMcp()); // 确保使用正确的模式(根据请求决定是否流式) boolean isStream = request.getStream() != null && request.getStream(); @@ -88,18 +95,13 @@ public class OpenAICompatibleController { String userMessage = extractUserMessage(request); log.info("提取的用户消息: {}", userMessage != null ? userMessage.substring(0, Math.min(userMessage.length(), 100)) + "..." : "null"); - // 智能判断是否需要使用MCP工具 - boolean shouldUseMCP = shouldUseMCPTools(userMessage); - String extractedRepairId = extractRepairId(userMessage); - - log.info("MCP判断结果: shouldUseMCP={}, extractedRepairId={}", shouldUseMCP, extractedRepairId); - - if (shouldUseMCP) { - // 使用MCP增强的AI服务 - return handleMCPRequest(request, extractedRepairId, userMessage); + // 根据迁移配置选择处理方式 + if (migrationProperties.isUseTrueMcp()) { + // 使用真正的MCP系统 + return handleTrueMCPRequest(request, userMessage, httpRequest); } else { - // 使用普通聊天服务 - return handleRegularChat(request); + // 使用旧的假MCP系统(兼容性) + return handleLegacyMCPRequest(request, userMessage); } } catch (Exception e) { @@ -189,53 +191,139 @@ public class OpenAICompatibleController { } /** - * 处理MCP请求 + * 处理真正的MCP请求(动态工具调用) */ - private ResponseEntity> handleMCPRequest(ChatRequest chatRequest, String repairId, String userMessage) { + private ResponseEntity> handleTrueMCPRequest(ChatRequest chatRequest, String userMessage, HttpServletRequest httpRequest) { + long startTime = System.currentTimeMillis(); + try { - // 构建AIAnswerRequest - AIAnswerRequest aiRequest = new AIAnswerRequest(); - aiRequest.setRepairId(repairId != null ? repairId : "UNKNOWN"); // 如果没有工单ID,使用UNKNOWN - aiRequest.setUserId(chatRequest.getUserId()); - aiRequest.setSessionId(chatRequest.getSessionId() != null ? chatRequest.getSessionId() : UUID.randomUUID().toString()); - aiRequest.setStream(false); - aiRequest.setTemperature(chatRequest.getTemperature() != null ? chatRequest.getTemperature() : 0.7); - aiRequest.setMaxTokens(chatRequest.getMaxTokens() != null ? chatRequest.getMaxTokens() : 2000); - aiRequest.setUseMcpTools(true); + log.info("使用真正的MCP系统处理请求: session={}", chatRequest.getSessionId()); - // 重要:为MCP服务提供用户问题上下文,支持相似度搜索 - aiRequest.setUserQuestion(userMessage); + // 1. 获取可用的MCP工具 + List availableTools = mcpClient.getAvailableTools(); + if (availableTools.isEmpty()) { + log.warn("没有可用的MCP工具,回退到普通聊天"); + return handleRegularChat(chatRequest); + } - log.info("调用MCP服务: repairId={}, hasRepairId={}, userQuestion={}", repairId, repairId != null, - userMessage != null ? userMessage.substring(0, Math.min(userMessage.length(), 50)) + "..." : "null"); + // 2. 将MCP工具添加到请求中 + chatRequest.setTools(convertFunctionToolsToOpenAIFormat(availableTools)); + log.info("添加了{}个MCP工具到请求中", availableTools.size()); - // 调用MCP服务 - AIAnswerResponse aiResponse = aiAnswerServiceMCP.generateAnswerWithMCP(aiRequest); + // 3. 选择合适的LLM提供商 + LLMProvider selectedProvider = providerManager.selectProvider(chatRequest); + if (selectedProvider == null) { + log.error("没有可用的LLM提供商"); + return ResponseEntity.ok(RestResult.error(ResultCode.SYSTEM_INNER_ERROR, "没有可用的AI服务提供商")); + } - if ("completed".equals(aiResponse.getStatus())) { - // 转换为ChatResponse格式以保持OpenAI兼容性 - ChatResponse chatResponse = convertToChatResponse(aiResponse, chatRequest); - log.info("MCP服务调用成功: 使用工具={}, 质量分数={}", - aiResponse.getMcpToolsUsed(), aiResponse.getQualityScore()); - return ResponseEntity.ok(RestResult.ok(chatResponse)); + log.info("选择的LLM提供商: {}", selectedProvider.getProviderName()); + + // 4. 调用LLM进行聊天完成(包含工具调用) + ChatResponse chatResponse = selectedProvider.chatCompletion(chatRequest); + + // 5. 记录性能指标 + long processingTime = System.currentTimeMillis() - startTime; + chatResponse.setProcessingTimeMs(processingTime); + + log.info("真正的MCP请求处理完成: provider={}, time={}ms, tools_used={}", + selectedProvider.getProviderName(), processingTime, + chatResponse.getMcpToolsUsed() != null ? chatResponse.getMcpToolsUsed().size() : 0); + + // 6. 比较模式记录(如果启用) + if (migrationProperties.isComparisonMode()) { + recordComparisonData(chatRequest, chatResponse, userMessage, "true_mcp", processingTime); + } + + return ResponseEntity.ok(RestResult.ok(chatResponse)); + + } catch (Exception e) { + long processingTime = System.currentTimeMillis() - startTime; + log.error("真正的MCP请求处理失败: time={}ms", processingTime, e); + ErrorLogUtils.saveRuntimeError("OpenAICompatibleController.handleTrueMCPRequest", e, + "sessionId: " + chatRequest.getSessionId()); + + // 如果启用回退,尝试使用旧系统 + if (migrationProperties.isFallbackToLegacy()) { + log.info("回退到旧的MCP系统"); + return handleLegacyMCPRequest(chatRequest, userMessage); } else { - log.warn("MCP服务调用失败: {}, repairId={}", aiResponse.getErrorMessage(), repairId); + return ResponseEntity.ok(RestResult.error(ResultCode.SYSTEM_INNER_ERROR, + "MCP请求处理失败: " + e.getMessage())); + } + } + } + + /** + * 处理旧的MCP请求(硬编码工具调用)- 兼容性保持 + */ + private ResponseEntity> handleLegacyMCPRequest(ChatRequest chatRequest, String userMessage) { + long startTime = System.currentTimeMillis(); + + try { + log.info("使用旧的MCP系统处理请求: session={}", chatRequest.getSessionId()); + + // 智能判断是否需要使用MCP工具 + boolean shouldUseMCP = shouldUseMCPTools(userMessage); + String extractedRepairId = extractRepairId(userMessage); + + log.info("旧MCP判断结果: shouldUseMCP={}, extractedRepairId={}", shouldUseMCP, extractedRepairId); + + if (shouldUseMCP) { + // 构建AIAnswerRequest + AIAnswerRequest aiRequest = new AIAnswerRequest(); + aiRequest.setRepairId(extractedRepairId != null ? extractedRepairId : "UNKNOWN"); + aiRequest.setUserId(chatRequest.getUserId()); + aiRequest.setSessionId(chatRequest.getSessionId() != null ? chatRequest.getSessionId() : UUID.randomUUID().toString()); + aiRequest.setStream(false); + aiRequest.setTemperature(chatRequest.getTemperature() != null ? chatRequest.getTemperature() : 0.7); + aiRequest.setMaxTokens(chatRequest.getMaxTokens() != null ? chatRequest.getMaxTokens() : 2000); + aiRequest.setUseMcpTools(true); + aiRequest.setUserQuestion(userMessage); - // 如果MCP服务返回了有意义的回答(即使状态不是completed),也尝试使用 - if (aiResponse.getAnswer() != null && !aiResponse.getAnswer().trim().isEmpty()) { + log.info("调用旧MCP服务: repairId={}, userQuestion={}", extractedRepairId, + userMessage != null ? userMessage.substring(0, Math.min(userMessage.length(), 50)) + "..." : "null"); + + // 调用MCP服务 + AIAnswerResponse aiResponse = aiAnswerServiceMCP.generateAnswerWithMCP(aiRequest); + + long processingTime = System.currentTimeMillis() - startTime; + + if ("completed".equals(aiResponse.getStatus())) { ChatResponse chatResponse = convertToChatResponse(aiResponse, chatRequest); - log.info("MCP服务返回部分结果: 使用工具={}", aiResponse.getMcpToolsUsed()); + chatResponse.setProcessingTimeMs(processingTime); + + log.info("旧MCP服务调用成功: 使用工具={}, 质量分数={}, time={}ms", + aiResponse.getMcpToolsUsed(), aiResponse.getQualityScore(), processingTime); + + // 比较模式记录(如果启用) + if (migrationProperties.isComparisonMode()) { + recordComparisonData(chatRequest, chatResponse, userMessage, "legacy_mcp", processingTime); + } + return ResponseEntity.ok(RestResult.ok(chatResponse)); + } else { + log.warn("旧MCP服务调用失败: {}, repairId={}", aiResponse.getErrorMessage(), extractedRepairId); + + // 如果MCP服务返回了有意义的回答(即使状态不是completed),也尝试使用 + if (aiResponse.getAnswer() != null && !aiResponse.getAnswer().trim().isEmpty()) { + ChatResponse chatResponse = convertToChatResponse(aiResponse, chatRequest); + chatResponse.setProcessingTimeMs(processingTime); + log.info("旧MCP服务返回部分结果: 使用工具={}", aiResponse.getMcpToolsUsed()); + return ResponseEntity.ok(RestResult.ok(chatResponse)); + } + + // 回退到普通聊天 + return handleRegularChat(chatRequest); } - - // 只有在完全失败时才返回系统不可用回复 - log.warn("MCP服务完全失败,返回系统不可用回复"); - return ResponseEntity.ok(RestResult.ok(createSystemUnavailableResponse(chatRequest, userMessage))); + } else { + // 不需要MCP工具,使用普通聊天 + return handleRegularChat(chatRequest); } } catch (Exception e) { - log.error("MCP请求处理失败", e); - // 降级到普通聊天 + long processingTime = System.currentTimeMillis() - startTime; + log.error("旧MCP请求处理失败: time={}ms", processingTime, e); return handleRegularChat(chatRequest); } } @@ -401,6 +489,68 @@ public class OpenAICompatibleController { return response; } + /** + * 转换FunctionTool到OpenAI格式 + */ + private List> convertFunctionToolsToOpenAIFormat(List functionTools) { + List> openAITools = new ArrayList<>(); + + for (FunctionTool tool : functionTools) { + Map openAITool = new HashMap<>(); + openAITool.put("type", "function"); + + Map function = new HashMap<>(); + function.put("name", tool.getFunction().getName()); + function.put("description", tool.getFunction().getDescription()); + function.put("parameters", tool.getFunction().getParameters()); + + openAITool.put("function", function); + openAITools.add(openAITool); + } + + return openAITools; + } + + /** + * 记录比较数据(新旧系统对比) + */ + private void recordComparisonData(ChatRequest request, ChatResponse response, + String userMessage, String systemType, long processingTime) { + try { + if (!migrationProperties.getComparisonLogging().isEnabled()) { + return; + } + + Map comparisonData = new HashMap<>(); + comparisonData.put("sessionId", request.getSessionId()); + comparisonData.put("systemType", systemType); + comparisonData.put("processingTime", processingTime); + comparisonData.put("userMessage", userMessage != null ? userMessage.substring(0, Math.min(userMessage.length(), 100)) + "..." : "null"); + comparisonData.put("responseLength", response.getChoices() != null && !response.getChoices().isEmpty() && + response.getChoices().get(0).getMessage() != null ? + response.getChoices().get(0).getMessage().getContent().length() : 0); + comparisonData.put("qualityScore", response.getQualityScore()); + comparisonData.put("mcpToolsUsed", response.getMcpToolsUsed()); + comparisonData.put("timestamp", java.time.LocalDateTime.now()); + + // 记录详细信息(根据配置) + String detailLevel = migrationProperties.getComparisonLogging().getDetailLevel(); + if ("detailed".equals(detailLevel) || "full".equals(detailLevel)) { + if (response.getUsage() != null) { + comparisonData.put("tokenUsage", response.getUsage()); + } + if ("full".equals(detailLevel)) { + comparisonData.put("fullResponse", response); + } + } + + log.info("比较数据记录: {}", comparisonData); + + } catch (Exception e) { + log.warn("记录比较数据失败", e); + } + } + /** * 获取客户端IP地址 */ diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/provider/ProviderManager.java b/src/main/java/com/chinaweal/youfool/devops/ai/provider/ProviderManager.java new file mode 100644 index 0000000..0adc273 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/provider/ProviderManager.java @@ -0,0 +1,324 @@ +package com.chinaweal.youfool.devops.ai.provider; + +import com.chinaweal.youfool.devops.ai.config.MCPMigrationProperties; +import com.chinaweal.youfool.devops.ai.dto.llm.ChatRequest; +import com.chinaweal.youfool.devops.util.ErrorLogUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; + +/** + * LLM提供商管理器 + * + * 负责根据策略选择合适的LLM提供商,支持智能选择、负载均衡和故障转移 + * + * @author AI开发团队 + * @since 2025-08-17 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ProviderManager { + + private final MCPMigrationProperties migrationProperties; + private final List providers; + + // 提供商映射 + private Map providerMap = new HashMap<>(); + + // 轮询计数器 + private final AtomicInteger roundRobinCounter = new AtomicInteger(0); + + // 中文文本检测模式 + private static final Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]"); + + // 复杂推理关键词 + private static final Set COMPLEX_REASONING_KEYWORDS = Set.of( + "分析", "推理", "判断", "比较", "评估", "规划", "策略", "方案", "算法", "逻辑", + "analyze", "reasoning", "judgment", "compare", "evaluate", "planning", "strategy", "algorithm", "logic" + ); + + @PostConstruct + public void init() { + // 构建提供商映射 + for (LLMProvider provider : providers) { + if (provider.isAvailable()) { + providerMap.put(provider.getProviderName(), provider); + log.info("LLM提供商已注册: {} (支持函数调用: {}, 支持流式: {})", + provider.getProviderName(), + provider.supportsFunctionCalling(), + provider.supportsStreaming()); + } else { + log.warn("LLM提供商不可用: {}", provider.getProviderName()); + } + } + + log.info("提供商管理器初始化完成,可用提供商: {}, 选择策略: {}", + providerMap.keySet(), + migrationProperties.getProviderSelection().getStrategy()); + } + + /** + * 选择最适合的提供商 + */ + public LLMProvider selectProvider(ChatRequest request) { + try { + MCPMigrationProperties.ProviderSelection config = migrationProperties.getProviderSelection(); + String strategy = config.getStrategy(); + + log.debug("选择提供商策略: {}, 请求会话: {}", strategy, request.getSessionId()); + + LLMProvider selectedProvider = null; + + switch (strategy.toLowerCase()) { + case "intelligent": + selectedProvider = selectIntelligentProvider(request, config); + break; + case "round-robin": + selectedProvider = selectRoundRobinProvider(); + break; + case "load-balanced": + selectedProvider = selectLoadBalancedProvider(); + break; + case "fixed": + default: + selectedProvider = selectFixedProvider(config.getPrimary()); + break; + } + + // 如果选择的提供商不可用,尝试故障转移 + if (selectedProvider == null || !selectedProvider.isAvailable()) { + log.warn("选择的提供商不可用: {}, 尝试故障转移", + selectedProvider != null ? selectedProvider.getProviderName() : "null"); + selectedProvider = performFailover(config); + } + + if (selectedProvider != null) { + log.debug("已选择提供商: {} for session: {}", + selectedProvider.getProviderName(), request.getSessionId()); + return selectedProvider; + } else { + throw new RuntimeException("没有可用的LLM提供商"); + } + + } catch (Exception e) { + log.error("提供商选择失败", e); + ErrorLogUtils.saveRuntimeError("ProviderManager.selectProvider", e, + "sessionId: " + request.getSessionId()); + + // 返回默认提供商 + return getDefaultProvider(); + } + } + + /** + * 智能提供商选择 + */ + private LLMProvider selectIntelligentProvider(ChatRequest request, + MCPMigrationProperties.ProviderSelection config) { + + // 分析请求内容 + String content = extractRequestContent(request); + + // 1. 检查是否为中文内容 + if (containsChinese(content)) { + log.debug("检测到中文内容,优先选择中文提供商: {}", config.getChineseProvider()); + LLMProvider chineseProvider = providerMap.get(config.getChineseProvider()); + if (chineseProvider != null && chineseProvider.isAvailable()) { + return chineseProvider; + } + } + + // 2. 检查是否需要复杂推理 + if (requiresComplexReasoning(content)) { + log.debug("检测到复杂推理需求,选择推理提供商: {}", config.getComplexReasoningProvider()); + LLMProvider reasoningProvider = providerMap.get(config.getComplexReasoningProvider()); + if (reasoningProvider != null && reasoningProvider.isAvailable()) { + return reasoningProvider; + } + } + + // 3. 检查是否需要函数调用 + if (needsFunctionCalling(request)) { + log.debug("检测到函数调用需求,选择支持函数调用的提供商"); + for (LLMProvider provider : providers) { + if (provider.isAvailable() && provider.supportsFunctionCalling()) { + return provider; + } + } + } + + // 4. 默认选择主要提供商 + return providerMap.get(config.getPrimary()); + } + + /** + * 轮询选择提供商 + */ + private LLMProvider selectRoundRobinProvider() { + List availableProviders = getAvailableProviders(); + if (availableProviders.isEmpty()) { + return null; + } + + int index = roundRobinCounter.getAndIncrement() % availableProviders.size(); + return availableProviders.get(index); + } + + /** + * 负载均衡选择(简化版本,基于健康检查) + */ + private LLMProvider selectLoadBalancedProvider() { + List availableProviders = getAvailableProviders(); + if (availableProviders.isEmpty()) { + return null; + } + + // 简单的负载均衡:选择第一个健康的提供商 + for (LLMProvider provider : availableProviders) { + if (provider.healthCheck()) { + return provider; + } + } + + // 如果没有通过健康检查的,返回第一个可用的 + return availableProviders.get(0); + } + + /** + * 选择固定提供商 + */ + private LLMProvider selectFixedProvider(String providerName) { + return providerMap.get(providerName); + } + + /** + * 执行故障转移 + */ + private LLMProvider performFailover(MCPMigrationProperties.ProviderSelection config) { + if (!config.isAutoFailover()) { + log.debug("自动故障转移已禁用"); + return null; + } + + log.info("执行故障转移,尝试备用提供商: {}", Arrays.toString(config.getFallback())); + + for (String fallbackProvider : config.getFallback()) { + LLMProvider provider = providerMap.get(fallbackProvider); + if (provider != null && provider.isAvailable()) { + log.info("故障转移成功,选择备用提供商: {}", fallbackProvider); + return provider; + } + } + + log.warn("所有备用提供商都不可用"); + return null; + } + + /** + * 获取可用提供商列表 + */ + private List getAvailableProviders() { + return providers.stream() + .filter(LLMProvider::isAvailable) + .toList(); + } + + /** + * 提取请求内容 + */ + private String extractRequestContent(ChatRequest request) { + if (request.getMessages() == null || request.getMessages().isEmpty()) { + return ""; + } + + StringBuilder content = new StringBuilder(); + request.getMessages().forEach(message -> { + if (message.getContent() != null) { + content.append(message.getContent()).append(" "); + } + }); + + return content.toString(); + } + + /** + * 检查是否包含中文 + */ + private boolean containsChinese(String text) { + if (text == null || text.trim().isEmpty()) { + return false; + } + return CHINESE_PATTERN.matcher(text).find(); + } + + /** + * 检查是否需要复杂推理 + */ + private boolean requiresComplexReasoning(String content) { + if (content == null || content.trim().isEmpty()) { + return false; + } + + String lowerContent = content.toLowerCase(); + return COMPLEX_REASONING_KEYWORDS.stream() + .anyMatch(lowerContent::contains); + } + + /** + * 检查是否需要函数调用 + */ + private boolean needsFunctionCalling(ChatRequest request) { + return (request.getTools() != null && !request.getTools().isEmpty()) || + (request.getMcpTools() != null && !request.getMcpTools().isEmpty()); + } + + /** + * 获取默认提供商 + */ + private LLMProvider getDefaultProvider() { + // 按优先级返回第一个可用的提供商 + String[] defaultOrder = {"qwen", "claude", "openai"}; + + for (String providerName : defaultOrder) { + LLMProvider provider = providerMap.get(providerName); + if (provider != null && provider.isAvailable()) { + log.info("使用默认提供商: {}", providerName); + return provider; + } + } + + // 如果以上都不可用,返回任何一个可用的 + return getAvailableProviders().stream().findFirst().orElse(null); + } + + /** + * 获取指定名称的提供商 + */ + public LLMProvider getProvider(String providerName) { + return providerMap.get(providerName); + } + + /** + * 获取所有可用提供商名称 + */ + public Set getAvailableProviderNames() { + return providerMap.entrySet().stream() + .filter(entry -> entry.getValue().isAvailable()) + .map(Map.Entry::getKey) + .collect(HashSet::new, Set::add, Set::addAll); + } + + /** + * 检查提供商是否可用 + */ + public boolean isProviderAvailable(String providerName) { + LLMProvider provider = providerMap.get(providerName); + return provider != null && provider.isAvailable(); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerServiceMCP.java b/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerServiceMCP.java index 6a64c00..0f5fce6 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerServiceMCP.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerServiceMCP.java @@ -1,15 +1,20 @@ package com.chinaweal.youfool.devops.ai.service; import com.chinaweal.youfool.devops.ai.aspect.annotation.TrackedAIGeneration; +import com.chinaweal.youfool.devops.ai.client.MCPClient; +import com.chinaweal.youfool.devops.ai.config.MCPMigrationProperties; import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerRequest; import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerResponse; import com.chinaweal.youfool.devops.ai.dto.llm.ChatMessage; import com.chinaweal.youfool.devops.ai.dto.llm.ChatRequest; import com.chinaweal.youfool.devops.ai.dto.llm.ChatResponse; +import com.chinaweal.youfool.devops.ai.dto.llm.FunctionTool; import com.chinaweal.youfool.devops.ai.mcp.MCPServer; import com.chinaweal.youfool.devops.ai.mcp.MCPTool; import com.chinaweal.youfool.devops.ai.mcp.MCPResponse; import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition; +import com.chinaweal.youfool.devops.ai.provider.LLMProvider; +import com.chinaweal.youfool.devops.ai.provider.ProviderManager; import com.chinaweal.youfool.devops.util.ErrorLogUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,9 +41,12 @@ public class AIAnswerServiceMCP { private final QwenChatService qwenChatService; private final MCPServer mcpServer; + private final ProviderManager providerManager; + private final MCPClient mcpClient; + private final MCPMigrationProperties migrationProperties; /** - * 生成工单AI回答(MCP版本) + * 生成工单AI回答(MCP版本)- 增强的真MCP系统 */ @TrackedAIGeneration public AIAnswerResponse generateAnswerWithMCP(AIAnswerRequest request) { @@ -49,27 +57,18 @@ public class AIAnswerServiceMCP { response.setStatus("processing"); try { - log.info("开始MCP版本AI回答生成,工单: {}, 会话: {}", - request.getRepairId(), request.getSessionId()); + log.info("开始增强MCP版本AI回答生成,工单: {}, 会话: {}, useTrueMcp: {}", + request.getRepairId(), request.getSessionId(), migrationProperties.isUseTrueMcp()); - // 构建支持MCP的聊天请求 - ChatRequest chatRequest = buildMCPChatRequest(request); - - // 调试日志:输出MCP prompt - log.info("=== MCP版本 LLM Prompt 调试信息 ==="); - if (chatRequest.getMessages() != null) { - for (ChatMessage message : chatRequest.getMessages()) { - if ("system".equals(message.getRole())) { - log.info("MCP系统提示词: {}", message.getContent()); - } else if ("user".equals(message.getRole())) { - log.info("MCP用户消息: {}", message.getContent()); - } - } + ChatResponse chatResponse; + + if (migrationProperties.isUseTrueMcp()) { + // 使用真正的MCP系统 + chatResponse = generateWithTrueMCP(request); + } else { + // 使用旧的MCP系统(兼容性) + chatResponse = generateWithLegacyMCP(request); } - log.info("=== MCP版本 LLM Prompt 结束 ==="); - - // 调用支持MCP的LLM服务 - ChatResponse chatResponse = qwenChatService.chatCompletionWithMCP(chatRequest, mcpServer); // 解析响应 parseAndSetResponse(response, chatResponse, request); @@ -84,14 +83,15 @@ public class AIAnswerServiceMCP { response.setProcessingTimeMs( java.time.Duration.between(response.getStartTime(), response.getEndTime()).toMillis()); - log.info("MCP版本AI回答生成成功,工单: {}, 使用工具数: {}", + log.info("增强MCP版本AI回答生成成功,工单: {}, 使用工具数: {}, 处理时间: {}ms", request.getRepairId(), - response.getMcpToolsUsed() != null ? response.getMcpToolsUsed().size() : 0); + response.getMcpToolsUsed() != null ? response.getMcpToolsUsed().size() : 0, + response.getProcessingTimeMs()); return response; } catch (Exception e) { - log.error("MCP版本AI回答生成失败,工单: {}", request.getRepairId(), e); + log.error("增强MCP版本AI回答生成失败,工单: {}", request.getRepairId(), e); ErrorLogUtils.saveRuntimeError("AIAnswerServiceMCP.generateAnswerWithMCP", e, "repairId: " + request.getRepairId()); @@ -100,6 +100,21 @@ public class AIAnswerServiceMCP { response.setErrorMessage(e.getMessage()); response.setEndTime(LocalDateTime.now()); + // 如果启用回退,尝试使用另一种方式 + if (migrationProperties.isFallbackToLegacy() && migrationProperties.isUseTrueMcp()) { + log.info("回退到旧MCP系统重试"); + try { + ChatResponse fallbackResponse = generateWithLegacyMCP(request); + parseAndSetResponse(response, fallbackResponse, request); + response.setStatus("completed"); + response.setErrorCode(null); + response.setErrorMessage(null); + log.info("回退到旧MCP系统成功"); + } catch (Exception fallbackError) { + log.error("回退到旧MCP系统也失败", fallbackError); + } + } + return response; } } @@ -358,6 +373,198 @@ public class AIAnswerServiceMCP { response.setSuggestedActions(suggestedActions); } + /** + * 使用真正的MCP系统生成回答 + */ + private ChatResponse generateWithTrueMCP(AIAnswerRequest request) { + log.info("使用真正的MCP系统生成回答: repairId={}", request.getRepairId()); + + // 1. 构建基础聊天请求 + ChatRequest chatRequest = buildTrueMCPChatRequest(request); + + // 2. 获取可用的MCP工具 + List availableTools = mcpClient.getAvailableTools(); + if (availableTools.isEmpty()) { + log.warn("没有可用的MCP工具,使用普通聊天"); + return generateWithoutTools(chatRequest); + } + + // 3. 将工具添加到请求中 + chatRequest.setTools(convertFunctionToolsToOpenAIFormat(availableTools)); + log.info("添加了{}个MCP工具到请求中", availableTools.size()); + + // 4. 选择合适的LLM提供商 + LLMProvider selectedProvider = providerManager.selectProvider(chatRequest); + if (selectedProvider == null) { + throw new RuntimeException("没有可用的LLM提供商"); + } + + log.info("真正MCP系统选择的提供商: {}", selectedProvider.getProviderName()); + + // 5. 调用LLM进行聊天完成(包含动态工具调用) + return selectedProvider.chatCompletion(chatRequest); + } + + /** + * 使用旧的MCP系统生成回答 + */ + private ChatResponse generateWithLegacyMCP(AIAnswerRequest request) { + log.info("使用旧的MCP系统生成回答: repairId={}", request.getRepairId()); + + // 构建支持MCP的聊天请求 + ChatRequest chatRequest = buildMCPChatRequest(request); + + // 调试日志:输出MCP prompt + log.debug("=== 旧MCP版本 LLM Prompt 调试信息 ==="); + if (chatRequest.getMessages() != null) { + for (ChatMessage message : chatRequest.getMessages()) { + if ("system".equals(message.getRole())) { + log.debug("旧MCP系统提示词: {}", message.getContent().substring(0, Math.min(message.getContent().length(), 200)) + "..."); + } else if ("user".equals(message.getRole())) { + log.debug("旧MCP用户消息: {}", message.getContent()); + } + } + } + log.debug("=== 旧MCP版本 LLM Prompt 结束 ==="); + + // 调用支持MCP的LLM服务 + return qwenChatService.chatCompletionWithMCP(chatRequest, mcpServer); + } + + /** + * 构建真正的MCP聊天请求 + */ + private ChatRequest buildTrueMCPChatRequest(AIAnswerRequest request) { + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setSessionId(request.getSessionId()); + chatRequest.setUserId(request.getUserId()); + chatRequest.setSource("repair-answer-true-mcp"); + chatRequest.setTemperature(request.getTemperature() != null ? request.getTemperature() : 0.7); + chatRequest.setMaxTokens(request.getMaxTokens() != null ? request.getMaxTokens() : 2000); + + List messages = new ArrayList<>(); + + // 系统提示词 - 真MCP版本(更简洁,让LLM自己决定工具使用) + String systemPrompt = buildTrueMCPSystemPrompt(request); + messages.add(ChatMessage.system(systemPrompt)); + + // 用户查询 - 真MCP版本 + String userPrompt = buildTrueMCPUserPrompt(request); + messages.add(ChatMessage.user(userPrompt)); + + chatRequest.setMessages(messages); + + // 设置工具选择策略 + chatRequest.setToolChoice("auto"); // 让LLM自动决定是否使用工具 + chatRequest.setParallelToolCalls(true); // 启用并行工具调用 + + return chatRequest; + } + + /** + * 构建真MCP版本系统提示词(更简洁) + */ + private String buildTrueMCPSystemPrompt(AIAnswerRequest request) { + StringBuilder prompt = new StringBuilder(); + + prompt.append("你是一个运维服务助手,帮助用户解决技术问题。\n\n"); + + prompt.append("你可以使用以下工具来获取信息:\n"); + prompt.append("- repair_query: 根据工单ID查询工单详细信息\n"); + prompt.append("- repair_feedback_query: 查询工单的反馈处理结果(经过验证的解决方案)\n"); + prompt.append("- similarity_search: 基于文本进行相似问题检索\n"); + prompt.append("- knowledge_query: 在知识库中查找相关信息\n\n"); + + prompt.append("工作原则:\n"); + prompt.append("1. 根据用户问题智能选择合适的工具\n"); + prompt.append("2. 优先使用feedback查询获取已验证的解决方案\n"); + prompt.append("3. 如果没有具体工单,使用相似度搜索寻找类似案例\n"); + prompt.append("4. 提供简洁、专业、可操作的解决建议\n"); + prompt.append("5. 使用").append(request.getLanguage() != null ? request.getLanguage() : "中文").append("回答\n\n"); + + // 语调设置 + switch (request.getAnswerStyle() != null ? request.getAnswerStyle() : "professional") { + case "simple": + prompt.append("回答风格:简单直接\n"); + break; + case "friendly": + prompt.append("回答风格:友好亲切\n"); + break; + default: + prompt.append("回答风格:专业简洁\n"); + break; + } + + return prompt.toString(); + } + + /** + * 构建真MCP版本用户提示词 + */ + private String buildTrueMCPUserPrompt(AIAnswerRequest request) { + StringBuilder prompt = new StringBuilder(); + + // 判断是具体工单查询还是一般问题咨询 + boolean hasSpecificRepairId = request.getRepairId() != null && + !"UNKNOWN".equals(request.getRepairId()) && + !request.getRepairId().trim().isEmpty(); + + if (hasSpecificRepairId) { + // 有具体工单ID的场景 + prompt.append("请帮我处理工单ID为 '").append(request.getRepairId()).append("' 的问题。"); + } else if (request.getUserQuestion() != null && !request.getUserQuestion().trim().isEmpty()) { + // 没有具体工单ID,但有用户问题的场景 + prompt.append("用户咨询问题:").append(request.getUserQuestion()); + } else { + // 既没有工单ID也没有用户问题 + prompt.append("请提供工单ID或具体的问题描述,以便我为您提供帮助。"); + } + + return prompt.toString(); + } + + /** + * 不使用工具的普通聊天生成 + */ + private ChatResponse generateWithoutTools(ChatRequest chatRequest) { + log.info("没有可用工具,使用普通聊天模式"); + + // 选择提供商并生成回答 + LLMProvider selectedProvider = providerManager.selectProvider(chatRequest); + if (selectedProvider == null) { + throw new RuntimeException("没有可用的LLM提供商"); + } + + // 移除工具相关设置 + chatRequest.setTools(null); + chatRequest.setMcpTools(null); + chatRequest.setToolChoice("none"); + + return selectedProvider.chatCompletion(chatRequest); + } + + /** + * 转换FunctionTool到OpenAI格式 + */ + private List> convertFunctionToolsToOpenAIFormat(List functionTools) { + List> openAITools = new ArrayList<>(); + + for (FunctionTool tool : functionTools) { + Map openAITool = new HashMap<>(); + openAITool.put("type", "function"); + + Map function = new HashMap<>(); + function.put("name", tool.getFunction().getName()); + function.put("description", tool.getFunction().getDescription()); + function.put("parameters", tool.getFunction().getParameters()); + + openAITool.put("function", function); + openAITools.add(openAITool); + } + + return openAITools; + } + /** * 转换MCPTool到MCPToolDefinition */ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cd34369..d0f170f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -259,6 +259,44 @@ cors: # AI LLM配置 - 大语言模型配置 ai: + # MCP迁移配置 + mcp: + # 是否启用MCP服务 + enabled: true + # 迁移控制配置 + migration: + enabled: true + use-true-mcp: true # 使用真正的MCP系统 + fallback-to-legacy: true # 失败时回退到旧系统 + comparison-mode: false # 是否启用新旧系统比较 + comparison-logging: + enabled: true + detail-level: basic # basic, detailed, full + log-time-differences: true + log-quality-differences: true + log-tool-call-differences: true + provider-selection: + strategy: intelligent # intelligent, round-robin, fixed, load-balanced + primary: qwen + fallback: ["claude", "openai"] + auto-failover: true + failover-timeout-ms: 5000 + chinese-provider: qwen + complex-reasoning-provider: claude + performance-monitoring: + enabled: true + response-time-threshold-ms: 10000 + track-token-usage: true + track-tool-call-success-rate: true + track-user-experience-improvement: true + retention-days: 30 + # MCP客户端配置 + client: + enabled: true + base-url: http://localhost:8080/mcp + timeout: 30000 + max-retries: 3 + # Embedding服务配置 - 文本向量化 embedding: # 是否启用embedding服务 diff --git a/src/test/java/com/chinaweal/youfool/devops/ai/integration/MCPPasswordResetIntegrationTest.java b/src/test/java/com/chinaweal/youfool/devops/ai/integration/MCPPasswordResetIntegrationTest.java index da3e652..b0ec038 100644 --- a/src/test/java/com/chinaweal/youfool/devops/ai/integration/MCPPasswordResetIntegrationTest.java +++ b/src/test/java/com/chinaweal/youfool/devops/ai/integration/MCPPasswordResetIntegrationTest.java @@ -1,5 +1,7 @@ package com.chinaweal.youfool.devops.ai.integration; +import com.chinaweal.youfool.devops.ai.client.MCPClient; +import com.chinaweal.youfool.devops.ai.config.MCPMigrationProperties; import com.chinaweal.youfool.devops.ai.controller.OpenAICompatibleController; import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerRequest; import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerResponse; @@ -7,6 +9,7 @@ import com.chinaweal.youfool.devops.ai.dto.llm.ChatMessage; import com.chinaweal.youfool.devops.ai.dto.llm.ChatRequest; import com.chinaweal.youfool.devops.ai.mcp.MCPResponse; import com.chinaweal.youfool.devops.ai.mcp.MCPServer; +import com.chinaweal.youfool.devops.ai.provider.ProviderManager; import com.chinaweal.youfool.devops.ai.service.AIAnswerServiceMCP; import com.chinaweal.youfool.devops.ai.service.QwenChatService; import lombok.extern.slf4j.Slf4j; @@ -45,6 +48,15 @@ public class MCPPasswordResetIntegrationTest { @Mock private AIAnswerServiceMCP mockAIAnswerServiceMCP; + @Mock + private ProviderManager mockProviderManager; + + @Mock + private MCPClient mockMCPClient; + + @Mock + private MCPMigrationProperties mockMigrationProperties; + @Mock private MCPServer mockMCPServer; @@ -52,7 +64,13 @@ public class MCPPasswordResetIntegrationTest { @BeforeEach void setUp() { - openAIController = new OpenAICompatibleController(mockQwenChatService, mockAIAnswerServiceMCP); + openAIController = new OpenAICompatibleController( + mockQwenChatService, + mockAIAnswerServiceMCP, + mockProviderManager, + mockMCPClient, + mockMigrationProperties + ); } @Test diff --git a/src/test/java/com/chinaweal/youfool/devops/ai/service/MCPFixValidationTest.java b/src/test/java/com/chinaweal/youfool/devops/ai/service/MCPFixValidationTest.java index 03cd7b3..39db1dc 100644 --- a/src/test/java/com/chinaweal/youfool/devops/ai/service/MCPFixValidationTest.java +++ b/src/test/java/com/chinaweal/youfool/devops/ai/service/MCPFixValidationTest.java @@ -1,9 +1,12 @@ package com.chinaweal.youfool.devops.ai.service; +import com.chinaweal.youfool.devops.ai.client.MCPClient; +import com.chinaweal.youfool.devops.ai.config.MCPMigrationProperties; import com.chinaweal.youfool.devops.ai.controller.OpenAICompatibleController; import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerRequest; import com.chinaweal.youfool.devops.ai.dto.llm.ChatMessage; import com.chinaweal.youfool.devops.ai.dto.llm.ChatRequest; +import com.chinaweal.youfool.devops.ai.provider.ProviderManager; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -42,12 +45,27 @@ public class MCPFixValidationTest { @Mock private AIAnswerServiceMCP mockAIAnswerServiceMCP; + @Mock + private ProviderManager mockProviderManager; + + @Mock + private MCPClient mockMCPClient; + + @Mock + private MCPMigrationProperties mockMigrationProperties; + private OpenAICompatibleController openAIController; private AIAnswerServiceMCP aiAnswerServiceMCP; @BeforeEach void setUp() { - openAIController = new OpenAICompatibleController(mockQwenChatService, mockAIAnswerServiceMCP); + openAIController = new OpenAICompatibleController( + mockQwenChatService, + mockAIAnswerServiceMCP, + mockProviderManager, + mockMCPClient, + mockMigrationProperties + ); // 我们不能直接实例化AIAnswerServiceMCP因为它有依赖,所以只测试逻辑 }