前端過去是固定的東西。設計師畫好它,工程師建構它,使用者拿到出貨版本。
這已經過時了。
2026 年出貨的介面,有一部分是由 Agent 本身即時繪製而成,來自使用者實際要求的內容。要表格就給表格,而不是一段描述表格的文字。
生成式 UI 是讓 Agent 不再只用描述的層,而是直接展示。目前浮現了三種建構模式,而它們之間的差異比多數團隊意識到的更重要。
但建構這種東西的方法不只一種,而是三種。而且多數團隊在選擇時,並不知道自己做了選擇。
協定堆疊
三種協定,各司其職。
MCP 連接 Agent 與工具。A2A 連接 Agent 之間。AG-UI 連接 Agent 與使用者。
AG-UI 是串流層,承載你下面會看到的一切:工具呼叫、A2UI 結構、MCP App 事件、狀態差異。透過 SSE 運行。狀態在同一個串流中雙向流動。使用者編輯,Agent 看到。Agent 變更,使用者看到。
A2UI 是 Google 的規格,讓 Agent 以結構形式發出 UI。它建立在 AG-UI 之上。CopilotKit 在生產環境中出貨它。
你不需要為這些東西寫解析器。CopilotKit 是 AG-UI 用戶端,它會為你解碼串流。
多數團隊混淆的三種模式
問十個開發者什麼是生成式 UI,你會得到十種答案。大部分描述的只是他們當前框架出貨的那種模式。
其實只有三種。這條光譜從更多控制到更多彈性。
- 受控模式: 你預先建好元件,Agent 選擇要渲染哪一個。
- 宣告式模式: Agent 發出一個結構,你的應用程式將它對應到元件。
- 開放式模式: Agent 寫出原始 HTML,你的應用程式在沙盒中渲染它。

2026 年的每個 Gen 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: "Render a breakdown of expenses by category.",30 parameters: expenseChartSchema,31 render: ExpenseChart,32 });3334 return null;35}
這個 hook 將工具註冊到 CopilotKit 的運行時。運行時透過 AG-UI 將它告知 Agent。當 Agent 呼叫它時,引數會串流進來,你的元件直接渲染。不需要寫 Python 工具,不需要接線結構,不需要新增 API 路由。
你的設計系統繼續掌控一切。
那個支出圖表不是模擬圖。AI Financial Coach Agent 正是用類似的卡片來渲染真實的預算、儲蓄計畫和債務償還。
想要最基礎的 hook?它就在 Generative UI Starter Project 的 'use-generative-ui-examples.tsx' 中。
Token 稅
你註冊的每個元件,在使用者還沒說任何話之前,就已經佔據了 Agent 的上下文視窗。一個典型的工具描述加上它的 JSON 結構大約耗費 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 """Pin a metric to the user's dashboard."""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 個元件意味著每次 Agent 輪轉都有 25 個工具定義。
可能出錯的地方: Agent 選錯元件。兩個工具描述在語義上重疊。超過 15 個工具後,其中兩個大概讀起來都像「顯示資料」。修正方式:重寫描述,命名使用者意圖而非視覺效果。「當使用者要求比較整體的比例時使用」勝過「渲染圓餅圖」。
模式 2:宣告式(A2UI),Agent 發出結構

這是大多數生產級 Agent 應用程式最終需要的模式。
Agent 發出描述 UI 的 JSON 結構。你的應用程式有一個元件目錄,將結構節點對應到 React(或 Svelte、Flutter 等)。一個工具,多種 UI。
A2UI 是標準規格。CopilotKit 出貨運行時。ADK 運行 Agent。AG-UI 是傳輸線。
Agent 工具依序回傳三個操作:建立一個表面、推送元件樹、推送資料。
1def search_flights(flights: list[Flight]) -> dict[str, Any]:2 """Search flights and display them as rich cards."""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 容器,並將表面轉發到前端。加入飯店?新增結構檔案。用不同的 surface ID 再加一個函式。前端完全不需要額外工作。
固定結構 vs 動態結構
上面的元件樹存在 flights.json 中。是你寫的。Agent 只填入資料。這就是固定結構。
動態結構則相反:由一個次要 LLM 根據對話上下文在每次輪轉中寫出元件樹。結尾仍然是同一個 a2ui_operations 容器。Google ADK 展示兩者都有。
目錄就是合約
定義列出 Agent 允許發出的元件,並使用 Zod 結構來定義 props。渲染器負責填入 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});
這兩半都在 Generative UI Starter Project 中接線並匹配。search_flights 在 'a2ui_fixed_schema.py' 中,FlightCard 目錄在 'renderers.tsx' 中。詢問航班。看著卡片串流進入對話。
按鈕和其他互動元件在結構中帶有 action。基本目錄將它綁定到 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:開放式,無目錄,無規則

第三種模式是另一個極端。沒有目錄,沒有結構。只是一塊空白畫布。
這個桶子裡有兩個子模式。
MCP Apps
MCP 伺服器暴露 UI 表面,由 Agent 驅動。Excalidraw 是我印象最深的例子。Agent 完全控制畫布。根據你的上下文繪製圖表。擁有專案上的每個像素。

從頭實現用戶端協定很痛苦,所以 CopilotKit 出貨了 MCPAppsMiddleware。將它附加到你的 Agent,並指向任何 MCP Apps 伺服器。
1const agent = new BuiltInAgent({2 model: "openai/gpt-5.5",3 prompt: "You are a helpful assistant.",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 "You are a visualization assistant. When the user asks to see, "6 "draw, or visualize anything, generate an interactive HTML UI. "7 "Use Tailwind classes only. No external fonts. Stick to neutral "8 "colors unless the user names one."9 ),10)
如果沒有這些樣式規則,模型就會預設使用訓練資料中那個星期最突出的美學。有了它們,你大部分時候會得到接近你品牌的東西。但並非總是如此。
品牌不一致問題
我試過將開放式作為 Agent 的主要 UI。一週後就撤回了。
星期二「新粗獷主義」,星期三「iOS 4 複製版」。提示中的樣式規則會引導 Agent 靠近你的品牌,但並不保證。品牌一直在變,產品感覺不夠嚴謹。

開放式並非無用,而是被誤用了。
只適合一件事:一次性互動,使用者不在乎介面長怎樣,也不會再看到第二次。「告訴我電子是怎麼運作的。」「把我最近 10 個查詢做成一個奇怪的長條圖。」「將這個 API 回應視覺化。」就是你在 Google AI 摘要中看到的那種東西。
何時使用開放式: 一次性查詢。一次性視覺化。沙盒實驗。絕對不要作為主要表面。
可能出錯的地方: iframe 渲染了,但按鈕不能點,表單不能送出。沙盒標誌太嚴格,或者太鬆散導致瀏覽器拒絕。將 iframe 沙盒設為允許腳本和允許表單。其他都不要。絕對不要 allow-same-origin。
如何選擇
在寫程式碼之前先做決策樹。
設計師對這個流程有像素完美的 mockup?用受控模式。
要出貨幾十種卡片類型或小工具?用宣告式。
一次性、不會再看到的視覺化?用開放式。
無法決定?預設用宣告式。對前 3 個流程升級到受控模式。永遠不要預設用開放式。
如果你已經在出貨,但不確定自己落在哪個位置,數一下渲染工具。超過 15 個,你就是受控模式,而且瓶頸快到了。本週開始接線 A2UI。
三種模式,三種賭注
受控模式賭在你身上。預先建好的元件,像素完美。超過 25 個就會變貴。
宣告式賭在結構上。結構就是合約。Agent 填入內容。水平擴展。
開放式賭在模型上。沒有目錄,沒有結構,原始 HTML。適合一次性用途。任何會出貨兩次的東西都很脆弱。
錯誤不在於選錯模式,而在於不知道你選了模式。
多數團隊預設用受控模式,因為框架預設就是受控模式。他們在 25 個元件時撞牆,然後轉向開放式,因為它在展示中看起來很有吸引力。這兩者都不是決策,都是漂移。
有意識地選擇。將模式對應到問題。精確的流程用受控模式。長尾用宣告式。一次性的用開放式。
開放原始碼生成式 UI Agent 模板
這三種模式的參考都在新的 Generative UI Agents 區塊中,位於 awesome-llm-apps。複製你需要的,刪掉你不要的。
我會持續發表更多關於在生產環境中出貨 Agent、AG-UI 以及可擴展模式的文章。追蹤我 @Saboo_Shubham_ 以保持關注。










