进阶 📋 6 个步骤 第 625 / 626 篇

A2UI 1.0 稳定版落地实操:Genkit 中间件、Lit 渲染器、双向 action 与 v0.9/1.0 版本口径核验

A2UI 1.0 于 2026 年 11 月 11 日成为生成式 UI 协议稳定版,模型描述界面、宿主用自己注册的组件渲染。本教程用 @genkit-ai/a2ui 0.7.0 的一行 a2ui() 中间件让 Genkit Agent 吐出界面消息,用 @a2ui/lit 消费流式 envelope,接上表单取值与 actionToMessage 回传,再处理自定义 catalog、CSP 与不可信属性。插件默认协议版本 v0.9 与 1.0 稳定版的口径差异单列核验,不臆造升级参数。

2026.11.15· 18 分钟阅读· 约 3508 字· 🪟 A2UI 1.0 / 🧩 @genkit-ai/a2ui

到 2026 年 11 月,大多数 Agent 仍只会回一大段文字:让用户从三套方案里挑一个、填一张确认表单、核对一张订单,都得靠人把文字再抄一遍。A2UI 1.0 于 11 月 11 日成为该生成式 UI 协议的稳定版:模型描述「要显示什么」,宿主用自己注册过的原生组件把它画出来。本教程走一条能在本机复现的落地路线:用 @genkit-ai/a2ui 0.7.0 一行中间件让 Genkit Agent 吐出界面消息,用 @a2ui/lit 渲染器消费流式 envelope,接上表单与按钮回传,再把版本口径、表单取值和不可信内容三处真实坑点逐个核掉。

🧩 适合谁:用 Node.js / TypeScript 写 Agent 服务、又要在浏览器里给用户一张可点击卡片的全栈开发者。前置准备:Node.js 与 npm、一个可新建的练习目录、一个能跑 Express 与 Vite 的本地端口;服务端若真要调模型,需要按 Genkit 官方说明为 googleAI() 配置模型凭据。官方样例在 Genkit 仓库的 js/testapps/a2ui(Express 后端加 Vite 与 Lit 前端),可直接拿来对照。本教程不臆造凭据环境变量名:只做渲染层预演时可以不接模型;接模型会产生真实调用费用,请把 Key 放在环境变量里,不要写进会提交的代码。

先分清:协议稳定,不等于每个包都在 1.0

A2UI 的分工是「智能体描述界面、宿主负责渲染」:模型发的是声明式的流式 JSON,客户端只从自己批准过的组件目录里取组件来画,业务动作与权限仍留在应用代码里。1.0 版新增双向函数调用、无障碍支持与扩展元数据,官方列出的渲染器覆盖 React、Lit、Angular、Flutter、SwiftUI 与 Jetpack Compose,Agent SDK 有 Python、TypeScript、Dart;Web 与 Flutter 渲染器还能跑在 MCP Apps 宿主里。

你读到的版本号实际含义本教程怎么处理
A2UI 1.0协议稳定版(2026-11-11 宣布)作为背景与升级目标,不冒充插件默认行为
@genkit-ai/a2ui 0.7.0npm 上的 Genkit 插件,状态标注为 experimental主线按它的 README 原样落地
插件 version 默认 v0.9 stamped 在 envelope 上的协议版本客户端导入路径跟着用 /v0_9,两侧对齐
Compose / Gemini Enterprise公开报道仍停留在 0.9.1,1.0 支持尚在推进写进风险提示,不假设全平台版本一致

这处版本错位是本轮最容易踩的坑:npm README 的 Options 表把 version 默认值写成 'v0.9',客户端示例也从 @a2ui/web_core/v0_9 与 @a2ui/lit/v0_9 导入;而协议侧已经发布 1.0,社区文章里能看到 @a2ui/web_core/v1_0 的 React 用法。README 没有列出 version 的全部可选值,因此不要凭猜测把示例里的 v0_9 改成 v1_0。正确做法是先用 npm view 核对已安装包的版本与导出路径,让服务端与渲染器用同一套版本;需要 1.0 时以该版本官方文档给出的导入路径为准。

