AI 写业务逻辑已经很顺手,但设计稿还原?样式丢、布局乱、代码难维护。这不是模型不够强,是我们喂给它的输入不对。
落地过程中,我发现 AI D2C 的困难归结为两个根本问题。
问题一:AI 没有空间认知
LLM 是序列模型,处理的是 token 流,不是二维平面。当它看到:
json
{ "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 架构的固有特性------上下文越长,早期信息的影响力越弱。
两个问题互相放大
最要命的是,这两个问题是耦合的:
markdown
空间推理弱 → 需要更详细的布局描述来补偿
↓
上下文变大
↓
注意力涣散 → 连详细的描述也读不准了
↓
布局错误更多 → 需要更多修复指令
↓
上下文进一步膨胀 → 恶性循环
直接把 16MB 的设计稿丢给 AI,这两个问题同时爆发。
我的解法:在 LLM 之前把这两件事做掉
核心思路:别让 LLM 做空间计算,别让 LLM 处理长上下文。用工程手段在模型介入之前,把坐标关系翻译成语言描述,把大上下文拆成小片段。
css
[前置] 扫描项目代码风格
↓
设计文件 → DSL 压缩 → 语义理解 → 意图推断 → 代码生成
每个阶段都在对抗这两个根本问题:
| 阶段 | 对抗「无空间认知」 | 对抗「注意力涣散」 |
|---|---|---|
| DSL 压缩 | --- | 16MB → 2MB,减少无关 token |
| 区域拆分 | 预计算空间分组,告诉 AI「这些在一行」 | 2MB → 0.34MB,进一步缩小上下文 |
| 语义理解 | 把坐标关系翻译成语言:「card + list」 | 用语义标签替代原始坐标 |
| 意图推断 | 把「三个 300px 元素」翻译成「等分布局」 | 意图确认后不再需要原始数据 |
| 代码生成 | 输入已经是「flex:1」而不是坐标 | 每个模块独立生成,上下文极小 |
下面展开每个阶段的具体做法。
前置阶段:扫描项目代码风格
在处理设计稿之前,先扫描目标项目,提取现有代码的风格和规范。生成的代码要和项目保持一致。
提取什么
| 维度 | 来源 | 示例 |
|---|---|---|
| 框架类型 | package.json | Vue3 + Element Plus + TypeScript |
| 编码规范 | eslint/prettier/editorconfig | 单引号、无分号、缩进 2 空格 |
| 已有组件 | src/components/*.vue | ContentWrap、Pagination、Icon |
| 样式 Token | src/styles/*.scss | --el-color-primary: #409eff |
从现有代码学习
配置文件只能告诉你缩进是 2 空格还是 4 空格,真正的代码风格要从现有组件里学:
typescript
// 从项目现有 .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 个组件样本):
arduino
✔ 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 全量命中------这比全局统计更可信。
优先级
yaml
1. 现有代码学习 # 最高 --- 实际代码说了算
2. 配置文件 # 其次 --- ESLint/Prettier
3. 框架默认 # 最低 --- Vue3 官方推荐
配置文件与代码样本冲突时,以样本为准。项目组件数 < 3 时才用框架默认。
拿到设计文件
输入通常是这几种:
- 蓝湖导出的 JSON(3-10MB)
- Figma 导出的 JSON
- 设计图片(降级方案)
实际案例 :我们的 OA 首页设计稿,蓝湖导出的 ui.json 有 16.55MB ,画板尺寸 1920×3210px,包含 27 个模块 (审批、考勤、红黑榜、销售漏斗等)、107 个图片资源 、1387 个节点。
原始 JSON 长这样:
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 链接,需要建立映射:
json
// 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 解析出的模块结构:
ini
顶部区域 (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 个模块
怎么推断:
-
看名称 --- 设计师命名的
name字段常有提示:name: "按钮"→ button,name: "标题"→ headername: "内容"→ content、name: "导航"→ nav
-
看结构 --- 子节点重复 → list;有文本+边框+背景 → card
-
看尺寸 --- 25×25px 的图片节点 → icon;700×440 的容器 → card
每个推断带置信度分数。名称明确提示 → 0.9+,仅靠尺寸猜 → 0.5-0.7。低置信度的后面会让你确认。
意图推断
回答「设计师想要什么效果」。
这步最关键,也是传统方案最容易忽略的。
看这个例子:
设计稿上三张卡片,都是 300px 宽,间距 20px。问题:这是等分布局还是固定宽度?
视觉上一样,代码完全不同:
css
/* 等分 --- 容器变宽,卡片跟着变宽 */
.card { flex: 1; }
/* 固定 --- 容器变宽,卡片还是 300px */
.card { width: 300px; }
不搞清楚意图,生成的代码「看起来对,但行为错」。
需要推断的意图:
- 等分还是固定宽度?
- 允许换行吗?
- 超出怎么处理?截断、滚动、换行?
- 小屏幕怎么响应?堆叠、隐藏、缩小?
推断依据:
- 三个元素宽度相等,加起来接近容器宽度 → 可能等分
- 使用 8px 栅格 → 大概率响应式
- 后台管理系统 → 大概率固定宽度
置信度低于 0.7 时,直接问你:
css
检测到三张卡片,宽度均为 300px。请确认布局意图:
[ ] 等分布局(卡片宽度随容器变化)
[ ] 固定宽度(卡片始终 300px)
代码生成
输入:带语义标签的 DSL + 明确的意图 + 目标框架
语义映射到组件:
| 语义 | Vue3 + Element Plus | React + Ant Design |
|---|---|---|
| card | el-card | Card |
| button | el-button | Button |
| table | el-table | Table |
意图映射到布局:
| 意图 | CSS |
|---|---|
| 等分 | flex + flex:1 |
| 固定宽度 | flex + width |
| 允许换行 | flex-wrap: wrap |
只写差异样式(隐式样式分析):
这里有个关键概念:隐式样式层。
每个组件库都有默认样式,比如 Element Plus 的 el-card:
yaml
el-card:
background: "#fff"
border-radius: "4px"
border: "1px solid #ebeef5"
设计稿的背景也是 #fff?不用写。只写与默认值不同的部分:
scss
// ❌ 冗余
.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 变量?优先用变量:
scss
// 设计值 20px 匹配到 --el-component-size
padding: var(--el-component-size);
代码更规范,也能响应主题切换。
这套方法比「直接丢给 AI」好在哪
以 OA 首页为例:
| 维度 | 直接丢给 AI | 五阶段模型 |
|---|---|---|
| 输入大小 | 16.55MB 原始 JSON | 0.34MB 区域 DSL |
| 节点数 | 1387 个全部丢入 | 按模块拆分,每次 30-200 个 |
| 图片资源 | 无法处理 | 107 个图片自动建立 URL→本地文件映射 |
| 布局准确性 | 靠猜 | 意图明确后再推断 |
| 样式精确度 | 可能编造 | 从源数据提取 |
| 模块覆盖 | 可能漏掉模块 | 27 个模块全部识别 |
现有代码已实现 11 个模块,通过这套方案识别出了 13 个待开发模块、确定了 6 个可复用的排行榜组件。
回到根本问题:工程缓解策略
上面的流程是「预防」,但落地时注意力涣散仍然会发生。还需要额外的工程手段来弥补:
用工程手段弥补
规则强化 --- 在关键约束上用强语气标记:
markdown
⚠️ **核心原则:必须从 DSL 节点提取精确值**
❌ **禁止**:让 LLM 凭记忆生成颜色值
✅ **必须**:每个样式值标注来源节点 ID
⚠️、❌ 禁止、✅ 必须 这类标记能显著提升约束遵守率。
检查点机制 --- 流程中设强制验证点,没过就不能继续:
yaml
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 保持聚焦。
显式行范围 --- 精确指定该读哪段:
arduino
// 模糊(容易遗漏)
"读取 ui-dsl.json,找到模块信息"
// 精确(更可靠)
"读取 ui-dsl.json 的第 285-725 行,这是模块 A 的 DSL 数据"
来源标注 --- 每个样式值标注 DSL 节点 ID:
scss
// 来源:节点 3:793 的 text.style.color
color: rgba(51, 51, 51, 1);
要标注来源,就必须去读原始数据,而不是凭印象编造。
本质
这些策略的本质:用工程手段弥补模型的注意力缺陷。
- 规则强化 → 提升关键信息的权重
- 检查点 → 阻断错误传播
- 分治 → 缩小单次处理的上下文
- 显式范围 → 减少无关信息干扰
- 来源标注 → 强制回溯原始数据
演进方向:Teams Agents 架构
当前方案用「主 Agent + Subagent」,主 Agent 还是要理解全局,上下文逐步累积。更彻底的方案:多 Agent 协作。
当前方案
markdown
主 Agent(上下文膨胀)
├── 读 DSL
├── 分析语义
├── 推断意图
├── 调度 Subagent
└── 合并结果
Teams Agents 方案
css
共享状态(DSL + 分析结果)
│
├── DSL Agent → 只做压缩和结构化,输出写入共享状态
├── 语义 Agent → 读共享状态,只做语义标签
├── 意图 Agent → 读共享状态,只做意图推断
├── 代码 Agent 1 → 读共享状态,只生成模块 A
├── 代码 Agent 2 → 读共享状态,只生成模块 B
└── Leader Agent → 不处理细节,只做检查和调度
为什么更好
| 维度 | 单 Agent | Teams Agents |
|---|---|---|
| 上下文隔离 | Subagent 有隔离,主 Agent 没有 | 每个 Agent 都隔离 |
| Leader 负担 | 要理解全部细节 | 只看摘要和检查点 |
| 信息传递 | prompt 传递,容易丢失 | 共享状态,精确读取 |
| 并行能力 | 受主 Agent 调度限制 | 完全并行 |
| 单点故障 | 主 Agent 出错全崩 | 单个 Agent 出错可重试 |
Leader Agent 的职责
不再做: 读 DSL、理解设计细节、记住样式值
只做:
- 定义任务边界(哪个 Agent 负责哪个模块)
- 检查产出完整性(模块数对不对、覆盖率够不够)
- 处理冲突(两个 Agent 输出不一致时决策)
- 最终合并
跑通需要什么
- 共享状态存储 --- 文件系统 / 数据库 / 内存 KV
- Agent 通信协议 --- 谁先跑、谁依赖谁、怎么通知完成
- 支持 Teams 的平台 --- OpenAI Swarm、AutoGen、CrewAI、或自己搭
五阶段模型是基础,Teams 架构是优化。
边界
这套方案解决的是结构化设计稿到静态代码。这些不覆盖:
- 复杂动效 --- 需要额外的动效描述层
- 交互逻辑 --- 按钮点了做什么,得单独定义
- 数据绑定 --- 哪些是静态文本、哪些是动态数据,得标注
- 设计稿本身有问题 --- 方案假设设计稿是规范的
总结
AI D2C 的两个根本问题------无空间认知 和注意力涣散------不会因为模型变强而彻底消失。它们是 Transformer 架构的固有特性。
所以解法不是等更强的模型,而是用工程手段绕过它们:
- 扫描 --- 读懂项目现有代码风格
- 压缩 --- 去噪,缩小上下文
- 语义 --- 把坐标翻译成语言
- 意图 --- 把像素翻译成意图
- 生成 --- 用项目风格写对代码
再加上规则强化、检查点、分治等工程手段对抗注意力涣散,用 Teams 架构进一步隔离上下文。
欢迎讨论
这套方案是我在 OA 项目中落地的实践,不是最优解。
我很想听到不同的思路:
- 空间认知问题有没有更好的解法?比如给 LLM 加一个视觉编码器,或者用多模态模型直接看设计图?
- 注意力涣散除了分治和规则强化,还有什么工程技巧?
- 如果用 Teams Agents 架构,共享状态怎么设计最合理?
- 有没有人在做 Figma 插件 + LLM 的方案,跟这套思路有什么交集?
欢迎 PR、Issue 或直接讨论。