AI 探索技术手记

AI模型参数控制面板:多模型通用的前端参数系统设计

Michael Meng· 2026年7月23日· ◷ 5 分钟阅读
AI模型参数控制面板:多模型通用的前端参数系统设计

概述

在 AI 应用中,不同模型(LLM 大语言模型、文生图 Stable Diffusion、文生视频 SVD)的参数体系差异巨大。本文将构建一套 通用参数面板架构:以 TypeScript 配置表驱动动态渲染,统一处理温度、TopP、采样步数、CFG、帧率等参数,同时涵盖参数联动、互斥校验、非法输入拦截和预设模板持久化。

1. 参数体系:三类模型的差异化字段

LLM(大语言模型)

temperature: 0~2(默认 0.7)
top_p: 0~1(默认 0.9)
max_tokens: 1~32768
frequency_penalty: -2~2
presence_penalty: -2~2
stop_sequences: string[]
stream: boolean

文生图(SD / SDXL)

sampler: 'euler_a' | 'ddim' | 'dpm++_2m' | 'lcm'
steps: 1~150(默认 25)
cfg_scale: 1~30(默认 7)
seed: number(-1 = 随机)
width / height: 512~2048
high_res_fix: boolean

文生视频(SVD)

frames: 14~30
fps: 8~30
motion_bucket_id: 0~255
interpolation_mode: 'none' | 'frame_blend' | 'optical_flow'
duration: 自动计算 = frames / fps

2. 架构设计:配置表驱动渲染

核心理念:一份统一配置表,切换模型类型时动态加载对应参数组

// ===== 参数配置表声明 =====
interface ParamConfig {
  key: string;
  label: string;
  type: 'slider' | 'select' | 'toggle' | 'number' | 'tag-input';
  defaultValue: number | string | boolean | string[];
  min?: number;
  max?: number;
  step?: number;
  options?: { label: string; value: string | number }[];
  // 互斥规则
  disabledWhen?: (params: Record<string, any>) => boolean;
  // 联动计算
  computedBy?: (params: Record<string, any>) => number;
}

interface ModelPreset {
  id: string;
  name: string;
  type: 'llm' | 'txt2img' | 'txt2video';
  params: ParamConfig[];
}

// ===== 以 SD 为例的配置声明 =====
const SD_PRESET: ModelPreset = {
  id: 'sd-xl',
  name: 'SD XL',
  type: 'txt2img',
  params: [
    {
      key: 'sampler',
      label: '采样器',
      type: 'select',
      defaultValue: 'dpm++_2m',
      options: [
        { label: 'Euler A', value: 'euler_a' },
        { label: 'DDIM', value: 'ddim' },
        { label: 'DPM++ 2M', value: 'dpm++_2m' },
        { label: 'LCM', value: 'lcm' },
      ],
    },
    {
      key: 'steps',
      label: '采样步数',
      type: 'slider',
      defaultValue: 25,
      min: 1,
      max: 150,
      step: 1,
    },
    {
      key: 'cfg_scale',
      label: 'CFG (提示词强度)',
      type: 'slider',
      defaultValue: 7,
      min: 1,
      max: 30,
      step: 0.5,
    },
    {
      key: 'seed',
      label: '随机种子',
      type: 'number',
      defaultValue: -1,
      min: -1,
      max: 4294967295,
    },
    {
      key: 'high_res_fix',
      label: '高清修复',
      type: 'toggle',
      defaultValue: false,
      // 部分采样器不支持高清修复
      disabledWhen: (p) => ['ddim', 'lcm'].includes(p.sampler as string),
    },
  ],
};

3. 核心组件:参数面板渲染器

// ===== ParameterPanel.tsx =====
import React, { useEffect, useState, useCallback } from 'react';

interface Props {
  preset: ModelPreset;
  onChange: (params: Record<string, any>) => void;
  // 外部可控状态(用于预设加载)
  externalValues?: Record<string, any>;
}