Step 1:核对包版本,建一个干净的练习工程

1 先把「装了什么」查清楚

官方插件页会随发布更新,别把教程里看到的版本号当成命令前提。先查 registry 上的版本,再建目录并锁住依赖,后面一旦出问题就能说清是哪个包的哪个版本。

# 查当前 registry 版本(不安装,只读)
npm view @genkit-ai/a2ui version
npm view @a2ui/lit version
npm view @a2ui/web_core version

# 建练习工程
mkdir a2ui-demo && cd a2ui-demo
npm init -y

# 可选:让编码 Agent 先读 Genkit JS 官方技能,减少旧 API 误用
npx skills add genkit-ai/skills --skill developing-genkit-js

把 npm view 的输出和你最终写进 package.json 的版本记在笔记里。若团队要求可复现,请用锁文件固定版本;插件本身标注 experimental,升级前应先在练习工程里跑通再合入主分支。

💡 官方样例 js/testapps/a2ui 是「Express 后端 + Vite 与 Lit 前端」的完整可跑工程,包含 MarkdownRenderer 与字体的接线。读它的目录结构比从零猜项目布局省时间,本教程的命令与代码片段都沿用 README 给出的形式。

Step 2:装服务端插件与渲染器三件套

2 服务端一个包,浏览器侧三个包

服务端集成只有一处:把 a2ui() 放进 Agent 的 use 数组。浏览器侧除渲染器外还需要共享内核与 Markdown 渲染依赖,另外要加载 Material Symbols Outlined 字体,否则图标会显示成字面名称。

# Genkit 侧:A2UI 中间件
npm i @genkit-ai/a2ui

# 浏览器侧:Lit 渲染器 + 共享内核 + Markdown 渲染
npm i @a2ui/lit @a2ui/web_core @a2ui/markdown-it

# 字体在页面里引入(官方要求 basic catalog 的 Icon 靠字体连字渲染)
# https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined

# 装完复核实际版本与导出路径是否匹配
npm ls @genkit-ai/a2ui @a2ui/lit @a2ui/web_core

README 还点名两个宿主侧条件:Text 的标题变体走 Markdown,需要通过 Lit context 提供一个 MarkdownRenderer(可用 @a2ui/markdown-it 支撑);Icon 组件按字体连字渲染,需要 Material Symbols Outlined。缺了前者标题会显示成字面的 ##,缺了后者图标只剩名字。

这一步不涉及任何界面权限:装的是「能画什么」,不是「能做什么」。组件目录限制模型可以放哪些控件,但动作校验、身份认证、鉴权、数据访问与传输安全仍是应用自己的责任。别把「模型只发了声明式 JSON」误读成「业务操作已经安全」。

Step 3:在服务端加一行中间件,让模型吐界面消息

3 defineAgent 与一次性 generate 两种写法

插件的工作方式是把目录能力注入系统提示,拦截模型输出里的 a2ui 代码块,校验后改写成规范的 a2ui data part。它承载的 mime 类型是 application/json 之外的专用通道 application/a2ui+json,数据形如 { envelopes }。

import { genkit } from 'genkit/beta';
import { googleAI } from '@genkit-ai/google-genai';
import { a2ui } from '@genkit-ai/a2ui';

const ai = genkit({ plugins: [googleAI()] });

// 目录能力默认注入 system;默认目录 id 是 'basic'
export const uiAgent = ai.defineAgent({
  name: 'uiAgent',
  model: 'googleai/gemini-flash-latest',
  system: 'You help users. Render UI when it is clearer than prose.',
  use: [a2ui()],
});

// 一次性生成也可以挂同一个中间件
const res = await ai.generate({
  prompt: 'Show me the weather in Tokyo',
  use: [a2ui()],
});

a2ui() 的可选项都值得记下来:catalog 默认 'basic';instructions 默认 'system',设成 'none' 就不注入目录能力;validate 默认 'warn'(记录并丢弃不合规块),'strict' 会抛错,'off' 跳过检查;surfaceId 默认每个 surface 一个新 UUID,也可传固定字符串复用;version 默认 'v0.9'。

