AI Design-to-Code 的两个根本问题,和我的解法

101 阅读15分钟

AI 写业务逻辑已经很顺手,但设计稿还原?样式丢、布局乱、代码难维护。这不是模型不够强,是我们喂给它的输入不对。

落地过程中,我发现 AI D2C 的困难归结为两个根本问题。


问题一:AI 没有空间认知

LLM 是序列模型,处理的是 token 流,不是二维平面。当它看到:

{ "x": 285, "y": 725, "width": 700, "height": 440 }
{ "x": 1005, "y": 725, "width": 370, "height": 440 }
{ "x": 285, "y": 1165, "width": 340, "height": 400 }

它看到的是三组数字,不是「第一行两张卡片并排,第二行一张卡片靠左」。

人看设计稿是空间扫描 — 一眼看出对齐、等距、分栏。LLM 看坐标是数值推理 — 要算 1005 - 285 = 720,再跟 width: 700 比较,才能推断「这两个元素是水平排列的」。而数值推理恰恰是 LLM 最弱的能力之一。

这导致几类典型错误:

空间关系人的判断LLM 容易犯的错
水平对齐y 值接近就是一行把 y=725 和 y=730 判断成两行
等分布局三个等宽元素占满容器生成固定 px 而不是 flex:1
嵌套层级小元素在大元素内部坐标包含关系算错,层级打平
间距规律所有模块间距 20px部分写 20,部分写 16,不一致

本质原因:Transformer 的自注意力机制是在 token 维度上建立关联的,它没有内置的二维坐标系。它理解「猫坐在垫子上」比理解「x=100 的元素在 x=500 的元素左边」要容易得多——前者是语言语义,后者是空间计算。

问题二:上下文窗口造成注意力涣散

Transformer 的注意力是一个 softmax 分布——所有 token 共享一个概率空间。上下文从 2k 增长到 50k 时,每个 token 分到的平均注意力从 1/2000 降到 1/50000。

具体表现:

现象示例
遗忘前面强调"必须带 data-ir-id",后面生成的代码就忘了
偏移样式值从精确的 rgba(51,51,51,1) 变成随意的 #333
简化该生成 10 个模块,只生成了 3 个就停了
幻觉编造设计稿里不存在的元素

这不是模型能力问题,是 Transformer 架构的固有特性——上下文越长,早期信息的影响力越弱。

两个问题互相放大

最要命的是,这两个问题是耦合的:

空间推理弱 → 需要更详细的布局描述来补偿
                 ↓
         上下文变大
                 ↓
         注意力涣散 → 连详细的描述也读不准了
                 ↓
         布局错误更多 → 需要更多修复指令
                 ↓
         上下文进一步膨胀 → 恶性循环

直接把 16MB 的设计稿丢给 AI,这两个问题同时爆发。


我的解法:在 LLM 之前把这两件事做掉

核心思路:别让 LLM 做空间计算,别让 LLM 处理长上下文。用工程手段在模型介入之前,把坐标关系翻译成语言描述,把大上下文拆成小片段。

[前置] 扫描项目代码风格
   ↓
设计文件 → DSL 压缩 → 语义理解 → 意图推断 → 代码生成

每个阶段都在对抗这两个根本问题:

阶段对抗「无空间认知」对抗「注意力涣散」
DSL 压缩16MB → 2MB,减少无关 token
区域拆分预计算空间分组,告诉 AI「这些在一行」2MB → 0.34MB,进一步缩小上下文
语义理解把坐标关系翻译成语言:「card + list」用语义标签替代原始坐标
意图推断把「三个 300px 元素」翻译成「等分布局」意图确认后不再需要原始数据
代码生成输入已经是「flex:1」而不是坐标每个模块独立生成,上下文极小

下面展开每个阶段的具体做法。


前置阶段:扫描项目代码风格

在处理设计稿之前,先扫描目标项目,提取现有代码的风格和规范。生成的代码要和项目保持一致。

提取什么

维度来源示例
框架类型package.jsonVue3 + Element Plus + TypeScript
编码规范eslint/prettier/editorconfig单引号、无分号、缩进 2 空格
已有组件src/components/*.vueContentWrap、Pagination、Icon
样式 Tokensrc/styles/*.scss--el-color-primary: #409eff

从现有代码学习

配置文件只能告诉你缩进是 2 空格还是 4 空格,真正的代码风格要从现有组件里学:

// 从项目现有 .vue 文件中提取的模式
interface LearnedPatterns {
  // 脚本风格
  scriptStyle: 'setup' | 'options';       // <script setup> vs <script>
  useDefineOptions: boolean;               // defineOptions({ name: '...' })
  propsStyle: 'type-based' | 'runtime';   // defineProps<T>() vs defineProps({})
  emitsStyle: 'type-based' | 'runtime';   // defineEmits<T>() vs defineEmits([])
  // 导入风格
  importOrder: string[];                   // ['vue', 'element-plus', '@/xxx']
  importGrouping: 'grouped' | 'flat';
  pathAlias: Record<string, string>;       // { '@': 'src', '#': 'types' }
  // 样式风格
  styleLang: 'scss' | 'less' | 'css';
  styleScoped: boolean;
  useDeepSelector: ':deep()' | '::v-deep'; // 深度选择器风格
  // 命名风格
  cssClassStyle: 'kebab-case' | 'BEM';    // .card-header vs .card__header
  refNaming: 'xxxRef' | 'refXxx';         // tableRef vs refTable
}

学习方法:扫描项目现有组件(至少 3 个样本),统计各模式出现频率,取多数。每条结论必须附带来源文件路径。

实测基线(OA 项目,195 个组件样本):

184/195 组件用 <script setup>      → 采用 setup 风格
✔ 169/195 组件用 defineOptions        → 加 defineOptions
✔ 120/195 组件用 scoped 样式          → 加 scoped
✔ 123/195 组件用 lang="scss"          → 采用 SCSS
✔ 引号统计 single:3577 行 / double:46 行 → 单引号

还会按业务目录二次扫描。比如首页组件目录 23 个文件,script setup + defineOptions + scoped scss 全量命中——这比全局统计更可信。

优先级

1. 现有代码学习   # 最高 — 实际代码说了算
2. 配置文件         # 其次 — ESLint/Prettier
3. 框架默认         # 最低 — Vue3 官方推荐

配置文件与代码样本冲突时,以样本为准。项目组件数 < 3 时才用框架默认。


拿到设计文件

输入通常是这几种:

  • 蓝湖导出的 JSON(3-10MB)
  • Figma 导出的 JSON
  • 设计图片(降级方案)

实际案例:我们的 OA 首页设计稿,蓝湖导出的 ui.json16.55MB,画板尺寸 1920×3210px,包含 27 个模块(审批、考勤、红黑榜、销售漏斗等)、107 个图片资源1387 个节点

原始 JSON 长这样:

{
  "transform": [[1, 0, 0], [0, 1, 0]],
  "combinedFrame": { ... },
  "paths": [ ... ],
  "masks": [ ... ]
}

这些字段是渲染引擎需要的,开发者不需要,LLM 更不需要。


压缩成 DSL

目标:把「能画 UI」的数据变成「能描述 UI」的数据。

保留这些字段:

  • id — 节点标识
  • name — 节点名称(常有语义提示)
  • type — 节点类型
  • frame — 位置和尺寸
  • style — 样式(fills / borders / shadows)
  • text — 文本内容和样式
  • image — 图片 URL

删掉这些:

  • transform 矩阵
  • paths 路径数据
  • 蒙版、裁切信息
  • 无意义的分组中间层

实测效果(OA 首页):

文件大小说明
ui.json(原始)16.55 MB蓝湖导出,含全部绘图指令
ui-dsl.json(压缩后)2.02 MB只保留结构+样式+文本
region-row1.json(区域拆分)0.34 MB单个区域的 DSL,可直接喂给 LLM

压缩率 87%。进一步按区域拆分后,单个文件只有 300KB 左右,LLM 能轻松处理。

图片资源处理ui.json 里的图片是 URL 链接,需要建立映射:

// image-manifest.json (生成的映射表)
{
  "images": [
    {
      "url": "https://lanhuapp.com/.../icon-approve.png",
      "local": "assets/icons/icon-approve.png",
      "type": "png",
      "size": "25×25"
    }
  ],
  "total": 107,
  "uniquePng": 64,
  "uniqueSvg": 97
}

生成代码时,LLM 直接使用 local 路径,不用处理外部 URL。


语义理解

回答「这是什么」。

给每个节点打标签:container、card、button、list、table、nav、header、footer...

实际案例 — OA 首页 DSL 解析出的模块结构:

顶部区域 (y=0~265, 30节点)
    ├── logo (228×68)
    ├── 按钮: 刷新缓存
    ├── 按钮: 进入后台
    └── 对话: "下午好,何冰玉"
区域 Row1 (y=285~725, 234节点)
    ├── 本月目标完成情况 (700×440) → card + chart
    ├── 常用功能 (370×440) → grid
    └── 审批+预计收益+考勤+报销 (770×440) → 2×2 grid
区域 Row2 (y=745~1145, 194节点)
    ├── 红黑榜单 (340×400) → card + list
    ├── 异常榜 (340×400) → card + list
    ├── 销售榜 (370×400) → card + list
    └── 企业公告 (770×400) → card + list
...共 7 个区域,27 个模块

怎么推断:

  1. 看名称 — 设计师命名的 name 字段常有提示:

    • name: "按钮" → button,name: "标题" → header
    • name: "内容" → content、name: "导航" → nav
  2. 看结构 — 子节点重复 → list;有文本+边框+背景 → card

  3. 看尺寸 — 25×25px 的图片节点 → icon;700×440 的容器 → card

每个推断带置信度分数。名称明确提示 → 0.9+,仅靠尺寸猜 → 0.5-0.7。低置信度的后面会让你确认。


意图推断

回答「设计师想要什么效果」。

这步最关键,也是传统方案最容易忽略的。

看这个例子:

设计稿上三张卡片,都是 300px 宽,间距 20px。问题:这是等分布局还是固定宽度?

视觉上一样,代码完全不同:

/* 等分 — 容器变宽,卡片跟着变宽 */
.card { flex: 1; }

/* 固定 — 容器变宽,卡片还是 300px */
.card { width: 300px; }

不搞清楚意图,生成的代码「看起来对,但行为错」。

需要推断的意图:

  • 等分还是固定宽度?
  • 允许换行吗?
  • 超出怎么处理?截断、滚动、换行?
  • 小屏幕怎么响应?堆叠、隐藏、缩小?

推断依据:

  • 三个元素宽度相等,加起来接近容器宽度 → 可能等分
  • 使用 8px 栅格 → 大概率响应式
  • 后台管理系统 → 大概率固定宽度

置信度低于 0.7 时,直接问你:

检测到三张卡片,宽度均为 300px。请确认布局意图:
[ ] 等分布局(卡片宽度随容器变化)
[ ] 固定宽度(卡片始终 300px

代码生成

输入:带语义标签的 DSL + 明确的意图 + 目标框架

语义映射到组件:

语义Vue3 + Element PlusReact + Ant Design
cardel-cardCard
buttonel-buttonButton
tableel-tableTable

意图映射到布局:

意图CSS
等分flex + flex:1
固定宽度flex + width
允许换行flex-wrap: wrap

只写差异样式(隐式样式分析):

这里有个关键概念:隐式样式层

每个组件库都有默认样式,比如 Element Plus 的 el-card:

el-card:
  background: "#fff"
  border-radius: "4px"
  border: "1px solid #ebeef5"

设计稿的背景也是 #fff?不用写。只写与默认值不同的部分:

// ❌ 冗余
.card {
  background: #fff;      // el-card 默认
  border-radius: 4px;    // el-card 默认
  padding: 20px;
}

// ✅ 最小化
.card {
  padding: 20px;
}

这样做的好处:

  • 代码更干净
  • 不会覆盖组件库的主题变量
  • 后续换主题时不会出问题

样式值从 DSL 精确提取:

OA 首页提取出的设计 Token:

用途DSL 来源
主文字rgba(51,51,51,1)节点 3:793 text.style.color
模块标题Source Han Sans CN Bold 16px标题节点 text.style.font
页面边距20px从容器 frame.x 计算
模块间距20px从相邻模块 frame 差值计算

不让 LLM 凭记忆编颜色值——它会编错的。

Token 匹配:

设计值能匹配到项目的 CSS 变量?优先用变量:

// 设计值 20px 匹配到 --el-component-size
padding: var(--el-component-size);

代码更规范,也能响应主题切换。


这套方法比「直接丢给 AI」好在哪

以 OA 首页为例:

维度直接丢给 AI五阶段模型
输入大小16.55MB 原始 JSON0.34MB 区域 DSL
节点数1387 个全部丢入按模块拆分,每次 30-200 个
图片资源无法处理107 个图片自动建立 URL→本地文件映射
布局准确性靠猜意图明确后再推断
样式精确度可能编造从源数据提取
模块覆盖可能漏掉模块27 个模块全部识别

现有代码已实现 11 个模块,通过这套方案识别出了 13 个待开发模块、确定了 6 个可复用的排行榜组件。


回到根本问题:工程缓解策略

上面的流程是「预防」,但落地时注意力涣散仍然会发生。还需要额外的工程手段来弥补:

用工程手段弥补

规则强化 — 在关键约束上用强语气标记:

⚠️ **核心原则:必须从 DSL 节点提取精确值****禁止**:让 LLM 凭记忆生成颜色值
✅ **必须**:每个样式值标注来源节点 ID

⚠️❌ 禁止✅ 必须 这类标记能显著提升约束遵守率。

检查点机制 — 流程中设强制验证点,没过就不能继续:

checkpoint:
  name: "DSL 分析完成检查"
  required_outputs:
    - module_list_table  # 模块清单
    - colors             # 颜色值列表
    - coverage_status    # 覆盖率状态
  on_missing:
    action: "阻止继续,要求补全"

流水线上的质量门禁,错误不传递到下游。

Subagent 分治 — 大任务拆成小任务,每个 Subagent 只处理一个模块:

角色上下文大小职责
主 Agent~2k tokens调度、分配、合并
Subagent 1~5k tokens生成模块 A 代码
Subagent 2~5k tokens生成模块 B 代码

避免单个 Agent 处理 50k+ tokens,每个 Agent 保持聚焦。

显式行范围 — 精确指定该读哪段:

// 模糊(容易遗漏)
"读取 ui-dsl.json,找到模块信息"

// 精确(更可靠)
"读取 ui-dsl.json 的第 285-725 行,这是模块 A 的 DSL 数据"

来源标注 — 每个样式值标注 DSL 节点 ID:

// 来源:节点 3:793 的 text.style.color
color: rgba(51, 51, 51, 1);

要标注来源,就必须去读原始数据,而不是凭印象编造。

本质

这些策略的本质:用工程手段弥补模型的注意力缺陷

  • 规则强化 → 提升关键信息的权重
  • 检查点 → 阻断错误传播
  • 分治 → 缩小单次处理的上下文
  • 显式范围 → 减少无关信息干扰
  • 来源标注 → 强制回溯原始数据

演进方向:Teams Agents 架构

当前方案用「主 Agent + Subagent」,主 Agent 还是要理解全局,上下文逐步累积。更彻底的方案:多 Agent 协作

当前方案

主 Agent(上下文膨胀)
    ├── 读 DSL
    ├── 分析语义
    ├── 推断意图
    ├── 调度 Subagent
    └── 合并结果

Teams Agents 方案

共享状态(DSL + 分析结果)
    │
    ├── DSL Agent      → 只做压缩和结构化,输出写入共享状态
    ├── 语义 Agent     → 读共享状态,只做语义标签
    ├── 意图 Agent     → 读共享状态,只做意图推断
    ├── 代码 Agent 1   → 读共享状态,只生成模块 A
    ├── 代码 Agent 2   → 读共享状态,只生成模块 B
    └── Leader Agent   → 不处理细节,只做检查和调度

为什么更好

维度单 AgentTeams Agents
上下文隔离Subagent 有隔离,主 Agent 没有每个 Agent 都隔离
Leader 负担要理解全部细节只看摘要和检查点
信息传递prompt 传递,容易丢失共享状态,精确读取
并行能力受主 Agent 调度限制完全并行
单点故障主 Agent 出错全崩单个 Agent 出错可重试

Leader Agent 的职责

不再做: 读 DSL、理解设计细节、记住样式值

只做:

  • 定义任务边界(哪个 Agent 负责哪个模块)
  • 检查产出完整性(模块数对不对、覆盖率够不够)
  • 处理冲突(两个 Agent 输出不一致时决策)
  • 最终合并

跑通需要什么

  1. 共享状态存储 — 文件系统 / 数据库 / 内存 KV
  2. Agent 通信协议 — 谁先跑、谁依赖谁、怎么通知完成
  3. 支持 Teams 的平台 — OpenAI Swarm、AutoGen、CrewAI、或自己搭

五阶段模型是基础,Teams 架构是优化。


边界

这套方案解决的是结构化设计稿到静态代码。这些不覆盖:

  1. 复杂动效 — 需要额外的动效描述层
  2. 交互逻辑 — 按钮点了做什么,得单独定义
  3. 数据绑定 — 哪些是静态文本、哪些是动态数据,得标注
  4. 设计稿本身有问题 — 方案假设设计稿是规范的

总结

AI D2C 的两个根本问题——无空间认知注意力涣散——不会因为模型变强而彻底消失。它们是 Transformer 架构的固有特性。

所以解法不是等更强的模型,而是用工程手段绕过它们:

  1. 扫描 — 读懂项目现有代码风格
  2. 压缩 — 去噪,缩小上下文
  3. 语义 — 把坐标翻译成语言
  4. 意图 — 把像素翻译成意图
  5. 生成 — 用项目风格写对代码

再加上规则强化、检查点、分治等工程手段对抗注意力涣散,用 Teams 架构进一步隔离上下文。


欢迎讨论

这套方案是我在 OA 项目中落地的实践,不是最优解。

我很想听到不同的思路:

  • 空间认知问题有没有更好的解法?比如给 LLM 加一个视觉编码器,或者用多模态模型直接看设计图?
  • 注意力涣散除了分治和规则强化,还有什么工程技巧?
  • 如果用 Teams Agents 架构,共享状态怎么设计最合理?
  • 有没有人在做 Figma 插件 + LLM 的方案,跟这套思路有什么交集?

欢迎 PR、Issue 或直接讨论。