iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0

https://ithelp.ithome.com.tw/upload/images/20260823/201612900A24Hleopl.png

LLM 能用哪些元件,前端真的有沒有實作?

在前後端分離的現代系統中,最常見的痛苦莫過於「API 規格漂移(API Drift)」:後端加了新欄位,前端忘記接;或是前端改了命名,後端還在傳舊版。

到了 Generative UI(生成式 UI) 時代,這個問題會被放大十倍,演變成更詭異的 「契約失諧(Contract Mismatch)」

  • 後端 Prompt 承諾了前端沒有的元件:Prompt 興高采烈地告訴 LLM:「你可以使用 RadarChart(雷達圖)來展示客戶能力」,結果模型乖乖生成了 type: "RadarChart",但前端根本還沒安裝圖表套件,使用者畫面上直接噴出白屏或紅字報錯。
  • 前端已經實作,但 LLM 永遠不知道:前端花了一週刻了一個酷炫的 TimelineComparison(時間軸比較器),但後端的 Catalog Prompt 始終沒更新,LLM 只能繼續吐出簡陋的條列式清單。
  • Props 型別未對齊:後端以為 trend 是傳布林值(true / false),前端元件卻要求字串 enum("up" | "down"),導致圖標渲染失敗。

今天我們要建立一套自動化契約對帳與防漂移機制(Catalog Reconciliation),確保「AI 知道能用的」與「前端能畫出來的」100% 嚴格對齊。


1. 今天要解決的痛點與核心觀念

痛點背景:Generative UI 契約管理的三大死穴

  1. 靜態文檔不同步:前後端工程師各看各的 Notion 文檔,代碼一上線立刻在 Production 破功。
  2. 缺乏執行期防呆(Runtime Fallback):一旦 LLM 吐出未知 Component,整頁 React 渲染樹崩潰,其他正常的表格與指標全部連帶消失。
  3. Prompt 手工維護成本高:每次前端加一個 UI 元件,後端工程師就要手動改一次 System Prompt 的說明文字,極度容易疏漏。

觀念圖解:Catalog 驅動的自動對帳與 Prompt 生成鏈

https://ithelp.ithome.com.tw/upload/images/20260823/20161290czG6FQwzxH.jpg

  1. 共用 Catalog 定義 (Zod Schema):作為前後端唯一真實來源(Single Source of Truth)。
  2. 前端 Registry 登錄:驗證每個 Catalog 元件是否皆已於 React 中實作。
  3. 後端 Prompt 自動轉譯:將 Schema 自動轉為 LLM 結構化提示詞與 Tool 規範。
  4. 啟動期自動對帳 (Reconciliation & Fallback):比對前後端差異,若有未實作元件自動掛載 Fallback 容錯。

2. 官方核心技術依據與架構深度

json-renderembabel-generative-ui 規範中,這套契約機制由以下三大模組構成:

1. Zod Schema 即 Single Source of Truth

我們不撰寫純文字的 API 文檔,而是使用 TypeScript + Zod 定義元件。Zod 的好處是:

  • 可以直接在編譯期推導出 TypeScript Interface。
  • 可以使用 .describe("...") 為每個欄位加上中文語意說明,這些說明會被自動提取為 LLM 的 Prompt 指引。
  • 支援 .default().optional(),明確告訴模型哪些是選填欄位。

