iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Build on Google AI

今天學什麼?30 天用 Google AI 打造智慧學習卡系列 第 20 篇

Day 20|讓 Gemini API 也回傳固定格式:把 Structured Output 接進程式

  • 分享至 

  • xImage
  •  

昨天把原本只有一句話的 Dynamic Prompt,整理成了獨立的 Prompt Template。

原本只是:

請幫我產生適合 {ageGroup} 的『{topic}』學習卡,共 {cardCount} 張。

到了 Day 19,開始加入角色、學習條件、欄位與內容規則。

實際測試後,Gemini 的回答確實穩定不少,每張卡片基本上都按照:

Emoji
Title
Content
Question
Answer

產生內容。

看起來好像已經解決格式問題了。

但仔細看 API 回來的資料:

Emoji: 🧼
Title: 飯前飯後洗洗手
Content: ...
Question: ...
Answer: ...

它本質上還是一段文字。

也就是說,雖然人看得懂這些欄位代表什麼,但程式還是得自己想辦法拆解。

我真正希望拿到的是:

{
  "cards": [
    {
      "emoji": "🧼",
      "title": "飯前飯後洗洗手",
      "content": "...",
      "question": "...",
      "answer": "..."
    }
  ]
}

所以今天要把前面在 Google AI Studio 實驗過的 Structured Output,正式搬進目前的 Learning Card App。


回到之前做過的 Structured Output

其實這不是第一次碰 Structured Output。

前面 Day 09~Day 12,我已經從 Gemini 回傳格式不固定的問題,一路做到:

Prompt
↓
Structured Output
↓
JSON Schema
↓
LearningCardResponse

最後決定 Gemini 回傳的資料格式為:

{
  "cards": [
    {
      "emoji": "🐢",
      "title": "認識海龜",
      "content": "...",
      "question": "...",
      "answer": "..."
    }
  ]
}

當時主要是在 Google AI Studio 裡驗證:

Gemini 能不能按照指定的 Schema 產生 Learning Card?

而現在 App 已經有表單、Prompt Builder 和 Server-side Gemini API。

所以今天要做的,就是把前面的 Structured Output 實驗真正接進 App。


Day 19 和 Day 20 差在哪?

昨天的資料流程是:

Form Data
↓
Prompt Builder
↓
Prompt Template
↓
Gemini API
↓
Text Response

今天希望變成:

Form Data
↓
Prompt Builder
↓
Prompt Template
↓
Gemini API
+
Structured Output Schema
↓
Structured JSON

也就是今天先不碰真正的 Learning Card UI。

只確認一件事情:

Gemini API 能不能按照我們定義的 Schema,回傳固定結構的 JSON?


這次我怎麼請 Gemini 修改程式?

這次一樣直接讓 Gemini 協助修改目前的 App。

不過因為前幾天已經發現,AI 很容易在完成需求的同時「順便多做一點」,所以這次我把 Scope 寫得更清楚。

今天只做到 Structured Output,不要提前把資料接進 Learning Card UI。

我輸入的 Prompt:

目前這個 Learning Card App 已經完成:

Form Data
→ buildLearningCardPrompt()
→ Prompt Template
→ POST /api/generate-cards
→ server.ts
→ Gemini API
→ Text Response
→ React Client

目前 server.ts 已經使用 @google/genai 呼叫 Gemini API,
API Key 保留在 Server-side。

現在請幫我把 Gemini API 的 Response 改成 Structured Output,
使用 @google/genai 目前支援的 JSON Schema 設定。

目標 Response 格式:

{
  "cards": [
    {
      "emoji": "🐢",
      "title": "認識海龜",
      "content": "海龜住在大海裡。",
      "question": "海龜住在哪裡?",
      "answer": "大海裡。"
    }
  ]
}

Schema 規則:

- Root 必須是一個 Object
- Root 必須包含 cards
- cards 必須是一個 Array
- 每一個 Card 必須是一個 Object
- 每張 Card 必須包含以下欄位:
  - emoji: string
  - title: string
  - content: string
  - question: string
  - answer: string
- 上述五個欄位全部 required
- cards 的數量應符合表單傳入的 cardCount

如果適合,請把 Schema 獨立放在:

src/schemas/learningCardSchema.ts

並在 server.ts 呼叫 Gemini API 時加入 Structured Output 設定,例如:

