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;
|
}
|
|
}
|