2. 啟動期對帳鉤子(useCatalogReconciliation

前端在應用程式初始化或測試階段,會將前端的 Object.keys(registry) 與後端回傳的 /api/ui/catalog 進行 Diff 比對:

  • Missing in Frontend:後端允許但前端尚未實作(標記為 WARNING,掛載 Fallback)。
  • Unused in Backend:前端已實作但未開放給後端(標記為 INFO)。

3. 優雅降級容錯元件(Smart Fallback Component)

當遇到未定義元件或驗證失敗時,json-render 不會拋出 Uncaught Error,而是渲染一個美觀的除錯容器:

  • 顯示元件名稱、收到的 Props 預覽。
  • 依然正常渲染其 children 子元件,確保整個 Dashboard 的佈局結構不受單一元件影響。

3. 完整程式碼實戰(Production-Ready Code)

以下提供完整的生產級實裝:

  1. 完整 Zod Catalog 定義(涵蓋 5 大核心元件)
  2. React 自動對帳 Hook 與 Smart Fallback 元件
  3. Spring Boot 後端 Catalog 暴露 API

1. 前端 TypeScript:核心 Catalog 定義與型別提取

// src/schema/dashboardCatalog.ts
import { z } from 'zod';

/**
 * 儀表板元件型錄 (Catalog)
 * 定義 AI 允許使用的元件白名單、Props 屬性與中文說明
 */
export const DashboardCatalogSchema = {
  // 佈局容器
  Stack: z.object({
    direction: z.enum(["horizontal", "vertical"]).default("vertical").describe("排版方向"),
    gap: z.number().default(4).describe("元件間距 (Tailwind gap 單位)"),
  }),

  // 標題
  Heading: z.object({
    text: z.string().describe("標題文字"),
    level: z.enum(["h1", "h2", "h3"]).default("h2").describe("標題層級"),
  }),

  // 指標卡片
  MetricCard: z.object({
    title: z.string().describe("指標名稱 (如:近一年總消費)"),
    value: z.union([z.string(), z.number()]).describe("指標主數值"),
    trend: z.enum(["up", "down", "neutral"]).optional().describe("趨勢方向"),
    change: z.string().optional().describe("變動文字說明 (如:+15%)"),
  }),

  // 警示橫幅
  AlertBanner: z.object({
    severity: z.enum(["info", "warning", "error", "success"]).describe("警告嚴重等級"),
    message: z.string().describe("提示訊息內文"),
  }),

  // 數據表格 (資料列一律由 Java 填入)
  DataTable: z.object({
    columns: z.array(z.object({
      key: z.string().describe("欄位鍵值"),
      label: z.string().describe("欄位顯示名稱")
    })).describe("表格欄位定義"),
    rows: z.array(z.record(z.any())).describe("資料列清單 (由 Java 確定性生成)")
  })
};

export type CatalogSchemaType = typeof DashboardCatalogSchema;

2. 前端 React:對帳 Hook 與 Fallback 容錯實作

// src/components/renderer/CatalogReconciler.tsx
import React, { useEffect, useState } from 'react';
import { DashboardCatalogSchema } from '../../schema/dashboardCatalog';

interface ReconcileResult {
  isAligned: boolean;
  missingInFrontend: string[];
  extraInFrontend: string[];
}

/**
 * 前後端 Catalog 自動對帳 Hook
 */
export function useCatalogReconciliation(registeredComponentKeys: string[]): ReconcileResult {
  const [result, setResult] = useState<ReconcileResult>({
    isAligned: true,
    missingInFrontend: [],
    extraInFrontend: []
  });

  useEffect(() => {
    const catalogKeys = Object.keys(DashboardCatalogSchema);
    const registeredSet = new Set(registeredComponentKeys);

    // 找出後端有定義但前端未實作的元件
    const missing = catalogKeys.filter(key => !registeredSet.has(key));
    // 找出前端已實作但未在 Catalog 定義的元件
    const extra = registeredComponentKeys.filter(key => !catalogKeys.includes(key));

    const aligned = missing.length === 0;

    if (!aligned) {
      console.warn("[Catalog Reconciliation] 偵測到契約漂移!後端定義但前端未實作:", missing);
    }

    setResult({
      isAligned: aligned,
      missingInFrontend: missing,
      extraInFrontend: extra
    });
  }, [registeredComponentKeys]);

  return result;
}

/**
 * 智慧容錯 Fallback 元件
 */
export const SmartFallbackComponent: React.FC<{
  type: string;
  props: Record<string, any>;
  children?: React.ReactNode;
}> = ({ type, props, children }) => {
  return (
    <div className="my-2 p-3 border border-amber-500/50 bg-amber-950/20 rounded-lg text-amber-200">
      <div className="flex items-center gap-2 text-xs font-semibold">
        <span className="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
        <span>元件暫未支援: [{type}]</span>
      </div>
      <div className="mt-1 text-xs opacity-70 font-mono overflow-x-auto">
        Props: {JSON.stringify(props)}
      </div>
      {/* 依然保留子節點渲染能力 */}
      {children && <div className="mt-2 pl-2 border-l border-amber-800">{children}</div>}
    </div>
  );
};

3. 後端 Spring Boot:提供 Catalog 規格端點

package com.antechinus.travel.web;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

/**
 * UI Catalog 規格查詢端點
 * 供前端或治理平台查詢當前 Agent 允許生成的元件白名單
 */
@RestController
@RequestMapping("/api/ui")
public class UiCatalogController {

    /**
     * 取得後端宣告支援的元件清單
     */
    @GetMapping("/catalog")
    public ResponseEntity<Map<String, Object>> getSupportedCatalog() {
        return ResponseEntity.ok(Map.of(
            "version", "1.0.0",
            "supportedComponents", List.of("Stack", "Heading", "MetricCard", "AlertBanner", "DataTable"),
            "description", "Antechinus Travel 官方儀表板元件型錄"
        ));
    }
}

4. 生產環境避坑指南與對比分析

常見踩雷與除錯秘訣

  1. 雷區一:在對話中臨時口頭交代新元件給 LLM
    • 現象:工程師在 Prompt 裡寫:「請用 SuperChart 來畫圖」,但前端完全沒這元件,直接導致線上畫面破裂。
    • 解法嚴格禁止 Prompt 偷跑。任何新元件必須先寫入 dashboardCatalog.ts 並於前端實作完畢後,才允許放進後端 Prompt。
  2. 雷區二:Props 未提供預設值(Default Value)
    • 現象:LLM 生成 Stack 時漏傳了 gap,前端元件直接以 undefined 拼接成 gap-undefined 樣式錯誤。
    • 解法:在 Zod Schema 中強制指定 .default(4),並在 React 元件內部加上解構預設值(gap = 4)。
  3. 雷區三:把大型複雜表單塞進單一 Component
    • 現象:試圖定義一個 MegaCustomerDashboard 元件吃 30 個 props,失去原子化組合的彈性。
    • 解法:拆分成 MetricCardDataTableAlertBanner 等原子元件,由 LLM 透過 Stack 自由組合。

契約治理 Good vs Bad 對比表

治理維度 ❌ 傳統口頭約定 (Bad) ✅ Zod Catalog + 自動對帳 (Good)
契約定義方式 散落在 Markdown 文件或聊天記錄中 單一真實來源(Single Source of Truth),Zod 程式碼化
漂移偵測時機 使用者回報白屏 Bug 時才發現 前端啟動或 CI/CD 自動化測試中即時警示
未知元件處置 React 丟出 Uncaught TypeError 整頁白屏 SmartFallbackComponent 容錯渲染,保留佈局與子節點
Prompt 維護性 手動複製貼上 JSON 結構至 Prompt 由 Schema 自動生成 Prompt 結構化指引,零維護負擔

5. 實機畫面:前後端元件對齊的管理介面

實作系統的「元件型錄 → 型錄管理」頁把今天的主題做成了日常維運工具:型錄存在後端資料庫(供 LLM 生成時參考,僅「啟用」的元件會進 prompt),「前端實作」欄位即時比對該元件是否已在前端 Registry 註冊——若後端有定義但前端未實作,會標示 ⚠ 並在渲染時落到 Fallback 未知元件,缺口一眼可見。

https://ithelp.ithome.com.tw/upload/images/20260823/201612905ZlJ1mbPcX.png

6. 今日動手實作任務與發文備註

🛠️ 今日實作任務

  1. 定義 5 大核心元件 Schema:在前端專案中建立 dashboardCatalog.ts,完成 StackHeadingMetricCardAlertBannerDataTable 的 Zod 定義。
  2. 實作對帳機制:引入 useCatalogReconciliation Hook,刻意在 Catalog 中新增一個前端尚未實作的 RadarChart,驗證控制台是否如期跳出警告且畫面以 SmartFallbackComponent 優雅呈現。
  3. 思考題:如果後端需要動態限制某些初階客戶只能使用 MetricCard 而不能生成 DataTable,這種權限控制應該在後端 GOAP Action 處理,還是在前端 Registry 處理?

上一篇
Day 22:先把畫面骨架搭起來
系列文
讓 AI Agent 真的做事:用 Embabel 打造可控、可測試的智慧 Dashboard23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言