每一个做 AI 对话产品的团队,最终都会遇到同一个问题:用什么格式描述 AI 输出的 UI?有人选 Markdown——简单但无法表达交互组件。有人选 JSON——结构化但太重,大模型生成容易括号不匹配。有人选 HTML——标签冗长且 XSS 风险高。
TokUI 选择了一条不同的路:设计一门极简的 DSL,专为 AI 流式生成而优化。这篇文章拆解这门语言的设计取舍。
一、方括号语法:为什么不是尖括号
HTML 用尖括号 <tag> 有历史包袱——SGML 时代的遗产。但 AI 生成尖括号标签时有三个问题:标签名冗长(<button> 比 [btn] 多耗 Token)、闭合标签冗余(</button> 太长)、属性引号规则不一致(有时可省有时不行)。
TokUI 的 DSL 用方括号加极短标签名:
[btn tx:确认 v:primary clk:onSubmit]
[h1 产品概览]
[card tt:用户信息] 内容 [/card]
设计原则是一个字符能省就省。tx 代替 text,clk 代替 onclick,v 既是 value 又是 variant,ph 代替 placeholder。这不是随意缩写,而是在大模型 Tokenizer 的词表中反复验证后的选择——每个缩写都确保在主流模型(GPT、GLM、Claude)中是一个完整 Token。
从 JBoltAI 的实践经验来看,DSL 描述同一段 UI 的 Token 消耗约为 HTML 的 40%,约为 JSON 的 55%。在 AI 对话的流式场景下,省 Token 就是省延迟、省成本。
二、属性系统:冒号分隔与布尔简写
DSL 的属性格式是 key:value,空格分隔多个属性。两种特殊处理体现了工程精度。
布尔属性只写 key。 很多组件属性是开关式的——req(必填)、dis(禁用)、stripe(斑马纹)。写 req:true 纯属浪费 Token,直接写 req 即可生效:
[input l:用户名 ph:请输入 req]
[btn tx:提交 v:primary dis]
[table stripe]
解析器内部有一个 BOOLEAN_ATTRS 集合维护所有布尔属性名,遇到这些 key 不需要 value 就设为 true。
引号内值可含空格和特殊字符。 普通属性值以空格为分隔符,但用双引号包裹的值可以包含空格、逗号甚至方括号:
[btn tx:"含空格的按钮文本" v:"primary,pill"]
[stat tt:总营收 v:"¥1.2亿" trend:up]
从 JBoltAI 踩过的坑来看,最容易出错的是属性值里含冒号的情况——链接地址中的 :// 会被误解析为嵌套属性。解决方式是引号包裹整个值。
三、容器与自闭合:对标 HTML 但更宽容
组件分两类:自闭合和容器。自闭合组件是一个原子标签,不包含子节点。容器组件必须开闭配对,子节点放在中间。
[h1 这是自闭合标题]
[card tt:卡片标题]
[p 这是容器的子节点]
[btn tx:操作]
[/card]
关键设计决策是容错。AI 写 DSL 时经常带 HTML 肌肉记忆——不写闭合标签、容器跨行漏写 ]。TokUI 的解析器不是零容忍报错,而是隐式补全:
[list]
[item 第一项]
[item 第二项]
[item 第三项]
[/list]
上面三个 [item] 都没有 [/item],但解析器在新 item 开标签时会自动关闭前一个。这是对标 HTML <li> 的裸标签语义。从 JBoltAI 踩过的坑来看,早期严格校验时约 15% 的 AI 输出因标签不闭合而渲染失败,容错机制把这个数字降到接近零。
四、变体系统:白名单而非自由拼接
传统前端组件库的 class 是开放的——开发者可以随意拼接 className。但在 AI 生成内容的场景下,自由 class 意味着 XSS 注入风险。
TokUI 用 VARIANTS 白名单约束变体。每个组件预定义允许的变体名:
[btn v:primary 主按钮]
[btn v:"danger,pill" 危险圆角]
[btn v:"success,sm" 成功小尺寸]
[h1 v:underline 带下划线标题]
[p v:muted 弱化文本]
v:primary 渲染为 CSS 类 tokui-btn--primary。如果 AI 输出了白名单中不存在的变体名,解析器静默丢弃,不报错也不注入。这既保证了视觉一致性,也堵死了 class 注入攻击面。
五、原始内容模式:代码块内的方括号不解析
代码块、Markdown、diff、terminal 这些容器内部的内容是原始文本,里面的 [ 不应该被解析为标签。否则一段含 arr[0] 的代码会让解析器崩溃。
[code js]
const arr = [1, 2, 3];
arr.forEach((item, i) => {
console.log(`arr[${i}] = ${item}`);
});
[/code]
进入原始内容模式后,解析器只认对应的闭标签 [/code],中间所有 [ 都视为字面文本。这个设计看似简单,但在流式场景下极其复杂——闭标签可能被 SSE 分块劈成 [/co 和 de],解析器必须回持半截闭标签不发,等下一块到齐再判断。
六、动态更新协议:upd 的精准定位
传统前端更新 UI 的方式是重新渲染整棵 DOM 树。TokUI 的 upd 组件只更新目标元素的值和状态,不触碰其他 DOM:
[progress id:cpu v:35 l:CPU使用率]
[upd id:cpu v:67]
[upd id:cpu v:89 status:error]
upd 通过 id 精准定位已渲染的元素,单次更新耗时在毫秒级。以 JBoltAI 的实践来看,这个机制在实时监控看板场景下表现极好——每秒推送几十次 upd 更新,页面流畅无卡顿。
设计取舍总结
| 设计决策 | 选择 | 替代方案 | 理由 |
|---|---|---|---|
| 标签分隔符 | [...] | <...> 或 JSON | 短、省 Token、AI 友好 |
| 属性格式 | key:value | JSON key-value | 无逗号无引号开销 |
| 布尔属性 | 只写 key | key:true | 省 6 个字符 |
| 容错策略 | 隐式闭合 | 严格报错 | AI 输出 15% 失败率→0 |
| 变体约束 | 白名单 | 自由 class | 防 XSS 注入 |
| 原始内容 | 模式切换 | 转义处理 | 流式逐字渲染的基石 |
以 JBoltAI 的工程经验来看,DSL 设计的核心矛盾是"表达能力 vs 生成可靠性"。JSON 表达力强但 AI 生成易出错,Markdown 可靠但无法表达交互。TokUI DSL 的选择是:用最小的语法集覆盖最多的组件形态,同时容忍 AI 输出的所有毛刺。 这不是理论上的最优解,是在真实 AI 对话产品中打磨出来的工程最优解。




