AI 数据通道:JSON·Markdown

一、元认知:LLM 输出的根本矛盾 #

LLM 是概率模型,每次生成都从巨大输出空间里采样。问题随之而来:当你需要一个稳定的、可校验的、视觉一致的产物时,概率模型的天然方差就是敌人。

这矛盾不是 Qoder 独有,是所有”让 AI 生成结构化产物”的场景都要面对的根本问题:

  • AI 生成 JSON → 可能缺字段、类型错
  • AI 生成 SQL → 可能语法错、注入风险
  • AI 生成代码 → 可能符号不存在、import 缺
  • AI 生成 UI → 可能样式不一致、布局错乱

解决思路有两条:

  1. 事后校验:让 AI 自由生成,然后用 schema / 编译器 / 类型系统过滤错误。
  2. 事前约束:限制 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:#ff0000color:redcolor:rgb(255,0,0) 三种写法等价,AI 会随机选——视觉抖动不可避免。

DSL 让模型只写语义 tone="success",渲染器统一映射到具体色值、字号、间距。审美决策从模型层上移到渲染层固化。模型只说”这是什么”,渲染器决定”长什么样”。

这背后是设计系统的核心理念:把审美决策固化为有限原语集合和语义化属性枚举。设计系统不是”组件库”,是”约束模型与人能做什么决策的契约”。

两个手段叠加的对比 #

维度原生 HTMLcanvas DSL
输出空间图灵完备,无限原语笛卡尔积,有限
决策维度结构 + 样式 + 行为 + 语义仅语义 + 数据
一致性来源依赖每次生成质量渲染器保证
类型约束TS 接口 + 枚举
校验需解析 HTML/CSS直接校验 props
安全可能 XSS白名单组件
Token 成本高(重复 boilerplate)低(声明式)
演进改 prompt 重新生成改设计系统,旧产物自动跟随

隐藏属性:可计算性 #

还有个常被忽略的原理:DSL 输出是结构化数据,可二次聚合

  • 跨任务汇总:把多次 Goal 执行的 Stat 加总
  • 对比两次执行:diff 两份 DSL 树
  • 抽取指标入库:Stat / Tag 的 tone 转成监控指标

HTML 是展示态,不是数据态,做不了这些。Goal 模式的报告未来要做 BI 分析,必须结构化。这点决定了 DSL 不只是”为了好看”,是”为了可计算”。

三、案例即原理:Qoder canvas 剖析 #

来看用户给的实际样例(节选):

1
2
3
4
5
6
<Grid columns={4} gap={16}>
<Stat value="1" label="新增方法" tone="success" />
<Stat value="2" label="新增 import" tone="info" />
<Stat value="0" label="Service/DAO 改动" tone="success" />
<Stat value="50" label="最大返回条数" tone="primary" />
</Grid>

这段代码体现了前面说的所有原理:

  1. 降维:模型只需决策 4 个 Stat 的 value / label / tone,不需要决策卡片样式、间距、颜色、布局——这些由 GridStat 组件内置。
  2. 语义而非视觉tone="success" 是语义层(”这是正向结果”),渲染器决定 success 长什么样。未来设计系统改色板,所有旧报告自动跟随。
  3. 可计算:4 个 Stat 是结构化数据,可以提取成 {改动量: {新增方法:1, 新增import:2, ...}} 入库。
  4. 安全:模型无法注入 onclick<script>,因为 Stat 不暴露这些 props。

再看验证清单表:

1
2
3
4
5
6
7
<Table
headers={['检查项', '结果', '证据']}
rows={[
['能力ID模糊匹配', <Tag tone="success">通过</Tag>, "SQL: id like ..."],
]}
rowTone={['success', 'success', ...]}
/>

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 只是这个问题的一种解法,它的核心策略是两条:

  1. 把开放生成降维成结构化填充(约束语法层)
  2. 把审美决策从模型层上移到渲染层(约束视觉层)

这两条策略不是 Qoder 发明的,是所有”AI 生成结构化产物”系统的通用原理:

工具 / 协议约束手段
Qoder canvas受限 JSX 原语 + tone 枚举
Vercel v0shadcn/ui 组件约束 AI 输出 JSX
BlockNote / Editor.js块 schema 约束 AI 输出块结构
OpenAI function callingJSON schema 约束 AI 输出参数
任何 AI Agent 调工具工具的参数 schema 约束输出

形式不同,原理一致:用受限的、类型化的、可枚举的原语集合,把 LLM 的概率性输出收敛成可校验、可复用、可计算的确定性产物

理解了这个原理,你就能判断任何”AI 生成 X”的工具该用什么策略:

  • X 形态可枚举 → DSL(约束生成)
  • X 形态开放 → HTML / 自由格式(开放生成 + 兜底校验)
  • X 需要二次计算 → 必须结构化
  • X 需要跨次一致 → 必须上移视觉层

至于”web 上能用吗”——能用,但要分清你解决的是哪个问题。如果命中”AI 输出一致性 + 可计算性”,自造一个 DSL;如果只是”普通前端开发”,HTML/JSX + 组件库就够,别多包一层。复用 Qoder 的不是代码,是它的约束策略