生成式 UI 正在成为新的前端
Agent 应用的界面正在从固定前端变成实时生成。Controlled、Declarative 和 Open-ended 三种模式,分别对应控制力、扩展性和自由度的取舍。

前端过去是固定的东西。设计师画出来,工程师实现,用户只能使用已经发布的界面。
这个阶段正在结束。
2026 年交付的界面,有一部分会由 Agent 根据用户刚刚提出的需求实时绘制。用户要表格,就直接得到表格,而不是得到一段描述表格的文字。
生成式 UI 是让 Agent 从“描述”走向“展示”的一层。现在已经出现三种构建模式,它们之间的差异比很多团队意识到的更重要。
但这件事没有唯一做法。其实有三种,而且很多团队在没有意识到的情况下,已经选了其中一种。
协议栈
三个协议,各做一件事。
MCP 把 Agent 连接到工具。 A2A 把 Agent 连接到彼此。 AG-UI 把 Agent 连接到用户。
AG-UI 是下面这些内容的流式传输层:工具调用、 A2UI schema 、 MCP App 事件、状态增量。它运行在 SSE 上。同一条流里,状态可以双向流动:用户修改,Agent 能看到;Agent 修改,用户也能看到。
A2UI 是 Google 用来让 Agent 以 schema 形式输出 UI 的规范。它运行在 AG-UI 之上。 CopilotKit 已经在生产环境里提供这套能力。
这里不需要自己写解析器。 CopilotKit 本身就是 AG-UI client,会帮你解码这条流。
容易混淆的三种模式
问十个开发者什么是 Generative UI,可能会得到十种答案。大多数答案其实只是在描述他们当前框架默认支持的那一种模式。
实际只有三类。它们构成一条从“控制力更强”到“灵活性更高”的谱系:
- Controlled:预先构建组件,Agent 只负责选择渲染哪一个。
- Declarative:Agent 输出 schema,应用把 schema 映射到组件。
- Open-ended:Agent 直接写原始 HTML,应用在沙盒里渲染。

2026 年的每个 Gen UI 框架,基本都在这条线上。差异是架构上的,不是外观上的。规模一上来,每种模式都会以不同方式把应用搞坏。
我试过不同技术栈。多数栈只把其中一种模式做得不错。最后选 CopilotKit,是因为它在同一个运行时上支持三种模式,并且都跑在 AG-UI 上。下面所有示例都基于这套栈。
模式一:Controlled,前端掌控 UI

多数团队会从这里开始,也常常卡在这里。
先预构建一个 React 组件,把它绑定到一个工具名。 Agent 选择这个工具后,组件就会以内联形式渲染在聊天里,Agent 传入的 args 会作为 props 。
一个前端 hook,不需要 Agent 侧代码,就这么简单。
"use client";
import { z } from "zod";
import { useComponent } from "@copilotkit/react-core/v2";
const expenseChartSchema = z.object({
title: z.string(),
data: z.array(z.object({ label: z.string(), value: z.number() })),
});
function ExpenseChart({ title, data }: z.infer<typeof expenseChartSchema>) {
return (
<section className="rounded-xl border p-4">
<h3 className="text-sm font-medium">{title}</h3>
<ul className="mt-2 grid gap-1">
{data.map((d) => (
<li key={d.label} className="flex justify-between text-sm">
<span>{d.label}</span>
<span>${d.value}</span>
</li>
))}
</ul>
</section>
);
}
export function ExpensesCopilot() {
useComponent({
name: "showExpenseChart",
description: "Render a breakdown of expenses by category.",
parameters: expenseChartSchema,
render: ExpenseChart,
});
return null;
}
这个 hook 会把工具注册到 CopilotKit runtime 。 runtime 通过 AG-UI 把它暴露给 Agent 。 Agent 调用工具时,参数以流式方式进来,组件在聊天里内联渲染。不用写 Python 工具,不用接 schema,也不用加 API route 。
设计系统仍然由你掌控。
这个 expense chart 不是 mockup 。 AI Financial Coach Agent 会用同样方式为真实预算、储蓄计划和债务偿还渲染卡片。

如果想先看最小 hook,可以去 Generative UI Starter Project 里的 use-generative-ui-examples.tsx。
Token 成本
你注册的每个组件,都会在用户开口之前就放进 Agent 的上下文窗口。一个典型工具描述加 JSON schema 大约 400 tokens 。 25 个组件就是每轮请求 10,000 tokens 。这个成本每次请求都会付。
Agent 也会选错组件。相似组件太多时,饼图和环形图都像是在“展示占比”,它只能猜。
什么时候需要 Agent 侧状态
共享状态是少数值得写 Python 工具的场景。 Agent 写入 session state,UI 其他部分订阅后直接重渲染,不需要第二次 LLM 调用。固定一个指标,dashboard 更新;加一行数据,表格重绘。
from google.adk.agents import LlmAgent
from google.adk.tools import ToolContext
def pin_metric(tool_context: ToolContext, label: str, value: float) -> dict:
"""Pin a metric to the user's dashboard."""
pinned = tool_context.state.get("pinnedMetrics", [])
tool_context.state["pinnedMetrics"] = pinned + [{"label": label, "value": value}]
return {"status": "pinned"}
agent = LlmAgent(name="dashboard_agent", model="gemini-3.5-flash", tools=[pin_metric])
前端通过 CopilotKit 的 shared-state hook 读取 pinned metrics 。聊天组件仍然以内联方式渲染,因为同一个工具名也接了前端 hook 。
在聊天里 pin 一个指标,面板不需要第二次模型调用就会重绘。这就是 AI Dashboard Canvas Agent 的模式。

AI Deep Research Agent 走得更远:计划、每次搜索、每个文件写入,都会以 live cards 形式流式展示。除此之外,前端 hook 就是整个故事。

适合使用 Controlled 的情况:10 个以内高价值流程;设计精度很重要;你明确知道需要哪些 UI 。
不适合的情况:用例增长时,代码库会线性膨胀。 25 个组件意味着每轮 Agent 调用里都塞着 25 个工具定义。
容易出问题的地方:Agent 选错组件。两个工具描述在语义上重叠。超过 15 个工具后,很可能有两个都写着“展示数据”。修法是重写描述,描述用户意图,而不是描述视觉外观。Use when the user asks to compare proportions of a whole 比 renders a pie chart 更好。
模式二:Declarative(A2UI),Agent 输出 schema

这是多数生产级 Agent 应用最后会需要的模式。
Agent 输出一份描述 UI 的 JSON schema 。应用侧有一套组件目录,把 schema 节点映射到 React 、 Svelte 、 Flutter 或其他渲染框架。一个工具,多种 UI 。
A2UI 是标准规范。 CopilotKit 提供 runtime,ADK 运行 Agent,AG-UI 负责传输。
Agent 工具会按顺序返回三个操作:创建 surface,推送组件树,推送数据。
def search_flights(flights: list[Flight]) -> dict[str, Any]:
"""Search flights and display them as rich cards."""
return {
"a2ui_operations": [
{"type": "create_surface", "surfaceId": SURFACE_ID, "catalogId": CATALOG_ID},
{"type": "update_components", "surfaceId": SURFACE_ID, "components": FLIGHT_SCHEMA},
{"type": "update_data_model", "surfaceId": SURFACE_ID, "data": {"flights": flights}},
]
}
这是真实函数,不是伪代码。 runtime middleware 会识别工具结果里的 a2ui_operations 容器,并把 surfaces 转发到前端。要增加酒店场景?新增一个 schema 文件,再写一个带不同 surface ID 的函数。前端不用额外改。
固定 schema 和动态 schema
上面的 component tree 存在 flights.json 里,由你预先写好。 Agent 只负责填数据,这就是 fixed schema 。
Dynamic schema 则反过来:每一轮由第二个 LLM 根据对话上下文生成 component tree 。最终仍然用同样的 a2ui_operations 容器。 Google ADK showcase 同时提供了两种模式。
Catalog 就是合约
Definitions 会列出 Agent 允许输出的组件,并用 Zod schema 定义 props 。 Renderers 负责填充 React 。拼写错误会变成 build error,而不是空白页面。
const renderers: CatalogRenderers<TravelDefinitions> = {
FlightCard: ({ props }) => (
<article className="rounded-xl border p-4">
<header className="flex justify-between">
<span>{(props as any).airline}</span>
<span>{(props as any).price}</span>
</header>
<div className="text-sm text-muted-foreground">
{(props as any).origin} → {(props as any).destination} · {(props as any).departureTime}
</div>
</article>
),
};
export const travelCatalog = createCatalog(travelDefinitions, renderers, {
catalogId: "copilotkit://travel-catalog",
includeBasicCatalog: true,
});
两部分都在 Generative UI Starter Project 里,已经接好并匹配:a2ui_fixed_schema.py 里的 search_flights,以及 renderers.tsx 里的 FlightCard catalog 。询问航班,卡片就会流式进入聊天窗口。

按钮和其他交互组件可以在 schema 里携带 action 。 basic catalog 会把它接到 onClick。点击会通过 AG-UI 把事件送回 Agent,Agent 决定接下来渲染什么。不需要手写 click handlers 。
Token 计算
50 种卡片也好,500 种卡片也好,Agent 只看到一个函数。组件库扩张时,每轮 token 成本保持平坦。
它也能扩展到任何渲染框架,因为本质只是 JSON 。任何已经支持 AG-UI 的 Agent,第一天就能驱动 A2UI 。你不用再动 Agent 代码。
代价是:LLM 拥有布局权。即使在同一个 catalog 内,输出也可能每次不同。如果你在做法律披露、营销页面,或者任何需要像素级固定位置的界面,这不是合适的模式。
Declarative 适合长尾。 Dashboard 、搜索结果、表单、卡片、 widget,都适合放在这里。
适合使用 Declarative 的情况:用例比预构建时间更多;原型之后开始关心 token 经济性。
容易出问题的地方:明明写了自定义 FlightCard,每个航班却都渲染成 basic catalog 的通用卡片。控制台没有报错。问题通常是 Agent 里的 CATALOG_ID 和前端 createCatalog 里的 catalogId 不匹配。两边字符串必须完全一致。
模式三:Open-ended,没有 catalog,也没有规则

第三种模式走到另一个极端:没有 catalog,没有 schema,只有一块空白画布。
这里又分成两个子模式。
MCP Apps
MCP server 暴露 UI surfaces,Agent 驱动它们。 Excalidraw 是一个很有代表性的例子。 Agent 可以完全控制画布,根据上下文画图,并拥有画板上的每个像素。

从零实现 client protocol 很痛苦,所以 CopilotKit 提供了 MCPAppsMiddleware。把它挂到 Agent 上,再指向任意 MCP Apps server 即可。
const agent = new BuiltInAgent({
model: "openai/gpt-5.5",
prompt: "You are a helpful assistant.",
}).use(
new MCPAppsMiddleware({
mcpServers: [{ type: "http", url: "https://mcp.excalidraw.com/mcp", serverId: "my-server" }],
}),
);
启动 MCP Apps Showcase 后,就可以在聊天窗口里订机票、订酒店。还是同一个 middleware,只是接上真实 MCP servers 。还可以继续往前走。
AI MCP App Builder 可以让 Agent 在 E2B sandbox 里写一个全新的 app,然后实时渲染出来。

沙盒 HTML
Agent 直接写原始 HTML 。应用把它放进 sandboxed iframe 里渲染,防止它劫持 session 。
runtime 会注册一个 HTML rendering tool,并通过 AG-UI 交给 Agent 。 Agent 用任意 markup 调用它。 Agent 侧不需要额外定义 HTML tool,runtime 会注入。
Agent 侧 instruction 在这里确实很重要:
canvas_agent = LlmAgent(
name="canvas_agent",
model="gemini-3.5-flash",
instruction=(
"You are a visualization assistant. When the user asks to see, "
"draw, or visualize anything, generate an interactive HTML UI. "
"Use Tailwind classes only. No external fonts. Stick to neutral "
"colors unless the user names one."
),
)
没有这些风格规则,模型会默认采用训练数据里最近最吵的审美。有这些规则后,大多数时候能更接近你的品牌,但不能保证每次都稳定。
品牌不一致问题
我试过把 Open-ended 当成一个 Agent 的主 UI 。一周后就撤掉了。
周二像 Neo-brutalist,周三像 iOS 4 clone 。提示词里的风格规则可以把 Agent 往品牌方向推,但不能保证。品牌一直变,产品会显得不严肃。

Open-ended 不是没用,而是经常被用错。
它适合一种事:一次性的交互,用户不在意界面长什么样,也不会第二次看到它。比如“展示电子如何运动”“给我最近 10 次查询画个奇怪的柱状图”“可视化这个 API response”。这类东西在 Google AI overviews 里很常见。
适合使用 Open-ended 的情况:一次性查询、可丢弃的可视化、沙盒实验。不要把它作为主界面。
容易出问题的地方:iframe 渲染了,但按钮不能点,表单不能提交。 sandbox flags 要么太紧,要么太松到浏览器拒绝。设置 iframe sandbox 时,只允许 scripts 和 forms,不要允许其他权限,也不要 allow-same-origin。
怎么选择
写代码之前先跑一遍决策树。
设计师已经给了像素级 mockup?选 Controlled 。
要交付几十种 card 或 widget?选 Declarative 。
只是一次性可视化,用户不会第二次看到?选 Open-ended 。
拿不准时,默认选 Declarative 。前三个高价值流程再升级到 Controlled 。永远不要把 Open-ended 当默认方案。
如果已经上线,但不确定自己落在哪一种,数一下 render tools 。超过 15 个,基本就是 Controlled,而且快撞墙了。这周就该开始接 A2UI 。
三种模式,三种押注
Controlled 押注你自己:预构建组件,像素级精确,但超过 25 个以后很贵。
Declarative 押注 schema:schema 是合约,Agent 填数据,扩展成本更平坦。
Open-ended 押注模型:没有 catalog,没有 schema,原始 HTML 。适合可丢弃场景,不适合要反复上线的界面。
错误不在于选错模式,而在于不知道自己已经选了模式。
多数团队会默认走 Controlled,因为框架默认支持 Controlled 。等到 25 个组件时撞墙,又因为 Open-ended demo 看起来很炫,就转向 Open-ended 。两个选择都不是设计决策,而是被默认值推着走。
应该有意识地选。让模式匹配问题。需要精确的流程,用 Controlled 。长尾界面,用 Declarative 。可丢弃的一次性界面,用 Open-ended 。
开源 Generative UI Agent 模板
三种模式的参考实现都放在 awesome-llm-apps 新增的 Generative UI Agents 区域。需要哪部分就 clone 哪部分,不需要的删掉。
来源:https://x.com/saboo_shubham_/status/2062220865643982875?s=52