feat: 评测任务完成进度收尾与指标维度汇总展示
- platform_store: 评测任务完成时置进度 100% 并落地 completed_time - eval_runner: 新增指标维度汇总,供雷达图等维度展示 - 前端: 评测详情/列表展示优化、模型管理增强
This commit is contained in:
@@ -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 "",
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 || []) {
|
||||||
name: item.name,
|
if (item.available === false || !Number.isFinite(Number(item.score))) continue
|
||||||
value: Math.max(0, Math.min(100, Number(item.score) / Math.max(Number(item.max_score) || 100, 1) * 100)),
|
dimensions.set(item.name, {
|
||||||
})))
|
name: item.name,
|
||||||
|
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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user