fix(admin): 模型级健康分统一为 succ^α 口径,探测/重置结果即时落盘

能力列表的模型级健康分用线性 succ×100,而调度 healthFactor、维护快照落盘、健康变化
日志三处都用 succ^α×100。前端两处共用同一套 80/50 阈值,于是 succ=0.89 时模型行显示
89「健康」、渠道行显示 79「降级」,同一个渠道点进去自相矛盾。改为经
dispatchadapter.HealthAlpha() 取当前生效策略的 α(策略支持热更新,不硬编码 2)。
该入口刻意不走 Coordinator(),避免管理后台读接口有装配整个调度引擎的副作用。

渠道测试与重置健康度后请求即时重算快照:维护循环 5 分钟一轮,此前测试成功后 Redis
立刻变、能力列表(直读 Redis)立刻变,唯独渠道健康分要等下一轮,看起来像「测试没
生效」。重置单模型的分支此前完全不写 chn_health_scores,展示分永远停在旧值,一并修复。
This commit is contained in:
chengqian authored and qian committed 2026-08-27 21:59:43 +08:00
1 parent a5e2ecf527
commit 4f07b64ac7
2 files changed
+18 -3

No files matched your search

+14 -3
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"time"
do "github.com/qianfree/team-api/internal/model/do"
@@ -791,6 +792,9 @@ func (s *sAdmin) batchGetModelHealth(ctx context.Context, channelID int64, abili
}
redis := g.Redis()
// 健康分折算口径必须与调度 healthFactor / 维护快照落盘一致(succ^α×100),
// α 取当前生效的路由策略而非硬编码——策略支持热更新。
alpha := dispatchadapter.HealthAlpha()
for _, ab := range abilities {
healthKey := fmt.Sprintf("dispatch:v1:health:%d:%s", channelID, ab.ModelName)
breakerKey := fmt.Sprintf("dispatch:v1:breaker:%d:%s", channelID, ab.ModelName)
@@ -808,8 +812,8 @@ func (s *sAdmin) batchGetModelHealth(ctx context.Context, channelID int64, abili
var succEWMA float64
if _, err := fmt.Sscanf(succStr, "%f", &succEWMA); err == nil && succEWMA >= 0 {
data.SuccEWMA = &succEWMA
// 健康分 = succ_ewma * 100
healthScore := succEWMA * 100
// 健康分 = succ_ewma^α × 100(与调度 healthFactor 同源)
healthScore := math.Pow(succEWMA, alpha) * 100
data.HealthScore = &healthScore
}
}
@@ -959,13 +963,17 @@ func (s *sAdmin) ResetChannelHealth(ctx context.Context, req *v1.ChannelResetHea
// 调度层:只复位指定模型的熔断 + 成功率恢复
dispatchadapter.ResetModelHealth(ctx, req.ID, req.ModelName)
// 单模型重置不直接写 chn_health_scores(渠道分是全模型聚合值,不能按单模型拍值),
// 改为请求重算——否则展示分永远停在旧值,管理员会以为重置没生效
dispatchadapter.RequestHealthSnapshot(req.ID)
return nil, nil
}
// 未指定模型名,重置整个渠道(含所有模型)
// 展示层:健康分落库为 80(调度不读此表,仅供仪表盘/审计展示;下次维护快照会按策略重算)
// 展示层:健康分落库为 80(调度不读此表,仅供仪表盘/审计展示;随后的按需重算/
// 维护快照会按 Redis 实际读值重算,此处只是让界面立即有反馈)
affected, err := dao.ChnHealthScores.Ctx(ctx).
Where("channel_id", req.ID).
Data(do.ChnHealthScores{
@@ -998,6 +1006,9 @@ func (s *sAdmin) ResetChannelHealth(ctx context.Context, req *v1.ChannelResetHea
// 调度层:复位渠道级/模型级熔断 + 成功率恢复,渠道立即恢复被选择能力
dispatchadapter.ResetChannelHealth(ctx, req.ID)
// 请求按 Redis 实际读值重算落盘:上面写死的 80 只是调度引擎未组装时的兜底,
// 重算后会收敛到与调度同源的 0.9^α×100
dispatchadapter.RequestHealthSnapshot(req.ID)
return nil, nil
}
+4
View File
@@ -121,6 +121,10 @@ func (s *sAdmin) TestChannel(ctx context.Context, req *v1.ChannelTestReq) (*v1.C
// 渠道启用时 MarkChannelRecovered 会复位熔断,无需在禁用期间预热。
if ch.Status == "active" {
dispatchadapter.ReportProbeOutcome(ctx, channelID, testModel, result.Success, float64(latencyMs))
// 请求立即重算渠道健康快照:维护循环 5 分钟一轮,不触发的话管理员会看到
// "测试成功了但渠道健康度纹丝不动"(模型级能力列表直读 Redis 会立刻变,
// 渠道健康分读的是 chn_health_scores 落盘值)
dispatchadapter.RequestHealthSnapshot(channelID)
if dispatchadapter.CatalogHasModel(channelID, testModel) {
g.Log().Infof(ctx, "[ChannelProbe] 渠道 %s (%d) 探测结果已投递健康上报 | 模型: %s | success: %v | 延迟: %dms(EWMA 变化将打印 [ChannelHealth] 日志,5 分钟内落盘刷新)",
ch.Name, channelID, testModel, result.Success, latencyMs)