package com.zy.ai.service.impl; 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; import com.zy.ai.enums.AiPromptScene; 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; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @Slf4j @Service @RequiredArgsConstructor public class AutoTuneAgentServiceImpl implements AutoTuneAgentService { private static final int MAX_TOOL_ROUNDS = 10; private static final double TEMPERATURE = 0.2D; private static final int MAX_TOKENS = 2048; private static final String TOOL_GET_SNAPSHOT = "wcs_local_dispatch_get_auto_tune_snapshot"; 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 ALLOWED_TOOL_NAMES = Set.of( TOOL_GET_SNAPSHOT, TOOL_GET_RECENT_JOBS, TOOL_APPLY_CHANGES, TOOL_REVERT_LAST_JOB ); 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; StringBuilder summaryBuffer = new StringBuilder(); try { List tools = filterAllowedTools(mcpToolManager.buildOpenAiTools()); if (tools == null || tools.isEmpty()) { throw new IllegalStateException("No auto-tune MCP tools registered"); } AiPromptTemplate promptTemplate = aiPromptTemplateService.resolvePublished(AiPromptScene.AUTO_TUNE_DISPATCH.getCode()); List messages = buildMessages(promptTemplate, normalizedTriggerType, controlMode); for (int round = 0; round < MAX_TOOL_ROUNDS; round++) { ChatCompletionResponse response = llmChatService.chatCompletionOrThrow(messages, TEMPERATURE, MAX_TOKENS, tools); ChatCompletionRequest.Message assistantMessage = extractAssistantMessage(response); usageCounter.add(response.getUsage()); messages.add(assistantMessage); appendSummary(summaryBuffer, assistantMessage.getContent()); List toolCalls = assistantMessage.getTool_calls(); if (toolCalls == null || toolCalls.isEmpty()) { return buildResult(runState.isSuccessful(), normalizedTriggerType, summaryBuffer, runState, usageCounter, false, controlMode); } for (ChatCompletionRequest.ToolCall toolCall : toolCalls) { Object toolOutput = callMountedTool(toolCall, runState, normalizedTriggerType); messages.add(buildToolMessage(toolCall, toolOutput)); } } maxRoundsReached = true; 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, controlMode); } } private List buildMessages(AiPromptTemplate promptTemplate, String triggerType, AutoTuneControlModeSnapshot controlMode) { List messages = new ArrayList<>(); ChatCompletionRequest.Message systemMessage = new ChatCompletionRequest.Message(); systemMessage.setRole("system"); systemMessage.setContent(promptTemplate == null ? "" : promptTemplate.getContent()); messages.add(systemMessage); ChatCompletionRequest.Message userMessage = new ChatCompletionRequest.Message(); userMessage.setRole("user"); userMessage.setContent(AiPromptUtils.buildAutoTuneRuntimeGuard(triggerType, controlMode)); messages.add(userMessage); return messages; } private ChatCompletionRequest.Message extractAssistantMessage(ChatCompletionResponse response) { if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) { throw new IllegalStateException("LLM returned empty response"); } ChatCompletionRequest.Message message = response.getChoices().get(0).getMessage(); if (message == null) { throw new IllegalStateException("LLM returned empty message"); } return message; } 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) { ChatCompletionRequest.Message toolMessage = new ChatCompletionRequest.Message(); toolMessage.setRole("tool"); toolMessage.setTool_call_id(toolCall == null ? null : toolCall.getId()); toolMessage.setContent(JSON.toJSONString(toolOutput)); return toolMessage; } private String resolveToolName(ChatCompletionRequest.ToolCall toolCall) { if (toolCall == null || toolCall.getFunction() == null || isBlank(toolCall.getFunction().getName())) { throw new IllegalArgumentException("missing tool name"); } return toolCall.getFunction().getName(); } private JSONObject parseArguments(ChatCompletionRequest.ToolCall toolCall) { String rawArguments = toolCall == null || toolCall.getFunction() == null ? null : toolCall.getFunction().getArguments(); if (isBlank(rawArguments)) { return new JSONObject(); } try { return JSON.parseObject(rawArguments); } catch (Exception exception) { JSONObject arguments = new JSONObject(); arguments.put("_raw", rawArguments); return arguments; } } private AutoTuneAgentResult buildResult(boolean success, String triggerType, StringBuilder summaryBuffer, RunState runState, UsageCounter usageCounter, 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 && runState.getMcpCallCount() <= 0) { summary = "自动调参 Agent 未调用任何允许的 MCP 工具,未执行调参。" + (summary.isEmpty() ? "" : "\n" + summary); } else if (!runState.isSnapshotCalled()) { summary = summary + "\n自动调参 Agent 未调用快照工具,结果不完整。"; } 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 filterAllowedTools(List tools) { List allowedTools = new ArrayList<>(); if (tools == null || tools.isEmpty()) { return allowedTools; } for (Object tool : tools) { String toolName = resolveOpenAiToolName(tool); if (ALLOWED_TOOL_NAMES.contains(toolName)) { allowedTools.add(tool); } } return allowedTools; } private String resolveOpenAiToolName(Object tool) { if (!(tool instanceof Map toolMap)) { return null; } Object function = toolMap.get("function"); if (!(function instanceof Map functionMap)) { return null; } Object name = functionMap.get("name"); return name == null ? null : String.valueOf(name); } private void appendSummary(StringBuilder summaryBuffer, String content) { if (summaryBuffer == null || isBlank(content)) { return; } if (summaryBuffer.length() > 0) { summaryBuffer.append('\n'); } summaryBuffer.append(content.trim()); } private String normalizeTriggerType(String triggerType) { return isBlank(triggerType) ? "agent" : triggerType.trim(); } private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } private static class UsageCounter { private long promptTokens; private long completionTokens; private long totalTokens; private int llmCallCount; void add(ChatCompletionResponse.Usage usage) { llmCallCount++; if (usage == null) { return; } promptTokens += usage.getPromptTokens() == null ? 0L : usage.getPromptTokens(); completionTokens += usage.getCompletionTokens() == null ? 0L : usage.getCompletionTokens(); totalTokens += usage.getTotalTokens() == null ? 0L : usage.getTotalTokens(); } long getPromptTokens() { return promptTokens; } long getCompletionTokens() { return completionTokens; } long getTotalTokens() { return totalTokens; } int getLlmCallCount() { return llmCallCount; } } private static class RunState { 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 mcpCalls = new ArrayList<>(); void markToolSuccess(String toolName) { toolCallCount++; if (TOOL_GET_SNAPSHOT.equals(toolName)) { snapshotCalled = true; } } void markToolError() { 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 && !applyRejected; } int getToolCallCount() { return toolCallCount; } int getMcpCallCount() { return mcpCalls.size(); } boolean isSnapshotCalled() { return snapshotCalled; } 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 getMcpCalls() { return new ArrayList<>(mcpCalls); } } }