第一次提交

This commit is contained in:
wangjiming
2026-07-27 09:12:47 +08:00
commit b4ff5db17b
579 changed files with 48768 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
export interface BasicMetricSetupDraft {
bleu_enabled: boolean
bleu_n: number
rouge_enabled: boolean
rouge_methods: string[]
cosine_enabled: boolean
output_precision: number
}
defineProps<{
disabled?: boolean
}>()
const form = defineModel<BasicMetricSetupDraft>({ required: true })
const formRef = ref<FormInstance>()
const rules: FormRules<BasicMetricSetupDraft> = {
bleu_n: [{ required: true, type: 'number', min: 1, max: 8, message: 'BLEU ngram must be between 1 and 8', trigger: 'change' }],
output_precision: [{ required: true, type: 'number', min: 0, max: 8, message: 'Precision must be between 0 and 8', trigger: 'change' }],
}
async function validate() {
if (!formRef.value) return false
try {
return !!(await formRef.value.validate())
} catch {
return false
}
}
defineExpose({ validate })
</script>
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="140px"
class="basic-metric-form"
:disabled="disabled"
>
<el-form-item label="BLEU">
<el-switch v-model="form.bleu_enabled" />
</el-form-item>
<el-form-item v-if="form.bleu_enabled" label="BLEU ngram" prop="bleu_n">
<el-input-number v-model="form.bleu_n" :min="1" :max="8" />
</el-form-item>
<el-form-item label="ROUGE">
<el-switch v-model="form.rouge_enabled" />
</el-form-item>
<el-form-item v-if="form.rouge_enabled" label="ROUGE methods">
<el-checkbox-group v-model="form.rouge_methods">
<el-checkbox value="rouge_1">ROUGE-1</el-checkbox>
<el-checkbox value="rouge_2">ROUGE-2</el-checkbox>
<el-checkbox value="rouge_l">ROUGE-L</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="Cosine">
<el-switch v-model="form.cosine_enabled" />
</el-form-item>
<el-form-item label="Output precision" prop="output_precision">
<el-input-number v-model="form.output_precision" :min="0" :max="8" />
</el-form-item>
</el-form>
</template>
<style scoped>
.basic-metric-form {
max-width: 760px;
}
</style>

View File

