让 AI 替你搭页面:如何用多阶段 Pipeline 实现「自然语言到可编辑页面」的全链路

前言

前段时间,因为AI Coding的浪潮,我用AI从0到1做了个低代码搭建平台OrangeHome。感兴趣的可以查看这篇文章: juejin.cn/post/762287…

平台体验地址:

传统的低代码搭建平台都是用户通过拖拽组件、配置属性来创建网页——这是行业里非常成熟的模式。

这里有一个持续存在的痛点:对于非设计背景的用户,「从零开始搭建一个页面」的心智门槛依然很高。 即使组件面板里有几十个现成组件,面对空白画布,很多用户还是不知道从哪里开始。

在ai时代,这个问题需要被解决:能不能让用户用一句话描述想要的页面,AI 直接生成一个完整的初始版本,然后用户在这个基础上微调?

这看起来是一个「AI 生成 UI」的问题,但深入之后发现,它涉及的工程挑战远比想象中复杂。

核心挑战:不只是「让 AI 吐 JSON」

乍看之下,让 LLM 生成页面 JSON 似乎很简单——给它一个 Schema 结构定义,让它填充内容就好了。

但实际要面对的问题远不止于此:

1. Schema 必须可渲染。 编辑器用 Schema(JSON 树)驱动页面渲染。Schema 中每个节点的 type 字段必须对应一个真实注册的组件。如果 AI 「幻觉」出一个不存在的组件类型,画布直接白屏。

2. 生成结果必须可编辑。 用户拿到 AI 生成的页面后,应该能像手动搭建的页面一样自由编辑——选中组件、修改属性、拖拽排序、撤销重做。这意味着 AI 输出必须和人工操作产生的数据格式完全一致。

3. 体验不能是「等 30 秒然后一次性出结果」。 用户期待看到渐进式的生成过程,而不是一个漫长的 loading。

4. LLM 输出不可靠。 复杂嵌套 JSON 的生成质量参差不齐,必须有完善的容错机制。

这四个挑战最终塑造了我的架构设计。

整体架构

我的方案由三层组成:

┌─────────────────────────────┐
│       前端编辑器 (React)      │  ← Schema 驱动渲染 + AI 面板
│   画布 │ 物料面板 │ AI 搭建面板  │
└────────────┬────────────────┘
             │ REST + SSE
┌────────────▼────────────────┐
│       BFF 编排层 (Node.js)    │  ← 组装上下文、透传流式事件
└────────────┬────────────────┘
             │
┌────────────▼────────────────┐
│       AI Agent 服务           │  ← 多阶段 Pipeline + LLM 调用
│  [Intake → Plan → Layout     ││   → Content → Validate]      │
└─────────────────────────────┘

这不是一个简单的「前端调 LLM 接口」的方案。AI 生成被建模为一个 有状态的、多阶段的后端 Pipeline,通过 SSE 与前端实时通信。

下面我逐层展开。


第一层:Schema 驱动的编辑器——AI 生成的「着陆点」

在谈 AI 之前,必须先理解Oranghome低代码搭建的编辑器是如何工作的,因为 AI 的输出目标就是编辑器的核心数据结构。

一切皆 Schema

编辑器以一棵 JSON 树(我们称之为 Schema)作为页面的唯一数据源:

{
  "id": "root-1",
  "type": "root-container",
  "children": [
    {
      "id": "hero-1",
      "type": "container",
      "style": { "padding": "40px", "background": "#f5f5f5" },
      "children": [
        {
          "id": "title-1",
          "type": "text",
          "props": { "content": "欢迎来到我们的产品" },
          "style": { "fontSize": "32px", "fontWeight": "bold" }
        },
        {
          "id": "btn-1",
          "type": "button",
          "props": { "text": "立即体验", "type": "primary" }
        }
      ]
    }
  ]
}

这棵树包含了页面的全部信息:组件层级、类型、属性、样式。画布根据这棵树递归渲染,属性面板读写这棵树的节点,图层面板展示这棵树的结构。

一个关键的设计决策是:无论用户拖拽创建组件、手动编辑属性,还是 AI 生成页面,最终都是在操作同一棵 Schema 树。

这意味着 AI 只需要输出一棵合法的 Schema,写入 Store,编辑器自然就能渲染它、编辑它、撤销它——不需要任何特殊处理。

远程组件加载

Schema 中的 type 字段对应的不是硬编码在编辑器里的组件,而是从 CDN 动态加载的远程物料。编辑器维护一个 type → Bundle URL 的映射表,渲染时按需加载:

Schema 节点 (type: "button")
    → 查询映射表 → AMD/ESM Bundle URL
    → 动态加载 → 获得 React 组件
    → <RemoteComponent {...props} style={style}>{children}</RemoteComponent>

这个设计让物料和编辑器完全解耦——新增物料组件不需要修改编辑器代码。同时也意味着 AI 生成的 Schema 中的 type 值必须在这个映射表中存在,否则就是一个无法渲染的节点。

状态管理:单一数据流

我用 Zustand 管理状态,核心是一个 Schema Store:

任何来源的变更 (拖拽/属性编辑/AI生成/导入)
    │
    ▼
schemaStore.setSchema(newSchema, options)
    │
    ├── 记录历史快照 (支持 undo/redo)
    ├── 标记文档脏状态
    └── 同步选中节点 (清理已不存在的选中)
    │
    ▼
所有 UI 自动响应更新
    ├── 画布重新渲染
    ├── 图层面板更新
    └── 属性面板更新

setSchema 接收一个 options 参数来区分变更来源:来自持久化加载的不触发脏标记,来自用户操作的记录历史。AI 生成的 Schema 以 user 身份写入,这样用户可以 撤销 AI 的整次生成——体验上和手动编辑完全一致。


第二层:多阶段 AI Pipeline——真正的难点

「让 LLM 一次性生成完整页面 Schema」是最直觉的方案,但很快放弃了这个思路。

为什么不能一步到位?

我实验过让 LLM 一次性从用户描述生成完整 Schema,遇到了几个问题:

  1. 输出质量不稳定。 当 JSON 结构很深(3-4 层嵌套,十几个节点)时,模型容易生成格式错误的 JSON,或者在结构上出现逻辑混乱。
  2. 单次 prompt 负担过重。 需求理解、组件选择、布局设计、内容撰写——这些本质上是不同的认知任务,塞在一个 prompt 里效果很差。
  3. 无法提供中间反馈。 用户要等整个生成完成才能看到结果,体验很差。
  4. 难以定位问题。 生成结果不理想时,不知道是需求理解出了偏差,还是布局不合理,还是内容不对。

所以我最终选择了 多阶段 Pipeline 的方案。

五阶段流水线

用户输入: "帮我做一个产品介绍页,要有 Banner、功能特性和联系方式"
    
      ┌──────────────────────────────────────┐
    ├─▶│ Stage 1: 需求理解 (Intake)             
       输出: 结构化的产品需求文档 (PRD)          
      └──────────────────────────────────────┘
    
      ┌──────────────────────────────────────┐
    ├─▶│ Stage 2: 组件规划 (Component Plan)     
       输入: PRD + 可用组件列表                 
       输出: 选中的组件 + 缺口警告               
      └──────────────────────────────────────┘
    
      ┌──────────────────────────────────────┐
    ├─▶│ Stage 3: 布局生成 (Layout)             
       输入: PRD + 选中组件                    
       输出: Schema 骨架 (schemaDraft)         
        此时前端已可预览页面骨架               
      └──────────────────────────────────────┘
    
      ┌──────────────────────────────────────┐
    ├─▶│ Stage 4: 内容填充 (Content)            
       输入: Schema 骨架 + PRD                 
       输出: 完整 Schema (finalSchema)         
        前端看到内容填充完毕的完整页面          
      └──────────────────────────────────────┘
    
      ┌──────────────────────────────────────┐
    └─▶│ Stage 5: 校验修正 (Validate)           
        输入: 完整 Schema                      
        输出: 校验通过 / 修正后的 Schema          
       └──────────────────────────────────────┘

每个阶段是一次独立的 LLM 调用,上一阶段的输出是下一阶段的输入。这种设计带来了几个好处:

职责单一,prompt 更简洁。 每个阶段只需要关注一个任务。比如 Layout 阶段只关心「用选定的组件搭出合理的页面结构」,不需要同时操心内容是什么。

渐进式反馈。 Layout 阶段完成后就可以推送 Schema 骨架给前端预览,用户无需等到内容全部填充完毕。

故障隔离。 如果某个阶段的 LLM 输出解析失败,可以只对这个阶段做 fallback,前面已经成功的阶段成果不受影响。

可观测性。 每个阶段的输入输出都可以独立记录和审查,定位问题非常方便。

组件规划阶段:防止 LLM 幻觉

这是一个容易被忽视但至关重要的阶段。

我的做法是在调用 AI 之前,BFF 层先从数据库获取当前可用的物料组件列表,把每个组件的描述、能力和使用示例组装成上下文:

{
  "availableComponents": [
    {
      "type": "text",
      "description": "文本组件,用于展示标题、段落等文字内容",
      "bestPractices": "适用于标题、副标题、正文段落",
      "schemaExample": { "type": "text", "props": { "content": "示例文本" } }
    },
    {
      "type": "button",
      "description": "按钮组件,支持主要、次要等样式",
      "bestPractices": "用于 CTA、表单提交等场景"
    }
  ]
}

LLM 在 Component Plan 阶段被明确要求 只能从这个列表中选择组件。后续的 Layout 阶段也只使用已选中的组件来生成 Schema。

这个约束从根本上解决了「AI 幻觉出不存在的组件」的问题。即使 LLM 想用一个 carousel(轮播图)组件,但如果当前物料库里没有,Component Plan 阶段就会在 warnings 里标注「缺少轮播图组件,已用图片组件替代」。

结构化输出:Prompt 约束 + Zod 校验 + 多级 Fallback

我没有使用 OpenAI 的 Function Calling 或 Structured Outputs API。原因有二:

  1. 我使用的 LLM 服务(DeepSeek)对 Function Calling 的支持有限
  2. 我需要生成深度嵌套的 Schema 树,Function Calling 的 JSON Schema 表达力在嵌套场景下比较受限

取而代之的是一套 Prompt 约束 + 后处理 的方案:

Prompt 层面:System Prompt 明确要求模型只返回纯 JSON,不要包含 Markdown 代码块标记、不要添加解释文字。

解析层面:一个多级 JSON 提取器:

LLM 原始输出
    │
    ├── 尝试 1: 直接 JSON.parse
    │   失败 ↓
    ├── 尝试 2: 剥离 ```json ... ``` 标记后 parse
    │   失败 ↓
    └── 尝试 3: 正则提取第一个 {...} 后 parse

校验层面:用 Zod 定义每个阶段的输出 Schema,解析成功后立即校验结构。

Fallback 层面:如果上述全部失败(JSON 解析错误、Zod 校验不通过、API 超时等),系统退化到确定性生成——用预定义的模板和规则生成一个基础页面:

失败情况 → 确定性 Fallback
    │
    ├── 需求理解失败 → 基于关键词提取的启发式 PRD
    ├── 组件规划失败 → 基于规则的默认组件选择
    ├── 布局失败 → 预定义的通用页面模板
    └── 内容失败 → 占位符内容

这意味着用户永远不会看到一个空白结果或错误页面。 最坏情况下拿到的也是一个结构合理的基础页面,可以在此基础上编辑。

在我的实际运行数据中,LLM 输出直接通过校验的比例大约在 85-90%,经过多级解析器修复后提升到 95% 以上,真正触发 Fallback 的情况比较少见。


第三层:实时通信——让等待变成「观看」

为什么用 SSE 而不是 WebSocket?

AI 搭建场景是一个典型的 服务端单向推送 模型:后端逐阶段完成,前端逐阶段接收。不需要双向通信,SSE 的实现和运维成本都比 WebSocket 低得多。

阶段粒度 vs Token 粒度

一个值得讨论的决策:我在 AI 搭建场景中没有使用逐 token 的流式输出。

通用聊天场景确实用了 token 级的 SSE 流式——用户打字一样地看到 AI 回复逐字出现,体验很好。但 AI 搭建不一样:

  1. 每个阶段需要完整 JSON 才能校验。 不完整的 JSON 无法渲染到画布。
  2. 阶段粒度的反馈已经足够好。 Layout 阶段完成(通常 3-5 秒)后用户立即看到页面骨架,Content 阶段完成后看到完整内容——这已经是非常好的渐进式体验了。
  3. 简化了错误处理。 如果用 token 流式输出 JSON,中途出错就需要处理「半截 JSON」的情况,复杂度显著增加。

SSE 事件的格式很简洁:

event: build-event
id: 3
data: {"stage":"layout","status":"completed","payload":{"schemaDraft":{...}}}

前端收到带 schemaDraftfinalSchema 的事件后,直接写入 Schema Store,画布实时更新。

断线重连

SSE 本身支持 Last-Event-ID,利用这个机制实现了断线重连:每个事件有递增的序号,客户端重连时携带最后收到的序号,服务端从该位置继续推送。后端将所有事件持久化到数据库,所以即使服务重启也能恢复推送。


前端如何优雅地集成 AI 生成?

插件化架构的价值

编辑器采用 Slot + Extension 架构——核心编辑器只定义了布局骨架(头部、左侧面板、画布、右侧面板),所有功能以扩展的形式插入。

AI 搭建功能就是一个标准的扩展:

// AI 搭建扩展,注册为右侧面板的一个 Tab
class AIBuildExtension implements IExtension {
  id = 'ai-build';
  dependencies = ['right-panel'];

  init(context: IExtensionContext) {
    context.registerSlot('right-panel:content', {
      component: AIBuildPanel,
      order: 0,
      meta: { tabLabel: '智能搭建' }
    });
  }
}

这意味着 AI 功能的加入没有修改任何核心编辑器代码。画布渲染、组件选中、属性编辑、undo/redo——这些都不知道 AI 的存在。AI 扩展只是通过公共的 setSchema API 写入数据,和人手拖拽出来的数据走完全一样的路径。

这个架构决策的价值在后续维护中体现得非常明显:AI 功能的 bug 修复和迭代不会波及编辑器核心,反过来编辑器核心的重构也不影响 AI 功能。

AI 生成状态机

前端用一个状态机管理 AI 搭建的生命周期:

idle → starting → intake → component_plan → layout → content → validate → completed
                                                                    ↘
                                                                   failed

每个 SSE 事件都驱动状态机流转。UI 根据当前状态展示不同的内容:

  • intake:显示「正在理解您的需求...」
  • layout:显示「正在设计页面布局...」+ 画布出现骨架
  • content:显示「正在填充内容...」+ 画布出现完整内容
  • completed:显示生成摘要 + 「保存」/「重新生成」按钮
  • failed:显示错误信息 + 重试选项

被低估的工程细节

物料的 AI 技能描述

为了让 AI 更好地使用组件,我在物料协议中增加了 aiSkill 字段:

interface MaterialAiSkill {
  description: string;      // 组件的语义描述
  bestPractices?: string;   // 最佳使用场景
  schemaExample?: object;   // Schema 示例片段
}

这些信息由组件开发者维护,在 AI 搭建时自动注入到 LLM 的上下文中。实践中我发现,好的组件描述对 AI 生成质量的提升比优化 prompt 模板更显著。 比如当 bestPractices 写明「容器组件适合用作 Section 分区,建议设置 padding 和 background」时,LLM 生成的布局明显更合理。

这启发了一个认知:当你的系统需要 AI 理解领域知识时,与其不断调优 prompt,不如把知识结构化地内嵌到数据中。

并发控制

