实现真正的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")
|
@Schema(description = "请求来源", example = "repair-answer")
|
||||||
private String source;
|
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 = "置信度")
|
@Schema(description = "置信度")
|
||||||
private Double confidence;
|
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.gt(Repair::getUpdateTime, since);
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper.orderByDesc(Repair::getUpdateTime);
|
// 注意:COUNT查询不能包含ORDER BY,需要先进行计数查询
|
||||||
|
|
||||||
Integer totalCount = repairMapper.selectCount(wrapper);
|
Integer totalCount = repairMapper.selectCount(wrapper);
|
||||||
progress.setTotalCount(totalCount);
|
progress.setTotalCount(totalCount);
|
||||||
progress.setStatus("processing");
|
progress.setStatus("processing");
|
||||||
|
|
@ -231,6 +230,9 @@ public class RepairVectorizationService {
|
||||||
int pageSize = batchSize;
|
int pageSize = batchSize;
|
||||||
int totalPages = (int) Math.ceil((double) totalCount / pageSize);
|
int totalPages = (int) Math.ceil((double) totalCount / pageSize);
|
||||||
|
|
||||||
|
// 为分页查询添加ORDER BY子句(在COUNT查询完成后)
|
||||||
|
wrapper.orderByDesc(Repair::getUpdateTime);
|
||||||
|
|
||||||
for (int page = 1; page <= totalPages; page++) {
|
for (int page = 1; page <= totalPages; page++) {
|
||||||
try {
|
try {
|
||||||
Page<Repair> pageData = new Page<>(page, pageSize);
|
Page<Repair> pageData = new Page<>(page, pageSize);
|
||||||
|
|
@ -658,11 +660,24 @@ public class RepairVectorizationService {
|
||||||
if (similarity >= threshold) {
|
if (similarity >= threshold) {
|
||||||
aboveThresholdCount++;
|
aboveThresholdCount++;
|
||||||
SimilarRepair similarRepair = new SimilarRepair();
|
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.setTitle((String) record.get("title"));
|
||||||
similarRepair.setContent((String) record.get("content"));
|
similarRepair.setContent((String) record.get("content"));
|
||||||
similarRepair.setSimilarity(similarity);
|
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);
|
similarities.add(similarRepair);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -695,9 +710,16 @@ public class RepairVectorizationService {
|
||||||
log.info("最终返回 {} 条结果 (请求topK={})", result.size(), topK);
|
log.info("最终返回 {} 条结果 (请求topK={})", result.size(), topK);
|
||||||
for (int i = 0; i < result.size(); i++) {
|
for (int i = 0; i < result.size(); i++) {
|
||||||
SimilarRepair repair = result.get(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()),
|
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 结束 ===");
|
log.info("=== RepairVectorizationService.findSimilarRepairs 结束 ===");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,19 @@ public class AIAnswerService {
|
||||||
// 构建聊天请求
|
// 构建聊天请求
|
||||||
ChatRequest chatRequest = buildChatRequest(repair, request);
|
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<>();
|
List<AIAnswerResponse.SimilarCase> similarCases = new ArrayList<>();
|
||||||
if (request.getIncludeSimilarCases()) {
|
if (request.getIncludeSimilarCases()) {
|
||||||
|
|
@ -144,6 +157,19 @@ public class AIAnswerService {
|
||||||
ChatRequest chatRequest = buildChatRequest(repair, request);
|
ChatRequest chatRequest = buildChatRequest(repair, request);
|
||||||
chatRequest.setStream(true);
|
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服务
|
// 调用流式LLM服务
|
||||||
return qwenChatService.streamChatCompletion(chatRequest);
|
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表(兼容旧逻辑)
|
// 1. 首先尝试直接查询repair表(兼容旧逻辑)
|
||||||
Repair directRepair = repairService.getById(inputId);
|
Repair directRepair = repairService.getById(inputId);
|
||||||
if (directRepair != null) {
|
if (directRepair != null) {
|
||||||
log.debug("直接查询到工单: {}", inputId);
|
log.info("直接查询到工单: {},标题: {},故障描述: {}",
|
||||||
return directRepair;
|
inputId, directRepair.getTitle(),
|
||||||
|
directRepair.getFaultDescription() != null ?
|
||||||
|
directRepair.getFaultDescription().substring(0, Math.min(200, directRepair.getFaultDescription().length())) : "无");
|
||||||
|
|
||||||
|
// ⚠️ 关键问题:直接返回的工单没有经过processKnowledgeBaseResult处理,
|
||||||
|
// 因此不会包含【最终解决方案】标记!
|
||||||
|
// 我们需要对直接查询到的工单也进行feedback信息增强
|
||||||
|
return enhanceRepairWithFeedback(directRepair);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 尝试通过知识库ID精确匹配
|
// 2. 尝试通过知识库ID精确匹配
|
||||||
|
|
@ -322,8 +404,22 @@ public class AIAnswerService {
|
||||||
throw new RuntimeException("源工单不存在: " + sourceRepairId);
|
throw new RuntimeException("源工单不存在: " + sourceRepairId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 构建增强的工单信息
|
// 3. 构建增强的工单信息 - 优先突出feedback信息
|
||||||
StringBuilder enhancedDescription = new StringBuilder();
|
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(matchType).append("\n");
|
||||||
enhancedDescription.append("知识库条目:").append(kbEntry.getTitle()).append("\n\n");
|
enhancedDescription.append("知识库条目:").append(kbEntry.getTitle()).append("\n\n");
|
||||||
|
|
||||||
|
|
@ -332,28 +428,18 @@ public class AIAnswerService {
|
||||||
enhancedDescription.append(repair.getFaultDescription()).append("\n\n");
|
enhancedDescription.append(repair.getFaultDescription()).append("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加知识库内容
|
// 添加知识库内容(作为参考)
|
||||||
if (kbEntry.getContent() != null && !kbEntry.getContent().trim().isEmpty()) {
|
if (kbEntry.getContent() != null && !kbEntry.getContent().trim().isEmpty()) {
|
||||||
enhancedDescription.append("知识库内容:\n");
|
enhancedDescription.append("知识库内容(参考):\n");
|
||||||
enhancedDescription.append(kbEntry.getContent()).append("\n\n");
|
enhancedDescription.append(kbEntry.getContent()).append("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加解决步骤
|
// 添加解决步骤(作为参考)
|
||||||
if (kbEntry.getSolutionSteps() != null && !kbEntry.getSolutionSteps().trim().isEmpty()) {
|
if (kbEntry.getSolutionSteps() != null && !kbEntry.getSolutionSteps().trim().isEmpty()) {
|
||||||
enhancedDescription.append("解决步骤:\n");
|
enhancedDescription.append("通用解决步骤(参考):\n");
|
||||||
enhancedDescription.append(kbEntry.getSolutionSteps()).append("\n\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());
|
repair.setFaultDescription(enhancedDescription.toString());
|
||||||
|
|
||||||
|
|
@ -466,10 +552,10 @@ public class AIAnswerService {
|
||||||
prompt.append("你是一个运维服务助手,帮助用户解决技术问题。\n\n");
|
prompt.append("你是一个运维服务助手,帮助用户解决技术问题。\n\n");
|
||||||
|
|
||||||
prompt.append("核心处理原则:\n");
|
prompt.append("核心处理原则:\n");
|
||||||
prompt.append("1. **优先使用历史解决方案**:如果工单信息中包含\"处理反馈结果\",且与用户问题相符,请直接提供该解决方案\n");
|
prompt.append("1. **绝对优先使用最终解决方案**:如果看到\"【最终解决方案】\"标记,必须直接基于此内容回复,忽略其他信息\n");
|
||||||
prompt.append("2. **简洁明了**:回复应简短、直接,避免冗长的技术分析\n");
|
prompt.append("2. **原样传递关键信息**:如果最终解决方案包含具体要求(如\"请提供姓名、手机号\"等),必须完整传递给用户\n");
|
||||||
prompt.append("3. **用户友好**:使用通俗易懂的语言,避免专业术语\n");
|
prompt.append("3. **简洁明了**:回复应简短、直接,避免自行推测或技术分析\n");
|
||||||
prompt.append("4. **明确指导**:提供具体的操作步骤或联系方式\n\n");
|
prompt.append("4. **优先级顺序**:最终解决方案 > 其他所有信息\n\n");
|
||||||
|
|
||||||
prompt.append("回答格式要求:\n");
|
prompt.append("回答格式要求:\n");
|
||||||
prompt.append("- 如果有明确的解决方案:直接提供解决步骤(不超过3-5句话)\n");
|
prompt.append("- 如果有明确的解决方案:直接提供解决步骤(不超过3-5句话)\n");
|
||||||
|
|
@ -525,7 +611,7 @@ public class AIAnswerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.append("\n注意:如果上述信息中包含\"处理反馈结果\"且与用户问题匹配,请优先基于该反馈提供简洁的解决方案。");
|
context.append("\n【重要提醒】:如果上述信息中包含\"【最终解决方案】\",请严格按照该方案内容回复,不要自行修改或简化。所有具体要求(如联系方式、需要提供的信息等)都必须完整传递给用户。");
|
||||||
|
|
||||||
return context.toString();
|
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.LLMChatProperties;
|
||||||
import com.chinaweal.youfool.devops.ai.config.LLMStreamingProperties;
|
import com.chinaweal.youfool.devops.ai.config.LLMStreamingProperties;
|
||||||
import com.chinaweal.youfool.devops.ai.dto.llm.*;
|
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.chinaweal.youfool.devops.util.ErrorLogUtils;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
@ -66,12 +68,37 @@ public class QwenChatService {
|
||||||
@RateLimiter(name = "qwen-api")
|
@RateLimiter(name = "qwen-api")
|
||||||
@Retry(name = "qwen-api")
|
@Retry(name = "qwen-api")
|
||||||
public ChatResponse chatCompletion(ChatRequest request) {
|
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 {
|
try {
|
||||||
log.debug("Processing chat completion request for session: {}", request.getSessionId());
|
log.debug("Processing chat completion request for session: {}", request.getSessionId());
|
||||||
|
|
||||||
// 参数验证和预处理
|
// 参数验证和预处理
|
||||||
validateAndPreprocessRequest(request);
|
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请求
|
// 构建API请求
|
||||||
Map<String, Object> apiRequest = buildApiRequest(request, false);
|
Map<String, Object> apiRequest = buildApiRequest(request, false);
|
||||||
|
|
||||||
|
|
@ -83,6 +110,12 @@ public class QwenChatService {
|
||||||
// 解析响应
|
// 解析响应
|
||||||
ChatResponse chatResponse = parseApiResponse(response.getBody(), processingTime);
|
ChatResponse chatResponse = parseApiResponse(response.getBody(), processingTime);
|
||||||
|
|
||||||
|
// 设置MCP工具使用记录
|
||||||
|
if (!usedMcpTools.isEmpty()) {
|
||||||
|
chatResponse.setMcpToolsUsed(usedMcpTools);
|
||||||
|
log.info("MCP工具调用完成,使用的工具: {}", usedMcpTools);
|
||||||
|
}
|
||||||
|
|
||||||
// 计算质量评分
|
// 计算质量评分
|
||||||
calculateQualityMetrics(chatResponse, request);
|
calculateQualityMetrics(chatResponse, request);
|
||||||
|
|
||||||
|
|
@ -106,6 +139,23 @@ public class QwenChatService {
|
||||||
@CircuitBreaker(name = "qwen-api", fallbackMethod = "fallbackStreamChatCompletion")
|
@CircuitBreaker(name = "qwen-api", fallbackMethod = "fallbackStreamChatCompletion")
|
||||||
@RateLimiter(name = "qwen-api")
|
@RateLimiter(name = "qwen-api")
|
||||||
public SseEmitter streamChatCompletion(ChatRequest request) {
|
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();
|
String sessionId = request.getSessionId();
|
||||||
if (sessionId == null) {
|
if (sessionId == null) {
|
||||||
sessionId = UUID.randomUUID().toString();
|
sessionId = UUID.randomUUID().toString();
|
||||||
|
|
@ -603,4 +653,324 @@ public class QwenChatService {
|
||||||
public boolean hasError() { return hasError; }
|
public boolean hasError() { return hasError; }
|
||||||
public void setHasError(boolean hasError) { this.hasError = 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
|
auto-generate: true
|
||||||
# 是否异步处理
|
# 是否异步处理
|
||||||
async-processing: 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
|
quality-threshold: 0.7
|
||||||
# 最大处理时间(毫秒)
|
# 最大处理时间(毫秒)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue