iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Build on Google AI

用 Google AI 生態系 30 天從零打造一個全棧 AI SaaS 服務系列 第 10 篇

Day 10 -【結構化資料】實戰 Structured Outputs:用 JSON Schema 為 Gemini 戴上緊箍咒,前端元件渲染永不白屏!

  • 分享至 

  • xImage
  •  

打造 AI SaaS 最令人沮喪的時刻,莫過於當你的前端 UI 刻得美輪美奐,後端卻因為大語言模型(LLM)多吐了一個字元而當場崩潰。

在過往的開發經驗中,我們常在 Prompt 裡千叮嚀萬萬囑咐:

「請務必回傳標準 JSON 格式,不要加入任何額外解釋或 Markdown 標籤(如 ```json)...」

結果呢?大模型心情好的時候很配合,偶爾卻會在開頭加上「好的,這是為您生成的 JSON:」,或是在最後一筆陣列元素後手滑加了一個逗號(Trailing Comma),甚至是直接漏掉核心欄位。這會直接導致後端或前端的 JSON.parse() 拋出語法錯誤(SyntaxError),讓使用者看到尷尬的白畫面(White Screen of Death)。

在商業級 SaaS 應用中,「不可預測的格式」就是嚴重的系統 Bug。

今天我們將深入探討 Gemini 1.5 提供的殺手級特性 —— Structured Outputs(結構化輸出保證),並用完整的 TypeScript 代碼為 OmniVibe AI 構建強固的資料契約,保證模型輸出的每一筆資料 100% 符合型別定義!


🛑 傳統 Prompt 要求 JSON vs. 原生 Structured Outputs

為什麼以往僅靠 Prompt 要求回傳 JSON 總是會破功?這牽涉到底層的生成機制:

graph TD
    subgraph 傳統 Prompt 做法 (靠運氣)
        A1[下達 Prompt: 請回傳 JSON] --> B1[模型自由預測下一個 Token]
        B1 --> C1[可能產生 Markdown 標籤 或 口語文字]
        C1 --> D1[後端 JSON.parse 隨機崩潰]
    end

    subgraph 原生 Structured Outputs (語法導向約束)
        A2[傳入 JSON Schema 定義] --> B2[解碼器限制 (Constrained Decoding)]
        B2 --> C2[強制 Token 只能沿著合法的 JSON 語法樹生成]
        C2 --> D2[100% 絕對合法的結構化資料]
    end

  • 純 Prompt 引導:模型在標準機率分布下取樣下一個 Token,只要溫度值(Temperature)不為 0,模型就有可能選擇「非合法 JSON 語法」的字元。
  • Gemini 原生 Structured Outputs:Google 在模型推論引擎的解碼層(Decoding Layer)引入了語法約束演算法(Grammar-Guided Decoding)。模型在生成每個 Token 時,不符合給定 Schema 的字元在機率矩陣中會直接被遮罩(Masked Out),因此它在物理上不可能輸出違背 Schema 格式的內容。

📐 定義 OmniVibe AI 的資料契約(Schema Design)

在 OmniVibe AI 的規格中,我們需要模型一次性產出三大結構化資產:

  1. takeaways:核心論點清單。
  2. socialPosts:各社群平台的貼文陣列(標明平台、吸引人的 Hook、內文與 CTA)。
  3. shortClips:適合剪輯短影音的時間戳記分鏡腳本。
  4. executiveSummary:供高層決策的摘要物件。

在 Google Gen AI SDK 中,我們使用 SDK 內建的 SchemaType 列舉來定義精確的 JSON Schema。

1. 建立 TypeScript 型別與 Schema 定義 (src/lib/gemini/schema.ts)

// src/lib/gemini/schema.ts
import { ResponseSchema, SchemaType } from '@google/generative-ai';

// 1. 定義 TypeScript 介面,供前端與後端共用
export interface OmniVibeExtractionResult {
  takeaways: string[];
  socialPosts: Array<{
    platform: 'threads' | 'x' | 'linkedin';
    hook: string;
    body: string;
    hashtags: string[];
    callToAction: string;
  }>;
  shortClips: Array<{
    startTime: string;
    endTime: string;
    headline: string;
    visualCue: string;
    script: string;
  }>;
  executiveSummary: {
    coreThesis: string;
    actionItems: string[];
  };
}

// 2. 定義給 Gemini 解碼器使用的強制 Schema
export const omniVibeResponseSchema: ResponseSchema = {
  type: SchemaType.OBJECT,
  properties: {
    takeaways: {
      type: SchemaType.ARRAY,
      description: '3 到 5 個關鍵的核心洞見與事實論點',
      items: { type: SchemaType.STRING },
    },
    socialPosts: {
      type: SchemaType.ARRAY,
      description: '針對不同社群平台客製化的爆款貼文',
      items: {
        type: SchemaType.OBJECT,
        properties: {
          platform: {
            type: SchemaType.STRING,
            enum: ['threads', 'x', 'linkedin'],
            description: '目標發布社群平台',
          },
          hook: { type: SchemaType.STRING, description: '前 3 行極具吸睛效果的開頭鉤子' },
          body: { type: SchemaType.STRING, description: '條列式重點與乾貨論述' },
          hashtags: {
            type: SchemaType.ARRAY,
            items: { type: SchemaType.STRING },
            description: '熱門關聯標籤',
          },
          callToAction: { type: SchemaType.STRING, description: '引導互動留言的文案' },
        },
        required: ['platform', 'hook', 'body', 'hashtags', 'callToAction'],
      },
    },
    shortClips: {
      type: SchemaType.ARRAY,
      description: '適合剪成 60 秒短影音的分鏡腳本',
      items: {
        type: SchemaType.OBJECT,
        properties: {
          startTime: { type: SchemaType.STRING, description: '格式如 01:23' },
          endTime: { type: SchemaType.STRING, description: '格式如 02:15' },
          headline: { type: SchemaType.STRING, description: '該片段的吸睛標題' },
          visualCue: { type: SchemaType.STRING, description: '畫面剪輯或視覺動作提示' },
          script: { type: SchemaType.STRING, description: '口播台詞精華' },
        },
        required: ['startTime', 'endTime', 'headline', 'visualCue', 'script'],
      },
    },
    executiveSummary: {
      type: SchemaType.OBJECT,
      description: '高層決策筆記',
      properties: {
        coreThesis: { type: SchemaType.STRING, description: '核心論點總結' },
        actionItems: {
          type: SchemaType.ARRAY,
          items: { type: SchemaType.STRING },
          description: '具體可落地的行動方針',
        },
      },
      required: ['coreThesis', 'actionItems'],
    },
  },
  required: ['takeaways', 'socialPosts', 'shortClips', 'executiveSummary'],
};


💻 實作結構化 API Route

在 src/app/api/ai/transform-structured/route.ts 中,我們在呼叫 Gemini 1.5 Flash 時,透過 generationConfig 注入兩項最關鍵的屬性:

  • responseMimeType: 'application/json':告知模型回傳標準 JSON 字串。
  • responseSchema: omniVibeResponseSchema:強制約束資料骨架。
// src/app/api/ai/transform-structured/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { OMNIVIBE_SYSTEM_INSTRUCTION } from '@/lib/gemini/prompts';
import { omniVibeResponseSchema, OmniVibeExtractionResult } from '@/lib/gemini/schema';

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');

export async function POST(req: NextRequest) {
  try {
    const { content } = await req.json();

    if (!content || typeof content !== 'string') {
      return NextResponse.json({ error: '請提供合法的文字內容' }, { status: 400 });
    }

    // 1. 初始化模型並套用約束 Schema
    const model = genAI.getGenerativeModel({
      model: 'gemini-1.5-flash',
      systemInstruction: OMNIVIBE_SYSTEM_INSTRUCTION,
      generationConfig: {
        temperature: 0.3,
        responseMimeType: 'application/json',
        responseSchema: omniVibeResponseSchema, // 關鍵:鎖定 Schema
      },
    });

    const userPrompt = `請分析以下輸入素材,嚴格按照 Schema 要求產出多模態知識矩陣:\n\n${content}`;

    // 2. 呼叫模型
    const result = await model.generateContent(userPrompt);
    const rawText = result.response.text();

    // 3. 安全解析:因為有 Schema 保證,此處的 parse 具備 100% 確定性
    const parsedData: OmniVibeExtractionResult = JSON.parse(rawText);

    return NextResponse.json({
      success: true,
      data: parsedData,
      usageMetadata: result.response.usageMetadata,
    });
  } catch (error: any) {
    console.error('[Structured Output Error]:', error);
    return NextResponse.json(
      { error: '資料結構化轉換失敗', details: error.message },
      { status: 500 }
    );
  }
}


🧪 實測驗證:檢驗回傳純度

使用 curl 對這支全新端點發送測試文本:

curl -X POST http://localhost:3000/api/ai/transform-structured \
  -H "Content-Type: application/json" \
  -d '{
    "content": "在打造 AI SaaS 的過程中,很多團隊死於過度工程化。第一個月應該先專注在用戶是否願意為核心功能付費,而不是一開始就花大錢搭建高併發微服務。善用 Google AI 的 Gemini API 與 Firebase,單兵作戰就能在兩週內完成具備付費驗證的 MVP。"
  }'

伺服器回傳的真實 JSON(節錄):

{
  "success": true,
  "data": {
    "takeaways": [
      "AI SaaS 初期的致命傷在於過度工程化,而非技術深度不夠。",
      "MVP 的核心指標是付費意願驗證,而非過早追求高併發微服務。",
      "採用 Google AI (Gemini) 與 Firebase 可以在兩週內快速完成可商業化的產品閉環。"
    ],
    "socialPosts": [
      {
        "platform": "threads",
        "hook": "90% 的 AI 新創團隊,其實都死在『過度工程化』這把雙面刃下。",
        "body": "很多工程師一上來就規劃 Kubernetes、微服務架構,結果上線第一天根本沒人造訪。\n\n聰明開發者這樣做:\n1. 用 Gemini 1.5 解決核心推論\n2. 搭配 Firebase App Hosting 快速部署\n3. 兩週上線驗證真實付費意願",
        "hashtags": ["#獨立開發", "#VibeCoding", "#SaaS創業"],
        "callToAction": "你在開發初期踩過最大的坑是什麼?歡迎在下方分享!"
      }
    ],
    "shortClips": [
      {
        "startTime": "00:00",
        "endTime": "00:45",
        "headline": "為什麼你的 AI 專案不需要一開始就上微服務?",
        "visualCue": "對著鏡頭敲桌強調,後方背景畫面展示複雜的架構圖叉叉",
        "script": "停!不要再花一個月去架設沒人造訪的高併發架構了..."
      }
    ],
    "executiveSummary": {
      "coreThesis": "新創 AI 產品應以最精簡架構(Gemini + Firebase)實現極速市場驗證。",
      "actionItems": [
        "凍結非必要的微服務重構計畫",
        "兩週內完成整合 Stripe 的端到端 MVP",
        "建立第一批付費種子用戶反饋渠道"
      ]
    }
  }
}

關鍵細節亮點:

  1. 零 Markdown 標籤:輸出開頭不再有 ````json`,結尾也沒有多餘標記,是純正的 JSON 字串。
  2. 嚴格符合 Enum 限制:platform 欄位精準命中 threads、x、linkedin 之一,絕不自創未定義的字串。
  3. 無缺失欄位:所有在 required 陣列中宣告的鍵值全部被完整填補,前端 UI 元件可以直接進行 data.socialPosts.map(...) 渲染,永不觸發 undefined 錯誤!

🎯 總結與明日預告

今天我們為 OmniVibe AI 奠定了最堅實的資料骨架:

  1. 搞懂了傳統 Prompt 要求 JSON 與底層 Constrained Decoding(約束解碼) 的本質差異。
  2. 透過 @google/generative-ai 的 SchemaType 精確繪製了多模態知識提煉的資料契約。
  3. 實作了開箱即用的結構化 API Route,徹底消除前端解析白屏的風險。

現在,我們的 AI 已經具備了讀取長文件、辨識影音以及穩定回傳結構化資料的強大能力。但如果我們希望 AI 能更主動 —— 例如發現輸入內容提到了即時股市行情、或是需要即時搜尋 Google 取得最新趨勢呢?

👉 明天(Day 11),我們將進入【工具調用篇】:實戰 Function Calling(工具調用)!看我們如何讓 Gemini 突破知識庫的時間限制,主動聯網搜尋與調用外部 API,讓靜態模型變身自主 Agent!

敬請期待,我們明天見!🔥


上一篇
Day 09 -【多模態震撼】影音不求人:告別 Whisper!直接上傳音訊與影片,讓 Gemini 辨識語意與時間戳記切片
下一篇
Day 11 -【工具調用】實戰 Function Calling:賦予 Gemini 聯網與外部 API 調用能力,從被動問答進化為自主 Agent!
系列文
用 Google AI 生態系 30 天從零打造一個全棧 AI SaaS 服務 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言