共用方式為


什麼是外掛程式?

外掛程式是語意核心的重要元件。 如果您已在 Microsoft 365 中使用來自 ChatGPT 或 Copilot 延伸模組的外掛程式,您已經熟悉它們。 透過外掛程式,您可以將現有的 API 封裝到 AI 可以使用的集合中。 這可讓您讓 AI 能夠執行無法執行的動作。

在幕後,Semantic Kernel 會利用 函式呼叫,這是大部分最新 LLM 的原生功能,可讓 LLM 執行 規劃 及叫用 API。 透過函式調用,LLM(大型語言模型)可以要求執行特定函式。 語意核心接著會將要求轉交至程式碼庫中的適當函式,並將結果傳回 LLM,以便 LLM 產生最終回應。

語意核心外掛程式

並非所有 AI SDK 都有與外掛程式類似的概念(大部分只有函式或工具)。 不過,在企業案例中,外掛程式是有價值的,因為它們封裝了一組功能,以反映企業開發人員如何開發服務和 API。 外掛程式也能很好地與相依性注入配合得當。 在外掛程式的建構函式內,您可以插入執行外掛程式工作所需的服務(例如資料庫連線、HTTP 用戶端等)。 這很難與其他缺少外掛程式的 SDK 一起完成。

外掛程式的結構

概括而言,外掛程式是一組可公開給 AI 應用程式和服務的 函式。 然後,AI 應用程式可以協調外掛程式內的函式,以完成使用者要求。 在 Semantic Kernel 中,您可以使用函數呼叫自動叫用這些函式。

注意

在其他平臺上,函式通常稱為「工具」或「動作」。 在 Semantic Kernel 中,我們使用「函式」一詞,因為它們通常定義為程式代碼基底中的原生函式。

不過,只要提供函式就不足以製作外掛程式。 若要使用函式呼叫來提供自動協調流程,外掛程式也必須提供語意描述其行為的詳細數據。 必須以 AI 能夠理解的方式描述函式的輸入、輸出和副作用,否則 AI 將無法正確呼叫函式。

例如,右側的範例 WriterPlugin 外掛程式具有語意描述的函式,可描述每個函式的作用。 然後,LLM 可以使用這些描述來選擇要呼叫的最佳函式,以履行使用者的要求。

在右側的圖片中,LLM 可能會呼叫 ShortPoemStoryGen 函式,以滿足使用者的要求,這要歸功於所提供的語意描述。

WriterPlugin 外掛程式內的語意描述

匯入不同類型的外掛程式

將外掛程式匯入語意核心有兩種主要方式:使用 原生程式代碼 或使用 OpenAPI 規格。 前者可讓您在現有的程式代碼基底中撰寫外掛程式,以利用您已經擁有的相依性和服務。 後者可讓您從 OpenAPI 規格匯入外掛程式,此規格可以跨不同的程式設計語言和平台共用。

以下提供匯入和使用原生外掛程式的簡單範例。 若要深入瞭解如何匯入這些不同類型的外掛程式,請參閱下列文章:

提示

開始使用時,建議您使用原生程式碼外掛程式。 隨著應用程式成熟,而且當您跨跨平臺小組工作時,您可能想要考慮使用OpenAPI規格,跨不同的程式設計語言和平臺共用外掛程式。

不同類型的外掛程式函式

在外掛程式內,您通常會有兩類不同的函式:一種是用於資料擷取增強生成(RAG)的函式,另一種是用於自動執行任務的函式。 雖然每個類型在功能上都相同,但通常會在使用 Semantic Kernel 的應用程式內以不同的方式使用它們。

例如,使用擷取函式時,您可能想要使用策略來改善效能(例如快取和使用更便宜的中繼模型進行摘要)。 使用工作自動化功能時,您可能會想實施人工參與的審批流程,以確保任務正確完成。

若要深入瞭解不同類型的外掛程式函式,請參閱下列文章:

開始使用外掛程式

在 Semantic Kernel 中使用外掛程式始終包括三個步驟:

  1. 定義外掛程式
  2. 將外掛程式新增至核心
  3. 然後在其中一個提示中叫用外掛程式的函式,並呼叫函式

下面我們將提供如何在語意核心中使用外掛程式的高階範例。 如需如何建立和使用外掛程式的詳細資訊,請參閱上述連結。

1) 定義您的外掛程式

建立外掛程式最簡單的方式是定義 類別,並使用 KernelFunction 屬性標註其方法。 這讓語意核心知道這是可由 AI 呼叫或在提示中參考的函式。

您還可以從 OpenAPI 規格 匯入外掛程式。

下面,我們將建立可擷取燈光狀態並改變其狀態的外掛程式。

提示

由於大部分 LLM 都已使用 Python 進行函式呼叫訓練,因此建議您使用蛇狀架構來命名函式名稱和屬性名稱,即使您使用的是 C# 或 Java SDK 也一樣。

using System.ComponentModel;
using Microsoft.SemanticKernel;

public class LightsPlugin
{
   // Mock data for the lights
   private readonly List<LightModel> lights = new()
   {
      new LightModel { Id = 1, Name = "Table Lamp", IsOn = false, Brightness = 100, Hex = "FF0000" },
      new LightModel { Id = 2, Name = "Porch light", IsOn = false, Brightness = 50, Hex = "00FF00" },
      new LightModel { Id = 3, Name = "Chandelier", IsOn = true, Brightness = 75, Hex = "0000FF" }
   };

   [KernelFunction("get_lights")]
   [Description("Gets a list of lights and their current state")]
   [return: Description("An array of lights")]
   public async Task<List<LightModel>> GetLightsAsync()
   {
      return lights
   }

   [KernelFunction("get_state")]
   [Description("Gets the state of a particular light")]
   [return: Description("The state of the light")]
   public async Task<LightModel?> GetStateAsync([Description("The ID of the light")] int id)
   {
      // Get the state of the light with the specified ID
      return lights.FirstOrDefault(light => light.Id == id);
   }

   [KernelFunction("change_state")]
   [Description("Changes the state of the light")]
   [return: Description("The updated state of the light; will return null if the light does not exist")]
   public async Task<LightModel?> ChangeStateAsync(int id, LightModel LightModel)
   {
      var light = lights.FirstOrDefault(light => light.Id == id);

      if (light == null)
      {
         return null;
      }

      // Update the light with the new state
      light.IsOn = LightModel.IsOn;
      light.Brightness = LightModel.Brightness;
      light.Hex = LightModel.Hex;

      return light;
   }
}

public class LightModel
{
   [JsonPropertyName("id")]
   public int Id { get; set; }

   [JsonPropertyName("name")]
   public string Name { get; set; }

   [JsonPropertyName("is_on")]
   public bool? IsOn { get; set; }

   [JsonPropertyName("brightness")]
   public byte? Brightness { get; set; }

   [JsonPropertyName("hex")]
   public string? Hex { get; set; }
}
from typing import TypedDict, Annotated

class LightModel(TypedDict):
   id: int
   name: str
   is_on: bool | None
   brightness: int | None
   hex: str | None

class LightsPlugin:
   lights: list[LightModel] = [
      {"id": 1, "name": "Table Lamp", "is_on": False, "brightness": 100, "hex": "FF0000"},
      {"id": 2, "name": "Porch light", "is_on": False, "brightness": 50, "hex": "00FF00"},
      {"id": 3, "name": "Chandelier", "is_on": True, "brightness": 75, "hex": "0000FF"},
   ]

   @kernel_function
   async def get_lights(self) -> Annotated[list[LightModel], "An array of lights"]:
      """Gets a list of lights and their current state."""
      return self.lights

   @kernel_function
   async def get_state(
      self,
      id: Annotated[int, "The ID of the light"]
   ) -> Annotated[LightModel | None], "The state of the light"]:
      """Gets the state of a particular light."""
      for light in self.lights:
         if light["id"] == id:
               return light
      return None

   @kernel_function
   async def change_state(
      self,
      id: Annotated[int, "The ID of the light"],
      new_state: LightModel
   ) -> Annotated[Optional[LightModel], "The updated state of the light; will return null if the light does not exist"]:
      """Changes the state of the light."""
      for light in self.lights:
         if light["id"] == id:
               light["is_on"] = new_state.get("is_on", light["is_on"])
               light["brightness"] = new_state.get("brightness", light["brightness"])
               light["hex"] = new_state.get("hex", light["hex"])
               return light
      return None
public class LightsPlugin {

  // Mock data for the lights
  private final Map<Integer, LightModel> lights = new HashMap<>();

  public LightsPlugin() {
    lights.put(1, new LightModel(1, "Table Lamp", false));
    lights.put(2, new LightModel(2, "Porch light", false));
    lights.put(3, new LightModel(3, "Chandelier", true));
  }

  @DefineKernelFunction(name = "get_lights", description = "Gets a list of lights and their current state")
  public List<LightModel> getLights() {
    System.out.println("Getting lights");
    return new ArrayList<>(lights.values());
  }

  @DefineKernelFunction(name = "change_state", description = "Changes the state of the light")
  public LightModel changeState(
      @KernelFunctionParameter(name = "id", description = "The ID of the light to change") int id,
      @KernelFunctionParameter(name = "isOn", description = "The new state of the light") boolean isOn) {
    System.out.println("Changing light " + id + " " + isOn);
    if (!lights.containsKey(id)) {
      throw new IllegalArgumentException("Light not found");
    }

    lights.get(id).setIsOn(isOn);

    return lights.get(id);
  }
}

請注意,我們提供函式、傳回值和參數的描述。 這對於 AI 理解功能的作用以及如何使用它非常重要。

提示

如果 AI 呼叫函式時發生問題,請不要害怕提供函式的詳細描述。 使用少量訓練樣例的建議、關於什麼情況下應該使用或不應使用該函式,以及如何獲取所需參數的指引,都可以提供很大的幫助。

2) 將外掛程式新增至核心

定義外掛程式之後,您可以建立新的外掛程式實例,然後將其新增至核心的外掛程式集合。

此範例示範使用 AddFromType 方法將類別新增為外掛程式的最簡單方式。 若要瞭解新增外掛程式的其他方式,請參閱 新增原生外掛程式 一文。

var builder = new KernelBuilder();
builder.Plugins.AddFromType<LightsPlugin>("Lights")
Kernel kernel = builder.Build();
kernel = Kernel()
kernel.add_plugin(
   LightsPlugin(),
   plugin_name="Lights",
)
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
    "LightsPlugin");
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
    .withAIService(ChatCompletionService.class, chatCompletionService)
    .withPlugin(lightPlugin)
    .build();

3) 叫用外掛程式的函式

最後,您可以使用函式呼叫,讓 AI 叫用外掛程式的函式。 以下是一個範例,示範如何引導 AI 在呼叫 change_state 功能以開啟燈光之前,先從 Lights 插件中呼叫 get_lights 功能。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

// Create a kernel with Azure OpenAI chat completion
var builder = Kernel.CreateBuilder().AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey);

// Build the kernel
Kernel kernel = builder.Build();
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();

// Add a plugin (the LightsPlugin class is defined below)
kernel.Plugins.AddFromType<LightsPlugin>("Lights");

// Enable planning
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() 
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

// Create a history store the conversation
var history = new ChatHistory();
history.AddUserMessage("Please turn on the lamp");

// Get the response from the AI
var result = await chatCompletionService.GetChatMessageContentAsync(
   history,
   executionSettings: openAIPromptExecutionSettings,
   kernel: kernel);

