智能表单生成实战:用 LLM 从 JSON Schema 到生产级 UI 渲染
智能表单生成实战:用 LLM 从 JSON Schema 到生产级 UI 渲染
一、引言痛点:表单开发的效率瓶颈
表单开发是前端业务中最常见的任务之一,也是效率瓶颈最严重的环节。一个包含 20 个字段的业务表单,传统开发流程需要:编写 HTML 结构、编写 CSS 样式、实现数据绑定、处理表单验证、实现提交逻辑,通常需要 2-3 天。而当业务需求变更时,维护成本同样巨大。
JSON Schema 作为表单结构的标准化描述规范,为表单的声明式定义提供了可能。结合 LLM 的代码生成能力,可以实现从 JSON Schema 到生产级表单代码的自动转换,大幅提升开发效率。
本文将系统讲解基于 JSON Schema 的表单生成架构设计,探讨 LLM 在这一场景的能力边界,并给出生产级的实现方案。
二、系统架构设计
2.1 整体架构
表单生成系统的核心架构包含三个主要模块:
flowchart TD A[JSON Schema 输入] --> B[Schema 解析器] B --> C[字段配置生成] C --> D[UI 组件映射] D --> E[表单代码生成] F[LLM 增强] --> C F --> E F --> G[验证规则生成] H[设计系统规范] --> D I[业务约束配置] --> C J[国际化配置] --> E2.2 Schema 到 UI 的映射规则
不同类型的 JSON Schema 字段对应不同的 UI 组件:
flowchart LR A[Schema Type] --> B[UI Component] A1[string + format: email] --> B1[Input type=email] A2[string + format: uri] --> B2[Input type=url] A3[string + enum] --> B3[Select/Radio] A4[boolean] --> B4[Checkbox/Switch] A5[integer/number] --> B6[Input type=number] A6[array + items] --> B7[ArrayField/Repeatable] A7[string + textarea] --> B5[Textarea]三、生产级代码实现
3.1 JSON Schema 解析器
// schema-parser.ts import Ajv from 'ajv'; interface FieldConfig { name: string; type: string; label: string; placeholder?: string; defaultValue?: unknown; required: boolean; disabled?: boolean; readOnly?: boolean; options?: { label: string; value: unknown }[]; validation?: ValidationRule[]; ui?: UIConfig; } interface UIConfig { component?: 'input' | 'select' | 'radio' | 'checkbox' | 'textarea' | 'datepicker'; colSpan?: number; visible?: boolean; dependsOn?: { field: string; value: unknown }[]; } interface ValidationRule { type: 'required' | 'minLength' | 'maxLength' | 'pattern' | 'custom'; value?: unknown; message?: string; } /** * JSON Schema 到表单配置的转换器 */ class SchemaParser { private ajv: Ajv; constructor() { this.ajv = new Ajv({ allErrors: true }); } /** * 验证 Schema 合法性 */ validateSchema(schema: object): { valid: boolean; errors: string[] } { const validate = this.ajv.compile(schema); if (validate(schema)) { return { valid: true, errors: [] }; } return { valid: false, errors: validate.errors?.map(e => `${e.instancePath}: ${e.message}`) || [], }; } /** * 解析 JSON Schema 为字段配置数组 */ parseToFieldConfigs(schema: object, locale: string = 'zh-CN'): FieldConfig[] { const properties = (schema as any).properties || {}; const requiredFields = new Set((schema as any).required || []); const fields: FieldConfig[] = []; for (const [name, fieldSchema] of Object.entries(properties)) { const config = this.parseField(name, fieldSchema as any, requiredFields.has(name), locale); fields.push(config); } return fields; } private parseField( name: string, fieldSchema: any, required: boolean, locale: string ): FieldConfig { const config: FieldConfig = { name, type: fieldSchema.type || 'string', label: fieldSchema.title || name, required, placeholder: fieldSchema.description, defaultValue: fieldSchema.default, }; // 枚举值处理 if (fieldSchema.enum) { config.options = fieldSchema.enum.map((value: unknown) => ({ label: String(value), value, })); } // 验证规则生成 config.validation = this.generateValidationRules(fieldSchema, required); // UI 配置 config.ui = this.inferUIConfig(fieldSchema); return config; } private generateValidationRules(fieldSchema: any, required: boolean): ValidationRule[] { const rules: ValidationRule[] = []; if (required && fieldSchema.type !== 'boolean') { rules.push({ type: 'required', message: '该字段为必填项', }); } if (fieldSchema.type === 'string') { if (fieldSchema.minLength !== undefined) { rules.push({ type: 'minLength', value: fieldSchema.minLength, message: `最少 ${fieldSchema.minLength} 个字符`, }); } if (fieldSchema.maxLength !== undefined) { rules.push({ type: 'maxLength', value: fieldSchema.maxLength, message: `最多 ${fieldSchema.maxLength} 个字符`, }); } if (fieldSchema.pattern) { rules.push({ type: 'pattern', value: fieldSchema.pattern, message: fieldSchema.formatMessage || '格式不正确', }); } } if (fieldSchema.type === 'number' || fieldSchema.type === 'integer') { if (fieldSchema.minimum !== undefined) { rules.push({ type: 'minLength', // 复用,实际应区分 minNumber value: fieldSchema.minimum, message: `最小值为 ${fieldSchema.minimum}`, }); } if (fieldSchema.maximum !== undefined) { rules.push({ type: 'maxLength', // 复用 value: fieldSchema.maximum, message: `最大值为 ${fieldSchema.maximum}`, }); } } return rules; } private inferUIConfig(fieldSchema: any): UIConfig { const ui: UIConfig = {}; // 根据 format 推断 UI 组件 if (fieldSchema.format === 'email') { ui.component = 'input'; } else if (fieldSchema.format === 'uri') { ui.component = 'input'; } else if (fieldSchema.format === 'date' || fieldSchema.format === 'date-time') { ui.component = 'datepicker'; } else if (fieldSchema.enum?.length <= 3) { ui.component = 'radio'; } else if (fieldSchema.enum) { ui.component = 'select'; } else if (fieldSchema.type === 'boolean') { ui.component = 'checkbox'; } else if (fieldSchema.type === 'array') { ui.component = 'array'; } return ui; } }3.2 LLM 增强的表单生成
// llm-form-generator.ts import OpenAI from 'openai'; interface FormGenerationRequest { schema: object; fields: FieldConfig[]; designSystem: DesignSystemConfig; locale: string; } interface DesignSystemConfig { components: Record<string, ComponentConfig>; theme: Record<string, string>; } /** * LLM 增强的表单生成器 * 功能: * 1. 基于 Schema 生成业务友好的 label 和 placeholder * 2. 生成中文验证消息 * 3. 生成完整的表单组件代码 */ class LLMFormGenerator { private client: OpenAI; constructor(apiKey: string) { this.client = new OpenAI({ apiKey }); } /** * 生成表单组件代码 */ async generateFormCode(request: FormGenerationRequest): Promise<string> { const prompt = this.buildFormCodePrompt(request); const response = await this.client.chat.completions.create({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: prompt }], temperature: 0.3, }); return response.choices[0].message.content; } private buildFormCodePrompt(request: FormGenerationRequest): string { const { fields, designSystem, locale } = request; return ` 你是 React 前端工程师,负责根据表单字段配置生成生产级表单组件代码。 设计系统配置: ${JSON.stringify(designSystem.components, null, 2)} 字段配置: ${JSON.stringify(fields, null, 2)} 代码要求: 1. 使用 React + TypeScript 2. 使用 React Hook Form 进行表单状态管理 3. 使用 Zod 进行 schema 验证 4. 遵循 React 最佳实践: - 使用 forwardRef 处理 ref 传递 - 使用 useCallback 优化回调引用 - 正确处理 disabled 和 readOnly 状态 5. 完整实现: - 表单布局(支持栅格) - 错误消息展示 - 必填标记 - 国际化 label(使用 react-i18next) 6. 添加完整的 JSDoc 注释 请直接生成完整的 TSX 代码文件: `; } /** * 生成国际化翻译配置 */ async generateTranslations(fields: FieldConfig[]): Promise<Record<string, Record<string, string>>> { const prompt = ` 为以下表单字段生成国际化翻译配置,输出 JSON 格式: 字段列表: ${fields.map(f => `- ${f.name}: ${f.label}`).join('\n')} 输出格式: { "zh-CN": { "field_name": "中文label", ... }, "en-US": { "field_name": "English Label", ... } } `; const response = await this.client.chat.completions.create({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: prompt }], response_format: { type: 'json_object' }, }); return JSON.parse(response.choices[0].message.content); } }3.3 生成的表单使用示例
// GeneratedUserForm.tsx import React, { forwardRef } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; import { Button } from '@/components/ui/Button'; /** * 用户注册表单 * 自动生成日期:2024-01-15 * 源 Schema:user-registration.schema.json */ // Zod Schema 验证规则 const userSchema = z.object({ username: z.string() .min(3, '用户名至少3个字符') .max(20, '用户名最多20个字符') .regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'), email: z.string().email('请输入有效的邮箱地址'), password: z.string() .min(8, '密码至少8个字符') .regex(/[A-Z]/, '密码必须包含至少一个大写字母') .regex(/[a-z]/, '密码必须包含至少一个小写字母') .regex(/[0-9]/, '密码必须包含至少一个数字'), confirmPassword: z.string(), role: z.enum(['admin', 'user', 'guest']), agreeTerms: z.boolean().refine(val => val === true, '必须同意服务条款'), }).refine(data => data.password === data.confirmPassword, { message: '两次输入的密码不一致', path: ['confirmPassword'], }); type UserFormData = z.infer<typeof userSchema>; interface UserFormProps { onSubmit: (data: UserFormData) => Promise<void>; defaultValues?: Partial<UserFormData>; disabled?: boolean; } export const UserForm = forwardRef<HTMLFormElement, UserFormProps>( function UserForm({ onSubmit, defaultValues, disabled }, ref) { const { register, handleSubmit, control, formState: { errors, isSubmitting }, } = useForm<UserFormData>({ resolver: zodResolver(userSchema), defaultValues, }); return ( <form ref={ref} onSubmit={handleSubmit(onSubmit)} className="user-form"> <div className="form-row"> <div className="form-field"> <label htmlFor="username" className="form-label"> 用户名 <span className="required">*</span> </label> <Input id="username" {...register('username')} disabled={disabled || isSubmitting} error={errors.username?.message} placeholder="请输入用户名" /> </div> </div> <div className="form-row"> <div className="form-field"> <label htmlFor="email" className="form-label"> 邮箱 <span className="required">*</span> </label> <Input id="email" type="email" {...register('email')} disabled={disabled || isSubmitting} error={errors.email?.message} placeholder="example@domain.com" /> </div> </div> <div className="form-row form-row--two"> <div className="form-field"> <label htmlFor="password" className="form-label"> 密码 <span className="required">*</span> </label> <Input id="password" type="password" {...register('password')} disabled={disabled || isSubmitting} error={errors.password?.message} autoComplete="new-password" /> </div> <div className="form-field"> <label htmlFor="confirmPassword" className="form-label"> 确认密码 <span className="required">*</span> </label> <Input id="confirmPassword" type="password" {...register('confirmPassword')} disabled={disabled || isSubmitting} error={errors.confirmPassword?.message} autoComplete="new-password" /> </div> </div> <div className="form-row"> <div className="form-field"> <label htmlFor="role" className="form-label"> 用户角色 <span className="required">*</span> </label> <Controller name="role" control={control} render={({ field }) => ( <Select {...field} disabled={disabled || isSubmitting} error={errors.role?.message} options={[ { label: '普通用户', value: 'user' }, { label: '管理员', value: 'admin' }, { label: '访客', value: 'guest' }, ]} /> )} /> </div> </div> <div className="form-row"> <div className="form-field form-field--checkbox"> <Controller name="agreeTerms" control={control} render={({ field }) => ( <label className="checkbox-label"> <input type="checkbox" checked={field.value} onChange={field.onChange} disabled={disabled || isSubmitting} /> <span>我已阅读并同意 <a href="/terms">服务条款</a></span> {errors.agreeTerms && ( <span className="error-message">{errors.agreeTerms.message}</span> )} </label> )} /> </div> </div> <div className="form-actions"> <Button type="submit" variant="primary" loading={isSubmitting} disabled={disabled} > 注册 </Button> </div> </form> ); } );四、Trade-offs 分析
4.1 自动生成与人工设计的边界
LLM 生成的表单代码在简单场景下效率极高,但对于复杂业务逻辑(如动态字段联动、级联选择、条件显隐)仍需要人工补充。最佳实践是:将 LLM 生成作为"脚手架",人工审核后补充特殊逻辑。
4.2 Schema 标准化的成本
JSON Schema 规范本身存在一定的学习曲线。在团队中推广 Schema 驱动的表单开发,需要配套的文档、工具链和培训投入。对于小型项目,这可能得不偿失;但对于中大型项目,长期收益显著。
五、总结
JSON Schema 驱动的表单生成结合 LLM 的代码生成能力,为表单开发提供了一条高效的生产路径。核心要点:
- Schema 先行:建立团队统一的 JSON Schema 规范和字段命名约定
- 分层生成:基础字段用 Schema 解析器,复杂验证逻辑用 LLM 增强
- 人工审核:生成的代码必须经过开发者审核,不可完全自动化
- 持续迭代:根据业务反馈不断优化 Schema 模板和 Prompt 模板
表单开发效率的提升,最终目的是让开发者将更多精力投入到业务逻辑和用户体验的打磨中。
