feat(data_process): 问答对数据评测体系与质量分雷达图

- 三层评测:规则层沿用原五维规则分,语义层用本地 BGE 向量算问答/来源
  相关性,评审层复用生成模型按 rubric 打分(忠实度/正确性/清晰度等,
  区分 standard/reasoning/dpo 输出类型),任一层失败自动降级
- 组合分 = 规则 35% + 语义 20% + 评审 45%,缺层自动重归一
- 新增 results/evaluate-batch 批量评测接口,镜像批量重生成的并发、
  乐观锁与部分成功语义;生成阶段不再展示质量分
- 详情页与结果编辑页新增"数据评测"按钮和批量进度;质量分列悬停弹出
  雷达图浮窗(评审 5 维 + 语义 2 维、三层分项、评审理由)
- 手动编辑/恢复后重算规则与语义层并丢弃过期评审分,雷达图不再展示
  失效数据
This commit is contained in:
caoxiaozhu
2026-08-19 14:21:54 +08:00
parent f47611020a
commit 81c2f85c3a
16 changed files with 1917 additions and 33 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { BulkResultRegenerationState, PreviewItem, ResultItem } from './types'
import QualityRadarPopover from './QualityRadarPopover.vue'
import type { BulkResultRegenerationState, PreviewItem, ResultEvaluationState, ResultItem } from './types'
import type { DataProcessOutputType } from '@/types/dataProcess'
const props = defineProps<{
@@ -9,6 +10,7 @@ const props = defineProps<{
selectedId: string | null
regeneratingResultId: string | null
bulkRegeneration: BulkResultRegenerationState
evaluation: ResultEvaluationState
outputType: DataProcessOutputType
}>()
@@ -17,6 +19,7 @@ const emit = defineEmits<{
'update:field': [id: string, field: 'instruction' | 'input' | 'output' | 'chosen' | 'rejected', value: string]
'regenerate:item': [id: string]
'regenerate:all': []
'evaluate:all': []
}>()
const search = ref('')
@@ -30,6 +33,15 @@ const bulkRegenerationActive = computed(() => props.bulkRegeneration.status ===
const bulkRegenerationVisible = computed(() => (
props.bulkRegeneration.status !== 'idle' && props.bulkRegeneration.total > 0
))
const evaluationActive = computed(() => props.evaluation.status === 'running')
const evaluationVisible = computed(() => (
props.evaluation.status !== 'idle' && props.evaluation.total > 0
))
const evaluationPercentage = computed(() => {
if (!props.evaluation.total) return 0
return Math.round((props.evaluation.completed / props.evaluation.total) * 100)
})
const evaluatedCount = computed(() => props.items.filter((item) => item.qualityDetails?.evaluated).length)
const bulkRegenerationPercentage = computed(() => (
props.bulkRegeneration.total > 0
? Math.round((props.bulkRegeneration.completed / props.bulkRegeneration.total) * 100)
@@ -98,21 +110,47 @@ function selectRelative(offset: number) {
<template>
<section class="result-step">
<div class="result-workspace">
<aside class="result-list-pane" :class="{ 'has-bulk-progress': bulkRegenerationVisible }">
<aside class="result-list-pane" :class="{ 'has-bulk-progress': bulkRegenerationVisible || evaluationVisible }">
<div class="pane-header result-list-header">
<div class="result-list-title"><strong>生成结果</strong><span> {{ items.length }} </span></div>
<el-button
v-if="invalidCount > 0"
size="small"
plain
type="primary"
:loading="bulkRegenerationActive"
:disabled="Boolean(regeneratingResultId) || bulkRegenerationActive"
@click="emit('regenerate:all')"
>
<i v-if="!bulkRegenerationActive" class="fa fa-refresh" style="margin-right: 4px;" />
{{ bulkRegenerationActive ? '重新生成中' : `全部重新生成(${invalidCount}` }}
</el-button>
<div class="result-list-title">
<strong>生成结果</strong><span> {{ items.length }} <template v-if="evaluatedCount"> · 已评测 {{ evaluatedCount }}</template></span>
</div>
<div class="result-list-actions">
<el-button
size="small"
plain
:loading="evaluationActive"
:disabled="!items.length || bulkRegenerationActive || Boolean(regeneratingResultId)"
@click="emit('evaluate:all')"
>
<i v-if="!evaluationActive" class="fa fa-check-square-o" style="margin-right: 4px;" />
数据评测
</el-button>
<el-button
v-if="invalidCount > 0"
size="small"
plain
type="primary"
:loading="bulkRegenerationActive"
:disabled="Boolean(regeneratingResultId) || bulkRegenerationActive || evaluationActive"
@click="emit('regenerate:all')"
>
<i v-if="!bulkRegenerationActive" class="fa fa-refresh" style="margin-right: 4px;" />
{{ bulkRegenerationActive ? '重新生成中' : `全部重新生成(${invalidCount}` }}
</el-button>
</div>
</div>
<div v-if="evaluationVisible" class="bulk-regeneration-progress">
<div>
<span>数据评测 {{ evaluation.completed }} / {{ evaluation.total }}</span>
<span>成功 {{ evaluation.succeeded }} · 失败 {{ evaluation.failed }}</span>
</div>
<el-progress
:percentage="evaluationPercentage"
:show-text="false"
:stroke-width="5"
:color="evaluation.failed > 0 ? '#d97706' : '#5b50f2'"
/>
</div>
<div v-if="bulkRegenerationVisible" class="bulk-regeneration-progress">
<div>
@@ -146,6 +184,23 @@ function selectRelative(offset: number) {
<strong>{{ item.instruction || '未填写指令' }}</strong>
<small>{{ outputType === 'dpo' ? (item.chosen || '未填写 Chosen') : (item.output || '未填写输出') }}</small>
</span>
<el-popover
v-if="item.qualityScore != null && item.qualityDetails"
placement="right"
:width="296"
trigger="hover"
:show-after="150"
popper-class="quality-radar-popper"
>
<template #reference>
<span
class="result-score"
:class="item.qualityScore >= 80 ? 'is-success' : item.qualityScore >= 60 ? 'is-warning' : 'is-danger'"
@click.stop
>{{ item.qualityScore.toFixed(0) }}</span>
</template>
<QualityRadarPopover :quality="item.qualityDetails" :score="item.qualityScore" />
</el-popover>
<i v-if="itemRegenerating(item.id)" class="css-spinner" />
<i
v-else
@@ -303,6 +358,42 @@ function selectRelative(offset: number) {
}
}
.result-list-actions {
display: flex;
flex: none;
align-items: center;
gap: 8px;
}
.result-score {
flex: none;
min-width: 34px;
padding: 2px 8px;
border-radius: 10px;
color: #475467;
background: #f2f4f7;
font-size: 12px;
font-weight: 700;
font-variant-numeric: tabular-nums;
text-align: center;
cursor: default;
&.is-success {
color: #067647;
background: #e6f4ee;
}
&.is-warning {
color: #b54708;
background: #fef0c7;
}
&.is-danger {
color: #b42318;
background: #fee4e2;
}
}
.pane-header {
display: flex;
align-items: center;