iT邦幫忙

2025 iThome 鐵人賽

DAY 2
0
生成式 AI

當 .NET 遇見 AI Agents:用 Semantic Kernel × MCP 打造智慧協作應用系列 第 2

Day 2:Function Calling — Semantic Kernel 如何讓 AI Agent 動起來?

  • 分享至 

  • xImage
  •  

在聊到 AI Agent 的核心能力時,「Function Calling(功能調用)」幾乎是繞不開的一塊。這個機制讓大型語言模型(LLM)不再只是個文字生成器,而是能夠真正「做事」的助手。簡單來說,Function Calling 讓開發者可以指引模型:當使用者問了某些特定問題,或提出某些需求時,你不用自己編故事,而是可以「去問問外部的 API、查查資料庫、甚至執行一段真正的程式碼」。這樣一來,模型的回應就能結合即時、準確的資訊,甚至真的去幫你完成某些任務。

舉個例子,當你對 AI 說:「幫我查一下今天台北的天氣」,模型不是「憑空猜測」一個答案,而是會知道自己要完成這件任務時,應該需要呼叫某個天氣 API,把實際資料撈回來,再轉換成一段自然語言的回答給你。這不只是讓回答變得更精準,也讓整個互動邏輯更像是一個具備「行動力」的智能助理。

但問題是模型不是真的會執行程式碼,它只是「知道」什麼時候會需要呼叫函式,實際上模型會「產生」一段結構化的輸出(通常是 JSON 格式),告訴系統:「嘿,我需要呼叫這個函式,這是我需要的參數」,接著系統會負責把這個呼叫送出去,拿回結果,再回饋給模型,讓它能夠基於最新的資訊來生成最終的回答。

所以 Function Calling 的背後其實是一個清楚的流程:

  • 模型理解使用者的需求後,會「判斷」是否需要呼叫某個外部函式。
  • 如果需要,它會回傳一個結構化的「呼叫指令」——像是告訴我們:「我想呼叫 getWeather 這個函數,參數是 city=Taipei」。
  • 這時由外部系統(通常是我們的程式邏輯)來真正執行這個函數,把結果再回傳給模型。
  • 模型接收到結果後,根據這些資料生成一個更完整、有根據的回應。

在 Semantic Kernel(SK)這個框架裡,Function Calling 的整合也變得相對簡單。SK 幫你處理好了很多底層的溝通細節,讓你只要定義好哪些函數要提供給模型用、怎麼描述這些函數,模型就能自己決定什麼時候該用哪一個,並且你也不用擔心怎麼實作呼叫函式,怎麼把結果再回饋給模型,這些都由 SK 來幫你處理。

所以在這一篇內容中,讓我們來理解 Function Calling 的原理與實作,先講清楚「為什麼模型需要呼叫外部函式」與「呼叫過程中誰做什麼、資料如何流動」,然後再來看看如何利用 Semantic Kernel(SK)來裝配 Function Calling 能力。

核心概念速讀

首先聚焦兩個問題:為什麼需要外部函式、誰做什麼與資料如何流動。

為什麼模型需要呼叫外部函式

我們得先搞清楚一件事:LLM 模型本身只看得到你丟給它的提示詞和上下文,無法主動存取即時資料(像是天氣、庫存、內部系統狀態),也不能直接執行操作(例如寫入資料庫、發送訂單)。所以這就需要一個「中介層」來讓模型有行動力,而這個中介層的關鍵就是所謂的「外部函式(Function)」。

這些外部函式可以是查天氣、訂機票、查庫存、下訂單等等的功能。以 OpenAI 的 GPT 模型為例,它的 Function Calling 並不是模型本身會寫程式碼或操作 API,而是經過微調後,模型能夠辨識何時需要額外工具的協助,然後自動產出一段結構化的 JSON,其中包含要調用的函式名稱、所需的參數和對應的說明。這時候中介層就會出場,它負責接收這個 JSON、執行實際的函式呼叫,並把結果回傳給模型。最後模型再根據這些新資訊,生成一段更貼近現實、更具參考價值的回應。

所以,Function Calling 本質上就是透過提示工程(Prompt Engineering)和模型微調,賦予 LLM 延伸能力,讓它能夠跨出自己的知識框架,跟外部世界互動。不只可以幫助模型取得最新資料,也能處理複雜計算、甚至動手操作系統。

Function Calling 生命週期

可以分解為以下五個核心步驟

  1. 工具宣告
    開發者首先定義可供模型調用的函數(例如,query_delivery_date)。接著,在向 OpenAI Chat Completions API 發送請求時,除了使用者的提示(prompt)外,還需在 tools 參數中提供這些函數的詳細描述(JSON Schema)。這些描述告訴模型有哪些工具可用,以及如何使用它們。
  2. 模型決策
    LLM 模型接收到包含使用者提示和工具定義的請求後,會進行分析。根據對話的上下文和函數描述,決定"生成自然語言回覆"、"判斷需要調用一個或多個已定義的函數來取得額外資訊"或是進一步向使用者再次提問,以釐清模糊的請求,收集調用函數所需的必要參數。
  3. 模型生成結構化輸出:模型若覺得需要,模型輸出結構化的 function_call(函式名稱+參數 JSON);若不需要,直接產生最終答覆。以 OpenAI 的模型大概會生成這樣的 JSON:
{
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "query_delivery_date",
    "arguments": "{ \"order_id\": \"168888\" }"
  }
}
  1. 函式執行
    需要解析 tool_calls 陣列。根據 name 和 arguments,在本地執行對應的函數(例如,連接資料庫查詢訂單 order_168888 的配送日期)
  2. 將結果回傳給模型以生成最終回覆
    函數執行完畢後,再次調用 Chat Completions API。這次的請求中,除了包含原始的對話歷史外,還需要附加兩條新的訊息:一條是模型先前生成的 tool_calls 訊息,另一條是包含函數執行結果的 tool 角色訊息。這讓模型能夠將函數的執行結果納入考量。

最後,LLM 會基於這次收到的完整上下文(包含函數的輸出),生成一個最終的、通順的自然語言回覆給用戶,例如「您訂單 order_168888 的預計配送日期是 2025 年 9 月 10 日。」。

https://ithelp.ithome.com.tw/upload/images/20250916/20126569NDAl0Be94e.jpg

Semantic Kernel 如何實現 Function Calling

在 Semantic Kernel 中,以上 Function Calling 流程會由「定義 Plugins → 註冊到 Kernel → 提供工具描述給模型 → 攔截並執行 function_call → 回饋結果 → 控制終止條件與處理」構成完整的循環。

實作並啟用 Function Calling