@@ -0,0 +1,215 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, watch } from 'vue'
import { EVAL_METHODS, EVAL_METHOD_PROMPTS } from '@/constants/dimension'
import { DIMENSION_TYPE_MAP } from '@/constants'
import type { DimensionType, ModelItem } from '@/types'
// 编辑器体积较大,仅在用户进入包含 Prompt 的指标配置时加载。
const MdEditor = defineAsyncComponent(async () => {
await import('md-editor-v3/lib/style.css')
const module = await import('md-editor-v3')
return module.MdEditor
})
export interface DimensionFormDraft {
type: DimensionType | ''
description: string
eval_model: string
eval_method: string | string[]
eval_prompt: string
is_active: boolean
is_default: boolean
bleu_n: number
output_precision: number
score_min: number
score_max: number
pass_threshold: number
}
const props = withDefaults(
defineProps<{
evalModels: ModelItem[]
showDescription?: boolean
showStatusSettings?: boolean
allowedTypes?: DimensionType[]
}>(),
{
showDescription: true,
showStatusSettings: true,
allowedTypes: () => ['classification', 'metric', 'text_similarity'],
},
)
const form = defineModel<DimensionFormDraft>({ required: true })
const SCORE_STEP = 0.5
const typeOptions = computed(() =>
Object.entries(DIMENSION_TYPE_MAP)
.filter(([value]) => props.allowedTypes.includes(value as DimensionType))
.map(([value, label]) => ({ value, label })),
)
const currentMethods = computed(() => (form.value.type ? EVAL_METHODS[form.value.type] || [] : []))
/**
* 指标类型变化时初始化对应的评估方式和 Prompt。
* 同步执行可保证编辑页批量回填时,接口返回值最终覆盖默认值。
*/
watch(
() => form.value.type,
(type) => {
if (!type) return
const firstMethod = EVAL_METHODS[type]?.[0]?.value || ''
form.value.eval_method = type === 'text_similarity' ? [firstMethod] : firstMethod
if (type !== 'text_similarity' && EVAL_METHOD_PROMPTS[firstMethod]) {
form.value.eval_prompt = EVAL_METHOD_PROMPTS[firstMethod]
}
},
{ flush: 'sync' },
)
/** 评估方式变化时应用对应的预置 Prompt。 */
watch(
() => form.value.eval_method,
(method) => {
if (form.value.type === 'text_similarity' || !method) return
const selectedMethod = Array.isArray(method) ? method[0] : method
if (selectedMethod && EVAL_METHOD_PROMPTS[selectedMethod]) {
form.value.eval_prompt = EVAL_METHOD_PROMPTS[selectedMethod]
}
},
{ flush: 'sync' },
)
/** 始终保持评分区间合法,并把通过阈值收敛到当前区间内。 */
watch(
() => [form.value.type, form.value.score_min, form.value.score_max] as const,
([type, scoreMin, scoreMax]) => {
if (type !== 'metric') return
if (scoreMax <= scoreMin) {
form.value.score_max = scoreMin + SCORE_STEP
return
}
form.value.pass_threshold = Math.min(
Math.max(form.value.pass_threshold, scoreMin),
scoreMax,
)
},
{ flush: 'sync' },
)
</script>
<template>
<el-form-item label="指标类型" prop="type">
<el-select v-model="form.type" placeholder="请选择指标类型" style="width: 100%">
<el-option v-for="option in typeOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item v-if="showDescription" label="描述">
<el-input v-model="form.description" type="textarea" :rows="2" maxlength="200" show-word-limit />
</el-form-item>
<template v-if="form.type === 'classification' || form.type === 'metric'">
<el-form-item label="选择大模型" prop="eval_model">
<el-select v-model="form.eval_model" placeholder="请选择评测模型" filterable style="width: 100%">
<el-option v-for="model in evalModels" :key="model.id" :label="model.name" :value="model.name" />
</el-select>
</el-form-item>
<el-form-item label="评估方式" prop="eval_method">
<el-radio-group v-model="(form.eval_method as string)">
<el-radio v-for="method in currentMethods" :key="method.value" :value="method.value">
{{ method.name }}
<span class="method-desc">{{ method.desc }}</span>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="评估 Prompt" prop="eval_prompt">
<MdEditor
v-model="form.eval_prompt"
language="zh-CN"
:toolbars-exclude="['github', 'save']"
style="height: 320px"
/>
</el-form-item>
</template>
<template v-if="form.type === 'text_similarity'">
<el-form-item label="评估方式" prop="eval_method">
<el-checkbox-group v-model="(form.eval_method as string[])">
<el-checkbox v-for="method in currentMethods" :key="method.value" :value="method.value">
{{ method.name }}
<span class="method-desc">{{ method.desc }}</span>
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="BLEU n-gram">
<el-select v-model="form.bleu_n" style="width: 160px">
<el-option :value="1" label="1-gram" />
<el-option :value="2" label="2-gram" />
<el-option :value="3" label="3-gram" />
<el-option :value="4" label="4-gram" />
</el-select>
</el-form-item>
<el-form-item label="输出精度">
<el-select v-model="form.output_precision" style="width: 160px">
<el-option :value="1" label="1 位小数" />
<el-option :value="2" label="2 位小数" />
<el-option :value="3" label="3 位小数" />
<el-option :value="4" label="4 位小数" />
</el-select>
</el-form-item>
</template>
<template v-if="form.type === 'metric'">
<el-form-item label="评分最小值" prop="score_min">
<el-input-number
v-model="form.score_min"
:min="0"
:max="Math.max(form.score_max - SCORE_STEP, 0)"
:step="SCORE_STEP"
style="width: 160px"
/>
</el-form-item>
<el-form-item label="评分最大值" prop="score_max">
<el-input-number
v-model="form.score_max"
:min="form.score_min + SCORE_STEP"
:step="SCORE_STEP"
style="width: 160px"
/>
</el-form-item>
<el-form-item label="通过阈值" prop="pass_threshold">
<div style="width: 100%">
<el-slider
v-model="form.pass_threshold"
:min="form.score_min"
:max="form.score_max"
:step="0.5"
show-input
:show-input-controls="false"
/>
</div>
</el-form-item>
</template>
<template v-if="showStatusSettings">
<el-form-item label="启用该维度">
<el-switch v-model="form.is_active" />
</el-form-item>
<el-form-item label="设为默认">
<el-switch v-model="form.is_default" />
</el-form-item>
</template>
</template>
<style scoped>
.method-desc {
color: #909399;
font-size: 12px;
margin-left: 6px;
}
</style>

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import DimensionFormFields, { type DimensionFormDraft } from './DimensionFormFields.vue'
import type { DimensionType, ModelItem } from '@/types'
export type EvalRuleSetupDraft = DimensionFormDraft
defineProps<{
evalModels: ModelItem[]
disabled?: boolean
}>()
const form = defineModel<EvalRuleSetupDraft>({ required: true })
const newFormRef = ref<FormInstance>()
const LLM_METRIC_TYPES: DimensionType[] = ['classification', 'metric']
const dimensionRules: FormRules<DimensionFormDraft> = {
type: [{ required: true, message: '请选择大模型评测指标类型', trigger: 'change' }],
eval_model: [{ required: true, message: '请选择评测大模型', trigger: 'change' }],
eval_method: [{ required: true, message: '请选择评估方式', trigger: 'change' }],
eval_prompt: [{ required: true, message: '请填写评估 Prompt', trigger: 'blur' }],
}
async function validate() {
if (!newFormRef.value) return false
try {
return !!(await newFormRef.value.validate())
} catch {
return false
}
}
defineExpose({ validate })
</script>
<template>
<div class="rule-step">
<el-form
ref="newFormRef"
:model="form"
:rules="dimensionRules"
label-width="130px"
class="step-form step-form-wide"
:disabled="disabled"
scroll-to-error
>
<DimensionFormFields
v-model="form"
:eval-models="evalModels"
:show-description="false"
:show-status-settings="false"
:allowed-types="LLM_METRIC_TYPES"
/>
</el-form>
</div>
</template>
<style scoped>
.step-form {
max-width: 760px;
}
.step-form-wide {
max-width: 920px;
}
</style>

