昨天結尾丟出三個問題,一直沒有解決:
欄位會不會缺少?型別會不會錯?資料到底有沒有真的符合規則?
今天要老實面對一件事:Day10 那份 responseSchema,其實只是「拜託 Gemini 照這個格式回答」的一份請求書。它放在 response_format 裡,會讓 Gemini 大部分時候都乖乖照做,但**「大部分時候」不等於「保證」**。
程式這一端,到目前為止完全沒有自己的防線。今天要補上的,就是這道防線——過程中還意外踩到一個真實的坑,比預期中更有收穫。
Day10 的 responseSchema 只活在一個地方:送出 request 的那一刻,告訴 Gemini「我要的資料長這樣」。
但收到回應之後呢?程式做的事只有一行:
const result = JSON.parse(interaction.output_text);
JSON.parse() 只在乎「這是不是合法 JSON」,完全不在乎欄位對不對、型別對不對。如果哪次 Gemini 回傳的欄位跟預期兜不起來,JSON.parse() 照樣成功,程式接著讀取欄位只會拿到 undefined,錯誤要等到很後面才會爆出來,而且爆的地方通常跟真正的原因完全無關。
所以今天要做的,不是取代 response_format,而是在它後面再加一道程式自己的驗證關卡。
如果 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()。
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。