前幾天,我們一路把 Gemini 的回答變得越來越「像程式」。
Day 10,我們讓 Gemini 不再隨便輸出一大段文字,而是要求它回傳固定格式的 JSON。
Day 11,再往前一步,用 Zod 驗證這份資料,確保欄位與型別真的符合我們的預期。
做到這裡,DevPulse 已經可以比較可靠地「讀懂 Gemini 回傳的資料」。
但還有一個問題。
如果我問 Gemini:
台灣現在幾點?
Gemini 就算知道怎麼回答,也不代表它真的能直接取得我電腦目前的時間。
這時候,就需要今天的主角:
Function Calling。
先不要被名字嚇到。
Function Calling 可以先理解成:
讓 Gemini 判斷「現在需不需要使用某個工具」。
例如我們在程式裡準備一個函式:
getLocalTime("Asia/Taipei")
這個函式真的可以取得目前時間。
但 Gemini 並不會直接執行這段 JavaScript。
它真正做的事情比較像是:
使用者
↓
「台灣現在幾點?」
↓
Gemini
↓
我要呼叫 get_local_time
↓
Node.js 執行函式
↓
取得目前時間
↓
把結果交回 Gemini
↓
Gemini 整理成自然語言回答
Google 官方目前的 Function Calling 流程也是這樣設計:模型先產生 function_call,由應用程式執行實際函式,再把 function_result 傳回模型。
所以今天其實不是在教 Gemini「怎麼執行程式」。
而是在教 Gemini:
什麼時候該請我的程式幫它做事情。
這次我們故意不用天氣 API、股票 API 或其他第三方服務。
只做一個非常簡單的工具:
get_local_time
輸入:
Asia/Taipei
輸出:
目前的台灣時間
原因很簡單。
今天真正想學的不是「怎麼串第三方 API」,而是:
Gemini 怎麼提出工具呼叫,以及我們的 Node.js 怎麼接住它。
所以先把實驗縮到最小。
如果延續前幾天的 Node.js 專案,可以直接建立一個新的資料夾:
mkdir day12-function-calling
cd day12-function-calling
npm init -y
npm install @google/genai dotenv
npm pkg set type=module
npm pkg set scripts.start="node index.js"
完成後,建立:
day12-function-calling/
├── .env
├── .gitignore
├── index.js
└── package.json
.envGEMINI_API_KEY=你的_Gemini_API_Key
.gitignorenode_modules/
.env
這裡延續 Day 2~Day 8 的安全觀念:API Key 不直接寫進程式碼,也不要提交到 GitHub。
接下來建立 index.js:
import "dotenv/config";
import { GoogleGenAI } from "@google/genai";
const MODEL = "gemini-3.8-flash";
const ai = new GoogleGenAI({});
接著告訴 Gemini:
「我有一個叫做 get_local_time 的工具。」
const getLocalTimeTool = {
type: "function",
name: "get_local_time",
description: "取得指定 IANA 時區目前的日期與時間。",
parameters: {
type: "object",
properties: {
timezone: {
type: "string",
description: "IANA 時區,例如 Asia/Taipei。",
},
},
required: ["timezone"],
},
};
這裡會看到幾個熟悉的欄位:
name
description
parameters
是不是有點像昨天的 Schema?
沒錯。
因為我們必須先告訴 Gemini:
工具名稱:
get_local_time
它能做什麼?
取得指定時區的目前時間
需要什麼參數?
timezone,而且必須是字串
Google 官方目前的 Function Calling 寫法,也是透過 type: "function"、name、description 和 parameters 描述工具介面。
接下來,才是我們自己真正負責的部分。
function getLocalTime(timezone) {
try {
const formatter = new Intl.DateTimeFormat("zh-TW", {
timeZone: timezone,
dateStyle: "full",
timeStyle: "medium",
hour12: false,
});
return {
success: true,
timezone,
datetime: formatter.format(new Date()),
};
} catch (error) {
return {
success: false,
timezone,
error: `無效的時區:${timezone}`,
};
}
}
這個函式完全不知道 Gemini 是誰。
它只有一個工作:
輸入時區
↓
取得目前時間
↓
回傳結果
例如:
getLocalTime("Asia/Taipei");
就可能得到:
{
"success": true,
"timezone": "Asia/Taipei",
"datetime": "2026/9/25 星期五 20:10:30"
}
這裡可以先記住一個非常重要的觀念:
Gemini 負責「決定要不要使用工具」。
真正執行工具的工作,還是在我們自己的程式裡。
前面的程式各自看起來都很簡單。
現在把它們真的串起來。
先讓 Gemini 收到:
台灣現在幾點?
然後提供 get_local_time 這個工具。
const interaction = await ai.interactions.create({
model: MODEL,
input: "台灣現在幾點?",
tools: [getLocalTimeTool],
generation_config: {
tool_choice: {
allowed_tools: {
mode: "any",
tools: ["get_local_time"],
},
},
},
});
這裡的 tool_choice 是這次 Demo 的一個小技巧。
在實際應用裡,可以讓模型自己判斷要不要呼叫工具。
但我們今天是在做教學實驗,因此希望每次執行都能穩定看到 Function Calling 流程,所以直接要求這次使用 get_local_time。Google 官方目前也提供 auto、any、none 等工具選擇模式。
Gemini 不會直接幫我們執行 JavaScript。
它會回傳一個 function_call。
const functionCall = interaction.steps.find(
(step) => step.type === "function_call"
);
if (!functionCall) {
throw new Error("Gemini 沒有產生 function_call");
}
console.log("Gemini 要呼叫:", functionCall.name);
console.log("參數:", functionCall.arguments);
執行後可能看到:
Gemini 要呼叫: get_local_time
參數: { timezone: 'Asia/Taipei' }
這就是 Function Calling 最重要的瞬間。
Gemini 並沒有直接去執行:
getLocalTime("Asia/Taipei");
它只是告訴我們:
「我判斷這個問題需要
get_local_time,而且我要傳入Asia/Taipei。」
接下來怎麼做?
由我們的 Node.js 決定。
接收到 function_call 之後:
let toolResult;
if (functionCall.name === "get_local_time") {
const timezone =
functionCall.arguments?.timezone || "Asia/Taipei";
toolResult = getLocalTime(timezone);
} else {
throw new Error(
`未知的 function:${functionCall.name}`
);
}
console.log("工具執行結果:");
console.log(JSON.stringify(toolResult, null, 2));
現在才真正執行:
getLocalTime("Asia/Taipei");
也就是:
Gemini
↓
「我要 get_local_time」
↓
Node.js
↓
真的執行 getLocalTime()
這個差異非常重要。
Function Calling 並不是把你的 JavaScript 函式丟給 Gemini 執行。
而是:
Gemini 提出工具請求
↓
你的程式接收
↓
你的程式決定是否執行
↓
你的程式取得結果
這也是為什麼 Function Calling 可以拿來連接資料庫、內部 API、檔案系統,甚至是你自己寫的各種工具。
現在時間已經查到了。
但事情還沒完成。
因為 Gemini 還不知道:
{
"success": true,
"timezone": "Asia/Taipei",
"datetime": "2026/9/25 星期五 20:10:30"
}
所以我們再把工具結果交回 Gemini。
const finalInteraction = await ai.interactions.create({
model: MODEL,
previous_interaction_id: interaction.id,
tools: [getLocalTimeTool],
input: [
{
type: "function_result",
name: functionCall.name,
call_id: functionCall.id,
result: [
{
type: "text",
text: JSON.stringify(toolResult),
},
],
},
],
});
最後:
console.log("\nGemini 最終回答:");
console.log(finalInteraction.output_text);
Google 官方目前的 Interactions API 範例,也是透過 previous_interaction_id 延續前一次互動,並將 function_result 傳回模型。
完成 .env 與 index.js 之後:
npm start
可以看到類似:
================================
Day 12 - Function Calling
================================
使用者:台灣現在幾點?
Gemini 要呼叫:
Function:get_local_time
Arguments:{"timezone":"Asia/Taipei"}
Node.js 正在執行工具...
timezone = Asia/Taipei
工具執行結果:
{
"success": true,
"timezone": "Asia/Taipei",
"datetime": "2026/9/25 星期五 20:10:30"
}
Gemini 最終回答:
台灣目前是晚上 8 點 10 分左右。
時間會依照實際執行時間而不同。
這次最值得觀察的,其實不是最後那一句:
台灣目前是晚上 8 點 10 分左右。
真正重要的是前面的:
Gemini 要呼叫:
Function:get_local_time
Arguments:{"timezone":"Asia/Taipei"}
以及:
Node.js 正在執行工具...
因為這兩段終於讓我們看到:
AI 和程式之間真的開始「分工」了。
把剛才的流程重新畫一次:
使用者
│
│ 台灣現在幾點?
▼
Gemini
│
│ function_call
│ get_local_time
│ timezone = Asia/Taipei
▼
Node.js
│
│ 執行 getLocalTime()
▼
取得目前時間
│
│ function_result
▼
Gemini
│
│ 整理結果
▼
自然語言回答
可以把今天的三個角色簡化成:
負責:
理解使用者需求
↓
判斷要使用哪個工具
↓
提出 function_call
負責:
接收 function_call
↓
真正執行 Function
↓
取得真實世界的資料
最後負責:
理解 function_result
↓
整理成使用者看得懂的回答
也就是:
Gemini
↓
「我想做什麼?」
你的程式
↓
「真的去做。」
Gemini
↓
「把結果說清楚。」
這就是今天最重要的觀念。
其實今天完全可以直接做:
get_weather()
甚至可以讓 Gemini 幫我們查:
台北現在幾度?
但我刻意沒有這麼做。
因為一旦加入第三方天氣 API,就會多出:
這些東西很容易讓初學者把注意力放到:
「天氣 API 怎麼串?」
而不是:
「Function Calling 到底怎麼運作?」
所以今天故意選擇:
get_local_time()
它不需要任何第三方服務。
只靠 Node.js 本身,就可以把 Function Calling 的整個流程跑完。
先把觀念做小,再慢慢把功能做大。
這也是這個系列一直採用的方式。
Day 11:
Gemini
↓
Structured Output
↓
Zod
↓
確認「資料長什麼樣子」
Day 12:
使用者需求
↓
Gemini
↓
Function Calling
↓
決定「我要做什麼」
↓
Node.js 執行
↓
回傳結果
所以可以把這兩天理解成:
Day 11 是讓 AI 的「資料」可靠。
Day 12 是讓 AI 的「行動」開始有出口。
這是一個很重要的進步。
昨天的 Gemini 比較像:
你問問題
↓
它產生資料
↓
你的程式驗證資料
今天開始變成:
你問問題
↓
Gemini 判斷需要什麼工具
↓
你的程式真的去做
↓
結果再交回 Gemini
DevPulse 也因此開始從單純的「AI 問答小工具」,慢慢變成一個可以與外部世界互動的程式。
而這件事情,也開始替後面要學的 Agent、AI Workflow 打基礎。
今天最大的觀念轉變,是第一次發現:
AI 不一定要直接回答問題,它也可以先決定「要叫哪個工具來幫忙」。
而 Function Calling 最值得記住的,也不是某一個 API 參數。
而是這條流程:
想做什麼
↓
提出工具呼叫
↓
程式真的執行
↓
拿結果回來
↓
AI 再回答
昨天我們讓 AI 的輸出變得更可靠。
今天,終於讓 AI 開始有機會「做事情」。
原來 AI 應用程式真正強大的地方,不一定是模型自己知道多少,而是我們可以提供什麼工具給它。
下一篇,換個方向。
如果兩段程式碼內容完全不同,但其實是在解決非常接近的問題,Gemini 能不能看出它們的「語意相似度」?
Day 13:
Embedding。