Junjie
2 天以前 63b01db83d9aad8a15276b4236a9a22e4aeef065
src/main/java/com/zy/ai/service/impl/AutoTuneAgentServiceImpl.java
@@ -2,6 +2,9 @@
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.entity.ChatCompletionResponse;
@@ -9,7 +12,9 @@
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;
@@ -31,6 +36,9 @@
    private static final String TOOL_GET_RECENT_JOBS = "wcs_local_dispatch_get_recent_auto_tune_jobs";
    private static final String TOOL_APPLY_CHANGES = "wcs_local_dispatch_apply_auto_tune_changes";
    private static final String TOOL_REVERT_LAST_JOB = "wcs_local_dispatch_revert_last_auto_tune_job";
    private static final String MCP_STATUS_SUCCESS = "success";
    private static final String MCP_STATUS_REJECTED = "rejected";
    private static final String MCP_STATUS_FAILED = "failed";
    private static final Set<String> ALLOWED_TOOL_NAMES = Set.of(
            TOOL_GET_SNAPSHOT,
            TOOL_GET_RECENT_JOBS,
@@ -41,10 +49,12 @@
    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;
@@ -57,10 +67,10 @@
            }
            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.chatCompletion(messages, TEMPERATURE, MAX_TOKENS, tools);
                ChatCompletionResponse response = llmChatService.chatCompletionOrThrow(messages, TEMPERATURE, MAX_TOKENS, tools);
                ChatCompletionRequest.Message assistantMessage = extractAssistantMessage(response);
                usageCounter.add(response.getUsage());
                messages.add(assistantMessage);
@@ -69,25 +79,29 @@
                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) {
                    Object toolOutput = callMountedTool(toolCall, runState);
                    Object toolOutput = callMountedTool(toolCall, runState, normalizedTriggerType);
                    messages.add(buildToolMessage(toolCall, toolOutput));
                }
            }
            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();
@@ -97,10 +111,7 @@
        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。"
                + "不要输出自由格式 JSON 供外层解析。");
        userMessage.setContent(AiPromptUtils.buildAutoTuneRuntimeGuard(triggerType, controlMode));
        messages.add(userMessage);
        return messages;
    }
