Semantic Kernel의 플러그인(Plugin) 이해하기
Semantic Kernel의 플러그인(Plugin) 이해하기
플러그인은 Semantic Kernel에서 가장 핵심이 되는 구성 요소예요. ChatGPT의 플러그인이나 Microsoft 365의 Copilot 확장을 써 본 적이 있다면 이미 그 개념에는 익숙하실 거예요. 쉽게 말해, 여러분이 이미 갖고 있는 API를 AI가 쓸 수 있도록 한 덩어리로 묶어 주는 것이 플러그인입니다. 이렇게 하면 AI가 원래는 할 수 없었던 동작(예: 데이터 조회, 계산, 외부 시스템 호출)을 직접 수행하게 할 수 있어요.
그런데 뒷단에서는 무슨 일이 일어날까요? Semantic Kernel은 대부분의 최신 LLM이 기본으로 지원하는 함수 호출(function calling)을 활용해서, LLM이 플래닝(planning)을 수행하고 여러분의 API를 호출하게 만듭니다. 함수 호출 덕분에 LLM이 특정 함수를 요청(즉, 호출)할 수 있고, Semantic Kernel은 그 요청을 코드베이스의 알맞은 함수로 전달(marshal)한 뒤 결과를 다시 LLM에 돌려줘서 최종 응답을 만들게 해요.
모든 AI SDK가 플러그인과 같은 개념을 갖고 있는 건 아니에요(대부분은 함수나 도구만 있을 뿐이죠). 하지만 엔터프라이즈 시나리오에서 플러그인은 가치가 있어요. 플러그인은 기업 개발자가 이미 서비스와 API를 만드는 방식과 닮은 기능 묶음을 캡슐화하기 때문이에요. 또 플러그인은 의존성 주입(Dependency Injection)과도 잘 어울립니다. 플러그인의 생성자 안에서 플러그인 작업에 필요한 서비스(예: DB 커넥션, HTTP 클라이언트 등)를 주입할 수 있거든요. 플러그인 개념이 없는 다른 SDK에서는 이게 꽤 까다롭습니다.
플러그인의 구조
높은 수준에서 보면 플러그인은 AI 앱과 서비스에 노출할 수 있는 함수들의 묶음이에요. 플러그인 안의 함수들은 사용자 요청을 처리하기 위해 AI 애플리케이션이 오케스트레이션할 수 있죠. Semantic Kernel에서는 함수 호출로 이 함수들을 자동으로 호출할 수도 있습니다.
알아두면 좋아요. 다른 플랫폼에서는 함수를 "도구(tools)"나 "액션(actions)"이라고 부르는 경우가 많아요. Semantic Kernel은 보통 코드베이스의 네이티브 함수로 정의되기 때문에 "함수(functions)"라는 용어를 씁니다.
그런데 함수만 제공한다고 플러그인이 되는 건 아니에요. 함수 호출로 자동 오케스트레이션을 하려면, 플러그인은 자신이 어떻게 동작하는지를 의미적으로 설명하는 정보도 함께 제공해야 합니다. 함수의 입력, 출력, 부수 효과(side effects)까지 모두 AI가 이해할 수 있는 방식으로 설명돼야 해요. 그렇지 않으면 AI가 함수를 제대로 호출하지 못합니다.
예를 들어 WriterPlugin 샘플은 각 함수가 무엇을 하는지 설명하는 의미적 설명(semantic description)을 갖고 있어요. LLM은 이 설명을 보고 사용자 요청을 처리하기에 가장 알맞은 함수를 고를 수 있습니다. 이미지 설명이 잘 붙어 있다면, LLM이 사용자의 부탁을 충족시키기 위해 ShortPoem과 StoryGen 함수를 호출할 가능성이 높아요.
다양한 유형의 플러그인 가져오기
Semantic Kernel에 플러그인을 가져오는 방법은 크게 세 가지입니다.
- 네이티브 코드 — 여러분이 이미 갖고 있는 코드베이스에서 플러그인을 작성하고, 이미 쓰는 의존성·서비스를 활용하는 방법.
- OpenAPI 명세 — OpenAPI 스펙으로부터 플러그인을 가져오는 방법.
- MCP 서버 — MCP 서버로부터 플러그인을 가져오는 방법.
마지막 네이티브 코드 방식은 코드베이스에서 바로 작성하기 때문에 가장 쉽게 시작할 수 있고, 앞의 두 방식은 OpenAPI 스펙이나 MCP 서버에서 가져오는 것이라 여러 프로그래밍 언어·플랫폼에서 공유할 수 있어요.
팁. 처음 시작할 때는 네이티브 코드 플러그인을 권장해요. 애플리케이션이 성숙해지고 크로스 플랫폼 팀과 함께 일하게 되면, OpenAPI 스펙을 사용해 여러 언어·플랫폼에서 플러그인을 공유하는 걸 고려해 보세요. 그러면 커널 인스턴스에서 MCP 서버도 만들 수 있어서, 다른 애플리케이션이 여러분의 플러그인을 서비스 형태로 소비할 수 있게 됩니다.
플러그인 함수의 두 유형
한 플러그인 안에는 보통 두 종류의 함수가 있어요. RAG(Retrieval Augmented Generation)를 위해 데이터를 검색하는 함수와, 작업을 자동화하는 함수가 그것입니다. 기능적으로는 같지만 Semantic Kernel을 쓰는 애플리케이션 안에서는 보통 다르게 사용됩니다.
- 검색 함수에서는 성능을 높이기 위한 전략(예: 캐싱, 요약에 더 저렴한 중간 모델 사용)을 쓰고 싶을 거예요.
- 작업 자동화 함수에서는 작업이 올바르게 완료되는지 보장하기 위해 사람이 개입하는 승인(human-in-the-loop approval) 절차를 구현하고 싶을 겁니다.
자세한 내용은 데이터 검색 함수와 작업 자동화 함수 문서를 참고하세요.
플러그인 시작하기
Semantic Kernel에서 플러그인을 쓰는 과정은 항상 세 단계입니다.
아래에서 플러그인을 쓰는 방법을 높은 수준의 예시로 살펴볼게요.
1) 플러그인 정의하기
플러그인을 만드는 가장 쉬운 방법은 클래스를 정의하고 메서드에 KernelFunction 특성(attribute)을 붙이는 거예요. 이렇게 하면 Semantic Kernel이 "이 함수는 AI가 호출할 수 있고 프롬프트에서 참조할 수 있는 함수"임을 알게 됩니다. OpenAPI 명세에서 플러그인을 가져오는 방법도 가능해요.
아래는 조명(lights)의 상태를 가져오고 바꾸는 플러그인을 만드는 예시입니다.
팁. 대부분의 LLM이 Python 코드로 함수 호출을 학습했기 때문에, C#이나 Java SDK를 쓰더라도 함수 이름과 속성 이름은 스네이크 케이스(snake case)를 권장해요.
C#
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")]
public async Task<List<LightModel>> GetLightsAsync()
{
return lights
}
[KernelFunction("get_state")]
[Description("Gets the state of a particular 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")]
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; }
}
Python
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) -> List[LightModel]:
"""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"]
) -> Optional[LightModel]:
"""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
) -> Optional[LightModel]:
"""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
Java
// LightsPlugin.java 참고: semantic-kernel-samples-java 저장소의
// learnDocs/LightsApp/src/main/java/LightsPlugin.java 샘플
함수와 파라미터에 설명(description)을 붙인 걸 볼 수 있어요. 이건 AI가 함수가 무엇을 하고 어떻게 쓰는지 이해하는 데 중요합니다.
팁. AI가 함수를 제대로 호출하지 못한다면, 함수에 자세한 설명을 붙이는 걸 망설이지 마세요. 몇 샷(few-shot) 예시, 함수를 언제 써야 하고(그리고 쓰지 말아야 할 때)에 대한 권장, 필요한 파라미터를 어디서 얻을지에 대한 안내가 모두 도움이 됩니다.
2) 커널에 플러그인 추가하기
플러그인을 정의했다면, 플러그인의 새 인스턴스를 만들어 커널의 플러그인 컬렉션에 추가하면 됩니다. 아래 예시는 AddFromType 메서드로 클래스를 플러그인으로 추가하는 가장 쉬운 방법을 보여줘요. 다른 추가 방법은 네이티브 플러그인 추가 문서에서 확인하세요.
C#
var builder = new KernelBuilder();
builder.Plugins.AddFromType<LightsPlugin>("Lights")
Kernel kernel = builder.Build();
Python
kernel = Kernel()
kernel.add_plugin(
LightsPlugin(),
plugin_name="Lights",
)
Java
// LightsApp.java 참고: semantic-kernel-samples-java 저장소의
// learnDocs/LightsApp/src/main/java/LightsApp.java 샘플 (importplugin / buildkernel)
3) 플러그인의 함수 실행하기
마지막으로, 함수 호출을 사용해 AI가 플러그인의 함수를 호출하게 할 수 있습니다. 아래 예시는 change_state 함수로 조명을 켜기 전에, Lights 플러그인의 get_lights 함수를 호출하도록 AI를 유도하는 방법을 보여줘요.
C#
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);
Python
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, AzureChatPromptExecutionSettings
from semantic_kernel.connectors.ai import FunctionChoiceBehavior
from semantic_kernel.contents import ChatHistory
from semantic_kernel.functions import KernelArguments
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())
Java
// LightsAppNonInteractive.java 참고: semantic-kernel-samples-java 저장소의
// learnDocs/LightsApp/src/main/java/LightsAppNonInteractive.java 샘플 (invoke)
위 코드를 돌리면 아래처럼 대화가 진행됩니다. AI가 get_lights를 먼저 호출해 조명 목록을 가져오고, change_state로 첫 번째 조명을 켠 뒤 최종 응답을 내보내는 걸 볼 수 있어요.
| Role | Message |
|---|---|
| 🔵 User | Please turn on the lamp |
| 🔴 Assistant (function call) | Lights.get_lights() |
| 🟢 Tool | [{ "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 (function call) | Lights.change_state(1, { "isOn": true }) |
| 🟢 Tool | { "id": 1, "name": "Table Lamp", "isOn": true, "brightness": 100, "hex": "FF0000" } |
| 🔴 Assistant | The lamp is now on |
팁. 플러그인 함수를 직접 호출하는 것도 가능은 하지만 권장하지 않아요. 어떤 함수를 호출할지는 AI가 결정해야 하니까요. 어떤 함수가 호출되는지 명시적으로 제어해야 한다면 플러그인 대신 코드베이스의 일반 메서드를 쓰는 걸 고려해 보세요.
플러그인 작성 일반 권장사항
시나리오마다 요구사항이 다르고 플러그인 설계도 제각각이며 여러 LLM을 쓰기도 하다 보니, 플러그인 설계에 "만능" 가이드를 주긴 어려워요. 다만 플러그인이 AI 친화적이고 LLM이 쉽고 효율적으로 소비할 수 있게 하는 일반 권장사항 몇 가지를 소개할게요.
필요한 플러그인만 가져오기
여러분의 특정 시나리오에 필요한 함수를 담은 플러그인만 가져오세요. 이렇게 하면 소비되는 입력 토큰 수를 줄일 뿐 아니라, 시나리오에서 쓰지 않는 함수를 잘못 호출하는 오발동(함수 미스콜)도 줄어들어요. 전반적으로 함수 호출 정확도가 높아지고 거짓 양성이 줄어듭니다.
추가로 OpenAI는 단일 API 호출에서 20개 이하의 도구를 쓰기를 권장하며, 이상적으로는 10개 이하를 권장해요. OpenAI가 말하길, "단일 API 호출에서 20개 이상의 도구를 쓰지 않는 걸 권장합니다. 보통 10~20개 사이의 도구가 정의되면 모델이 올바른 도구를 고를 능력이 줄어드는 걸 볼 수 있습니다." 자세한 내용은 OpenAI Function Calling Guide를 참고하세요.
플러그인을 AI 친화적으로 만들기
LLM이 플러그인을 이해하고 활용하는 능력을 높이려면 다음 지침을 따르는 게 좋아요.
-
설명적이고 간결한 함수 이름을 쓰세요. 함수 이름이 목적을 분명히 드러내야 모델이 언제 어떤 함수를 고를지 이해할 수 있어요. 함수 이름이 애매하면 명확히 바꾸는 걸 고민하세요. 함수 이름을 줄이려고 약어나 두문자어를 쓰는 건 피하고, 토큰 소비를 최소화하기 위해
DescriptionAttribute는 꼭 필요할 때만 쓰세요. -
함수 파라미터를 최소화하세요. 함수 파라미터 수를 제한하고 가능하면 원시(primitive) 타입을 쓰세요. 이러면 토큰 소비가 줄고 함수 시그니처가 단순해져 LLM이 파라미터를 효과적으로 맞추기 쉬워집니다.
-
파라미터 이름을 분명하게 지으세요. 파라미터에 설명적인 이름을 붙여 목적을 밝히세요. 파라미터 이름을 줄이려고 약어를 쓰지 마세요. 함수 이름과 마찬가지로 토큰 소비를 줄이기 위해
DescriptionAttribute는 필요한 경우에만 쓰세요.
함수의 개수와 책임 사이의 균형 찾기
한편으로, 단일 책임을 가진 함수를 만드는 것은 좋은 관행이에요. 함수를 단순하고 여러 시나리오에 재사용 가능하게 유지해 주니까요. 다른 한편으로, 함수 호출 하나마다 네트워크 왕복 지연과 소비되는 입력·출력 토큰 수에서 오버헤드가 발생합니다. 입력 토큰은 함수 정의와 호출 결과를 LLM에 보내는 데 쓰이고, 출력 토큰은 모델로부터 함수 호출을 받을 때 소비되지요.
대안으로 하나의 함수에 여러 책임을 넣으면 소비 토큰 수를 줄이고 네트워크 오버헤드를 낮출 수 있지만, 다른 시나리오에서의 재사용성이 줄어드는 대가가 따라요. 게다가 여러 책임을 하나의 함수로 합치면 파라미터 수와 복잡성, 반환 타입이 늘어납니다. 이 복잡성이 커지면 모델이 함수 파라미터를 제대로 맞추지 못해 파라미터를 빠뜨리거나 잘못된 타입의 값을 보내는 상황이 생길 수 있어요. 따라서 네트워크 오버헤드를 줄이기 위한 함수 수와, 각 함수가 갖는 책임 수 사이의 적절한 균형을 찾는 게 필수적입니다.
Semantic Kernel 함수 변환하기
Transforming Semantic Kernel Functions 블로그에 설명된 변환 기법을 활용할 수 있어요.
-
함수 동작 바꾸기. 함수의 기본 동작이 원하는 결과와 맞지 않고 원본 함수 구현을 수정하기 어려운 시나리오가 있어요. 그럴 땐 원본 함수를 감싸고 동작을 바꾸는 새 함수를 만들 수 있습니다.
-
컨텍스트 정보 제공하기. 함수에는 LLM이 추론할 수 없거나 추론해서는 안 되는 파라미터가 필요할 수 있어요. 예를 들어 함수가 현재 사용자를 대신해 동작해야 하거나 인증 정보가 필요하다면, 이 컨텍스트는 보통 호스트 애플리케이션에만 있고 LLM에는 없어요. 그럴 땐 함수를 변환해서, LLM이 제공한 인자와 함께 호스팅 애플리케이션의 필요한 컨텍스트 정보를 채워 원본 함수를 호출하게 할 수 있습니다.
-
파라미터 목록·타입·이름 바꾸기. 원본 함수가 LLM이 해석하기 어려운 복잡한 시그니처를 갖고 있다면, LLM이 더 쉽게 이해하는 단순한 시그니처를 가진 함수로 변환할 수 있어요. 여기엔 파라미터 이름·타입·개수를 바꾸고 복잡한 파라미터를 평탄화하거나 다시 묶는 등의 조정이 포함됩니다.
로컬 상태 활용하기
문서, 기사, 민감 정보를 담은 이메일처럼 비교적 크거나 기밀인 데이터셋에서 동작하는 플러그인을 설계할 때는, LLM에 보낼 필요가 없는 원본 데이터나 중간 결과를 로컬 상태로 저장하는 걸 고려하세요. 그런 시나리오의 함수는 상태 id를 받고 반환할 수 있어요. 그러면 실제 데이터를 LLM에 넘겼다가 다음 함수 호출의 인자로 다시 받는 대신, 데이터를 로컬에서 조회·접근할 수 있습니다.
데이터를 로컬에 저장하면 정보를 비공개·안전하게 유지하면서 함수 호출 중 불필요한 토큰 소비를 피할 수 있어요. 이는 데이터 프라이버시를 높일 뿐 아니라 크거나 민감한 데이터셋 처리의 전반적 효율도 좋게 만듭니다.
AI 모델에 함수 반환 타입 스키마 제공하기
함수 반환 타입 정보를 함수 설명에 제공하기 섹션에 설명된 기법 중 하나를 사용해 AI 모델에 함수의 반환 타입 스키마를 제공하세요. 잘 정의된 반환 타입 스키마를 쓰면 AI 모델이 의도된 속성을 정확히 식별할 수 있어요. 스키마가 없을 때 모델이 불완전하거나 모호한 정보로 추정하면서 생길 수 있는 부정확성을 없애 주니까요. 결과적으로 함수 호출의 정확도가 높아지고 더 신뢰할 수 있고 정밀한 결과로 이어집니다.