为什么 AI 编码工具用 DSL 而不是 HTML:约束输出的原理
一、元认知:LLM 输出的根本矛盾 #
LLM 是概率模型,每次生成都从巨大输出空间里采样。问题随之而来:当你需要一个稳定的、可校验的、视觉一致的产物时,概率模型的天然方差就是敌人。
这矛盾不是 Qoder 独有,是所有”让 AI 生成结构化产物”的场景都要面对的根本问题:
- AI 生成 JSON → 可能缺字段、类型错
- AI 生成 SQL → 可能语法错、注入风险
- AI 生成代码 → 可能符号不存在、import 缺
- AI 生成 UI → 可能样式不一致、布局错乱
解决思路有两条:
- 事后校验:让 AI 自由生成,然后用 schema / 编译器 / 类型系统过滤错误。
- 事前约束:限制 AI 能说什么,把它能产出的形态固化成有限原语。
第一条路线是”开放生成 + 兜底”,第二条是”受限生成 + 保证”。两条路各有适用场景,但当产物形态相对固定(如任务报告、表单、Dashboard)时,第二条路更优——因为它把”开放生成问题”降维成了”结构化数据填充问题”。
类比:让秘书写会议纪要。给一张白纸,每次格式都不一样,你要花时间统一;给一个模板(议题 / 决策 / 待办 / 责任人),秘书只需填空,输出天然一致。模板就是约束,填空就是降维。
二、搭积木:约束输出的两个工程手段 #
把”约束”拆开,有两个层次,常被混为一谈。
手段一:限制语法层——约束输出 #
第一个手段是收缩模型的决策维度。
原生 HTML 给模型的自由度有四维:
| 维度 | 模型要做的决策 |
|---|---|
| 结构 | div 还是 section 还是 article? |
| 样式 | color: red 还是 #ff0000 还是 rgb(255,0,0)? |
| 行为 | 要不要加 onclick?要不要内联 <script>? |
| 语义 | class 叫什么?role 怎么写? |
每个维度都是自由度,组合空间巨大。模型每次都要在这四维里做选择,每次选择都可能不一致。
DSL 只暴露 Stack / Grid / H1 / Stat / Table / Tag / Callout 这套有限原语 + tone 枚举。模型只需决策两件事:
- 选哪个原语(有限枚举)
- 填什么内容(受 TypeScript 接口约束的数据)
从信息论看:输出熵 ↓,与目标契合度的互信息 ↑。这是把开放生成问题降维成结构化数据填充问题——表单 vs 自由文本,表单保证字段齐全、类型正确,自由文本可能丢字段、类型错。
手段二:上移视觉层——复用设计系统 #
第二个手段是关注点分离。
HTML + 内联 CSS 让模型同时承担语义和视觉两层职责。color:#ff0000、color:red、color:rgb(255,0,0) 三种写法等价,AI 会随机选——视觉抖动不可避免。
DSL 让模型只写语义 tone="success",渲染器统一映射到具体色值、字号、间距。审美决策从模型层上移到渲染层固化。模型只说”这是什么”,渲染器决定”长什么样”。
这背后是设计系统的核心理念:把审美决策固化为有限原语集合和语义化属性枚举。设计系统不是”组件库”,是”约束模型与人能做什么决策的契约”。
两个手段叠加的对比 #
| 维度 | 原生 HTML | canvas DSL |
|---|---|---|
| 输出空间 | 图灵完备,无限 | 原语笛卡尔积,有限 |
| 决策维度 | 结构 + 样式 + 行为 + 语义 | 仅语义 + 数据 |
| 一致性来源 | 依赖每次生成质量 | 渲染器保证 |
| 类型约束 | 无 | TS 接口 + 枚举 |
| 校验 | 需解析 HTML/CSS | 直接校验 props |
| 安全 | 可能 XSS | 白名单组件 |
| Token 成本 | 高(重复 boilerplate) | 低(声明式) |
| 演进 | 改 prompt 重新生成 | 改设计系统,旧产物自动跟随 |
隐藏属性:可计算性 #
还有个常被忽略的原理:DSL 输出是结构化数据,可二次聚合。
- 跨任务汇总:把多次 Goal 执行的 Stat 加总
- 对比两次执行:diff 两份 DSL 树
- 抽取指标入库:Stat / Tag 的 tone 转成监控指标
HTML 是展示态,不是数据态,做不了这些。Goal 模式的报告未来要做 BI 分析,必须结构化。这点决定了 DSL 不只是”为了好看”,是”为了可计算”。
三、案例即原理:Qoder canvas 剖析 #
来看用户给的实际样例(节选):
1 | |
这段代码体现了前面说的所有原理:
- 降维:模型只需决策 4 个 Stat 的 value / label / tone,不需要决策卡片样式、间距、颜色、布局——这些由
Grid和Stat组件内置。 - 语义而非视觉:
tone="success"是语义层(”这是正向结果”),渲染器决定 success 长什么样。未来设计系统改色板,所有旧报告自动跟随。 - 可计算:4 个 Stat 是结构化数据,可以提取成
{改动量: {新增方法:1, 新增import:2, ...}}入库。 - 安全:模型无法注入
onclick或<script>,因为Stat不暴露这些 props。
再看验证清单表:
1 | |
rowTone 是个很妙的设计:让整行的视觉强调(如未通过项标红)由数据驱动,而非让模型写 CSS。模型只说”这行是 warning 语义”,渲染器决定 warning 行怎么渲染。
这就是”案例即原理”——每段代码都在印证前面的原理,不是 Qoder 偶然这么设计,是它必须这么设计才能达到约束输出的目标。
为什么 Goal 模式特别需要这套 #
Goal 模式产出的是机器自主执行结果,用户需要快速判断”它干得对不对”。报告要:
- 可扫描(Stat 一眼看到改动量)
- 可核验(验证清单表 + 证据列)
- 可追溯(调用链路 Code 块 + 设计决策 Callout)
这套语义恰好对应 canvas 的 Stat / Table / Code / Callout 原语。Goal 模式的”汇报”场景定义了所需原语集合,canvas DSL 就是这集合的具体语法。换 HTML 等于让模型自己造汇报 UI,失去了”统一报告范式”的意义。
一句话:canvas 不是为了”画网页”,是为了”约束 Agent 汇报格式”,HTML 给的自由度在这里是反需求。
四、缺陷与批判:这套方案的代价 #
任何工程取舍都有代价,canvas DSL 不是银弹。
缺陷一:表达力天花板低 #
受限原语意味着表达力有上限。当用户想画一个 DSL 没有的东西(自定义图表、特殊布局、交互式组件),DSL 就崩了。
为什么?DSL 的本质是”白名单”,白名单外的都不存在;HTML 是”开放集合”,什么都能表达。
为什么这是固有缺陷而非实现问题:表达力和约束是负相关的,这是数学上的对偶。要约束强,就必须收缩原语集合;要表达力强,就必须放宽约束。两者不可兼得,只能选平衡点。Qoder 选了”汇报场景”这个窄带,所以能用强约束;一旦场景扩张到”通用 UI 生成”,DSL 必须放宽,约束效益随之衰减。
缺陷二:设计系统锁定 #
DSL 把视觉决策上移到渲染层,代价是所有产物都被锁死在这套设计系统里。想换风格?要么改渲染器(影响所有旧产物),要么迁移所有 DSL 代码。
为什么这是问题:设计系统是”审美决策的固化”,但审美会变。品牌升级、视觉改版时,HTML 产物可以逐个改 CSS,DSL 产物要么全量重生成,要么依赖渲染器向后兼容(设计系统会越来越臃肿)。
缺陷三:可迁移性差 #
qoder/canvas 是 Qoder 私有的 DSL。用户的产物绑死在 Qoder 生态里,迁出到别的 IDE / 平台要重写。
为什么这是问题:开放标准(HTML/CSS)的价值在于”任何地方都能跑”。DSL 牺牲了开放性换约束力。如果未来 Qoder 倒闭或转向,所有 DSL 产物变成债务。
缺陷四:适用场景窄 #
只有”形态相对固定的产物”才适合 DSL。Goal 模式的报告是典型——每次都是”摘要 + 表 + 链路 + 清单”的固定套路。但很多 AI 生成 UI 的场景不是这样:
| 场景 | 形态 | 更合适 |
|---|---|---|
| Goal 模式汇报 | 固定套路 | DSL |
| 用户让 AI 画”个人主页” | 开放 | HTML |
| 用户让 AI 设计落地页 | 视觉创意重要 | HTML |
| 用户让 AI 改已有网页 | 保留原结构 | HTML |
为什么这是原理性限制:DSL 假设”产物形态可枚举”。当形态不可枚举时,DSL 必须无限扩张原语集合,最终退化成 HTML。所以 DSL 的适用边界就是”形态可枚举的场景”。
五、总结事物本身 #
回到本质:AI 生成 UI 这件事,真正的问题不是”生成 HTML 还是 DSL”,而是”如何让 LLM 的概率性输出收敛到确定性产物”。
canvas DSL 只是这个问题的一种解法,它的核心策略是两条:
- 把开放生成降维成结构化填充(约束语法层)
- 把审美决策从模型层上移到渲染层(约束视觉层)
这两条策略不是 Qoder 发明的,是所有”AI 生成结构化产物”系统的通用原理:
| 工具 / 协议 | 约束手段 |
|---|---|
| Qoder canvas | 受限 JSX 原语 + tone 枚举 |
| Vercel v0 | shadcn/ui 组件约束 AI 输出 JSX |
| BlockNote / Editor.js | 块 schema 约束 AI 输出块结构 |
| OpenAI function calling | JSON schema 约束 AI 输出参数 |
| 任何 AI Agent 调工具 | 工具的参数 schema 约束输出 |
形式不同,原理一致:用受限的、类型化的、可枚举的原语集合,把 LLM 的概率性输出收敛成可校验、可复用、可计算的确定性产物。
理解了这个原理,你就能判断任何”AI 生成 X”的工具该用什么策略:
- X 形态可枚举 → DSL(约束生成)
- X 形态开放 → HTML / 自由格式(开放生成 + 兜底校验)
- X 需要二次计算 → 必须结构化
- X 需要跨次一致 → 必须上移视觉层
至于”web 上能用吗”——能用,但要分清你解决的是哪个问题。如果命中”AI 输出一致性 + 可计算性”,自造一个 DSL;如果只是”普通前端开发”,HTML/JSX + 组件库就够,别多包一层。复用 Qoder 的不是代码,是它的约束策略。