// Print the results
Console.WriteLine("Assistant > " + result);

// Add the message from the agent to the chat history
history.AddAssistantMessage(result);
import asyncio

from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.functions.kernel_arguments import KernelArguments

from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
    AzureChatPromptExecutionSettings,
)

async def main():
   # Initialize the kernel
   kernel = Kernel()

   # Add Azure OpenAI chat completion
   chat_completion = AzureChatCompletion(
      deployment_name="your_models_deployment_name",
      api_key="your_api_key",
      base_url="your_base_url",
   )
   kernel.add_service(chat_completion)

   # Add a plugin (the LightsPlugin class is defined below)
   kernel.add_plugin(
      LightsPlugin(),
      plugin_name="Lights",
   )

   # Enable planning
   execution_settings = AzureChatPromptExecutionSettings()
   execution_settings.function_call_behavior = FunctionChoiceBehavior.Auto()

   # Create a history of the conversation
   history = ChatHistory()
   history.add_message("Please turn on the lamp")

   # Get the response from the AI
   result = await chat_completion.get_chat_message_content(
      chat_history=history,
      settings=execution_settings,
      kernel=kernel,
   )

   # Print the results
   print("Assistant > " + str(result))

   # Add the message from the agent to the chat history
   history.add_message(result)

# Run the main function
if __name__ == "__main__":
    asyncio.run(main())
// Enable planning
InvocationContext invocationContext = new InvocationContext.Builder()
    .withReturnMode(InvocationReturnMode.LAST_MESSAGE_ONLY)
    .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
    .build();

// Create a history to store the conversation
ChatHistory history = new ChatHistory();
history.addUserMessage("Turn on light 2");

List<ChatMessageContent<?>> results = chatCompletionService
    .getChatMessageContentsAsync(history, kernel, invocationContext)
    .block();

System.out.println("Assistant > " + results.get(0));

使用上述程式代碼,您應該會收到如下所示的回應:

角色 消息
🔵 使用者 請開啟燈
🔴 助理(函式呼叫) Lights.get_lights()
🟢 工具 [{ "id": 1, "name": "Table Lamp", "isOn": false, "brightness": 100, "hex": "FF0000" }, { "id": 2, "name": "Porch light", "isOn": false, "brightness": 50, "hex": "00FF00" }, { "id": 3, "name": "Chandelier", "isOn": true, "brightness": 75, "hex": "0000FF" }]
🔴 助理 (函式呼叫) Lights.change_state(1, { “isOn”: true })
🟢 工具 { "id": 1, "name": "Table Lamp", "isOn": true, "brightness": 100, "hex": "FF0000" }
🔴 助理 燈現在開啟

提示

雖然您可以直接調用外掛程式函式,但不建議這樣做,因為應該由 AI 決定要調用哪些函式。 如果您需要明確控制呼叫哪些函式,請考慮在程式代碼基底中使用標準方法,而不是外掛程式。

撰寫外掛程式的一般建議

考慮到每個案例都有獨特的需求、利用不同的外掛程式設計,並可能併入多個 LLM,因此很難提供一個通用的外掛程式設計指南。 不過,以下是一些一般建議和指導方針,以確保外掛程式是 AI 友善的,且可被 LLM 輕鬆有效地使用。

只匯入必要的外掛程式

只匯入包含特定案例所需函式的外掛程式。 此方法不僅會減少所耗用的輸入令牌數目,還會減少在情境中未使用之函式的誤召喚次數。 整體而言,此策略應增強函式呼叫精確度,並減少誤判數目。

此外,OpenAI 建議您在單一 API 呼叫中使用不超過 20 個工具;在理想情況下,不超過10個工具。 如 OpenAI 所述:「我們建議您在單一 API 呼叫中使用不超過 20 個工具。開發人員通常會看到模型在定義 10 到 20 個工具之間之後選取正確工具的能力降低。」* 如需詳細資訊,您可以在 OpenAI 函數呼叫指南瀏覽其檔。

