目录
[一、 架构思考:为什么我们需要一个"中间商"?](#一、 架构思考:为什么我们需要一个“中间商”?)
[二、 核心实现:给 AI 立规矩,教它说"DSL"](#二、 核心实现:给 AI 立规矩,教它说“DSL”)
[三、 渲染引擎:把 JSON 变现为 DevUI 组件](#三、 渲染引擎:把 JSON 变现为 DevUI 组件)
[四、 价值闭环:不仅能看,还能"带走"](#四、 价值闭环:不仅能看,还能“带走”)
[五、 场景演示:MateChat 的"双面"能力](#五、 场景演示:MateChat 的“双面”能力)
[六、 总结与展望](#六、 总结与展望)
前言
大家在做后台系统开发时,有没有遇到过这样的场景?运营同事跑过来说:"我想要个简单的库存报表,这就这几个字段,能不能马上弄好?"
这时候,你看着手头堆积如山的需求,心里可能在想:要是能直接跟电脑说一句"给我个库存表",界面就能自己长出来该多好啊!
这就是 NL2UI (Natural Language to User Interface) 的终极梦想------用自然语言直接生成界面。但说实话,让 AI 直接写 Vue 代码稍微有点"吓人",代码质量不可控不说,改起来还费劲。
今天,我们换个思路。我们不追求一步到位的"全自动",而是基于 华为云 DevUI MateChat 组件,打造一个"受限但绝对可靠"的 UI 生成引擎。
我们会用一套自己定义的 JSON DSL(领域特定语言) 作为中间层,让 AI 做"填空题",而不是"作文题"。这样既利用了 AI 的理解能力,又保证了生成的界面是 100% 可用的。
为了方便大家验证,我把这个引擎的完整代码都开源了。大家可以去 GitCode 仓库 https://gitcode.com/kaminono/MateChatNL2UIEngine 看看源码,或者直接点这个 https://mate-chat-nl-2-ui-engine-components.vercel.app/ 在线体验一下"说话变界面"。
一、 架构思考:为什么我们需要一个"中间商"?
在动手写代码之前,我们得先定个基调。要实现 NL2UI,我们面临两个选择:是让 AI 直接吐出 Vue 代码,还是让它生成一个 JSON 数据?
我们坚定地选择了后者。直接生成代码就像是"开盲盒",你永远不知道 AI 会不会引入什么奇怪的依赖或者写出有安全漏洞的逻辑。
而生成 JSON DSL 就稳妥多了。我们把 DevUI 的组件------比如 d-card、d-form、d-chart------看作是乐高积木。我们只允许 AI 挑选这些积木来搭建页面。
我们可以把整个流程看作一个流水线:
- Input : 用户在 MateChat 输入自然语言。
- Reasoning : LLM 基于 System Prompt 进行意图识别,转化为标准 JSON。
- Parser : 前端引擎拦截消息,正则清洗数据,校验 JSON 合法性。
- Render : 递归组件读取 JSON,动态映射为 DevUI 组件。
这种"控制反转"的设计,是我们保证系统高可靠性的基石。
二、 核心实现:给 AI 立规矩,教它说"DSL"
搞定了架构,我们来看看核心代码是怎么实现的。这个引擎的"大脑"在 useNlParser.ts 文件里。
我们需要利用 Prompt Engineering(提示词工程),把我们的 DSL 语法"喂"给大模型。我们得明确告诉它:你只能用白名单里的组件,输出格式必须是标准的 JSON。
看看这段真实的代码,我们在 System Prompt 里做了非常严格的约束:
// playground/src/nl2ui-engine/composables/useNlParser.tsconst systemPrompt = ``
`你是一个专业的前端 UI 构建专家。你的任务是将用户的自然语言需求转换为特定的 UI DSL (JSON 格式)。`
`### 🔴 严禁使用不存在的组件!只能使用以下白名单:`
`1. 布局: "d-row", "d-col"`
`2. 容器: "d-card" (必须包含 children), "d-form" (children 必须是 d-form-item)`
`3. 表单项: "d-form-item" (props: label), "d-input", "d-select", "d-button"`
`4. 图表: "simple-stat", "simple-chart"`
`### 输出格式规范 (JSON)`
`必须严格遵守以下 JSON 结构,不要包含 markdown 代码块标记:`
`{`
` "page": { "title": "页面标题", "layout": "grid" | "default" },`
` "components": [`
` {`
` "component": "d-card",`
` "props": { "title": "卡片标题" },`
` "children": [ ... ]`
` }`
` ]`
`}`
``;`
`
通过这种方式,无论用户怎么描述需求,AI 最终吐出来的都是我们能看懂、能渲染的标准数据。这就像是给 AI 戴上了"紧箍咒",让它的创造力在规则的轨道上运行。
在实际开发中,LLM 经常会在返回的 JSON 外面包裹 Markdown 标记(如 ```json ... ```)。如果不处理,JSON.parse 必挂。 我们在解析层做了一层"清洗":
// 解析逻辑片段`
`const parseResponse = (content: string) => {`
` // 1. 利用正则提取最外层的 {} 内容,去除废话和 markdown 符号`
` const jsonMatch = content.match(/\{[\s\S]*\}/);`
` if (!jsonMatch) return null;`
` try {`
` return JSON.parse(jsonMatch[0]);`
` } catch (e) {`
` console.error("JSON 解析失败,AI 生成了非法格式", e);`
` // 这里甚至可以触发一个重试机制`
` return null;`
` }`
`}`
`
三、 渲染引擎:把 JSON 变现为 DevUI 组件
拿到了 JSON 数据,下一步就是把它变成真实的界面。我们在 DslRenderer.vue 里实现了一个递归渲染器。
这个组件的设计非常巧妙,它利用了 Vue 的 h() 函数和 defineAsyncComponent。我们建立了一个组件注册表,按需加载 DevUI 的组件。
这里有个关键点:对于 AI 可能产生的"幻觉"(比如生成了不存在的组件),我们做了兜底处理。
// playground/src/nl2ui-engine/components/DslRenderer.vue// 1. 建立组件白名单映射const componentRegistry: Record<string, any> = {`
` 'd-card': defineAsyncComponent(() => import('vue-devui/card')),`
` 'd-form': defineAsyncComponent(() => import('vue-devui/form')),`
` 'd-input': defineAsyncComponent(() => import('vue-devui/input')),`
` // ... 其他组件`
`};`
`// 2. 核心渲染函数const renderNode = (node: any): any => {`
` // 兜底策略:如果 AI 生成了纯文本,直接渲染文本if (typeof node === 'string') return String(node);`
` let Component = componentRegistry[node.component];`
` // 错误处理:遇到未知组件,渲染一个红框提示,而不是让页面崩溃if (!Component) {`
` return h('div', { style: 'border: 1px dashed red;' }, `[未知: ${node.component}]`);`
` }`
` // 递归渲染子节点const children = node.children?.map(renderNode);`
` return h(Component, node.props, { default: () => children });`
`};`
`
这段代码保证了渲染器的健壮性。哪怕 AI 偶尔"发疯",我们的页面也不会白屏,开发者一眼就能看出是哪里出了问题。
递归渲染树 (The Recursive Magic)是引擎最精妙的地方。因为 UI 结构是树形的(Card 里有 Row,Row 里有 Col,Col 里有 Button),我们的渲染函数必须是递归的。
const renderNode = (node: any): any => {`
` if (typeof node === 'string') return node;`
` const Component = componentRegistry[node.component];`
` if (!Component) return h('div', { style: 'color:red' }, `[未知组件: ${node.component}]`);`
` // 核心:处理 props 和 children`
` // 1. 透传 AI 生成的属性 (如 label, placeholder)`
` const props = { ...node.props };`
` // 2. 递归构建子节点`
` const children = node.children `
` ? { default: () => node.children.map(renderNode) } // 插槽形式传递子节点`
` : null;`
` return h(Component, props, children);`
`};`
`
这段代码仅用十几行,就实现了理论上无限嵌套的 UI 构建能力。
四、 价值闭环:不仅能看,还能"带走"
如果只能在预览里看,那这个工具充其量只是个玩具。为了让它真正产生价值,我们必须实现"从对话到源码"的闭环。
试想一下,你让 AI 生成了一个复杂的表单,觉得效果不错。这时候,你肯定不想照着预览图再去手写一遍代码吧?
所以,我们开发了 useCodeGenerator.ts。它能把当前的 JSON DSL 逆向编译成标准的 Vue SFC(单文件组件)代码。
// playground/src/nl2ui-engine/composables/useCodeGenerator.tsconst generateVueCode = (dsl: UiDsl) => {`
` // 1. 逆向生成 Templateconst templateBody = dsl.components`
` .map(node => generateTemplateNode(node, 2))`
` .join('\n');`
` // 2. 智能分析依赖,生成 Scriptconst imports = analyzeImports(dsl.components);`
` // 3. 拼接成完整的 Vue 文件字符串return `<template>`
` <div class="generated-page">`
`${templateBody}`
` </div>`
`</template>`
`<script setup>`
`import { ${imports.join(', ')} } from 'vue-devui';`
`</script>`;`
`};`
`
在我们的 Demo 右侧,专门做了一个"查看源码"的 Tab。点击它,你就能复制这段生成的代码,直接粘贴到你的项目里。这才是真正的提效 。
五、 场景演示:MateChat 的"双面"能力
最后,我们看看这套系统在实际场景中的表现。我们设计了一个"左指令、右预览"的布局。
左边是大家熟悉的 MateChat 聊天窗口,它作为交互的入口。用户在这里输入自然语言,比如"帮我生成一个销售看板,要看总收入和活跃用户"。
MateChat 会显示"正在构建组件树...",几秒钟后,右边的预览区就会实时渲染出一个包含数据卡片和图表的 Dashboard。
如果你输入"创建一个用户注册表单,包含用户名和密码",右侧瞬间就会变成一个带有校验规则的 DevUI 表单。
这种"即问即答、即答即现"的体验,彻底改变了我们构建 UI 的方式。

六、 总结与展望
通过这个项目,我们验证了一个核心观点:受限的 DSL 反而是 AI 落地的最佳路径 。
我们没有追求让 AI 直接写出完美的代码,而是利用 MateChat 做交互,利用 DSL 做约束,利用 DevUI 做渲染。这套组合拳打下来,既保证了系统的稳定性,又发挥了 AI 的灵活性。
未来,这套架构还有很大的想象空间。比如,我们可以把 DSL 喂给后端,直接生成数据库模型;或者结合语音识别,实现"动动嘴做软件"的科幻场景。
希望这个开源项目能给大家带来一点启发,也欢迎大家来 GitCode 提 PR,我们一起把这个 NL2UI 引擎打磨得更强大!
附官方链接: