OpenUI 是一个生成式 UI 库,它允许语言模型以一种名为 openui-lang 的声明性格式生成完整的交互式 UI。代理返回的不是聊天消息,而是一个包含卡片、图表、表格、选项卡和表单的组件树,由 Renderer 将其转换为真实的 React UI。 这种集成非常适合数据密集型输出,如报告、仪表板和数据探索器,模型在其中同时充当数据分析师和 UI 设计师的角色。

工作原理

  1. 生成系统提示词: 启动时调用一次 openuiLibrary.prompt();它会生成一个完整的 openui-lang 参考文档,模型使用它来编写有效的组件树。
  2. 首次消息时注入: 当新对话开始时,将系统提示词作为第一条系统消息发送。
  3. 模型编写 openui-lang: 模型响应的是像 root = Stack([header, kpis, chart]) 这样的程序,而不是纯文本。
  4. 使用 Renderer 渲染: 将文本传递给 OpenUI 的 Renderer 和组件库;它会解析并渲染该树。

安装

OpenUI 需要 React 19+ 和 zustand。前端代码仅限 React;LangGraph 代理后端可以用 TypeScript 或 Python 编写。

导入组件样式

在您的 CSS 入口点或直接在您的根组件中导入 OpenUI 的打包样式:

生成系统提示词

OpenUI 提供了一个 openuiLibrary.prompt() 函数,用于生成完整的 openui-lang 参考文档,包含所有组件签名、语法规则、流式传输提示和示例。在模块加载时调用一次:
preamble 覆盖了默认的角色设定。添加 additionalRules 以注入特定于任务的约束:

通过 useStream 注入系统提示词

在每个新线程的第一条消息中发送系统提示词。检查 stream.messages.length === 0 以检测新线程,并在开头添加一个 system 消息:

使用 Renderer 渲染

将 AI 消息的文本内容直接传递给 Renderer,并附上 openuiLibrary:
在活动流期间传递 isStreaming={true},以便 Renderer 在定义到达时优雅地处理未解析的引用。

openui-lang 格式

模型编写的是程序而不是 JSON 规范。每个语句都是一个赋值;root 是入口点。官方提示词会教导模型这种格式,包括变量提升——先写 root,以便 UI 外壳立即出现:
启用变量提升(推荐)后,root 行会首先被写入,因此页面结构会立即出现,并且每个部分会在模型定义时逐步填充。

渐进式渲染工具

将 useStream 直接连接到 Renderer 会导致在每个流式传输的令牌上重新渲染,并在每个响应中产生数百次无操作的重新解析。当图表组件的数据尚未到达时,这会导致它们崩溃。下面的工具解决了这些问题: 将完整的代码块复制到您的项目中,并将 stable 传递给 <Renderer>:

后续查询

OpenUI 的 Button 组件支持 continue_conversation 动作类型。当用户点击后续查询按钮时,Renderer 会触发 onAction,上面的 AIMessageView 会将按钮的标签作为下一条用户消息提交,这与在输入框中输入消息的代码路径完全相同。 通过在系统提示词的 additionalRules 中添加“探索更多”部分到每个报告中:

使用 Deep Agents 构建并行仪表板

上面的流程将一个 OpenUI 程序渲染到一个界面。对于更丰富的应用程序,Deep Agents 协调器可以委托给多个专业代理,每个代理通过单个 useStream 连接并发地流式传输它们自己的 OpenUI 面板。OpenUI 并行仪表板示例 将一个仪表板简报转换为独立流式传输的 Stripe、PostHog、GitHub 和 Calendar 面板,无需自定义图或流多路分解代码。

共享一个 OpenUI 库

在服务器端(用于生成面板提示词)和客户端(作为 Renderer 属性)使用相同的库对象,这样模型被告知的组件总是与渲染器可以绘制的组件相匹配:
library.ts

定义协调器和面板代理

createDeepAgent 构建一个协调器,其唯一职责是路由:它选择简报所需的专业人员,并在一条消息中发出它们所有的 task() 调用,以便面板可以并发运行。每个面板子代理共享一个预先生成的 OpenUI 系统提示词,并且仅接收其数据领域的工具。
agent.ts
协调器从不编写 openui-lang。每个面板代理调用其工具,然后返回一个完整的程序,该程序以 root 开头,以便其渲染器可以在模型完成其余语句之前进行绘制。

注册图

将 langgraph.json 指向导出的协调器:
langgraph.json

在前端发现和渲染面板

一个 useStream 连接承载协调器和每个面板。面板不是硬编码的:每个并行的 task() 调用都会显示为一个 stream.subagents 快照。对于每个快照,限定一个 useMessages(stream, snapshot) 投影,以便面板仅接收其自身子代理的消息,然后将其 OpenUI 程序输入到一个隔离的 Renderer 中:
App.tsx
因为 SDK 将子代理的令牌事件排除在根存储之外,并且每个 Panel 都基于其快照标识进行 memo,所以一个面板的令牌永远不会重新渲染另一个面板。

最佳实践

  • 在模块加载时生成系统提示词: 不要在 React 组件内部;提示词有几千字节,应该只计算一次。
  • 仅在新线程中注入系统提示词: 检查 stream.messages.length === 0,并在后续轮次中跳过注入,以避免在线程历史记录中重复提示词。
  • 使用变量提升顺序: 先写 root = Stack([...]);UI 外壳会立即出现,并且各个部分会在模型定义每个部分时逐步填充。
  • 基于完整语句进行门控: 避免在每个令牌上重新渲染 Renderer;仅当一个完整的语句(name = ComponentCall(...))到达时才更新。
  • 渲染前验证图表数据: 图表组件需要在包含在稳定快照中之前,定义好它们的 Series 和标签数组。
  • 保持驼峰式变量名: openui-lang 解析器只接受驼峰式标识符;在系统提示词的 additionalRules 中强调这一点。
  • 在一条消息中委托面板: 当向 Deep Agents 专业人员扇出时,在一条协调器消息中发出所有的 task() 调用,以便面板并发流式传输,而不是一次一个。
  • 将每个面板限定到其子代理: 从 stream.subagents 发现面板,并将每个快照传递给 useMessages(stream, snapshot),以便面板仅渲染其自身子代理的输出。