플러그 인이란?
플러그 인은 의미 체계 커널의 핵심 구성 요소입니다. Microsoft 365의 ChatGPT 또는 Copilot 확장에서 플러그 인을 이미 사용한 경우 이미 익숙한 것입니다. 플러그 인을 사용하면 기존 API를 AI에서 사용할 수 있는 컬렉션에 캡슐화할 수 있습니다. 이렇게 하면 AI가 달리 수행할 수 없는 작업을 수행할 수 있는 기능을 제공할 수 있습니다.
무대 뒤에서 시맨틱 커널은 대부분의 최신 LLM에서 제공하는 기본 기능인 함수 호출을 활용하여, LLM이 계획을 수행하고 API를 호출할 수 있도록 합니다. 함수 호출을 사용하면 LLM이 특정 함수를 요청(즉, 호출)할 수 있습니다. 그런 다음 의미 체계 커널은 요청을 코드베이스의 적절한 함수로 마샬링하고 LLM이 최종 응답을 생성할 수 있도록 결과를 LLM에 다시 반환합니다.
모든 AI SDK에 플러그 인과 유사한 개념이 있는 것은 아닙니다(대부분 함수 또는 도구만 포함). 그러나 엔터프라이즈 시나리오에서 플러그 인은 엔터프라이즈 개발자가 이미 서비스 및 API를 개발하는 방법을 반영하는 기능 집합을 캡슐화하기 때문에 유용합니다. 플러그인은 종속성 주입과 잘 호환됩니다. 플러그 인의 생성자 내에서 플러그 인(예: 데이터베이스 연결, HTTP 클라이언트 등)의 작업을 수행하는 데 필요한 서비스를 삽입할 수 있습니다. 플러그 인이 부족한 다른 SDK에서는 이 작업을 수행하기가 어렵습니다.
플러그 인의 해부학
개략적으로 플러그 인은 AI 앱 및 서비스에 노출될 수 있는 함수 그룹입니다. 그런 다음, 플러그 인 내의 함수를 AI 애플리케이션에서 오케스트레이션하여 사용자 요청을 수행할 수 있습니다. 의미 체계 커널 내에서 함수 호출을 사용하여 이러한 함수를 자동으로 호출할 수 있습니다.
메모
다른 플랫폼에서는 함수를 "도구" 또는 "작업"이라고도 합니다. 의미 체계 커널에서는 일반적으로 코드베이스에서 네이티브 함수로 정의되므로 "functions"라는 용어를 사용합니다.
그러나 함수를 제공하는 것만으로는 플러그 인을 만들기에 충분하지 않습니다. 함수 호출을 사용하여 자동 오케스트레이션에 전원을 공급하려면 플러그 인이 동작 방식을 의미적으로 설명하는 세부 정보도 제공해야 합니다. 함수의 입력, 출력 및 부작용의 모든 내용은 AI가 이해할 수 있는 방식으로 설명해야 합니다. 그렇지 않으면 AI가 함수를 올바르게 호출하지 않습니다.
예를 들어 오른쪽의 샘플 WriterPlugin
플러그 인에는 각 함수가 수행하는 작업을 설명하는 의미 체계 설명이 포함된 함수가 있습니다. 그런 다음 LLM은 이러한 설명을 사용하여 사용자의 요청을 충족하기 위해 호출할 최상의 함수를 선택할 수 있습니다.
오른쪽 그림에서 LLM은 제공된 의미 체계 설명 덕분에 사용자가 요청하는 것을 만족시키기 위해 ShortPoem
및 StoryGen
함수를 호출할 가능성이 높습니다.
WriterPlugin 플러그인 내의
다양한 유형의 플러그 인 가져오기
플러그 인을 의미 체계 커널로 가져오는 두 가지 기본 방법은 네이티브 코드 사용하거나 OpenAPI 사양사용하는 것입니다. 전자를 사용하면 기존 코드베이스에서 이미 가지고 있는 종속성 및 서비스를 활용할 수 있는 플러그 인을 작성할 수 있습니다. 후자를 사용하면 여러 프로그래밍 언어 및 플랫폼에서 공유할 수 있는 OpenAPI 사양에서 플러그 인을 가져올 수 있습니다.
아래에서는 네이티브 플러그 인을 가져오고 사용하는 간단한 예제를 제공합니다. 이러한 다양한 유형의 플러그 인을 가져오는 방법에 대한 자세한 내용은 다음 문서를 참조하세요.
팁
시작할 때 네이티브 코드 플러그 인을 사용하는 것이 좋습니다. 애플리케이션이 완성되고 플랫폼 간 팀에서 작업할 때 OpenAPI 사양을 사용하여 다양한 프로그래밍 언어 및 플랫폼에서 플러그 인을 공유하는 것이 좋습니다.
다양한 유형의 플러그 인 함수
플러그 인 내에는 일반적으로 RAG(검색 보강 생성)에 대한 데이터를 검색하는 함수와 작업을 자동화하는 두 가지 유형의 함수가 있습니다. 각 형식은 기능적으로 동일하지만 일반적으로 의미 체계 커널을 사용하는 애플리케이션 내에서 다르게 사용됩니다.
예를 들어 검색 함수를 사용하면 전략을 사용하여 성능을 향상시킬 수 있습니다(예: 요약을 위해 저렴한 중간 모델 캐싱 및 사용). 태스크 자동화 함수를 사용하는 반면, 휴먼 인 더 루프 승인 프로세스를 구현하여 태스크가 올바르게 완료되도록 할 수 있습니다.
다양한 유형의 플러그 인 함수에 대해 자세히 알아보려면 다음 문서를 참조하세요.
플러그인 사용 시작하기
의미 체계 커널 내에서 플러그 인을 사용하는 것은 항상 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가 플러그 인의 함수를 호출하게 할 수 있습니다. 다음은 조명을 켜기 위해 change_state
함수를 호출하기 전에 AI를 동축하여 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이 함수 매개 변수를 효과적으로 쉽게 일치시킬 수 있습니다.
Name 함수 매개 변수를 명확하게 지정합니다. 함수 매개 변수에 설명이 포함된 이름을 할당하여 용도를 명확히 합니다. 약어 또는 두문자어를 사용하여 매개 변수 이름을 줄이는 것을 피하세요. 이렇게 하면 LLM이 매개 변수를 추론하고 정확한 값을 제공하는 데 도움이 됩니다. 함수 이름과 마찬가지로 토큰 사용량을 최소화하기 위해 필요한 경우에만
DescriptionAttribute
사용합니다.
함수 수와 해당 책임 간의 적절한 균형 찾기
한편, 단일 책임이 있는 함수를 갖는 것은 여러 시나리오에서 함수를 간단하고 재사용 가능하게 유지하는 것이 좋습니다. 반면, 각 함수 호출은 네트워크 왕복 대기 시간 및 사용된 입력 및 출력 토큰 수 측면에서 오버헤드를 발생합니다. 입력 토큰은 함수 정의 및 호출 결과를 LLM에 보내는 데 사용되고 출력 토큰은 모델에서 함수 호출을 수신할 때 사용됩니다.
또는 여러 책임이 있는 단일 함수를 구현하여 사용된 토큰 수를 줄이고 네트워크 오버헤드를 줄일 수 있습니다. 하지만 다른 시나리오에서는 재사용 가능성이 감소합니다.
그러나 많은 책임을 단일 함수로 통합하면 함수 매개 변수 및 반환 형식의 수와 복잡성이 증가할 수 있습니다. 이러한 복잡성으로 인해 모델이 함수 매개 변수를 올바르게 일치시키는 데 어려움을 겪을 수 있으므로 누락된 매개 변수 또는 잘못된 형식의 값이 발생할 수 있습니다. 따라서 모델에서 함수 매개 변수를 정확하게 일치시킬 수 있도록 네트워크 오버헤드를 줄이기 위해 함수 수와 각 함수의 책임 수 사이의 적절한 균형을 맞추는 것이 중요합니다.
의미적 커널 함수 변환
의미 체계 커널 함수 변환에 대한 변환 기술을 블로그 게시물에서 설명한 대로 활용합니다.
함수 동작 변경: 함수의 기본 동작이 원하는 결과와 일치하지 않을 수 있고 원래 함수의 구현을 수정할 수 없는 시나리오가 있습니다. 이러한 경우 원래 함수를 래핑하고 그에 따라 동작을 수정하는 새 함수를 만들 수 있습니다.
컨텍스트 정보 제공: Functions에는 LLM이 유추할 수 없거나 유추해서는 안 되는 매개 변수가 필요할 수 있습니다. 예를 들어 함수가 현재 사용자를 대신하여 작동해야 하거나 인증 정보가 필요한 경우 이 컨텍스트는 일반적으로 호스트 애플리케이션에서 사용할 수 있지만 LLM에는 사용할 수 없습니다. 이러한 경우 LLM에서 제공하는 인수와 함께 호스팅 애플리케이션에서 필요한 컨텍스트 정보를 제공하면서 원래 함수를 호출하도록 함수를 변환할 수 있습니다.
매개 변수 목록, 형식 및 이름 변경: 원래 함수에 LLM이 해석하는 데 어려움을 겪고 있는 복잡한 서명이 있는 경우 LLM이 더 쉽게 이해할 수 있는 간단한 시그니처를 사용하여 함수를 하나로 변환할 수 있습니다. 여기에는 매개 변수 이름, 형식, 매개 변수 수 변경, 복잡한 매개 변수 평면화 또는 래핑 해제 등이 포함될 수 있습니다.
로컬 상태 사용률
기밀 정보가 포함된 문서, 기사, 또는 이메일과 같이 비교적 크거나 민감한 데이터 세트에서 작동하는 플러그인을 설계할 때는, LLM에 전송할 필요가 없는 원본 데이터 또는 중간 결과를 저장하기 위해 로컬 상태를 활용할 것을 고려하십시오. 이러한 시나리오에 대한 함수는 상태 ID를 수락하고 반환할 수 있으므로 실제 데이터를 LLM에 전달하는 대신 로컬로 데이터를 조회하고 액세스할 수 있으며, 다음 함수 호출에 대한 인수로 다시 수신할 수 있습니다.
데이터를 로컬로 저장하면 함수 호출 중에 불필요한 토큰 소비를 방지하면서 정보를 비공개로 안전하게 유지할 수 있습니다. 이 방법은 데이터 개인 정보를 향상시킬 뿐만 아니라 크고 중요한 데이터 세트를 처리하는 전반적인 효율성을 향상시킵니다.