View File

@@ -0,0 +1,163 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import type { DatasetItem, GpuInfo, TrainedModel } from '@/types'
export interface EvalTaskSetupDraft {
eval_task_name: string
model_id: string | number
gpu_id: string | number
data_source: 'dataset' | 'inference'
dataset_id: string | number
leaderboard: boolean
}
defineProps<{
trainedModels: TrainedModel[]
evalDatasets: DatasetItem[]
gpus: GpuInfo[]
loading?: boolean
disabled?: boolean
}>()
const form = defineModel<EvalTaskSetupDraft>({ required: true })
const formRef = ref<FormInstance>()
const rules: FormRules<EvalTaskSetupDraft> = {
eval_task_name: [
{ required: true, message: '请输入任务名称', trigger: 'blur' },
{ max: 50, message: '任务名称不超过 50 个字符', trigger: 'blur' },
],
model_id: [{ required: true, message: '请选择评测模型', trigger: 'change' }],
gpu_id: [{ required: true, message: '请选择 GPU', trigger: 'change' }],
dataset_id: [
{
validator: (_rule, value, callback) => {
if (form.value.data_source === 'dataset' && (value === '' || value === null || value === undefined)) {
callback(new Error('请选择评测数据集'))
return
}
callback()
},
trigger: 'change',
},
],
}
watch(
() => form.value.data_source,
(source) => {
if (source !== 'dataset') {
form.value.dataset_id = ''
formRef.value?.clearValidate('dataset_id')
}
},
)
async function validate() {
if (!formRef.value) return false
try {
const valid = await formRef.value.validate()
return !!valid
} catch {
return false
}
}
defineExpose({ validate })
</script>
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
class="step-form"
:disabled="disabled"
scroll-to-error
>
<el-form-item label="任务名称" prop="eval_task_name">
<el-input v-model="form.eval_task_name" placeholder="请输入任务名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="评测模型" prop="model_id">
<el-select
v-model="form.model_id"
placeholder="请选择已训练且已合并的模型"
filterable
style="width: 100%"
:loading="loading"
>
<el-option
v-for="model in trainedModels"
:key="model.id"
:label="model.name"
:value="model.id"
:disabled="!model.merged"
/>
</el-select>
<div class="field-tip">未完成合并的模型不可用于评测</div>
</el-form-item>
<el-form-item label="选择 GPU" prop="gpu_id">
<el-select v-model="form.gpu_id" placeholder="请选择 GPU" style="width: 100%" :loading="loading">
<el-option
v-for="(gpu, index) in gpus"
:key="index"
:label="`${gpu.name} (GPU ${index})`"
:value="index"
/>
</el-select>
</el-form-item>
<el-form-item label="数据来源">
<el-radio-group v-model="form.data_source">
<el-radio value="dataset">评测数据集</el-radio>
<el-radio value="inference">推理结果集</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.data_source === 'dataset'" label="评测数据集" prop="dataset_id">
<el-select
v-model="form.dataset_id"
placeholder="请选择评测数据集"
filterable
style="width: 100%"
:loading="loading"
>
<el-option v-for="dataset in evalDatasets" :key="dataset.id" :label="dataset.name" :value="dataset.id" />
</el-select>
</el-form-item>
<el-alert
v-else
title="将使用该模型最近一次可用的推理结果集。"
type="info"
:closable="false"
show-icon
class="source-alert"
/>
<el-form-item label="加入排行榜">
<el-switch v-model="form.leaderboard" />
</el-form-item>
</el-form>
</template>
<style scoped>
.step-form {
max-width: 760px;
}
.field-tip {
color: #909399;
font-size: 12px;
line-height: 20px;
}
.source-alert {
margin: -4px 0 20px 120px;
max-width: 640px;
}
</style>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { DatasetItem, GpuInfo, ModelItem, TrainedModel } from '@/types'
import type { BasicMetricSetupDraft } from './BasicMetricSetupStep.vue'
import type { EvalRuleSetupDraft } from './EvalRuleSetupStep.vue'
import type { EvalTaskSetupDraft } from './EvalTaskSetupStep.vue'
const props = defineProps<{
task: EvalTaskSetupDraft
llmMetric: EvalRuleSetupDraft
basicMetrics: BasicMetricSetupDraft
trainedModels: TrainedModel[]
evalDatasets: DatasetItem[]
evalModels: ModelItem[]
gpus: GpuInfo[]
}>()
function nameOf<T extends { id: string | number; name?: string }>(items: T[], id: string | number) {
return items.find((item) => item.id === id)?.name || String(id || '-')
}
</script>
<template>
<div class="start-eval-step">
<el-descriptions :column="2" border>
<el-descriptions-item label="Task">{{ props.task.eval_task_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="Model">{{ nameOf(props.trainedModels, props.task.model_id) }}</el-descriptions-item>
<el-descriptions-item label="GPU">GPU {{ props.task.gpu_id || 0 }}</el-descriptions-item>
<el-descriptions-item label="Dataset">
{{ props.task.data_source === 'dataset' ? nameOf(props.evalDatasets, props.task.dataset_id) : 'Inference results' }}
</el-descriptions-item>
<el-descriptions-item label="Eval model">{{ nameOf(props.evalModels, props.llmMetric.eval_model || '') }}</el-descriptions-item>
<el-descriptions-item label="Eval method">{{ props.llmMetric.eval_method || '-' }}</el-descriptions-item>
<el-descriptions-item label="Basic metrics" :span="2">
<el-space wrap>
<el-tag v-if="props.basicMetrics.bleu_enabled">BLEU-{{ props.basicMetrics.bleu_n }}</el-tag>
<el-tag v-if="props.basicMetrics.rouge_enabled">{{ props.basicMetrics.rouge_methods.join(', ') }}</el-tag>
<el-tag v-if="props.basicMetrics.cosine_enabled">Cosine</el-tag>
<el-tag effect="plain">Precision {{ props.basicMetrics.output_precision }}</el-tag>
</el-space>
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<style scoped>
.start-eval-step {
max-width: 920px;
}
</style>