跳到主要内容

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:587)。


3. 组件签名:Zod schema 怎么变成 Card(children, title?)

它要解决的小问题: 模型得知道每个组件吃几个参数、什么类型、哪个可选。

buildComponentSpecs(library.ts:318)对每个组件,内省它的 Zod schema 形状(analyzeFieldsbuildSignature),产出一行签名字符串。难点在「把 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:587)把提示词当成一串 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

每段是一个独立函数(syntaxRulesquerySectionactionSection…),职责单一。两段特别值得看:

  • 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:591-594),且有依赖关系的默认值:

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:597-599),usesBindings 看有没有 $binding(prompt.ts:544-545)。库里没用到的能力,提示词里就不教——提示词随库的能力自动伸缩


6. 工具清单:把后端工具喂给模型

如果传了 tools,renderToolsSection(prompt.ts:473)把每个工具渲染成签名行。工具可以是简单字符串名,也可以是带 schema 的 ToolSpec(形状借鉴了 MCP 的 tool schema:name/description/inputSchema/outputSchema/annotations,prompt.ts:9-15)。对有 outputSchema 的工具,还会用 defaultForSchema 生成一份「最小默认值」,教模型拿来当 Query 的 defaults(prompt.ts:500-511),这样界面在真数据到达前就能渲染骨架。

段尾有一句硬规则:只准用列出的工具,不准编造工具名(prompt.ts:514-516)——配合运行时 tool-not-found 错误回灌,构成「教 + 纠错」的闭环。


7. 巧妙之处

  • 提示词与解析器同源。 都从一个 Zod 库派生(prompt()toJSONSchema()),结构上消灭漂移(library.ts:createLibrary)。
  • 内置函数文档即注册表。 提示词的 Built-in 段从 BUILTINS 自动生成,运行时实现和提示文档永远一致(prompt.ts:builtinFunctionsSection)。
  • 提示词随能力伸缩。 三个开关 + 两个自动检测,让没用到的特性整段不出现,省 token 又降错误面(prompt.ts:generatePrompt)。
  • 教模型自检。 末尾 ## Final Verification 列出「root 在第一行吗?每个引用都定义了吗?每个 $binding 都用上了吗?」让模型自己走查(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.tscreateLibraryLibrary.prompt
定义组件packages/lang-core/src/library.tsdefineComponent
Zod 内省成类型注解packages/lang-core/src/library.tsresolveTypeAnnotationbuildSignature
提示词拼装packages/lang-core/src/parser/prompt.tsgeneratePrompt
各段落生成器packages/lang-core/src/parser/prompt.tssyntaxRulesquerySectionactionSectionstreamingRules
工具清单packages/lang-core/src/parser/prompt.tsrenderToolsSectionToolSpec
响应式标记packages/lang-core/src/reactive.tsmarkReactiveisReactiveSchema