responseMimeType: 'application/json'
responseSchema: learningCardResponseSchema

請保留目前:

- buildLearningCardPrompt()
- Prompt Template
- POST /api/generate-cards
- Server-side GEMINI_API_KEY
- CardGeneratorForm
- oceanCards
- LearningCard type
- 其他既有 Learning Card 功能

這次先不要:

- 把 Gemini Response 轉成 App 使用的 LearningCard[]
- 自動加入 id
- 用 Gemini Response 取代 oceanCards
- 修改目前 Learning Card UI
- 修改既有 Learning Card 資料

Structured Output 成功後:

1. Server Console 印出 Gemini 回傳的 Structured JSON
2. API 將解析後的資料回傳給 Client
3. Client Console 印出 Structured JSON
4. 可以在目前 Response Preview 顯示格式化後的 JSON

今天只完成:

Form Data
→ Prompt Builder
→ Prompt Template
→ Gemini API + Structured Output Schema
→ Structured JSON Response

不要提前實作 LearningCard[] 與 UI 串接。

這次我特別把:

這次先不要:

以及最後的:

不要提前實作 LearningCard[] 與 UI 串接。

寫得很清楚。

除了告訴 AI「要做什麼」,也開始告訴它:

今天不要做什麼。

這幾天實際用下來,我覺得這對控制 AI 修改程式的範圍還滿重要的。

Day20 prompt


Gemini 實際修改了什麼?

這次使用的仍然是 Gemini 3.8 Flash。

執行結果:

Gemini 3.8 Flash
Ran for 79s

Edited 3 files

src/schemas/learningCardSchema.ts
server.ts
src/components/CardGeneratorForm.tsx

Built

主要修改內容可以分成三部分:

learningCardSchema.ts
→ 定義 Structured Output Schema

server.ts
→ Gemini API 加入 Structured Output

CardGeneratorForm.tsx
→ 接收並顯示 Structured JSON

而這次 Gemini 沒有去修改:

oceanCards
LearningCard type
既有 Learning Card 資料
目前的 Learning Card UI

也沒有提前幫 Response 補 id 或轉成 LearningCard[]。

這次修改範圍基本上有守在今天設定的 Scope 裡。


把 Schema 獨立出來

首先是新增:

src/schemas/learningCardSchema.ts

這裡負責定義 Gemini Response 應該長什麼樣子。

整體結構可以理解成:

Response
└── cards
    ├── Card
    │   ├── emoji
    │   ├── title
    │   ├── content
    │   ├── question
    │   └── answer
    ├── Card
    └── ...

Root 是 Object,裡面一定要有 cards。

cards 是 Array。

Array 裡的每一個 Card 又是一個 Object,而且必須包含:

emoji
title
content
question
answer

五個欄位都是必填。

這樣就把原本寫在 Prompt 裡的「希望你按照這些欄位回答」,進一步變成 API 輸出的資料結構限制。


為什麼 Schema 裡沒有 id?

這裡有一個前面曾經遇過的問題。

目前 App 真正使用的 LearningCard 是:

interface LearningCard {
  id: string;
  emoji: string;
  title: string;
  pinyin?: string;
  content: string;
  question: string;
  answer: string;
  parentTip?: string;
  soundName?: string;
}

其中:

id: string;

是 required。

那為什麼今天不直接要求 Gemini 一起產生 id?

因為 Day 16 把 Day 12 的 Structured Output 放進 React 時,就曾經遇過:

Property 'id' is missing...

後來我才更清楚地發現:

Gemini Output Model
        ≠
Frontend View Model

Gemini 負責的是教材內容:

emoji
title
content
question
answer

而 id 是 App 自己管理資料時需要的欄位。

所以今天沒有為了配合 React,反過來要求 Gemini 幫每張卡片產生 id。

這件事情留給前端自己處理。


把 Schema 接進 Gemini API

Schema 準備好之後,下一步就是修改 server.ts。

這次最重要的 Structured Output 設定是:

config: {
  responseMimeType: 'application/json',
  responseSchema: learningCardResponseSchema,
}

可以先簡單理解成:

responseMimeType
→ 希望 Response 使用 JSON

responseSchema
→ JSON 必須符合什麼資料結構

所以現在送給 Gemini 的,不只有 Prompt。

還多了一份 Schema:

Prompt
→ 告訴 Gemini「內容要產生什麼」

Schema
→ 告訴 Gemini「資料要長什麼樣子」

這也再次回到前面做 Structured Output 時整理出的概念:

Prompt 管內容,Schema 管結構。

Day20 new learningCardSchema


今天先不要碰 Learning Card UI

Structured Output 接好之後,其實下一步很容易就會想:

那是不是可以直接把這些資料顯示成 Learning Card?

但我今天刻意先不做。

因為如果一次完成:

API
→ Schema
→ JSON
→ LearningCard[]
→ State
→ UI

中間任何一段有問題,Debug 範圍就會變大。

所以今天的終點只設定在:

Form Data
↓
Prompt Builder
↓
Prompt Template
↓
Gemini API + Schema
↓
Structured JSON
↓
Console / Preview

只要 JSON 能成功回來,今天就算完成。


Test A:7~8 歲+浩瀚太空+3 張

實作完成後,我先沿用 Day 18、Day 19 測試過的第一組條件:

學習對象:7~8 歲兒童
學習主題:浩瀚太空
卡片數量:3

Client 取得的 Form Data:

{
  "ageGroup": "7~8 歲兒童",
  "topic": "浩瀚太空",
  "cardCount": 3
}

接著一樣經過 Day 19 建立的 Prompt Builder。

Server Console 這次則多了一個很重要的訊息:

[Server] 嘗試呼叫模型:
gemini-2.5-flash (使用 Structured Output)

最後成功取得 Response:

[Server] 模型 gemini-2.5-flash 回應成功,字數: 399

這次 Gemini 回來的不再是:

Emoji: ...
Title: ...
Content: ...
Question: ...
Answer: ...

而是:

{
  "cards": [
    {
      "emoji": "🚀",
      "title": "太空之旅",
      "content": "太空是個好大好大的地方,裡面住著好多星星和美麗的行星。地球就是我們住的行星喔!",
      "question": "我們住在哪個行星上呢?",
      "answer": "地球"
    },
    {
      "emoji": "☀️",
      "title": "太陽和星星",
      "content": "太陽是我們地球的大燈泡,它會發光發熱。晚上看到的亮晶晶小點點,那些都是遙遠的星星喔。",
      "question": "太陽會發光還是發熱呢?",
      "answer": "太陽會發光發熱。"
    },
    {
      "emoji": "🌕",
      "title": "月亮的故事",
      "content": "晚上我們會看到月亮掛在天上。月亮是地球的好朋友,它會繞著地球跑,有時候圓圓的,有時候彎彎的。",
      "question": "月亮是誰的好朋友呢?",
      "answer": "月亮是地球的好朋友。"
    }
  ]
}

這次確實產生了 3 張卡片。

而且每一張都有:

emoji
title
content
question
answer

沒有多出「插畫建議」、「你知道嗎?」或其他額外欄位。

Day20 testa 7-8 space 3 cards 01

Day20 testa 7-8 space 3 cards 02


Server 有 JSON,Client 也真的收到了嗎?

只有 Server 印出 JSON 還不夠。

最後 React 還是得拿到這份資料。

這次 Client Console 也成功出現:

[Client] 成功取得 Gemini Structured JSON Response:

後面就是完整的:

{
  "cards": [...]
}

Server 端也有:

[Server] Gemini 回傳的 Structured JSON:

所以目前可以確認整條流程已經跑到:

React Form
↓
Prompt Builder
↓
Prompt Template
↓
Server
↓
Gemini + Structured Output
↓
Structured JSON
↓
Server
↓
React Client

Structured Output 不再只是之前在 Google AI Studio 裡單獨做的實驗,而是真的進入目前 App 的資料流程。


Test B:3~4 歲+日常好習慣+7 張

接著換另一組之前測過的條件:

學習對象:3~4 歲幼兒
學習主題:日常好習慣
卡片數量:7

這次 Server 同樣成功:

[Server] 模型 gemini-2.5-flash 回應成功,字數: 1032

Client 也成功取得 Structured JSON。

實際產生:

🦷 刷牙好健康
🧼 洗手手乾淨
🍎 好好吃飯飯
😴 早睡早起精神好
🧸 玩具收回家
🙏 說謝謝真有禮
🚽 沖水馬桶真乾淨

例如「洗手」這張:

