Junjie
2026-04-27 dc1078bcd01c3b650163cf45553bd098a85a4c82
feat: add background auto tune scheduler
3个文件已添加
2个文件已修改
429 ■■■■■ 已修改文件
src/main/java/com/zy/ai/service/AutoTuneCoordinatorService.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/impl/AutoTuneCoordinatorServiceImpl.java 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/timer/AutoTuneScheduler.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/enums/RedisKeyType.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/AutoTuneCoordinatorService.java
New file
@@ -0,0 +1,40 @@
package com.zy.ai.service;
import lombok.Data;
import java.io.Serializable;
public interface AutoTuneCoordinatorService {
    AutoTuneCoordinatorResult runAutoTuneIfEligible();
    @Data
    class AutoTuneCoordinatorResult implements Serializable {
        private static final long serialVersionUID = 1L;
        private Boolean skipped;
        private String reason;
        private Boolean triggered;
        private AutoTuneAgentService.AutoTuneAgentResult agentResult;
        public static AutoTuneCoordinatorResult skipped(String reason) {
            AutoTuneCoordinatorResult result = new AutoTuneCoordinatorResult();
            result.setSkipped(true);
            result.setTriggered(false);
            result.setReason(reason);
            return result;
        }
        public static AutoTuneCoordinatorResult triggered(AutoTuneAgentService.AutoTuneAgentResult agentResult) {
            AutoTuneCoordinatorResult result = new AutoTuneCoordinatorResult();
            result.setSkipped(false);
            result.setTriggered(true);
            result.setReason("triggered");
            result.setAgentResult(agentResult);
            return result;
        }
    }
}
src/main/java/com/zy/ai/service/impl/AutoTuneCoordinatorServiceImpl.java
New file
@@ -0,0 +1,230 @@
package com.zy.ai.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zy.ai.domain.autotune.AutoTuneJobStatus;
import com.zy.ai.domain.autotune.AutoTuneTriggerType;
import com.zy.ai.entity.AiAutoTuneJob;
import com.zy.ai.service.AiAutoTuneJobService;
import com.zy.ai.service.AutoTuneAgentService;
import com.zy.ai.service.AutoTuneCoordinatorService;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.WrkMastService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.WrkStsType;
import com.zy.system.entity.OperateLog;
import com.zy.system.service.ConfigService;
import com.zy.system.service.OperateLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Service("autoTuneCoordinatorService")
@RequiredArgsConstructor
public class AutoTuneCoordinatorServiceImpl implements AutoTuneCoordinatorService {
    private static final String CONFIG_ENABLED = "aiAutoTuneEnabled";
    private static final String CONFIG_INTERVAL_MINUTES = "aiAutoTuneIntervalMinutes";
    private static final String DEFAULT_ENABLED = "N";
    private static final int DEFAULT_INTERVAL_MINUTES = 10;
    private static final int MIN_INTERVAL_MINUTES = 5;
    private static final int MAX_INTERVAL_MINUTES = 60;
    private static final int RUNNING_LOCK_SECONDS = 20 * 60;
    private static final long SYSTEM_USER_ID = 9527L;
    private static final List<Long> FINAL_WRK_STS_LIST = Arrays.asList(
            WrkStsType.COMPLETE_INBOUND.sts,
            WrkStsType.SETTLE_INBOUND.sts,
            WrkStsType.COMPLETE_OUTBOUND.sts,
            WrkStsType.SETTLE_OUTBOUND.sts,
            WrkStsType.COMPLETE_LOC_MOVE.sts,
            WrkStsType.COMPLETE_CRN_MOVE.sts
    );
    private final ConfigService configService;
    private final WrkMastService wrkMastService;
    private final AiAutoTuneJobService aiAutoTuneJobService;
    private final AutoTuneAgentService autoTuneAgentService;
    private final RedisUtil redisUtil;
    private final OperateLogService operateLogService;
    @Override
    public AutoTuneCoordinatorResult runAutoTuneIfEligible() {
        if (!isEnabled()) {
            return AutoTuneCoordinatorResult.skipped("disabled");
        }
        int intervalMinutes = resolveIntervalMinutes();
        if (!hasActiveTasks()) {
            return AutoTuneCoordinatorResult.skipped("no_active_tasks");
        }
        if (isLastTriggerGuardActive()) {
            return AutoTuneCoordinatorResult.skipped("last_trigger_guard_active");
        }
        AiAutoTuneJob latestSuccessfulJob = latestSuccessfulAutoJob();
        if (!isIntervalReached(latestSuccessfulJob, intervalMinutes)) {
            return AutoTuneCoordinatorResult.skipped("interval_not_reached");
        }
        String lockKey = RedisKeyType.AI_AUTO_TUNE_RUNNING_LOCK.key;
        String lockToken = UUID.randomUUID().toString();
        if (!redisUtil.trySetStringIfAbsent(lockKey, lockToken, RUNNING_LOCK_SECONDS)) {
            return AutoTuneCoordinatorResult.skipped("running_lock_not_acquired");
        }
        AutoTuneAgentService.AutoTuneAgentResult agentResult = null;
        try {
            agentResult = autoTuneAgentService.runAutoTune(AutoTuneTriggerType.AUTO.getCode());
            writeOperateLog(agentResult);
            markNoChangeGuardIfNeeded(latestSuccessfulJob, agentResult, intervalMinutes);
            return AutoTuneCoordinatorResult.triggered(agentResult);
        } catch (Exception exception) {
            log.error("Auto tune coordinator failed to run agent", exception);
            agentResult = failedAgentResult(exception);
            writeOperateLog(agentResult);
            return AutoTuneCoordinatorResult.triggered(agentResult);
        } finally {
            redisUtil.compareAndDelete(lockKey, lockToken);
        }
    }
    private boolean isEnabled() {
        String enabled = configService.getConfigValue(CONFIG_ENABLED, DEFAULT_ENABLED);
        if (enabled == null) {
            return false;
        }
        String normalized = enabled.trim();
        return "Y".equalsIgnoreCase(normalized)
                || "true".equalsIgnoreCase(normalized)
                || "1".equals(normalized);
    }
    private int resolveIntervalMinutes() {
        String value = configService.getConfigValue(CONFIG_INTERVAL_MINUTES, String.valueOf(DEFAULT_INTERVAL_MINUTES));
        try {
            int intervalMinutes = Integer.parseInt(value.trim());
            if (intervalMinutes < MIN_INTERVAL_MINUTES || intervalMinutes > MAX_INTERVAL_MINUTES) {
                return DEFAULT_INTERVAL_MINUTES;
            }
            return intervalMinutes;
        } catch (Exception exception) {
            return DEFAULT_INTERVAL_MINUTES;
        }
    }
    private boolean hasActiveTasks() {
        QueryWrapper<WrkMast> queryWrapper = new QueryWrapper<>();
        queryWrapper.and(wrapper -> wrapper.notIn("wrk_sts", FINAL_WRK_STS_LIST).or().isNull("wrk_sts"));
        return wrkMastService.count(queryWrapper) > 0;
    }
    private boolean isLastTriggerGuardActive() {
        Object guardValue = redisUtil.get(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key);
        return guardValue != null;
    }
    private AiAutoTuneJob latestSuccessfulAutoJob() {
        QueryWrapper<AiAutoTuneJob> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("status", AutoTuneJobStatus.SUCCESS.getCode());
        queryWrapper.eq("trigger_type", AutoTuneTriggerType.AUTO.getCode());
        queryWrapper.last("order by coalesce(finish_time, create_time) desc limit 1");
        List<AiAutoTuneJob> jobs = aiAutoTuneJobService.list(queryWrapper);
        if (jobs == null || jobs.isEmpty()) {
            return null;
        }
        return jobs.get(0);
    }
    private boolean isIntervalReached(AiAutoTuneJob latestSuccessfulJob, int intervalMinutes) {
        if (latestSuccessfulJob == null) {
            return true;
        }
        Date latestFinishTime = latestSuccessfulJob.getFinishTime();
        if (latestFinishTime == null) {
            latestFinishTime = latestSuccessfulJob.getCreateTime();
        }
        if (latestFinishTime == null) {
            return true;
        }
        long intervalMillis = intervalMinutes * 60L * 1000L;
        return System.currentTimeMillis() - latestFinishTime.getTime() >= intervalMillis;
    }
    private void markNoChangeGuardIfNeeded(AiAutoTuneJob beforeJob,
                                           AutoTuneAgentService.AutoTuneAgentResult agentResult,
                                           int intervalMinutes) {
        if (agentResult == null || !Boolean.TRUE.equals(agentResult.getSuccess())) {
            return;
        }
        AiAutoTuneJob afterJob = latestSuccessfulAutoJob();
        if (!isSameJob(beforeJob, afterJob)) {
            return;
        }
        long expireSeconds = intervalMinutes * 60L;
        redisUtil.set(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key,
                String.valueOf(System.currentTimeMillis()), expireSeconds);
    }
    private boolean isSameJob(AiAutoTuneJob beforeJob, AiAutoTuneJob afterJob) {
        if (beforeJob == null || afterJob == null) {
            return beforeJob == afterJob;
        }
        return beforeJob.getId() != null && beforeJob.getId().equals(afterJob.getId());
    }
    private AutoTuneAgentService.AutoTuneAgentResult failedAgentResult(Exception exception) {
        AutoTuneAgentService.AutoTuneAgentResult result = new AutoTuneAgentService.AutoTuneAgentResult();
        result.setSuccess(false);
        result.setTriggerType(AutoTuneTriggerType.AUTO.getCode());
        result.setSummary("自动调参后台任务执行异常: " + exception.getMessage());
        result.setToolCallCount(0);
        result.setLlmCallCount(0);
        result.setPromptTokens(0L);
        result.setCompletionTokens(0L);
        result.setTotalTokens(0L);
        result.setMaxRoundsReached(false);
        return result;
    }
    private void writeOperateLog(AutoTuneAgentService.AutoTuneAgentResult agentResult) {
        if (agentResult == null) {
            return;
        }
        OperateLog operateLog = new OperateLog();
        operateLog.setAction("ai_auto_tune_background_scheduler");
        operateLog.setUserId(SYSTEM_USER_ID);
        operateLog.setIp("system");
        operateLog.setRequest(JSON.toJSONString(buildRequestSummary(agentResult)));
        operateLog.setResponse(JSON.toJSONString(buildResponseSummary(agentResult)));
        operateLog.setCreateTime(new Date());
        operateLogService.save(operateLog);
    }
    private Map<String, Object> buildRequestSummary(AutoTuneAgentService.AutoTuneAgentResult agentResult) {
        Map<String, Object> request = new LinkedHashMap<>();
        request.put("trigger", agentResult.getTriggerType());
        return request;
    }
    private Map<String, Object> buildResponseSummary(AutoTuneAgentService.AutoTuneAgentResult agentResult) {
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("success", agentResult.getSuccess());
        response.put("summary", agentResult.getSummary());
        response.put("toolCallCount", agentResult.getToolCallCount());
        response.put("llmCallCount", agentResult.getLlmCallCount());
        response.put("promptTokens", agentResult.getPromptTokens());
        response.put("completionTokens", agentResult.getCompletionTokens());
        response.put("totalTokens", agentResult.getTotalTokens());
        response.put("maxRoundsReached", agentResult.getMaxRoundsReached());
        return response;
    }
}
src/main/java/com/zy/ai/timer/AutoTuneScheduler.java
New file
@@ -0,0 +1,31 @@
package com.zy.ai.timer;
import com.zy.ai.service.AutoTuneCoordinatorService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AutoTuneScheduler {
    @Autowired
    private AutoTuneCoordinatorService autoTuneCoordinatorService;
    @Scheduled(cron = "0 * * * * ? ")
    public void runAutoTune() {
        try {
            AutoTuneCoordinatorService.AutoTuneCoordinatorResult result =
                    autoTuneCoordinatorService.runAutoTuneIfEligible();
            if (Boolean.TRUE.equals(result.getSkipped())) {
                log.debug("Auto tune scheduler skipped, reason={}", result.getReason());
                return;
            }
            log.info("Auto tune scheduler triggered, success={}",
                    result.getAgentResult() == null ? null : result.getAgentResult().getSuccess());
        } catch (Exception exception) {
            log.error("Auto tune scheduler failed", exception);
        }
    }
}
src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -78,6 +78,8 @@
    CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"),
    MAIN_PROCESS_PSEUDOCODE("main_process_pseudocode"),
    AI_AUTO_TUNE_RUNNING_LOCK("ai_auto_tune_running_lock"),
    AI_AUTO_TUNE_LAST_TRIGGER_GUARD("ai_auto_tune_last_trigger_guard"),
    PLANNER_SCHEDULE("planner_schedule_"),
    HIGH_PRIVILEGE_GRANT("high_privilege_grant_"),
    ;
src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java
@@ -6,6 +6,7 @@
import com.zy.ai.domain.autotune.AutoTuneApplyResult;
import com.zy.ai.domain.autotune.AutoTuneChangeCommand;
import com.zy.ai.domain.autotune.AutoTuneSnapshot;
import com.zy.ai.domain.autotune.AutoTuneTriggerType;
import com.zy.ai.entity.AiAutoTuneChange;
import com.zy.ai.entity.AiAutoTuneJob;
import com.zy.ai.entity.AiPromptTemplate;
@@ -14,6 +15,12 @@
import com.zy.ai.mcp.service.SpringAiMcpToolManager;
import com.zy.ai.mcp.tool.AutoTuneMcpTools;
import com.zy.ai.service.impl.AutoTuneAgentServiceImpl;
import com.zy.ai.service.impl.AutoTuneCoordinatorServiceImpl;
import com.zy.asrs.service.WrkMastService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.system.service.ConfigService;
import com.zy.system.service.OperateLogService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -24,6 +31,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -39,6 +47,8 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -63,6 +73,16 @@
    private SpringAiMcpToolManager mcpToolManager;
    @Mock
    private AiPromptTemplateService aiPromptTemplateService;
    @Mock
    private ConfigService configService;
    @Mock
    private WrkMastService wrkMastService;
    @Mock
    private AutoTuneAgentService autoTuneAgentService;
    @Mock
    private RedisUtil redisUtil;
    @Mock
    private OperateLogService operateLogService;
    @BeforeEach
    void setUp() {
@@ -239,6 +259,88 @@
    }
    @Test
    void coordinatorSkipsWhenDisabled() {
        when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("N");
        AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible();
        assertTrue(result.getSkipped());
        assertEquals("disabled", result.getReason());
        verify(wrkMastService, never()).count(any(Wrapper.class));
        verify(autoTuneAgentService, never()).runAutoTune(anyString());
    }
    @Test
    void coordinatorSkipsWhenNoActiveTasks() {
        when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("Y");
        when(configService.getConfigValue("aiAutoTuneIntervalMinutes", "10")).thenReturn("10");
        when(wrkMastService.count(any(Wrapper.class))).thenReturn(0L);
        AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible();
        assertTrue(result.getSkipped());
        assertEquals("no_active_tasks", result.getReason());
        verify(autoTuneAgentService, never()).runAutoTune(anyString());
    }
    @Test
    void coordinatorSkipsWhenIntervalNotReached() {
        AiAutoTuneJob recentJob = new AiAutoTuneJob();
        recentJob.setId(11L);
        recentJob.setFinishTime(new Date());
        when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("true");
        when(configService.getConfigValue("aiAutoTuneIntervalMinutes", "10")).thenReturn("10");
        when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L);
        when(redisUtil.get(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key)).thenReturn(null);
        when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(recentJob));
        AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible();
        assertTrue(result.getSkipped());
        assertEquals("interval_not_reached", result.getReason());
        verify(autoTuneAgentService, never()).runAutoTune(anyString());
    }
    @Test
    void coordinatorTriggersAgentWhenEligible() {
        AutoTuneAgentService.AutoTuneAgentResult agentResult = successfulAgentResult();
        when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("1");
        when(configService.getConfigValue("aiAutoTuneIntervalMinutes", "10")).thenReturn("10");
        when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L);
        when(redisUtil.get(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key)).thenReturn(null);
        when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.emptyList(), Collections.emptyList());
        when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(true);
        when(autoTuneAgentService.runAutoTune(AutoTuneTriggerType.AUTO.getCode())).thenReturn(agentResult);
        AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible();
        assertFalse(result.getSkipped());
        assertTrue(result.getTriggered());
        assertSame(agentResult, result.getAgentResult());
        verify(autoTuneAgentService).runAutoTune(AutoTuneTriggerType.AUTO.getCode());
        verify(operateLogService).save(any());
        verify(redisUtil).set(anyString(), any(), anyLong());
        verify(redisUtil).compareAndDelete(anyString(), anyString());
    }
    @Test
    void coordinatorSkipsWhenRunningLockIsNotAcquired() {
        when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("Y");
        when(configService.getConfigValue("aiAutoTuneIntervalMinutes", "10")).thenReturn("10");
        when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L);
        when(redisUtil.get(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key)).thenReturn(null);
        when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.emptyList());
        when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(false);
        AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible();
        assertTrue(result.getSkipped());
        assertEquals("running_lock_not_acquired", result.getReason());
        verify(autoTuneAgentService, never()).runAutoTune(anyString());
        verify(redisUtil, never()).compareAndDelete(anyString(), anyString());
    }
    @Test
    void agentExecutesSnapshotDryRunAndRealApplyToolSequence() {
        AutoTuneAgentServiceImpl service = new AutoTuneAgentServiceImpl(
                llmChatService,
@@ -370,6 +472,30 @@
        return new AutoTuneAgentServiceImpl(llmChatService, mcpToolManager, aiPromptTemplateService);
    }
    private AutoTuneCoordinatorServiceImpl coordinatorService() {
        return new AutoTuneCoordinatorServiceImpl(
                configService,
                wrkMastService,
                aiAutoTuneJobService,
                autoTuneAgentService,
                redisUtil,
                operateLogService);
    }
    private AutoTuneAgentService.AutoTuneAgentResult successfulAgentResult() {
        AutoTuneAgentService.AutoTuneAgentResult result = new AutoTuneAgentService.AutoTuneAgentResult();
        result.setSuccess(true);
        result.setTriggerType(AutoTuneTriggerType.AUTO.getCode());
        result.setSummary("no changes needed");
        result.setToolCallCount(1);
        result.setLlmCallCount(1);
        result.setPromptTokens(10L);
        result.setCompletionTokens(5L);
        result.setTotalTokens(15L);
        result.setMaxRoundsReached(false);
        return result;
    }
    private AutoTuneChangeCommand change(String targetType, String targetId, String targetKey, String newValue) {
        AutoTuneChangeCommand command = new AutoTuneChangeCommand();
        command.setTargetType(targetType);