- 三层评测:规则层沿用原五维规则分,语义层用本地 BGE 向量算问答/来源 相关性,评审层复用生成模型按 rubric 打分(忠实度/正确性/清晰度等, 区分 standard/reasoning/dpo 输出类型),任一层失败自动降级 - 组合分 = 规则 35% + 语义 20% + 评审 45%,缺层自动重归一 - 新增 results/evaluate-batch 批量评测接口,镜像批量重生成的并发、 乐观锁与部分成功语义;生成阶段不再展示质量分 - 详情页与结果编辑页新增"数据评测"按钮和批量进度;质量分列悬停弹出 雷达图浮窗(评审 5 维 + 语义 2 维、三层分项、评审理由) - 手动编辑/恢复后重算规则与语义层并丢弃过期评审分,雷达图不再展示 失效数据
253 lines
6.0 KiB
Vue
253 lines
6.0 KiB
Vue
<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>
|
||
|