数据截至 (上游 commit 5a6a215cab71)
OpenUI Lang — 提示词生成
本章讲什么: 前两章是「模型已经吐出 OpenUI Lang 之后怎么解析」。这一章是反方向:OpenUI 怎么从你的组件库 自动写出一段系统提示词,把这门语言、可用组件、Query/Action 用法全教给模型。这是整个框架「让模型听话且与解析器对齐」的关键。
1. 为什么要自动生成提示词
你当然可以手写一大段「你是个会用 OpenUI Lang 的助手,可用组件有 Card、Table……」的提示词。但有两个问题:
- 会漂移。 你给组件加了个参数,忘了同步提示词,模型就会用错签名。
- 重复劳动。 每套组件库都要重写一遍语法教学。
OpenUI 的解法:提示词从库自动生成。你只管用 Zod 定义组件,library.prompt() 反向工程出签名与规则。最关键的是——提示词和解析器的 ParamMap 同源于一个库,从结构上保证「教给模型的签名」与「解析器认的参数」一致(见 [02 章] compileSchema)。
2. 一步看清:库怎么变成提示词
createLibrary({ components: [Card, Table, ...] })
│
├── toJSONSchema() ──► ParamMap ──► 解析器(第 02 章)
│
└── prompt(options) ─────────────────────────────┐
▼
buildComponentSpecs: Zod 内省每个组件
Card.props.shape ──► "Card(children: Component[], title?: string)"
│
▼
generatePrompt(spec): 按开关拼接 N 个段落 ──► 一大段系统提示词
入口是 library.prompt(options)(library.ts:375),它组一个 PromptSpec 再交给 generatePrompt(parser/prompt.ts:600)。
3. 组件签名:Zod schema 怎么变成 Card(children, title?)
它要解决的小问题: 模型得知道每个组件吃几个参数、什么类型、哪个可选。
buildComponentSpecs(library.ts:318)对每个组件,内省它的 Zod schema 形状(analyzeFields → buildSignature),产出一行签名字符串。难点在「把 Zod 类型翻成人/模型都好读的类型注解」,由 resolveTypeAnnotation 递归完成(library.ts:217):
| Zod | 提示词里的类型 |
|---|---|
z.string() | string |
z.enum(["a","b"]) | "a" | "b" |
z.array(Child.ref) | Child[] |
标了 .optional() | 参数名后加 ? |
reactive(z.string()) | $binding<string> ← 双向绑定标记 |
| 子组件 schema | 解析回它注册的组件名 |
那个 $binding<...> 前缀很关键(library.ts:222-223):它告诉模型「这个参数能接一个 $变量 做双向绑定」,实现靠 reactive.ts 的 WeakSet 标记(框架适配层调 markReactive,内省层用 isReactiveSchema 查)。
子组件名怎么解析?库创建时用一个 Zod registry 给每个组件 schema 登记 id(library.ts:359);内省时先查 registry,查不到再退回 defineComponent 打的 WeakMap 标签(library.ts:184-197,getSchemaId)。
4. 提示词由哪些段落拼成
generatePrompt(prompt.ts:600)把提示词当成一串 parts 拼接。完整(全特性开启时)的骨架:
Preamble(你必须只输出 openui-lang)
## Syntax Rules ← 语句/位置参数/可达性/表达式
## Component Signatures ← 上一节生成的签名,可按 group 分组
## Built-in Functions ← @Count/@Sum/@Each...(仅表达式开启时)
## Query — Live Data ← 仅 toolCalls 开启
## Mutation — Write Ops ← 仅 toolCalls 开启
## Action — Button Behavior ← 仅组件用了 ActionExpression
## Interactive Filters/Forms← 仅 toolCalls && bindings
## Data Workflow ← 仅 toolCalls
## Available Tools ← 仅传了 tools
## Hoisting & Streaming ← 教「先写 root」
## Examples / Edit / Inline ← 按 options
## Important Rules + Final Verification
每段是一个独立函数(syntaxRules、querySection、actionSection…),职责单一。两段特别值得看:
- Built-in Functions 段(
prompt.ts:145)直接从BUILTINS注册表生成——@${b.signature} — ${b.description}逐条列出。这意味着[01 章]运行时用的内置函数和提示词里教的,是同一份数据(builtins.ts:BUILTINS),不会漂移。这是「single source of truth」模式的典范。 - Hoisting & Streaming 段(
prompt.ts:333)明确教模型最佳语句顺序:root第一 →$变量→ Query → 组件 → 叶子数据,这样流式 reveal 最顺。
5. 特性开关:按需裁剪整段教学
它要解决的小问题: 不是每个应用都 需要 Query、双向绑定。把用不到的教学塞进提示词只会浪费 token、增加模型犯错面。
generatePrompt 开头解析三个布尔开关(prompt.ts:604-607),且有依赖关系的默认值:
hasTools = 是否传了 tools
toolCalls = options.toolCalls ?? hasTools // 有工具默认开
bindings = options.bindings ?? toolCalls // 开了工具默认也开绑定
supportsExpressions = toolCalls || bindings // 任一开则启用表达式/内置函数
于是提示词是自适应的:
| 你的库 | 提示词里会出现 |
|---|---|
| 纯静态组件,无工具 | 只有语法 + 组件签名 + 流式规则(最瘦) |
| 传了 tools | 多出 Query/Mutation/Data Workflow/工具清单 |
组件用了 reactive() | 多出 $binding 说明、Interactive Filters、Forms |
| 组件用了 ActionExpression | 多出 Action 段(@Run/@Set/@OpenUrl…) |
后两个不是靠 option,而是扫描组件签名自动检测:usesActionExpression 看签名里有没有 ActionExpression(prompt.ts:610-612),usesBindings 看有没有 $binding(prompt.ts:556-557)。库里没用到的能力,提示词里就不教——提示词随库的能力自动伸缩。
6. 工具清单:把后端工具喂给模型
如果传了 tools,renderToolsSection(prompt.ts:485)把每个工具渲染成签名行。工具可以是简单字符串名,也可以是带 schema 的 ToolSpec(形状借鉴了 MCP 的 tool schema:name/description/inputSchema/outputSchema/annotations,prompt.ts:9-15)。对有 outputSchema 的工具,还会用 defaultForSchema 生成一份「最小默认值」,教模型拿来当 Query 的 defaults(prompt.ts:511-520),这样界面在真数据到达前就能渲染骨架。
段尾有一句硬规则:只准用列出的工具,不准编造工具名(prompt.ts:527)——配合运行时 tool-not-found 错误回灌,构成「教 + 纠错」的闭环。
7. 巧妙之处
- 提示词与解析器同源。 都从一个 Zod 库派生(
prompt()与toJSONSchema()),结构上消灭漂移(library.ts:createLibrary)。 - 内置函数文档即注册表。 提示 词的 Built-in 段从
BUILTINS自动生成,运行时实现和提示文档永远一致(prompt.ts:builtinFunctionsSection)。 - 提示词随能力伸缩。 三个开关 + 两个自动检测,让没用到的特性整段不出现,省 token 又降错误面(
prompt.ts:generatePrompt)。 - 教模型自检。 末尾
## Final Verification列出「root 在第一行吗?每个引用都定义了吗?每个 $binding 都用上了吗?每个可见的 filter $binding 都出现在至少一个 Query 的 args 里吗?」让模型自己走查(prompt.ts:importantRules)。
8. 边界与局限
- 提示工程是「软约束」。 位置参数、类型、可达性都靠提示词文字劝模型,解析器只在结构层兜底(丢弃/记错),不能保证模型 100% 守规矩。
- 签名翻译有兜底。 遇到无法识别的 Zod 类型(tuple/date 等),
resolveBaseType退化成any(library.ts:283-284)——模型看到的是param: any,不够精确。 - 强制 Zod 4。 用 Zod 3 schema 会在
defineComponent直接抛错(library.ts:28-37,assertV4Schema)。
9. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 库 + 提示入口 | packages/lang-core/src/library.ts | createLibrary、Library.prompt |
| 定义组件 | packages/lang-core/src/library.ts | defineComponent |
| Zod 内省成类型注解 | packages/lang-core/src/library.ts | resolveTypeAnnotation、buildSignature |
| 提示词拼装 | packages/lang-core/src/parser/prompt.ts | generatePrompt |
| 各段落生成器 | packages/lang-core/src/parser/prompt.ts | syntaxRules、querySection、actionSection、streamingRules |
| 工具清单 | packages/lang-core/src/parser/prompt.ts | renderToolsSection、ToolSpec |
| 响应式标记 | packages/lang-core/src/reactive.ts | markReactive、isReactiveSchema |