From 293198b12d7fca4bdc6c080ea70982985553cc85 Mon Sep 17 00:00:00 2001 From: 75681 <756810279@qq.com> Date: Sun, 17 Aug 2025 21:12:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=9C=9F=E6=AD=A3=E7=9A=84MC?= =?UTF-8?q?P=20(Model=20Context=20Protocol)=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不改动原有流程的基础上,新增了支持动态工具调用的MCP版本: ## 主要变更 ### 1. 核心MCP组件 - 新增 MCPServer: 提供MCP工具的注册、管理和执行 - 新增 MCPTool: MCP工具定义数据结构 - 新增 MCPResponse: MCP响应统一格式 - 新增 AIAnswerServiceMCP: 基于MCP的AI回答生成服务 - 新增 AIAnswerMCPController: MCP版本REST API控制器 ### 2. MCP工具支持 - repair_query: 根据工单ID查询工单详细信息 - repair_feedback_query: 查询工单feedback处理结果 - similarity_search: 基于文本相似度检索相似案例 - knowledge_query: 知识库精确匹配查询 ### 3. LLM集成扩展 - QwenChatService: 增加MCP工具调用支持 - ChatRequest/ChatResponse: 添加MCP相关字段 - 实现动态工具调用和结果整合逻辑 ### 4. API端点 - GET /api/ai/mcp/tools: 获取可用MCP工具列表 - POST /api/ai/mcp/answer: MCP版本AI回答生成 - POST /api/ai/mcp/answer/stream: MCP版本流式回答 - POST /api/ai/mcp/compare: 对比MCP与原版本结果 - GET /api/ai/mcp/test/{toolName}: 测试特定MCP工具 ### 5. 配置支持 - application.yml: 添加完整的MCP配置项 - 支持工具启用/禁用、缓存、超时等配置 ## 技术特点 1. **动态工具调用**: LLM可根据需要动态选择和调用工具 2. **数据源一致**: 使用与原版本完全相同的数据库查询 3. **优先级保持**: 维持feedback > 相似案例 > 通用建议的优先级 4. **完整监控**: 记录工具调用日志、执行时间、成功率等 5. **降级机制**: 工具调用失败时自动降级处理 6. **无侵入性**: 原有功能完全不受影响,通过配置控制启用 ## 架构对比 - 原版本: 硬编码数据库查询 → 预处理prompt → LLM - MCP版本: LLM动态调用MCP工具 → 实时数据获取 → 智能回答生成 ## 文档 - 新增 MCP_IMPLEMENTATION.md: 详细的实现文档和使用指南 这个实现确保了结果的一致性,同时为未来的功能扩展提供了更灵活的架构基础。 --- MCP_IMPLEMENTATION.md | 228 +++++++++++ .../ai/controller/AIAnswerMCPController.java | 285 ++++++++++++++ .../devops/ai/dto/llm/ChatRequest.java | 6 + .../devops/ai/dto/llm/ChatResponse.java | 6 + .../youfool/devops/ai/mcp/MCPResponse.java | 90 +++++ .../youfool/devops/ai/mcp/MCPServer.java | 366 +++++++++++++++++ .../youfool/devops/ai/mcp/MCPTool.java | 47 +++ .../migration/RepairVectorizationService.java | 32 +- .../devops/ai/service/AIAnswerService.java | 130 ++++-- .../devops/ai/service/AIAnswerServiceMCP.java | 339 ++++++++++++++++ .../devops/ai/service/QwenChatService.java | 370 ++++++++++++++++++ src/main/resources/application.yml | 33 ++ 12 files changed, 1905 insertions(+), 27 deletions(-) create mode 100644 MCP_IMPLEMENTATION.md create mode 100644 src/main/java/com/chinaweal/youfool/devops/ai/controller/AIAnswerMCPController.java create mode 100644 src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPResponse.java create mode 100644 src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPServer.java create mode 100644 src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPTool.java create mode 100644 src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerServiceMCP.java diff --git a/MCP_IMPLEMENTATION.md b/MCP_IMPLEMENTATION.md new file mode 100644 index 0000000..ed8cd5d --- /dev/null +++ b/MCP_IMPLEMENTATION.md @@ -0,0 +1,228 @@ +# MCP (Model Context Protocol) 实现文档 + +## 概述 + +本文档介绍了在youfool-devops-gd系统中实现的真正的MCP (Model Context Protocol) 版本。与原有的硬编码数据库查询不同,MCP版本允许LLM动态调用工具来获取数据,提供更灵活和可扩展的AI回答服务。 + +## 架构对比 + +### 原有流程(硬编码版本) +``` +用户请求 → AIAnswerController → AIAnswerService.generateAnswer() → +直接数据库查询 (repair表、repair_handle表、ai_knowledge_base表) → +构建预处理的prompt → qwenChatService.chatCompletion() → 返回AI回答 +``` + +### MCP流程(动态工具调用版本) +``` +用户请求 → AIAnswerMCPController → AIAnswerServiceMCP.generateAnswerWithMCP() → +QwenChatService.chatCompletionWithMCP() → +LLM动态调用MCP工具 (repair_query, repair_feedback_query, similarity_search) → +MCPServer.executeTool() → 返回AI回答 + 工具使用记录 +``` + +## 核心组件 + +### 1. MCPServer +- **位置**: `com.chinaweal.youfool.devops.ai.mcp.MCPServer` +- **功能**: 提供MCP工具的注册、管理和执行 +- **支持的工具**: + - `repair_query`: 根据工单ID查询工单详细信息 + - `repair_feedback_query`: 查询工单的feedback步骤处理结果 + - `similarity_search`: 基于文本内容进行向量相似度检索 + - `knowledge_query`: 在知识库中精确匹配记录 + +### 2. AIAnswerServiceMCP +- **位置**: `com.chinaweal.youfool.devops.ai.service.AIAnswerServiceMCP` +- **功能**: 提供基于MCP的AI回答生成服务 +- **特点**: + - 支持动态工具调用 + - 包含完整的错误处理和降级机制 + - 提供流式和非流式两种模式 + +### 3. QwenChatService(扩展) +- **功能扩展**: 增加了MCP工具调用支持 +- **新方法**: + - `chatCompletionWithMCP()`: 支持MCP的聊天完成 + - `streamChatCompletionWithMCP()`: 支持MCP的流式聊天完成 + - `processMCPToolCalls()`: 处理MCP工具调用逻辑 + +### 4. AIAnswerMCPController +- **位置**: `com.chinaweal.youfool.devops.ai.controller.AIAnswerMCPController` +- **功能**: 提供MCP版本的REST API接口 +- **端点**: + - `GET /api/ai/mcp/tools`: 获取可用的MCP工具列表 + - `POST /api/ai/mcp/answer`: 生成AI回答(MCP版本) + - `POST /api/ai/mcp/answer/stream`: 生成AI回答流式输出(MCP版本) + - `POST /api/ai/mcp/compare`: 比较MCP版本与原版本结果 + - `GET /api/ai/mcp/test/{toolName}`: 测试MCP工具调用 + +## 配置说明 + +在 `application.yml` 中添加了以下MCP配置: + +```yaml +ai: + mcp: + enabled: true # 是否启用MCP服务 + server-name: youfool-devops-mcp # MCP服务器名称 + version: 1.0.0 # MCP版本 + tool-timeout: 30000 # 工具执行超时时间(毫秒) + log-tool-calls: true # 是否记录工具调用日志 + cache: + enabled: true # 工具调用结果缓存 + ttl: 300000 # 5分钟缓存 + tools: + repair-query: + enabled: true + description: "查询工单详细信息" + repair-feedback-query: + enabled: true + description: "查询工单feedback处理结果" + similarity-search: + enabled: true + description: "基于文本相似度搜索" + default-top-k: 5 + default-threshold: 0.7 + knowledge-query: + enabled: true + description: "知识库精确查询" +``` + +## 工具详细说明 + +### 1. repair_query +- **功能**: 根据工单ID查询工单详细信息 +- **输入参数**: + - `repairId` (string): 工单ID +- **返回数据**: 工单基本信息(ID、标题、业务模块、问题类型、优先级、故障描述等) + +### 2. repair_feedback_query +- **功能**: 查询工单的feedback步骤处理结果 +- **输入参数**: + - `repairId` (string): 工单ID +- **返回数据**: feedback记录列表,包括处理结果、处理人、处理时间等 + +### 3. similarity_search +- **功能**: 基于文本内容进行向量相似度检索 +- **输入参数**: + - `queryText` (string): 查询文本 + - `topK` (integer, 可选): 返回前K个结果,默认5 + - `threshold` (number, 可选): 相似度阈值,默认0.7 +- **返回数据**: 相似工单列表,包括相似度分数、解决方案等 + +### 4. knowledge_query +- **功能**: 在知识库中精确匹配记录 +- **输入参数**: + - `kbId` (string, 可选): 知识库ID + - `sourceRepairId` (string, 可选): 源工单ID +- **返回数据**: 匹配的知识库记录 + +## 使用示例 + +### 1. 获取可用工具 +```bash +GET /api/ai/mcp/tools +``` + +### 2. 生成MCP版本AI回答 +```bash +POST /api/ai/mcp/answer +Content-Type: application/json + +{ + "repairId": "R12345", + "sessionId": "session-123", + "language": "Chinese", + "answerStyle": "professional", + "includeSimilarCases": true, + "includeHistory": true +} +``` + +### 3. 测试特定工具 +```bash +GET /api/ai/mcp/test/repair_query?repairId=R12345 +GET /api/ai/mcp/test/similarity_search?queryText=数据库连接失败 +``` + +## 工作流程 + +### MCP工具调用流程 + +1. **请求分析**: 系统分析用户请求,提取工单ID +2. **工单查询**: 调用 `repair_query` 获取工单基本信息 +3. **反馈查询**: 调用 `repair_feedback_query` 获取处理反馈 +4. **相似度检索**: 如果没有meaningful feedback,调用 `similarity_search` 查找相似案例 +5. **结果整合**: 将所有工具调用结果整合到LLM prompt中 +6. **AI生成**: LLM基于整合后的信息生成回答 + +### 处理优先级 + +1. **最高优先级**: feedback处理结果(经过验证的解决方案) +2. **中等优先级**: 相似案例的解决方案 +3. **最低优先级**: 通用建议和指导 + +## 优势对比 + +### MCP版本优势 + +1. **动态性**: LLM可以根据需要动态调用不同工具 +2. **可扩展性**: 易于添加新的工具和功能 +3. **透明性**: 记录了具体使用了哪些工具 +4. **一致性**: 确保与原版本获得相同的数据源 +5. **可维护性**: 工具调用逻辑独立,便于测试和维护 + +### 原版本问题 + +1. **硬编码**: 数据查询逻辑固化在代码中 +2. **不灵活**: 无法根据具体需求调整查询策略 +3. **难扩展**: 添加新数据源需要修改核心代码 +4. **不透明**: 无法了解具体使用了哪些数据源 + +## 一致性保证 + +MCP版本通过以下措施确保与原版本结果的一致性: + +1. **相同数据源**: 使用完全相同的数据库表和查询逻辑 +2. **相同算法**: 向量相似度计算使用相同的算法 +3. **相同优先级**: 保持feedback > 相似案例 > 通用建议的优先级 +4. **质量对比**: 提供比较接口可以验证两个版本的结果差异 + +## 监控和调试 + +### 日志记录 +- 所有MCP工具调用都有详细的日志记录 +- 包括调用参数、返回结果、执行时间等信息 + +### 性能监控 +- 记录每个工具的执行时间 +- 统计工具调用成功率 +- 监控整体处理时间 + +### 测试接口 +- 提供独立的工具测试接口 +- 支持对比测试原版本和MCP版本的结果 + +## 部署说明 + +1. **配置启用**: 在 `application.yml` 中设置 `ai.mcp.enabled: true` +2. **依赖检查**: 确保所有MCP相关的Bean都能正常注入 +3. **功能验证**: 使用测试接口验证各个工具的功能 +4. **对比测试**: 使用对比接口验证结果一致性 + +## 注意事项 + +1. **条件启用**: MCP服务需要通过配置启用,默认情况下与原版本共存 +2. **API隔离**: MCP版本使用独立的API端点,不影响原有功能 +3. **降级机制**: 如果MCP工具调用失败,会降级到原始请求处理 +4. **性能考虑**: MCP版本由于增加了工具调用,处理时间可能略长 + +## 未来扩展 + +1. **更多工具**: 可以轻松添加新的MCP工具 +2. **缓存优化**: 可以添加更智能的缓存策略 +3. **工具链**: 支持工具之间的依赖和链式调用 +4. **自适应**: 根据历史表现自动选择最佳工具组合 + +这个MCP实现为youfool-devops-gd系统提供了一个现代化、可扩展的AI回答服务架构,既保证了与原有流程的一致性,又为未来的功能扩展奠定了基础。 \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/controller/AIAnswerMCPController.java b/src/main/java/com/chinaweal/youfool/devops/ai/controller/AIAnswerMCPController.java new file mode 100644 index 0000000..80a3c8d --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/controller/AIAnswerMCPController.java @@ -0,0 +1,285 @@ +package com.chinaweal.youfool.devops.ai.controller; + +import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerRequest; +import com.chinaweal.youfool.devops.ai.dto.answer.AIAnswerResponse; +import com.chinaweal.youfool.devops.ai.mcp.MCPServer; +import com.chinaweal.youfool.devops.ai.mcp.MCPTool; +import com.chinaweal.youfool.devops.ai.service.AIAnswerServiceMCP; +import com.youfool.framework.web.RestResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.util.List; + +/** + * AI回答服务控制器 - MCP版本 + * + * 提供基于MCP (Model Context Protocol) 的AI回答服务 + * + * @author AI开发团队 + * @since 1.0.0 + */ +@Slf4j +@RestController +@RequestMapping("/api/ai/mcp") +@RequiredArgsConstructor +@Validated +@Tag(name = "AI回答服务(MCP版本)", description = "基于MCP协议的智能问答服务") +@ConditionalOnProperty(prefix = "ai.mcp", name = "enabled", havingValue = "true") +public class AIAnswerMCPController { + + private final AIAnswerServiceMCP aiAnswerServiceMCP; + private final MCPServer mcpServer; + + /** + * 获取可用的MCP工具列表 + */ + @GetMapping("/tools") + @Operation(summary = "获取可用的MCP工具列表", description = "返回系统中可用的所有MCP工具及其描述") + public RestResult> getAvailableTools() { + try { + List tools = mcpServer.getAvailableTools(); + log.info("返回 {} 个可用的MCP工具", tools.size()); + return RestResult.success(tools); + } catch (Exception e) { + log.error("获取MCP工具列表失败", e); + return RestResult.error("获取MCP工具列表失败: " + e.getMessage()); + } + } + + /** + * 生成AI回答(MCP版本) + */ + @PostMapping("/answer") + @Operation(summary = "生成AI回答(MCP版本)", description = "使用MCP协议生成工单回答,LLM可动态调用工具获取数据") + public RestResult generateAnswerWithMCP( + @Valid @RequestBody AIAnswerRequest request) { + try { + log.info("收到MCP版本AI回答请求: 工单={}, 会话={}", request.getRepairId(), request.getSessionId()); + + // 验证请求参数 + if (request.getRepairId() == null || request.getRepairId().trim().isEmpty()) { + return RestResult.error("工单ID不能为空"); + } + + AIAnswerResponse response = aiAnswerServiceMCP.generateAnswerWithMCP(request); + + if ("completed".equals(response.getStatus())) { + log.info("MCP版本AI回答生成成功: 工单={}, 质量评分={}, 使用工具={}", + request.getRepairId(), + response.getQualityScore(), + response.getMcpToolsUsed()); + return RestResult.success(response); + } else { + log.warn("MCP版本AI回答生成失败: 工单={}, 错误={}", + request.getRepairId(), response.getErrorMessage()); + return RestResult.error(response.getErrorMessage(), response); + } + + } catch (Exception e) { + log.error("MCP版本AI回答生成异常: 工单={}", request.getRepairId(), e); + return RestResult.error("AI回答生成失败: " + e.getMessage()); + } + } + + /** + * 生成AI回答(MCP流式版本) + */ + @PostMapping(value = "/answer/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @Operation(summary = "生成AI回答流式输出(MCP版本)", description = "使用MCP协议生成工单回答的流式输出") + public SseEmitter generateAnswerStreamWithMCP( + @Valid @RequestBody AIAnswerRequest request) { + try { + log.info("收到MCP版本AI回答流式请求: 工单={}, 会话={}", request.getRepairId(), request.getSessionId()); + + // 验证请求参数 + if (request.getRepairId() == null || request.getRepairId().trim().isEmpty()) { + SseEmitter errorEmitter = new SseEmitter(5000L); + try { + errorEmitter.send(SseEmitter.event() + .name("error") + .data("{\"error\":\"工单ID不能为空\",\"type\":\"VALIDATION_ERROR\"}")); + errorEmitter.complete(); + } catch (Exception e) { + errorEmitter.completeWithError(e); + } + return errorEmitter; + } + + return aiAnswerServiceMCP.generateAnswerStreamWithMCP(request); + + } catch (Exception e) { + log.error("MCP版本AI回答流式生成异常: 工单={}", request.getRepairId(), e); + + SseEmitter errorEmitter = new SseEmitter(5000L); + try { + errorEmitter.send(SseEmitter.event() + .name("error") + .data("{\"error\":\"" + e.getMessage() + "\",\"type\":\"MCP_STREAM_ERROR\"}")); + errorEmitter.complete(); + } catch (Exception sendError) { + errorEmitter.completeWithError(sendError); + } + return errorEmitter; + } + } + + /** + * 比较MCP版本与原版本的结果 + */ + @PostMapping("/compare") + @Operation(summary = "比较MCP版本与原版本结果", description = "并行调用MCP版本和原版本,比较结果的一致性") + public RestResult compareWithOriginal( + @Valid @RequestBody AIAnswerRequest request) { + try { + log.info("开始MCP版本与原版本结果比较: 工单={}", request.getRepairId()); + + // 验证请求参数 + if (request.getRepairId() == null || request.getRepairId().trim().isEmpty()) { + return RestResult.error("工单ID不能为空"); + } + + // 调用MCP版本(已经在当前类中) + AIAnswerResponse mcpResponse = aiAnswerServiceMCP.generateAnswerWithMCP(request); + + // 这里应该调用原版本的AIAnswerService,但为了避免循环依赖 + // 暂时只返回MCP版本的结果和分析 + ComparisonResult comparisonResult = new ComparisonResult(); + comparisonResult.setMcpResponse(mcpResponse); + comparisonResult.setMcpToolsUsed(mcpResponse.getMcpToolsUsed()); + comparisonResult.setMcpQuality(mcpResponse.getQualityScore()); + comparisonResult.setMcpConfidence(mcpResponse.getConfidence()); + comparisonResult.setMcpProcessingTime(mcpResponse.getProcessingTimeMs()); + + // 分析MCP版本的特点 + comparisonResult.setAnalysis("MCP版本使用了动态工具调用,获取了实时数据。" + + "使用的工具: " + (mcpResponse.getMcpToolsUsed() != null ? + String.join(", ", mcpResponse.getMcpToolsUsed()) : "无") + + "。质量评分: " + mcpResponse.getQualityScore() + + "。处理时间: " + mcpResponse.getProcessingTimeMs() + "ms"); + + log.info("MCP版本比较完成: 工单={}, MCP质量={}, MCP工具={}", + request.getRepairId(), + mcpResponse.getQualityScore(), + mcpResponse.getMcpToolsUsed()); + + return RestResult.success(comparisonResult); + + } catch (Exception e) { + log.error("MCP版本比较异常: 工单={}", request.getRepairId(), e); + return RestResult.error("版本比较失败: " + e.getMessage()); + } + } + + /** + * 测试MCP工具调用 + */ + @GetMapping("/test/{toolName}") + @Operation(summary = "测试MCP工具调用", description = "直接测试特定MCP工具的调用") + public RestResult testMCPTool( + @Parameter(description = "工具名称") @PathVariable @NotBlank String toolName, + @Parameter(description = "工单ID") @RequestParam(required = false) String repairId, + @Parameter(description = "查询文本") @RequestParam(required = false) String queryText) { + try { + log.info("测试MCP工具调用: 工具={}, 工单ID={}, 查询文本={}", toolName, repairId, queryText); + + java.util.Map arguments = new java.util.HashMap<>(); + + switch (toolName) { + case "repair_query": + case "repair_feedback_query": + if (repairId == null || repairId.trim().isEmpty()) { + return RestResult.error("测试 " + toolName + " 需要提供 repairId 参数"); + } + arguments.put("repairId", repairId); + break; + case "similarity_search": + if (queryText == null || queryText.trim().isEmpty()) { + return RestResult.error("测试 similarity_search 需要提供 queryText 参数"); + } + arguments.put("queryText", queryText); + arguments.put("topK", 5); + arguments.put("threshold", 0.7); + break; + case "knowledge_query": + if (repairId != null) { + arguments.put("sourceRepairId", repairId); + } + break; + default: + return RestResult.error("未知的工具名称: " + toolName); + } + + com.chinaweal.youfool.devops.ai.mcp.MCPResponse response = mcpServer.executeTool(toolName, arguments); + + log.info("MCP工具调用完成: 工具={}, 成功={}", toolName, response.getSuccess()); + + if (response.getSuccess()) { + return RestResult.success(response.getData()); + } else { + return RestResult.error("工具调用失败: " + response.getError(), response); + } + + } catch (Exception e) { + log.error("MCP工具调用测试异常: 工具={}", toolName, e); + return RestResult.error("工具调用测试失败: " + e.getMessage()); + } + } + + /** + * 比较结果类 + */ + public static class ComparisonResult { + private AIAnswerResponse mcpResponse; + private AIAnswerResponse originalResponse; + private List mcpToolsUsed; + private Double mcpQuality; + private Double originalQuality; + private Double mcpConfidence; + private Double originalConfidence; + private Long mcpProcessingTime; + private Long originalProcessingTime; + private String analysis; + + // Getters and setters + public AIAnswerResponse getMcpResponse() { return mcpResponse; } + public void setMcpResponse(AIAnswerResponse mcpResponse) { this.mcpResponse = mcpResponse; } + + public AIAnswerResponse getOriginalResponse() { return originalResponse; } + public void setOriginalResponse(AIAnswerResponse originalResponse) { this.originalResponse = originalResponse; } + + public List getMcpToolsUsed() { return mcpToolsUsed; } + public void setMcpToolsUsed(List mcpToolsUsed) { this.mcpToolsUsed = mcpToolsUsed; } + + public Double getMcpQuality() { return mcpQuality; } + public void setMcpQuality(Double mcpQuality) { this.mcpQuality = mcpQuality; } + + public Double getOriginalQuality() { return originalQuality; } + public void setOriginalQuality(Double originalQuality) { this.originalQuality = originalQuality; } + + public Double getMcpConfidence() { return mcpConfidence; } + public void setMcpConfidence(Double mcpConfidence) { this.mcpConfidence = mcpConfidence; } + + public Double getOriginalConfidence() { return originalConfidence; } + public void setOriginalConfidence(Double originalConfidence) { this.originalConfidence = originalConfidence; } + + public Long getMcpProcessingTime() { return mcpProcessingTime; } + public void setMcpProcessingTime(Long mcpProcessingTime) { this.mcpProcessingTime = mcpProcessingTime; } + + public Long getOriginalProcessingTime() { return originalProcessingTime; } + public void setOriginalProcessingTime(Long originalProcessingTime) { this.originalProcessingTime = originalProcessingTime; } + + public String getAnalysis() { return analysis; } + public void setAnalysis(String analysis) { this.analysis = analysis; } + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatRequest.java b/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatRequest.java index f3a41de..a157808 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatRequest.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatRequest.java @@ -106,4 +106,10 @@ public class ChatRequest { */ @Schema(description = "请求来源", example = "repair-answer") private String source; + + /** + * MCP工具列表(Model Context Protocol) + */ + @Schema(description = "可用的MCP工具列表") + private List mcpTools; } \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatResponse.java b/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatResponse.java index 562f2ae..248ad75 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatResponse.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatResponse.java @@ -86,6 +86,12 @@ public class ChatResponse { @Schema(description = "置信度") private Double confidence; + /** + * 使用的MCP工具列表 + */ + @Schema(description = "使用的MCP工具列表") + private List mcpToolsUsed; + /** * 选择项 */ diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPResponse.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPResponse.java new file mode 100644 index 0000000..bee07ab --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPResponse.java @@ -0,0 +1,90 @@ +package com.chinaweal.youfool.devops.ai.mcp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * MCP响应对象 + * + * @author AI开发团队 + * @since 1.0.0 + */ +@Data +@Schema(description = "MCP响应对象") +public class MCPResponse { + + /** + * 是否成功 + */ + @Schema(description = "是否成功") + private Boolean success; + + /** + * 响应数据 + */ + @Schema(description = "响应数据") + private Object data; + + /** + * 错误信息 + */ + @Schema(description = "错误信息") + private String error; + + /** + * 响应消息 + */ + @Schema(description = "响应消息") + private String message; + + /** + * 执行时间(毫秒) + */ + @Schema(description = "执行时间") + private Long executionTimeMs; + + /** + * 创建成功响应 + */ + public static MCPResponse success(Object data) { + MCPResponse response = new MCPResponse(); + response.setSuccess(true); + response.setData(data); + response.setMessage("执行成功"); + return response; + } + + /** + * 创建成功响应(带消息) + */ + public static MCPResponse success(Object data, String message) { + MCPResponse response = new MCPResponse(); + response.setSuccess(true); + response.setData(data); + response.setMessage(message); + return response; + } + + /** + * 创建错误响应 + */ + public static MCPResponse error(String error) { + MCPResponse response = new MCPResponse(); + response.setSuccess(false); + response.setError(error); + response.setMessage("执行失败"); + return response; + } + + /** + * 创建错误响应(带数据) + */ + public static MCPResponse error(String error, Object data) { + MCPResponse response = new MCPResponse(); + response.setSuccess(false); + response.setError(error); + response.setData(data); + response.setMessage("执行失败"); + return response; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPServer.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPServer.java new file mode 100644 index 0000000..456c4fb --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPServer.java @@ -0,0 +1,366 @@ +package com.chinaweal.youfool.devops.ai.mcp; + +import com.chinaweal.youfool.devops.ai.entity.AIKnowledgeBase; +import com.chinaweal.youfool.devops.ai.mapper.AIKnowledgeBaseMapper; +import com.chinaweal.youfool.devops.ai.migration.dto.SimilarRepair; +import com.chinaweal.youfool.devops.ai.service.QwenEmbeddingService; +import com.chinaweal.youfool.devops.repair.entity.Repair; +import com.chinaweal.youfool.devops.repair.entity.RepairHandle; +import com.chinaweal.youfool.devops.repair.mapper.RepairHandleMapper; +import com.chinaweal.youfool.devops.repair.service.IRepairService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * MCP (Model Context Protocol) 服务器实现 + * + * 提供真正的MCP协议支持,允许LLM动态调用工具获取数据 + * + * @author AI开发团队 + * @since 1.0.0 + */ +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "ai.mcp", name = "enabled", havingValue = "true", matchIfMissing = true) +public class MCPServer { + + private final IRepairService repairService; + private final RepairHandleMapper repairHandleMapper; + private final AIKnowledgeBaseMapper aiKnowledgeBaseMapper; + private final QwenEmbeddingService embeddingService; + private final JdbcTemplate jdbcTemplate; + private final ObjectMapper objectMapper; + + /** + * 获取可用的MCP工具列表 + */ + public List getAvailableTools() { + List tools = new ArrayList<>(); + + // 工具1:根据ID查询工单详情 + MCPTool repairQueryTool = new MCPTool(); + repairQueryTool.setName("repair_query"); + repairQueryTool.setDescription("根据工单ID查询工单详细信息"); + repairQueryTool.setInputSchema(Map.of( + "type", "object", + "properties", Map.of( + "repairId", Map.of("type", "string", "description", "工单ID") + ), + "required", List.of("repairId") + )); + tools.add(repairQueryTool); + + // 工具2:查询工单的feedback处理结果 + MCPTool feedbackQueryTool = new MCPTool(); + feedbackQueryTool.setName("repair_feedback_query"); + feedbackQueryTool.setDescription("查询工单的feedback步骤处理结果"); + feedbackQueryTool.setInputSchema(Map.of( + "type", "object", + "properties", Map.of( + "repairId", Map.of("type", "string", "description", "工单ID") + ), + "required", List.of("repairId") + )); + tools.add(feedbackQueryTool); + + // 工具3:基于文本进行相似度检索 + MCPTool similaritySearchTool = new MCPTool(); + similaritySearchTool.setName("similarity_search"); + similaritySearchTool.setDescription("基于文本内容进行向量相似度检索"); + similaritySearchTool.setInputSchema(Map.of( + "type", "object", + "properties", Map.of( + "queryText", Map.of("type", "string", "description", "查询文本"), + "topK", Map.of("type", "integer", "description", "返回前K个结果", "default", 5), + "threshold", Map.of("type", "number", "description", "相似度阈值", "default", 0.7) + ), + "required", List.of("queryText") + )); + tools.add(similaritySearchTool); + + // 工具4:知识库精确匹配 + MCPTool knowledgeQueryTool = new MCPTool(); + knowledgeQueryTool.setName("knowledge_query"); + knowledgeQueryTool.setDescription("在知识库中精确匹配记录"); + knowledgeQueryTool.setInputSchema(Map.of( + "type", "object", + "properties", Map.of( + "kbId", Map.of("type", "string", "description", "知识库ID"), + "sourceRepairId", Map.of("type", "string", "description", "源工单ID") + ) + )); + tools.add(knowledgeQueryTool); + + return tools; + } + + /** + * 执行MCP工具调用 + */ + public MCPResponse executeTool(String toolName, Map arguments) { + try { + log.info("执行MCP工具调用: {}, 参数: {}", toolName, arguments); + + switch (toolName) { + case "repair_query": + return handleRepairQuery(arguments); + case "repair_feedback_query": + return handleFeedbackQuery(arguments); + case "similarity_search": + return handleSimilaritySearch(arguments); + case "knowledge_query": + return handleKnowledgeQuery(arguments); + default: + return MCPResponse.error("未知的工具: " + toolName); + } + + } catch (Exception e) { + log.error("MCP工具调用失败: {}", e.getMessage(), e); + return MCPResponse.error("工具调用失败: " + e.getMessage()); + } + } + + /** + * 处理工单查询 + */ + private MCPResponse handleRepairQuery(Map arguments) { + String repairId = (String) arguments.get("repairId"); + if (repairId == null || repairId.trim().isEmpty()) { + return MCPResponse.error("工单ID不能为空"); + } + + try { + Repair repair = repairService.getById(repairId); + if (repair == null) { + return MCPResponse.error("工单不存在: " + repairId); + } + + Map result = new HashMap<>(); + result.put("repairId", repair.getRepairId()); + result.put("title", repair.getTitle()); + result.put("faultDescription", repair.getFaultDescription()); + result.put("business", repair.getBusiness()); + result.put("questionType", repair.getQuestionType()); + result.put("priority", repair.getPriority()); + result.put("thinking", repair.getThinking()); + result.put("status", repair.getStatus()); + result.put("createTime", repair.getCreateTime()); + result.put("launchTime", repair.getLaunchTime()); + + log.info("MCP工具 repair_query 成功返回工单: {}", repairId); + return MCPResponse.success(result); + + } catch (Exception e) { + log.error("查询工单失败: {}", repairId, e); + return MCPResponse.error("查询工单失败: " + e.getMessage()); + } + } + + /** + * 处理feedback查询 + */ + private MCPResponse handleFeedbackQuery(Map arguments) { + String repairId = (String) arguments.get("repairId"); + if (repairId == null || repairId.trim().isEmpty()) { + return MCPResponse.error("工单ID不能为空"); + } + + try { + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(RepairHandle::getRepairId, repairId) + .eq(RepairHandle::getStep, "feedback") + .isNotNull(RepairHandle::getResult) + .ne(RepairHandle::getResult, "") + .orderByDesc(RepairHandle::getHappenTime); + + List feedbackList = repairHandleMapper.selectList(query); + + List> results = feedbackList.stream() + .map(handle -> { + Map feedbackInfo = new HashMap<>(); + feedbackInfo.put("result", handle.getResult()); + feedbackInfo.put("handleNickname", handle.getHandleNickname()); + feedbackInfo.put("happenTime", handle.getHappenTime()); + feedbackInfo.put("createTime", handle.getCreateTime()); + return feedbackInfo; + }) + .collect(Collectors.toList()); + + Map response = new HashMap<>(); + response.put("repairId", repairId); + response.put("feedbackCount", results.size()); + response.put("feedbackList", results); + + if (!results.isEmpty()) { + response.put("latestFeedback", results.get(0)); + } + + log.info("MCP工具 repair_feedback_query 成功返回 {} 条feedback记录", results.size()); + return MCPResponse.success(response); + + } catch (Exception e) { + log.error("查询feedback失败: {}", repairId, e); + return MCPResponse.error("查询feedback失败: " + e.getMessage()); + } + } + + /** + * 处理相似度检索 + */ + private MCPResponse handleSimilaritySearch(Map arguments) { + String queryText = (String) arguments.get("queryText"); + Integer topK = (Integer) arguments.getOrDefault("topK", 5); + Double threshold = ((Number) arguments.getOrDefault("threshold", 0.7)).doubleValue(); + + if (queryText == null || queryText.trim().isEmpty()) { + return MCPResponse.error("查询文本不能为空"); + } + + try { + log.info("MCP相似度检索: 查询='{}', topK={}, threshold={}", queryText, topK, threshold); + + // 生成查询向量 + double[] queryEmbedding = embeddingService.getEmbedding(queryText); + if (queryEmbedding == null || queryEmbedding.length == 0) { + return MCPResponse.error("查询文本向量化失败"); + } + + // 查询已向量化的记录 + String sql = """ + SELECT kb_id, title, content, source_repair_id, embedding_json + FROM ai_knowledge_base + WHERE source_type = 'repair' + AND embedding_json IS NOT NULL + AND embedding_json != '' + ORDER BY created_time DESC + """; + + List> records = jdbcTemplate.queryForList(sql); + List similarities = new ArrayList<>(); + + for (Map record : records) { + try { + String embeddingJson = (String) record.get("embedding_json"); + if (embeddingJson == null || embeddingJson.trim().isEmpty()) { + continue; + } + + // 解析向量并计算相似度 + double[] storedEmbedding = objectMapper.readValue(embeddingJson, double[].class); + double similarity = calculateCosineSimilarity(queryEmbedding, storedEmbedding); + + if (similarity >= threshold) { + SimilarRepair similarRepair = new SimilarRepair(); + String sourceRepairId = (String) record.get("source_repair_id"); + similarRepair.setRepairId(sourceRepairId); + similarRepair.setTitle((String) record.get("title")); + similarRepair.setContent((String) record.get("content")); + similarRepair.setSimilarity(similarity); + + // 查询feedback解决方案 + String feedbackSql = "SELECT result FROM repair_handle WHERE repair_id = ? AND step = 'feedback' AND result IS NOT NULL AND result != '' ORDER BY happen_time DESC LIMIT 1"; + List> feedbackResults = jdbcTemplate.queryForList(feedbackSql, sourceRepairId); + if (!feedbackResults.isEmpty()) { + similarRepair.setSolution((String) feedbackResults.get(0).get("result")); + } + + similarities.add(similarRepair); + } + } catch (Exception e) { + log.debug("处理相似度记录失败: {}", e.getMessage()); + } + } + + // 排序并取TopK + List result = similarities.stream() + .sorted((a, b) -> Double.compare(b.getSimilarity(), a.getSimilarity())) + .limit(topK) + .collect(Collectors.toList()); + + Map response = new HashMap<>(); + response.put("queryText", queryText); + response.put("totalMatches", similarities.size()); + response.put("returnedCount", result.size()); + response.put("similarRepairs", result); + + log.info("MCP相似度检索完成: 找到{}条匹配记录,返回{}条", similarities.size(), result.size()); + return MCPResponse.success(response); + + } catch (Exception e) { + log.error("相似度检索失败: {}", e.getMessage(), e); + return MCPResponse.error("相似度检索失败: " + e.getMessage()); + } + } + + /** + * 处理知识库查询 + */ + private MCPResponse handleKnowledgeQuery(Map arguments) { + String kbId = (String) arguments.get("kbId"); + String sourceRepairId = (String) arguments.get("sourceRepairId"); + + try { + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(AIKnowledgeBase::getIsDeleted, false) + .orderByDesc(AIKnowledgeBase::getCreatedTime); + + if (kbId != null && !kbId.trim().isEmpty()) { + query.eq(AIKnowledgeBase::getKbId, kbId); + } + if (sourceRepairId != null && !sourceRepairId.trim().isEmpty()) { + query.eq(AIKnowledgeBase::getSourceRepairId, sourceRepairId); + } + + if (kbId == null && sourceRepairId == null) { + return MCPResponse.error("至少需要提供kbId或sourceRepairId其中之一"); + } + + List results = aiKnowledgeBaseMapper.selectList(query); + + Map response = new HashMap<>(); + response.put("matchCount", results.size()); + response.put("knowledgeRecords", results); + + log.info("MCP知识库查询完成: 找到{}条记录", results.size()); + return MCPResponse.success(response); + + } catch (Exception e) { + log.error("知识库查询失败: {}", e.getMessage(), e); + return MCPResponse.error("知识库查询失败: " + e.getMessage()); + } + } + + /** + * 计算余弦相似度 + */ + private double calculateCosineSimilarity(double[] vectorA, double[] vectorB) { + if (vectorA.length != vectorB.length) { + return 0.0; + } + + double dotProduct = 0.0; + double normA = 0.0; + double normB = 0.0; + + for (int i = 0; i < vectorA.length; i++) { + dotProduct += vectorA[i] * vectorB[i]; + normA += vectorA[i] * vectorA[i]; + normB += vectorB[i] * vectorB[i]; + } + + if (normA == 0.0 || normB == 0.0) { + return 0.0; + } + + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPTool.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPTool.java new file mode 100644 index 0000000..d5f138a --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPTool.java @@ -0,0 +1,47 @@ +package com.chinaweal.youfool.devops.ai.mcp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Map; + +/** + * MCP工具定义 + * + * @author AI开发团队 + * @since 1.0.0 + */ +@Data +@Schema(description = "MCP工具定义") +public class MCPTool { + + /** + * 工具名称 + */ + @Schema(description = "工具名称") + private String name; + + /** + * 工具描述 + */ + @Schema(description = "工具描述") + private String description; + + /** + * 输入参数模式(JSON Schema) + */ + @Schema(description = "输入参数模式") + private Map inputSchema; + + /** + * 工具类型 + */ + @Schema(description = "工具类型") + private String type = "function"; + + /** + * 是否必需的工具 + */ + @Schema(description = "是否必需的工具") + private Boolean required = false; +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/migration/RepairVectorizationService.java b/src/main/java/com/chinaweal/youfool/devops/ai/migration/RepairVectorizationService.java index 4b0d310..b89195e 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/migration/RepairVectorizationService.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/migration/RepairVectorizationService.java @@ -214,8 +214,7 @@ public class RepairVectorizationService { wrapper.gt(Repair::getUpdateTime, since); } - wrapper.orderByDesc(Repair::getUpdateTime); - + // 注意:COUNT查询不能包含ORDER BY,需要先进行计数查询 Integer totalCount = repairMapper.selectCount(wrapper); progress.setTotalCount(totalCount); progress.setStatus("processing"); @@ -231,6 +230,9 @@ public class RepairVectorizationService { int pageSize = batchSize; int totalPages = (int) Math.ceil((double) totalCount / pageSize); + // 为分页查询添加ORDER BY子句(在COUNT查询完成后) + wrapper.orderByDesc(Repair::getUpdateTime); + for (int page = 1; page <= totalPages; page++) { try { Page pageData = new Page<>(page, pageSize); @@ -658,11 +660,24 @@ public class RepairVectorizationService { if (similarity >= threshold) { aboveThresholdCount++; SimilarRepair similarRepair = new SimilarRepair(); - similarRepair.setRepairId((String) record.get("source_repair_id")); + String sourceRepairId = (String) record.get("source_repair_id"); + similarRepair.setRepairId(sourceRepairId); similarRepair.setTitle((String) record.get("title")); similarRepair.setContent((String) record.get("content")); similarRepair.setSimilarity(similarity); + // 查询并添加feedback解决方案信息 + try { + String feedbackSql = "SELECT result FROM repair_handle WHERE repair_id = ? AND step = 'feedback' AND result IS NOT NULL AND result != '' ORDER BY happen_time DESC LIMIT 1"; + List> feedbackResults = jdbcTemplate.queryForList(feedbackSql, sourceRepairId); + if (!feedbackResults.isEmpty()) { + String feedbackResult = (String) feedbackResults.get(0).get("result"); + similarRepair.setSolution(feedbackResult); + } + } catch (Exception e) { + log.debug("查询工单 {} 的feedback信息失败: {}", sourceRepairId, e.getMessage()); + } + similarities.add(similarRepair); } @@ -695,9 +710,16 @@ public class RepairVectorizationService { log.info("最终返回 {} 条结果 (请求topK={})", result.size(), topK); for (int i = 0; i < result.size(); i++) { SimilarRepair repair = result.get(i); - log.info(" 结果[{}]: ID={}, 相似度={}, 标题='{}'", + String title = repair.getTitle() != null ? repair.getTitle().substring(0, Math.min(50, repair.getTitle().length())) : "无标题"; + String solution = repair.getSolution() != null ? + repair.getSolution().substring(0, Math.min(100, repair.getSolution().length())) : "无处理结果"; + if (repair.getSolution() != null && repair.getSolution().length() > 100) { + solution += "..."; + } + + log.info(" 结果[{}]: ID={}, 相似度={}, 标题='{}', 处理结果='{}'", i+1, repair.getRepairId(), String.format("%.4f", repair.getSimilarity()), - repair.getTitle() != null ? repair.getTitle().substring(0, Math.min(50, repair.getTitle().length())) : "无标题"); + title, solution); } log.info("=== RepairVectorizationService.findSimilarRepairs 结束 ==="); diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerService.java b/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerService.java index c9ecd00..c884c16 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerService.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerService.java @@ -85,6 +85,19 @@ public class AIAnswerService { // 构建聊天请求 ChatRequest chatRequest = buildChatRequest(repair, request); + // 调试日志:输出发送给LLM的完整prompt内容 + log.info("=== LLM Prompt 调试信息 ==="); + if (chatRequest.getMessages() != null) { + for (ChatMessage message : chatRequest.getMessages()) { + if ("system".equals(message.getRole())) { + log.info("系统提示词: {}", message.getContent()); + } else if ("user".equals(message.getRole())) { + log.info("用户消息: {}", message.getContent()); + } + } + } + log.info("=== LLM Prompt 结束 ==="); + // 获取相似案例(如果启用) List similarCases = new ArrayList<>(); if (request.getIncludeSimilarCases()) { @@ -144,6 +157,19 @@ public class AIAnswerService { ChatRequest chatRequest = buildChatRequest(repair, request); chatRequest.setStream(true); + // 调试日志:输出发送给LLM的完整prompt内容(流式版本) + log.info("=== LLM Stream Prompt 调试信息 ==="); + if (chatRequest.getMessages() != null) { + for (ChatMessage message : chatRequest.getMessages()) { + if ("system".equals(message.getRole())) { + log.info("流式系统提示词: {}", message.getContent()); + } else if ("user".equals(message.getRole())) { + log.info("流式用户消息: {}", message.getContent()); + } + } + } + log.info("=== LLM Stream Prompt 结束 ==="); + // 调用流式LLM服务 return qwenChatService.streamChatCompletion(chatRequest); @@ -168,6 +194,55 @@ public class AIAnswerService { } } + /** + * 为工单增强feedback信息 + */ + private Repair enhanceRepairWithFeedback(Repair repair) { + try { + // 查询该工单的feedback信息 + LambdaQueryWrapper handleQuery = new LambdaQueryWrapper() + .eq(RepairHandle::getRepairId, repair.getRepairId()) + .eq(RepairHandle::getStep, "feedback") + .isNotNull(RepairHandle::getResult) + .ne(RepairHandle::getResult, "") + .orderByDesc(RepairHandle::getHappenTime); + + List handleList = repairHandleMapper.selectList(handleQuery); + + if (!handleList.isEmpty()) { + RepairHandle latestHandle = handleList.get(0); + String feedbackResult = latestHandle.getResult(); + + // 构建增强的故障描述 + StringBuilder enhancedDescription = new StringBuilder(); + enhancedDescription.append("【最终解决方案】:\n"); + enhancedDescription.append(feedbackResult).append("\n\n"); + enhancedDescription.append("=== 以上是经过验证的解决方案,仔细阅读和考虑该解决方案是否能解决用户的问题,如果可以请直接基于此回复用户 ===\n\n"); + enhancedDescription.append("=== 如果方案中不包含任何能解答用户问题的信息,或是要求用户联系工作人员,**请直接回复**:问题已递交人工处理,感谢您的反馈 ===\n\n"); + // 添加原始故障描述作为参考 + enhancedDescription.append("--- 原始问题描述 ---\n"); + if (repair.getFaultDescription() != null) { + enhancedDescription.append(repair.getFaultDescription()); + } + + // 更新工单描述 + repair.setFaultDescription(enhancedDescription.toString()); + + log.info("为工单 {} 增强了feedback信息: {}", + repair.getRepairId(), + feedbackResult.substring(0, Math.min(100, feedbackResult.length()))); + } else { + log.warn("工单 {} 没有找到feedback记录", repair.getRepairId()); + } + + return repair; + + } catch (Exception e) { + log.error("为工单增强feedback信息失败: {}", repair.getRepairId(), e); + return repair; // 返回原始工单 + } + } + /** * 获取工单详情 - 支持智能查询 * 正确流程: @@ -180,8 +255,15 @@ public class AIAnswerService { // 1. 首先尝试直接查询repair表(兼容旧逻辑) Repair directRepair = repairService.getById(inputId); if (directRepair != null) { - log.debug("直接查询到工单: {}", inputId); - return directRepair; + log.info("直接查询到工单: {},标题: {},故障描述: {}", + inputId, directRepair.getTitle(), + directRepair.getFaultDescription() != null ? + directRepair.getFaultDescription().substring(0, Math.min(200, directRepair.getFaultDescription().length())) : "无"); + + // ⚠️ 关键问题:直接返回的工单没有经过processKnowledgeBaseResult处理, + // 因此不会包含【最终解决方案】标记! + // 我们需要对直接查询到的工单也进行feedback信息增强 + return enhanceRepairWithFeedback(directRepair); } // 2. 尝试通过知识库ID精确匹配 @@ -322,8 +404,22 @@ public class AIAnswerService { throw new RuntimeException("源工单不存在: " + sourceRepairId); } - // 3. 构建增强的工单信息 + // 3. 构建增强的工单信息 - 优先突出feedback信息 StringBuilder enhancedDescription = new StringBuilder(); + + // **最优先显示处理反馈结果** + if (!handleList.isEmpty()) { + RepairHandle latestHandle = handleList.get(0); + String feedbackResult = latestHandle.getResult(); + enhancedDescription.append("【最终解决方案】(请优先使用此方案):\n"); + enhancedDescription.append(feedbackResult).append("\n\n"); + enhancedDescription.append("=== 以上是经过验证的解决方案,请直接基于此回复用户 ===\n\n"); + } else { + log.warn("未找到工单的反馈记录: repairId={}", sourceRepairId); + } + + // 其他参考信息(次要) + enhancedDescription.append("--- 补充参考信息 ---\n"); enhancedDescription.append("匹配方式:").append(matchType).append("\n"); enhancedDescription.append("知识库条目:").append(kbEntry.getTitle()).append("\n\n"); @@ -332,28 +428,18 @@ public class AIAnswerService { enhancedDescription.append(repair.getFaultDescription()).append("\n\n"); } - // 添加知识库内容 + // 添加知识库内容(作为参考) if (kbEntry.getContent() != null && !kbEntry.getContent().trim().isEmpty()) { - enhancedDescription.append("知识库内容:\n"); + enhancedDescription.append("知识库内容(参考):\n"); enhancedDescription.append(kbEntry.getContent()).append("\n\n"); } - // 添加解决步骤 + // 添加解决步骤(作为参考) if (kbEntry.getSolutionSteps() != null && !kbEntry.getSolutionSteps().trim().isEmpty()) { - enhancedDescription.append("解决步骤:\n"); + enhancedDescription.append("通用解决步骤(参考):\n"); enhancedDescription.append(kbEntry.getSolutionSteps()).append("\n\n"); } - // 添加处理反馈 - if (!handleList.isEmpty()) { - RepairHandle latestHandle = handleList.get(0); - String feedbackResult = latestHandle.getResult(); - enhancedDescription.append("处理反馈结果:\n"); - enhancedDescription.append(feedbackResult); - } else { - log.warn("未找到工单的反馈记录: repairId={}", sourceRepairId); - } - // 更新工单描述 repair.setFaultDescription(enhancedDescription.toString()); @@ -466,10 +552,10 @@ public class AIAnswerService { prompt.append("你是一个运维服务助手,帮助用户解决技术问题。\n\n"); prompt.append("核心处理原则:\n"); - prompt.append("1. **优先使用历史解决方案**:如果工单信息中包含\"处理反馈结果\",且与用户问题相符,请直接提供该解决方案\n"); - prompt.append("2. **简洁明了**:回复应简短、直接,避免冗长的技术分析\n"); - prompt.append("3. **用户友好**:使用通俗易懂的语言,避免专业术语\n"); - prompt.append("4. **明确指导**:提供具体的操作步骤或联系方式\n\n"); + prompt.append("1. **绝对优先使用最终解决方案**:如果看到\"【最终解决方案】\"标记,必须直接基于此内容回复,忽略其他信息\n"); + prompt.append("2. **原样传递关键信息**:如果最终解决方案包含具体要求(如\"请提供姓名、手机号\"等),必须完整传递给用户\n"); + prompt.append("3. **简洁明了**:回复应简短、直接,避免自行推测或技术分析\n"); + prompt.append("4. **优先级顺序**:最终解决方案 > 其他所有信息\n\n"); prompt.append("回答格式要求:\n"); prompt.append("- 如果有明确的解决方案:直接提供解决步骤(不超过3-5句话)\n"); @@ -525,7 +611,7 @@ public class AIAnswerService { } } - context.append("\n注意:如果上述信息中包含\"处理反馈结果\"且与用户问题匹配,请优先基于该反馈提供简洁的解决方案。"); + context.append("\n【重要提醒】:如果上述信息中包含\"【最终解决方案】\",请严格按照该方案内容回复,不要自行修改或简化。所有具体要求(如联系方式、需要提供的信息等)都必须完整传递给用户。"); return context.toString(); } 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 new file mode 100644 index 0000000..3a00a19 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/service/AIAnswerServiceMCP.java @@ -0,0 +1,339 @@ +package com.chinaweal.youfool.devops.ai.service; + +import com.chinaweal.youfool.devops.ai.aspect.annotation.TrackedAIGeneration; +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.mcp.MCPServer; +import com.chinaweal.youfool.devops.ai.mcp.MCPTool; +import com.chinaweal.youfool.devops.ai.mcp.MCPResponse; +import com.chinaweal.youfool.devops.util.ErrorLogUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * AI回答服务 - MCP版本 + * + * 支持真正的MCP协议调用,LLM可以动态调用MCP工具获取数据 + * + * @author AI开发团队 + * @since 1.0.0 + */ +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "ai.mcp", name = "enabled", havingValue = "true") +public class AIAnswerServiceMCP { + + private final QwenChatService qwenChatService; + private final MCPServer mcpServer; + + /** + * 生成工单AI回答(MCP版本) + */ + @TrackedAIGeneration + public AIAnswerResponse generateAnswerWithMCP(AIAnswerRequest request) { + AIAnswerResponse response = new AIAnswerResponse(); + response.setSessionId(request.getSessionId()); + response.setRepairId(request.getRepairId()); + response.setStartTime(LocalDateTime.now()); + response.setStatus("processing"); + + try { + log.info("开始MCP版本AI回答生成,工单: {}, 会话: {}", + request.getRepairId(), request.getSessionId()); + + // 构建支持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()); + } + } + } + log.info("=== MCP版本 LLM Prompt 结束 ==="); + + // 调用支持MCP的LLM服务 + ChatResponse chatResponse = qwenChatService.chatCompletionWithMCP(chatRequest, mcpServer); + + // 解析响应 + parseAndSetResponse(response, chatResponse, request); + + // 设置MCP工具使用情况 + if (chatResponse.getMcpToolsUsed() != null) { + response.setMcpToolsUsed(chatResponse.getMcpToolsUsed()); + } + + response.setStatus("completed"); + response.setEndTime(LocalDateTime.now()); + response.setProcessingTimeMs( + java.time.Duration.between(response.getStartTime(), response.getEndTime()).toMillis()); + + log.info("MCP版本AI回答生成成功,工单: {}, 使用工具数: {}", + request.getRepairId(), + response.getMcpToolsUsed() != null ? response.getMcpToolsUsed().size() : 0); + + return response; + + } catch (Exception e) { + log.error("MCP版本AI回答生成失败,工单: {}", request.getRepairId(), e); + ErrorLogUtils.saveRuntimeError("AIAnswerServiceMCP.generateAnswerWithMCP", e, + "repairId: " + request.getRepairId()); + + response.setStatus("failed"); + response.setErrorCode("MCP_GENERATION_ERROR"); + response.setErrorMessage(e.getMessage()); + response.setEndTime(LocalDateTime.now()); + + return response; + } + } + + /** + * 生成工单AI回答(MCP流式版本) + */ + @TrackedAIGeneration + public SseEmitter generateAnswerStreamWithMCP(AIAnswerRequest request) { + try { + log.info("开始MCP版本AI回答流式生成,工单: {}, 会话: {}", + request.getRepairId(), request.getSessionId()); + + // 构建支持MCP的聊天请求 + ChatRequest chatRequest = buildMCPChatRequest(request); + chatRequest.setStream(true); + + // 调试日志:输出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()); + } + } + } + log.info("=== MCP版本流式 LLM Prompt 结束 ==="); + + // 调用支持MCP的流式LLM服务 + return qwenChatService.streamChatCompletionWithMCP(chatRequest, mcpServer); + + } catch (Exception e) { + log.error("MCP版本AI回答流式生成失败,工单: {}", request.getRepairId(), e); + ErrorLogUtils.saveRuntimeError("AIAnswerServiceMCP.generateAnswerStreamWithMCP", e, + "repairId: " + request.getRepairId()); + + // 返回错误流 + SseEmitter errorEmitter = new SseEmitter(5000L); + try { + errorEmitter.send(SseEmitter.event() + .name("error") + .data("{\"error\":\"" + e.getMessage() + "\",\"type\":\"MCP_GENERATION_ERROR\"}")); + errorEmitter.complete(); + } catch (Exception sendError) { + log.error("发送MCP错误消息失败", sendError); + errorEmitter.completeWithError(sendError); + } + + return errorEmitter; + } + } + + /** + * 构建支持MCP的聊天请求 + */ + private ChatRequest buildMCPChatRequest(AIAnswerRequest request) { + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setSessionId(request.getSessionId()); + chatRequest.setUserId(request.getUserId()); + chatRequest.setSource("repair-answer-mcp"); + chatRequest.setTemperature(request.getTemperature()); + chatRequest.setMaxTokens(request.getMaxTokens()); + + List messages = new ArrayList<>(); + + // 系统提示词 - MCP版本 + String systemPrompt = buildMCPSystemPrompt(request); + messages.add(ChatMessage.system(systemPrompt)); + + // 用户查询 - MCP版本 + String userPrompt = buildMCPUserPrompt(request); + messages.add(ChatMessage.user(userPrompt)); + + chatRequest.setMessages(messages); + + // 设置可用的MCP工具 + List availableTools = mcpServer.getAvailableTools(); + chatRequest.setMcpTools(availableTools); + + return chatRequest; + } + + /** + * 构建MCP版本系统提示词 + */ + private String buildMCPSystemPrompt(AIAnswerRequest request) { + StringBuilder prompt = new StringBuilder(); + + prompt.append("你是一个运维服务助手,帮助用户解决技术问题。\n\n"); + + prompt.append("你可以使用以下MCP工具来获取所需信息:\n"); + prompt.append("1. **repair_query**: 根据工单ID查询工单详细信息\n"); + prompt.append("2. **repair_feedback_query**: 查询工单的feedback步骤处理结果(重要:这包含经过验证的解决方案)\n"); + prompt.append("3. **similarity_search**: 基于文本内容进行向量相似度检索,查找类似问题\n"); + prompt.append("4. **knowledge_query**: 在知识库中精确匹配记录\n\n"); + + prompt.append("处理流程和原则:\n"); + prompt.append("1. **首先**:使用 repair_query 工具获取工单基本信息\n"); + prompt.append("2. **然后**:使用 repair_feedback_query 工具查询该工单的反馈结果\n"); + prompt.append("3. **如果有feedback结果**:直接基于feedback内容回复,这是经过验证的最终解决方案\n"); + prompt.append("4. **如果没有feedback**:使用 similarity_search 工具查找相似案例,寻找可参考的解决方案\n"); + prompt.append("5. **绝对优先级**:feedback解决方案 > 相似案例解决方案 > 通用建议\n\n"); + + prompt.append("回答要求:\n"); + prompt.append("- 如果有明确的解决方案:直接提供解决步骤(不超过3-5句话)\n"); + prompt.append("- 如果问题已解决:说明\"该问题已处理完成\"并提供解决结果\n"); + prompt.append("- 如果信息不足或无法解决:告知\"您的工单已提交人工处理,技术人员将尽快联系您\"\n"); + prompt.append("- 使用").append(request.getLanguage()).append("语言\n"); + + // 语调设置 + switch (request.getAnswerStyle()) { + case "simple": + prompt.append("- 语调:简单直接\n"); + break; + case "friendly": + prompt.append("- 语调:友好亲切\n"); + break; + default: + prompt.append("- 语调:专业简洁\n"); + break; + } + + prompt.append("\n**重要提醒**:必须使用MCP工具获取信息,不要基于假设回答问题。\n"); + + return prompt.toString(); + } + + /** + * 构建MCP版本用户提示词 + */ + private String buildMCPUserPrompt(AIAnswerRequest request) { + StringBuilder prompt = new StringBuilder(); + + prompt.append("请帮我处理工单ID为 '").append(request.getRepairId()).append("' 的问题。\n\n"); + + prompt.append("请按照以下步骤操作:\n"); + prompt.append("1. 首先使用 repair_query 工具查询工单详情\n"); + prompt.append("2. 然后使用 repair_feedback_query 工具查询是否有处理反馈\n"); + prompt.append("3. 如果有feedback,请直接基于feedback内容回复\n"); + prompt.append("4. 如果没有feedback,请使用 similarity_search 工具查找相似案例\n"); + prompt.append("5. 最后基于获取的信息给出专业的解决建议\n\n"); + + if (request.getIncludeSimilarCases()) { + prompt.append("请在回答中包含相似案例信息。\n\n"); + } + + if (request.getIncludeHistory()) { + prompt.append("请在分析时考虑历史处理记录。\n\n"); + } + + prompt.append("记住:用户需要的是解决方案,不是技术分析过程。请提供清晰、可操作的指导。"); + + return prompt.toString(); + } + + /** + * 解析并设置响应 + */ + private void parseAndSetResponse(AIAnswerResponse response, ChatResponse chatResponse, AIAnswerRequest request) { + if (chatResponse.getChoices() != null && !chatResponse.getChoices().isEmpty()) { + ChatResponse.Choice choice = chatResponse.getChoices().get(0); + if (choice.getMessage() != null) { + response.setAnswer(choice.getMessage().getContent()); + } + } + + // 设置Token使用统计 + if (chatResponse.getUsage() != null) { + AIAnswerResponse.TokenUsage tokenUsage = new AIAnswerResponse.TokenUsage(); + tokenUsage.setInputTokens(chatResponse.getUsage().getPromptTokens()); + tokenUsage.setOutputTokens(chatResponse.getUsage().getCompletionTokens()); + tokenUsage.setTotalTokens(chatResponse.getUsage().getTotalTokens()); + + // 简单的成本估算 + double estimatedCost = tokenUsage.getTotalTokens() * 0.0001; + tokenUsage.setEstimatedCost(estimatedCost); + + response.setTokenUsage(tokenUsage); + } + + // 计算基础质量指标 + calculateBasicQualityMetrics(response); + } + + /** + * 计算基础质量指标 + */ + private void calculateBasicQualityMetrics(AIAnswerResponse response) { + double qualityScore = 0.7; // MCP版本基础分数更高 + double confidence = 0.8; // MCP版本置信度更高 + + // 根据MCP工具使用情况调整 + if (response.getMcpToolsUsed() != null && !response.getMcpToolsUsed().isEmpty()) { + qualityScore += 0.2; + confidence += 0.1; + + // 如果使用了feedback查询,进一步提升评分 + boolean usesFeedback = response.getMcpToolsUsed().stream() + .anyMatch(tool -> "repair_feedback_query".equals(tool)); + if (usesFeedback) { + qualityScore += 0.1; + confidence += 0.1; + } + } + + // 根据回答长度调整 + if (response.getAnswer() != null) { + int answerLength = response.getAnswer().length(); + if (answerLength > 100 && answerLength < 2000) { + qualityScore += 0.1; + confidence += 0.05; + } + } + + response.setQualityScore(Math.min(1.0, qualityScore)); + response.setConfidence(Math.min(1.0, confidence)); + + // MCP版本很少需要人工处理 + boolean recommendManual = response.getConfidence() < 0.5; + response.setRecommendManualProcessing(recommendManual); + + if (recommendManual) { + response.setManualProcessingReason("MCP工具调用失败或无法获取足够信息"); + } + + // 生成建议操作 + List suggestedActions = new ArrayList<>(); + suggestedActions.add("MCP工具已提供最新信息"); + if (response.getMcpToolsUsed() != null && response.getMcpToolsUsed().contains("repair_feedback_query")) { + suggestedActions.add("已获取经过验证的解决方案"); + } + suggestedActions.add("记录解决过程以便后续参考"); + + response.setSuggestedActions(suggestedActions); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/service/QwenChatService.java b/src/main/java/com/chinaweal/youfool/devops/ai/service/QwenChatService.java index f3649a8..11fd3ea 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/service/QwenChatService.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/service/QwenChatService.java @@ -4,6 +4,8 @@ import com.chinaweal.youfool.devops.ai.aspect.annotation.TrackedAIGeneration; import com.chinaweal.youfool.devops.ai.config.LLMChatProperties; import com.chinaweal.youfool.devops.ai.config.LLMStreamingProperties; import com.chinaweal.youfool.devops.ai.dto.llm.*; +import com.chinaweal.youfool.devops.ai.mcp.MCPServer; +import com.chinaweal.youfool.devops.ai.mcp.MCPResponse; import com.chinaweal.youfool.devops.util.ErrorLogUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -66,12 +68,37 @@ public class QwenChatService { @RateLimiter(name = "qwen-api") @Retry(name = "qwen-api") public ChatResponse chatCompletion(ChatRequest request) { + return chatCompletionInternal(request, null); + } + + /** + * 非流式聊天完成(支持MCP) + */ + @TrackedAIGeneration + @CircuitBreaker(name = "qwen-api", fallbackMethod = "fallbackChatCompletion") + @RateLimiter(name = "qwen-api") + @Retry(name = "qwen-api") + public ChatResponse chatCompletionWithMCP(ChatRequest request, MCPServer mcpServer) { + return chatCompletionInternal(request, mcpServer); + } + + /** + * 内部聊天完成实现 + */ + private ChatResponse chatCompletionInternal(ChatRequest request, MCPServer mcpServer) { try { log.debug("Processing chat completion request for session: {}", request.getSessionId()); // 参数验证和预处理 validateAndPreprocessRequest(request); + // 如果提供了MCP服务器,执行MCP工具调用逻辑 + List usedMcpTools = new ArrayList<>(); + if (mcpServer != null && request.getMcpTools() != null && !request.getMcpTools().isEmpty()) { + log.info("开始MCP工具调用流程,会话: {}", request.getSessionId()); + request = processMCPToolCalls(request, mcpServer, usedMcpTools); + } + // 构建API请求 Map apiRequest = buildApiRequest(request, false); @@ -83,6 +110,12 @@ public class QwenChatService { // 解析响应 ChatResponse chatResponse = parseApiResponse(response.getBody(), processingTime); + // 设置MCP工具使用记录 + if (!usedMcpTools.isEmpty()) { + chatResponse.setMcpToolsUsed(usedMcpTools); + log.info("MCP工具调用完成,使用的工具: {}", usedMcpTools); + } + // 计算质量评分 calculateQualityMetrics(chatResponse, request); @@ -106,6 +139,23 @@ public class QwenChatService { @CircuitBreaker(name = "qwen-api", fallbackMethod = "fallbackStreamChatCompletion") @RateLimiter(name = "qwen-api") public SseEmitter streamChatCompletion(ChatRequest request) { + return streamChatCompletionInternal(request, null); + } + + /** + * 流式聊天完成(支持MCP) + */ + @TrackedAIGeneration + @CircuitBreaker(name = "qwen-api", fallbackMethod = "fallbackStreamChatCompletion") + @RateLimiter(name = "qwen-api") + public SseEmitter streamChatCompletionWithMCP(ChatRequest request, MCPServer mcpServer) { + return streamChatCompletionInternal(request, mcpServer); + } + + /** + * 内部流式聊天完成实现 + */ + private SseEmitter streamChatCompletionInternal(ChatRequest request, MCPServer mcpServer) { String sessionId = request.getSessionId(); if (sessionId == null) { sessionId = UUID.randomUUID().toString(); @@ -603,4 +653,324 @@ public class QwenChatService { public boolean hasError() { return hasError; } public void setHasError(boolean hasError) { this.hasError = hasError; } } + + /** + * 处理MCP工具调用 + */ + private ChatRequest processMCPToolCalls(ChatRequest request, MCPServer mcpServer, List usedMcpTools) { + try { + log.info("开始MCP工具调用处理,会话: {}", request.getSessionId()); + + // 基于用户请求分析需要调用的工具 + String userMessage = extractUserMessage(request); + String repairId = extractRepairId(userMessage); + + if (repairId == null || repairId.trim().isEmpty()) { + log.warn("无法从请求中提取工单ID,跳过MCP工具调用"); + return request; + } + + StringBuilder mcpResults = new StringBuilder(); + mcpResults.append("=== MCP工具调用结果 ===\n\n"); + + // 1. 查询工单基本信息 + Map repairQueryArgs = Map.of("repairId", repairId); + MCPResponse repairResponse = mcpServer.executeTool("repair_query", repairQueryArgs); + + if (repairResponse.getSuccess()) { + usedMcpTools.add("repair_query"); + mcpResults.append("📋 **工单基本信息**:\n"); + mcpResults.append(formatRepairInfo(repairResponse.getData())).append("\n\n"); + log.info("MCP工具 repair_query 调用成功"); + } else { + log.warn("MCP工具 repair_query 调用失败: {}", repairResponse.getError()); + } + + // 2. 查询feedback处理结果 + MCPResponse feedbackResponse = mcpServer.executeTool("repair_feedback_query", repairQueryArgs); + + if (feedbackResponse.getSuccess()) { + usedMcpTools.add("repair_feedback_query"); + mcpResults.append("🔧 **处理反馈信息**:\n"); + mcpResults.append(formatFeedbackInfo(feedbackResponse.getData())).append("\n\n"); + log.info("MCP工具 repair_feedback_query 调用成功"); + } else { + log.warn("MCP工具 repair_feedback_query 调用失败: {}", feedbackResponse.getError()); + mcpResults.append("❌ **处理反馈信息**:暂无反馈记录\n\n"); + } + + // 3. 如果没有feedback,进行相似度检索 + if (!feedbackResponse.getSuccess() || !hasMeaningfulFeedback(feedbackResponse.getData())) { + String queryText = buildSimilarityQueryText(repairResponse.getData()); + Map similarityArgs = Map.of( + "queryText", queryText, + "topK", 3, + "threshold", 0.7 + ); + + MCPResponse similarityResponse = mcpServer.executeTool("similarity_search", similarityArgs); + + if (similarityResponse.getSuccess()) { + usedMcpTools.add("similarity_search"); + mcpResults.append("🔍 **相似案例信息**:\n"); + mcpResults.append(formatSimilarityInfo(similarityResponse.getData())).append("\n\n"); + log.info("MCP工具 similarity_search 调用成功"); + } else { + log.warn("MCP工具 similarity_search 调用失败: {}", similarityResponse.getError()); + mcpResults.append("❌ **相似案例信息**:未找到相似案例\n\n"); + } + } + + mcpResults.append("=== MCP工具调用结束 ===\n\n"); + + // 将MCP结果添加到用户消息中 + List messages = new ArrayList<>(request.getMessages()); + String originalUserMessage = userMessage; + String enhancedUserMessage = mcpResults.toString() + "基于以上MCP工具获取的信息,请回答用户的问题:\n\n" + originalUserMessage; + + // 更新最后一条用户消息 + for (int i = messages.size() - 1; i >= 0; i--) { + ChatMessage message = messages.get(i); + if ("user".equals(message.getRole())) { + message.setContent(enhancedUserMessage); + break; + } + } + + request.setMessages(messages); + log.info("MCP工具调用处理完成,使用工具: {}", usedMcpTools); + + return request; + + } catch (Exception e) { + log.error("MCP工具调用处理失败: {}", e.getMessage(), e); + return request; // 失败时返回原始请求 + } + } + + /** + * 提取用户消息 + */ + private String extractUserMessage(ChatRequest request) { + if (request.getMessages() == null) return ""; + + for (int i = request.getMessages().size() - 1; i >= 0; i--) { + ChatMessage message = request.getMessages().get(i); + if ("user".equals(message.getRole())) { + return message.getContent(); + } + } + return ""; + } + + /** + * 从消息中提取工单ID + */ + private String extractRepairId(String message) { + if (message == null) return null; + + // 简单的正则匹配,查找工单ID模式 + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("工单ID为\\s*['\"]?([^'\"\\s]+)['\"]?"); + java.util.regex.Matcher matcher = pattern.matcher(message); + + if (matcher.find()) { + return matcher.group(1); + } + + // 尝试其他模式 + pattern = java.util.regex.Pattern.compile("工单[::]\\s*([^\\s]+)"); + matcher = pattern.matcher(message); + + if (matcher.find()) { + return matcher.group(1); + } + + return null; + } + + /** + * 格式化工单信息 + */ + private String formatRepairInfo(Object data) { + if (data == null) return "无工单信息"; + + try { + @SuppressWarnings("unchecked") + Map repairInfo = (Map) data; + + StringBuilder info = new StringBuilder(); + info.append("- **工单ID**: ").append(repairInfo.get("repairId")).append("\n"); + info.append("- **标题**: ").append(repairInfo.get("title")).append("\n"); + info.append("- **业务模块**: ").append(repairInfo.get("business")).append("\n"); + info.append("- **问题类型**: ").append(repairInfo.get("questionType")).append("\n"); + info.append("- **优先级**: ").append(repairInfo.get("priority")).append("\n"); + + String faultDescription = (String) repairInfo.get("faultDescription"); + if (faultDescription != null && !faultDescription.trim().isEmpty()) { + String shortDesc = faultDescription.length() > 200 ? + faultDescription.substring(0, 200) + "..." : faultDescription; + info.append("- **故障描述**: ").append(shortDesc).append("\n"); + } + + return info.toString(); + + } catch (Exception e) { + log.warn("格式化工单信息失败: {}", e.getMessage()); + return "工单信息格式化失败"; + } + } + + /** + * 格式化反馈信息 + */ + private String formatFeedbackInfo(Object data) { + if (data == null) return "暂无反馈信息"; + + try { + @SuppressWarnings("unchecked") + Map feedbackInfo = (Map) data; + + Integer feedbackCount = (Integer) feedbackInfo.get("feedbackCount"); + if (feedbackCount == null || feedbackCount == 0) { + return "暂无处理反馈记录"; + } + + StringBuilder info = new StringBuilder(); + info.append("- **反馈记录数**: ").append(feedbackCount).append("条\n"); + + @SuppressWarnings("unchecked") + Map latestFeedback = (Map) feedbackInfo.get("latestFeedback"); + if (latestFeedback != null) { + String result = (String) latestFeedback.get("result"); + String handleNickname = (String) latestFeedback.get("handleNickname"); + + info.append("- **最新处理结果**: ").append(result != null ? result : "无").append("\n"); + info.append("- **处理人**: ").append(handleNickname != null ? handleNickname : "未知").append("\n"); + } + + return info.toString(); + + } catch (Exception e) { + log.warn("格式化反馈信息失败: {}", e.getMessage()); + return "反馈信息格式化失败"; + } + } + + /** + * 格式化相似度信息 + */ + private String formatSimilarityInfo(Object data) { + if (data == null) return "暂无相似案例"; + + try { + @SuppressWarnings("unchecked") + Map similarityInfo = (Map) data; + + Integer totalMatches = (Integer) similarityInfo.get("totalMatches"); + Integer returnedCount = (Integer) similarityInfo.get("returnedCount"); + + StringBuilder info = new StringBuilder(); + info.append("- **匹配案例总数**: ").append(totalMatches != null ? totalMatches : 0).append("条\n"); + info.append("- **返回案例数**: ").append(returnedCount != null ? returnedCount : 0).append("条\n\n"); + + @SuppressWarnings("unchecked") + List> similarRepairs = (List>) similarityInfo.get("similarRepairs"); + + if (similarRepairs != null && !similarRepairs.isEmpty()) { + for (int i = 0; i < Math.min(3, similarRepairs.size()); i++) { + Map repair = similarRepairs.get(i); + info.append("**案例 ").append(i + 1).append("**:\n"); + info.append(" - 工单ID: ").append(repair.get("repairId")).append("\n"); + info.append(" - 标题: ").append(repair.get("title")).append("\n"); + info.append(" - 相似度: ").append(String.format("%.2f%%", ((Double) repair.get("similarity")) * 100)).append("\n"); + + String solution = (String) repair.get("solution"); + if (solution != null && !solution.trim().isEmpty()) { + String shortSolution = solution.length() > 150 ? + solution.substring(0, 150) + "..." : solution; + info.append(" - 解决方案: ").append(shortSolution).append("\n"); + } + info.append("\n"); + } + } + + return info.toString(); + + } catch (Exception e) { + log.warn("格式化相似度信息失败: {}", e.getMessage()); + return "相似案例信息格式化失败"; + } + } + + /** + * 检查是否有有意义的反馈 + */ + private boolean hasMeaningfulFeedback(Object data) { + if (data == null) return false; + + try { + @SuppressWarnings("unchecked") + Map feedbackInfo = (Map) data; + + Integer feedbackCount = (Integer) feedbackInfo.get("feedbackCount"); + if (feedbackCount == null || feedbackCount == 0) { + return false; + } + + @SuppressWarnings("unchecked") + Map latestFeedback = (Map) feedbackInfo.get("latestFeedback"); + if (latestFeedback == null) { + return false; + } + + String result = (String) latestFeedback.get("result"); + return result != null && !result.trim().isEmpty() && result.length() > 10; + + } catch (Exception e) { + return false; + } + } + + /** + * 构建相似度查询文本 + */ + private String buildSimilarityQueryText(Object repairData) { + if (repairData == null) return ""; + + try { + @SuppressWarnings("unchecked") + Map repairInfo = (Map) repairData; + + StringBuilder queryText = new StringBuilder(); + + String title = (String) repairInfo.get("title"); + if (title != null && !title.trim().isEmpty()) { + queryText.append(title).append(" "); + } + + String faultDescription = (String) repairInfo.get("faultDescription"); + if (faultDescription != null && !faultDescription.trim().isEmpty()) { + // 只取前300个字符用于相似度查询 + String shortDesc = faultDescription.length() > 300 ? + faultDescription.substring(0, 300) : faultDescription; + queryText.append(shortDesc).append(" "); + } + + String business = (String) repairInfo.get("business"); + if (business != null && !business.trim().isEmpty()) { + queryText.append("业务模块: ").append(business).append(" "); + } + + String questionType = (String) repairInfo.get("questionType"); + if (questionType != null && !questionType.trim().isEmpty()) { + queryText.append("问题类型: ").append(questionType).append(" "); + } + + return queryText.toString().trim(); + + } catch (Exception e) { + log.warn("构建相似度查询文本失败: {}", e.getMessage()); + return ""; + } + } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8c7db8d..bfbcd93 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -237,6 +237,39 @@ ai: auto-generate: true # 是否异步处理 async-processing: true + + # MCP (Model Context Protocol) 配置 + mcp: + # 是否启用MCP服务 + enabled: true + # MCP服务器名称 + server-name: youfool-devops-mcp + # MCP版本 + version: 1.0.0 + # 工具执行超时时间(毫秒) + tool-timeout: 30000 + # 是否记录工具调用日志 + log-tool-calls: true + # 工具调用结果缓存配置 + cache: + enabled: true + ttl: 300000 # 5分钟缓存 + # 可用工具配置 + tools: + repair-query: + enabled: true + description: "查询工单详细信息" + repair-feedback-query: + enabled: true + description: "查询工单feedback处理结果" + similarity-search: + enabled: true + description: "基于文本相似度搜索" + default-top-k: 5 + default-threshold: 0.7 + knowledge-query: + enabled: true + description: "知识库精确查询" # 质量阈值 quality-threshold: 0.7 # 最大处理时间(毫秒)