💡 想少踩「模型不吐界面」的坑,先别急着调提示词,用 validate: 'strict' 跑一轮:结构不对的 envelope 会立刻抛错,能快速区分「模型没生成」和「生成了但被 warn 模式悄悄丢掉」。

strict 不是安全边界:README 明确写它只校验 envelope 结构与组件类型名,不校验组件属性和数据模型里的值。模型可控的 Image.url、会被渲染成 HTML 的行内 Markdown 都不会被挡住。要收紧,得在 a2ui() 之后再加自己的模型中间件检查并改写,或在渲染器侧做转义与净化。

Step 4:在浏览器消费流式 envelope,把 surface 画出来

4 MessageProcessor 是协议状态的持有者

客户端的活儿是三段:把模型返回的每个 chunk 里的 a2ui 信封摘出来、喂给 MessageProcessor、在 surface 创建时把它交给页面上的渲染组件。README 给出的 remoteAgent 写法如下,导入路径中的 v0_9 必须和上一步插件的协议版本对齐。

import { MessageProcessor } from '@a2ui/web_core/v0_9';
import { basicCatalog } from '@a2ui/lit/v0_9';
import '@a2ui/lit/v0_9'; // 注册 a2ui-surface 与基础组件
import { a2uiEnvelopesFromParts } from '@genkit-ai/a2ui/client';
import { remoteAgent } from 'genkit/beta/client';

const processor = new MessageProcessor([basicCatalog]);
processor.onSurfaceCreated((s) => {
  document.querySelector('a2ui-surface').surface = s;
});

const chat = remoteAgent({ url: '/api/uiAgent' }).chat();
const turn = chat.sendStream('weather in Tokyo');
for await (const chunk of turn.stream) {
  if (chunk.text) appendProse(chunk.text);
  const envelopes = a2uiEnvelopesFromParts(chunk.raw.modelChunk?.content);
  if (envelopes.length) processor.processMessages(envelopes);
}

这里体现「流式」的含义:客户端不必等模型把整段回答说完,消息到达就增量处理,先出结构再补数据。若你没用完整的 remoteAgent,@genkit-ai/a2ui/client 还提供轻量的 streamA2uiAgent({ url, message, sessionId }) 异步生成器,产出 { type: 'text' } 与 { type: 'envelopes' } 两类事件。

别漏掉错误与中断状态:增量渲染意味着你要自己处理半截消息、非法数据与连接中断。README 把 action 校验、传输安全等留给应用;生产环境请给 surface 绑定明确的路由或面板,避免一个后台任务把用户正在填写的表单覆盖掉。

Step 5:让按钮真的回话,并把表单取值接上

5 actionToMessage 与两处表单绑定

用户点击按钮时渲染器发出 A2uiClientAction;把它转成下一轮 Agent 输入,界面才从「展示」变成「对话」。插件会把动作名当作用户消息,并把含 context 的完整动作作为 a2ui data part 附回去。

import { actionToMessage, type A2uiClientAction } from '@genkit-ai/a2ui/client';

const processor = new MessageProcessor([basicCatalog], (action) => {
  const turn = chat.sendStream({ message: actionToMessage(action) });
  // 接着按上一步的方式消费 turn.stream
});

// 表单取值要在两处对齐(README 给出的规则):
// 1. 输入组件的 value 绑定到数据模型路径,例如 {"path":"/email"}
// 2. 提交按钮的 action.event.context 里回显同样的路径
// 少了任意一处,动作到达时 context 是空的。

TextField、CheckBox、Slider 都不会自动把自己的值送出去。官方目录注入的提示会教模型做这两步绑定,但你仍应在客户端校验:提交时打印一次 action 的 context,确认字段路径与模型生成的路径逐字一致。哪些字段可编辑、哪些动作被允许、哪些响应能更新哪个 surface,都应由应用代码决定,而不是听模型消息指挥。