一个用户同时发起多个 AI 搭建请求会怎样?Schema Store 被覆盖、SSE 事件交叉——这是灾难。

我用 Redis 实现了 per-user 的构建锁:同一用户同一时刻只能有一个活跃的 AI 搭建会话。后续请求会收到明确的提示,而不是静默失败。

每次 LLM 调用都有审计

我记录了 AI 搭建过程中每次 LLM 调用的完整信息:

  • 输入(System Prompt + User Message)
  • 原始输出文本
  • 解析结果(成功/失败)
  • Zod 校验结果
  • 是否触发了 Fallback
  • 耗时和 token 用量

这些数据在两个场景中非常有价值:

  1. 线上问题排查。 用户反馈「生成的页面不对」时,可以回溯每个阶段的输入输出,快速定位是哪个阶段出了问题。
  2. 持续优化。 统计每个阶段的成功率和 Fallback 率,针对性地优化 prompt 或调整模型参数。

关键设计决策复盘

回顾整个项目,有几个决策对最终效果影响最大:

决策 1:AI 输出直接进入编辑器的核心数据流

没有做一个独立的「AI 生成预览」,再让用户「导入到编辑器」。而是让 AI 输出直接写入 Schema Store。

好处: 零集成成本;undo/redo 自动可用;后续编辑完全无缝。 代价: AI 输出的 Schema 格式必须和手动创建的完全一致,这增加了 AI 端的约束。

决策 2:多阶段 Pipeline 而非单次生成

好处: 渐进式体验;每阶段可独立优化和容错;可观测性强。 代价: 总耗时可能更长(多次 LLM 调用);阶段间的数据传递增加了复杂度。

实际测量发现,虽然调用次数多了,但每次调用的 prompt 更短、输出更简洁,总 token 消耗反而差不多。而体验上的提升是显而易见的。

决策 3:确定性 Fallback 而非暴露错误

好处: 用户永远能拿到一个可用的结果。 代价: Fallback 生成的页面质量有限(基本是模板);用户可能不知道 AI 没有真正理解他的需求。

通过在生成摘要中标注「部分内容使用了默认模板」来缓解这个问题。

决策 4:约束可用组件而非自由生成

好处: 从根本上消除了「不可渲染的 Schema」问题。 代价: 限制了 AI 的创造性;物料库组件不足时,生成结果会比较单一。

这个 trade-off 在产品层面是合理的——无代码平台的核心价值就是在「有限组件」内提供「可靠体验」。


后续探索方向

多轮对话式调整。 当前是单次 prompt 生成整个页面。下一步计划支持「在已有页面上继续对话」——比如用户说「把 Banner 区域换成轮播图」「颜色改成蓝色调」,AI 理解当前 Schema 上下文后做增量修改。

基于模板的精细化。 积累足够多的用户实际使用的页面后,可以做 few-shot learning——在 prompt 中注入和用户需求最相似的历史页面作为参考,提升生成质量。

多模型策略。 不同阶段对 LLM 的要求不同。需求理解需要强理解力,布局生成需要结构化能力,内容填充需要创造力。未来可以在不同阶段使用不同的模型,在质量和成本间取得更好的平衡。


总结

「让 AI 搭建页面」听起来是一个 AI 应用层的问题,但实际上它更像一个系统工程问题。真正的挑战不在于让 LLM 生成 JSON——而在于:

  1. 设计统一的数据模型,让 AI 生成和人工编辑走同一条路
  2. 分解认知任务,用多阶段 Pipeline 替代单次生成
  3. 建立约束体系,防止 AI 幻觉(组件白名单、Schema 校验)
  4. 构建容错层,确保在 LLM 不可靠时系统仍然可用
  5. 打通实时通信,把「等待」变成「渐进式观看」

如果你也在做类似的「AI 生成结构化输出」类产品,希望这些经验对你有帮助。核心 takeaway 是:不要把 LLM 当成一个可靠的函数调用——把它当成一个需要被精心编排、约束和兜底的不确定性来源。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com