iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Build on Google AI

30 天玩轉 Google AI 全家桶:初學者的隨身 Coding 助理養成記系列 第 11 篇

Day 11:讓格式更可靠:用 Zod 幫 Gemini 回應加上一道型別防線

  • 分享至 

  • xImage
  •  

Day 11:讓格式絕對可靠:結合 Zod 定義資料型別

昨天結尾丟出三個問題,一直沒有解決:

欄位會不會缺少?型別會不會錯?資料到底有沒有真的符合規則?

今天要老實面對一件事:Day10 那份 responseSchema,其實只是「拜託 Gemini 照這個格式回答」的一份請求書。它放在 response_format 裡,會讓 Gemini 大部分時候都乖乖照做,但**「大部分時候」不等於「保證」**。

程式這一端,到目前為止完全沒有自己的防線。今天要補上的,就是這道防線——過程中還意外踩到一個真實的坑,比預期中更有收穫。


一、Schema 說給 AI 聽,跟說給程式聽,是兩件事

Day10 的 responseSchema 只活在一個地方:送出 request 的那一刻,告訴 Gemini「我要的資料長這樣」。

但收到回應之後呢?程式做的事只有一行:

const result = JSON.parse(interaction.output_text);

JSON.parse() 只在乎「這是不是合法 JSON」,完全不在乎欄位對不對、型別對不對。如果哪次 Gemini 回傳的欄位跟預期兜不起來,JSON.parse() 照樣成功,程式接著讀取欄位只會拿到 undefined,錯誤要等到很後面才會爆出來,而且爆的地方通常跟真正的原因完全無關。

所以今天要做的,不是取代 response_format,而是在它後面再加一道程式自己的驗證關卡。


二、用 Zod 定義一次,兩邊都用

如果 API 要一份 JSON Schema、程式驗證又要另一份規則,等於同一件事寫兩遍。比較乾淨的做法:用 Zod 定義一份規則,同時拿去做兩件事——轉成 API 要的 JSON Schema,也拿來驗證 Gemini 實際回傳的東西:

npm install zod
import { z } from "zod";

const AnalysisResult = z.object({
  issue: z.string(),
  suggestion: z.string(),
});

const responseSchema = z.toJSONSchema(AnalysisResult);

AnalysisResult 這個 Zod 物件,之後同時擔任兩個角色:轉成 JSON Schema 塞進 response_format,也拿來驗證 Gemini 實際回傳的東西。單一事實來源,不用維護兩份,而且不需要額外安裝任何轉換套件——Zod 4 已經內建 z.toJSONSchema()。


三、核心代碼:safeParse 才是重點

import 'dotenv/config';
import { GoogleGenAI } from "@google/genai";
import { z } from "zod";

const ai = new GoogleGenAI({});

const AnalysisResult = z.object({
  issue: z.string(),
  suggestion: z.string(),
});

async function analyzeCode(code) {
  const interaction = await ai.interactions.create({
    model: "gemini-3-flash-preview",
    input: `請分析下面這段程式碼,找出最主要的問題,並提出修正建議。

\`\`\`javascript
${code}
\`\`\``,
    generation_config: {
      thinking_level: "minimal",
    },
    response_format: {
      type: "text",
      mime_type: "application/json",
      schema: z.toJSONSchema(AnalysisResult),
    },
  });

  console.log("Gemini 原始回傳:");
  console.log(interaction.output_text);

  const rawResult = JSON.parse(interaction.output_text);
  const validation = AnalysisResult.safeParse(rawResult);

  if (!validation.success) {
    console.error("⚠️ Gemini 回傳的資料不符合預期格式:");
    console.error(validation.error.format());
    throw new Error("AnalysisResult 驗證失敗,已攔截,不往下執行");
  }

  return validation.data;
}

const analysisResult = await analyzeCode(`
const user = {};
console.log(user.profile.name);
`);

console.log("\n=== 分析結果 ===");
console.log("問題:", analysisResult.issue);
console.log("建議:", analysisResult.suggestion);

跟昨天比,多出來的不是 JSON.parse() 之後那一步,而是用 safeParse() 攔在它後面。parse() 驗證失敗會直接 throw,讓整支腳本崩潰;safeParse() 回傳的是 { success, data } 或 { success, error },可以先檢查、印出清楚原因,再決定要中止、重試,還是給一個預設值,主控權留在自己手上。

