iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
生成式 AI

用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰系列 第 6

Day 06 - 函式呼叫實作:運用 Function Calling 擴充 AI 能力

  • 分享至 

  • xImage
  •  

在前幾篇文章中,我們已經能讓聊天機器人具備多輪對話能力,並透過提示工程與參數控制回應風格。但到目前為止,它仍然只能依靠模型本身的知識來回答問題。一旦使用者提出需要即時或外部資訊的問題,模型往往就會顯得無能為力。

為了突破這個限制,我們可以善用 OpenAI 的 Function Calling 機制。藉由這項功能,模型在判斷有需要時,可以主動呼叫我們定義的外部函式來查詢資料或執行操作,從而讓 AI 從單純的對話角色進化為真正能夠採取行動的智慧助理。

大型語言模型的侷限

大型語言模型(LLM)雖然能生成自然流暢的文字,並且在許多情境中提供相當有用的回答,但它並不是全知全能的智慧系統。從本質上來說,LLM 只是一個根據既有資料來預測文字的工具,並沒有真正連接到外部世界。在沒有外力介入的情況下,它回答問題時,完全依賴的是訓練過程中所學到的知識,而非即時查詢的結果。

舉個例子,如果我們下達這樣的指令:

你是一位氣象專家,請回答使用者輸入的城市目前天氣狀況。

模型並不會真的去查詢該城市的最新天氣數據,而是根據過去訓練資料中所見過的描述來推測。例如,它可能會回答「基隆經常下雨」或「新竹風很大」。這些描述或許符合印象,但與實際的即時天氣往往有落差。換句話說,模型給出的只是基於經驗的猜測,而不是可靠的即時資訊。

這樣的限制會帶來兩個主要問題:

  1. 資料過時:模型的知識固定在訓練的時間點,例如 GPT-4 的知識只更新到 2023 年底,因此對 2024 年之後的事件一無所知。
  2. 幻覺現象:當模型缺乏足夠資訊時,仍可能生成看起來合理但實際錯誤的答案,這種「一本正經地胡說八道」就是所謂的 幻覺(Hallucination)

在日常對話中,這些限制或許不會造成太大困擾。但若 AI 的輸出被直接引用在報告、決策或商業流程中,錯誤資訊就可能被放大,甚至造成嚴重的後果。

Function Calling:讓模型會做事

為了突破這些限制,OpenAI 提供了 Function Calling 機制。它讓語言模型不再只是單純的文字產生器,而能夠在需要時主動請求系統執行某個功能,例如查天氣、搜尋資料、呼叫外部 API,並根據回傳結果給出更完整且可靠的回答。

舉例來說,當使用者詢問天氣時,模型若判斷必須透過外部資訊才能正確回答,就會輸出一段結構化的 JSON,描述要呼叫的工具與對應參數,例如:

{
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "getWeather",
        "arguments": "{ \"location\": \"Taipei\" }"
      }
    }
  ]
}

這段 JSON 表示模型希望呼叫名為 getWeather 的工具,並傳入參數 { "location": "Taipei" }。應用程式收到這個請求後,就能執行對應的函式(例如實際查詢氣象 API),取得結果後再回傳給模型。最後,模型會根據真實資料整理並輸出自然語言回覆給使用者。

整個流程可以拆解為三個步驟:

  1. 模型輸出請求:判斷需要外部幫助,並生成要呼叫的工具名稱與參數。
  2. 應用程式執行邏輯:依照請求執行外部功能,例如 API 查詢或資料庫操作。
  3. 模型整合回覆:接收執行結果,轉換成自然語言回答給使用者。

https://ithelp.ithome.com.tw/upload/images/20250906/201501501823nX4LDb.png

透過這樣的機制,模型不再需要「假裝知道答案」,而是能主動查詢、取得正確資料,再進行整合後回覆。這不僅降低了資料過時與幻覺的問題,也讓 AI 助理在真實應用情境中更加實用與可信。

如何使用 Function Calling?

要讓模型能正確使用工具,我們必須先告訴它有哪些功能,以及該如何使用。這就像先準備好一份「工具清單」與「使用說明書」,模型才能在需要時主動挑選並呼叫合適的工具。

整體流程如下圖所示:

https://ithelp.ithome.com.tw/upload/images/20250906/20150150jjS4Pd9Sm2.png

定義模型可用工具

在 OpenAI API 中,工具清單透過 tools 參數傳入。每個工具需要包含三個核心資訊:

  • name:模型依名稱呼叫對應的函式
  • description:描述在什麼情況下應使用這個工具
  • parameters:使用 JSON Schema 格式,定義所需欄位、型別與說明

以下示範定義一個 getWeather 工具:

import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const tools = [
  {
    type: "function",
    function: {
      name: "getWeather", // 工具名稱
      description: "取得指定城市的天氣資訊", // 工具用途
      parameters: {
        type: "object", // 接收參數型別
        properties: {
          location: {
            type: "string",
            description: "城市名稱,例如 Taipei",
          },
        },
        required: ["location"], // 必填參數
      },
    },
  },
];

這段程式碼定義了一個名為 getWeather 的工具,使用者只要輸入 location,模型就能透過它查詢當地的天氣資訊。

模型呼叫工具

定義好工具後,就能將 tools 傳進 API 請求:

const response = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "台北今天天氣如何?" }],
  tools, // 告訴模型有哪些工具可用
});

console.log(JSON.stringify(response.choices[0].message, null, 2));

此時,模型可能會輸出一段 tool_calls 工具呼叫請求:

{
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "getWeather",
        "arguments": "{ \"location\": \"Taipei\" }"
      }
    }
  ]
}

這表示模型希望呼叫 getWeather 工具,並傳入參數 location: "Taipei"

回傳執行結果給模型

接下來,應用程式要攔截這段工具請求,執行對應邏輯,並將結果回傳給模型。
假設我們有一個模擬的 getWeather 函式:

// 模擬天氣查詢
async function getWeather(location) {
  return `今天 ${location} 天氣晴朗,氣溫約 25°C`;
}

整合流程如下:

const message = response.choices[0].message;

if (message.tool_calls) {
  const toolCall = message.tool_calls[0];
  const args = JSON.parse(toolCall.function.arguments);

  // 執行工具邏輯
  const result = await getWeather(args.location);

  // 回傳執行結果給模型
  const secondResponse = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "user", content: "台北今天天氣如何?" },
      message, // 模型輸出的 tool_call
      {
        role: "tool",
        tool_call_id: toolCall.id, // 對應工具呼叫 ID
        content: result, // 工具查詢結果
      },
    ],
  });

  console.log("AI 回覆:", secondResponse.choices[0].message.content);
}

在這裡,我們把工具的輸出結果以 role: "tool" 的形式回傳給模型,並透過 tool_call_id 對應到原本的呼叫請求。接著,模型就會基於真實資料整理出最終的自然語言回覆。

最後,模型就會依據真實查詢結果,生成自然語言回應,例如:

AI 回覆:台北今天的天氣是晴朗,氣溫大約 25°C,適合外出活動。

透過這樣的流程,模型能完成 「判斷需求 → 呼叫工具 → 接收結果 → 整合回覆」 的閉環,不再只是「猜答案」,而是能真正查詢並提供可靠資訊。

實作:打造支援 Function Calling 的 CLI Chatbot

在前面我們已經完成一個基礎的 CLI Chatbot,這次要替它加上 Function Calling 的能力,讓它能查詢「指定地點的目前時間」。這個例子雖然簡單,但能完整展示工具定義、模型調用工具、以及回傳結果給模型的流程。

定義 Tool 與對應函式

在使用 Function Calling 時,第一步就是定義工具,告訴模型有哪些功能可用,以及應該如何使用。接著,還需要準備一個對應的實作函式,確保當模型真的發出工具呼叫時,應用程式能執行邏輯並回傳結果。

以下範例定義了一個 getCurrentTime 工具,用來查詢指定時區的當前時間:

// src/tools.ts
import type { ChatCompletionTool } from 'openai/resources';

export const tools: ChatCompletionTool[] = [
  {
    type: 'function',
    function: {
      name: 'getCurrentTime',
      description: '取得指定時區的當前時間',
      parameters: {
        type: 'object',
        properties: {
          timeZone: {
            type: 'string',
            description: 'IANA 時區名稱,例如 Asia/Taipei, America/New_York',
          },
        },
        required: ['timeZone'],
      },
    },
  },
];

export const toolFunctions: Record<string, any> = {
  getCurrentTime: async ({ timeZone }: { timeZone: string }) => {
    const date = new Date();
    return date.toLocaleString('zh-TW', { timeZone });
  },
};

這裡有兩個關鍵部分:

  1. tools:定義工具的結構,包括名稱、用途描述與參數規格。這份清單會傳給 OpenAI API,讓模型知道有哪些工具可用。
  2. toolFunctions:實際的函式實作。當模型輸出工具呼叫時,應用程式會根據工具名稱找到對應函式,執行並取得結果。

在這個範例中,我們建立了一個名為 getCurrentTime 的工具,它需要一個必填參數 timeZone,並透過 JavaScript 的 Date.toLocaleString 方法回傳對應時區的時間。

整合主程式邏輯

接著,我們需要修改 CLI Chatbot 的主程式,讓它具備處理工具呼叫與回傳結果的能力。

以下是完整的程式碼範例:

