1. 从一句需求到可运行应用这套组合方案到底在解决什么问题业务侧提需求的速度永远比研发排期的速度快。这是我做了这么多年项目最深的体会。一个内部审批工具、一张数据登记表、一个简单的库存看板业务同事描述起来就是一句话的事但落到研发这边从建表、写接口、画页面、配权限到部署上线没有两三天根本下不来。更麻烦的是这类需求往往零散、多变、生命周期还短投入产出比极低研发不愿意接业务又等不起。我最近折腾的一套方案核心思路就是用开源大模型负责理解自然语言需求用低代码引擎负责把理解结果渲染成真实可用的业务应用中间用一层结构化的中间表示我习惯叫它应用描述协议把两者串起来。用户输入帮我做一个员工请假登记表包含姓名、部门、请假类型、起止时间、审批状态系统解析后直接生成一张带表单、列表、增删改查和基础权限的页面点一下就能跑起来。这套东西适合谁如果你是小团队的技术负责人手上有一堆琐碎的内部工具需求压着如果你是独立开发者想快速给客户交付一个能演示的原型或者你本身就是低代码平台的维护者想给平台加一个自然语言入口那这套思路都值得参考。它不追求生成复杂的业务系统目标很明确把一句话需求到能用的页面之间的时间从几天压缩到几分钟。需要提前说清楚的是大模型在这里不是万能的。它擅长的是把模糊的自然语言翻译成结构化的配置而不是凭空写出可靠的业务逻辑。所以整套方案的关键不在于模型多强而在于中间那层协议设计得够不够稳以及低代码引擎的渲染能力够不够全。这两点决定了生成结果的可控性和可用性。下面我会把源码拆开一层一层讲清楚每个环节为什么这么设计以及我在实测中踩过的那些坑。2. 整体架构拆解三层结构如何各司其职2.1 为什么是模型协议引擎而不是端到端生成很多人第一反应是让大模型直接吐出一段前端代码复制粘贴就能用。我一开始也这么试过结果非常不稳定。同一个需求问两次生成的代码结构可能完全不同变量命名五花八门样式全靠内联稍微复杂一点的表单就会出现字段对不上、事件绑错的问题。更致命的是生成的代码没法校验出了错你只能一行行读完全失去了低代码配置化的意义。所以我最终选择了三层解耦的结构。第一层是语义理解层用开源大模型把自然语言转成一份 JSON 格式的应用描述第二层是协议校验层用 JSON Schema 对这份描述做严格校验字段类型、必填项、枚举值全部卡死第三层是渲染执行层低代码引擎读取校验通过的描述动态生成页面和接口。这三层之间只通过那份 JSON 协议通信任何一层出问题都能单独定位、单独替换。这个设计最大的好处是可控。模型再飘只要它输出的 JSON 过不了 Schema 校验就直接打回重试绝不会把错误带到渲染层。而且协议是稳定的今天用这个模型明天换个更强的模型只要它还能输出符合协议的 JSON整个系统就不用改。这一点在模型快速迭代的当下特别重要。2.2 应用描述协议长什么样协议是整个系统的骨架我把它设计成三个核心部分meta、schema、ui。meta描述应用的基本信息比如名称、版本、描述schema描述数据模型也就是有哪些字段、什么类型、是否必填ui描述界面布局包括用表单还是表格、字段的排列顺序、哪些操作按钮要显示。{ meta: { name: 员工请假登记, version: 1.0.0 }, schema: { fields: [ { key: name, label: 姓名, type: string, required: true }, { key: dept, label: 部门, type: string, required: true }, { key: leaveType, label: 请假类型, type: enum, options: [事假, 病假, 年假], required: true }, { key: startTime, label: 开始时间, type: datetime, required: true }, { key: endTime, label: 结束时间, type: datetime, required: true }, { key: status, label: 审批状态, type: enum, options: [待审批, 已通过, 已驳回], default: 待审批 } ] }, ui: { layout: form-with-table, actions: [create, update, delete, query] } }这份协议看起来简单但每一个字段的设计都有讲究。比如type我只允许有限的几种string、number、boolean、datetime、enum。为什么不做更细的类型因为类型越多模型出错的概率越大渲染层要处理的边界情况也越多。enum类型强制要求提供options这样模型就不能含糊其辞必须把可选项列清楚。default字段是可选的但一旦提供渲染层就会用它初始化表单。提示协议设计的第一原则是约束模型而不是信任模型。凡是能让 Schema 卡住的地方绝不留给人眼检查。2.3 三层之间的数据流与错误处理数据流很清晰用户输入 - 模型生成 JSON - Schema 校验 - 渲染引擎执行。但真正跑起来错误处理才是重头戏。我在每一层都设了明确的失败出口。模型层如果生成的 JSON 解析失败或者校验不通过系统会自动把错误信息拼回提示词让模型重新生成最多重试三次。三次都失败就返回一个友好的错误提示告诉用户需求描述可能不够清晰请补充字段信息。协议层如果发现字段类型冲突比如同一个 key 定义了两次会直接拒绝并给出具体位置。渲染层如果遇到不支持的组件类型会降级成纯文本展示而不是整个页面崩溃。这套错误处理机制是我踩了无数坑之后才补上的。早期版本没有重试机制模型偶尔抽风输出一段带注释的 JSON整个流程就断了用户体验极差。加上重试和降级之后成功率从大概七成提升到了九成五以上。3. 语义理解层怎么让模型稳定吐出结构化配置3.1 提示词工程的核心给模型画好格子让模型输出 JSON 不难难的是让它每次都输出结构一致、字段完整、类型正确的 JSON。我试过直接说请输出 JSON 格式结果模型有时候用 markdown 代码块包起来有时候加一段解释文字有时候字段名用中文稳定性惨不忍睹。后来我改用了一套画格子的提示词策略。核心做法是在系统提示里明确给出协议的完整 Schema 定义并且提供一个完整的示例输入输出对。提示词的结构大致是这样的先说明角色和任务再给出协议定义然后给一个 few-shot 示例最后强调输出必须是纯 JSON不能有任何额外文字。SYSTEM_PROMPT 你是一个业务应用配置生成器。 你的任务是把用户的自然语言需求转换成符合以下 Schema 的 JSON。 Schema 定义 {json_schema} 示例 用户输入做一个客户信息登记表包含姓名、电话、公司 输出{example_output} 要求 1. 只输出 JSON不要有任何解释文字 2. 不要用 markdown 代码块包裹 3. 字段 key 必须用英文小驼峰 4. 无法确定的字段类型一律用 string 这里有几个细节值得展开。第一示例必须完整且正确模型会模仿示例的格式示例里字段顺序、命名风格都会被学去。第二明确禁止 markdown 代码块因为很多模型默认喜欢加加了之后解析就得多一步清洗。第三给出兜底规则比如无法确定类型用 string这样模型遇到模糊需求时不会卡住或者瞎猜。3.2 用 JSON Schema 做硬校验而不是靠模型自觉提示词再完善也不能保证模型百分百听话。所以我在模型输出之后加了一道硬校验。用 Python 的jsonschema库把协议定义写成标准 Schema模型输出先json.loads再validate任何一步失败都触发重试。import json from jsonschema import validate, ValidationError APP_SCHEMA { type: object, required: [meta, schema, ui], properties: { meta: { type: object, required: [name, version], properties: { name: {type: string}, version: {type: string} } }, schema: { type: object, required: [fields], properties: { fields: { type: array, minItems: 1, items: { type: object, required: [key, label, type], properties: { key: {type: string, pattern: ^[a-z][a-zA-Z0-9]*$}, label: {type: string}, type: {enum: [string, number, boolean, datetime, enum]}, required: {type: boolean}, options: {type: array, items: {type: string}}, default: {} } } } } }, ui: { type: object, required: [layout], properties: { layout: {enum: [form, table, form-with-table]}, actions: {type: array, items: {type: string}} } } } } def parse_model_output(raw_text): try: data json.loads(raw_text) except json.JSONDecodeError as e: return None, fJSON 解析失败: {e} try: validate(instancedata, schemaAPP_SCHEMA) except ValidationError as e: return None, fSchema 校验失败: {e.message} return data, None注意key字段我加了正则约束^[a-z][a-zA-Z0-9]*$强制小驼峰。这个约束非常关键因为 key 会直接变成数据库字段名和前端变量名如果模型输出中文或者带特殊字符后面全乱套。加上正则之后模型输出的 key 就规范多了。3.3 重试策略与提示词回填校验失败之后不能直接放弃要把错误信息回填给模型让它重试。我的做法是把上一次的输出和具体的错误信息一起拼进新一轮的提示词里让模型知道错在哪。def generate_app_config(user_input, max_retries3): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input} ] for attempt in range(max_retries): raw call_llm(messages) data, error parse_model_output(raw) if data: return data messages.append({role: assistant, content: raw}) messages.append({ role: user, content: f上次输出有误{error}。请修正后重新输出纯 JSON。 }) raise RuntimeError(多次重试仍无法生成合法配置)实测下来第一次成功率大概在 75% 左右加上重试之后整体成功率能到 96% 以上。绝大多数失败都是 JSON 格式问题比如多了个逗号、少了引号回填错误信息后模型基本都能自己改对。真正需要三次以上重试的通常是需求本身太模糊比如做个管理系统这种模型不知道该生成什么字段这时候重试也没用得引导用户补充信息。注意重试次数不要设太多三次足够。设多了不仅浪费 token还会让用户等太久。三次失败就该提示用户细化需求而不是死磕模型。4. 渲染执行层把 JSON 变成能点的页面4.1 动态表单渲染的实现思路渲染层的核心任务是把schema.fields数组变成真实的表单控件。我用的是前端框架的动态组件能力根据字段的type映射到不同的组件。string映射到输入框number映射到数字输入框boolean映射到开关datetime映射到日期时间选择器enum映射到下拉选择。const componentMap { string: Input, number: InputNumber, boolean: Switch, datetime: DateTimePicker, enum: Select }; function renderField(field) { const Comp componentMap[field.type] || Input; const props { label: field.label, required: field.required || false }; if (field.type enum) { props.options field.options.map(o ({ label: o, value: o })); } if (field.default ! undefined) { props.defaultValue field.default; } return { component: Comp, props, key: field.key }; }这段代码看起来简单但有几个坑我踩过。第一enum类型的options必须做空值保护因为模型偶尔会忘记输出options直接.map会报错。第二default值要做类型转换比如datetime类型的默认值如果是字符串得转成日期对象否则组件不认。第三required字段在表单校验时要用上不能只显示个星号就完事。4.2 列表页与增删改查的自动生成光有表单不够业务应用通常还需要一个列表页来展示数据以及增删改查的操作。这部分我是根据ui.actions数组来动态生成的。如果actions里包含create就渲染一个新建按钮包含delete就在每行数据后面加删除操作包含query就在列表上方加搜索框。列表的列定义直接复用schema.fields但会做一些取舍。比如datetime类型在列表里只显示日期部分不显示时分秒避免列太宽。enum类型在列表里用标签tag展示比纯文本更直观。这些细节都是根据实际使用体验调出来的不是拍脑袋定的。function buildTableColumns(fields) { return fields.map(f { const col { title: f.label, dataIndex: f.key, key: f.key }; if (f.type datetime) { col.render (val) val ? val.split( )[0] : -; } if (f.type enum) { col.render (val) Tag${val}/Tag; } return col; }); }这里有个经验列表列不要全部展示。如果字段超过八个列表会横向滚动体验很差。我的做法是只展示前六个字段剩下的放到详情页里看。这个规则我写死在渲染逻辑里不依赖模型判断因为模型对多少个字段算多没有概念。4.3 数据持久化从配置到真实存储页面渲染出来只是第一步数据得存下来才算完整。我的做法是在应用创建时根据schema.fields自动生成一张数据表。字段类型映射关系是string和enum映射到文本类型number映射到数值类型boolean映射到布尔类型datetime映射到时间类型。CREATE TABLE app_leave_register ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, dept VARCHAR(255) NOT NULL, leave_type VARCHAR(64) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status VARCHAR(64) DEFAULT 待审批, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );建表语句是动态拼出来的表名用meta.name的拼音或英文转写加随机后缀避免冲突。字段名直接用key因为前面 Schema 已经强制小驼峰了这里可以直接用。required为 true 的字段加NOT NULL约束default值映射到DEFAULT子句。提示动态建表一定要做表名和字段名的白名单校验只允许字母、数字和下划线防止注入风险。虽然模型输出的 key 已经被正则约束过但多一道校验多一层安心。5. 实测中暴露的问题与我的应对方案5.1 模型对模糊需求的处理能力有限我拿十几个不同类型的需求测过发现模型对明确的需求处理得很好比如包含姓名、电话、地址的客户表字段列得清清楚楚生成结果基本一次过。但遇到模糊需求就露馅了比如做个项目管理工具模型会自己脑补一堆字段什么项目名称、负责人、开始时间、结束时间、进度、状态、备注看起来挺全但很多字段用户根本不需要反而增加了干扰。我的应对方案是在提示词里加了一条规则如果用户需求中字段不明确只生成最核心的两到三个字段并在 meta 里加一个needClarify标记。渲染层看到这个标记会在页面顶部显示一个提示条引导用户补充字段。这样既不会生成一堆无用字段也不会因为信息不足而卡住。5.2 字段类型推断的准确率问题模型推断字段类型时经常出错。最典型的是电话字段模型有时候推断成number有时候推断成string。推断成number就麻烦了电话号码前面的 0 会被丢掉带区号的号码里的横杠也没法存。类似的还有金额推断成string就没法做数值计算和排序。我的解决办法是在提示词里加了一份类型推断规则表明确告诉模型哪些关键词对应哪种类型。比如包含电话、手机、编号、身份证的一律用string包含金额、数量、年龄、分数的一律用number包含时间、日期的一律用datetime。这份规则表是硬编码在提示词里的不依赖模型自己判断。加上之后类型推断的准确率从大概七成提升到了九成以上。关键词推荐类型原因电话、手机、编号、身份证string避免前导零丢失支持特殊字符金额、数量、年龄、分数number需要数值计算和排序时间、日期datetime需要日期选择和格式化状态、类型、级别enum取值有限适合下拉选择是否、启用boolean二值判断适合开关5.3 生成结果的过度设计倾向模型有个毛病喜欢过度设计。你让它做个简单的登记表它给你加上搜索、筛选、导出、批量删除、分页恨不得把能想到的功能全塞进去。结果就是页面很复杂用户真正需要的核心功能反而被淹没了。我一开始想通过提示词限制说只生成必要功能但效果不好模型对必要的理解和我不一样。后来我换了个思路在协议里把actions设计成白名单枚举只允许create、update、delete、query四个值模型只能从这四个里选。默认情况下我只给create和query其他两个需要用户在需求里明确提到才加。这样就把要不要加功能的决策权从模型手里拿回来了。5.4 完整排查链路一次生成失败的复盘有一次测试用户输入做一个图书借阅登记包含书名、借阅人、借出日期、归还日期系统连续三次生成失败。我把每一轮的输出都打出来看发现第一轮模型输出的 JSON 里datetime字段的default值写成了2024-01-01但 Schema 里default没有类型约束校验通过了。问题出在渲染层日期组件拿到字符串默认值直接报错整个页面白屏。排查过程是这样的先看浏览器控制台发现是日期组件的defaultValue类型错误再回头看生成的 JSON确认default是字符串然后检查 Schema发现default字段确实没做类型约束。根因是协议设计有漏洞default的类型应该和字段的type保持一致但 Schema 里没体现这个约束。修复方案有两个层面。短期方案是在渲染层加类型转换字符串日期自动转成日期对象。长期方案是修改 Schema用if-then条件校验当type为datetime时default必须是符合日期格式的字符串。我两个都做了渲染层的转换作为兜底Schema 的约束作为第一道防线。修复之后同类问题再没出现过。这个案例给我的教训是协议设计要考虑字段之间的关联约束不能每个字段独立定义。default和type的关系、enum和options的关系都属于这类关联约束必须在 Schema 里显式表达。6. 源码结构与我个人的一些实践建议6.1 项目目录组织整个项目我分成四个模块llm负责模型调用和提示词管理protocol负责协议定义和校验renderer负责前端渲染server负责接口和持久化。每个模块独立目录之间通过明确的接口通信不互相依赖内部实现。project/ ├── llm/ │ ├── prompt.py # 提示词模板 │ ├── client.py # 模型调用封装 │ └── parser.py # 输出解析与重试 ├── protocol/ │ ├── schema.py # JSON Schema 定义 │ └── validator.py # 校验逻辑 ├── renderer/ │ ├── form.js # 表单渲染 │ ├── table.js # 列表渲染 │ └── actions.js # 操作按钮 └── server/ ├── app.py # 主入口 ├── db.py # 数据库操作 └── api.py # 接口路由这样分的好处是替换成本低。比如你想换个模型只改llm/client.py就行想换个前端框架只改renderer目录。模块边界清晰改一处不会牵动全身。6.2 提示词版本管理提示词是要反复调的今天加一条规则明天改一个示例改来改去很容易乱。我的做法是把提示词当成代码来管理每次修改都记录版本号和修改原因存在单独的配置文件里。这样出问题可以快速回滚也能对比不同版本的效果。PROMPT_VERSIONS { v1.0: 基础版本包含 Schema 定义和单示例, v1.1: 增加类型推断规则表, v1.2: 增加模糊需求处理规则引入 needClarify 标记, v1.3: 优化示例增加 enum 类型完整示例 }实测下来v1.2 和 v1.3 的提升最明显。v1.2 解决了模糊需求乱生成字段的问题v1.3 解决了 enum 类型经常缺 options 的问题。每次改动我都拿同一批测试用例跑一遍确认没有回退才合并。6.3 给想上手的朋友几条实在建议第一先把协议定死再动手写代码。我见过太多人一上来就调模型调了半天发现输出格式不稳定回头再改协议前面的工作全白费。协议是地基地基不稳上面盖什么都是危房。第二测试用例要覆盖边界情况。别只测包含姓名电话的客户表这种标准需求要测模糊需求、超长需求、带特殊字符的需求、字段特别多的需求。我维护了一个三十多条的测试集每次改提示词或协议都跑一遍能提前发现大部分问题。第三不要追求一次生成完美结果。这套方案的目标是生成一个能用的初版剩下的微调交给用户在低代码界面上手动改。想一步到位生成完全符合预期的应用现阶段不现实也没必要。把生成当成起点而不是终点心态会好很多。第四模型选择上开源模型完全够用。我用过好几个不同规模的开源模型参数量在 7B 到 14B 之间的配合好的提示词和校验效果已经很不错了。没必要非得上最大的模型成本和速度都不划算。关键是提示词和协议要打磨到位模型只是执行者。第五日志要打全。每次生成的输入、输出、校验结果、重试次数都记下来出问题的时候这些日志就是救命稻草。我靠日志定位过好几次偶发的生成失败没有日志根本无从下手。这套方案我前后迭代了大概两个月从最初的一团乱麻到现在基本稳定中间踩的坑比预想的多得多。但跑通之后那种一句话变出一个应用的感觉确实很爽。如果你也在做类似的事情希望这些经验能帮你少走点弯路。