將機器碼新增為外掛程式
提供 AI 代理程式功能的最簡單方式是將原生程式代碼包裝至外掛程式。 這可讓您利用您身為應用程式開發人員的現有技能,擴充 AI 代理程式的功能。
在幕後,Semantic Kernel 接著會使用您提供的描述以及反映,以語意方式描述 AI 代理程式的外掛程式。 這可讓 AI 代理程式瞭解外掛程式的功能,以及如何與其互動。
提供 LLM 的正確資訊
撰寫外掛程式時,您必須提供 AI 代理程式正確的資訊,以瞭解外掛程式及其功能的功能。 這包括:
- 外掛程式的名稱
- 函式的名稱
- 函式的描述
- 函式的參數
- 參數的架構
- 傳回值的架構
Semantic Kernel 的值是,它可以從程式碼本身自動產生大部分的資訊。 身為開發人員,這隻表示您必須提供函式和參數的語意描述,讓 AI 代理程式可以了解它們。 不過,如果您正確批註並標註程式代碼,您可能已經手頭有此資訊。
接下來,我們將逐步解說兩種不同的方式,為您的 AI 代理程式提供原生程式代碼,以及如何提供此語意資訊。
使用類別定義外掛程式
建立原生外掛程式最簡單的方式是從 類別開始,然後新增以 KernelFunction
屬性標註的方法。 此外,也建議您自由使用 Description
註釋來提供 AI 代理程式所需的資訊,以瞭解函式。
public class LightsPlugin
{
private readonly List<LightModel> _lights;
public LightsPlugin(LoggerFactory loggerFactory, List<LightModel> lights)
{
_lights = lights;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
return _lights;
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
// Find the light to change
var light = _lights.FirstOrDefault(l => l.Id == changeState.Id);
// If the light does not exist, return null
if (light == null)
{
return null;
}
// Update the light state
light.IsOn = changeState.IsOn;
light.Brightness = changeState.Brightness;
light.Color = changeState.Color;
return light;
}
}
from typing import List, Optional, Annotated
class LightsPlugin:
def __init__(self, lights: List[LightModel]):
self._lights = lights
@kernel_function
async def get_lights(self) -> List[LightModel]:
"""Gets a list of lights and their current state."""
return self._lights
@kernel_function
async def change_state(
self,
change_state: LightModel
) -> Optional[LightModel]:
"""Changes the state of the light."""
for light in self._lights:
if light["id"] == change_state["id"]:
light["is_on"] = change_state.get("is_on", light["is_on"])
light["brightness"] = change_state.get("brightness", light["brightness"])
light["hex"] = change_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, LightModel.Brightness.MEDIUM, "#FFFFFF"));
lights.put(2, new LightModel(2, "Porch light", false, LightModel.Brightness.HIGH, "#FF0000"));
lights.put(3, new LightModel(3, "Chandelier", true, LightModel.Brightness.LOW, "#FFFF00"));
}
@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 = "model",
description = "The new state of the model to set. Example model: " +
"{\"id\":99,\"name\":\"Head Lamp\",\"isOn\":false,\"brightness\":\"MEDIUM\",\"color\":\"#FFFFFF\"}",
type = LightModel.class) LightModel model
) {
System.out.println("Changing light " + model.getId() + " " + model.getIsOn());
if (!lights.containsKey(model.getId())) {
throw new IllegalArgumentException("Light not found");
}
lights.put(model.getId(), model);
return lights.get(model.getId());
}
}
提示
由於 LLM 是以 Python 程式代碼為主的訓練,因此建議針對函式名稱和參數使用snake_case(即使您使用 C# 或 Java)。 這有助於 AI 代理程式進一步瞭解函式及其參數。
提示
您的函式可以指定 Kernel
、KernelArguments
、ILoggerFactory
、ILogger
、IAIServiceSelector
、CultureInfo
、IFormatProvider
、CancellationToken
作為參數,而且這些參數不會公告給 LLM,而且會在呼叫函式時自動設定。
如果您依賴 KernelArguments
而不是明確的輸入自變數,則程式代碼將負責執行類型轉換。
如果您的函式具有複雜物件做為輸入變數,Semantic Kernel 也會產生該對象的架構,並將它傳遞至 AI 代理程式。 與函式類似,您應該為 AI 不明顯的屬性提供 Description
註釋。 以下是 類別和LightState
列舉的定義Brightness
。
using System.Text.Json.Serialization;
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 Brightness? Brightness { get; set; }
[JsonPropertyName("color")]
[Description("The color of the light with a hex code (ensure you include the # symbol)")]
public string? Color { get; set; }
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Brightness
{
Low,
Medium,
High
}
from typing import TypedDict
class LightModel(TypedDict):
id: int
name: str
is_on: bool | None
brightness: int | None
hex: str | None
public class LightModel {
private int id;
private String name;
private Boolean isOn;
private Brightness brightness;
private String color;
public enum Brightness {
LOW,
MEDIUM,
HIGH
}
public LightModel(int id, String name, Boolean isOn, Brightness brightness, String color) {
this.id = id;
this.name = name;
this.isOn = isOn;
this.brightness = brightness;
this.color = color;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Boolean getIsOn() {
return isOn;
}
public void setIsOn(Boolean isOn) {
this.isOn = isOn;
}
public Brightness getBrightness() {
return brightness;
}
public void setBrightness(Brightness brightness) {
this.brightness = brightness;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
注意
雖然這是「有趣的」範例,但它會執行良好作業,顯示外掛程式的參數有多複雜。 在此單一案例中,我們有一個具有 四 種不同類型屬性的複雜物件:整數、字串、布爾值和列舉。 語意核心的值是,它可以自動產生此對象的架構,並將其傳遞至 AI 代理程式,並將 AI 代理程式所產生的參數封送處理至正確的物件。
完成撰寫外掛程式類別之後,您可以使用 或 AddFromType<>
方法將其新增至核心AddFromObject
。
提示
建立函式時,請一律問自己「如何提供 AI 額外協助以使用此函式?這包括使用特定輸入類型(盡可能避免字串)、提供描述和範例。
使用 AddFromObject
方法新增外掛程式
AddFromObject
方法可讓您將外掛程式類別的實例直接新增至外掛程式集合,以防您想要直接控制外掛程式的建構方式。
例如,類別的 LightsPlugin
建構函式需要燈光清單。 在此情況下,您可以建立外掛程式類別的實例,並將其新增至外掛程式集合。
List<LightModel> lights = new()
{
new LightModel { Id = 1, Name = "Table Lamp", IsOn = false, Brightness = Brightness.Medium, Color = "#FFFFFF" },
new LightModel { Id = 2, Name = "Porch light", IsOn = false, Brightness = Brightness.High, Color = "#FF0000" },
new LightModel { Id = 3, Name = "Chandelier", IsOn = true, Brightness = Brightness.Low, Color = "#FFFF00" }
};
kernel.Plugins.AddFromObject(new LightsPlugin(lights));
使用 AddFromType<>
方法新增外掛程式
使用 AddFromType<>
方法時,核心會自動使用相依性插入來建立外掛程式類別的實例,並將它新增至外掛程式集合。
如果您的建構函式需要將服務或其他相依性插入外掛程式,這會很有説明。 例如,我們的 LightsPlugin
類別可能需要將記錄器和光線服務插入其中,而不是燈光清單。
public class LightsPlugin
{
private readonly Logger _logger;
private readonly LightService _lightService;
public LightsPlugin(LoggerFactory loggerFactory, LightService lightService)
{
_logger = loggerFactory.CreateLogger<LightsPlugin>();
_lightService = lightService;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
_logger.LogInformation("Getting lights");
return lightService.GetLights();
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
_logger.LogInformation("Changing light state");
return lightService.ChangeState(changeState);
}
}
使用相依性插入,您可以在建置核心之前,將必要的服務和外掛程式新增至核心產生器。
var builder = Kernel.CreateBuilder();
// Add dependencies for the plugin
builder.Services.AddLogging(loggingBuilder => loggingBuilder.AddConsole().SetMinimumLevel(LogLevel.Trace));
builder.Services.AddSingleton<LightService>();
// Add the plugin to the kernel
builder.Plugins.AddFromType<LightsPlugin>("Lights");
// Build the kernel
Kernel kernel = builder.Build();
使用函式集合定義外掛程式
較不常見,但仍很有用的是使用函式集合來定義外掛程式。 如果您需要在運行時間從一組函式動態建立外掛程式,這會特別有用。
使用此程式需要您先使用函式處理站建立個別函式,再將它們新增至外掛程式。
kernel.Plugins.AddFromFunctions("time_plugin",
[
KernelFunctionFactory.CreateFromMethod(
method: () => DateTime.Now,
functionName: "get_time",
description: "Get the current time"
),
KernelFunctionFactory.CreateFromMethod(
method: (DateTime start, DateTime end) => (end - start).TotalSeconds,
functionName: "diff_time",
description: "Get the difference between two times in seconds"
)
]);
使用相依性插入新增原生程序代碼的其他策略
如果您正在使用相依性插入,您可以採取其他策略來建立和新增外掛程式至核心。 以下是如何使用相依性插入新增外掛程式的一些範例。
插入外掛程式集合
提示
建議您讓您的外掛程式集合成為暫時性服務,以便在每次使用之後處置,因為外掛程式集合是可變的。 為每個用途建立新的外掛程式集合很便宜,因此它不應該是效能問題。
var builder = Host.CreateApplicationBuilder(args);
// Create native plugin collection
builder.Services.AddTransient((serviceProvider)=>{
KernelPluginCollection pluginCollection = [];
pluginCollection.AddFromType<LightsPlugin>("Lights");
return pluginCollection;
});
// Create the kernel service
builder.Services.AddTransient<Kernel>((serviceProvider)=> {
KernelPluginCollection pluginCollection = serviceProvider.GetRequiredService<KernelPluginCollection>();
return new Kernel(serviceProvider, pluginCollection);
});
提示
如核心文章所述,核心非常輕量,因此,將每個用途建立新的核心做為暫時性不是效能考慮。
將外掛程式產生為單一外掛程式
外掛程式不可變動,因此通常會安全地將其建立為單一。 這可以使用外掛程式處理站,並將產生的外掛程式新增至您的服務集合來完成。
var builder = Host.CreateApplicationBuilder(args);
// Create singletons of your plugin
builder.Services.AddKeyedSingleton("LightPlugin", (serviceProvider, key) => {
return KernelPluginFactory.CreateFromType<LightsPlugin>();
});
// Create a kernel service with singleton plugin
builder.Services.AddTransient((serviceProvider)=> {
KernelPluginCollection pluginCollection = [
serviceProvider.GetRequiredKeyedService<KernelPlugin>("LightPlugin")
];
return new Kernel(serviceProvider, pluginCollection);
});
使用 add_plugin
方法新增外掛程式
方法 add_plugin
可讓您將外掛程式實例新增至核心。 以下是如何建構 類別並將其 LightsPlugin
新增至核心的範例。
# Create the kernel
kernel = Kernel()
# Create dependencies for the plugin
lights = [
{"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"},
]
# Create the plugin
lights_plugin = LightsPlugin(lights)
# Add the plugin to the kernel
kernel.add_plugin(lights_plugin)
使用 createFromObject
方法新增外掛程式
createFromObject
方法可讓您使用批注方法從 Object 建置核心外掛程式。
// 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();
將功能回傳型別架構提供給 LLM
目前,沒有定義完善的全產業標準,為 AI 模型提供函式傳回型別元數據。 在建立這類標準之前,對於傳回型別屬性的名稱不足以讓 LLM 對其內容產生原因的案例,或需要與傳回型別相關聯的其他內容或處理指示來建立模型或增強案例的案例,可以考慮下列技術。
在採用上述任何技術之前,建議您為傳回類型屬性提供更描述性的名稱,因為這是改善 LLM 對傳回類型瞭解的最直接方式,而且在令牌使用方面也是符合成本效益的方法。
在函式描述中提供函式傳回型別資訊
若要套用這項技術,請在函式的描述屬性中包含傳回型別架構。 架構應該詳細說明屬性名稱、描述和類型,如下列範例所示:
public class LightsPlugin
{
[KernelFunction("change_state")]
[Description("""Changes the state of the light and returns:
{
"type": "object",
"properties": {
"id": { "type": "integer", "description": "Light ID" },
"name": { "type": "string", "description": "Light name" },
"is_on": { "type": "boolean", "description": "Is light on" },
"brightness": { "type": "string", "enum": ["Low", "Medium", "High"], "description": "Brightness level" },
"color": { "type": "string", "description": "Hex color code" }
},
"required": ["id", "name"]
}
""")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
...
}
}
某些模型可能會限制函式描述的大小,因此建議保持架構簡潔,且只包含重要資訊。
如果類型資訊並不重要,且將令牌耗用量降至最低是優先順序,請考慮在函式的描述屬性中提供傳回類型的簡短描述,而不是完整架構。
public class LightsPlugin
{
[KernelFunction("change_state")]
[Description("""Changes the state of the light and returns:
id: light ID,
name: light name,
is_on: is light on,
brightness: brightness level (Low, Medium, High),
color: Hex color code.
""")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
...
}
}
上述兩種方法都需要手動新增傳回型別架構,並在每次傳回型別變更時更新它。 若要避免這種情況,請考慮下一項技術。
提供函式傳回型別架構做為函式傳回值的一部分
這項技術牽涉到同時提供函式的傳回值和其架構給 LLM,而不只是傳回值。 這可讓 LLM 使用架構來推斷傳回值的屬性。
若要實作這項技術,您必須建立並註冊自動函式調用篩選器。 如需詳細資訊,請參閱 自動函數調用篩選 一文。 此篩選應該將函式的傳回值包裝在包含原始傳回值及其架構的自定義物件中。 以下是範例:
private sealed class AddReturnTypeSchemaFilter : IAutoFunctionInvocationFilter
{
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
await next(context); // Invoke the original function
// Crete the result with the schema
FunctionResultWithSchema resultWithSchema = new()
{
Value = context.Result.GetValue<object>(), // Get the original result
Schema = context.Function.Metadata.ReturnParameter?.Schema // Get the function return type schema
};
// Return the result with the schema instead of the original one
context.Result = new FunctionResult(context.Result, resultWithSchema);
}
private sealed class FunctionResultWithSchema
{
public object? Value { get; set; }
public KernelJsonSchema? Schema { get; set; }
}
}
// Register the filter
Kernel kernel = new Kernel();
kernel.AutoFunctionInvocationFilters.Add(new AddReturnTypeSchemaFilter());
註冊篩選后,您現在可以提供傳回型別及其屬性的描述,語意核心會自動擷取:
[Description("The state of the light")] // Equivalent to annotating the function with the [return: Description("The state of the light")] attribute
public class LightModel
{
[JsonPropertyName("id")]
[Description("The ID of the light")]
public int Id { get; set; }
[JsonPropertyName("name")]
[Description("The name of the light")]
public string? Name { get; set; }
[JsonPropertyName("is_on")]
[Description("Indicates whether the light is on")]
public bool? IsOn { get; set; }
[JsonPropertyName("brightness")]
[Description("The brightness level of the light")]
public Brightness? Brightness { get; set; }
[JsonPropertyName("color")]
[Description("The color of the light with a hex code (ensure you include the # symbol)")]
public string? Color { get; set; }
}
此方法不需要在每次傳回型別變更時手動提供和更新傳回型別架構,因為語意核心會自動擷取架構。
下一步
既然您已瞭解如何建立外掛程式,您現在可以瞭解如何將其與 AI 代理程式搭配使用。 根據您新增至外掛程式的函式類型而定,您應該遵循不同的模式。 如需擷取函式,請參閱 使用擷取函 式一文。 如需工作自動化函式,請參閱 使用工作自動化函 式一文。