dotnet-semantic-kernel について
Microsoft Semantic Kernel を使い、.NET アプリケーションに AI と LLM のオーケストレーションを組み込むスキルです。プラグイン、プロンプト、ベクトルストア検索、エージェント、クラウド・ローカル AI サービスを扱います。
主な機能
- 01Kernel と AI サービスの構成
- 02.NET メソッドを呼び出し可能なプラグインとして公開
- 03Handlebars・Liquid プロンプトの作成
- 04ベクトルストアによる RAG
- 05エージェントとグループチャットの構築
- 06チャット応答のストリーミング
活用シーン
- 01アプリケーションのデータに基づくモデル応答
- 02ドキュメント検索と質問応答の構築
- 03タスクに応じた専門エージェントの連携
- 04チャットアプリでの応答ストリーミング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-semantic-kernelインストール後、Claude Code で/dotnet-semantic-kernelを入力して読み込まれたか確認します。
詳細
dotnet-semantic-kernel
.NET アプリケーションで AI と LLM のオーケストレーションを行う Microsoft Semantic Kernel について説明します。カーネルのセットアップと構成、プラグイン/関数呼び出し、Handlebars および Liquid 構文を使ったプロンプトテンプレート、メモリとベクトルストアの統合、プランナー、エージェントフレームワーク、Azure OpenAI、OpenAI、ローカルモデルとの統合を扱います。
対象外: 一般的な async/await パターンとキャンセレーショントークンの伝播については、[skill:dotnet-csharp-async-patterns] を参照してください。DI コンテナーの仕組みとサービスのライフタイム管理については、[skill:dotnet-csharp-dependency-injection] を参照してください。HTTP クライアントの回復性と再試行ポリシーについては、[skill:dotnet-resilience] を参照してください。構成のバインド(オプションパターン、シークレット)については、[skill:dotnet-csharp-configuration] を参照してください。
相互参照:チャット完了で使用する非同期ストリーミングパターンについては [skill:dotnet-csharp-async-patterns]、ASP.NET Core でのカーネルサービスの登録については [skill:dotnet-csharp-dependency-injection]、AI サービス呼び出しの再試行ポリシーについては [skill:dotnet-resilience]、API キーとモデル構成の管理については [skill:dotnet-csharp-configuration] を参照してください。
カーネルのセットアップ
Kernel は Semantic Kernel の中心となるオブジェクトです。AI サービスへの接続、プラグイン、関数の呼び出しを管理します。
パッケージの概要
| パッケージ | 用途 |
|---|---|
Microsoft.SemanticKernel | カーネルコア、関数呼び出し、プロンプトテンプレート |
Microsoft.SemanticKernel.Connectors.AzureOpenAI | Azure OpenAI のチャット/埋め込み/画像サービス |
Microsoft.SemanticKernel.Connectors.OpenAI | OpenAI のチャット/埋め込み/画像サービス |
Microsoft.SemanticKernel.Connectors.Ollama | Ollama のローカルモデル統合 |
Microsoft.SemanticKernel.Plugins.Core | 組み込みプラグイン(時刻、数学、テキスト) |
Microsoft.SemanticKernel.Agents.Core | エージェントフレームワーク(チャットエージェント、グループチャット) |
Microsoft.Extensions.VectorData.Abstractions | ベクトルストアの抽象化レイヤー |
Microsoft.SemanticKernel.Connectors.Qdrant | Qdrant ベクトルストアコネクター |
Microsoft.SemanticKernel.Connectors.AzureAISearch | Azure AI Search ベクトルストアコネクター |
基本的なカーネル構成
using Microsoft.SemanticKernel;
var builder = Kernel.CreateBuilder();
// Azure OpenAI
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!,
apiKey: Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY")!);
var kernel = builder.Build();
ASP.NET Core との DI 統合
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKernel();
builder.Services.AddAzureOpenAIChatCompletion(
deploymentName: builder.Configuration["AI:DeploymentName"]!,
endpoint: builder.Configuration["AI:Endpoint"]!,
apiKey: builder.Configuration["AI:ApiKey"]!);
// Register plugins
builder.Services.AddSingleton<OrderPlugin>();
builder.Services.AddSingleton(sp =>
{
var kernel = sp.GetRequiredService<Kernel>();
kernel.Plugins.AddFromObject(sp.GetRequiredService<OrderPlugin>());
return kernel;
});
複数の AI サービス
複数の AI サービスを登録し、サービス ID で選択します。
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: endpoint,
apiKey: apiKey,
serviceId: "gpt4o");
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o-mini",
endpoint: endpoint,
apiKey: apiKey,
serviceId: "gpt4o-mini");
var kernel = builder.Build();
// Select service at invocation time
var settings = new PromptExecutionSettings { ServiceId = "gpt4o-mini" };
var result = await kernel.InvokePromptAsync("Summarize: {{$input}}", new(settings)
{
["input"] = longDocument
});
Ollama を使ったローカルモデル
#pragma warning disable SKEXP0070 // Ollama connector is experimental
var builder = Kernel.CreateBuilder();
builder.AddOllamaChatCompletion(
modelId: "llama3.2",
endpoint: new Uri("http://localhost:11434"));
var kernel = builder.Build();
プラグインと関数呼び出し
プラグインは .NET のメソッドを、AI モデルが呼び出せる関数として公開します。これは、LLM の応答を実際のデータやアクションに基づかせるための主要な仕組みです。
プラグインの定義
using Microsoft.SemanticKernel;
using System.ComponentModel;
public sealed class OrderPlugin
{
private readonly IOrderRepository _repository;
public OrderPlugin(IOrderRepository repository) => _repository = repository;
[KernelFunction("get_order")]
[Description("Retrieves an order by its ID")]
public async Task<OrderSummary?> GetOrderAsync(
[Description("The unique order identifier")] string orderId,
CancellationToken ct = default)
{
var order = await _repository.GetByIdAsync(orderId, ct);
return order is null ? null : new OrderSummary(order);
}
[KernelFunction("list_recent_orders")]
[Description("Lists the most recent orders for a customer")]
public async Task<IReadOnlyList<OrderSummary>> ListRecentOrdersAsync(
[Description("The customer ID")] string customerId,
[Description("Maximum number of orders to return")] int limit = 10,
CancellationToken ct = default)
{
var orders = await _repository.GetRecentAsync(customerId, limit, ct);
return orders.Select(o => new OrderSummary(o)).ToList();
}
}
プラグインの登録
var kernel = builder.Build();
// From an object instance (DI-friendly)
kernel.Plugins.AddFromObject(new OrderPlugin(orderRepo), "Orders");
// From a type (kernel creates the instance)
kernel.Plugins.AddFromType<TimePlugin>("Time");
// From functions directly
kernel.Plugins.AddFromFunctions("Math",
[
KernelFunctionFactory.CreateFromMethod(
([Description("First number")] double a, [Description("Second number")] double b) => a + b,
"Add",
"Adds two numbers")
]);
自動関数呼び出し
チャット中にモデルが自動で関数を呼び出せるようにします。
var settings = new AzureOpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("What's the status of order ORD-12345?");
var result = await kernel.GetRequiredService<IChatCompletionService>()
.GetChatMessageContentAsync(chatHistory, settings, kernel);
// The model calls get_order("ORD-12345") automatically and responds with the result
Console.WriteLine(result.Content);
関数フィルター
ログ記録、認可、変更を目的として関数呼び出しをインターセプトします。
public sealed class AuthorizationFilter : IFunctionInvocationFilter
{
public async Task OnFunctionInvocationAsync(
FunctionInvocationContext context,
Func<FunctionInvocationContext, Task> next)
{
// Check authorization before function execution
if (context.Function.Name == "get_order")
{
var orderId = context.Arguments["orderId"]?.ToString();
// Validate access...
}
await next(context);
// Post-execution: log or modify result
}
}
// Register the filter
builder.Services.AddSingleton<IFunctionInvocationFilter, AuthorizationFilter>();
プロンプトテンプレート
プロンプトテンプレートでは、構造化されたプロンプト内で変数を置換し、関数を呼び出せます。
インラインプロンプト
var result = await kernel.InvokePromptAsync(
"Summarize the following text in {{$style}} style:\n\n{{$input}}",
new KernelArguments
{
["input"] = articleText,
["style"] = "concise bullet points"
});
Handlebars テンプレート
Handlebars テンプレートは、条件分岐、ループ、関数呼び出しに対応しています。
var templateString = """
<message role="system">
You are a helpful customer service agent.
{{#if isVip}}You are speaking with a VIP customer. Be extra attentive.{{/if}}
</message>
<message role="user">
Customer: {{customerName}}
Query: {{query}}
Recent orders:
{{#each orders}}
- Order {{this.Id}}: {{this.Status}} ({{this.Date}})
{{/each}}
</message>
""";
var factory = new HandlebarsPromptTemplateFactory();
var template = factory.Create(new PromptTemplateConfig(templateString)
{
TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat
});
var result = await template.RenderAsync(kernel, new KernelArguments
{
["customerName"] = "Alice",
["query"] = "Where is my order?",
["isVip"] = true,
["orders"] = recentOrders
});
YAML プロンプト構成
関心事を分離するため、プロンプトを YAML ファイルとして定義します。
# prompts/summarize.yaml
name: Summarize
description: Summarizes text to a specified length
template_format: handlebars
template: |
<message role="system">
Summarize the following text in approximately {{maxWords}} words.
Focus on key facts and actionable items.
</message>
<message role="user">{{input}}</message>
input_variables:
- name: input
description: The text to summarize
is_required: true
- name: maxWords
description: Target word count
default: "100"
execution_settings:
default:
temperature: 0.3
max_tokens: 500
var yamlContent = File.ReadAllText("prompts/summarize.yaml");
var function = kernel.CreateFunctionFromPromptYaml(yamlContent);
var result = await kernel.InvokeAsync(function, new KernelArguments
{
["input"] = longText,
["maxWords"] = "50"
});
メモリとベクトルストア
Semantic Kernel はベクトルストレージの抽象化を提供し、検索拡張生成(RAG)パターンを実現します。
ベクトルストアの抽象化
using Microsoft.Extensions.VectorData;
public sealed class DocumentRecord
{
[VectorStoreRecordKey]
public string Id { get; set; } = string.Empty;
[VectorStoreRecordData(IsFilterable = true)]
public string Source { get; set; } = string.Empty;
[VectorStoreRecordData(IsFullTextSearchable = true)]
public string Content { get; set; } = string.Empty;
[VectorStoreRecordVector(Dimensions: 1536)]
public ReadOnlyMemory<float> Embedding { get; set; }
}
ベクトルストアの登録
using Microsoft.SemanticKernel.Connectors.Qdrant;
var builder = Kernel.CreateBuilder();
// Register embedding generation
builder.AddAzureOpenAITextEmbeddingGeneration(
deploymentName: "text-embedding-3-small",
endpoint: endpoint,
apiKey: apiKey);
// Register vector store
builder.Services.AddQdrantVectorStore("localhost", 6334);
RAG パターン
public sealed class RagService
{
private readonly IVectorStoreRecordCollection<string, DocumentRecord> _collection;
private readonly ITextEmbeddingGenerationService _embeddingService;
private readonly IChatCompletionService _chatService;
public RagService(
IVectorStore vectorStore,
ITextEmbeddingGenerationService embeddingService,
IChatCompletionService chatService)
{
_collection = vectorStore.GetCollection<string, DocumentRecord>("documents");
_embeddingService = embeddingService;
_chatService = chatService;
}
public async Task<string> AskAsync(string question, CancellationToken ct = default)
{
// 1. Generate embedding for the question
var questionEmbedding = await _embeddingService
.GenerateEmbeddingAsync(question, cancellationToken: ct);
// 2. Search for relevant documents
var searchResults = _collection.VectorizedSearchAsync(
questionEmbedding,
new VectorSearchOptions { Top = 5 },
ct);
// 3. Build context from search results
var contextBuilder = new StringBuilder();
await foreach (var result in searchResults)
{
contextBuilder.AppendLine(result.Record.Content);
contextBuilder.AppendLine("---");
}
// 4. Generate answer with context
var chatHistory = new ChatHistory();
chatHistory.AddSystemMessage(
$"Answer based on the following context:\n\n{contextBuilder}");
chatHistory.AddUserMessage(question);
var response = await _chatService
.GetChatMessageContentAsync(chatHistory, cancellationToken: ct);
return response.Content ?? string.Empty;
}
}
ドキュメントの取り込み
public async Task IngestAsync(
string documentId,
string content,
string source,
CancellationToken ct = default)
{
await _collection.CreateCollectionIfNotExistsAsync(ct);
var embedding = await _embeddingService
.GenerateEmbeddingAsync(content, cancellationToken: ct);
await _collection.UpsertAsync(new DocumentRecord
{
Id = documentId,
Content = content,
Source = source,
Embedding = embedding
}, cancellationToken: ct);
}
エージェントフレームワーク
Semantic Kernel のエージェントフレームワークを使うと、特化したエージェントがタスクに協力して取り組むマルチエージェントシステムを構築できます。
チャット完了エージェント
#pragma warning disable SKEXP0110 // Agents framework is experimental
using Microsoft.SemanticKernel.Agents;
var agent = new ChatCompletionAgent
{
Name = "OrderAssistant",
Instructions = """
You are an order management assistant. Help customers check order status,
process returns, and answer questions about their orders.
Always verify the customer's identity before sharing order details.
""",
Kernel = kernel,
Arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
})
};
// Invoke via a thread (required -- agents do not accept bare strings)
var thread = new ChatHistoryAgentThread();
await foreach (var message in agent.InvokeAsync(
"What's the status of my order ORD-12345?", thread))
{
Console.WriteLine(message.Content);
}
エージェントグループチャット
終了条件を設定したグループチャットで、複数のエージェントが協力できます。
var analyst = new ChatCompletionAgent
{
Name = "DataAnalyst",
Instructions = "You analyze data and provide insights. Present findings clearly.",
Kernel = kernel
};
var writer = new ChatCompletionAgent
{
Name = "ReportWriter",
Instructions = "You take analytical findings and write clear, actionable reports.",
Kernel = kernel
};
var chat = new AgentGroupChat(analyst, writer)
{
ExecutionSettings = new AgentGroupChatSettings
{
TerminationStrategy = new ApprovalTerminationStrategy
{
MaximumIterations = 6
}
}
};
chat.AddChatMessage(
new ChatMessageContent(AuthorRole.User, "Analyze Q4 sales trends and write a summary report."));
await foreach (var message in chat.InvokeAsync())
{
Console.WriteLine($"[{message.AuthorName}]: {message.Content}");
}
OpenAI アシスタントエージェント
組み込みツール(コードインタープリター、ファイル検索)を使ったステートフルな会話の場合:
#pragma warning disable SKEXP0110
// Create the assistant via the builder pattern
OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync(
kernel,
new OpenAIAssistantDefinition("gpt-4o")
{
Name = "DataProcessor",
Instructions = "You process CSV data and generate insights.",
EnableCodeInterpreter = true
});
try
{
// Assistant agents use threads for stateful conversations
var thread = await agent.CreateThreadAsync();
await foreach (var message in agent.InvokeAsync(
"Analyze the attached sales data.", thread))
{
Console.WriteLine(message.Content);
}
}
finally
{
await agent.DeleteAsync();
}
注:エージェントフレームワークは実験的です(SKEXP0110)。Semantic Kernel のリリース間で API は頻繁に変更されます。導入時には、最新のサンプルでメソッドのシグネチャを確認してください。
ストリーミング応答
チャットアプリケーションでは、応答をトークンごとにストリーミングします:
var chatService = kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory("You are a helpful assistant.");
chatHistory.AddUserMessage(userInput);
var settings = new AzureOpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(
chatHistory, settings, kernel))
{
Console.Write(chunk.Content);
}
重要な原則
- プロンプトへの詰め込みより関数呼び出しを使う -- すべてをプロンプトに注入するのではなく、モデルがプラグインを呼び出してリアルタイムデータを取得できるようにします
- プラグインの役割を絞る -- 各プラグインは単一のドメインを表すようにします。関数とパラメーターに
[Description]属性を使い、モデルが呼び出すタイミングと方法を把握できるようにします - 本番環境では YAML プロンプトを使う -- プロンプトの内容をコードから分離し、反復作業や開発者以外による編集を容易にします
- コードに API キーを保存しない -- 環境変数、Azure Key Vault、または .NET シークレットマネージャーを使用します([skill:dotnet-csharp-configuration] を参照)
- ベクトルストアの抽象化を優先する --
IVectorStoreを使って実装し、Qdrant、Azure AI Search、その他のプロバイダーを切り替えられるようにします - 実験的な API は明示的に扱う --
SKEXP*の警告はグローバルにではなく呼び出しごとに抑制し、API が安定版になったことに気づけるようにします
エージェントで注意すべき点
- Kernel ビルダー呼び出しで API キーやエンドポイントをハードコードしない --
builder.Configurationまたは環境変数を使用します。ハードコードされたシークレットはソース管理に漏洩し、環境ごとの構成を妨げます。 - すべての
SKEXP*警告をグローバルに抑制しない -- 実験的な API は頻繁に変更されます。使用箇所ごとに抑制し(#pragma warning disable SKEXP0110)、新たな実験的 API の使用箇所がコンパイラーによって検出されるようにします。 - ASP.NET Core でリクエストごとに新しい
Kernelインスタンスを作成しない -- DI にカーネルをシングルトンとして登録し(スレッドセーフです)、リクエストごとの状態が必要な場合はkernel.Clone()で複製します。 - プラグイン関数で
CancellationTokenを無視しない -- AI 関数の呼び出しは、ユーザーやタイムアウトポリシーによってキャンセルされることがあります。プラグインのメソッドシグネチャを通じて、常にCancellationTokenを引き継ぎます。 - プラグイン関数から大きなオブジェクトを返さない -- モデルはシリアル化された結果をコンテキストとして受け取ります。トークン制限を超えないよう、エンティティグラフ全体ではなく、要約 DTO を返します。
serviceIdを指定せずにAddAzureOpenAIChatCompletionとAddOpenAIChatCompletionを併用しない -- サービス ID を指定しない場合、最後の登録が有効になります。複数の AI サービスを登録するときは、明示的なserviceIdを使用します。
前提条件
Microsoft.SemanticKernelNuGet パッケージ(安定版 1.x)- AI サービスのエンドポイント(Azure OpenAI、OpenAI API キー、またはローカルモデル用の Ollama)
- ベクトルストアを使う場合:選択したプロバイダーの実行中のインスタンス(Qdrant、Azure AI Search など)