{
  "emoji": "🧼",
  "title": "洗手手乾淨",
  "content": "玩玩具後、吃飯前,還有上完廁所,都要用肥皂把小手洗乾淨。洗手手,細菌跑光光,身體健康又快樂。",
  "question": "什麼時候要洗手呢?",
  "answer": "玩玩具後、吃飯前和上完廁所。"
}

這次一樣只有我們定義的五個欄位。

Day20 testb 3-4 hobby 7 cards 01

Day20 testb 3-4 hobby 7 cards 02


兩組 Structured Output 都成功了

把兩次測試整理一下:

驗證項目 Test A Test B
學習對象 7~8 歲 3~4 歲
主題 浩瀚太空 日常好習慣
指定卡片數 3 7
cards 存在 ✅ ✅
cards 為 Array ✅ ✅
實際卡片數 3 7
emoji ✅ ✅
title ✅ ✅
content ✅ ✅
question ✅ ✅
answer ✅ ✅
額外欄位 無 無

至少在這兩次實際測試中,不管是 3 張還是 7 張,Gemini 都維持相同的 JSON 結構。

這就是今天最主要想驗證的事情。


卡片數量也是 Schema 保證的嗎?

這裡有一個容易混在一起的地方。

Test A:

cardCount = 3

Gemini 產生了 3 張。

Test B:

cardCount = 7

Gemini 也真的產生了 7 張。

但目前「這次要產生幾張」主要還是寫在 Prompt Template:

- 卡片數量:{cardCount}

請依照 cardCount 產生指定數量的 Learning Card。

而 Schema 主要負責:

cards 必須是 Array

每一個 Card 必須包含:
emoji
title
content
question
answer

換句話說:

Prompt
→ 這次產生幾張、內容要寫什麼

Schema
→ 每張資料必須長什麼樣子

所以不能因為這兩次剛好得到 3 張和 7 張,就直接說:

Schema 保證了 cardCount。

目前比較準確的說法是:

兩次測試中,Gemini 都按照 Prompt 指定的 cardCount 產生了正確數量。

Prompt 和 Schema 並不是互相取代,而是各自負責不同的事情。


那 Day 19 的 Emoji 重複問題呢?

昨天 Test B 有一個有趣的小地方:

Emoji: 🧼
Title: 🧼 飯前飯後洗洗手

明明已經有獨立的 Emoji 欄位,Gemini 還是自己把 Emoji 放進 Title。

今天 Test B 則變成:

{
  "emoji": "🧼",
  "title": "洗手手乾淨"
}

而且這次 7 張都沒有再發生 Emoji 重複。

看起來好像是 Structured Output 解決了這個問題?

其實還不能這樣說。

因為目前 Schema 只規定:

emoji → string
title → string

如果 Gemini 哪次回:

{
  "emoji": "🧼",
  "title": "🧼 洗手手乾淨"
}

它依然符合目前的 Schema。

所以這次只能說:

兩次測試剛好沒有再出現 Emoji 重複。

不能說 Schema 保證 Title 裡不會出現 Emoji。

如果真的希望限制這種內容規則,還是得從 Prompt 或後續資料驗證下手。

這也再次讓前面的概念變得更具體:

Prompt 管內容,Schema 管結構。


Text Response 和 Structured Output 差在哪?

做到這裡,我覺得 Day 19 和 Day 20 的差異終於變得很明顯。

Day 19 的 Response:

Emoji: 🧼
Title: 飯前飯後洗洗手
Content: ...
Question: ...
Answer: ...

人看得懂,也看得出它「好像有固定格式」。

但如果程式真的要處理,還是得想辦法辨識:

哪裡是 Emoji?
哪裡是 Title?
哪裡是 Content?
哪裡是 Question?
哪裡是 Answer?

今天加入 Structured Output 後:

{
  "cards": [
    {
      "emoji": "🧼",
      "title": "洗手手乾淨",
      "content": "...",
      "question": "...",
      "answer": "..."
    }
  ]
}

現在:

cards 就是 cards
emoji 就是 emoji
title 就是 title
question 就是 question
answer 就是 answer

不用再從一整段自然語言或 Markdown 中猜哪一段代表哪個欄位。

這也是為什麼對 App 來說:

看起來有結構的文字,和真正受到 Schema 約束的輸出,是兩件不同的事情。