💡 一个省事的排障顺序:先看 processor.onSurfaceCreated 有没有触发,再看 a2uiEnvelopesFromParts 返回的数组长度,最后才看按钮回调。三段分别对应「渲染器没注册」「信封没抽出来」「动作没回传」,逐段定位比整页重写快得多。

Step 6:换成自己的组件目录,并收紧信任边界

6 自定义 catalog + CSP + 密钥隔离

默认 'basic' 目录开箱可用,但产品往往只想让模型摆自己设计系统里的组件。插件把目录当成一个 id 从 Genkit registry 解析,支持从 JSON 文件加载或在内存里定义。

// my-catalog.json
{
  "id": "https://my-app.org/catalogs/custom.json",
  "components": [
    {
      "name": "Banner",
      "description": "Displays a prominent alert banner at the top of a section.",
      "props": "title: string (required); severity?: info|warning|error."
    },
    {
      "name": "Text",
      "description": "Displays a plain or inline-markdown text run.",
      "props": "text: string (required); variant?: body|caption."
    }
  ]
}

// 服务端注册
import { loadCatalog } from '@genkit-ai/a2ui';
await loadCatalog(ai, { id: 'my-catalog', file: './my-catalog.json' });

// 引用时按 id 取
export const uiAgent = ai.defineAgent({
  name: 'uiAgent',
  model: 'googleai/gemini-flash-latest',
  use: [a2ui({ catalog: 'my-catalog' })],
});

目录条目只有三项:id(全局不重复的 URI,会作为 catalogId 出现在 createSurface 上)、components 数组,以及每项的 name、description、props。props 是给模型看的一段简短文字,刻意保持精简以少占系统提示。也可以用 loadCatalog(ai, { id, catalog }) 直接传内存对象。

把模型输出当成不可信输入:README 要求渲染器与自定目录自己负责属性转义与净化;服务端应配好 CSP,把 img-src 等指令限制在可信来源,避免模型给出的图片或链接地址加载外部内容、造成数据外带;任何绑定进数据模型的值都可能随动作的 context 回显,因此不要把密钥、令牌或个人敏感值放进数据模型。自定义目录的渲染组件同样要做属性净化。

做到哪一步算成功:验收清单与交付前自查

按下面四条逐项打勾,能全部复现就说明「模型消息 → 信封 → 原生组件 → 用户动作回传」这条链路通了,而不是只装了几个 npm 包。

验收项预期现象
版本对齐npm ls 显示的包版本与代码里的导入路径一致,服务端与渲染器用同一协议版本
界面出现onSurfaceCreated 触发,页面上的 a2ui-surface 渲染出卡片、文本与按钮
动作回传点击按钮后发出下一轮请求,服务端收到的消息含动作名与 context
表单有值绑定路径与提交按钮回显路径一致,context 里能读到填写内容

交付前自查:确认 validate 在生产里不是 'off';确认 CSP 已限制外部图片与脚本;确认数据模型里没有密钥;确认 Markdown 渲染器与字体都已接好;确认 surface 有明确的归属路由;确认升级插件前先在练习工程核对 version 默认值变化。

成本与合规:接真实模型会产生按量计费的调用;把用户输入、业务数据拼进系统提示或数据模型前,先按你们的数据政策评估。官方关于低延迟、高保真的描述来自发布方,公开报道里没有独立测量数据,别把它当成性能承诺写进方案。

常见问题速查

现象大概率原因 & 处理
界面一直不出现先查导入路径与插件 version 是否一致;再把 validate 调成 'strict' 看是否有块被丢弃
标题显示成字面 ##缺 Lit context 里的 MarkdownRenderer,用 @a2ui/markdown-it 接上
图标显示成英文名字页面没加载 Material Symbols Outlined 字体
提交表单后 context 为空输入 value 没绑数据模型路径,或按钮 action.event.context 没回显同样路径
自定义组件不生效loadCatalog 的注册 id 与 a2ui({ catalog: ... }) 里的引用 id 不一致
担心模型注入外链图片strict 只查结构;改用 CSP 限制 img-src,并在 a2ui() 之后加中间件检查属性
← 返回教程中心