Vad är ett plugin-program?
Plugin-program är en viktig komponent i semantisk kernel. Om du redan har använt plugin-program från ChatGPT- eller Copilot-tillägg i Microsoft 365 är du redan bekant med dem. Med plugin-program kan du kapsla in dina befintliga API:er i en samling som kan användas av en AI. På så sätt kan du ge din AI möjlighet att utföra åtgärder som den inte skulle kunna göra annars.
I bakgrunden utnyttjar Semantic Kernel funktionen som anropar, en inbyggd funktion i de flesta av de senaste LLM:erna för att tillåta dem att utföra planering och anropa dina API:er. Med funktionsanrop kan LLM:er begära (d.v.s. anropa) en viss funktion. Semantisk kernel konverterar sedan begäran till lämplig funktion i din kodbas och returnerar resultatet tillbaka till LLM så att LLM kan generera ett slutligt svar.
Alla AI-SDK:er har inte ett liknande begrepp som plugin-program (de flesta har bara funktioner eller verktyg). I företagsscenarier är dock plugin-program värdefulla eftersom de kapslar in en uppsättning funktioner som speglar hur företagsutvecklare redan utvecklar tjänster och API:er. Plugins fungerar också bra med beroendeinjektion. I en plugin-konstruktor kan du mata in tjänster som är nödvändiga för att utföra plugin-programmets arbete (t.ex. databasanslutningar, HTTP-klienter osv.). Detta är svårt att åstadkomma med andra SDK:er som saknar plugin-program.
Anatomi för ett plugin-program
På hög nivå är ett plugin-program en grupp med funktioner som kan exponeras för AI-appar och -tjänster. Funktionerna i plugin-program kan sedan orkestreras av ett AI-program för att utföra användarbegäranden. I semantisk kernel kan du anropa dessa funktioner automatiskt med funktionsanrop.
Not
På andra plattformar kallas funktioner ofta för "verktyg" eller "åtgärder". I Semantic Kernel använder vi termen "functions" eftersom de vanligtvis definieras som inbyggda funktioner i din kodbas.
Att bara tillhandahålla funktioner räcker dock inte för att skapa ett plugin-program. För att kunna utföra automatisk orkestrering med funktionsanrop måste plugin-program också tillhandahålla information som semantiskt beskriver hur de beter sig. Allt från funktionens indata, utdata och biverkningar måste beskrivas på ett sätt som AI:n kan förstå, annars anropar AI:n inte funktionen korrekt.
Exempelprogrammet WriterPlugin
till höger har till exempel funktioner med semantiska beskrivningar som beskriver vad varje funktion gör. En LLM kan sedan använda dessa beskrivningar för att välja de bästa funktionerna att anropa för att uppfylla en användares fråga.
På bilden till höger skulle en LLM sannolikt anropa funktionerna ShortPoem
och StoryGen
för att tillfredsställa användarnas frågor tack vare de angivna semantiska beskrivningarna.
Importera olika typer av plugin-program
Det finns två huvudsakliga sätt att importera plugin-program till semantisk kernel: använda inbyggd kod eller använda en OpenAPI-specifikation. Med det förra kan du skapa plugin-program i din befintliga kodbas som kan utnyttja beroenden och tjänster som du redan har. Med det senare kan du importera plugin-program från en OpenAPI-specifikation som kan delas mellan olika programmeringsspråk och plattformar.
Nedan visas ett enkelt exempel på hur du importerar och använder ett inbyggt plugin-program. Mer information om hur du importerar dessa olika typer av plugin-program finns i följande artiklar:
Tips
När du kommer igång rekommenderar vi att du använder inbyggda kodtillägg. Allt eftersom ditt program mognar, och när du arbetar mellan plattformsoberoende team, kanske du vill överväga att använda OpenAPI-specifikationer för att dela plugin-program mellan olika programmeringsspråk och plattformar.
De olika typerna av plugin-funktioner
I ett plugin-program har du vanligtvis två olika typer av funktioner, de som hämtar data för hämtning av utökad generering (RAG) och de som automatiserar uppgifter. Även om varje typ fungerar likadant används de vanligtvis på olika sätt i program som använder semantisk kernel.
Med hämtningsfunktioner kanske du till exempel vill använda strategier för att förbättra prestanda (t.ex. cachelagring och användning av billigare mellanliggande modeller för sammanfattning). Medan du med automatiserade uppgifter förmodligen vill implementera godkännandeprocesser med mänsklig övervakning för att säkerställa att uppgifterna slutförs korrekt.
Mer information om de olika typerna av plugin-funktioner finns i följande artiklar:
Komma igång med plugin-program
Att använda plugin-program i semantisk kernel är alltid en process i tre steg:
- Definiera din plugin
- Lägg till plugin-programmet i kärnan
- Och anropa sedan antingen plugin-programmets funktioner i antingen en prompt med funktionsanrop
Nedan visas ett exempel på hur du använder ett plugin-program i semantisk kernel. Mer detaljerad information om hur du skapar och använder plugin-program finns i länkarna ovan.
1) Definiera plugin-programmet
Det enklaste sättet att skapa ett plugin-program är att definiera en klass och kommentera dess metoder med attributet KernelFunction
. För att den semantiska kärnan ska veta att detta är en funktion som kan anropas av en AI eller refereras till i en prompt.
Du kan också importera plugin-program från en OpenAPI-specifikation.
Nedan skapar vi ett plugin-program som kan hämta lampornas tillstånd och ändra dess tillstånd.
Tips
Eftersom de flesta LLM har tränats med Python vid funktionsanrop rekommenderas det att använda snake case för funktionsnamn och egenskapsnamn, även om du använder C# eller 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);
}
}
Observera att vi tillhandahåller beskrivningar för funktionen, returvärdet och parametrarna. Det här är viktigt för AI:n att förstå vad funktionen gör och hur den ska användas.
Tips
Var inte rädd för att ange detaljerade beskrivningar för dina funktioner om en AI har problem med att anropa dem. Få exempel, rekommendationer för när du ska använda (och inte använda) funktionen och vägledning om var du kan hämta obligatoriska parametrar kan vara till hjälp.
2) Lägg till plugin-programmet i din kernel
När du har definierat plugin-programmet kan du lägga till det i kerneln genom att skapa en ny instans av plugin-programmet och lägga till det i kernelns plugin-samling.
Det här exemplet visar det enklaste sättet att lägga till en klass som ett plugin-program med metoden AddFromType
. Mer information om andra sätt att lägga till plugin-program finns i artikeln att lägga till inbyggda plugin-program.
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) Anropa plugin-programmets funktioner
Slutligen kan du låta AI:n anropa plugin-programmets funktioner med hjälp av funktionsanrop. Nedan visas ett exempel som visar hur du koaxar AI:n för att anropa funktionen get_lights
från Lights
-plugin-programmet innan du anropar funktionen change_state
för att aktivera en lampa.
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_choice_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));
Med koden ovan bör du få ett svar som ser ut så här:
Roll | Meddelande |
---|---|
🔵 användare | Slå på lampan |
🔴 Assistant (funktionsanrop) | Lights.get_lights() |
🟢 Verktyg | [{ "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" }] |
🔴 Assistant (funktionsanrop) | Lights.change_state(1, { "isOn": true }) |
🟢 Verktyg | { "id": 1, "name": "Table Lamp", "isOn": true, "brightness": 100, "hex": "FF0000" } |
🔴 Assistant | Lampan är nu på |
Tips
Du kan anropa en plugin-funktion direkt, men det rekommenderas inte eftersom AI:n bör vara den som bestämmer vilka funktioner som ska anropas. Om du behöver explicit kontroll över vilka funktioner som anropas bör du överväga att använda standardmetoder i din kodbas i stället för plugin-program.
Allmänna rekommendationer för redigering av plugin-program
Med tanke på att varje scenario har unika krav, använder distinkta plugin-design och kan innehålla flera LLM:er är det svårt att tillhandahålla en guide som passar alla för plugin-design. Nedan visas dock några allmänna rekommendationer och riktlinjer för att säkerställa att plugin-program är AI-vänliga och lätt och effektivt kan användas av LLM:er.
Importera endast nödvändiga plugin-program
Importera endast de plugin-program som innehåller de funktioner som krävs för ditt specifika scenario. Den här metoden minskar inte bara antalet förbrukade indatatoken, utan minimerar även förekomsten av funktionsfelanrop till funktioner som inte används i scenariot. Sammantaget bör den här strategin förbättra precisionen för funktionsanrop och minska antalet falska positiva identifieringar.
Dessutom rekommenderar OpenAI att du inte använder fler än 20 verktyg i ett enda API-anrop. helst inte mer än 10 verktyg. Enligt OpenAI: "Vi rekommenderar att du inte använder fler än 20 verktyg i ett enda API-anrop. Utvecklare ser vanligtvis en minskning av modellens möjlighet att välja rätt verktyg när de har definierat mellan 10 och 20 verktyg."* Mer information finns i dokumentationen till OpenAI-funktionssamtalsguiden.
Gör plugin-program AI-vänliga
För att förbättra LLM:s förmåga att förstå och använda plugin-program rekommenderar vi att du följer dessa riktlinjer:
Använd beskrivande och koncisa funktionsnamn: Kontrollera att funktionsnamn tydligt förmedlar sitt syfte för att hjälpa modellen att förstå när varje funktion ska väljas. Om ett funktionsnamn är tvetydigt kan du överväga att byta namn på det för tydlighetens skull. Undvik att använda förkortningar eller akronymer för att förkorta funktionsnamn. Använd
DescriptionAttribute
för att endast tillhandahålla ytterligare kontext och instruktioner när det behövs, vilket minimerar tokenförbrukningen.Minimera funktionsparametrar: Begränsa antalet funktionsparametrar och använd primitiva typer när det är möjligt. Den här metoden minskar tokenförbrukningen och förenklar funktionssignaturen, vilket gör det enklare för LLM att matcha funktionsparametrar effektivt.
Namnge funktionsparametrar tydligt: Tilldela beskrivande namn till funktionsparametrar för att klargöra deras syfte. Undvik att använda akronymer eller förkortningar för att förkorta parameternamn, eftersom detta hjälper LLM att resonera om parametrarna och tillhandahålla korrekta värden. Precis som med funktionsnamn använder du endast
DescriptionAttribute
när det behövs för att minimera tokenförbrukningen.
Hitta rätt balans mellan antalet funktioner och deras ansvarsområden
Å ena sidan är det bra att ha funktioner med ett enda ansvar som gör det möjligt att hålla funktionerna enkla och återanvändbara i flera scenarier. Å andra sidan medför varje funktionsanrop omkostnader när det gäller svarstid för nätverksåterhämtning och antalet förbrukade in- och utdatatoken: indatatoken används för att skicka funktionsdefinitionen och anropsresultatet till LLM, medan utdatatoken förbrukas när du tar emot funktionsanropet från modellen.
Alternativt kan en enskild funktion med flera ansvarsområden implementeras för att minska antalet förbrukade token och lägre nätverkskostnader, även om detta sker på bekostnad av minskad återanvändning i andra scenarier.
Att konsolidera många ansvarsområden i en enda funktion kan dock öka antalet och komplexiteten hos funktionsparametrar och dess returtyp. Den här komplexiteten kan leda till situationer där modellen kan ha svårt att matcha funktionsparametrarna korrekt, vilket resulterar i missade parametrar eller värden av felaktig typ. Därför är det viktigt att hitta rätt balans mellan antalet funktioner för att minska nätverkskostnaderna och antalet ansvarsområden som varje funktion har, vilket säkerställer att modellen korrekt kan matcha funktionsparametrar.
Transformera semantiska kernelfunktioner
Använd transformeringsteknikerna för semantiska kernelfunktioner enligt beskrivningen i Transformera semantiska kernelfunktioner blogginlägget till:
Ändra funktionsbeteende: Det finns scenarier där standardbeteendet för en funktion kanske inte överensstämmer med önskat resultat och det inte är möjligt att ändra implementeringen av den ursprungliga funktionen. I sådana fall kan du skapa en ny funktion som omsluter den ursprungliga funktionen och ändrar dess beteende i enlighet med detta.
Ange kontextinformation: Functions kan kräva parametrar som LLM inte kan eller inte bör härleda. Om en funktion till exempel behöver agera för den aktuella användarens räkning eller kräver autentiseringsinformation, är den här kontexten vanligtvis tillgänglig för värdprogrammet men inte för LLM. I sådana fall kan du transformera funktionen så att den anropar den ursprungliga samtidigt som du anger nödvändig kontextinformation från värdprogrammet, tillsammans med argument som tillhandahålls av LLM.
Ändra parameterlista, typer och namn: Om den ursprungliga funktionen har en komplex signatur som LLM har svårt att tolka kan du omvandla funktionen till en med en enklare signatur som LLM lättare kan förstå. Det kan handla om att ändra parameternamn, typer, antal parametrar och platta ut eller återställa komplexa parametrar, bland andra justeringar.
Lokal tillståndsanvändning
När du utformar plugin-program som fungerar på relativt stora eller konfidentiella datamängder, till exempel dokument, artiklar eller e-postmeddelanden som innehåller känslig information, bör du överväga att använda det lokala tillståndet för att lagra ursprungliga data eller mellanliggande resultat som inte behöver skickas till LLM. Funktioner för sådana scenarier kan acceptera och returnera ett tillstånds-ID, så att du kan söka efter och komma åt data lokalt i stället för att skicka faktiska data till LLM, bara för att få tillbaka dem som argument för nästa funktionsanrop.
Genom att lagra data lokalt kan du hålla informationen privat och säker samtidigt som du undviker onödig tokenförbrukning under funktionsanrop. Den här metoden förbättrar inte bara datasekretessen utan förbättrar även den övergripande effektiviteten vid bearbetning av stora eller känsliga datamängder.