昨天 Developer UI 跑起來了,卻只用它按了一下 Run。今天把它真正拿來做它最擅長的事:看清楚每一次 AI 呼叫到底發生了什麼。
還記得昨天那則 503 嗎?當時我就是靠 Trace details 確認呼叫鏈有跑到底,才知道問題不在我的程式。今天把這個功能好好拆開來看。
今天做三件事:
1. 搞懂 Trace 與 Span 是什麼
2. 寫一個兩步驟的小 flow,讓 trace 有東西可看
3. 用 trace 回答三個問題:送出了什麼?花了多久?用了多少 token?
今天沿用昨天的 genkit-day15/ 沙盒,只新增一支檔案,不動 devpulse/。
Day 14 的 gemini.js 想知道「模型到底收到什麼」,得自己 console.log。在 Genkit 裡,這些資訊每一層都自動記錄好了。
昨天的 helloFlow 只有三層,看起來不夠有感。今天在 genkit-day15/ 新增 genkit-trace.js,做成「先組 prompt、再呼叫模型」兩個步驟:
import "dotenv/config";
import { genkit, z } from "genkit";
import { googleAI } from "@genkit-ai/google-genai";
const ai = genkit({
plugins: [googleAI()],
model: googleAI.model("gemini-flash-latest"),
});
export const reviewTraceFlow = ai.defineFlow(
{ name: "reviewTraceFlow", inputSchema: z.string(), outputSchema: z.string() },
async (code) => {
// 步驟 1:ai.run 會在 trace 裡產生一個自訂名稱的 span
const prompt = await ai.run("build-prompt", async () =>
`請指出下面程式碼最主要的一個問題:\n\n${code}`
);
// 步驟 2:呼叫模型
const { text } = await ai.generate({
system: "你是耐心的 Coding 導師,用繁體中文回答,不超過 100 字。",
prompt,
config: { temperature: 0.3 },
});
return text;
}
);
值得看懂的只有兩處:
1. ai.run("build-prompt", ...):把一段普通程式包成一個有名字的步驟,名字會直接出現在 trace 裡,之後找問題很方便。
2. system 與 config:故意放進去,等一下要在 trace 裡確認它們真的有送到模型。
啟動:
genkit start -- node genkit-trace.js
昨天用的
--watch今天先拿掉,原因在文末的踩坑小記。
打開 http://localhost:4000,到 Flows 選 reviewTraceFlow。
Input 欄是 JSON 格式,因為 inputSchema 是 z.string(),程式碼要包成一個字串(換行寫成 \n、內部雙引號要跳脫)。這次用 DevPulse 的經典 Bug:forEach 裡用 await。
"async function sendAll(users) {\n users.forEach(async (u) => {\n await sendMail(u);\n });\n console.log(\"全部寄完\");\n}"
按 Run 之後,展開 trace,呼叫鏈是這樣:
reviewTraceFlow
├─ build-prompt
└─ generate
└─ googleai/gemini-flash-latest
點開不同的 span,各自能看到不同資訊:
| 我想知道 | 去哪裡看 | 今天的發現 |
|---|---|---|
| 模型實際收到什麼? | generate 的 Input |
System prompt 與 User 訊息都如實送出,跟程式碼寫的一致 |
| 模型回了什麼? | generate 的 Output |
抓到 forEach 不會等待 await,建議改用 for...of 或 Promise.all |
| 每一步花了多久? | 各 span 的耗時 | build-prompt 只要 1ms,整個 flow 約 10 秒,幾乎全花在模型呼叫 |
| 用了多少 token? | flow 結果下方的用量標籤 | 顯示成「輸入 / 輸出 / 思考」三個數字 |
最實用的是第一題。以前 Prompt 效果不如預期,只能猜是不是 system 指令沒生效,現在打開 Input 就能直接對照。

下一節的長輸入,第一次跑就遇到 503。打開 trace,build-prompt 是綠色打勾,紅色標示出現在 generate 與底下的模型呼叫。
不用讀完終端機那一長串堆疊,光看呼叫鏈就知道:我的 prompt 組裝沒問題,是模型那端拒絕了請求。用量標籤也顯示 0 in / 0 out,因為請求根本沒被處理。

同一個 flow,換一段約 40 行、包含 6 個經典 Bug 的長輸入(SQL 注入、深層取值沒防禦、迴圈邊界錯誤、金鑰寫死等),跟短輸入各跑一次:
| 輸入 | 輸入 token | 輸出 token | 思考 token | 耗時 |
|---|---|---|---|---|
| 短(6 行) | 83 | 69 | 618 | 11.27 秒 |
| 長(約 40 行) | 316 | 53 | 804 | 10.28 秒 |
三個觀察:
先講清楚限制: 每種只跑一次。長輸入在免費額度連續遇到 503,最後改用付費 API key 才跑通,兩次條件不同,這組數字只能當「觀察」,不能當嚴謹的效能結論。付費 key 只用於這次測試,之後的系列仍以免費額度為主。

今天連續踩到三種「看起來都像壞掉」的狀況,最後發現是不同的問題:
| 現象 | 實際原因 | 怎麼分辨 |
|---|---|---|
Run 之後 trace 卡在 generate 轉圈快一分鐘 |
--watch 偵測到 node_modules 內檔案變動而重啟,中斷了執行中的請求 |
終端機出現 Restarting 'genkit-trace.js' |
503 Service Unavailable |
Google 模型端流量過大,與我的程式無關 | 展開 trace,build-prompt 綠色打勾,紅色出現在模型呼叫那層 |
Failed to save trace ... fetch failed |
程式連不上 Genkit 的 Telemetry 服務 | 終端機出現 Failed to save trace |
前兩個靠終端機訊息或 trace 就能判斷,第三個關掉舊視窗、拿掉 --watch 乾淨重啟後就正常了。
教訓是:Developer UI 出問題時,先分清楚是「模型端」、「程序被重啟」還是「trace 傳輸」,再決定要不要懷疑自己的程式碼。
今天用 defineFlow 包了一個 flow,但只是為了讓 trace 有東西看,對它的輸入輸出型別、錯誤處理都還沒細講。
明天來正式拆解:Day 17:定義高強韌流程,用 defineFlow 封裝代碼分析邏輯。
參考資料