Integrate true MCP system with existing API endpoints
Major Enhancements: - Updated OpenAICompatibleController to use dynamic LLM provider selection and real MCP tool calling - Enhanced AIAnswerServiceMCP with true MCP system support while maintaining legacy compatibility - Implemented intelligent ProviderManager for optimal LLM provider selection (Qwen for Chinese, Claude for complex reasoning) - Added comprehensive MCPMigrationProperties for feature flags and gradual rollout control - Integrated MCPClient for dynamic tool discovery and execution via TrueMCPServer New Features: - True MCP system: LLM autonomously decides which tools to call and with what parameters - Provider selection strategies: intelligent, round-robin, fixed, load-balanced - Automatic failover and health checking across providers - Performance monitoring and comparison logging between old/new systems - Configuration-driven migration with fallback capabilities Benefits: - User Query: "我无法修改密码" → LLM automatically uses similarity_search with optimal parameters - Dynamic tool calling replaces hardcoded keyword detection - Improved user experience through intelligent provider selection - Seamless backward compatibility during migration Configuration: - ai.mcp.migration.use-true-mcp: true (enable new system) - ai.mcp.migration.fallback-to-legacy: true (safety fallback) - ai.mcp.migration.comparison-mode: false (disable A/B testing) - Provider selection strategy: intelligent (Chinese→Qwen, Complex→Claude)
This commit is contained in:
parent
c3940e0991
commit
a8e70d8bde
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<RestResult<Object>> 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<RestResult<Object>> handleMCPRequest(ChatRequest chatRequest, String repairId, String userMessage) {
|
||||
private ResponseEntity<RestResult<Object>> handleTrueMCPRequest(ChatRequest chatRequest, String userMessage, HttpServletRequest httpRequest) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
log.info("使用真正的MCP系统处理请求: session={}", chatRequest.getSessionId());
|
||||
|
||||
// 1. 获取可用的MCP工具
|
||||
List<FunctionTool> availableTools = mcpClient.getAvailableTools();
|
||||
if (availableTools.isEmpty()) {
|
||||
log.warn("没有可用的MCP工具,回退到普通聊天");
|
||||
return handleRegularChat(chatRequest);
|
||||
}
|
||||
|
||||
// 2. 将MCP工具添加到请求中
|
||||
chatRequest.setTools(convertFunctionToolsToOpenAIFormat(availableTools));
|
||||
log.info("添加了{}个MCP工具到请求中", availableTools.size());
|
||||
|
||||
// 3. 选择合适的LLM提供商
|
||||
LLMProvider selectedProvider = providerManager.selectProvider(chatRequest);
|
||||
if (selectedProvider == null) {
|
||||
log.error("没有可用的LLM提供商");
|
||||
return ResponseEntity.ok(RestResult.error(ResultCode.SYSTEM_INNER_ERROR, "没有可用的AI服务提供商"));
|
||||
}
|
||||
|
||||
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 {
|
||||
return ResponseEntity.ok(RestResult.error(ResultCode.SYSTEM_INNER_ERROR,
|
||||
"MCP请求处理失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理旧的MCP请求(硬编码工具调用)- 兼容性保持
|
||||
*/
|
||||
private ResponseEntity<RestResult<Object>> 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(repairId != null ? repairId : "UNKNOWN"); // 如果没有工单ID,使用UNKNOWN
|
||||
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);
|
||||
|
||||
// 重要:为MCP服务提供用户问题上下文,支持相似度搜索
|
||||
aiRequest.setUserQuestion(userMessage);
|
||||
|
||||
log.info("调用MCP服务: repairId={}, hasRepairId={}, userQuestion={}", repairId, repairId != null,
|
||||
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格式以保持OpenAI兼容性
|
||||
ChatResponse chatResponse = convertToChatResponse(aiResponse, chatRequest);
|
||||
log.info("MCP服务调用成功: 使用工具={}, 质量分数={}",
|
||||
aiResponse.getMcpToolsUsed(), aiResponse.getQualityScore());
|
||||
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(), repairId);
|
||||
log.warn("旧MCP服务调用失败: {}, repairId={}", aiResponse.getErrorMessage(), extractedRepairId);
|
||||
|
||||
// 如果MCP服务返回了有意义的回答(即使状态不是completed),也尝试使用
|
||||
if (aiResponse.getAnswer() != null && !aiResponse.getAnswer().trim().isEmpty()) {
|
||||
ChatResponse chatResponse = convertToChatResponse(aiResponse, chatRequest);
|
||||
log.info("MCP服务返回部分结果: 使用工具={}", aiResponse.getMcpToolsUsed());
|
||||
chatResponse.setProcessingTimeMs(processingTime);
|
||||
log.info("旧MCP服务返回部分结果: 使用工具={}", aiResponse.getMcpToolsUsed());
|
||||
return ResponseEntity.ok(RestResult.ok(chatResponse));
|
||||
}
|
||||
|
||||
// 只有在完全失败时才返回系统不可用回复
|
||||
log.warn("MCP服务完全失败,返回系统不可用回复");
|
||||
return ResponseEntity.ok(RestResult.ok(createSystemUnavailableResponse(chatRequest, userMessage)));
|
||||
// 回退到普通聊天
|
||||
return handleRegularChat(chatRequest);
|
||||
}
|
||||
} 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<Map<String, Object>> convertFunctionToolsToOpenAIFormat(List<FunctionTool> functionTools) {
|
||||
List<Map<String, Object>> openAITools = new ArrayList<>();
|
||||
|
||||
for (FunctionTool tool : functionTools) {
|
||||
Map<String, Object> openAITool = new HashMap<>();
|
||||
openAITool.put("type", "function");
|
||||
|
||||
Map<String, Object> 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<String, Object> 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地址
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<LLMProvider> providers;
|
||||
|
||||
// 提供商映射
|
||||
private Map<String, LLMProvider> providerMap = new HashMap<>();
|
||||
|
||||
// 轮询计数器
|
||||
private final AtomicInteger roundRobinCounter = new AtomicInteger(0);
|
||||
|
||||
// 中文文本检测模式
|
||||
private static final Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
|
||||
|
||||
// 复杂推理关键词
|
||||
private static final Set<String> 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<LLMProvider> availableProviders = getAvailableProviders();
|
||||
if (availableProviders.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int index = roundRobinCounter.getAndIncrement() % availableProviders.size();
|
||||
return availableProviders.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 负载均衡选择(简化版本,基于健康检查)
|
||||
*/
|
||||
private LLMProvider selectLoadBalancedProvider() {
|
||||
List<LLMProvider> 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<LLMProvider> 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<String> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
ChatResponse chatResponse;
|
||||
|
||||
// 调试日志:输出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());
|
||||
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<FunctionTool> 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<ChatMessage> 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<Map<String, Object>> convertFunctionToolsToOpenAIFormat(List<FunctionTool> functionTools) {
|
||||
List<Map<String, Object>> openAITools = new ArrayList<>();
|
||||
|
||||
for (FunctionTool tool : functionTools) {
|
||||
Map<String, Object> openAITool = new HashMap<>();
|
||||
openAITool.put("type", "function");
|
||||
|
||||
Map<String, Object> 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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服务
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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因为它有依赖,所以只测试逻辑
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue