#AI
zhou zhou
4 小时以前 51877df13075ad10ef51107f15bcd21f1661febe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package com.vincent.rsf.server.ai.service;
 
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.system.entity.AiModelRoute;
import com.vincent.rsf.server.system.service.AiModelRouteService;
import lombok.Data;
import org.springframework.stereotype.Service;
 
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
 
@Service
public class AiModelRouteRuntimeService {
 
    @Resource
    private AiRuntimeConfigService aiRuntimeConfigService;
    @Resource
    private AiModelRouteService aiModelRouteService;
    @Resource
    private com.vincent.rsf.server.ai.config.AiProperties aiProperties;
 
    /**
     * 为一次聊天/诊断请求解析候选模型列表。
     * 顺序是:会话首选模型 -> 当前场景可用路由 -> 默认模型。
     */
    public List<RouteCandidate> resolveCandidates(Long tenantId, String sceneCode, String preferredModelCode) {
        List<RouteCandidate> output = new ArrayList<>();
        Set<String> seen = new LinkedHashSet<>();
        if (preferredModelCode != null && !preferredModelCode.trim().isEmpty()) {
            RouteCandidate preferredCandidate = toCandidate(null, preferredModelCode, seen);
            if (preferredCandidate != null) {
                output.add(preferredCandidate);
            }
        }
        String routeCode = resolveRouteCode(sceneCode);
        for (AiModelRoute route : aiModelRouteService.listAvailableRoutes(tenantId, routeCode)) {
            RouteCandidate candidate = toCandidate(route, route.getModelCode(), seen);
            if (candidate != null) {
                output.add(candidate);
            }
        }
        if (output.isEmpty()) {
            RouteCandidate defaultCandidate = toCandidate(null, aiRuntimeConfigService.resolveDefaultModelCode(), seen);
            if (defaultCandidate != null) {
                output.add(defaultCandidate);
            }
        }
        return output;
    }
 
    /**
     * 标记某条模型路由本次调用成功,并清空失败计数和冷却状态。
     */
    public void markSuccess(Long routeId) {
        if (routeId == null) {
            return;
        }
        aiModelRouteService.update(new LambdaUpdateWrapper<AiModelRoute>()
                .setSql("success_count = IFNULL(success_count, 0) + 1")
                .set(AiModelRoute::getFailCount, 0)
                .set(AiModelRoute::getCooldownUntil, null)
                .eq(AiModelRoute::getId, routeId));
    }
 
    /**
     * 标记某条模型路由本次调用失败。
     * 连续失败达到阈值后会进入冷却期,避免短时间内持续命中故障模型。
     */
    public void markFailure(Long routeId) {
        if (routeId == null) {
            return;
        }
        AiModelRoute route = aiModelRouteService.getById(routeId);
        if (route == null) {
            return;
        }
        int nextFailCount = (route.getFailCount() == null ? 0 : route.getFailCount()) + 1;
        Date cooldownUntil = route.getCooldownUntil();
        if (nextFailCount >= aiProperties.getRouteFailThreshold()) {
            cooldownUntil = new Date(System.currentTimeMillis() + aiProperties.getRouteCooldownMinutes() * 60_000L);
        }
        route.setFailCount(nextFailCount);
        route.setCooldownUntil(cooldownUntil);
        aiModelRouteService.updateById(route);
    }
 
    /**
     * 手动重置一条路由的成功/失败统计和冷却时间。
     */
    public void resetRoute(Long routeId) {
        if (routeId == null) {
            return;
        }
        aiModelRouteService.update(new LambdaUpdateWrapper<AiModelRoute>()
                .set(AiModelRoute::getFailCount, 0)
                .set(AiModelRoute::getSuccessCount, 0)
                .set(AiModelRoute::getCooldownUntil, null)
                .eq(AiModelRoute::getId, routeId));
    }
 
    /**
     * 将聊天场景统一映射成路由组编码。
     */
    private String resolveRouteCode(String sceneCode) {
        if (AiSceneCode.SYSTEM_DIAGNOSE.equals(sceneCode)) {
            return AiSceneCode.SYSTEM_DIAGNOSE;
        }
        return AiSceneCode.GENERAL_CHAT;
    }
 
    /**
     * 基于模型编码和路由记录组装运行时候选项,并负责去重与可用性校验。
     */
    private RouteCandidate toCandidate(AiModelRoute route, String modelCode, Set<String> seen) {
        if (modelCode == null || modelCode.trim().isEmpty() || seen.contains(modelCode)) {
            return null;
        }
        AiRuntimeConfigService.ModelRuntimeConfig runtimeConfig = aiRuntimeConfigService.resolveModel(modelCode);
        if (runtimeConfig == null || !Boolean.TRUE.equals(runtimeConfig.getEnabled())) {
            return null;
        }
        seen.add(modelCode);
        RouteCandidate candidate = new RouteCandidate();
        candidate.setRouteId(route == null ? null : route.getId());
        candidate.setRouteCode(route == null ? null : route.getRouteCode());
        candidate.setAttemptModelCode(runtimeConfig.getCode());
        candidate.setRuntimeConfig(runtimeConfig);
        return candidate;
    }
 
    @Data
    public static class RouteCandidate {
        private Long routeId;
        private String routeCode;
        private String attemptModelCode;
        private AiRuntimeConfigService.ModelRuntimeConfig runtimeConfig;
    }
 
}