@@ -116,20 +127,291 @@
        return message;
    }
    private Object callMountedTool(ChatCompletionRequest.ToolCall toolCall, RunState runState) {
    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);
        }
        JSONObject arguments = parseArguments(toolCall);
        applySchedulerTriggerType(toolName, triggerType, arguments);
        long startTimeMillis = System.currentTimeMillis();
        try {
            Object output = mcpToolManager.callTool(toolName, arguments);
            runState.markToolSuccess(toolName);
            recordMutationResult(toolName, arguments, output, runState);
            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;
        } catch (Exception exception) {
            runState.addMcpCall(buildMcpCall(toolName, arguments, null, startTimeMillis, exception));
            throw new IllegalStateException("Auto-tune MCP tool failed: " + toolName + ", " + exception.getMessage(),
                    exception);
        }
    }
    private AutoTuneAgentService.McpCallResult buildMcpCall(String toolName,
                                                            JSONObject arguments,
                                                            Object output,
                                                            long startTimeMillis,
                                                            Exception exception) {
        AutoTuneAgentService.McpCallResult call = new AutoTuneAgentService.McpCallResult();
        call.setToolName(toolName);
        call.setDryRun(resolveDryRun(arguments));
        call.setDurationMs(Math.max(0L, System.currentTimeMillis() - startTimeMillis));
        call.setRequestJson(JSON.toJSONString(arguments == null ? new JSONObject() : arguments));
        if (exception != null) {
            call.setStatus(MCP_STATUS_FAILED);
            call.setErrorMessage(exception.getMessage());
            return call;
        }
        call.setStatus(resolveMcpStatus(output));
        call.setResponseJson(JSON.toJSONString(output));
        call.setApplyJobId(resolveApplyJobId(output));
        call.setSuccessCount(resolveSuccessCount(output));
        call.setRejectCount(resolveRejectCount(output));
        if (MCP_STATUS_REJECTED.equals(call.getStatus())) {
            call.setErrorMessage(resolveApplyError(output));
        }
        return call;
    }
    private Boolean resolveDryRun(JSONObject arguments) {
        if (arguments == null || !arguments.containsKey("dryRun")) {
            return null;
        }
        return Boolean.TRUE.equals(arguments.getBoolean("dryRun"));
    }
    private String resolveMcpStatus(Object output) {
        if (isRejectedApplyResult(output)) {
            return MCP_STATUS_REJECTED;
        }
        return MCP_STATUS_SUCCESS;
    }
    private Long resolveApplyJobId(Object output) {
        if (output instanceof AutoTuneApplyResult) {
            return ((AutoTuneApplyResult) output).getJobId();
        }
        if (output instanceof Map<?, ?>) {
            Object jobId = ((Map<?, ?>) output).get("jobId");
            if (jobId instanceof Number) {
                return ((Number) jobId).longValue();
            }
            if (jobId != null) {
                try {
                    return Long.parseLong(String.valueOf(jobId));
                } catch (NumberFormatException ignore) {
                    return null;
                }
            }
        }
        return null;
    }
    private Integer resolveSuccessCount(Object output) {
        if (output instanceof AutoTuneApplyResult) {
            return safeCount(((AutoTuneApplyResult) output).getSuccessCount());
        }
        if (output instanceof Map<?, ?>) {
            return safeCount(((Map<?, ?>) output).get("successCount"));
        }
        return null;
    }
    private Integer resolveRejectCount(Object output) {
        if (output instanceof AutoTuneApplyResult) {
            return safeCount(((AutoTuneApplyResult) output).getRejectCount());
        }
        if (output instanceof Map<?, ?>) {
            return safeCount(((Map<?, ?>) output).get("rejectCount"));
        }
        return null;
    }
    private String resolveApplyError(Object output) {
        if (output instanceof AutoTuneApplyResult) {
            AutoTuneApplyResult result = (AutoTuneApplyResult) output;
            return firstRejectReason(result.getChanges(), result.getSummary());
        }
        if (output instanceof Map<?, ?>) {
            Map<?, ?> result = (Map<?, ?>) output;
            Object changes = result.get("changes");
            if (changes instanceof List<?>) {
                for (Object change : (List<?>) changes) {
                    if (change instanceof Map<?, ?>) {
                        Object rejectReason = ((Map<?, ?>) change).get("rejectReason");
                        if (!isBlank(rejectReason == null ? null : String.valueOf(rejectReason))) {
                            return String.valueOf(rejectReason);
                        }
                    }
                }
            }
            Object summary = result.get("summary");
            return summary == null ? null : String.valueOf(summary);
        }
        return null;
    }
    private String firstRejectReason(List<?> changes, String fallback) {
        if (changes != null) {
            for (Object change : changes) {
                String rejectReason = null;
                if (change instanceof com.zy.ai.entity.AiAutoTuneChange) {
                    rejectReason = ((com.zy.ai.entity.AiAutoTuneChange) change).getRejectReason();
                } else if (change instanceof Map<?, ?>) {
                    Object value = ((Map<?, ?>) change).get("rejectReason");
                    rejectReason = value == null ? null : String.valueOf(value);
                }
                if (!isBlank(rejectReason)) {
                    return rejectReason;
                }
            }
        }
        return fallback;
    }
    private void recordMutationResult(String toolName, JSONObject arguments, Object output, RunState runState) {
        if (TOOL_APPLY_CHANGES.equals(toolName)) {
            boolean dryRun = Boolean.TRUE.equals(arguments.getBoolean("dryRun"));
            if (!dryRun) {
                runState.addCounts(output);
                if (outputHasSuccessfulChange(output)) {
                    runState.markActualApply();
                }
            } else if (isRejectedApplyResult(output)) {
                runState.addCounts(output);
            }
            return;
        }
        if (TOOL_REVERT_LAST_JOB.equals(toolName)) {
            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) {
        if (output instanceof AutoTuneApplyResult) {
            AutoTuneApplyResult result = (AutoTuneApplyResult) output;
            return !Boolean.TRUE.equals(result.getSuccess()) || safeCount(result.getRejectCount()) > 0;
        }
        if (output instanceof Map<?, ?>) {
            Map<?, ?> result = (Map<?, ?>) output;
            if (!isApplyResultShape(result)) {
                return false;
            }
            if (!Boolean.TRUE.equals(result.get("success"))) {
                return true;
            }
            return safeCount(result.get("rejectCount")) > 0 || hasRejectedChange(result.get("changes"));
        }
        return false;
    }
    private boolean isApplyResultShape(Map<?, ?> result) {
        return result.containsKey("success")
                || result.containsKey("rejectCount")
                || result.containsKey("changes")
                || result.containsKey("dryRun")
                || result.containsKey("dryRunToken");
    }
    private Object withRejectedApplyInstruction(Object output) {
        JSONObject wrappedOutput = JSON.parseObject(JSON.toJSONString(output));
        wrappedOutput.put("agentInstruction",
                "本次 dry-run/apply 未完全通过,禁止继续实际应用。必须读取 changes[].rejectReason,"
                        + "并回到 snapshot.ruleSnapshot 按每个目标参数的 minValue、maxValue/dynamicMaxValue、maxStep、cooldownMinutes 和 note 重新校验。"
                        + "只有所有 changes 均满足规则并通过 dry-run 后,才允许实际应用。");
        return wrappedOutput;
    }
    private boolean hasRejectedChange(Object changes) {
        if (!(changes instanceof List<?>)) {
            return false;
        }
        for (Object change : (List<?>) changes) {
            if (!(change instanceof Map<?, ?>)) {
                continue;
            }
            Object status = ((Map<?, ?>) change).get("resultStatus");
            if ("rejected".equals(status) || "failed".equals(status)) {
                return true;
            }
        }
        return false;
    }
    private static int safeCount(Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value == null) {
            return 0;
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (NumberFormatException ignore) {
            return 0;
        }
    }
    private void applySchedulerTriggerType(String toolName, String triggerType, JSONObject arguments) {
        if (!TOOL_APPLY_CHANGES.equals(toolName)) {
            return;
        }
        if (!AutoTuneTriggerType.AUTO.getCode().equals(triggerType)) {
            return;
        }
        arguments.put("triggerType", AutoTuneTriggerType.AUTO.getCode());
    }
    private ChatCompletionRequest.Message buildToolMessage(ChatCompletionRequest.ToolCall toolCall, Object toolOutput) {
@@ -168,19 +450,28 @@
                                            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());
        result.setCompletionTokens(usageCounter.getCompletionTokens());
        result.setTotalTokens(usageCounter.getTotalTokens());
        result.setMaxRoundsReached(maxRoundsReached);
        result.setActualApplyCalled(runState.isActualApplyCalled());
        result.setRollbackCalled(runState.isRollbackCalled());
        result.setSuccessCount(runState.getSuccessCount());
        result.setRejectCount(runState.getRejectCount());
        result.setMcpCalls(runState.getMcpCalls());
        String summary = summaryBuffer == null ? "" : summaryBuffer.toString().trim();
        if (runState.getToolCallCount() <= 0) {
        if (runState.getToolCallCount() <= 0 && runState.getMcpCallCount() <= 0) {
            summary = "自动调参 Agent 未调用任何允许的 MCP 工具,未执行调参。" + (summary.isEmpty() ? "" : "\n" + summary);
        } else if (!runState.isSnapshotCalled()) {
            summary = summary + "\n自动调参 Agent 未调用快照工具,结果不完整。";
@@ -188,11 +479,33 @@
        if (runState.hasToolError()) {
            summary = summary + "\n自动调参 Agent 存在工具调用错误,已标记为失败。";
        }
        if (runState.hasApplyRejected()) {
            summary = summary + "\n自动调参 Agent 存在被拒绝的 dry-run/apply 结果,未视为成功调参。";
            if (!isBlank(runState.getFirstRejectReason())) {
                summary = summary + "拒绝原因: " + runState.getFirstRejectReason();
            }
        }
        if (success && !runState.hasActualMutation()) {
            summary = "自动调参 Agent 未调用实际应用或回滚工具,未修改运行参数。"
                    + (summary.isEmpty() ? "" : "\n" + summary);
        }
        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) {
@@ -235,7 +548,7 @@
        return isBlank(triggerType) ? "agent" : triggerType.trim();
    }
    private boolean isBlank(String value) {
    private static boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
@@ -276,6 +589,13 @@
        private int toolCallCount;
        private boolean snapshotCalled;
        private boolean toolError;
        private boolean applyRejected;
        private String firstRejectReason;
        private boolean actualApplyCalled;
        private boolean rollbackCalled;
        private int successCount;
        private int rejectCount;
        private final List<AutoTuneAgentService.McpCallResult> mcpCalls = new ArrayList<>();
        void markToolSuccess(String toolName) {
            toolCallCount++;
@@ -288,12 +608,53 @@
            toolError = true;
        }
        void markApplyRejected(String rejectReason) {
            applyRejected = true;
            if (isBlank(firstRejectReason) && !isBlank(rejectReason)) {
                firstRejectReason = rejectReason;
            }
        }
        void markActualApply() {
            actualApplyCalled = true;
        }
        void markRollback() {
            rollbackCalled = true;
        }
        void addCounts(Object output) {
            if (output instanceof AutoTuneApplyResult) {
                AutoTuneApplyResult result = (AutoTuneApplyResult) output;
                successCount += safeCount(result.getSuccessCount());
                rejectCount += safeCount(result.getRejectCount());
                return;
            }
            if (output instanceof Map<?, ?>) {
                Map<?, ?> result = (Map<?, ?>) output;
                successCount += safeCount(result.get("successCount"));
                rejectCount += safeCount(result.get("rejectCount"));
            }
        }
        void addMcpCall(AutoTuneAgentService.McpCallResult mcpCall) {
            if (mcpCall == null) {
                return;
            }
            mcpCall.setCallSeq(mcpCalls.size() + 1);
            mcpCalls.add(mcpCall);
        }
        boolean isSuccessful() {
            return toolCallCount > 0 && snapshotCalled && !toolError;
            return toolCallCount > 0 && snapshotCalled && !toolError && !applyRejected;
        }
        int getToolCallCount() {
            return toolCallCount;
        }
        int getMcpCallCount() {
            return mcpCalls.size();
        }
        boolean isSnapshotCalled() {
@@ -303,5 +664,37 @@
        boolean hasToolError() {
            return toolError;
        }
        boolean hasApplyRejected() {
            return applyRejected;
        }
        String getFirstRejectReason() {
            return firstRejectReason;
        }
        boolean isActualApplyCalled() {
            return actualApplyCalled;
        }
        boolean isRollbackCalled() {
            return rollbackCalled;
        }
        boolean hasActualMutation() {
            return actualApplyCalled || rollbackCalled;
        }
        int getSuccessCount() {
            return successCount;
        }
        int getRejectCount() {
            return rejectCount;
        }
        List<AutoTuneAgentService.McpCallResult> getMcpCalls() {
            return new ArrayList<>(mcpCalls);
        }
    }
}