From 4f07b64ac77364c701c76fafd2d0ddf6e91c9811 Mon Sep 17 00:00:00 2001 From: chengqian Date: Thu, 27 Aug 2026 17:00:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E6=A8=A1=E5=9E=8B=E7=BA=A7?= =?UTF-8?q?=E5=81=A5=E5=BA=B7=E5=88=86=E7=BB=9F=E4=B8=80=E4=B8=BA=20succ^?= =?UTF-8?q?=CE=B1=20=E5=8F=A3=E5=BE=84=EF=BC=8C=E6=8E=A2=E6=B5=8B/?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E7=BB=93=E6=9E=9C=E5=8D=B3=E6=97=B6=E8=90=BD?= =?UTF-8?q?=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 能力列表的模型级健康分用线性 succ×100,而调度 healthFactor、维护快照落盘、健康变化 日志三处都用 succ^α×100。前端两处共用同一套 80/50 阈值,于是 succ=0.89 时模型行显示 89「健康」、渠道行显示 79「降级」,同一个渠道点进去自相矛盾。改为经 dispatchadapter.HealthAlpha() 取当前生效策略的 α(策略支持热更新,不硬编码 2)。 该入口刻意不走 Coordinator(),避免管理后台读接口有装配整个调度引擎的副作用。 渠道测试与重置健康度后请求即时重算快照:维护循环 5 分钟一轮,此前测试成功后 Redis 立刻变、能力列表(直读 Redis)立刻变,唯独渠道健康分要等下一轮,看起来像「测试没 生效」。重置单模型的分支此前完全不写 chn_health_scores,展示分永远停在旧值,一并修复。 --- internal/logic/admin/channel.go | 17 ++++++++++++++--- internal/logic/admin/channel_testing.go | 4 ++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/internal/logic/admin/channel.go b/internal/logic/admin/channel.go index c914d7e..4f95097 100644 --- a/internal/logic/admin/channel.go +++ b/internal/logic/admin/channel.go @@ -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 } diff --git a/internal/logic/admin/channel_testing.go b/internal/logic/admin/channel_testing.go index 5360107..f3932ad 100644 --- a/internal/logic/admin/channel_testing.go +++ b/internal/logic/admin/channel_testing.go @@ -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)