diff --git a/frontend/scripts/regression-data-process-wizard.mjs b/frontend/scripts/regression-data-process-wizard.mjs index 5593ea0..2c4c1b7 100644 --- a/frontend/scripts/regression-data-process-wizard.mjs +++ b/frontend/scripts/regression-data-process-wizard.mjs @@ -41,15 +41,20 @@ assert.match(viewSource, /onBeforeRouteLeave\(async \(\) =>/, '路由离开确 assert.doesNotMatch(viewSource, /window\.confirm|ElMessageBox/, '创建页仍在使用系统或 Element Plus 确认框') assert.match(viewSource, /const WIZARD_STEPS = \[/, '向导步骤尚未改为固定常量') -for (const title of ['创建任务', '上传文件', '数据预览', '开始生成', '结果编辑与保存']) { +for (const title of ['创建任务', '大模型选择', '上传文件', '数据预览', '开始生成', '结果编辑与保存']) { assert.ok(viewSource.includes(`title: '${title}'`), `缺少固定步骤:${title}`) } assert.match( viewSource, - /\{ id: 'create',[\s\S]*?\{ id: 'upload',[\s\S]*?\{ id: 'preview',[\s\S]*?\{ id: 'generate',[\s\S]*?\{ id: 'results'/, - '五步向导顺序必须为创建任务、上传文件、数据预览、开始生成、结果编辑与保存', + /\{ id: 'create',[\s\S]*?\{ id: 'model',[\s\S]*?\{ id: 'upload',[\s\S]*?\{ id: 'preview',[\s\S]*?\{ id: 'generate',[\s\S]*?\{ id: 'results'/, + '六步向导顺序必须为创建任务、大模型选择、上传文件、数据预览、开始生成、结果编辑与保存', ) assert.doesNotMatch(viewSource, /steps\s*=\s*computed|all\.filter/, '步骤仍根据处理类型动态增减') +assert.match( + viewStyleSource, + /@media \(max-width: 1100px\)[\s\S]*?\.step-title\s*\{[\s\S]*?display:\s*none[\s\S]*?\.step-item\.is-active \.step-title\s*\{[\s\S]*?display:\s*block/, + '六步向导在中等宽度下没有收起非当前步骤标题', +) assert.match(draftSource, /localStorage\.setItem\(DATA_PROCESS_DRAFT_STORAGE_KEY/, '草稿没有持久化') assert.match(draftSource, /localStorage\.getItem\(DATA_PROCESS_DRAFT_STORAGE_KEY\)/, '草稿没有恢复读取') assert.match(viewSource, /restoreDraft\(\)/, '页面没有恢复草稿') @@ -57,6 +62,7 @@ assert.ok(viewSource.split('\n').length < 800, 'DataProcessCreateView 拆分后 const expectedComponents = [ 'TaskSetupStep.vue', + 'ModelSelectionStep.vue', 'SourceUploadStep.vue', 'PreviewCompareStep.vue', 'GenerationStep.vue', @@ -82,7 +88,7 @@ for (const field of ['sourceStart', 'sourceEnd', 'originalContent', 'editedConte assert.ok(typesSource.includes(field), `PreviewItem 缺少字段:${field}`) } assert.match(typesSource, /sourceFileId/, 'PreviewItem 缺少来源文件标识') -assert.match(typesSource, /export type StepId = 'create' \| 'upload' \| 'preview' \| 'generate' \| 'results'/, '步骤类型缺少独立上传步骤') +assert.match(typesSource, /export type StepId = 'create' \| 'model' \| 'upload' \| 'preview' \| 'generate' \| 'results'/, '步骤类型缺少独立大模型选择步骤') assert.match(modelSource, /export function buildPreviewItems/, '缺少切片来源映射生成函数') assert.match(modelSource, /export function sourceLines/, '缺少源文件行偏移生成函数') assert.match(modelSource, /sourceFileId/, '切片生成没有写入来源文件标识') @@ -102,10 +108,10 @@ for (const marker of [ 'preview-editor', 'scrollIntoView', ]) { - assert.ok(previewSource.includes(marker), `第三步缺少结构或行为:${marker}`) + assert.ok(previewSource.includes(marker), `第四步缺少结构或行为:${marker}`) } -assert.match(previewSource, /sourceStart/, '第三步未使用来源起始偏移') -assert.match(previewSource, /sourceEnd/, '第三步未使用来源结束偏移') +assert.match(previewSource, /sourceStart/, '第四步未使用来源起始偏移') +assert.match(previewSource, /sourceEnd/, '第四步未使用来源结束偏移') assert.match(previewSource, /filterable/, '文件选择器必须可搜索') assert.match(previewSource, /当前文件/, '预览缺少当前文件切换器') assert.doesNotMatch(previewSource, /located-badge|sync-label|已定位到/, '源文件栏不应显示冗余定位提示') @@ -130,13 +136,14 @@ assert.match(previewSource, />保存修改<\/el-button>/, '编辑器缺少保存 assert.match(previewSource, /\.editor-actions\s*\{[\s\S]*?justify-content:\s*flex-end/, '取消和保存按钮必须在编辑器右侧对齐') assert.doesNotMatch(previewSource, /item-token|item-status|modifiedOnly|仅看已修改/, '切片列表不应再显示 Token 或修改状态') assert.match(previewSource, /\.preview-editor\s*\{[\s\S]*?flex:\s*1 1 auto[\s\S]*?overflow-y:\s*auto/, '编辑模式必须占据右侧剩余区域并可滚动') -assert.match(previewSource, /@media \(max-width: 900px\)/, '第三步缺少窄屏上下布局') +assert.match(previewSource, /@media \(max-width: 900px\)/, '第四步缺少窄屏上下布局') const taskSetupPath = path.join(createDir, 'TaskSetupStep.vue') const structuredOptionsPath = path.join(createDir, 'StructuredOptionsPanel.vue') const unstructuredOptionsPath = path.join(createDir, 'UnstructuredOptionsPanel.vue') const datasetSplitEditorPath = path.join(createDir, 'DatasetSplitEditor.vue') const generationOptionsPath = path.join(createDir, 'GenerationOptionsPanel.vue') +const modelSelectionPath = path.join(createDir, 'ModelSelectionStep.vue') const sourceUploadPath = path.join(createDir, 'SourceUploadStep.vue') const [ taskSetupSource, @@ -144,6 +151,7 @@ const [ unstructuredOptionsSource, datasetSplitEditorSource, generationControlSource, + modelSelectionSource, sourceUploadSource, ] = await Promise.all([ readFile(taskSetupPath, 'utf8'), @@ -151,6 +159,7 @@ const [ readFile(unstructuredOptionsPath, 'utf8'), readFile(datasetSplitEditorPath, 'utf8'), readFile(generationOptionsPath, 'utf8'), + readFile(modelSelectionPath, 'utf8'), readFile(sourceUploadPath, 'utf8'), ]) const taskSetupFeatureSource = [ @@ -172,20 +181,29 @@ assert.match(unstructuredOptionsSource, /= 0 && nextFromUploadStart > nextFromCreateStart, '缺少创建步骤与上传步骤的独立跳转函数') -const nextFromCreateSource = viewSource.slice(nextFromCreateStart, nextFromUploadStart) +assert.ok( + nextFromCreateStart >= 0 && nextFromModelStart > nextFromCreateStart && nextFromUploadStart > nextFromModelStart, + '缺少创建、大模型选择与上传步骤的独立跳转函数', +) +const nextFromCreateSource = viewSource.slice(nextFromCreateStart, nextFromModelStart) +const nextFromModelSource = viewSource.slice(nextFromModelStart, nextFromUploadStart) const nextFromUploadSource = viewSource.slice(nextFromUploadStart, selectPreviewFileStart) assert.match(nextFromCreateSource, /taskSetupRef\.value\?\.validate\(\)/, '创建步骤继续前没有校验任务配置') -assert.match(nextFromCreateSource, /goToStep\('upload'\)/, '创建步骤校验通过后没有进入上传文件') +assert.match(nextFromCreateSource, /goToStep\('model'\)/, '创建步骤校验通过后没有进入大模型选择') assert.doesNotMatch(nextFromCreateSource, /uploadedFiles|buildPreviewItems/, '创建步骤仍在校验文件或提前生成预览') +assert.match(nextFromModelSource, /modelSelectionRef\.value\?\.validate\(\)/, '大模型选择步骤继续前没有校验模型配置') +assert.match(nextFromModelSource, /goToStep\('upload'\)/, '大模型选择完成后没有进入上传文件') assert.match(nextFromUploadSource, /uploadedFiles\.value\.length === 0/, '上传步骤继续前没有校验源数据') assert.match(nextFromUploadSource, /buildPreviewItems\(/, '上传步骤没有在进入预览前生成预览数据') assert.match(nextFromUploadSource, /goToStep\('preview'\)/, '上传步骤完成后没有进入数据预览') @@ -217,8 +235,6 @@ for (const option of [ assert.ok(structuredOptionsSource.includes(option), `结构化预处理缺少选项:${option}`) } assert.ok(structuredOptionsSource.includes('生成选项'), '结构化配置缺少生成选项分类') -assert.ok(structuredOptionsSource.includes('语义丰富表达'), '生成选项缺少语义丰富表达开关') -assert.ok(structuredOptionsSource.includes('使用大模型将问答表述得更自然、柔和'), '语义丰富表达缺少辅助说明') for (const splitName of ['训练集', '验证集', '测试集']) { assert.ok(datasetSplitEditorSource.includes(splitName), `生成选项缺少数据集划分:${splitName}`) } @@ -232,7 +248,6 @@ for (const splitField of ['train', 'validation', 'test']) { `数据集划分字段 ${splitField} 缺少 0~100 的整数限制`, ) } -assert.match(structuredOptionsSource, //, '父页面缺少结构化配置状态') assert.match(stateSource, /datasetSplit:\s*\{ train: 80, validation: 10, test: 10 \}/, '数据集划分默认值必须为 80/10/10') @@ -252,12 +267,16 @@ for (const field of [ assert.ok(typesSource.includes(field), `生成控制配置缺少字段:${field}`) assert.ok(implementationSource.includes(field), `父页面默认值或草稿状态缺少字段:${field}`) } -assert.match(structuredOptionsSource, /GenerationOptionsPanel/, '结构化生成选项没有复用统一的大模型与质量筛选组件') -assert.match(unstructuredOptionsSource, /GenerationOptionsPanel/, '非结构化生成选项没有复用统一的大模型与质量筛选组件') +assert.match(structuredOptionsSource, /GenerationOptionsPanel/, '结构化生成选项没有复用统一的质量筛选组件') +assert.match(unstructuredOptionsSource, /GenerationOptionsPanel/, '非结构化生成选项没有复用统一的质量筛选组件') assert.match(structuredOptionsSource, /:options="options"/, '结构化生成选项未接入统一配置组件') assert.match(unstructuredOptionsSource, /:options="options"/, '非结构化生成选项未接入统一配置组件') -assert.match(taskSetupFeatureSource, /

大模型<\/h3>/, '大模型必须作为与生成选项平级的独立分类') -assert.match(taskSetupFeatureSource, /section="model"/, '独立大模型分类没有挂载模型配置') +assert.doesNotMatch(taskSetupFeatureSource, /

大模型<\/h3>|section="model"/, '第一步不应继续承载大模型配置') +assert.match(modelSelectionSource, /]*>大模型选择<\/h3>/, '独立步骤缺少大模型选择标题') +assert.match(modelSelectionSource, /section="model"/, '独立步骤没有挂载模型配置') +assert.match(modelSelectionSource, /defineExpose\(\{ validate \}\)/, '独立大模型选择步骤没有暴露继续前校验') +assert.match(modelSelectionSource, /class="form-section"/, '大模型选择步骤没有沿用第一步的通栏表单分区') +assert.doesNotMatch(modelSelectionSource, /max-width:\s*980px/, '大模型选择步骤不应使用比第一步更窄的固定内容宽度') assert.match(taskSetupFeatureSource, /section="quality"/, '质量筛选没有保留在生成选项分类中') assert.doesNotMatch(generationControlSource, /

大模型<\/h4>/, '大模型不应继续作为生成选项内部子分类') for (const label of ['大模型', '数据生成模型', '默认提示语', '质量筛选', '过滤低质量内容', '过滤过短内容', '最少字数']) { @@ -267,15 +286,21 @@ assert.match(generationControlSource, /filterable/, '数据生成模型下拉必 assert.match(generationControlSource, /maxlength="500"/, '默认提示语缺少合理的长度限制') assert.match(generationControlSource, /\.model-field\s*\{[\s\S]*?display:\s*flex[\s\S]*?flex-direction:\s*column/, '大模型字段没有使用稳定的纵向表单布局') assert.match(generationControlSource, /\.generation-config-group\s*\{[\s\S]*?border:\s*1px solid #e2e5ec/, '大模型配置没有保留统一配置面板边框') +assert.match(generationControlSource, /\.model-config-group\s*\{[\s\S]*?padding:\s*0[\s\S]*?border:\s*0/, '独立大模型步骤仍存在嵌套卡片挤压') +assert.match( + generationControlSource, + /\.model-config-group \.advanced-settings-grid\s*\{[\s\S]*?grid-template-columns:\s*1fr/, + '大模型高级参数没有改为与第一步一致的纵向布局', +) assert.match(stateSource, /const DEFAULT_GENERATION_PROMPT\s*=\s*['"][^'"]{40,}['"]/, '大模型配置缺少可直接使用的默认提示语') assert.equal((stateSource.match(/generationPrompt:\s*DEFAULT_GENERATION_PROMPT/g) || []).length, 2, '结构化与非结构化任务必须共用默认提示语') assert.match(generationControlSource, /v-if="options\.qualityFilterEnabled"/, '质量规则没有随总开关渐进显示') assert.match(generationControlSource, /v-if="options\.filterShortContent"/, '最少字数没有随短内容规则显示') assert.match(generationControlSource, /:min="1"[\s\S]*:max="1000"/, '最少字数缺少 1 到 1000 的边界限制') -assert.match(taskSetupSource, /generationValidationMessage/, '生成模型与质量规则缺少继续前校验') +assert.match(taskSetupSource, /qualityValidationMessage/, '质量规则缺少继续前校验') assert.match(viewSource, /useModelsStore/, '创建页没有加载模型列表') assert.match(viewSource, /model\.type === 'LLM'/, '数据生成模型列表没有排除非大模型') -assert.match(viewSource, /:generation-models="generationModels"/, '创建页没有向生成选项传递模型列表') +assert.match(viewSource, / props\.options\.chunkMethod,[\s\S]*?method === 'custom'[\s\S]*?advancedChunkSettingsOpen\.value = true/, '选择自定义分隔符时没有自动展开高级设置') -assert.match(unstructuredOptionsSource, /function revealValidation\(\)[\s\S]*?advancedChunkSettingsOpen\.value = true/, '非结构化面板没有暴露高级校验定位能力') +assert.doesNotMatch(unstructuredOptionsSource, /advancedChunkSettingsOpen|>高级设置]*label=["']描述["']/, '共享维度字段必须支持按场景隐藏描述输入框', ) +assert.match( + dimensionFieldsSource, + /score_min[\s\S]*?score_max[\s\S]*?pass_threshold[\s\S]*?Math\.min[\s\S]*?Math\.max/, + '指标型评分区间变化时必须把通过阈值约束在合法范围内', +) +assert.match( + dimensionFieldsSource, + /prop=["']score_min["'][\s\S]*?:max=["'][^"']*score_max[^"']*["'][\s\S]*?prop=["']score_max["'][\s\S]*?:min=["'][^"']*score_min[^"']*["']/, + '评分最小值和最大值输入必须具备交叉边界约束', +) assert.match(createSource, /currentStep\s*=\s*ref\(0\)/, '评测创建向导缺少当前步骤状态') assert.match(createSource, /class=["']custom-wizard-steps["']/, '评测向导未复用数据处理的自定义步骤条结构') assert.match(createSource, /class=["']step-connector["']/, '评测向导步骤条缺少连接线') @@ -102,8 +128,18 @@ assert.match( ) assert.match( createSource, - /\s*开始评测\s* get('/model-eval') @@ -11,7 +11,7 @@ export const getEvalDetail = (id: string | number) => get(`/mode export const deleteEval = (id: string | number) => del(`/model-eval/${id}`) /** 启动评测 */ -export const startEval = (data: any) => post('/model-eval/start', data) +export const startEval = (data: StartEvalPayload) => post('/model-eval/start', data) /** 评测维度列表 */ export const getDimensionList = () => get('/dimension') diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index e19f269..af5fa6b 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -36,7 +36,7 @@ const menuGroups = [ { title: '模型服务', items: [ - { key: 'fine-tune', label: '模型微调', icon: 'fa-cogs', to: '/fine-tune' }, + { key: 'fine-tune', label: '模型训练', icon: 'fa-cogs', to: '/fine-tune' }, { key: 'model-eval', label: '模型评测', icon: 'fa-line-chart', to: '/model-eval' }, { key: 'model-inference', label: '模型推理', icon: 'fa-server', to: '/model-inference' }, { key: 'model-manage', label: '模型管理', icon: 'fa-cube', to: '/model-manage' }, @@ -45,8 +45,8 @@ const menuGroups = [ { title: '数据治理', items: [ - { key: 'data-process', label: '数据处理', icon: 'fa-filter', to: '/data-process' }, { key: 'dataset', label: '数据集管理', icon: 'fa-file-text', to: '/dataset' }, + { key: 'data-process', label: '数据处理', icon: 'fa-filter', to: '/data-process' }, ], }, { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 15f1cbb..7afd835 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -25,7 +25,7 @@ const routes: RouteRecordRaw[] = [ path: 'fine-tune', name: 'fine-tune', component: () => import('@/views/fine-tune/FineTuneListView.vue'), - meta: { title: '模型微调', pageSurface: 'self' }, + meta: { title: '模型训练', pageSurface: 'self' }, }, { path: 'fine-tune/create', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index a4174df..f3dd501 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -224,6 +224,34 @@ export interface EvalTask { create_time?: string } +/** 启动评测时提交的可选基础指标配置。 */ +export interface BasicEvalMetricsConfig { + bleu: { + enabled: boolean + ngram: number + } + rouge: { + enabled: boolean + methods: string[] + } + cosine: { + enabled: boolean + } + output_precision: number +} + +export interface StartEvalPayload { + eval_task_name: string + eval_type: EvalType + model_id: string | number + gpu_id: string | number + dataset_id: string | number + dimension_id: string | number + data_source: 'dataset' | 'inference' + leaderboard: boolean + basic_metrics: BasicEvalMetricsConfig +} + /** 单个测试样本的评测结果 */ export interface EvalSampleResult { id: number | string diff --git a/frontend/src/views/data-process/DataProcessCreateView.vue b/frontend/src/views/data-process/DataProcessCreateView.vue index a8eb7c9..4aea84e 100644 --- a/frontend/src/views/data-process/DataProcessCreateView.vue +++ b/frontend/src/views/data-process/DataProcessCreateView.vue @@ -5,6 +5,7 @@ import { ElMessage, type UploadFile } from 'element-plus' import { storeToRefs } from 'pinia' import AppConfirmDialog from '@/components/AppConfirmDialog.vue' import TaskSetupStep from './create/TaskSetupStep.vue' +import ModelSelectionStep from './create/ModelSelectionStep.vue' import SourceUploadStep from './create/SourceUploadStep.vue' import PreviewCompareStep from './create/PreviewCompareStep.vue' import GenerationStep from './create/GenerationStep.vue' @@ -22,6 +23,7 @@ import { useDataProcessGeneration } from './create/useDataProcessGeneration' import { useModelsStore } from '@/stores/models' import type { ExternalDataSource, + GenerationControlOptions, PreviewItem, ProcessType, StepId, @@ -35,22 +37,27 @@ const modelsStore = useModelsStore() const { list: modelList } = storeToRefs(modelsStore) const generationModels = computed(() => modelList.value.filter((model) => model.type === 'LLM')) const taskSetupRef = ref>() +const modelSelectionRef = ref>() const confirmDialogRef = ref>() const PREVIEW_MODEL_VERSION = 'document-chunk-v2' const WIZARD_STEPS = [ { id: 'create', title: '创建任务', desc: '填写任务信息与处理配置' }, + { id: 'model', title: '大模型选择', desc: '选择生成模型并设置输出要求' }, { id: 'upload', title: '上传文件', desc: '上传或接入待处理的源数据' }, { id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' }, { id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' }, - { id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' }, + { id: 'results', title: '编辑与保存', desc: '检查、修改并保存结果' }, ] as const satisfies ReadonlyArray<{ id: StepId; title: string; desc: string }> const currentStep = ref(0) const currentStepId = computed(() => WIZARD_STEPS[currentStep.value]?.id ?? 'create') const task = reactive({ name: '', description: '' }) -const processType = ref('unstructured') +const processType = ref('structured') const structuredOptions = ref(createDefaultStructuredOptions()) const unstructuredOptions = ref(createDefaultUnstructuredOptions()) +const modelSelectionOptions = computed(() => ( + processType.value === 'unstructured' ? unstructuredOptions.value : structuredOptions.value +)) const uploadedFiles = ref([]) const externalSource = reactive({ @@ -119,7 +126,8 @@ const previewFiles = computed(() => uploadedFiles.value.map((file) => { } })) const primaryActionLabel = computed(() => { - if (currentStepId.value === 'create') return '继续:上传文件' + if (currentStepId.value === 'create') return '继续:选择大模型' + if (currentStepId.value === 'model') return '继续:上传文件' if (currentStepId.value === 'upload') return '继续:数据预览' if (currentStepId.value === 'preview') return '确认预览并继续' if (currentStepId.value === 'results') return '保存任务' @@ -144,6 +152,14 @@ function goToStep(stepId: StepId) { if (nextStepIndex >= 0) currentStep.value = nextStepIndex } +function updateModelSelectionOptions(value: GenerationControlOptions) { + if (processType.value === 'unstructured') { + unstructuredOptions.value = { ...unstructuredOptions.value, ...value } + return + } + structuredOptions.value = { ...structuredOptions.value, ...value } +} + const { persistDraft, restoreDraft } = useDataProcessDraft({ currentStepId, task, @@ -393,6 +409,12 @@ function resetSourceDataForProcessTypeChange() { async function nextFromCreate() { const valid = await taskSetupRef.value?.validate() if (!valid) return + goToStep('model') +} + +async function nextFromModel() { + const valid = await modelSelectionRef.value?.validate() + if (!valid) return goToStep('upload') } @@ -504,6 +526,10 @@ async function handlePrimaryAction() { await nextFromCreate() return } + if (currentStepId.value === 'model') { + await nextFromModel() + return + } if (currentStepId.value === 'upload') { nextFromUpload() return @@ -596,27 +622,31 @@ onMounted(() => {
-
-
-
-
- - {{ index + 1 }} -
-
-
{{ step.title }}
+
@@ -629,7 +659,14 @@ onMounted(() => { v-model:process-type="processType" v-model:structured-options="structuredOptions" v-model:unstructured-options="unstructuredOptions" - :generation-models="generationModels" + /> + + () -const advancedModelSettingsOpen = ref(false) - const emit = defineEmits<{ 'update:options': [value: GenerationControlOptions] }>() @@ -61,7 +59,7 @@ function sectionValidationMessage() { @update:model-value="updateField('generationModelId', $event)" >
- - -
-
- - - -
- -
-
- 强制 JSON 格式输出 - 要求大模型返回纯 JSON 对象,有助于提高下游结构化提取的稳定性 -
- + + + +
+ +
+
+ 强制 JSON 格式输出 + 要求大模型返回纯 JSON 对象,有助于提高下游结构化提取的稳定性
+