| src/main/java/com/zy/ai/controller/WcsDiagnosisController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/entity/ChatCompletionRequest.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/mcp/controller/McpController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/LlmChatService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/PythonService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/WcsDiagnosisService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/utils/AiPromptUtils.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/utils/AiUtils.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/application.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/ai/diagnosis.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
@@ -2,7 +2,6 @@ import com.zy.ai.entity.ChatCompletionRequest; import com.zy.ai.entity.WcsDiagnosisRequest; import com.zy.ai.entity.WcsDiagnosisResponse; import com.zy.ai.service.WcsDiagnosisService; import com.zy.ai.utils.AiUtils; import com.zy.common.web.BaseController; @@ -24,13 +23,6 @@ private WcsDiagnosisService wcsDiagnosisService; @Autowired private AiUtils aiUtils; @GetMapping("/runAi") public WcsDiagnosisResponse runAi() { WcsDiagnosisRequest request = aiUtils.makeAiRequest(1000, "系统当前不执行任务,但具体原因不明,请根据以下信息帮助判断。\n\n"); WcsDiagnosisResponse response = diagnose(request); return response; } @GetMapping("/runAiStream") public SseEmitter runAiStream() { @@ -77,18 +69,5 @@ @GetMapping("/chats/{chatId}/history") public List<ChatCompletionRequest.Message> getChatHistory(@PathVariable("chatId") String chatId) { return wcsDiagnosisService.getChatHistory(chatId); } /** * POST /api/ai/diagnose/wcs */ @PostMapping("/wcs") public WcsDiagnosisResponse diagnose(@RequestBody WcsDiagnosisRequest request) { String analysis = wcsDiagnosisService.diagnose(request); WcsDiagnosisResponse resp = new WcsDiagnosisResponse(); resp.setAnalysis(analysis); resp.setOriginalRequest(request); return resp; } } src/main/java/com/zy/ai/entity/ChatCompletionRequest.java
@@ -13,10 +13,28 @@ private Double temperature; private Integer max_tokens; private Boolean stream; private List<Object> tools; private Object tool_choice; @Data public static class Message { private String role; // "user" / "assistant" / "system" private String content; private String name; private String tool_call_id; private List<ToolCall> tool_calls; } @Data public static class ToolCall { private String id; private String type; private Function function; } @Data public static class Function { private String name; private String arguments; } } src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java
@@ -13,10 +13,10 @@ public static void registerAll(ToolRegistry registry, final WcsDataFacade facade) { registry.register(tool( "device.get_crn_status", "Query realtime status of a crn device by deviceNo.", "device_get_crn_status", "通过堆垛机编号查询堆垛机设备实时数据", schemaObj( propInt("crnNos", true) propArr("crnNos", true, "integer") ), schemaObj( propObj("devices", true) @@ -29,8 +29,8 @@ )); registry.register(tool( "device.get_station_status", "Query realtime status of a station device", "device_get_station_status", "查询输送线站点设备实时数据", schemaObj( ), @@ -45,10 +45,10 @@ )); registry.register(tool( "device.get_rgv_status", "Query realtime status of a rgv device by deviceNo.", "device_get_rgv_status", "通过RGV编号查询RGV设备实时数据", schemaObj( propInt("rgvNos", true) propArr("rgvNos", true, "integer") ), schemaObj( propObj("devices", true) @@ -61,8 +61,8 @@ )); registry.register(tool( "task.list", "List tasks by filters (status/CrnDevice/RgvDevice//time window).", "task_list", "通过筛选条件查询任务数据", schemaObj( propInt("crnNo", false), propInt("rgvNo", false), @@ -78,8 +78,8 @@ )); registry.register(tool( "log.query", "Query logs by keyword/level/time window/device/task. Return clipped log lines.", "log_query", "通过筛选条件查询日志数据", schemaObj( propInt("limit", false) ), @@ -92,8 +92,8 @@ )); registry.register(tool( "config.get_device_config", "Get device config by deviceCode.", "config_get_device_config", "通过设备编号查询设备配置数据", schemaObj( propArr("crnNos", false, "integer"), propArr("rgvNos", false, "integer"), @@ -108,8 +108,8 @@ )); registry.register(tool( "config.get_system_config", "Get key system configs for diagnosis.", "config_get_system_config", "查询系统配置数据", schemaObj( ), @@ -229,4 +229,4 @@ m.put("items", items); return m; } } } src/main/java/com/zy/ai/mcp/controller/McpController.java
@@ -87,4 +87,19 @@ return JsonRpcResponse.err(id, -32000, "Server error", e.getMessage()); } } } public List<Map<String, Object>> listTools() { return registry.listTools(); } public Object callTool(String toolName, JSONObject arguments) throws Exception { if (toolName == null || toolName.trim().isEmpty()) { throw new IllegalArgumentException("missing tool name"); } ToolDefinition def = registry.get(toolName); if (def == null) { throw new IllegalArgumentException("tool not found: " + toolName); } return def.getHandler().handle(arguments == null ? new JSONObject() : arguments); } } src/main/java/com/zy/ai/service/LlmChatService.java
@@ -12,6 +12,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Flux; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; import java.util.concurrent.LinkedBlockingQueue; @@ -33,6 +34,9 @@ @Value("${llm.model}") private String model; @Value("${llm.pythonPlatformUrl}") private String pythonPlatformUrl; /** * 通用对话方法:传入 messages,返回大模型文本回复 @@ -74,6 +78,47 @@ } return response.getChoices().get(0).getMessage().getContent(); } public ChatCompletionResponse chatCompletion(List<ChatCompletionRequest.Message> messages, Double temperature, Integer maxTokens, List<Object> tools) { ChatCompletionRequest req = new ChatCompletionRequest(); req.setModel(model); req.setMessages(messages); req.setTemperature(temperature != null ? temperature : 0.3); req.setMax_tokens(maxTokens != null ? maxTokens : 1024); req.setStream(false); if (tools != null && !tools.isEmpty()) { req.setTools(tools); req.setTool_choice("auto"); } return complete(req); } public ChatCompletionResponse complete(ChatCompletionRequest req) { try { return llmWebClient.post() .uri("/chat/completions") .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM) .bodyValue(req) .exchangeToMono(resp -> resp.bodyToFlux(String.class) .collectList() .map(list -> { String payload = String.join("\n\n", list); return parseCompletion(payload); })) .doOnError(ex -> log.error("调用 LLM 失败", ex)) .onErrorResume(ex -> Mono.empty()) .block(); } catch (Exception e) { log.error("调用 LLM 失败", e); return null; } } public void chatStream(List<ChatCompletionRequest.Message> messages, @@ -172,6 +217,207 @@ }); } public void chatStreamWithTools(List<ChatCompletionRequest.Message> messages, Double temperature, Integer maxTokens, List<Object> tools, Consumer<String> onChunk, Runnable onComplete, Consumer<Throwable> onError) { ChatCompletionRequest req = new ChatCompletionRequest(); req.setModel(model); req.setMessages(messages); req.setTemperature(temperature != null ? temperature : 0.3); req.setMax_tokens(maxTokens != null ? maxTokens : 1024); req.setStream(true); if (tools != null && !tools.isEmpty()) { req.setTools(tools); req.setTool_choice("auto"); } Flux<String> flux = llmWebClient.post() .uri("/chat/completions") .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.TEXT_EVENT_STREAM) .bodyValue(req) .retrieve() .bodyToFlux(String.class) .doOnError(ex -> log.error("调用 LLM 流式失败", ex)); AtomicBoolean doneSeen = new AtomicBoolean(false); AtomicBoolean errorSeen = new AtomicBoolean(false); LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); Thread drain = new Thread(() -> { try { while (true) { String s = queue.poll(5, TimeUnit.SECONDS); if (s != null) { try { onChunk.accept(s); } catch (Exception ignore) {} } if (doneSeen.get() && queue.isEmpty()) { if (!errorSeen.get()) { try { if (onComplete != null) onComplete.run(); } catch (Exception ignore) {} } break; } } } catch (InterruptedException ignore) { ignore.printStackTrace(); } }); drain.setDaemon(true); drain.start(); flux.subscribe(payload -> { if (payload == null || payload.isEmpty()) return; String[] events = payload.split("\\r?\\n\\r?\\n"); for (String part : events) { String s = part; if (s == null || s.isEmpty()) continue; if (s.startsWith("data:")) { s = s.substring(5); if (s.startsWith(" ")) s = s.substring(1); } if ("[DONE]".equals(s.trim())) { doneSeen.set(true); continue; } try { JSONObject obj = JSON.parseObject(s); JSONArray choices = obj.getJSONArray("choices"); if (choices != null && !choices.isEmpty()) { JSONObject c0 = choices.getJSONObject(0); JSONObject delta = c0.getJSONObject("delta"); if (delta != null) { String content = delta.getString("content"); if (content != null) { try { queue.offer(content); } catch (Exception ignore) {} } } } } catch (Exception e) { e.printStackTrace(); } } }, err -> { errorSeen.set(true); doneSeen.set(true); if (onError != null) onError.accept(err); }, () -> { if (!doneSeen.get()) { errorSeen.set(true); doneSeen.set(true); if (onError != null) onError.accept(new RuntimeException("LLM 流意外完成")); } else { doneSeen.set(true); } }); } public void chatStreamRunPython(String prompt, String chatId, Consumer<String> onChunk, Runnable onComplete, Consumer<Throwable> onError) { HashMap<String, Object> req = new HashMap<>(); req.put("prompt", prompt); req.put("chatId", chatId); Flux<String> flux = llmWebClient.post() .uri(pythonPlatformUrl) .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.TEXT_EVENT_STREAM) .bodyValue(req) .retrieve() .bodyToFlux(String.class) .doOnError(ex -> log.error("调用 LLM 流式失败", ex)); AtomicBoolean doneSeen = new AtomicBoolean(false); AtomicBoolean errorSeen = new AtomicBoolean(false); LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); Thread drain = new Thread(() -> { try { while (true) { String s = queue.poll(2, TimeUnit.SECONDS); if (s != null) { try { onChunk.accept(s); } catch (Exception ignore) { } } if (doneSeen.get() && queue.isEmpty()) { if (!errorSeen.get()) { try { if (onComplete != null) onComplete.run(); } catch (Exception ignore) { } } break; } } } catch (InterruptedException ignore) { ignore.printStackTrace(); } }); drain.setDaemon(true); drain.start(); flux.subscribe(payload -> { if (payload == null || payload.isEmpty()) return; String[] events = payload.split("\\r?\\n\\r?\\n"); for (String part : events) { String s = part; if (s == null || s.isEmpty()) continue; if (s.startsWith("data:")) { s = s.substring(5); if (s.startsWith(" ")) s = s.substring(1); } if ("[DONE]".equals(s.trim())) { doneSeen.set(true); continue; } if("<think>".equals(s.trim()) || "</think>".equals(s.trim())) { queue.offer(s.trim()); continue; } try { JSONObject obj = JSON.parseObject(s); JSONArray choices = obj.getJSONArray("choices"); if (choices != null && !choices.isEmpty()) { JSONObject c0 = choices.getJSONObject(0); JSONObject delta = c0.getJSONObject("delta"); if (delta != null) { String content = delta.getString("content"); if (content != null) { try { queue.offer(content); } catch (Exception ignore) { } } } } } catch (Exception e) { e.printStackTrace(); } } }, err -> { errorSeen.set(true); doneSeen.set(true); if (onError != null) onError.accept(err); }, () -> { if (!doneSeen.get()) { errorSeen.set(true); doneSeen.set(true); if (onError != null) onError.accept(new RuntimeException("LLM 流意外完成")); } else { doneSeen.set(true); } }); } private ChatCompletionResponse mergeSseChunk(ChatCompletionResponse acc, String payload) { if (payload == null || payload.isEmpty()) return acc; String[] events = payload.split("\\r?\\n\\r?\\n"); src/main/java/com/zy/ai/service/PythonService.java
New file @@ -0,0 +1,55 @@ package com.zy.ai.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @Slf4j @Component public class PythonService { @Autowired private LlmChatService llmChatService; public boolean runPython(String prompt, String chatId, SseEmitter emitter) { try { llmChatService.chatStreamRunPython(prompt, chatId, s -> { try { String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n"); if (!safe.isEmpty()) { sse(emitter, safe); } } catch (Exception ignore) { } }, () -> { try { sse(emitter, "\\n\\n【AI】运行已停止(正常结束)\\n\\n"); log.info("AI MCP diagnose stopped: final end"); emitter.complete(); } catch (Exception ignore) { } }, e -> { sse(emitter, "\\n\\n【AI】分析出错,正在回退...\\n\\n"); }); return true; } catch (Exception e) { try { sse(emitter, "\\n\\n【AI】运行已停止(异常)\\n\\n"); log.error("AI MCP diagnose stopped: error", e); emitter.completeWithError(e); } catch (Exception ignore) {} return true; } } private void sse(SseEmitter emitter, String data) { if (data == null) return; try { emitter.send(SseEmitter.event().data(data)); } catch (Exception e) { log.warn("SSE send failed", e); } } } src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -1,15 +1,20 @@ package com.zy.ai.service; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.zy.ai.entity.ChatCompletionRequest; import com.zy.ai.entity.ChatCompletionResponse; import com.zy.ai.entity.WcsDiagnosisRequest; import com.zy.ai.mcp.controller.McpController; import com.zy.ai.utils.AiPromptUtils; import com.zy.ai.utils.AiUtils; import com.zy.common.utils.RedisUtil; import com.zy.core.enums.RedisKeyType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -18,10 +23,13 @@ @Service @RequiredArgsConstructor @Slf4j public class WcsDiagnosisService { private static final long CHAT_TTL_SECONDS = 7L * 24 * 3600; @Value("${llm.platform}") private String platform; @Autowired private LlmChatService llmChatService; @Autowired @@ -30,31 +38,27 @@ private AiPromptUtils aiPromptUtils; @Autowired private AiUtils aiUtils; /** * 针对“系统不执行任务 / 不知道哪个设备没在运行”的通用 AI 诊断 */ public String diagnose(WcsDiagnosisRequest request) { List<ChatCompletionRequest.Message> messages = new ArrayList<>(); // 1. system:定义专家身份 + 输出结构 ChatCompletionRequest.Message system = new ChatCompletionRequest.Message(); system.setRole("system"); system.setContent(aiPromptUtils.getAiDiagnosePrompt()); messages.add(system); ChatCompletionRequest.Message user = new ChatCompletionRequest.Message(); user.setRole("user"); user.setContent(aiUtils.buildDiagnosisUserContent(request)); messages.add(user); // 调用大模型 return llmChatService.chat(messages, 0.2, 2048); } @Autowired(required = false) private McpController mcpController; @Autowired private PythonService pythonService; public void diagnoseStream(WcsDiagnosisRequest request, SseEmitter emitter) { List<ChatCompletionRequest.Message> messages = new ArrayList<>(); ChatCompletionRequest.Message mcpSystem = new ChatCompletionRequest.Message(); mcpSystem.setRole("system"); mcpSystem.setContent(aiPromptUtils.getAiDiagnosePromptMcp()); ChatCompletionRequest.Message mcpUser = new ChatCompletionRequest.Message(); mcpUser.setRole("user"); mcpUser.setContent(aiUtils.buildDiagnosisUserContentMcp(request)); if (runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, 0.3, 2048, emitter, null)) { return; } messages = new ArrayList<>(); ChatCompletionRequest.Message system = new ChatCompletionRequest.Message(); system.setRole("system"); system.setContent(aiPromptUtils.getAiDiagnosePrompt()); @@ -65,7 +69,7 @@ user.setContent(aiUtils.buildDiagnosisUserContent(request)); messages.add(user); llmChatService.chatStream(messages, 0.2, 2048, s -> { llmChatService.chatStream(messages, 0.3, 2048, s -> { try { String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n"); if (!safe.isEmpty()) { @@ -73,9 +77,16 @@ } } catch (Exception ignore) {} }, () -> { try { emitter.complete(); } catch (Exception ignore) {} try { log.info("AI diagnose stream stopped: normal end"); emitter.complete(); } catch (Exception ignore) {} }, e -> { try { emitter.completeWithError(e); } catch (Exception ignore) {} try { try { emitter.send(SseEmitter.event().data("【AI】运行已停止(异常)")); } catch (Exception ignore) {} log.error("AI diagnose stream stopped: error", e); emitter.completeWithError(e); } catch (Exception ignore) {} }); } @@ -84,12 +95,12 @@ String chatId, boolean reset, SseEmitter emitter) { List<ChatCompletionRequest.Message> base = new ArrayList<>(); if (platform.equals("python")) { pythonService.runPython(prompt, chatId, emitter); return; } ChatCompletionRequest.Message system = new ChatCompletionRequest.Message(); system.setRole("system"); system.setContent(aiPromptUtils.getWcsSensorPrompt()); base.add(system); List<ChatCompletionRequest.Message> messages = new ArrayList<>(); List<ChatCompletionRequest.Message> history = null; String historyKey = null; @@ -107,21 +118,11 @@ ChatCompletionRequest.Message m = convertToMessage(o); if (m != null) history.add(m); } if (!history.isEmpty()) base.addAll(history); if (!history.isEmpty()) messages.addAll(history); } else { history = new ArrayList<>(); } } ChatCompletionRequest.Message contextMsg = new ChatCompletionRequest.Message(); contextMsg.setRole("user"); contextMsg.setContent(aiUtils.buildAskUserContent(request)); base.add(contextMsg); ChatCompletionRequest.Message questionMsg = new ChatCompletionRequest.Message(); questionMsg.setRole("user"); questionMsg.setContent("【用户提问】\n" + (prompt == null ? "" : prompt)); base.add(questionMsg); StringBuilder assistantBuffer = new StringBuilder(); final String finalChatId = chatId; @@ -129,7 +130,30 @@ final String finalMetaKey = metaKey; final String finalPrompt = prompt; llmChatService.chatStream(base, 0.2, 2048, s -> { ChatCompletionRequest.Message mcpSystem = new ChatCompletionRequest.Message(); mcpSystem.setRole("system"); mcpSystem.setContent(aiPromptUtils.getWcsSensorPromptMcp()); ChatCompletionRequest.Message mcpUser = new ChatCompletionRequest.Message(); mcpUser.setRole("user"); mcpUser.setContent("【用户提问】\n" + (prompt == null ? "" : prompt)); if (runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, 0.3, 2048, emitter, finalChatId)) { return; } messages = new ArrayList<>(); ChatCompletionRequest.Message system = new ChatCompletionRequest.Message(); system.setRole("system"); system.setContent(aiPromptUtils.getWcsSensorPrompt()); messages.add(system); ChatCompletionRequest.Message questionMsg = new ChatCompletionRequest.Message(); questionMsg.setRole("user"); questionMsg.setContent("【用户提问】\n" + (prompt == null ? "" : prompt)); messages.add(questionMsg); llmChatService.chatStream(messages, 0.3, 2048, s -> { try { String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n"); if (!safe.isEmpty()) { @@ -231,5 +255,294 @@ String p = prompt.replaceAll("\n", " ").trim(); return p.length() > 20 ? p.substring(0, 20) : p; } private boolean runMcpStreamingDiagnosis(List<ChatCompletionRequest.Message> baseMessages, ChatCompletionRequest.Message systemPrompt, ChatCompletionRequest.Message userQuestion, Double temperature, Integer maxTokens, SseEmitter emitter, String chatId) { try { if (mcpController == null) return false; List<Object> tools = buildOpenAiTools(); if (tools.isEmpty()) return false; baseMessages.add(systemPrompt); baseMessages.add(userQuestion); List<ChatCompletionRequest.Message> messages = new ArrayList<>(baseMessages.size() + 8); messages.addAll(baseMessages); sse(emitter, "<think>\\n正在初始化诊断与工具环境...\\n"); int maxRound = 10; int i = 0; while(true) { sse(emitter, "\\n正在分析(第" + (i + 1) + "轮)...\\n"); ChatCompletionResponse resp = llmChatService.chatCompletion(messages, temperature, maxTokens, tools); if (resp == null || resp.getChoices() == null || resp.getChoices().isEmpty() || resp.getChoices().get(0).getMessage() == null) { sse(emitter, "\\n分析出错,正在回退...\\n"); return false; } ChatCompletionRequest.Message assistant = resp.getChoices().get(0).getMessage(); messages.add(assistant); sse(emitter, assistant.getContent()); List<ChatCompletionRequest.ToolCall> toolCalls = assistant.getTool_calls(); if (toolCalls == null || toolCalls.isEmpty()) { break; } for (ChatCompletionRequest.ToolCall tc : toolCalls) { String toolName = tc != null && tc.getFunction() != null ? tc.getFunction().getName() : null; if (toolName == null || toolName.trim().isEmpty()) continue; sse(emitter, "\\n准备调用工具:" + toolName + "\\n"); JSONObject args = new JSONObject(); if (tc.getFunction() != null && tc.getFunction().getArguments() != null && !tc.getFunction().getArguments().trim().isEmpty()) { try { args = JSON.parseObject(tc.getFunction().getArguments()); } catch (Exception ignore) { args = new JSONObject(); args.put("_raw", tc.getFunction().getArguments()); } } Object output; try { output = mcpController.callTool(toolName, args); } catch (Exception e) { java.util.LinkedHashMap<String, Object> err = new java.util.LinkedHashMap<String, Object>(); err.put("tool", toolName); err.put("error", e.getMessage()); output = err; } sse(emitter, "\\n工具返回,正在继续推理...\\n"); ChatCompletionRequest.Message toolMsg = new ChatCompletionRequest.Message(); toolMsg.setRole("tool"); toolMsg.setTool_call_id(tc == null ? null : tc.getId()); toolMsg.setContent(JSON.toJSONString(output)); messages.add(toolMsg); } if(i++ >= maxRound) break; } sse(emitter, "\\n正在根据数据进行分析...\\n</think>\\n\\n"); ChatCompletionRequest.Message diagnosisMessage = new ChatCompletionRequest.Message(); diagnosisMessage.setRole("system"); diagnosisMessage.setContent("根据以上信息进行分析,并给出完整的诊断结论。"); messages.add(diagnosisMessage); StringBuilder assistantBuffer = new StringBuilder(); llmChatService.chatStreamWithTools(messages, temperature, maxTokens, tools, s -> { try { String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n"); if (!safe.isEmpty()) { sse(emitter, safe); assistantBuffer.append(safe); } } catch (Exception ignore) {} }, () -> { try { sse(emitter, "\\n\\n【AI】运行已停止(正常结束)\\n\\n"); log.info("AI MCP diagnose stopped: final end"); emitter.complete(); if (chatId != null) { String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; String metaKey = RedisKeyType.AI_CHAT_META.key + chatId; ChatCompletionRequest.Message a = new ChatCompletionRequest.Message(); a.setRole("assistant"); a.setContent(assistantBuffer.toString()); redisUtil.lSet(historyKey, userQuestion); redisUtil.lSet(historyKey, a); redisUtil.expire(historyKey, CHAT_TTL_SECONDS); Map<Object, Object> old = redisUtil.hmget(metaKey); Long createdAt = old != null && old.get("createdAt") != null ? (old.get("createdAt") instanceof Number ? ((Number) old.get("createdAt")).longValue() : Long.valueOf(String.valueOf(old.get("createdAt")))) : System.currentTimeMillis(); Map<String, Object> meta = new java.util.HashMap<>(); meta.put("chatId", chatId); meta.put("title", buildTitleFromPrompt(userQuestion.getContent())); meta.put("createdAt", createdAt); meta.put("updatedAt", System.currentTimeMillis()); redisUtil.hmset(metaKey, meta, CHAT_TTL_SECONDS); } } catch (Exception ignore) {} }, e -> { sse(emitter, "\\n\\n【AI】分析出错,正在回退...\\n\\n"); }); return true; } catch (Exception e) { try { sse(emitter, "\\n\\n【AI】运行已停止(异常)\\n\\n"); log.error("AI MCP diagnose stopped: error", e); emitter.completeWithError(e); } catch (Exception ignore) {} return true; } } private void sse(SseEmitter emitter, String data) { if (data == null) return; try { emitter.send(SseEmitter.event().data(data)); } catch (Exception e) { log.warn("SSE send failed", e); } } private List<Object> buildOpenAiTools() { if (mcpController == null) return java.util.Collections.emptyList(); List<Map<String, Object>> mcpTools = mcpController.listTools(); if (mcpTools == null || mcpTools.isEmpty()) return java.util.Collections.emptyList(); List<Object> tools = new ArrayList<>(); for (Map<String, Object> t : mcpTools) { if (t == null) continue; Object name = t.get("name"); if (name == null) continue; Object inputSchema = t.get("inputSchema"); java.util.LinkedHashMap<String, Object> function = new java.util.LinkedHashMap<String, Object>(); function.put("name", String.valueOf(name)); Object desc = t.get("description"); if (desc != null) function.put("description", String.valueOf(desc)); function.put("parameters", inputSchema == null ? new java.util.LinkedHashMap<String, Object>() : inputSchema); java.util.LinkedHashMap<String, Object> tool = new java.util.LinkedHashMap<String, Object>(); tool.put("type", "function"); tool.put("function", function); tools.add(tool); } return tools; } private void sendLargeText(SseEmitter emitter, String text) { if (text == null) return; String safe = text.replace("\r", "").replace("\n", "\\n"); int chunkSize = 256; int i = 0; while (i < safe.length()) { int end = Math.min(i + chunkSize, safe.length()); String part = safe.substring(i, end); if (!part.isEmpty()) { try { emitter.send(SseEmitter.event().data(part)); } catch (Exception ignore) {} } i = end; } } private static final java.util.regex.Pattern DSML_INVOKE_PATTERN = java.util.regex.Pattern.compile("<\\uFF5CDSML\\uFF5Cinvoke\\s+name=\\\"([^\\\"]+)\\\"[^>]*>([\\s\\S]*?)</\\uFF5CDSML\\uFF5Cinvoke>", java.util.regex.Pattern.MULTILINE); private static final java.util.regex.Pattern JSON_OBJECT_PATTERN = java.util.regex.Pattern.compile("\\{[\\s\\S]*\\}"); private static final java.util.regex.Pattern DSML_PARAM_PATTERN = java.util.regex.Pattern.compile("<\\uFF5CDSML\\uFF5Cparameter\\s+name=\\\"([^\\\"]+)\\\"\\s*([^>]*)>([\\s\\S]*?)</\\uFF5CDSML\\uFF5Cparameter>", java.util.regex.Pattern.MULTILINE); private java.util.List<DsmlInvocation> parseDsmlInvocations(String content) { java.util.List<DsmlInvocation> list = new java.util.ArrayList<>(); if (content == null || content.isEmpty()) return list; java.util.regex.Matcher m = DSML_INVOKE_PATTERN.matcher(content); while (m.find()) { String name = m.group(1); String inner = m.group(2); com.alibaba.fastjson.JSONObject args = null; if (inner != null) { java.util.regex.Matcher jm = JSON_OBJECT_PATTERN.matcher(inner); if (jm.find()) { String json = jm.group(); try { args = com.alibaba.fastjson.JSON.parseObject(json); } catch (Exception ignore) {} } java.util.regex.Matcher pm = DSML_PARAM_PATTERN.matcher(inner); while (pm.find()) { if (args == null) args = new com.alibaba.fastjson.JSONObject(); String pName = pm.group(1); String attr = pm.group(2); String valText = pm.group(3); boolean isString = attr != null && attr.toLowerCase().contains("string=\"true\""); String t = valText == null ? "" : valText.trim(); if (isString) { args.put(pName, t); } else { if ("true".equalsIgnoreCase(t) || "false".equalsIgnoreCase(t)) { args.put(pName, Boolean.valueOf(t)); } else { try { if (t.contains(".")) { args.put(pName, Double.valueOf(t)); } else { args.put(pName, Long.valueOf(t)); } } catch (Exception ex) { args.put(pName, t); } } } } } DsmlInvocation inv = new DsmlInvocation(); inv.name = name; inv.arguments = args; list.add(inv); } return list; } private static class DsmlInvocation { String name; com.alibaba.fastjson.JSONObject arguments; } private List<DsmlInvocation> buildDefaultStatusInvocations() { List<DsmlInvocation> list = new ArrayList<>(); DsmlInvocation crn = new DsmlInvocation(); crn.name = "device.get_crn_status"; com.alibaba.fastjson.JSONObject a1 = new com.alibaba.fastjson.JSONObject(); a1.put("limit", 20); crn.arguments = a1; list.add(crn); DsmlInvocation st = new DsmlInvocation(); st.name = "device.get_station_status"; com.alibaba.fastjson.JSONObject a2 = new com.alibaba.fastjson.JSONObject(); a2.put("limit", 20); st.arguments = a2; list.add(st); DsmlInvocation rgv = new DsmlInvocation(); rgv.name = "device.get_rgv_status"; com.alibaba.fastjson.JSONObject a3 = new com.alibaba.fastjson.JSONObject(); a3.put("limit", 20); rgv.arguments = a3; list.add(rgv); return list; } private void ensureStatusCoverage(List<DsmlInvocation> invs) { if (invs == null) return; java.util.Set<String> names = new java.util.HashSet<String>(); for (DsmlInvocation d : invs) { if (d != null && d.name != null) names.add(d.name); } if (!names.contains("device.get_crn_status") || !names.contains("device.get_station_status") || !names.contains("device.get_rgv_status")) { List<DsmlInvocation> defaults = buildDefaultStatusInvocations(); for (DsmlInvocation d : defaults) { if (!names.contains(d.name)) invs.add(d); } } } private boolean isConclusionText(String content) { if (content == null) return false; String c = content; int len = c.length(); boolean longEnough = len >= 200; boolean hasAllSections = c.contains("问题概述") && c.contains("可疑设备列表") && c.contains("可能原因") && c.contains("建议排查步骤") && c.contains("风险评估"); boolean hasExplicitConclusion = (c.contains("结论") || c.contains("诊断结果")) && longEnough; return hasAllSections || hasExplicitConclusion; } } src/main/java/com/zy/ai/utils/AiPromptUtils.java
@@ -6,6 +6,117 @@ public class AiPromptUtils { //AI诊断系统Prompt public String getAiDiagnosePromptMcp() { String prompt = "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。\n\n" + "你可以按需调用系统提供的工具以获取实时数据与上下文(工具返回 JSON):\n" + "- 任务:task_list\n" + "- 设备实时状态:device_get_crn_status / device_get_station_status / device_get_rgv_status\n" + "- 日志:log_query\n" + "- 设备配置:config_get_device_config\n" + "- 系统配置:config_get_system_config\n\n" + "使用策略:\n" + "1)避免臆测。如信息不足,先调用相应工具收集必要数据;可多轮调用。\n" + "2)对工具返回的 JSON 先进行结构化归纳,提炼关键字段,再做推理。\n" + "3)优先顺序:任务→设备状态→日志→配置;按需调整。\n\n" + "你将收到以下几类数据:\n" + "1)任务信息(tasks):当前待执行/在执行/挂起任务\n" + "2)设备实时数据(deviceRealtimeData):每台设备当前状态、是否在线、当前任务号等\n" + "3)设备配置信息(deviceConfigs):设备是否启用、服务区域、允许的任务类型等\n" + "4)系统日志(logs):按时间顺序的日志文本\n" + "5)额外上下文(extraContext):如仓库代码、WCS 版本等\n\n" + "6)系统配置信息(systemConfigs):系统的配置参数,数据库表名sys_config\n\n" + "你的目标是:帮助现场运维人员分析,为什么系统当前不执行任务,或者任务执行效率异常,指出可能是哪些设备导致的问题。\n\n" + "请按以下结构输出诊断结果(使用简体中文):\n" + "1. 问题概述(1-3 句话,概括当前系统状态)\n" + "2. 可疑设备列表(列出 1-N 个设备编号,并说明每个设备为什么可疑,例如:配置禁用/长时间空闲/状态异常/任务分配不到它等)\n" + "3. 可能原因(从任务分配、设备状态、配置错误、接口/通信异常等角度,列出 3-7 条)\n" + "4. 建议排查步骤(步骤 1、2、3...,每步要尽量具体、可操作,例如:在某页面查看某字段、检查某个开关、对比某个状态位等)\n" + "5. 风险评估(说明当前问题对业务影响程度:高/中/低,以及是否需要立即人工干预)\n" + "如需要额外数据,请先调用合适的工具再继续回答。"; return prompt; } //WCS高级专家Prompt public String getWcsSensorPromptMcp() { // String prompt = "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。\n\n" + // "你可以按需调用系统提供的工具以获取实时数据与上下文(工具返回 JSON):\n" + // "- 任务:task_list\n" + // "- 设备实时状态:device_get_crn_status / device_get_station_status / device_get_rgv_status\n" + // "- 日志:log_query\n" + // "- 设备配置:config_get_device_config\n" + // "- 系统配置:config_get_system_config\n\n" + // "请先用工具获取必要信息,再以简洁、明确的中文作答,并在需要时给出可执行的排查建议。" + // "如需要额外数据,请先调用合适的工具再继续回答。"; String prompt = "你是一名资深 WCS(仓储控制系统)与自动化立库专家,\n" + "精通堆垛机、输送线、提升机、穿梭车、RGV、工位等设备的\n" + "任务分配、运行状态流转与异常处理。\n" + "\n" + "你的职责是:**基于实时数据进行工程级诊断,而不是凭经验猜测。**\n" + "\n" + "==================== 工作规则(非常重要) ====================\n" + "\n" + "1. **禁止在未获取实时数据的情况下直接下结论。**\n" + " - 若问题涉及“当前状态 / 是否卡死 / 是否有任务 / 是否异常”,\n" + " 你必须先调用工具获取数据,再进行分析。\n" + "\n" + "2. **优先使用聚合式信息,其次再使用单项查询。**\n" + " - 若需要整体判断系统状态,请优先调用:\n" + " → build_diagnosis_snapshot(如可用)\n" + " - 若只需要局部补充信息,再调用单项工具。\n" + "\n" + "3. **当信息不足以判断时,不得猜测原因。**\n" + " - 必须明确指出“缺少哪些数据”,并调用对应工具获取。\n" + "\n" + "4. **工具返回的数据是事实依据,必须引用其关键信息进行推理。**\n" + "\n" + "==================== 可用工具(返回 JSON) ====================\n" + "\n" + "【任务相关】\n" + "- task_list —— 查询当前/历史任务及状态\n" + "\n" + "【设备实时状态】\n" + "- device_get_crn_status —— 堆垛机实时状态\n" + "- device_get_station_status —— 工位实时状态\n" + "- device_get_rgv_status —— RGV / 穿梭车实时状态\n" + "\n" + "【日志】\n" + "- log_query —— 查询系统/设备日志(按时间/关键字)\n" + "\n" + "【配置】\n" + "- config_get_device_config —— 设备配置(启用、模式、策略)\n" + "- config_get_system_config —— 系统级调度/策略配置\n" + "\n" + "==================== 推荐诊断流程 ====================\n" + "\n" + "当接到诊断请求时,请遵循以下步骤:\n" + "\n" + "Step 1\uFE0F⃣ 明确诊断目标 \n" + "- 当前要判断的是:设备是否异常?任务是否卡死?调度是否阻塞?\n" + "\n" + "Step 2\uFE0F⃣ 调用必要工具获取事实数据 \n" + "- 设备状态 → 是否在线 / 是否空闲 / 当前任务\n" + "- 任务状态 → 是否存在待执行/挂起任务\n" + "- 日志 → 是否存在关键异常、等待确认、命令未响应等信息\n" + "\n" + "Step 3\uFE0F⃣ 基于数据进行逻辑分析 \n" + "- 使用 WCS 专业知识进行因果判断(而非猜测)\n" + "\n" + "Step 4\uFE0F⃣ 输出结构化结论 \n" + "- 【现象总结】\n" + "- 【关键证据(来自工具返回)】\n" + "- 【可能原因(按优先级)】\n" + "- 【可执行的排查 / 处理建议】\n" + "\n" + "==================== 输出要求 ====================\n" + "\n" + "- 使用**简洁、明确的中文**\n" + "- 避免泛泛而谈、避免“可能/也许”式空泛描述\n" + "- 若需要进一步数据,请**先调用工具,再继续分析**\n"; return prompt; } //AI诊断系统Prompt public String getAiDiagnosePrompt() { String prompt = "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。\n\n" + "你将收到以下几类数据:\n" + src/main/java/com/zy/ai/utils/AiUtils.java
@@ -265,4 +265,15 @@ return sb.toString(); } public String buildDiagnosisUserContentMcp(WcsDiagnosisRequest request) { StringBuilder sb = new StringBuilder(); if (request.getAlarmMessage() != null && !request.getAlarmMessage().isEmpty()) { sb.append("【问题描述】\n"); sb.append(request.getAlarmMessage()).append("\n\n"); } return sb.toString(); } } src/main/resources/application.yml
@@ -78,15 +78,23 @@ expireDays: 7 llm: platform: python pythonPlatformUrl: http://127.0.0.1:9000/ai/diagnose/askStream # base-url: https://api.siliconflow.cn/v1 # api-key: sk-sxdtebtquwrugzrmaqqqkzdzmrgzhzmplwwuowysdasccent # model: deepseek-ai/DeepSeek-V3.2 base-url: http://47.76.147.249:9998/e/7g7kqxxt1ei2un71 api-key: app-mP0O6aY5WpbfaHs7BNnjVkli model: deepseek-ai/DeepSeek-V3.2 # base-url: http://47.76.147.249:9998/e/7g7kqxxt1ei2un71 # api-key: app-mP0O6aY5WpbfaHs7BNnjVkli # model: deepseek-ai/DeepSeek-V3.2 # base-url: http://34.2.134.223:3000/v1 # api-key: sk-WabrmtOezCFwVo7XvVOrO3QkmfcKG7T7jy0BaVnmQTWm5GXh # model: gemini-3-pro-preview # base-url: http://127.0.0.1:8317/v1 # api-key: WznOjAGJNVFKSe9kBZTr # model: gpt-5 base-url: https://api.xiaomimimo.com/v1 api-key: sk-cw7e4se9cal8cxdgjml8dmtn4pdmqtvfccg5fcermt0ddtys model: mimo-v2-flash perf: methodTiming: src/main/webapp/views/ai/diagnosis.html
@@ -28,6 +28,26 @@ .time { font-size: 12px; color: #909399; text-align: right; margin-top: 6px; } .output .el-card__body { height: 100%; padding: 0; } .assistant-running { display: flex; align-items: center; gap: 8px; color: #909399; } details.think-block { border: 1px solid #e4e7ed; border-radius: 4px; padding: 8px; margin: 8px 0; background-color: #fcfcfc; } details.think-block summary { cursor: pointer; color: #909399; font-size: 13px; font-weight: bold; outline: none; } details.think-block .content { margin-top: 8px; color: #606266; font-size: 13px; white-space: pre-wrap; } </style> </head> <body> @@ -130,6 +150,17 @@ } }, methods: { renderMarkdown: function(md, streaming) { if (!md) return ''; var src = md.replace(/\\n/g, '\n'); var openAttr = streaming ? ' open' : ''; src = src.replace(/<think>/g, '<details class="think-block"' + openAttr + '><summary>AI深度思考</summary><div class="content">'); src = src.replace(/<\/think>/g, '</div></details>'); if (streaming && src.indexOf('<details class="think-block"') >= 0 && src.indexOf('</div></details>') < 0) { src += '</div></details>'; } return DOMPurify.sanitize(marked.parse(src)); }, loadChats: function() { var self = this; fetch(baseUrl + '/ai/diagnose/chats', { headers: { 'token': localStorage.getItem('token') } }) @@ -146,7 +177,7 @@ var msgs = []; for (var i=0;i<arr.length;i++) { var m = arr[i]; if (m.role === 'assistant') msgs.push({ role: 'assistant', md: m.content || '', html: DOMPurify.sanitize(marked.parse((m.content||'').replace(/\\n/g,'\n'))), ts: self.nowStr() }); if (m.role === 'assistant') msgs.push({ role: 'assistant', md: m.content || '', html: self.renderMarkdown(m.content || '', false), ts: self.nowStr() }); else msgs.push({ role: 'user', text: m.content || '', ts: self.nowStr() }); } self.messages = msgs; @@ -274,8 +305,7 @@ self.lastRenderTs = now; var last = self.messages.length > 0 ? self.messages[self.messages.length - 1] : null; if (last && last.role === 'assistant') { var renderSource = (last.md || '').replace(/\\n/g, '\n'); last.html = DOMPurify.sanitize(marked.parse(renderSource)); last.html = self.renderMarkdown(last.md || '', true); self.$nextTick(function() { self.scrollToBottom(true); }); } } @@ -298,8 +328,7 @@ this.typingTimer = null; } if (last && last.role === 'assistant') { var renderSource = (last.md || '').replace(/\n/g, '\n'); last.html = DOMPurify.sanitize(marked.parse(renderSource)); last.html = this.renderMarkdown(last.md || '', false); } this.$nextTick(function() { this.scrollToBottom(true); }.bind(this)); this.loadChats();