package store import "context" // 评测观测查询(admin「自动评测」页做实用;此前该页纯 mock)。全平台口径 → WithoutTenant。 // 注:评测的「纠偏前后全文轨迹」后端未持久化,只存了 Reason(评语)/Corrected(是否已纠偏采纳)/ // 各维度分,故错题本展示这些真数据,不含编造的 before/after 对照。 // EvalDay 是评测趋势按天一行。 type EvalDay struct { Day string `json:"day"` // YYYYMMDD AvgOverall float64 `json:"avg_overall"` // 当日综合分均值 [0,1] AvgFaithful float64 `json:"avg_faithful"` // 当日忠实度均值(仅计有来源的评测) Count int64 `json:"count"` PoorCount int64 `json:"poor_count"` // 当日 poor 级条数(幻觉/低质趋势) } // EvalTrend 按天聚合评测(from/to 为 YYYYMMDD)。avg_faithful 只算有检索来源的评测(sources>0), // 无来源的忠实度恒 0 会把均值压低失真。 func (p *Postgres) EvalTrend(ctx context.Context, from, to string) []EvalDay { if p.db == nil { return nil } var out []EvalDay p.db.WithContext(ctx).Model(&Eval{}). Select("to_char(created_at,'YYYYMMDD') as day, "+ "avg(overall) as avg_overall, "+ "avg(case when sources > 0 then faithful end) as avg_faithful, "+ "count(*) as count, "+ "count(case when level = 'poor' then 1 end) as poor_count"). Where("to_char(created_at,'YYYYMMDD') >= ? AND to_char(created_at,'YYYYMMDD') <= ?", from, to). Group("day").Order("day").Scan(&out) return out } // EvalSummary 是评测总览计数。 type EvalSummary struct { Total int64 `json:"total"` OK int64 `json:"ok"` Warn int64 `json:"warn"` Poor int64 `json:"poor"` Corrected int64 `json:"corrected"` // 经低分自动纠偏重生成后采纳的条数(恒温器闭环成效) AvgOverall float64 `json:"avg_overall"` // 区间综合分均值 } // EvalSummaryFor 区间内评测计数(from/to 为 YYYYMMDD)。 func (p *Postgres) EvalSummaryFor(ctx context.Context, from, to string) EvalSummary { var s EvalSummary if p.db == nil { return s } p.db.WithContext(ctx).Model(&Eval{}). Select("count(*) as total, "+ "count(case when level='ok' then 1 end) as ok, "+ "count(case when level='warn' then 1 end) as warn, "+ "count(case when level='poor' then 1 end) as poor, "+ "count(case when corrected then 1 end) as corrected, "+ "coalesce(avg(overall),0) as avg_overall"). Where("to_char(created_at,'YYYYMMDD') >= ? AND to_char(created_at,'YYYYMMDD') <= ?", from, to). Scan(&s) return s } // PoorEval 是错题本一行(低分评测 + 评语 + 纠偏标记;带租户名免前端二次查)。 type PoorEval struct { TaskID string `json:"task_id"` TenantName string `json:"tenant_name"` Owner string `json:"owner"` Overall float64 `json:"overall"` Rule float64 `json:"rule"` LLM float64 `json:"llm"` Faithful float64 `json:"faithful"` Level string `json:"level"` Reason string `json:"reason"` Sources int `json:"sources"` Corrected bool `json:"corrected"` CreatedAt string `json:"created_at"` } // PoorEvals 最近的低分评测(level=poor/warn,错题本)。 func (p *Postgres) PoorEvals(ctx context.Context, limit int) []PoorEval { if p.db == nil { return nil } if limit <= 0 || limit > 100 { limit = 30 } var out []PoorEval p.db.WithContext(ctx).Table("sundynix_eval e"). Select("e.task_id, coalesce(t.name,'') as tenant_name, e.owner, e.overall, e.rule, e.llm, " + "e.faithful, e.level, e.reason, e.sources, e.corrected, " + "to_char(e.created_at,'YYYY-MM-DD HH24:MI') as created_at"). Joins("left join sundynix_tenant t on t.id = e.tenant_id"). Where("e.level in ('poor','warn') AND e.deleted_at IS NULL"). Order("e.created_at desc").Limit(limit).Scan(&out) return out }