feat(data_process): 问答对数据评测体系与质量分雷达图
- 三层评测:规则层沿用原五维规则分,语义层用本地 BGE 向量算问答/来源 相关性,评审层复用生成模型按 rubric 打分(忠实度/正确性/清晰度等, 区分 standard/reasoning/dpo 输出类型),任一层失败自动降级 - 组合分 = 规则 35% + 语义 20% + 评审 45%,缺层自动重归一 - 新增 results/evaluate-batch 批量评测接口,镜像批量重生成的并发、 乐观锁与部分成功语义;生成阶段不再展示质量分 - 详情页与结果编辑页新增"数据评测"按钮和批量进度;质量分列悬停弹出 雷达图浮窗(评审 5 维 + 语义 2 维、三层分项、评审理由) - 手动编辑/恢复后重算规则与语义层并丢弃过期评审分,雷达图不再展示 失效数据
This commit is contained in:
252
frontend/src/views/data-process/create/QualityRadarPopover.vue
Normal file
252
frontend/src/views/data-process/create/QualityRadarPopover.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import '@/plugins/echarts'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import type { ResultQualityDetails } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
quality: ResultQualityDetails
|
||||
score?: number
|
||||
}>()
|
||||
|
||||
// 轴标签按词意预置断行,避免长中文标签把雷达网格挤偏或被机械切词。
|
||||
const JUDGE_DIMENSION_LABELS: Record<string, string> = {
|
||||
faithfulness: '忠实度',
|
||||
correctness: '正确性',
|
||||
clarity: '问题\n清晰度',
|
||||
completeness: '回答\n完整性',
|
||||
alignment: '指令\n对齐',
|
||||
reasoning_validity: '推理\n有效性',
|
||||
chosen_quality: 'chosen\n质量',
|
||||
rejected_quality: 'rejected\n质量',
|
||||
preference_reasonableness: '偏好\n区分',
|
||||
}
|
||||
|
||||
const SEMANTIC_DIMENSION_LABELS: Record<string, string> = {
|
||||
question_answer: '问答\n相关',
|
||||
answer_source: '来源\n覆盖',
|
||||
}
|
||||
|
||||
interface RadarDimension {
|
||||
name: string
|
||||
value: number
|
||||
}
|
||||
|
||||
const radarDimensions = computed<RadarDimension[]>(() => {
|
||||
const dimensions: RadarDimension[] = []
|
||||
for (const [key, value] of Object.entries(props.quality?.judge?.scores ?? {})) {
|
||||
dimensions.push({
|
||||
name: JUDGE_DIMENSION_LABELS[key] ?? key,
|
||||
value: Math.round(value * 20),
|
||||
})
|
||||
}
|
||||
for (const [key, value] of Object.entries(props.quality?.semantic ?? {})) {
|
||||
if (key === 'overall' || typeof value !== 'number') continue
|
||||
dimensions.push({
|
||||
name: SEMANTIC_DIMENSION_LABELS[key] ?? key,
|
||||
value: Math.round(value),
|
||||
})
|
||||
}
|
||||
return dimensions
|
||||
})
|
||||
|
||||
// 可用维度太少时雷达图失去意义,降级为分层分数展示。
|
||||
const showRadar = computed(() => radarDimensions.value.length >= 3)
|
||||
|
||||
const radarOption = computed<EChartsOption>(() => ({
|
||||
radar: {
|
||||
indicator: radarDimensions.value.map((dimension) => ({
|
||||
name: dimension.name,
|
||||
max: 100,
|
||||
})),
|
||||
radius: '56%',
|
||||
center: ['50%', '50%'],
|
||||
splitNumber: 4,
|
||||
axisName: {
|
||||
color: '#667085',
|
||||
fontSize: 10,
|
||||
lineHeight: 13,
|
||||
},
|
||||
splitArea: { areaStyle: { color: ['#fbfbfd', '#f2f4f8'] } },
|
||||
splitLine: { lineStyle: { color: '#e4e7ec' } },
|
||||
axisLine: { lineStyle: { color: '#e4e7ec' } },
|
||||
},
|
||||
series: [{
|
||||
type: 'radar',
|
||||
symbol: 'circle',
|
||||
symbolSize: 3,
|
||||
data: [{
|
||||
value: radarDimensions.value.map((dimension) => dimension.value),
|
||||
name: '质量维度',
|
||||
areaStyle: { color: 'rgba(91, 80, 242, 0.18)' },
|
||||
lineStyle: { color: '#5b50f2', width: 1.5 },
|
||||
itemStyle: { color: '#5b50f2' },
|
||||
}],
|
||||
}],
|
||||
}))
|
||||
|
||||
const layerScores = computed(() => {
|
||||
const layers = props.quality?.layers ?? {}
|
||||
return [
|
||||
{ label: '规则层', value: layers.rule },
|
||||
{ label: '语义层', value: layers.semantic },
|
||||
{ label: '评审层', value: layers.judge },
|
||||
].filter((layer): layer is { label: string; value: number } => (
|
||||
typeof layer.value === 'number'
|
||||
))
|
||||
})
|
||||
|
||||
const displayScore = computed(() => {
|
||||
if (typeof props.score === 'number' && !isNaN(props.score)) {
|
||||
return props.score.toFixed(1)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const scoreTone = computed(() => {
|
||||
const numScore = props.score ?? 0
|
||||
return numScore >= 80 ? 'is-success' : numScore >= 60 ? 'is-warning' : 'is-danger'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="quality-popover">
|
||||
<div v-if="displayScore !== null" class="popover-header">
|
||||
<strong>质量评测</strong>
|
||||
<span class="popover-score" :class="scoreTone">{{ displayScore }}</span>
|
||||
</div>
|
||||
|
||||
<VChart
|
||||
v-if="showRadar"
|
||||
class="quality-radar"
|
||||
:option="radarOption"
|
||||
autoresize
|
||||
/>
|
||||
<div v-else class="radar-fallback">
|
||||
维度数据不足,已评测维度少于 3 个时以分层分数为准。
|
||||
</div>
|
||||
|
||||
<div class="layer-scores">
|
||||
<div v-for="layer in layerScores" :key="layer.label" class="layer-item">
|
||||
<span>{{ layer.label }}</span>
|
||||
<el-progress
|
||||
:percentage="Math.round(layer.value)"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
:color="layer.value >= 80 ? '#12b76a' : layer.value >= 60 ? '#f0b429' : '#d92d20'"
|
||||
/>
|
||||
<em>{{ layer.value.toFixed(0) }}</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="quality?.judge?.reason" class="judge-reason">{{ quality.judge.reason }}</p>
|
||||
<div v-if="quality?.judge?.issues?.length" class="judge-issues">
|
||||
<span v-for="issue in quality.judge.issues" :key="issue" class="issue-tag">{{ issue }}</span>
|
||||
</div>
|
||||
<div v-if="quality?.judge?.model" class="judge-model">评审模型:{{ quality.judge.model }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.quality-popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #f2f4f7;
|
||||
|
||||
strong {
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-score {
|
||||
color: #344054;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&.is-success { color: #12b76a; }
|
||||
&.is-warning { color: #d99b0b; }
|
||||
&.is-danger { color: #d92d20; }
|
||||
}
|
||||
|
||||
.quality-radar {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.radar-fallback {
|
||||
padding: 18px 10px;
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.layer-scores {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr 28px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #667085;
|
||||
font-size: 11px;
|
||||
|
||||
em {
|
||||
color: #344054;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
.judge-reason {
|
||||
margin: 0;
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.judge-issues {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.issue-tag {
|
||||
padding: 2px 8px;
|
||||
color: #b54708;
|
||||
background: #fef0c7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.judge-model {
|
||||
color: #98a2b3;
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.quality-radar-popper {
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user