完善评估能力:筛选参数 + 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:
@@ -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: '◈' },
|
||||
]
|
||||
|
||||
/** 报眉日期行,与前台同款式 */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 /> },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user