export default function ParameterPanel({ preset, onChange, externalValues }: Props) {
  const [params, setParams] = useState<Record<string, any>>({});

  // 初始化默认值
  useEffect(() => {
    const defaults: Record<string, any> = {};
    preset.params.forEach((p) => {
      defaults[p.key] = p.defaultValue;
    });
    setParams(externalValues ?? defaults);
  }, [preset.id, externalValues]);

  // 通知外部变化
  useEffect(() => {
    onChange(params);
  }, [params, onChange]);

  // 安全更新单个参数
  const updateParam = useCallback((key: string, rawValue: any) => {
    const cfg = preset.params.find((p) => p.key === key);
    if (!cfg) return;

    let value = rawValue;

    // ---- 非法输入处理 ----
    if (cfg.type === 'number' || cfg.type === 'slider') {
      const num = Number(rawValue);
      if (isNaN(num)) {
        // 非数字:恢复默认值
        value = cfg.defaultValue;
      } else {
        // 边界吸附
        if (cfg.min !== undefined && num < cfg.min) value = cfg.min;
        else if (cfg.max !== undefined && num > cfg.max) value = cfg.max;
        else value = num;
      }
    }

    setParams((prev) => {
      const next = { ...prev, [key]: value };

      // ---- 参数联动 ----
      preset.params.forEach((p) => {
        if (p.computedBy) {
          next[p.key] = p.computedBy(next);
        }
      });
      return next;
    });
  }, [preset]);

  // 渲染单个控件
  const renderControl = (cfg: ParamConfig) => {
    const value = params[cfg.key];
    const disabled = cfg.disabledWhen?.(params) ?? false;

    switch (cfg.type) {
      case 'slider':
        return (
          <div key={cfg.key} className="param-row">
            <label>{cfg.label}: {value}</label>
            <input
              type="range"
              min={cfg.min}
              max={cfg.max}
              step={cfg.step}
              value={value}
              disabled={disabled}
              onChange={(e) => updateParam(cfg.key, e.target.value)}
            />
            {disabled && <span className="disabled-hint">该采样器不支持高清修复</span>}
          </div>
        );

      case 'select':
        return (
          <div key={cfg.key} className="param-row">
            <label>{cfg.label}</label>
            <select
              value={value as string}
              disabled={disabled}
              onChange={(e) => updateParam(cfg.key, e.target.value)}
            >
              {cfg.options?.map((opt) => (
                <option key={opt.value} value={opt.value}>{opt.label}</option>
              ))}
            </select>
          </div>
        );

      case 'toggle':
        return (
          <div key={cfg.key} className="param-row">
            <label>
              <input
                type="checkbox"
                checked={!!value}
                disabled={disabled}
                onChange={(e) => updateParam(cfg.key, e.target.checked)}
              />{' '}
              {cfg.label}
            </label>
            {disabled && <span className="disabled-hint">当前采样器不可用</span>}
          </div>
        );

      case 'number':
        return (
          <div key={cfg.key} className="param-row">
            <label>{cfg.label}</label>
            <input
              type="number"
              min={cfg.min}
              max={cfg.max}
              step={cfg.step}
              value={value}
              disabled={disabled}
              onChange={(e) => updateParam(cfg.key, e.target.value)}
              onBlur={(e) => {
                const num = Number(e.target.value);
                if (isNaN(num) || (cfg.min !== undefined && num < cfg.min)) {
                  updateParam(cfg.key, cfg.min ?? cfg.defaultValue);
                }
                if (cfg.max !== undefined && num > cfg.max) {
                  updateParam(cfg.key, cfg.max);
                }
              }}
            />
          </div>
        );

      case 'tag-input':
        return (
          <div key={cfg.key} className="param-row">
            <label>{cfg.label}</label>
            <div className="tag-list">
              {(value as string[]).map((tag, i) => (
                <span key={i} className="tag">
                  {tag}
                  <button onClick={() => {
                    const next = (value as string[]).filter((_, j) => j !== i);
                    updateParam(cfg.key, next);
                  }}>×</button>
                </span>
              ))}
              <input
                placeholder="输入停止词,回车添加"
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && e.currentTarget.value.trim()) {
                    const next = [...(value as string[]), e.currentTarget.value.trim()];
                    updateParam(cfg.key, next);
                    e.currentTarget.value = '';
                  }
                }}
              />
            </div>
          </div>
        );

      default:
        return null;
    }
  };

  return (
    <div className="parameter-panel">
      <h3>{preset.name} 参数</h3>
      {preset.params.map(renderControl)}
    </div>
  );
}

