| | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.zy.ai.domain.autotune.AutoTuneApplyResult; |
| | | import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneTriggerType; |
| | | import com.zy.ai.entity.AiPromptTemplate; |
| | | import com.zy.ai.entity.ChatCompletionRequest; |
| | |
| | | import com.zy.ai.mcp.service.SpringAiMcpToolManager; |
| | | import com.zy.ai.service.AiPromptTemplateService; |
| | | import com.zy.ai.service.AutoTuneAgentService; |
| | | import com.zy.ai.service.AutoTuneControlModeService; |
| | | import com.zy.ai.service.LlmChatService; |
| | | import com.zy.ai.utils.AiPromptUtils; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | |
| | | private final LlmChatService llmChatService; |
| | | private final SpringAiMcpToolManager mcpToolManager; |
| | | private final AiPromptTemplateService aiPromptTemplateService; |
| | | private final AutoTuneControlModeService autoTuneControlModeService; |
| | | |
| | | @Override |
| | | public AutoTuneAgentResult runAutoTune(String triggerType) { |
| | | String normalizedTriggerType = normalizeTriggerType(triggerType); |
| | | AutoTuneControlModeSnapshot controlMode = buildControlModeSnapshot(); |
| | | UsageCounter usageCounter = new UsageCounter(); |
| | | RunState runState = new RunState(); |
| | | boolean maxRoundsReached = false; |
| | |
| | | } |
| | | |
| | | AiPromptTemplate promptTemplate = aiPromptTemplateService.resolvePublished(AiPromptScene.AUTO_TUNE_DISPATCH.getCode()); |
| | | List<ChatCompletionRequest.Message> messages = buildMessages(promptTemplate, normalizedTriggerType); |
| | | List<ChatCompletionRequest.Message> messages = buildMessages(promptTemplate, normalizedTriggerType, controlMode); |
| | | |
| | | for (int round = 0; round < MAX_TOOL_ROUNDS; round++) { |
| | | ChatCompletionResponse response = llmChatService.chatCompletionOrThrow(messages, TEMPERATURE, MAX_TOKENS, tools); |
| | |
| | | List<ChatCompletionRequest.ToolCall> toolCalls = assistantMessage.getTool_calls(); |
| | | if (toolCalls == null || toolCalls.isEmpty()) { |
| | | return buildResult(runState.isSuccessful(), normalizedTriggerType, summaryBuffer, runState, |
| | | usageCounter, false); |
| | | usageCounter, false, controlMode); |
| | | } |
| | | |
| | | for (ChatCompletionRequest.ToolCall toolCall : toolCalls) { |
| | |
| | | } |
| | | } |
| | | maxRoundsReached = true; |
| | | return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached); |
| | | return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached, |
| | | controlMode); |
| | | } catch (Exception exception) { |
| | | log.error("Auto tune agent stopped with error", exception); |
| | | appendSummary(summaryBuffer, "自动调参 Agent 执行异常: " + exception.getMessage()); |
| | | runState.markToolError(); |
| | | return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached); |
| | | return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached, |
| | | controlMode); |
| | | } |
| | | } |
| | | |
| | | private List<ChatCompletionRequest.Message> buildMessages(AiPromptTemplate promptTemplate, String triggerType) { |
| | | private List<ChatCompletionRequest.Message> buildMessages(AiPromptTemplate promptTemplate, |
| | | String triggerType, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | List<ChatCompletionRequest.Message> messages = new ArrayList<>(); |
| | | |
| | | ChatCompletionRequest.Message systemMessage = new ChatCompletionRequest.Message(); |
| | |
| | | |
| | | ChatCompletionRequest.Message userMessage = new ChatCompletionRequest.Message(); |
| | | userMessage.setRole("user"); |
| | | userMessage.setContent("请执行一次后台 WCS 自动调参。triggerType=" + triggerType |
| | | + "。必须先调用 wcs_local_dispatch_get_auto_tune_snapshot 获取事实;如需提交变更," |
| | | + "必须先 dry-run,再根据 dry-run 结果决定是否实际应用;实际应用时必须带上 dry-run 返回的 dryRunToken。" |
| | | + "禁止在没有收到 wcs_local_dispatch_apply_auto_tune_changes 或 wcs_local_dispatch_revert_last_auto_tune_job 工具返回结果时声称已试算、已应用或已回滚。" |
| | | + "必须检查 taskSnapshot.stationLimitBlockedTasks 和 taskSnapshot.outboundTaskSamples 中的 systemMsg、wrkSts、batchSeq,判断是否存在被上限挡住的早序出库任务。" |
| | | + "所有提交给 wcs_local_dispatch_apply_auto_tune_changes 的 changes 都必须先匹配 snapshot.ruleSnapshot 中对应 targetType/targetKey 的规则。" |
| | | + "每个参数都必须满足 minValue、maxValue 或 dynamicMaxValue、maxStep、cooldownMinutes 和规则 note;找不到规则或无法证明动态上限时禁止提交。" |
| | | + "dry-run 返回 success=false 或 rejectCount>0 时,必须停止实际应用并说明拒绝原因,或重新提交完全合法的 dry-run。" |
| | | + "不要输出自由格式 JSON 供外层解析。"); |
| | | userMessage.setContent(AiPromptUtils.buildAutoTuneRuntimeGuard(triggerType, controlMode)); |
| | | messages.add(userMessage); |
| | | return messages; |
| | | } |
| | |
| | | return message; |
| | | } |
| | | |
| | | private Object callMountedTool(ChatCompletionRequest.ToolCall toolCall, RunState runState, String triggerType) { |
| | | private Object callMountedTool(ChatCompletionRequest.ToolCall toolCall, |
| | | RunState runState, |
| | | String triggerType) { |
| | | String toolName = resolveToolName(toolCall); |
| | | if (!ALLOWED_TOOL_NAMES.contains(toolName)) { |
| | | throw new IllegalArgumentException("Disallowed auto-tune MCP tool: " + toolName); |
| | |
| | | Object output = mcpToolManager.callTool(toolName, arguments); |
| | | runState.markToolSuccess(toolName); |
| | | recordMutationResult(toolName, arguments, output, runState); |
| | | if (TOOL_APPLY_CHANGES.equals(toolName) && isRejectedApplyResult(output)) { |
| | | runState.markApplyRejected(); |
| | | if (isRejectedApplyResult(output)) { |
| | | runState.markApplyRejected(resolveApplyError(output)); |
| | | if (TOOL_APPLY_CHANGES.equals(toolName)) { |
| | | Object wrappedOutput = withRejectedApplyInstruction(output); |
| | | runState.addMcpCall(buildMcpCall(toolName, arguments, wrappedOutput, startTimeMillis, null)); |
| | | return wrappedOutput; |
| | | } |
| | | } |
| | | runState.addMcpCall(buildMcpCall(toolName, arguments, output, startTimeMillis, null)); |
| | | return output; |
| | |
| | | if (TOOL_APPLY_CHANGES.equals(toolName)) { |
| | | boolean dryRun = Boolean.TRUE.equals(arguments.getBoolean("dryRun")); |
| | | if (!dryRun) { |
| | | runState.markActualApply(); |
| | | runState.addCounts(output); |
| | | if (outputHasSuccessfulChange(output)) { |
| | | runState.markActualApply(); |
| | | } |
| | | } else if (isRejectedApplyResult(output)) { |
| | | runState.addCounts(output); |
| | | } |
| | |
| | | runState.markRollback(); |
| | | runState.addCounts(output); |
| | | } |
| | | } |
| | | |
| | | private boolean outputHasSuccessfulChange(Object output) { |
| | | if (output instanceof AutoTuneApplyResult) { |
| | | AutoTuneApplyResult result = (AutoTuneApplyResult) output; |
| | | if (result.getChanges() != null) { |
| | | return hasSuccessfulChange(result.getChanges()); |
| | | } |
| | | return safeCount(result.getSuccessCount()) > 0; |
| | | } |
| | | if (output instanceof Map<?, ?>) { |
| | | Map<?, ?> result = (Map<?, ?>) output; |
| | | if (!isApplyResultShape(result)) { |
| | | return false; |
| | | } |
| | | if (result.containsKey("changes")) { |
| | | return hasSuccessfulChange(result.get("changes")); |
| | | } |
| | | return safeCount(result.get("successCount")) > 0; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean hasSuccessfulChange(Object changes) { |
| | | if (!(changes instanceof List<?>)) { |
| | | return false; |
| | | } |
| | | for (Object change : (List<?>) changes) { |
| | | String resultStatus = null; |
| | | if (change instanceof com.zy.ai.entity.AiAutoTuneChange) { |
| | | resultStatus = ((com.zy.ai.entity.AiAutoTuneChange) change).getResultStatus(); |
| | | } else if (change instanceof Map<?, ?>) { |
| | | Object status = ((Map<?, ?>) change).get("resultStatus"); |
| | | resultStatus = status == null ? null : String.valueOf(status); |
| | | } |
| | | if (resultStatus != null && "success".equalsIgnoreCase(resultStatus.trim())) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean isRejectedApplyResult(Object output) { |
| | |
| | | StringBuilder summaryBuffer, |
| | | RunState runState, |
| | | UsageCounter usageCounter, |
| | | boolean maxRoundsReached) { |
| | | boolean maxRoundsReached, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | AutoTuneAgentResult result = new AutoTuneAgentResult(); |
| | | result.setSuccess(success); |
| | | result.setTriggerType(triggerType); |
| | | result.setAnalysisOnly(controlMode.getAnalysisOnly()); |
| | | result.setAllowApply(controlMode.getAllowApply()); |
| | | result.setExecutionMode(controlMode.getModeCode()); |
| | | result.setToolCallCount(runState.getToolCallCount()); |
| | | result.setLlmCallCount(usageCounter.getLlmCallCount()); |
| | | result.setPromptTokens(usageCounter.getPromptTokens()); |
| | |
| | | } |
| | | if (runState.hasApplyRejected()) { |
| | | summary = summary + "\n自动调参 Agent 存在被拒绝的 dry-run/apply 结果,未视为成功调参。"; |
| | | if (!isBlank(runState.getFirstRejectReason())) { |
| | | summary = summary + "拒绝原因: " + runState.getFirstRejectReason(); |
| | | } |
| | | } |
| | | if (success && !runState.hasActualMutation()) { |
| | | summary = "自动调参 Agent 未调用实际应用或回滚工具,未修改运行参数。" |
| | |
| | | if (maxRoundsReached) { |
| | | summary = summary + "\n自动调参 Agent 达到最大工具调用轮次,已停止。"; |
| | | } |
| | | summary = buildModeSummary(controlMode) + (summary.isEmpty() ? "" : "\n" + summary); |
| | | result.setSummary(summary); |
| | | return result; |
| | | } |
| | | |
| | | private String buildModeSummary(AutoTuneControlModeSnapshot controlMode) { |
| | | return "执行模式: " + controlMode.getModeCode() |
| | | + ",analysisOnly=" + controlMode.getAnalysisOnly() |
| | | + ",allowApply=" + controlMode.getAllowApply() |
| | | + ",modeLabel=" + controlMode.getModeLabel(); |
| | | } |
| | | |
| | | private AutoTuneControlModeSnapshot buildControlModeSnapshot() { |
| | | return autoTuneControlModeService.currentMode(); |
| | | } |
| | | |
| | | private List<Object> filterAllowedTools(List<Object> tools) { |
| | |
| | | return isBlank(triggerType) ? "agent" : triggerType.trim(); |
| | | } |
| | | |
| | | private boolean isBlank(String value) { |
| | | private static boolean isBlank(String value) { |
| | | return value == null || value.trim().isEmpty(); |
| | | } |
| | | |
| | |
| | | private boolean snapshotCalled; |
| | | private boolean toolError; |
| | | private boolean applyRejected; |
| | | private String firstRejectReason; |
| | | private boolean actualApplyCalled; |
| | | private boolean rollbackCalled; |
| | | private int successCount; |
| | |
| | | toolError = true; |
| | | } |
| | | |
| | | void markApplyRejected() { |
| | | void markApplyRejected(String rejectReason) { |
| | | applyRejected = true; |
| | | if (isBlank(firstRejectReason) && !isBlank(rejectReason)) { |
| | | firstRejectReason = rejectReason; |
| | | } |
| | | } |
| | | |
| | | void markActualApply() { |
| | |
| | | return applyRejected; |
| | | } |
| | | |
| | | String getFirstRejectReason() { |
| | | return firstRejectReason; |
| | | } |
| | | |
| | | boolean isActualApplyCalled() { |
| | | return actualApplyCalled; |
| | | } |