昨天把原本只有一句話的 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。
前面 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。
昨天的資料流程是:
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 協助修改目前的 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 修改程式的範圍還滿重要的。

這次使用的仍然是 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 裡。
首先是新增:
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 輸出的資料結構限制。
這裡有一個前面曾經遇過的問題。
目前 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 準備好之後,下一步就是修改 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 管結構。

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 能成功回來,今天就算完成。
實作完成後,我先沿用 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
沒有多出「插畫建議」、「你知道嗎?」或其他額外欄位。


只有 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 的資料流程。
接著換另一組之前測過的條件:
學習對象:3~4 歲幼兒
學習主題:日常好習慣
卡片數量:7
這次 Server 同樣成功:
[Server] 模型 gemini-2.5-flash 回應成功,字數: 1032
Client 也成功取得 Structured JSON。
實際產生:
🦷 刷牙好健康
🧼 洗手手乾淨
🍎 好好吃飯飯
😴 早睡早起精神好
🧸 玩具收回家
🙏 說謝謝真有禮
🚽 沖水馬桶真乾淨
例如「洗手」這張:
{
"emoji": "🧼",
"title": "洗手手乾淨",
"content": "玩玩具後、吃飯前,還有上完廁所,都要用肥皂把小手洗乾淨。洗手手,細菌跑光光,身體健康又快樂。",
"question": "什麼時候要洗手呢?",
"answer": "玩玩具後、吃飯前和上完廁所。"
}
這次一樣只有我們定義的五個欄位。


把兩次測試整理一下:
| 驗證項目 | Test A | Test B |
|---|---|---|
| 學習對象 | 7~8 歲 | 3~4 歲 |
| 主題 | 浩瀚太空 | 日常好習慣 |
| 指定卡片數 | 3 | 7 |
cards 存在 |
✅ | ✅ |
cards 為 Array |
✅ | ✅ |
| 實際卡片數 | 3 | 7 |
emoji |
✅ | ✅ |
title |
✅ | ✅ |
content |
✅ | ✅ |
question |
✅ | ✅ |
answer |
✅ | ✅ |
| 額外欄位 | 無 | 無 |
至少在這兩次實際測試中,不管是 3 張還是 7 張,Gemini 都維持相同的 JSON 結構。
這就是今天最主要想驗證的事情。
這裡有一個容易混在一起的地方。
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 並不是互相取代,而是各自負責不同的事情。
昨天 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 管結構。
做到這裡,我覺得 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 約束的輸出,是兩件不同的事情。
也不是。
例如 Test A 有一張:
{
"emoji": "☀️",
"title": "太陽和星星",
"content": "太陽是我們地球的大燈泡,它會發光發熱。晚上看到的亮晶晶小點點,那些都是遙遠的星星喔。",
"question": "太陽會發光還是發熱呢?",
"answer": "太陽會發光發熱。"
}
從 Schema 角度來看,它完全符合要求。
五個欄位都有。
但如果從教材內容來看:
「太陽是我們地球的大燈泡」
這種說法是否適合 7~8 歲兒童?
以及:
太陽會發光還是發熱呢?
這個問題是不是夠精準?
就是另外一回事了。
所以:
Schema 正確
≠
內容一定正確
Structured Output 解決的是「資料格式」。
它不是教材品質檢查器。
這部分先記下來,後面再專門處理 AI 產生教材的內容品質。
做到這裡,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 的資料正式接進前端了。
今天做到:
Gemini
↓
Structured JSON
↓
Client
明天繼續:
Structured JSON
↓
response.cards
↓
補上 id
↓
LearningCard[]
↓
React State
↓
Learning Card UI
前面 Day 16 曾經因為缺少 id 被 TypeScript 擋下來。
明天就來看看,同樣的 Data Contract 問題,到了真正串接 Gemini Response 時,要怎麼處理。