有 Schema,就代表 AI 內容一定正確嗎?

也不是。

例如 Test A 有一張:

{
  "emoji": "☀️",
  "title": "太陽和星星",
  "content": "太陽是我們地球的大燈泡,它會發光發熱。晚上看到的亮晶晶小點點,那些都是遙遠的星星喔。",
  "question": "太陽會發光還是發熱呢?",
  "answer": "太陽會發光發熱。"
}

從 Schema 角度來看,它完全符合要求。

五個欄位都有。

但如果從教材內容來看:

「太陽是我們地球的大燈泡」

這種說法是否適合 7~8 歲兒童?

以及:

太陽會發光還是發熱呢?

這個問題是不是夠精準?

就是另外一回事了。

所以:

Schema 正確
≠
內容一定正確

Structured Output 解決的是「資料格式」。

它不是教材品質檢查器。

這部分先記下來,後面再專門處理 AI 產生教材的內容品質。


JSON 回來了,但還不能直接丟進目前的 UI

做到這裡,Gemini 已經可以回:

{
  "cards": [
    {
      "emoji": "🦷",
      "title": "刷牙好健康",
      "content": "...",
      "question": "...",
      "answer": "..."
    }
  ]
}

但目前 App 使用的 LearningCard 是:

interface LearningCard {
  id: string;
  emoji: string;
  title: string;
  pinyin?: string;
  content: string;
  question: string;
  answer: string;
  parentTip?: string;
  soundName?: string;
}

其中最明顯的差異就是:

id: string;

Gemini 沒有產生 id。

而這是刻意的。

因為 id 是 App 內部需要的資料,不需要交給 AI 決定。

所以現在還不能直接說:

Gemini Response = LearningCard[]

目前比較準確的是:

Gemini Structured JSON
↓
{
  cards: [...]
}

接下來還需要:

response.cards
↓
補上 App 需要的 id
↓
LearningCard[]
↓
React State
↓
Learning Card UI

這就是下一步要處理的事情。


今天的小結

今天沒有新增新的 Learning Card 功能,也沒有讓 Gemini 產生的卡片直接取代目前畫面上的 oceanCards。

做的事情其實很集中:

Prompt Template
↓
Gemini API
+
Structured Output
↓
Structured JSON

實作上新增了獨立的 Schema,並在 Gemini API 加入:

config: {
  responseMimeType: 'application/json',
  responseSchema: learningCardResponseSchema,
}

接著分別測試:

7~8 歲兒童
+
浩瀚太空
+
3 張

以及:

3~4 歲幼兒
+
日常好習慣
+
7 張

兩次都成功取得:

{
  "cards": [
    {
      "emoji": "...",
      "title": "...",
      "content": "...",
      "question": "...",
      "answer": "..."
    }
  ]
}

而且兩次測試的卡片數量都符合 Prompt 的要求,每張 Card 也都有 Schema 定義的五個欄位。

從 Day 19 到 Day 20,看起來只是多了一份 Schema,但對程式來說差異很大。

昨天比較像是:

「請按照這個格式回答」

今天則開始變成:

「輸出的資料結構必須符合這份 Schema」

也讓之前那句話變得更具體:

Prompt 管內容,Schema 管結構。

現在 Gemini 終於能回傳 App 比較容易處理的 Structured JSON 了。

不過新的問題也跟著出現:

JSON 有了,要怎麼真的變成 React 現在使用的 LearningCard[]?

尤其 Gemini 沒有 id,但 App 的 LearningCard 卻要求 id。

看來下一步,就是把 AI 的資料正式接進前端了。


明天預告

Day 21|JSON 回來了,React 怎麼接?把 Gemini Response 變成 LearningCard[]

今天做到:

Gemini
↓
Structured JSON
↓
Client

明天繼續:

Structured JSON
↓
response.cards
↓
補上 id
↓
LearningCard[]
↓
React State
↓
Learning Card UI

前面 Day 16 曾經因為缺少 id 被 TypeScript 擋下來。

明天就來看看,同樣的 Data Contract 問題,到了真正串接 Gemini Response 時,要怎麼處理。


上一篇
Day 19|API 接通了,但 Prompt 太簡單:把學習條件整理成 Prompt Template
系列文
今天學什麼?30 天用 Google AI 打造智慧學習卡 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言