前端曾经是固定的东西。设计师画出来。工程师构建出来。用户用上最终发布的东西。
时代变了。
2026 年发布的新界面,其中一部分是由 Agent 本身根据用户的实际需求实时绘制出来的。想要表格,就给你表格,而不是一段描述表格的文字。
生成式 UI 让 Agent 不再只是描述,而是开始展示。现在,构建它的方式已经出现了三种模式,它们之间的差异比大多数团队意识到的更为重要。
但构建它的方法不只一种,而是三种。而且大多数团队在不知情的情况下选了一种。
协议栈
三种协议,各司其职。
MCP 连接 Agent 与工具。A2A 连接 Agent 与 Agent。AG-UI 连接 Agent 与用户。
AG-UI 是一个流式传输层,承载下面你会看到的所有内容:工具调用、A2UI 模式、MCP App 事件、状态增量。它运行在 SSE 上。状态在同一条流上双向流动。用户编辑,Agent 看到。Agent 修改,用户看到。
A2UI 是 Google 制定的规范,让 Agent 以 schema 的形式生成 UI。它运行在 AG-UI 之上。CopilotKit 将其投入生产环境。
你不需要为这些东西编写解析器。CopilotKit 是一个 AG-UI 客户端,它会为你解码流。
大多数团队混淆的三种模式
问十个开发者什么是生成式 UI,你得到十个答案。大多数人描述的都是他们当前使用的框架自带的那个模式。
实际上就只有三种。这个光谱从“更多控制”延伸到“更多灵活性”。
- 受控模式: 你预先构建好组件。Agent 选择渲染哪一个。
- 声明式模式: Agent 输出 schema。你的应用将其映射到组件。
- 开放式模式: Agent 编写原始 HTML。你的应用在沙盒中渲染。

2026 年的每一个生成式 UI 框架都落在这个光谱的某处。差异在于架构,而不是表面形式。每种模式在规模化时都会以不同的方式破坏你的应用。
我试过不同的技术栈。大多数只擅长一种模式。最终我选定了 CopilotKit,因为它支持在同一个运行时上使用全部三种模式,并且基于 AG-UI。下面所有内容都运行在这个技术栈上。
模式 1:受控模式,前端掌控 UI

这是大多数团队起步的地方,也是大多数团队卡住的地方。
你预先构建一个 React 组件。你把它绑定到一个工具名称上。Agent 选择那个工具,该组件就会以 Agent 的参数作为 props 内联渲染在聊天中。
一个前端 hook,零 Agent 代码。搞定。
1"use client";2import { z } from "zod";3import { useComponent } from "@copilotkit/react-core/v2";45const expenseChartSchema = z.object({6 title: z.string(),7 data: z.array(z.object({ label: z.string(), value: z.number() })),8});910function ExpenseChart({ title, data }: z.infer<typeof expenseChartSchema>) {11 return (12 <section className="rounded-xl border p-4">13 <h3 className="text-sm font-medium">{title}</h3>14 <ul className="mt-2 grid gap-1">15 {data.map((d) => (16 <li key={d.label} className="flex justify-between text-sm">17 <span>{d.label}</span>18 <span>${d.value}</span>19 </li>20 ))}21 </ul>22 </section>23 );24}2526export function ExpensesCopilot() {27 useComponent({28 name: "showExpenseChart",29 description: "按类别显示费用明细。",30 parameters: expenseChartSchema,31 render: ExpenseChart,32 });3334 return null;35}
这个 hook 将工具注册到 CopilotKit 的运行时中。运行时通过 AG-UI 将其告知 Agent。当 Agent 调用它时,参数流式传入,你的组件内联渲染。无需编写 Python 工具,无需连接 schema,无需添加 API 路由。
你的设计系统仍然掌控全局。
这个费用图表不是模拟图。AI Financial Coach Agent 在实际的预算、储蓄计划和债务偿还中渲染了完全一样的卡片。
想看最简化的 hook 范例?它就在 生成式 UI 启动项目 的 use-generative-ui-examples.tsx 中。
Token 开销
你注册的每个组件在用户说任何话之前就已经占用 Agent 的上下文窗口。一个典型的工具描述及其 JSON schema 大约需要 400 个 token。25 个组件意味着每次轮询就有 10,000 个 token。你每次请求都要支付这个成本。
Agent 也会选错组件。太多组件看起来相似。饼图和环图都“显示比例”。它会猜错。
何时添加 Agent 端状态
共享状态是唯一值得编写 Python 工具的情况。Agent 写入会话状态。UI 的其他部分订阅状态,无需第二次 LLM 调用即可重新渲染。固定某个指标,仪表板立即更新。添加一行,表格立刻重绘。
1from google.adk.agents import LlmAgent2from google.adk.tools import ToolContext34def pin_metric(tool_context: ToolContext, label: str, value: float) -> dict:5 """将指标固定到用户的仪表板。"""6 pinned = tool_context.state.get("pinnedMetrics", [])7 tool_context.state["pinnedMetrics"] = pinned + [{"label": label, "value": value}]8 return {"status": "pinned"}910agent = LlmAgent(name="dashboard_agent", model="gemini-3.5-flash", tools=[pin_metric])
前端通过 CopilotKit 的共享状态 hook 读取固定指标。聊天组件仍然内联渲染,因为同一个工具名称通过前端 hook 进行了绑定。
在聊天中固定一个指标。面板无需第二次模型调用即可重绘。这就是 AI Dashboard Canvas Agent。
AI Deep Research Agent 更进一步。计划、每次搜索、每次文件写入——所有内容都流式显示为实时卡片。对于其他所有情况,前端 hook 就是全部。
何时采用受控模式: 十个或更少的高价值流程。设计精度很重要。你知道你需要的具体 UI。
何时不采用: 你的代码库会随着用例数量线性增长。25 个组件意味着 25 个工具定义,在每次 Agent 轮询中都会出现。
可能出问题的地方: Agent 选错组件。两个工具描述在语义上重叠。超过 15 个工具后,其中两个很可能读起来像“显示数据”。解决办法:重写描述,命名用户意图,而不是视觉效果。“当用户要求比较整体中的比例时使用”比“渲染一个饼图”要好。
模式 2:声明式(A2UI), Agent 输出 Schema

这是大多数生产级 Agent 应用最终需要的模式。
Agent 输出一个描述 UI 的 JSON schema。你的应用有一个组件目录,将 schema 节点映射到 React(或 Svelte、Flutter 等)。一个工具,多种 UI。
A2UI 是标准规范。CopilotKit 提供运行时。ADK 运行 Agent。AG-UI 是连接层。
Agent 工具按顺序返回三个操作:创建一个表面,推送组件树,推送数据。
1def search_flights(flights: list[Flight]) -> dict[str, Any]:2 """搜索航班并以丰富卡片形式显示。"""3 return {4 "a2ui_operations": [5 {"type": "create_surface", "surfaceId": SURFACE_ID, "catalogId": CATALOG_ID},6 {"type": "update_components", "surfaceId": SURFACE_ID, "components": FLIGHT_SCHEMA},7 {"type": "update_data_model", "surfaceId": SURFACE_ID, "data": {"flights": flights}},8 ]9 }
这是实际的函数,不是伪代码。运行时中间件在工具结果中看到 a2ui_operations 容器,并将表面转发到前端。要添加酒店?新建 schema 文件。再写一个函数,使用不同的 surface ID。前端无需额外工作。
固定 schema 与动态 schema
上面的组件树位于 flights.json 中。你编写了它。Agent 只填充数据。这就是固定 schema。
动态 schema 则相反:一个次级 LLM 根据对话上下文为每次轮询编写组件树。最终仍然是同一个 a2ui_operations 容器。Google ADK 示例展示了两种方式。
目录即契约
定义列出了 Agent 允许输出的组件,并包含 props 的 Zod schema。渲染器填充 React。拼写错误变成构建错误,而不是空白屏幕。
1const renderers: CatalogRenderers<TravelDefinitions> = {2 FlightCard: ({ props }) => (3 <article className="rounded-xl border p-4">4 <header className="flex justify-between">5 <span>{(props as any).airline}</span>6 <span>{(props as any).price}</span>7 </header>8 <div className="text-sm text-muted-foreground">9 {(props as any).origin} → {(props as any).destination} · {(props as any).departureTime}10 </div>11 </article>12 ),13};1415export const travelCatalog = createCatalog(travelDefinitions, renderers, {16 catalogId: "copilotkit://travel-catalog",17 includeBasicCatalog: true,18});
这两部分都位于 生成式 UI 启动项目 中,已连接并匹配。a2ui_fixed_schema.py 中的 search_flights,renderers.tsx 中的 FlightCard 目录。询问航班,卡片就会流式进入聊天。
按钮和其他交互式组件在 schema 中携带一个动作。基础目录将其连接到 onClick。点击触发事件通过 AG-UI 发回 Agent。Agent 决定下一步渲染什么。零点击处理代码。
Token 计算
无论 50 种还是 500 种卡片类型,Agent 只看到一个函数。随着组件库的增长,每次轮询的 token 数量保持不变。
它对任何渲染框架都是可扩展的,因为它只是 JSON。任何已经支持 AG-UI 的 Agent 都可以在零天时间内驱动 A2UI。你不需要修改 Agent 代码来连接它。
权衡: LLM 控制布局。在你的目录内,每次运行输出都会变化。如果你需要发布法律声明、营销页面或任何要求精确像素定位的内容,这不适合你。
声明式模式是为长尾需求而生的。仪表板、结果、表单、卡片、小部件。
何时采用声明式: 你的用例数量多到你没有时间预构建。你在原型阶段之后仍然关心 token 经济。
可能出问题的地方: 你构建了一个自定义的 FlightCard。但每个航班都渲染为基础目录的通用卡片。控制台没有报错。原因:Agent 上的 CATALOG_ID 和前端 createCatalog 中的 catalogId 不匹配。前端无法识别 Agent 指向的目录,回退到基础目录。请确保两边的字符串完全一致。
模式 3:开放式,无目录,无规则

第三种模式是另一个极端。没有目录,没有 schema,只有一张空白画布。
这个类别下有两种子模式。
MCP Apps
一个 MCP 服务器暴露 UI 表面,由 Agent 驱动。Excalidraw 是我印象深刻的例子。Agent 获得画布的完全控制权,根据你的上下文绘制图表,掌控板上的每一个像素。

从头实现客户端协议很痛苦,所以 CopilotKit 提供了 MCPAppsMiddleware。将其附加到你的 Agent,并指向任何 MCP Apps 服务器。
1const agent = new BuiltInAgent({2 model: "openai/gpt-5.5",3 prompt: "你是一个有用的助手。",4}).use(5 new MCPAppsMiddleware({6 mcpServers: [{ type: "http", url: "https://mcp.excalidraw.com/mcp", serverId: "my-server" }],7 }),8);
启动 MCP Apps Showcase,你就可以在聊天窗口内预订航班和酒店。同样的中间件,真实的 MCP 服务器。或者更进一步。
AI MCP App Builder 让 Agent 在 E2B 沙盒中编写一个全新的应用,然后实时渲染。
0:45
沙盒化 HTML
Agent 编写原始 HTML。你的应用在沙盒化的 iframe 中渲染,以防止其劫持会话。
运行时注册一个 HTML 渲染工具,并通过 AG-UI 将其发送给 Agent。Agent 用任何它想要的标记调用它。Agent 端无需定义 HTML 工具。运行时自身注入它。
Agent 端的指令做着实际工作:
1canvas_agent = LlmAgent(2 name="canvas_agent",3 model="gemini-3.5-flash",4 instruction=(5 "你是一个可视化助手。当用户要求查看、绘制或可视化任何内容时,"6 "生成一个交互式 HTML UI。仅使用 Tailwind 类。不使用外部字体。"7 "除非用户指定颜色,否则使用中性色彩。"8 ),9)
没有这些样式规则,模型就会默认使用它训练数据中那个星期最强烈的审美。加上这些规则后,大多数时候你能得到接近你品牌的效果。但并非总是如此。
品牌不一致问题
我尝试过将开放式模式作为某个 Agent 的主要 UI。一周之后就撤回了。
“新粗野主义”在周二出现,“iOS 4 克隆”在周三出现。提示词中的样式规则会引导 Agent 靠近你的品牌,但不能保证。品牌一直在变化,产品感觉不够专业。

开放式模式并非无用,而是被错误应用了。
它只适合一种场景:一次性交互,用户不关心界面长什么样,而且永远不会再看到它。“给我演示一下电子是如何工作的。”“给我一个关于我最近 10 次查询的怪异柱状图。”“可视化这个 API 响应。”类似于你在 Google AI 概览中看到的那种。
何时采用开放式模式: 一次性查询。可丢弃的可视化。沙盒实验。永远不要作为主要界面。
可能出问题的地方: iframe 渲染了,但按钮不能点击,表单无法提交。沙盒标志设置得太严格,或者太宽松导致浏览器拒绝。将 iframe 的 sandbox 设置为 allow scripts 和 allow forms。除此之外什么也不要加。永远不要设置 allow-same-origin。
如何选择
在写代码之前先运行决策树。
设计师有像素级完美的这些流程的模型?→ 受控模式。
需要发布几十种卡片类型或小部件?→ 声明式。
一次性、可丢弃的可视化,用户永远不会再看第二次?→ 开放式。
无法决定?→ 默认使用声明式。对前 3 个重要流程升级为受控模式。永远不要默认使用开放式模式。
如果你已经在发布产品,但不确定自己落在了哪里,数一下渲染工具的数量。超过 15 个,你就处于受控模式,瓶颈就在眼前。本周开始连接 A2UI。
三种模式,三种赌注
受控模式赌的是你。预先构建的组件,像素级完美。超过 25 个之后成本高昂。
声明式模式赌的是 schema。Schema 是契约。Agent 填充它。扩展性平坦。
开放式模式赌的是模型。没有目录,没有 schema,只有原始 HTML。适用于一次性场景,但对于任何需要重复发布的内容都很脆弱。
错误不在于选错模式,而在于不知道自己选了哪一个。
大多数团队默认选择受控模式,因为框架默认为受控模式。他们在 25 个组件时撞墙,然后转向开放式模式,因为它在演示中看起来很吸引人。这两者都不是决策,而是随波逐流。
有目的地选择。将模式与问题匹配。需要精确的流程用受控模式。长尾需求用声明式。一次性需求用开放式。
开源生成式 UI Agent 模板
这三种模式的参考实现都放在 awesome-llm-apps 中新增的 生成式 UI Agents 部分。克隆你需要的,删掉不需要的。
我将继续发布关于生产环境中 Agent 部署、AG-UI 以及可扩展模式的更多内容。关注我 @Saboo_Shubham_ 以保持关注。