console.log(interaction.output_text) 這行我刻意保留在正式代碼裡,不是寫好就刪掉的除錯用垃圾——等一下你就知道為什麼。


四、執行成果與反饋

實際跑一次,終端機輸出長這樣:

Gemini 原始回傳:
{
  "issue": "TypeError: Cannot read properties of undefined (reading 'name')",
  "suggestion": "Accessing 'user.profile.name' fails because 'user.profile' is undefined. Use Optional Chaining (?.) or initialize the object structure."
}

=== 分析結果 ===
問題: TypeError: Cannot read properties of undefined (reading 'name')
建議: Accessing 'user.profile.name' fails because 'user.profile' is undefined. Use Optional Chaining (?.) or initialize the object structure.

欄位對、型別對、內容也切題,safeParse() 順利放行。但這份乾淨的結果,其實是繞了一圈路才拿到的。


五、今天的踩坑:一個「應該可以」卻默默失敗的版本

第一版代碼,我用的是網路上最常見的教學寫法:先用 Zod 定義 schema,再用第三方套件 zod-to-json-schema 把它轉成 API 要的 JSON Schema:

import { zodToJsonSchema } from "zod-to-json-schema";

response_format: {
  type: "text",
  mime_type: "application/json",
  schema: zodToJsonSchema(AnalysisResult),
},

裝套件、補齊 import,程式順利跑完,JSON.parse() 也沒報錯——結果 safeParse() 卻攔了下來:

⚠️ Gemini 回傳的資料不符合預期格式:
{
  _errors: [],
  issue: { _errors: [ 'Invalid input: expected string, received undefined' ] },
  suggestion: { _errors: [ 'Invalid input: expected string, received undefined' ] }
}

JSON.parse() 成功,代表 Gemini 確實回了一包合法 JSON;但 issue、suggestion 都是 undefined,代表那包 JSON 裡根本沒有這兩個欄位。也就是說:schema 沒有真的把 Gemini 綁住,它是憑著 prompt 自由發揮回的內容,剛好跟我要的欄位對不上。

查了一下才發現,zod-to-json-schema 這個套件在 Zod 4 生態圈裡已經逐漸被取代——Zod 4 版本內建了原生的 z.toJSONSchema(),官方文件現在也直接用這個方法示範跟 Gemini 這類 API 的整合,不少知名專案的 Zod 4 遷移指南也都明講:升級後直接移除 zod-to-json-schema 依賴即可。換句話說,我用的那個轉換套件,跟我當時的 Zod 版本本來就不是最佳搭檔。

把 zodToJsonSchema(AnalysisResult) 換成 Zod 內建的 z.toJSONSchema(AnalysisResult),不用多裝任何套件,問題就解決了——也就是上面第三段代碼的最終版本。

至於當初那個第三方套件的輸出「具體」哪裡跟 Gemini 兜不起來,我沒有再往下深挖——修好就先往前走,這是鐵人賽該有的取捨。但這也是我把 console.log(interaction.output_text) 留在正式代碼裡的原因:下次再遇到「格式看起來對,內容卻是空的」,第一件事就是先印出 AI 到底回了什麼,而不是假設 schema 一定有生效。


六、今天真正學到的事情

Day10 讓 Gemini「照格式回答」;Day11 讓程式「不要照單全收」。

response_format + schema
↓
拜託 Gemini 照格式回答

Zod safeParse
↓
就算 Gemini 沒做到,程式也不會被騙

資料格式現在真的穩了——欄位不會少、型別不會錯,錯了也會在第一時間被攔下來,而不是流竄到看不見的地方。

但穩定的資料只是起點。DevPulse 現在能穩穩「分析」一段代碼,可下一步呢——如果我想讓 Gemini 不只是回答問題,而是主動去「做」一件事,比如查一下現在幾點、讀一個檔案?

這已經不是格式問題了,是另一種能力。明天來看 Function Calling。


上一篇
Day 10:拒絕廢話連篇:強制 Gemini 回傳標準 JSON
下一篇
Day 12:讓 AI 連接外部世界:Gemini Function Calling 極簡體驗
系列文
30 天玩轉 Google AI 全家桶:初學者的隨身 Coding 助理養成記 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言