讓外掛程式變得 AI 友好

若要增強 LLM 瞭解及利用外掛程式的能力,建議您遵循下列指導方針:

  • 使用描述性和簡潔的函式名稱: 確定函式名稱清楚傳達其用途,以協助模型瞭解何時選取每個函式。 如果函式名稱模棱兩可,請考慮重新命名,以便清楚起見。 避免使用縮寫或首字母縮略詞來縮短函式名稱。 請利用 DescriptionAttribute,只在必要時提供額外的內容和指示,將令牌耗用量降至最低。

  • 最小化函式參數: 盡可能限制函式參數數目並使用基本類型。 此方法可減少令牌耗用量並簡化函式簽章,讓 LLM 更容易有效地比對函式參數。

  • 名稱函式參數清楚: 將描述性名稱指派給函式參數,以釐清其用途。 請避免使用縮寫或縮略字來縮短參數名稱,因為這將有助於 LLM 推理參數並提供精確的值。 如同函式名稱,只有在需要時才使用 DescriptionAttribute,以將令牌耗用量降到最低。

尋找功能數目與其對應責任之間的適當平衡

一方面,具有單一責任的函式是一個很好的做法,可讓函式在多個案例中保持簡單且可重複使用。 另一方面,每個函式呼叫都會在網路來回延遲和已取用的輸入和輸出令牌數目方面產生額外負荷:輸入令牌可用來將函式定義和調用結果傳送至 LLM,而從模型接收函式呼叫時會取用輸出令牌。 或者,可以實作具有多個責任的單一函式,以減少已取用的令牌數目並降低網路負擔,但這樣做會降低在其他情境中的重複使用性。

不過,將許多責任合併成單一函式可能會增加函式參數及其傳回型別的數目和複雜度。 這種複雜性可能會導致模型可能難以正確比對函式參數的情況,導致遺漏的參數或不正確的類型值。 因此,請務必在函式數目之間取得正確的平衡,以減少網路額外負荷,以及每個函式所承擔的責任數目,確保模型能夠準確地比對函式參數。

轉換語意核心函式

使用轉換技術來應用於語意核心函式,如 轉換語意核心函式 部落格文章中所述:

  • 變更函式行為: 在某些情況下,函式的預設行為可能無法與所需的結果一致,而且修改原始函式的實作是不可行的。 在這種情況下,您可以建立新的函式來包裝原始函式,並據以修改其行為。

  • 提供背景資訊: 函數可能需要一些參數,而這些參數是大型語言模型(LLM)無法或不應該推斷的。 例如,如果函數需要代替當前使用者執行動作或需要驗證資訊,這些上下文通常由主應用程式提供,但不適用於LLM。 在這種情況下,您可以轉換功能來呼叫原始功能,並從主控應用程式提供需要的上下文資訊,以及 LLM 提供的參數。

  • 變更參數清單、類型和名稱: 如果原始函式具有 LLM 難以解譯的複雜簽章,您可以將函式轉換成具有 LLM 更容易了解的簡單簽章。 這可能涉及變更參數名稱、類型、參數數目,以及扁平化或取消擴充複雜參數等調整。

本機狀態使用率

設計在相對大型或機密數據集上運作的外掛程式時,例如包含敏感性資訊的檔、文章或電子郵件,請考慮使用本機狀態來儲存不需要傳送至 LLM 的原始數據或中繼結果。 這類案例的函式可以接受並傳回狀態標識碼,讓您在本機查閱和存取數據,而不是將實際數據傳遞至 LLM,只接收它作為下一個函式調用的自變數。

藉由將數據儲存在本機,您可以將資訊保持私密且安全,同時避免在函數調用期間使用不必要的令牌。 這種方法不僅能增強數據隱私權,還能提升處理大型或敏感性數據集的整體效率。