接下來,我們將逐步實作在 Semantic Kernel 中啟用 Function Calling 的過程。假設我們要打造一個訂單助理,能根據使用者輸入的「訂單編號」,先查詢訂單狀況,再回覆給使用者。

  • 定義 Plugins
    我們需要定義一個訂單服務的Plugin,讓模型能夠調用它來處理有關訂單的功能服務,例如獲取訂單資訊。簡單來說,我們會撰寫一個 C# 類別,並提供相關的方法,而方法中會使用 SK 的標註(Attribute)來描述這個函式的用途、參數。這些 Attribute 將會被用來生成對應的 API 文件說明,並幫助模型理解如何正確調用這些函式,是相當重要且關鍵的部分,會影響到整個 Function Calling 的正確性。

    public class OrderService
     {
        // 模擬的訂單資料(訂單編號 -> 狀態)
        private readonly Dictionary<string, string> _orders = new Dictionary<string, string>
        {
           { "A001", "已出貨" },
           { "A002", "處理中" },
           { "A003", "已取消" },
           { "A004", "已完成" }
        };
    
        // 查詢訂單狀態的方法
        [KernelFunction]
        [Description("Retrieves the order status by order ID.")]
        public string GetOrderStatus(
           [Description("The ID of the order to retrieve the status for.")]
           string orderId)
        {
           if (string.IsNullOrWhiteSpace(orderId))
           {
                 return "訂單編號不可為空";
           }
           if (_orders.TryGetValue(orderId, out var status))
           {
                 return status;
           }
           else
           {
                 return "查無此訂單";
           }
        }
     }
    
  • 註冊 Plugins 到 Kernel
    將定義好的 Plugin 註冊到 Kernel 中,讓模型能夠識別並在需要時能夠調用它。

     IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
     kernelBuilder.AddOpenAIChatCompletion(
                          apiKey: Config.OpenAI_ApiKey,
                          modelId: Config.ModelId).Build();
    
     kernelBuilder.Services.AddSingleton<IFunctionInvocationFilter, WatchFunctionFilter>();
    
     var kernel = kernelBuilder.Build();
     kernel.Plugins.AddFromType<OrderService>();
    
  • 設定 Kernel 啟用 Function 自動調用
    在 Kernel 中啟用 Function 自動調用,讓模型能夠在需要時自動選擇 Plugin 並調用相應的 Function。

     OpenAIPromptExecutionSettings settings =
          new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
    
  • 設計系統提示與使用規則(Prompt)
    另外也可以在系統提示內撰寫指引,引導模型在需要時使用 Plugin,而非臆測。例如:

      ChatHistory history = new();
          string devPrompt = $"""
          你是一個訂單智能助手,能夠根據使用者的需求調用相應的功能。請遵循以下規則:
          1. 當你需要獲取訂單狀態時,請使用 `GetOrderStatus` 函數。
          2. 當你需要額外的上下文資訊時,請主動詢問使用者。
          3. 請始終使用繁體中文回答問題。
          4. 嚴禁提供訂單服務以外的功能。
          5. 所有回覆必須基於已知的訂單資訊,並不得臆測或編造內容。
          """;
      history.AddDeveloperMessage(devPrompt);
          // 從 kernel 取得聊天服務
      var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
    
  • 進行對話

     // 開始聊天對話
     Console.Write("User > ");
     string? userInput;
     while ((userInput = Console.ReadLine()) is not null)
     {
    
        if (string.IsNullOrWhiteSpace(userInput) || userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
              break;
    
        // 加入使用者訊息到對話歷史
        history.AddUserMessage(userInput);
    
        // 以串流方式獲取 AI 回應
        var result = chatCompletionService.GetStreamingChatMessageContentsAsync(
                             history,
                             settings,
                             kernel: kernel);
    
        // Stream the results
        string fullMessage = "";
        var first = true;
        await foreach (var content in result)
        {
              if (content.Role.HasValue && first)
              {
                 Console.Write("Assistant > ");
                 first = false;
              }
              Console.Write(content.Content);
              fullMessage += content.Content;
        }
        Console.WriteLine();
        // 加入 AI 回應到對話歷史
        history.AddAssistantMessage(fullMessage);
    
        // Get user input again
        Console.Write("User > ");
     }
    
     Console.WriteLine("\n Bye!");
    

AI 回應結果

User > 我要問訂單目前處理的如何了
Assistant > 請提供您的訂單編號,以便我為您查詢訂單的最新處理狀態。
User > A003
Assistant > 您的訂單編號 A003 目前的狀態為「已取消」。若您需要進一步協助,請告知!
User > 說個笑話
Assistant > 很抱歉,我只能協助您查詢與訂單相關的服務。如需查詢訂單狀態或有其他訂單問題,歡迎隨時告訴我!

常見錯誤與處理

從上面的實作過程中,我們可以看到 Function Calling 主要依賴於幾個關鍵點:函式的正確定義與描述、模型的決策能力、以及系統提示的設計。這些環節都可能導致整個流程無法正確呼叫外部函式,進而影響最終的生成結果。因此,在實際應用中,我們需要特別注意這些潛在的問題,並採取相應的解決策略:

  • 工具描述不精確 → 導致模型誤選工具或亂填參數,縮短描述、加動詞開頭,在 system-prompt 裡提供指引。
  • 幻覺函式名稱 → 在 system-prompt 中明訂只可用列出的工具。
  • 外部 API 限流/逾時 → 重試、快取。
  • 工具回傳過大 → 摘要後再回饋模型,避免超出 token 限制。
  • 工具調用錯誤 → 同一個 kernel 註冊過多的函式,導致混淆,需精簡數量僅附卦必要的工具與明確化函式定義。

結語

Function Calling 是將靜態的大型語言模型轉變為動態 AI Agent 的關鍵技術。透過這個機制,我們的 AI 系統不再只能基於訓練資料回答問題,而能夠真正與外部世界互動,取得即時資訊並執行實際操作。

在本篇的實作中,我們看到了 Semantic Kernel 如何簡化 Function Calling 的複雜度,讓開發者能夠專注於業務邏輯的實作,而不需要處理底層的模型溝通細節。從定義 Plugin、註冊到 Kernel、設計系統提示,到最終的對話執行,每個步驟都有其重要性,而任何一個環節的疏忽都可能影響整體的表現。

更重要的是,也提出了一些實作上常見的問題與解決策略。在實際的產品開發中,這些「細節」會決定 AI Agent 的實用性與它的可靠性。往後還會探討更多進階的主題,看看如何讓 AI Agent 具備更複雜的推理能力或是多步驟任務執行能力。Function Calling 只是第一步,真正的挑戰在於如何讓 AI 在複雜的任務場景中保持穩定與可控。


上一篇
Day 1: 踏入 AI Agent 的世界 - 認識 Semantic Kernel
下一篇
Day 3:不是所有 LLM 應用都需要成為 Agent - 選擇合適的架構設計
系列文
當 .NET 遇見 AI Agents:用 Semantic Kernel × MCP 打造智慧協作應用3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言