实现真正的MCP (Model Context Protocol) 版本
不改动原有流程的基础上,新增了支持动态工具调用的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: 详细的实现文档和使用指南
这个实现确保了结果的一致性,同时为未来的功能扩展提供了更灵活的架构基础。
This commit is contained in:
parent
88c6fd4220
commit
293198b12d
|
|
@ -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回答服务架构,既保证了与原有流程的一致性,又为未来的功能扩展奠定了基础。
|
||||
|
|
@ -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<List<MCPTool>> getAvailableTools() {
|
||||
try {
|
||||
List<MCPTool> 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<AIAnswerResponse> 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<ComparisonResult> 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<Object> 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<String, Object> 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<String> 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<String> getMcpToolsUsed() { return mcpToolsUsed; }
|
||||
public void setMcpToolsUsed(List<String> 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -106,4 +106,10 @@ public class ChatRequest {
|
|||
*/
|
||||
@Schema(description = "请求来源", example = "repair-answer")
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* MCP工具列表(Model Context Protocol)
|
||||
*/
|
||||
@Schema(description = "可用的MCP工具列表")
|
||||
private List<com.chinaweal.youfool.devops.ai.mcp.MCPTool> mcpTools;
|
||||
}
|
||||
|
|
@ -86,6 +86,12 @@ public class ChatResponse {
|
|||
@Schema(description = "置信度")
|
||||
private Double confidence;
|
||||
|
||||
/**
|
||||
* 使用的MCP工具列表
|
||||
*/
|
||||
@Schema(description = "使用的MCP工具列表")
|
||||
private List<String> mcpToolsUsed;
|
||||
|
||||
/**
|
||||
* 选择项
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MCPTool> getAvailableTools() {
|
||||
List<MCPTool> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> arguments) {
|
||||
String repairId = (String) arguments.get("repairId");
|
||||
if (repairId == null || repairId.trim().isEmpty()) {
|
||||
return MCPResponse.error("工单ID不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
LambdaQueryWrapper<RepairHandle> query = new LambdaQueryWrapper<RepairHandle>()
|
||||
.eq(RepairHandle::getRepairId, repairId)
|
||||
.eq(RepairHandle::getStep, "feedback")
|
||||
.isNotNull(RepairHandle::getResult)
|
||||
.ne(RepairHandle::getResult, "")
|
||||
.orderByDesc(RepairHandle::getHappenTime);
|
||||
|
||||
List<RepairHandle> feedbackList = repairHandleMapper.selectList(query);
|
||||
|
||||
List<Map<String, Object>> results = feedbackList.stream()
|
||||
.map(handle -> {
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<Map<String, Object>> records = jdbcTemplate.queryForList(sql);
|
||||
List<SimilarRepair> similarities = new ArrayList<>();
|
||||
|
||||
for (Map<String, Object> 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<Map<String, Object>> 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<SimilarRepair> result = similarities.stream()
|
||||
.sorted((a, b) -> Double.compare(b.getSimilarity(), a.getSimilarity()))
|
||||
.limit(topK)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> 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<String, Object> arguments) {
|
||||
String kbId = (String) arguments.get("kbId");
|
||||
String sourceRepairId = (String) arguments.get("sourceRepairId");
|
||||
|
||||
try {
|
||||
LambdaQueryWrapper<AIKnowledgeBase> query = new LambdaQueryWrapper<AIKnowledgeBase>()
|
||||
.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<AIKnowledgeBase> results = aiKnowledgeBaseMapper.selectList(query);
|
||||
|
||||
Map<String, Object> 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Object> inputSchema;
|
||||
|
||||
/**
|
||||
* 工具类型
|
||||
*/
|
||||
@Schema(description = "工具类型")
|
||||
private String type = "function";
|
||||
|
||||
/**
|
||||
* 是否必需的工具
|
||||
*/
|
||||
@Schema(description = "是否必需的工具")
|
||||
private Boolean required = false;
|
||||
}
|
||||
|
|
@ -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<Repair> 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<Map<String, Object>> 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 结束 ===");
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AIAnswerResponse.SimilarCase> 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<RepairHandle> handleQuery = new LambdaQueryWrapper<RepairHandle>()
|
||||
.eq(RepairHandle::getRepairId, repair.getRepairId())
|
||||
.eq(RepairHandle::getStep, "feedback")
|
||||
.isNotNull(RepairHandle::getResult)
|
||||
.ne(RepairHandle::getResult, "")
|
||||
.orderByDesc(RepairHandle::getHappenTime);
|
||||
|
||||
List<RepairHandle> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ChatMessage> 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<MCPTool> 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<String> suggestedActions = new ArrayList<>();
|
||||
suggestedActions.add("MCP工具已提供最新信息");
|
||||
if (response.getMcpToolsUsed() != null && response.getMcpToolsUsed().contains("repair_feedback_query")) {
|
||||
suggestedActions.add("已获取经过验证的解决方案");
|
||||
}
|
||||
suggestedActions.add("记录解决过程以便后续参考");
|
||||
|
||||
response.setSuggestedActions(suggestedActions);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> usedMcpTools = new ArrayList<>();
|
||||
if (mcpServer != null && request.getMcpTools() != null && !request.getMcpTools().isEmpty()) {
|
||||
log.info("开始MCP工具调用流程,会话: {}", request.getSessionId());
|
||||
request = processMCPToolCalls(request, mcpServer, usedMcpTools);
|
||||
}
|
||||
|
||||
// 构建API请求
|
||||
Map<String, Object> 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<String> 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<String, Object> 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<String, Object> 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<ChatMessage> 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<String, Object> repairInfo = (Map<String, Object>) 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<String, Object> feedbackInfo = (Map<String, Object>) 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<String, Object> latestFeedback = (Map<String, Object>) 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<String, Object> similarityInfo = (Map<String, Object>) 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<Map<String, Object>> similarRepairs = (List<Map<String, Object>>) similarityInfo.get("similarRepairs");
|
||||
|
||||
if (similarRepairs != null && !similarRepairs.isEmpty()) {
|
||||
for (int i = 0; i < Math.min(3, similarRepairs.size()); i++) {
|
||||
Map<String, Object> 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<String, Object> feedbackInfo = (Map<String, Object>) data;
|
||||
|
||||
Integer feedbackCount = (Integer) feedbackInfo.get("feedbackCount");
|
||||
if (feedbackCount == null || feedbackCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> latestFeedback = (Map<String, Object>) 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<String, Object> repairInfo = (Map<String, Object>) 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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
# 最大处理时间(毫秒)
|
||||
|
|
|
|||
Loading…
Reference in New Issue