数据截至 (上游 commit 5a6a215cab71)
OpenUI (OpenUI Lang) — 架构与原理
30 秒导读: OpenUI 是一个「生成式 UI」框架。它的核心是一门叫 OpenUI Lang 的小语言——你让大模型用这门语言(而不是 JSON 或纯文本)回话,前端一边接收 token 一边解析、一边把界面画出来。比起 JSON,它的 token 用量少约一半;而且语言里内置了「变量、数据查询、按钮动作」,所以模型吐出来的不是死的界面截图,而是一个会自己取数、能交互、能被增量编辑的活页面。
本仓库克隆目录名是 crayon,但它实际就是 thesysdev/openui(npm 组织 @openuidev/*)。下文统一称 OpenUI。
1. 这是什么(零基础也能懂)
一句话定义
OpenUI 是一套全栈生成式 UI 框架:一门为流式输出设计的紧凑 UI 语言(OpenUI Lang)、一个把这门语言边流边渲染成 React 界面的运行时、外加几套现成的组件库与聊天外壳。
它解决谁的什么问题
假设你在做一个 AI 助手,希望模型的回答不只是一段文字,而是一张图表、一个表格、一个可填写的表单。你有两条老路:
- 让模型吐 JSON,前端再把 JSON 映射成组件。问题:JSON 又啰嗦(键名重复、引号括号一大堆)又费 token,流式时还很难「半个 JSON」就渲染。
- 让模型直接吐 HTML/JSX。问题:不可控、容易 XSS、组件不受你约束。
OpenUI 给的是第三条路:定义一个你允许模型使用的组件库 → 由这个库自动生成一段系统提示词 → 模型按提示词用 OpenUI Lang 回话 → 运行时边流边渲染。给谁用?需要做 AI 聊天/copilot/仪表盘前端,又想把模型输出限定在自己组件集里的工程师。
它能做什么
- 结构化、可流式的 UI 生成(token 比等价 JSON 少约 50%)。
- 受控渲染:模型只能用你注册过的组件,不认识的组件直接被丢弃。
- 活数据:语言里有
Query()(读)和Mutation()(写),界面能自己取数、定时刷新、按钮触发写操作。 - 交互与响应式:
$变量做双向绑定,下拉框一改,相关 Query 自动重取。 - 增量编辑:模型只吐「改了哪几行」,运行时按语句名合并,未触及的部分自动保留。
- 多框架:React / Vue / Svelte 绑定,外加 CDN bundle。
用起来什么样
模型不再回一段 markdown,而是回这样一段 OpenUI Lang(# 示意,展示语言长相):
root = Card([header, chart])
header = TextContent("本周销量")
sales = Query("get_sales", {days: 7}, {rows: []})
chart = BarChart(sales.rows.label, sales.rows.value)
读法:
- 每行是一条
名字 = 表达式语句;root是入口。 Card、BarChart是你库里定义的组件;参数按位置传(不是按名字)。sales = Query(...)声明一次数据获取;sales.rows.value把每行的value字段「拔」成一个数组喂给图表。- 模型先吐
root,界面外壳立刻出现;chart还没流到时先空着,流到后补上——这就是「渐进式 reveal」。
一句话直觉 / 类比
把它想成**「给大模型用的、专为流式设计的 JSX 方言」**:像 JSX 一样声明组件树,但语法砍到最瘦(省 token)、加了变量和数据查询(让界面活起来)、且每个 token 流进来都能被「半句也能渲染」地解析。
2. 顶层全景(它大概怎么转)
一条主线:从组件库到活界面
你写的组件库(Zod schema)
│ library.prompt()
▼
① 系统提示词 ───────────────► ② 大模型
(组件签名+语法规则+Query教学) │ 流式吐 OpenUI Lang
▼
┌──────────────────────┐
library.toJSONSchema() ───►│ ③ 流式解析器 │ 每来一个 chunk
(告诉解析器每个组件的参数) │ push(chunk)→ParseResult│ 重算一次
└──────────┬───────────┘
│ root: ElementNode 树
▼
┌──────────────────────┐
toolProvider(取数后端) ─►│ ④ 运行时 │
store(响应式 $变量) │ evaluator 求值 AST │
│ queryManager 取数缓存 │
└──────────┬───────────┘
│ 求值后的 props 树
▼
⑤ React Renderer → 活界面
怎么读这张图: 上半部分是「离线一次性」——库 → 提示词 → 模型。下半部分是「每个 token 都在跑」——解析 → 求值 → 渲染。两条信息从库里分叉:prompt() 喂给模型,toJSONSchema() 喂给解析器,它们必须对得上(同一个库)。
部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
| Library | 用 Zod 定义组件,产出提示词 + JSON Schema | packages/lang-core/src/library.ts |
| Prompt generator | 把库反向工程成系统提示词 | packages/lang-core/src/parser/prompt.ts |
| Lexer + Parser | 词法分析 + 把每条语句解析成 AST | packages/lang-core/src/parser/lexer.ts、parser.ts |
| Materialize | 把 AST 降级成 ElementNode 树,顺手校验 | packages/lang-core/src/parser/materialize.ts |
| Stream parser | 增量喂 chunk,每次返回最新 ParseResult | packages/lang-core/src/parser/parser.ts(createStreamParser) |
| Evaluator | 运行时把 AST 节点求值成真实值 | packages/lang-core/src/runtime/evaluator.ts |
| QueryManager | 执行 Query/Mutation、缓存、刷新、错误 | packages/lang-core/src/runtime/queryManager.ts |
| Store | 响应式 $变量 状态 | packages/lang-core/src/runtime/store.ts |
| Renderer | React 适配:把 ElementNode 画成组件 | packages/react-lang/src/Renderer.tsx |
仓库布局(monorepo)
核心全在 packages/lang-core(框架无关、无 React 依赖)。其余包都是它的「外壳」:
| 包 | 作用 |
|---|---|
@openuidev/lang-core | 解析器 + 提示生成 + 运行时求值 + 类型(本文主角) |
@openuidev/react-lang | React 渲染运行时(Renderer) |
@openuidev/react-headless | 无头聊天状态 + 流式适配器 |
@openuidev/react-ui | 现成聊天布局 + 两套内置组件库 |
@openuidev/vue-lang / svelte-lang | Vue / Svelte 绑定 |
@openuidev/cli(目录名 packages/openui-cli) | 脚手架 + 从库生成提示词/JSON Schema |
3. 阅读地图(建议顺序)
本项目复杂度集中在「一门小语言 + 它的流式解析 + 它的提示生成 + 它的运行时」四块,因此拆成四章,由浅入深:
-
01-language-and-grammar.md — 先认识这门语言本身:有哪些语句、表达式、
$变量、Query、@内置函数,以及「词法 → Pratt 表达式解析 → 语句分类」三步怎么把文本变 成 AST。先读这章,后面才有共同词汇。 -
02-streaming-parser.md — 这门语言最与众不同的地方:为流式而生。讲
autoClose怎么把半句补全、增量解析器怎么用 watermark 只处理新增、materialize怎么把 AST 降级成 ElementNode 并做校验、以及 hoisting(可前引用)如何造就「结构先出、数据后填」的渐进式 reveal。 -
03-prompt-generation.md — 反方向的一章:从组件库自动生成系统提示词。讲 Zod schema 怎么内省成
Card(children, title?)这种签名、提示词由哪些段落拼成、toolCalls/bindings等特性开关如何按需裁剪整段教学。这是整个框架「让模型听话」的关键。 -
04-runtime-evaluation.md — 最后是「活起来」的一章:运行时求值。讲 evaluator 怎么算
sales.rows.value这种表达式、queryManager 的取数/缓存/刷新/错误模型、store 的响应式$变量、Action 把按钮点击编排成有序步骤、以及 edit-mode 按语句名合并 + 垃圾回收。
如果你只想抓主线:01 → 02 已经能让你讲清「OpenUI Lang 是什么、为什么能流式」。要讲「模型为什么会按规矩输出」读 03,要讲「界面如何取数交互」读 04。