feat: 评测任务完成进度收尾与指标维度汇总展示

- platform_store: 评测任务完成时置进度 100% 并落地 completed_time
- eval_runner: 新增指标维度汇总,供雷达图等维度展示
- 前端: 评测详情/列表展示优化、模型管理增强
This commit is contained in:
wuyongtao
2026-08-21 10:48:58 +08:00
parent 03254f8196
commit 0233755859
6 changed files with 151 additions and 41 deletions

View File

@@ -3361,6 +3361,32 @@ class PlatformStore:
}) })
if new_status == "completed": if new_status == "completed":
updates["completed_time"] = utcnow() updates["completed_time"] = utcnow()
if new_status == "completed":
final_total = int(
(result_content or {}).get("sample_count")
or task.get("sample_count")
or (progress_detail or {}).get("total")
or 0
)
final_completed = int(
(result_content or {}).get("completed_count")
or task.get("completed_count")
or final_total
)
updates.update({
"progress": 100,
"progress_detail": {
**progress_detail,
"status": "completed",
"stage": "completed",
"total": final_total,
"completed": max(final_completed, final_total),
"percentage": 100,
"current_index": final_total,
"message": "评测完成",
},
"completed_time": utcnow(),
})
elif new_status in {"failed", "stopped"}: elif new_status in {"failed", "stopped"}:
updates.update({ updates.update({
"error": job.get("error") or task.get("error") or "", "error": job.get("error") or task.get("error") or "",

View File

@@ -170,6 +170,30 @@ def _metric_record(score: float | None, sample_count: int, error: str = "", avai
} }
def _metric_dimension_summary(metrics: dict[str, Any]) -> list[dict[str, Any]]:
labels = {
"bleu": "BLEU",
"rouge": "ROUGE-L",
"cosine": "Cosine 相似度",
"exact_match": "精确匹配",
"text_similarity": "文本相似度",
}
result: list[dict[str, Any]] = []
for name, item in metrics.items():
if not isinstance(item, dict) or item.get("score") is None:
continue
result.append({
"name": labels.get(name, name),
"score": float(item.get("score") or 0),
"max_score": float(item.get("max_score") or 100),
"pass_rate": float(item.get("score") or 0),
"sample_count": int(item.get("sample_count") or 0),
"available": item.get("available", True),
"error": item.get("error", ""),
})
return result
def _rouge_tokens(text: str) -> str: def _rouge_tokens(text: str) -> str:
text = str(text or "").strip().lower() text = str(text or "").strip().lower()
tokens: list[str] = [] tokens: list[str] = []
@@ -650,7 +674,10 @@ def run_eval(config: dict[str, Any]) -> dict[str, Any]:
"score": overall_score, "score": overall_score,
"max_score": 100, "max_score": 100,
"pass_rate": round(passed_count / max(completed, 1) * 100, 1), "pass_rate": round(passed_count / max(completed, 1) * 100, 1),
}] "sample_count": completed,
"available": bool(scored),
"error": "部分样本未返回可解析评分" if len(scored) < completed else "",
}] + _metric_dimension_summary(metrics_result)
overall_evaluation = f"评测完成:{completed} 样本,{passed_count} 通过,平均 {avg_score}/100 分" overall_evaluation = f"评测完成:{completed} 样本,{passed_count} 通过,平均 {avg_score}/100 分"
else: else:
passed_count = 0 passed_count = 0
@@ -661,16 +688,7 @@ def run_eval(config: dict[str, Any]) -> dict[str, Any]:
] ]
overall_score = round(sum(enabled_scores) / len(enabled_scores), output_precision) if enabled_scores else 0 overall_score = round(sum(enabled_scores) / len(enabled_scores), output_precision) if enabled_scores else 0
overall_score_max = 100 overall_score_max = 100
dimension_summary = [ dimension_summary = _metric_dimension_summary(metrics_result)
{
"name": name,
"score": float(item.get("score") or 0),
"max_score": 100,
"pass_rate": float(item.get("score") or 0),
}
for name, item in metrics_result.items()
if isinstance(item, dict) and item.get("enabled", True) and item.get("score") is not None
]
overall_evaluation = f"评测完成:{completed} 样本(未配置 LLM 评委)" overall_evaluation = f"评测完成:{completed} 样本(未配置 LLM 评委)"
result = { result = {

View File

@@ -56,7 +56,19 @@ const overallScore = computed(() => formatScore(detail.value?.overall_score, det
const displayModelName = computed(() => detail.value?.model_name || String(detail.value?.model_id || '-')) const displayModelName = computed(() => detail.value?.model_name || String(detail.value?.model_id || '-'))
const displayMetric = computed(() => detail.value?.metric_label || detail.value?.metric || '-') const displayMetric = computed(() => detail.value?.metric_label || detail.value?.metric || '-')
const progressDetail = computed(() => detail.value?.progress_detail) const progressDetail = computed(() => detail.value?.progress_detail)
const progressPercentage = computed(() => Math.max(0, Math.min(100, Math.round(Number(progressDetail.value?.percentage ?? completionRate.value))))) const progressTotal = computed(() => Math.max(
Number(detail.value?.sample_count || 0),
Number(progressDetail.value?.total || 0),
Number(detail.value?.samples?.length || 0),
))
const progressCompleted = computed(() => detail.value?.status === 'completed'
? progressTotal.value
: Math.min(progressTotal.value || Number(detail.value?.completed_count || 0), Number(progressDetail.value?.completed ?? detail.value?.completed_count ?? 0)))
const progressPercentage = computed(() => detail.value?.status === 'completed'
? 100
: detail.value?.status === 'failed' || detail.value?.status === 'stopped'
? Math.max(0, Math.min(100, Math.round(Number(progressDetail.value?.percentage ?? detail.value?.progress ?? completionRate.value))))
: Math.max(0, Math.min(100, Math.round(Number(progressDetail.value?.percentage ?? completionRate.value)))))
const progressStage = computed(() => ({ const progressStage = computed(() => ({
dataset: '准备数据集', dataset: '准备数据集',
model_loading: '加载模型', model_loading: '加载模型',
@@ -64,14 +76,32 @@ const progressStage = computed(() => ({
metrics: '计算指标', metrics: '计算指标',
completed: '评测完成', completed: '评测完成',
failed: '评测失败', failed: '评测失败',
}[String(progressDetail.value?.stage || '')] || (detail.value?.status === 'running' ? '任务运行中' : '等待开始'))) }[detail.value?.status === 'completed' ? 'completed' : String(progressDetail.value?.stage || '')] || (detail.value?.status === 'running' ? '任务运行中' : '等待开始')))
const radarDimensions = computed(() => (detail.value?.dimension_summary || []) const radarDimensions = computed(() => {
.filter((item) => item.available !== false && Number.isFinite(Number(item.score))) const dimensions = new Map<string, { name: string; value: number }>()
.map((item) => ({ for (const item of detail.value?.dimension_summary || []) {
if (item.available === false || !Number.isFinite(Number(item.score))) continue
dimensions.set(item.name, {
name: item.name, name: item.name,
value: Math.max(0, Math.min(100, Number(item.score) / Math.max(Number(item.max_score) || 100, 1) * 100)), value: Math.max(0, Math.min(100, Number(item.score) / Math.max(Number(item.max_score) || 100, 1) * 100)),
}))) })
}
const metricLabels: Record<string, string> = {
bleu: 'BLEU',
rouge: 'ROUGE-L',
cosine: 'Cosine 相似度',
exact_match: '精确匹配',
text_similarity: '文本相似度',
}
for (const [key, metric] of Object.entries(detail.value?.basic_metrics || {})) {
const score = Number(metric?.score)
if (!Number.isFinite(score) || metric?.available === false) continue
const name = metricLabels[key] || key
if (!dimensions.has(name)) dimensions.set(name, { name, value: Math.max(0, Math.min(100, score)) })
}
return [...dimensions.values()]
})
const radarOption = computed<EChartsOption>(() => ({ const radarOption = computed<EChartsOption>(() => ({
tooltip: { trigger: 'item' }, tooltip: { trigger: 'item' },
@@ -197,7 +227,7 @@ onUnmounted(stopPolling)
</div> </div>
<div class="overview-item"> <div class="overview-item">
<span>评测进度</span> <span>评测进度</span>
<strong>{{ progressDetail?.completed ?? detail.completed_count }} / {{ progressDetail?.total ?? detail.sample_count }}</strong> <strong>{{ progressCompleted }} / {{ progressTotal }}</strong>
<el-progress :percentage="progressPercentage" :show-text="false" :stroke-width="5" /> <el-progress :percentage="progressPercentage" :show-text="false" :stroke-width="5" />
<small>{{ progressStage }}{{ progressDetail?.message ? ' · ' + progressDetail.message : '' }}</small> <small>{{ progressStage }}{{ progressDetail?.message ? ' · ' + progressDetail.message : '' }}</small>
</div> </div>

View File

@@ -65,6 +65,20 @@ function displayMetric(row: Partial<EvalTask>) {
return row.metric_label || row.metric || '-' return row.metric_label || row.metric || '-'
} }
function progressPercentage(row: Partial<EvalTask>) {
if (row.status === 'completed') return 100
return Math.max(0, Math.min(100, Math.round(Number(row.progress_detail?.percentage ?? row.progress ?? 0))))
}
function progressCompleted(row: Partial<EvalTask>) {
if (row.status === 'completed') return row.progress_detail?.total ?? '-'
return row.progress_detail?.completed ?? 0
}
function progressTotal(row: Partial<EvalTask>) {
return row.progress_detail?.total ?? '-'
}
const { start: startPolling, stop: stopPolling } = usePolling( const { start: startPolling, stop: stopPolling } = usePolling(
async () => { async () => {
await loadEvalList({ silent: true }) await loadEvalList({ silent: true })
@@ -125,18 +139,15 @@ onUnmounted(() => {
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="评分" prop="score" width="100" align="center" /> <el-table-column label="评分" prop="score" width="100" align="center">
<template #default="{ row }">
{{ row.score == null ? '-' : `${Number(row.score).toFixed(2)} / 100` }}
</template>
</el-table-column>
<el-table-column label="评测进度" width="130" align="center"> <el-table-column label="评测进度" width="130" align="center">
<template #default="{ row }"> <template #default="{ row }">
<template v-if="ACTIVE_STATUSES.has(String(row.status || ''))"> <el-progress :percentage="progressPercentage(row)" :stroke-width="6" :show-text="false" />
<el-progress <small>{{ progressCompleted(row) }} / {{ progressTotal(row) }}</small>
:percentage="Math.max(0, Math.min(100, Math.round(Number(row.progress_detail?.percentage ?? row.progress ?? 0))))"
:stroke-width="6"
:show-text="false"
/>
<small>{{ row.progress_detail?.completed ?? 0 }} / {{ row.progress_detail?.total ?? '-' }}</small>
</template>
<span v-else>{{ row.score == null ? '-' : Number(row.score).toFixed(2) + ' / 100' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100" align="center"> <el-table-column label="状态" width="100" align="center">

View File

@@ -56,8 +56,8 @@ async function handleMerge() {
compute_node_id: form.compute_node_id, compute_node_id: form.compute_node_id,
output_model_name: `${form.model_name}-merged`, output_model_name: `${form.model_name}-merged`,
}) })
ElMessage.success('合并成功') ElMessage.success('合并任务已提交,完成后会自动更新状态')
router.push('/model-manage') router.push({ path: '/model-manage', query: { tab: 'trained' } })
} catch { } catch {
// ignore // ignore
} finally { } finally {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import DataTablePage from '@/components/DataTablePage.vue' import DataTablePage from '@/components/DataTablePage.vue'
@@ -20,6 +20,7 @@ import { MODEL_SOURCE_MAP, MODEL_TYPE_MAP, PURPOSE_MAP } from '@/constants'
import type { ModelItem, TrainedModel } from '@/types' import type { ModelItem, TrainedModel } from '@/types'
import { mergeStatusLabel, mergeStatusType, statusLabel, statusTagType } from '@/utils/status' import { mergeStatusLabel, mergeStatusType, statusLabel, statusTagType } from '@/utils/status'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { usePolling } from '@/composables/usePolling'
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
@@ -35,7 +36,7 @@ type TrainedModelRuntime = {
loaded: boolean loaded: boolean
} }
const activeTab = ref<TabKey>('config') const activeTab = ref<TabKey>(router.currentRoute.value.query.tab === 'trained' ? 'trained' : 'config')
const loading = ref(false) const loading = ref(false)
const configList = ref<ModelItem[]>([]) const configList = ref<ModelItem[]>([])
const trainedList = ref<TrainedModel[]>([]) const trainedList = ref<TrainedModel[]>([])
@@ -131,19 +132,36 @@ async function handleExport(row: TrainedModel) {
} }
} }
async function loadTrained() { async function loadTrained(silent = false) {
loading.value = true if (!silent) loading.value = true
try { try {
const res = await getTrainedModels() const res = await getTrainedModels()
trainedList.value = res?.models || [] trainedList.value = res?.models || []
} finally { } finally {
loading.value = false if (!silent) loading.value = false
} }
} }
function loadData() { function hasActiveMerge() {
return trainedList.value.some((item) => Boolean(item.merging))
}
const { start: startTrainedPolling, stop: stopTrainedPolling } = usePolling(
async () => {
await loadTrained(true)
if (!hasActiveMerge()) stopTrainedPolling()
},
3000,
{ immediate: false },
)
async function loadData() {
if (activeTab.value === 'config') loadConfig() if (activeTab.value === 'config') loadConfig()
else loadTrained() else {
await loadTrained()
if (hasActiveMerge()) startTrainedPolling()
else stopTrainedPolling()
}
} }
async function loadTrainedRuntime(row: TrainedModel, force = false) { async function loadTrainedRuntime(row: TrainedModel, force = false) {
@@ -217,9 +235,16 @@ function handleRefresh() {
else loadTrained() else loadTrained()
} }
watch(activeTab, loadData) watch(activeTab, () => {
stopTrainedPolling()
void loadData()
})
onMounted(loadData) onMounted(() => {
void loadData()
})
onUnmounted(stopTrainedPolling)
</script> </script>
<template> <template>