4. 预设模板与 localStorage 持久化

// ===== preset-manager.ts =====

interface SavedTemplate {
  id: string;
  name: string;
  modelType: string;
  params: Record<string, any>;
}

const STORAGE_KEY = 'ai-param-templates';

export function loadTemplates(): SavedTemplate[] {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : [];
  } catch {
    return [];
  }
}

export function saveTemplate(template: SavedTemplate): void {
  const list = loadTemplates();
  const idx = list.findIndex((t) => t.id === template.id);
  if (idx >= 0) {
    list[idx] = template;
  } else {
    list.push(template);
  }
  localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
}

export function deleteTemplate(id: string): void {
  const list = loadTemplates().filter((t) => t.id !== id);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
}

export function importTemplateFromJSON(json: string): SavedTemplate | null {
  try {
    const obj = JSON.parse(json);
    if (!obj.name || !obj.params) return null;
    return { id: Date.now().toString(), name: obj.name, modelType: obj.type ?? 'llm', params: obj.params };
  } catch {
    return null;
  }
}

export function exportTemplateToJSON(template: SavedTemplate): string {
  return JSON.stringify({ name: template.name, type: template.modelType, params: template.params }, null, 2);
}

5. 视频模型参数联动:帧率 × 帧数 = 时长

// SVD 模型的联动计算示例
const SVD_PRESET: ModelPreset = {
  id: 'svd',
  name: 'SVD (文生视频)',
  type: 'txt2video',
  params: [
    {
      key: 'frames',
      label: '帧数',
      type: 'slider',
      defaultValue: 25,
      min: 14,
      max: 30,
      step: 1,
    },
    {
      key: 'fps',
      label: '帧率',
      type: 'slider',
      defaultValue: 8,
      min: 4,
      max: 30,
      step: 1,
    },
    {
      key: 'duration',
      label: '视频时长 (秒)',
      type: 'number',
      defaultValue: 3.1,
      // 只读显示,由 frames / fps 自动计算
      computedBy: (p) => {
        const frames = Number(p.frames) || 25;
        const fps = Number(p.fps) || 8;
        return Math.round((frames / fps) * 10) / 10;
      },
    },
  ],
};

6. 最终提交:序列化参数

// 面板层负责把参数打包成 API 要求的格式
function buildSubmitPayload(preset: ModelPreset, params: Record<string, any>) {
  return {
    model_type: preset.type,
    model_id: preset.id,
    params: { ...params },
    // 自动移除 -1 的随机 seed
    seed: params.seed === -1 ? Math.floor(Math.random() * 4294967295) : params.seed,
  };
}

总结

  • 配置表驱动:一份 ParamConfig 声明搞定所有模型,切换模型类型自动刷出对应控件
  • 校验三件套:非法输入→回填默认值、超出边界→自动吸附、互斥字段→动态 disabled
  • 联动计算computedBy 在每次 setParams 时自动触发,帧率变化自动换算时长
  • 预设持久化:localStorage 存模板,支持新建/加载/删除/导入导出 JSON
  • 面试亮点:能清晰对比 LLM / SD / SVD 三类模型的参数差异化处理

Comments 留言讨论

还没有评论,来抢个沙发,聊聊你的看法~

Michael.Meng

michaelnews@126.com
用 AI 记录,用文字沉淀

© 2026 Michael Meng · 保留所有权利 · Powered by FastAPI + Nuxt