完善评估能力:筛选参数 + degraded 排除 + 前端评估页

后端:
- settle_prediction 拒绝 degraded/failed(明确错误信息)
- get_eval_summary 支持 provider/model/prompt_version/mode 筛选
- 返回 filtered_settled/evaluated/skipped_degraded 等计数
- matches 游标分页方向修复(scheduled ASC 用 > 条件)
- available_at 加 2h 缓冲(近似完赛时间)
- bzzziro 统计字段映射注释(待真实响应验证)
- injuries 区分 no_local_data 与 success 空名单

前端:
- 新增 EvalPage(筛选控件 + 汇总卡片 + 准确率表格)
- 挂载 /admin/eval 路由与导航

测试:
- test_matches_cursor.py:游标方向
- test_available_at.py:2h 缓冲与回测防泄漏
- test_bzzoirot_stats.py:统计字段映射
- test_injuries_no_local_data.py:no_local_data vs success
- test_injuries_inserted_count.py:失败批不计入
- test_eval_excludes_degraded.py:degraded 排除准确率
This commit is contained in:
Profeto Agent
2026-09-19 09:40:24 +00:00
parent c2c4752856
commit 835d7217d0
21 changed files with 888 additions and 67 deletions
+1
View File
@@ -21,6 +21,7 @@ const NAV_ITEMS = [
{ to: '/admin/llm-config', label: 'LLM 配置', icon: '◬' },
{ to: '/admin/config', label: '系统配置', icon: '◑' },
{ to: '/admin/logs', label: '系统日志', icon: '▤' },
{ to: '/admin/eval', label: '评估管理', icon: '◈' },
]
/** 报眉日期行,与前台同款式 */
+16 -2
View File
@@ -106,9 +106,23 @@ export async function fetchPredictions(limit = 50): Promise<any[]> {
// ── 评估 & 回测 ─────────────────────────────────────────────────
export async function fetchEvalSummary(): Promise<EvalSummary | null> {
export async function fetchEvalSummary(params: {
limit?: number
provider?: string
model?: string
prompt_version?: string
mode?: string
league_code?: string
} = {}): Promise<EvalSummary | null> {
const sp = new URLSearchParams()
if (params.limit) sp.set('limit', String(params.limit))
if (params.provider) sp.set('provider', params.provider)
if (params.model) sp.set('model', params.model)
if (params.prompt_version) sp.set('prompt_version', params.prompt_version)
if (params.mode) sp.set('mode', params.mode)
if (params.league_code) sp.set('league_code', params.league_code)
try {
return await api.get<EvalSummary>(`${API_BASE}/eval/summary`)
return await api.get<EvalSummary>(`${API_BASE}/eval/summary?${sp}`)
} catch {
return null
}
+192
View File
@@ -0,0 +1,192 @@
/**
* Admin 后台 - 评估汇总页(报刊风)
*
* 功能:
* - 按 provider / model / prompt_version / mode / league_code 筛选
* - 展示汇总统计卡片(已结算/已评估/跳过数)
* - 准确率对比表格
* - 空态与加载态
*/
import { useCallback, useEffect, useState } from 'react'
import { fetchEvalSummary, fetchLeagues } from '../dal'
import type { EvalSummary } from '../types'
import { Card, CardBody, CardHeader, StatCard, Badge, DataTable, Alert, Spinner, EmptyState } from '../components'
interface Filters {
provider: string
model: string
prompt_version: string
mode: string
league_code: string
}
const EMPTY_FILTERS: Filters = { provider: '', model: '', prompt_version: '', mode: '', league_code: '' }
export default function EvalPage() {
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS)
const [leagues, setLeagues] = useState<Array<{ code: string; name: string }>>([])
const [data, setData] = useState<EvalSummary | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetchLeagues()
.then(l => setLeagues(l.map(x => ({ code: x.code, name: x.name }))))
.catch(() => setLeagues([]))
}, [])
const load = useCallback(async () => {
setLoading(true)
setError(null)
try {
const params: Record<string, string> = {}
if (filters.provider) params.provider = filters.provider
if (filters.model) params.model = filters.model
if (filters.prompt_version) params.prompt_version = filters.prompt_version
if (filters.mode) params.mode = filters.mode
if (filters.league_code) params.league_code = filters.league_code
const result = await fetchEvalSummary(params)
setData(result)
} catch (err: unknown) {
setError(err instanceof Error ? err.message : '加载失败')
} finally {
setLoading(false)
}
}, [filters])
useEffect(() => { load() }, [load])
const handleChange = (key: keyof Filters) => (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
setFilters(f => ({ ...f, [key]: e.target.value }))
}
const handleReset = () => setFilters(EMPTY_FILTERS)
const summary = data?.summary ?? []
return (
<div className="space-y-6">
{/* 筛选控件 */}
<Card>
<CardHeader title="筛选条件" description="按提供商 / 模型 / 版本 / 模式 / 联赛过滤评估数据" />
<CardBody>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
<label className="block">
<span className="text-2xs text-ink-500"></span>
<input
type="text"
value={filters.provider}
onChange={handleChange('provider')}
placeholder="如 openai"
className="mt-1 w-full border border-ink-300 bg-paper-50 px-2.5 py-1.5 text-sm text-ink-900 placeholder:text-ink-300 focus:border-ink-900 focus:outline-none"
/>
</label>
<label className="block">
<span className="text-2xs text-ink-500"></span>
<input
type="text"
value={filters.model}
onChange={handleChange('model')}
placeholder="如 gpt-4o"
className="mt-1 w-full border border-ink-300 bg-paper-50 px-2.5 py-1.5 text-sm text-ink-900 placeholder:text-ink-300 focus:border-ink-900 focus:outline-none"
/>
</label>
<label className="block">
<span className="text-2xs text-ink-500">Prompt </span>
<input
type="text"
value={filters.prompt_version}
onChange={handleChange('prompt_version')}
placeholder="如 v1"
className="mt-1 w-full border border-ink-300 bg-paper-50 px-2.5 py-1.5 text-sm text-ink-900 placeholder:text-ink-300 focus:border-ink-900 focus:outline-none"
/>
</label>
<label className="block">
<span className="text-2xs text-ink-500"></span>
<select
value={filters.mode}
onChange={handleChange('mode')}
className="mt-1 w-full border border-ink-300 bg-paper-50 px-2.5 py-1.5 text-sm text-ink-900 focus:border-ink-900 focus:outline-none"
>
<option value=""></option>
<option value="single">single</option>
<option value="multi">multi</option>
</select>
</label>
<label className="block">
<span className="text-2xs text-ink-500"></span>
<select
value={filters.league_code}
onChange={handleChange('league_code')}
className="mt-1 w-full border border-ink-300 bg-paper-50 px-2.5 py-1.5 text-sm text-ink-900 focus:border-ink-900 focus:outline-none"
>
<option value=""></option>
{leagues.map(l => (
<option key={l.code} value={l.code}>{l.name ?? l.code}</option>
))}
</select>
</label>
</div>
<div className="mt-3 flex gap-2">
<button onClick={load} disabled={loading} className="btn btn-sm">
{loading ? '加载中…' : '应用筛选'}
</button>
<button onClick={handleReset} disabled={loading} className="btn btn-sm btn-ghost">
</button>
</div>
</CardBody>
</Card>
{/* 错误态 */}
{error && <Alert kind="error" title="加载失败" message={error} />}
{/* 汇总统计 */}
{data && (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<StatCard label="已结算总数" value={data.total_settled} hint="含 degraded" />
<StatCard label="筛选后已结算" value={data.filtered_settled} hint="应用筛选条件后" />
<StatCard label="实际评估" value={data.evaluated} hint="status=success 且比分齐全" />
<StatCard label="跳过 degraded" value={data.skipped_degraded} hint="不计入准确率" />
</div>
)}
{/* 准确率表格 */}
<Card>
<CardHeader
title="准确率对比"
description="按 provider × 模型聚合,仅统计有效预测"
/>
<CardBody>
{loading ? (
<div className="flex items-center justify-center py-12">
<Spinner />
</div>
) : summary.length === 0 ? (
<EmptyText text="暂无评估数据,请调整筛选条件或先完成预测与结算" />
) : (
<DataTable
columns={[
{ key: 'provider', label: '提供商' },
{ key: 'model', label: '模型' },
{ key: 'total', label: '评估条数' },
{ key: 'accuracy_1x2', label: '1X2 准确率', render: (row: any) => (
<span className="tabular-nums">{row.accuracy_1x2 != null ? `${row.accuracy_1x2}%` : '—'}</span>
) },
{ key: 'avg_score_rmse', label: '比分 RMSE', render: (row: any) => (
<span className="tabular-nums">{row.avg_score_rmse != null ? row.avg_score_rmse.toFixed(2) : '—'}</span>
) },
{ key: 'avg_subjective_confidence', label: '平均置信度', render: (row: any) => (
<span className="tabular-nums">{row.avg_subjective_confidence != null ? row.avg_subjective_confidence.toFixed(2) : '—'}</span>
) },
]}
data={summary}
rowKey={(row: any) => `${row.provider}-${row.model}`}
emptyText="暂无评估数据"
/>
)}
</CardBody>
</Card>
</div>
)
}
+2
View File
@@ -16,6 +16,7 @@ import DataSourcesPage from './pages/DataSources'
import LLMConfigPage from './pages/LLMConfig'
import ConfigPage from './pages/Config'
import LogsPage from './pages/Logs'
import EvalPage from './pages/EvalPage'
export const adminRoutes = [
{
@@ -31,6 +32,7 @@ export const adminRoutes = [
{ path: 'llm-config', element: <LLMConfigPage /> },
{ path: 'config', element: <ConfigPage /> },
{ path: 'logs', element: <LogsPage /> },
{ path: 'eval', element: <EvalPage /> },
{ path: '*', element: <Navigate to="/admin" replace /> },
],
},
+10
View File
@@ -111,6 +111,16 @@ export interface EvalSummary {
avg_score_rmse?: number | null
avg_subjective_confidence?: number | null
}>
/** 全量已结算数 */
total_settled: number
/** 应用筛选后的已结算数 */
filtered_settled: number
/** 实际评估条数(status=success 且比分齐全) */
evaluated: number
/** 跳过的 degraded 条数 */
skipped_degraded: number
/** 跳过的比分不全条数 */
skipped_incomplete?: number
}
export interface BacktestRequest {