// src/index.ts
import 'dotenv/config';
import readline from 'readline';
import yargs, { Arguments } from 'yargs';
import { hideBin } from 'yargs/helpers';
import { OpenAI } from 'openai';
import { roles } from './prompts';
import { tools, toolFunctions } from './tools';

interface Argv {
  role: keyof typeof roles;
  temperature: number;
  top_p: number;
}

const argv = yargs(hideBin(process.argv))
  .option('role', {
    alias: 'r',
    type: 'string',
    choices: Object.keys(roles) as (keyof typeof roles)[],
    default: 'default',
    description: '指定助理角色',
  })
  .option('temperature', {
    alias: 't',
    type: 'number',
    description: '控制模型的創造力 (0 ~ 2)',
    default: 1,
  })
  .option('top_p', {
    alias: 'tp',
    type: 'number',
    description: '限制模型的選詞範圍 (0 ~ 1)',
    default: 1,
  })
  .help()
  .parseSync();

async function main(argv: Arguments<Argv>) {
  const openai = new OpenAI();
  const messages = [...roles[argv.role]];

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  console.log('GPT Chatbot 已啟動,輸入訊息開始對話(按 Ctrl+C 離開)。\n');
  rl.setPrompt('> ');
  rl.prompt();

  rl.on('line', async (input) => {
    messages.push({ role: 'user', content: input });

    try {
      const response = await openai.chat.completions.create({
        model: 'gpt-4o-mini',
        messages,
        tools,
        tool_choice: 'auto',
      });
      const message = response.choices[0].message;

      if (message.tool_calls) {
        const toolCall = message.tool_calls[0];
        const fn = toolFunctions[toolCall.function.name];
        const args = JSON.parse(toolCall.function.arguments);
        const result = await fn(args);

        messages.push(message);
        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          content: result,
        });
      }

      const stream = await openai.chat.completions.create({
        model: 'gpt-4o-mini',
        stream: true,
        temperature: argv.temperature,
        top_p: argv.top_p,
        messages,
      });

      let reply = '';

      process.stdout.write('\n');
      for await (const chunk of stream) {
        const content = chunk.choices[0]?.delta?.content || '';
        process.stdout.write(content);
        reply += content;
      }
      process.stdout.write('\n\n');

      messages.push({ role: 'assistant', content: reply });
    } catch (err) {
      console.error(err);
    }

    rl.prompt();
  });
}

main(argv);

這段程式的核心流程可以拆解為以下步驟:

  1. 第一次請求:模型先判斷是否需要呼叫工具 → 如果需要,會輸出 tool_calls
  2. 執行工具邏輯:應用程式解析 tool_calls,執行對應的函式並取得結果。
  3. 回傳工具結果:以 role: "tool" 的訊息形式,把執行結果交回給模型。
  4. 第二次請求:模型根據工具結果生成自然語言回覆,並透過串流輸出即時顯示在終端機。

這樣一來,我們的 CLI Chatbot 不僅能回答問題,還具備主動呼叫工具、整合外部資訊的能力。

啟動與測試

完成程式後,就可以啟動 CLI Chatbot:

npm run dev

在終端機中輸入:

現在台北時間?

模型會自動判斷這個問題需要呼叫工具,接著由程式執行 getCurrentTime 函式,並將結果回傳給模型,最後輸出自然語言回覆。

範例輸出可能會是:

現在台北時間是 2025/09/06 上午 08:00。

透過這樣的設計,Chatbot 不僅能單純回答問題,還能主動判斷需求、呼叫工具並整合外部資訊,功能上已經邁入初階 AI Agent 的範疇。

小結

今天我們介紹了如何透過 OpenAI 提供的 Function Calling 機制,讓大型語言模型不只是回答問題,而是能真正「做事」:

  • 大型語言模型(LLM)本質上只能基於訓練知識生成文字,容易遇到資料過時或產生幻覺的問題。
  • Function Calling 機制允許模型輸出結構化 JSON,請求應用程式呼叫外部函式並回傳結果。
  • 完整流程包含:模型判斷需求 → 輸出工具呼叫 → 應用程式執行邏輯 → 回傳結果給模型 → 模型整合成自然語言回覆。
  • 呼叫模型時需要傳入 tools 參數,用來定義可用工具的名稱、用途與 JSON Schema 格式的參數。
  • 實作上,程式需攔截 tool_calls,執行外部 API 或函式,並再將結果回饋給模型,以生成完整回應。

有了這套機制,AI 助理就能不再局限於訓練時的知識,而是具備真正行動力,讓回覆更即時、更可靠,往智慧助理的方向更進一步。


上一篇
Day 05 - 回應風格調控:掌握 Temperature 與 Top-P 設定
下一篇
Day 07 - 認識 LangChain:打造生成式 AI 應用的 LLM 框架
系列文
用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言