C#による生成AIアプリ開発で悩むエンジニア必見!Semantic Kernelを使いこなすための実装ノウハウ

C#とSemantic Kernelによる生成AIアプリ開発の実装ノウハウを象徴するコードとアーキテクチャのイメージ バックエンド

生成AIをC#アプリケーションに統合する際、多くのエンジニアがSDKの選定やプラグインの設計、プロンプト管理に頭を悩ませていることだろう。
特にSemantic Kernelは高い拡張性を持つ一方で、概念の多さと実装パターンの多様性が初学者を圧倒しがちである。
いくつかのチュートリアルを追試しただけでは、実際の業務アプリケーションへどう昇華させればよいか見えてこない、という声も少なくない。

本稿では、コンピューターサイエンスの基礎を活かした実装の視点から、Semantic Kernelの中核となる概念を整理し、C#による具体的な実装ノウハウを体系的に解説する。
単なるAPIラッパーとしての利用に留まらず、プラグイン、プランナー、メモリ、フィルタといった主要コンポーネントをいかに設計・運用すべきかという観点から、実務で即座に活用できる知見を提示する。

以下のような問いを抱える開発者にとって、明確な指針となるはずだ。

  • Semantic Kernelの依存関係注入とライフサイクル管理を正しく行う方法
  • プロンプトテンプレートとネイティブ関数を効率的に組み合わせる設計指針
  • 運用環境におけるログ出力、テレメトリ、エラーハンドリングの実装パターン

生成AIアプリケーションの品質は、LLMそのものの性能だけでなく、周辺のソフトウェアアーキテクチャの設計精度によって大きく左右される。
Semantic Kernelを真に使いこなし、堅牢なC#アプリケーションを構築するための実践的な第一歩として本稿を活用していただきたい。

  1. Semantic Kernelとは何か?C#での生成AI開発が直面する課題と解決策
  2. Semantic Kernelの中核概念を理解する:Kernel、Plugin、Plannerの関係性
    1. Kernelが担う責務とアーキテクチャ上の位置づけ
    2. Pluginが実現するプロンプトとコードの統合
    3. Plannerによる動的実行フローの制御
  3. 開発環境構築:NuGetパッケージ選定と依存性注入での初期設定
    1. 必要なNuGetパッケージの選定とバージョン管理
    2. KernelBuilderとIServiceCollectionの連携設定
    3. 構成管理:APIキーとエンドポイントの安全な取り扱い
  4. Kernelライフサイクルの設計指針:Singleton・Scoped・Transientの使い分け
    1. サービス登録パターンとスレッド安全性の検討
    2. ASP.NET Coreでの実践的なDI設定例
  5. プロンプトテンプレートとネイティブ関数:プラグイン設計の実装パターン
    1. PromptTemplateの記法とパラメータ化
    2. Native Functionの実装とKernelFunction属性
    3. 複数プラグインの合成と優先度制御
  6. Plannerの実装戦略:HandlebarsPlannerとFunction Callingによる動的実行
    1. HandlebarsPlannerのテンプレートエンジン活用
    2. Function Callingステップ実行の実装
    3. プランナー出力の検証とサンドボックス化
  7. ベクトルDB連携:Semantic Memoryを活用したRAGアーキテクチャの構築
    1. IMemoryConnectorの実装とベクトルストア選定
    2. テキストのベクトル化と保存フロー
    3. 検索結果のプロンプトへの動的注入
  8. 運用品質を担保する実装:フィルタ、テレメトリ、リトライ、エラーハンドリング
    1. FunctionFilterとPromptFilterの実装
    2. OpenTelemetryと構造化ログの統合
    3. Pollyによる回復性の高いHTTP戦略
  9. まとめ:Semantic Kernelを使いこなすC#エンジニアの次の一歩

Semantic Kernelとは何か?C#での生成AI開発が直面する課題と解決策

C#とSemantic Kernelを使った生成AIアプリ開発の課題解決を示すコードエディタの画面

生成AIをビジネスアプリケーションに組み込む際、多くの開発者が最初に直面するのは、単純なAPI呼び出しを超えた設計上の複雑性である。
特にC#を用いたエンタープライズ開発の文脈では、LLMとの対話をいかに型安全かつテスト可能な形で実装するか、プロンプトをコードから分離して管理するか、そして長期的な運用に耐えるアーキテクチャを構築するかが重要な課題となる。
こうした問いに対し、Microsoftが提供するSemantic Kernelは、強力な答えを提示するSDKである。

Semantic Kernelは、大規模言語モデルとの対話、プロンプト管理、プラグイン機構、自動計画生成を統合的に扱う軽量なSDKである。
その本質は、生成AIの能力を単なる文字列生成に留めず、既存のコード資産や外部APIと意味的に接続するためのオーケストレーション層を提供することにある。
コンピューターサイエンスの観点から言えば、これは従来のコンポーネントベース開発における「関心事の分離」の原則を、LLM時代に拡張したアーキテクチャパターンと言える。
なぜ単純なHTTPクライアントでは不足するのか。
それは、LLMとの対話が単なるリクエスト・レスポンスのやり取りではなく、文脈管理、関数呼び出し、メモリ参照といった複数の横断的関心事を含むためである。

C#エンジニアが生成AI開発で直面する代表的な課題は、主に以下の3つに集約される。

  • プロンプトの散在と管理コスト:ハードコードされたプロンプトはバージョン管理が困難であり、チーム開発における一貫性を損なう要因となる
  • 外部機能連携の複雑化:LLMが自律的にコードを呼び出す仕組みを安全かつ構造化された形で実装する際の設計負荷
  • テストと運用の困難さ:非決定論的なLLMの応答を、堅牢なエンタープライズアプリケーションに統合する際の観測性とエラーハンドリングの不足

これらの課題に対し、Semantic Kernelは明確な解決策を提供する。
以下に課題と解決策の対応を示す。

| 課題 | Semantic Kernelによる解決策 | C#開発における利点 |
| プロンプト管理の困難さ | Prompt Templateとファイル分離による宣言的管理 | 型安全なパラメータバインディング |
| 外部機能連携の複雑さ | Plugin機構による関数の抽象化と自動ディスカバリー | DIコンテナとの自然な統合 |
| 運用と観測性の不足 | FilterとTelemetryによる実行フローの可視化 | 既存の.NETログ基盤との親和性 |

特筆すべきは、Semantic Kernelが依存性注入を第一級の概念として扱う点である。
Kernelインスタンス、Plugin、各種コネクタをIServiceCollectionに登録する設計は、ASP.NET Core開発者にとって直感的なものである。
この設計思想は、生成AIの能力を従来のレイヤードアーキテクチャやクリーンアーキテクチャに組み込む際の摩擦を大幅に低減する。
インターフェースベースの設計により、テスト時のモック置換も容易であり、これは継続的インテグレーションの文脈において極めて重要な特性である。

さらに、Semantic Kernelは特定のモデルベンダーに依存しない抽象化を提供する。
OpenAI、Azure OpenAI、そしてローカルモデルなどを同一のインターフェースで扱えることは、長期的な技術選定の柔な技術選定の柔軟性を担保する上で極めて重要である。
エンタープライズ開発において、ベンダーロックインのリスクを最小化することは、コンピューターサイエンスの実践的知見として当然の要求である。
クラウドネイティブな環境においても、抽象化されたインターフェースは移行コストを劇的に低下させる。

生成AIアプリケーションの品質は、LLMの性能だけでなく、それを包み込むソフトウェアアーキテクチャの成熟度で決まる。
Semantic Kernelは単なるAPIクライアントではなく、生成AIを堅牢なシステムに統合するための設計言語を提供する。
C#エンジニアにとって、このSDKを理解することは、型安全性と設計原則を妥協することなく、次世代のアプリケーションを構築するための必須のステップである。
あなたのアプリケーションは、単なるプロンプトの羅列ではなく、保守可能なソフトウェアとしての価値を持つべきではないか。

Semantic Kernelの中核概念を理解する:Kernel、Plugin、Plannerの関係性

Semantic KernelのKernelとPluginとPlannerの関係性を表すアーキテクチャ図

Semantic Kernelを実装レベルで使いこなすためには、Kernel、Plugin、Plannerという3つの中核概念がどのように連携するかを正しく理解することが不可欠である。
これらは独立した機能ではなく、生成AIを統合したアプリケーションにおける責務分担を明確に定義した設計パターンとして位置づけられる。
コンピューターサイエンスの文脈で言えば、Kernelはランタイム環境、Pluginはリンクされたライブラリ群、Plannerは動的な実行スケジューラに相当する概念である。
以下に3つの概念の関係性を整理する。

| 概念 | 主な責務 | アーキテクチャ上の役割 | 開発者が意識すべき設計ポイント |
| Kernel | 依存関係管理と実行コンテキストの提供 | ファサード兼サービスロケーター | ライフサイクルとスレッド安全性の確保 |
| Plugin | 関数の抽象化とプロンプト・コードの統合 | モジュール単位の機能カプセル化 | インターフェースの一貫性とスキーマ定義の精度 |
| Planner | 目標に対する動的実行計画の生成 | 実行時の戦略パターン | ガードレール設計と計画検証の徹底 |

Kernelが担う責務とアーキテクチャ上の位置づけ

KernelはSemantic Kernelの中核となるオーケストレーターであり、LLMクライアント、Pluginのレジストリ、各種設定、そして実行コンテキストを一元的に管理する責務を持つ。
アーキテクチャ上の位置づけとして、Kernelはファサードパターンとサービスロケーターの性質を併せ持つ。
開発者はKernelを通じてAIサービスにアクセスし、Pluginを登録し、Promptの実行を依頼する。
この設計の本質は、複雑な依存関係を単一のエントリポイントの背後に隠蔽し、一貫した抽象を提供することにある。

Kernelの重要な特性はステートフルな実行コンテキストの管理である。
会話履歴、プラグインのディスカバリー、フィルタチェーン、そしてテレメトリの伝播は、Kernelインスタンスを通じて一貫して制御される。
ASP.NET Coreにおいては、KernelをScopedサービスとして登録することで、HTTPリクエスト単位での文脈分離が実現できる。
これはマルチテナント環境やセッション固有の会話管理において極めて重要な設計上の利点となる。
Kernelを単なるユーティリティクラスと誤認すると、文脈の漏洩や依存関係の制御不全を招く。
正しくは、生成AI処理の境界コンテキストとしてKernelを扱うべきである。

Pluginが実現するプロンプトとコードの統合

PluginはSemantic Kernelにおける機能の単位であり、Prompt Templateとネイティブ関数を同一の抽象下に統合する仕組みである。
従来のソフトウェア開発において、設定ファイルに記述されたSQL文とコード内のビジネスロジックは分離されていた。
Pluginはこの発想を生成AIの世界に拡張し、自然言語による指示と型安全なコードを対等な「関数」として扱う。
これにより、LLMは自己の知識だけでなく、開発者が提供する外部ツール群を自律的に呼び出すことが可能となる。

Pluginの構成要素は主に以下の2種類である。

  • Prompt functions:外部ファイルまたはインラインで定義されたプロンプトテンプレートであり、パラメータ化された自然言語処理を担当する
  • Native functions:C#で実装された通常のメソッドであり、KernelFunction属性によってSemantic Kernelの実行エンジンに公開される

この統一された関数モデルにより、Plannerは自然言語処理とコード実行の区別を意識することなく、実行計画を構築できる。
例えば、日付取得のネイティブ関数と要約生成のプロンプト関数を同一Plugin内に配置することで、LLMは「まず今日の日付を取得し、その日付に基づいてレポートを要約する」という連続的な処理を一貫して扱える。
これはモノリシックなプロンプトでは実現困難な、モジュール化された推論の基盤となる。

Plannerによる動的実行フローの制御

Plannerは、ユーザーの自然言語から意図を解釈し、登録されたPlugin群の中から必要な関数を選び出して実行順序を決定する、Semantic Kernelの動的计划生成コンポーネントである。
従来のプログラミングにおいて計算の手順はコンパイル時に固定されるが、Plannerは実行時に目標に対する最適な関数呼び出し列を生成する。
この特性は、明示的に分岐を記述しきれない複雑な対話フローや、要件変化に対して柔軟に対応すべきエージェント型アプリケーションにおいて特に有効である。

Plannerの動作は、関数のJSONスキーマ定義に大きく依存する。
各Plugin関数の入力・出力、説明文が整備されていることで、LLMは「どのツールがどのような状況で使用されるべきか」を論理的に推論できる。
HandlebarsPlannerの場合、生成される計画はテンプレートエンジンとして解釈可能な中間表現を持ち、条件分岐やループといった制御構造を含むことができる。
これは単なる関数呼び出しのリストではなく、サブゴールを含む階層的な実行計画を表現する。

ただし、Plannerの非決定論的な性質は運用上の課題も孕む。
生成された計画が無限ループを含んだり、不適切な関数を連続して呼び出したりするリスクがあるため、フィルタによる実行のインターセプトや、Planner出力の検証は必須である。
堅牢なアプリケーションを目指すなら、Plannerを無制限に委任するのではなく、許可リストや実行ステップ数の上限といったガードレールを設ける設計が求められる。

開発環境構築:NuGetパッケージ選定と依存性注入での初期設定

NuGetパッケージ選定とDI設定を行うVisual Studioの画面

Semantic Kernelを用いたC#アプリケーションの開発において、最初の設計判断はNuGetパッケージの選定と依存性注入コンテナへの正しい登録に集約される。
ここでの判断ミスは、後の機能拡張時に破滅的なリファクタリングを招く。
コンピューターサイエンスの観点から言えば、モジュール間の依存関係はコンパイル時に可能な限り静的に検証可能であるべきであり、実行時の構成エラーを最小化する設計が求められる。
以下では、実務で即座に活用できる初期設定の実装ノウハウを整理する。

必要なNuGetパッケージの選定とバージョン管理

Semantic Kernelは機能が複数のパッケージに分割されており、必要な機能に応じて正確なパッケージを選定することが重要である。
コア機能だけでなく、コネクタやプランナー、プロンプトテンプレートエンジンも個別のパッケージとして提供されている。

| パッケージ名 | 主な責務 | 導入タイミング |
| Microsoft.SemanticKernel | Kernelの中核機能とPlugin基盤 | すべてのプロジェクトで必須 |
| Microsoft.SemanticKernel.Connectors.OpenAI | OpenAIおよびAzure OpenAIとの通信抽象化 | OpenAI系モデルを使用する場合 |
| Microsoft.SemanticKernel.Planners.Handlebars | Handlebars構文による動的計画生成 | Planner機能を利用する場合 |
| Microsoft.SemanticKernel.PromptTemplates.Handlebars | Handlebars構文によるプロンプトテンプレート | 高度なテンプレート制御が必要な場合 |

パッケージのバージョン管理においては、Semantic Kernelの各パッケージは同一のリリースサイクルで更新されることが多いため、バージョン番号を揃える運用を推奨する。
異なるバージョンのパッケージを混在させると、依存関係の解決において予期しない実行時エラーを引き起こすリスクが増大する。
Central Package Managementを活用し、ソリューション全体でバージョンを一元的に管理することで、整合性の担保とセキュリティパッチの適用が容易になる。

KernelBuilderとIServiceCollectionの連携設定

Semantic Kernelの初期化には、従来のKernelBuilderを直接使用する方法と、IServiceCollectionへの統合登録する方法の2つが存在する。
エンタープライズアプリケーション、特にASP.NET Coreを想定する場合、後者のDIコンテナ連携が正しい選択である。
これにより、Kernelのライフサイクルはフレームワークに委ねられ、テスト時のモック置換も一貫した抽象を通じて実現できる。

builder.Services.AddKernel()
    .AddOpenAIChatCompletion(
        modelId: builder.Configuration["OpenAI:ModelId"]!,
        apiKey: builder.Configuration["OpenAI:ApiKey"]!);

AddKernelメソッドはKernelインスタンスをScopedとして登録し、同一HTTPリクエスト内で一貫した文脈を維持する。
KernelBuilderを手動でnewする方式はコンソールアプリケーションや短期間のスクリプトには適合するが、長期間動作するサーバーアプリケーションではサービスロケーターアンチパターンに陥りやすく、保守性を損なう。
設計の観点から、構成の責務と実行の責務を明確に分離することが重要である。

構成管理:APIキーとエンドポイントの安全な取り扱い

APIキーやエンドポイントURLをソースコードにハードコードすることは、セキュリティ上重大なリスクを伴う。
これはコンピューターサイエンスの基礎教養として、認証情報の保護がいかに重要かを示す最も基本的な例である。
開発環境では.NETのUser Secrets、ステージングや本番環境ではAzure Key Vaultまたは環境変数を通じて構成を注入する設計が望ましい。

builder.Configuration.AddUserSecrets<Program>();

var modelId = builder.Configuration["OpenAI:ModelId"]
    ?? throw new InvalidOperationException("OpenAI:ModelId is not configured.");
var apiKey = builder.Configuration["OpenAI:ApiKey"]
    ?? throw new InvalidOperationException("OpenAI:ApiKey is not configured.");

builder.Services.AddKernel()
    .AddOpenAIChatCompletion(modelId: modelId, apiKey: apiKey);

構成値の検証は起動時に厳格に行うべきである。
null許容参照型を有効化し、構成値の取得時に即座に検証例外を発生させることで、デプロイ後の実行時エラーを未然に防ぐ。
さらに、Azure OpenAIを使用する場合はエンドポイントURLも同様に構成から取得し、リージョンや環境の切り替えをコードの変更なしに実現できるようにする。
こうした構成の外部化は、12-Factor Appの原則に照らしても当然の設計である。

Kernelライフサイクルの設計指針:Singleton・Scoped・Transientの使い分け

DIコンテナでのKernelライフサイクル管理を示す図解

Semantic KernelのKernelインスタンスは、単なるステートレスなHTTPクライアントではない。
その内部にはPluginのレジストリ、フィルタチェーン、そして場合によっては会話の文脈が維持される。
したがって、依存性注入におけるライフサイクルの選定は、アプリケーションの挙動とデータの分離境界を直接左右する重要な設計判断となる。
コンピューターサイエンスの観点から言えば、これは共有状態の管理と並行アクセス制御の問題に他ならない。
不適切なスコープ設定は、情報の漏洩、競合状態、あるいは予期しない副作用を招く。

サービス登録パターンとスレッド安全性の検討

KernelをSingletonとして登録する場合、すべてのリクエストが同一インスタンスを共有することになる。
これはPluginの登録情報やLLM接続設定といった不変な構成に対しては有効な選択肢である。
しかし、会話履歴やリクエスト固有の引数がKernel内部に蓄積される設計である場合、Singletonはマルチスレッド環境において重大な状態汚染を引き起こす。
特にASP.NET Coreでは、複数のHTTPリクエストが並列に同一インスタンスにアクセスするため、スレッド安全性の検討は不可欠である。

ScopedはHTTPリクエスト単位、または明示的に定義されたスコープ単位でインスタンスを分離する。
これにより、リクエスト間での状態の混在を防ぎつつ、同一リクエスト内の複数のサービスで同一Kernel文脈を共有できる。
Transientは都度新しいインスタンスを生成するため、状態の分離は最も厳格であるが、Pluginの再登録や接続の再初期化といったオーバーヘッドが増大する。
以下に各パターンの特性を整理する。

| ライフサイクル | 状態分離性 | インスタンス生成コスト | 推奨される適用場面 | リスク |
| Singleton | 低い | 最小 | グローバル設定と不変Plugin | 状態の共有による情報漏洩 |
| Scoped | 高い | 中程度 | Webリクエスト単位の処理 | スコープ誤用によるメモリリーク |
| Transient | 完全 | 高い | バッチ処理や短寿命ジョブ | リソース消費の増大 |

ASP.NET Coreでの実践的なDI設定例

ASP.NET Coreにおいては、KernelをScopedとして登録することが最も実用的な選択である。
AddKernelメソッドはデフォルトでScopedとして登録されるが、これを明示的に理解し、周辺サービスとの整合性を保つことが重要である。
Pluginの中にはスコープを意識した設計が必要なものもある。
例えば、テナントIDを保持するPluginはScopedまたはTransientとして管理すべきである。

// Pluginを個別にScoped登録し、Kernelにインポートする実践的な例
builder.Services.AddScoped<ITimeProvider, UtcTimeProvider>();
builder.Services.AddScoped<MyEnterprisePlugin>();

builder.Services.AddKernel()
    .AddOpenAIChatCompletion(
        modelId: builder.Configuration["OpenAI:ModelId"]!,
        apiKey: builder.Configuration["OpenAI:ApiKey"]!);

// カスタムファクトリでPluginをKernelに統合する例
builder.Services.AddScoped<KernelPluginCollection>(sp =>
{
    var plugins = new KernelPluginCollection();
    plugins.AddFromObject(sp.GetRequiredService<MyEnterprisePlugin>());
    return plugins;
});

コントローラーまたはアプリケーションサービスでは、Kernelをコンストラクタ注入により受け取る。
この際、同一リクエスト内で複数のユースケースがKernelを共有する場合でも、Scopedであれば安全に同一文脈を維持できる。

public class AnalysisService
{
    private readonly Kernel _kernel;
    private readonly ILogger<AnalysisService> _logger;

    public AnalysisService(Kernel kernel, ILogger<AnalysisService> logger)
    {
        _kernel = kernel;
        _logger = logger;
    }

    public async Task<string> ExecuteAnalysisAsync(string input)
    {
        _logger.LogInformation("Executing analysis with Kernel instance.");
        var result = await _kernel.InvokePromptAsync(input);
        return result.ToString();
    }
}

ライフサイクルの選定は、最終的に「Kernelがどのような状態を持つべきか」という設計思想に帰結する。
状態を持たせず純粋な関数呼び出しのオーケストレーターとして運用するならSingletonも選択肢に入るが、多くのエンタープライズアプリケーションではリクエスト単位の文脈分離が必須である。
あなたのアプリケーションにおいて、Kernelは共有リソースなのか、それとも隔離された実行コンテキストなのか。
答えを明確にしてからサービス登録を行うべきである。

プロンプトテンプレートとネイティブ関数:プラグイン設計の実装パターン

プロンプトテンプレートとネイティブ関数を組み合わせるプラグイン設計図

Semantic KernelにおけるPluginの設計は、単に機能をまとめること以上の意味を持つ。
プロンプトテンプレートとネイティブ関数を同一の抽象下に配置することで、LLMは自然言語の指示と型安全なコードを区別することなく呼び出せる。
これはコンピューターサイエンスにおける「抽象化とカプセル化」の原則の具体的な実践である。
以下では、それぞれの構成要素の実装パターンと、複数Pluginを統合する際の設計指針を論じる。

PromptTemplateの記法とパラメータ化

PromptTemplateは、自然言語の指示に変数を埋め込むことで、実行時に動的な値を注入できるテンプレート機構である。
Semantic Kernelでは、主に {{$variableName}} 構文を用いてテンプレート内のプレースホルダーを宣言する。
この記法は、単なる文字列連結ではなく、テンプレートエンジンによる構文解析と検証の対象となるため、パラメータの欠落や型の不整合を早期に検出できる。

| 記法 | 用途 | 具体例 |
| {{$variable}} | 外部から注入されるスカラー値 | {{$userName}}、{{$language}} |
| {{functionName}} | 同一Plugin内または他Pluginの関数呼び出し | {{TimePlugin.GetCurrentTime}} |
| {{#with}} | オブジェクトコンテキストのスコープ設定 | {{#with user}} {{name}} {{/with}} |

テンプレートをC#コード内にハードコードするのではなく、外部ファイルとして配置することで、プロンプトエンジニアと開発者の責務を分離できる。
実際には、プロンプトテンプレートファイル(skprompt.txt または YAML形式の定義ファイル)をプロジェクトに含め、ビルド時にリソースとして扱うパターンが推奨される。
これにより、プロンプトの変更がコードの再コンパイルを必要としない運用フローを実現できる。

Native Functionの実装とKernelFunction属性

Native Functionは、C#で実装された通常のメソッドをSemantic Kernelの実行エンジンに公開する仕組みである。
メソッドに KernelFunction 属性を付与し、オプションで Description 属性で関数の目的と入出力の意味を記述することで、PlannerやLLMがその関数を適切な文脈で呼び出すためのメタデータが整備される。
コンピューターサイエンスの観点からは、これはリフレクションを活用した動的ディスパッチの一種であり、インターフェースの自己記述性を高める設計である。

public class MathPlugin
{
    [KernelFunction("add_numbers")]
    [Description("2つの整数を加算して結果を返す。負数も扱うことができる。")]
    public int AddNumbers(
        [Description("加算される最初の整数")] int number1,
        [Description("加算される2番目の整数")] int number2)
    {
        return number1 + number2;
    }

    [KernelFunction("get_tax_rate")]
    [Description("現在の消費税率をパーセンテージで返す")]
    public decimal GetTaxRate()
    {
        return 10.0m;
    }
}

上記の例で示すように、パラメータに対しても Description 属性を付与することが重要である。
LLMが関数呼び出しを行う際、各引数の意味を正しく解釈するための主要な情報源がこのメタデータである。
引数の型はプリミティブ型またはそのラッパーに限定されることが多く、複雑なオブジェクトを渡す場合はJSON文字列として受け取り、内部でデシリアライズする設計が実用的である。

複数プラグインの合成と優先度制御

実際のアプリケーションでは、単一のPluginでは要件を満たさず、複数のPluginをKernelに統合する必要がある。
例えば、時間取得Plugin、数学計算Plugin、企業固有のAPI呼び出しPluginを同一Kernelに登録し、Plannerにそれらを協調させる場面である。
Kernelは KernelPluginCollection を通じて複数のPluginを保持し、各関数は PluginName.FunctionName という完全修飾名で識別される。

var kernel = builder.Build();
kernel.ImportPluginFromType<TimePlugin>();
kernel.ImportPluginFromType<MathPlugin>();
kernel.ImportPluginFromObject(new EnterpriseApiPlugin(httpClient));

複数Pluginを統合する際の設計上の留意点は、関数名の衝突と優先度の制御である。
同一Kernel内で同名の関数が存在する場合、呼び出し時に曖昧性が生じる。
これを防ぐため、Plugin名は機能ドメインを反映した明確な命名規則に従うべきである。
例えば FinancePlugin.CalculateBudget と HrPlugin.CalculateBudget のように、名前空間と同様の論理構造をPlugin名に持たせることで、LLMが適切な関数を選択しやすくなる。

優先度制御については、Semantic Kernelのフィルタ機構を活用して、特定のPlugin関数の実行をインターセプトする設計が有効である。
あるいは、Plannerに渡すPluginのセットを事前に絞り込み、ユースケースごとに許可リストを定義することで、不必要な関数露出を防ぐ。
これはセキュリティ上も重要であり、すべてのPluginを常にPlannerに公開するのではなく、最小権限の原則に基づいて必要なPluginのみを文脈に応じて提供するべきである。
あなたの設計は、Pluginを単なる機能の集まりとしてではなく、明確な責務境界を持つモジュールとして扱っているか。
そこがエンタープライズアプリケーションと玩具の差異を分ける重要な分水嶺である。

Plannerの実装戦略:HandlebarsPlannerとFunction Callingによる動的実行

Plannerの動的実行戦略を示すSemantic Kernelの処理フロー図

Semantic KernelにおけるPlannerは、LLMの推論能力とアプリケーションの関数群を橋渡しする重要なコンポーネントである。
従来の手続き型プログラミングでは、実行フローはコンパイル時に決定されるが、Plannerは実行時に目標に対する最適な関数呼び出し列を生成する。
この特性は、明示的に分岐を記述しきれない複雑な対話フローや、要件変化に対して柔軟に対応すべきエージェント型アプリケーションにおいて特に有効である。
以下では、HandlebarsPlannerとFunction Callingステップ実行という2つの主要な戦略と、その運用上の安全策を論じる。

HandlebarsPlannerのテンプレートエンジン活用

HandlebarsPlannerは、生成した計画をHandlebarsテンプレートとして表現するPlannerである。
これにより、条件分岐や反復処理といった制御構造を含む動的な実行計画を生成できる。
単なる関数呼び出しのリストではなく、テンプレートとしての中間表現を持つため、複雑なサブゴールを含む階層的な実行計画を表現するのに適している。

var planner = new HandlebarsPlanner(
    new HandlebarsPlannerOptions() { MaxTokens = 4096 });

var plan = await planner.CreatePlanAsync(kernel, 
    "今日の日付を取得し、先月の売上データを集計して、要約レポートを作成してください");

var result = await plan.InvokeAsync(kernel, new KernelArguments());

HandlebarsPlannerの強みは、生成された計画の可読性と検査容易性にある。
plan.ToString() でテンプレートの中身を確認でき、どの関数がどのような順序で呼ばれるかを事前に把握できる。
ただし、テンプレートの複雑性が増すと、LLMによる計画生成の成功率が低下する傾向がある。
実用上は、Plugin関数のDescriptionを簡潔かつ正確に保ち、関数のインターフェースを最小限に抑えることで、計画生成の精度を高めることができる。

Function Callingステップ実行の実装

Function Callingベースの実行は、OpenAIのツール呼び出し機能を活用し、LLMが自律的に必要な関数を選択して実行するパターンである。
これはHandlebarsPlannerのような明示的な計画テンプレート生成ではなく、対話の各ターンでLLMが「次にどの関数を呼ぶべきか」を判断し、Kernelがその関数を実行して結果を返すサイクルを繰り返す。
Stepwiseな性質を持つため、動的な環境変化や中間結果に応じた柔軟な対応が可能である。

var executionSettings = new OpenAIPromptExecutionSettings
{
    ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};

var chatCompletion = kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("在庫状況を確認し、必要に応じて発注処理を実行してください");

var response = await chatCompletion.GetChatMessageContentAsync(
    chatHistory,
    executionSettings,
    kernel);

このパターンでは、Kernelが自動的に関数のディスパッチと結果の回収を管理する。
AutoInvokeKernelFunctionsを有効化することで、クライアントコードは複雑なループを記述することなく、複数ステップにわたる推論を委任できる。
ただし、無限ループや過剰な関数呼び出しのリスクを孕むため、MaxIterationsやMaxTokensによる制約設定は必須である。

プランナー出力の検証とサンドボックス化

Plannerの非決定論的な性質は、運用上の課題を伴う。
生成された計画が無限ループを含んだり、不適切な関数を連続して呼び出したりするリスクがあるため、実行前および実行中の検証は必須である。
堅牢なアプリケーションを目指すなら、Plannerを無制限に委任するのではなく、許可リストや実行ステップ数の上限といったガードレールを設ける設計が求められる。

| 検証項目 | 目的 | 実装アプローチ |
| 関数名の許可リスト照合 | 予期しない関数呼び出しの防止 | 計画生成後に使用関数を抽出し、許可セットと照合する |
| ステップ数の上限設定 | 無限ループおよび過剰実行の防止 | PlannerOptionsまたはExecutionSettingsで最大反復回数を指定する |
| 引数の範囲検証 | 不正なパラメータによる副作用の防止 | FunctionFilterで呼び出し前に引数を検査する |
| タイムアウト制御 | 長時間実行によるリソース占有の防止 | CancellationTokenを伝播させ、外部でタイムアウトを制御する |

特に重要なのは、FunctionFilterを活用した実行のインターセプトである。
計画の実行前に呼び出される関数を検査し、危険な操作や許可されていない外部APIアクセスを遮断できる。
これはコンピューターサイエンスにおけるサンドボックス化の考え方に基づく。
生成AIに与える能力は、常に最小権限の原則に従って制限すべきである。
あなたの実装は、Plannerの出力を盲信するのではなく、検証可能で制御された実行環境の上で動作させているか。
そこに、エンタープライズアプリケーションとしての信頼性がかかっている。

ベクトルDB連携:Semantic Memoryを活用したRAGアーキテクチャの構築

Semantic MemoryとベクトルDBを連携させたRAG構成図

大規模言語モデルは訓練データの知識に制限され、最新の情報や企業固有の内部文書を参照できない。
この根本的な制約を克服するため、Retrieval Augmented Generation(RAG)は生成AIアプリケーションの標準アーキテクチャとして確立されている。
Semantic KernelにおけるSemantic Memoryは、外部知識をベクトル化して保存し、推論時に関連情報を検索してプロンプトに動的に注入するための基盤を提供する。
以下では、ベクトルデータベースとの連携から、プロンプトへの情報注入までの実装パターンを論じる。

IMemoryConnectorの実装とベクトルストア選定

Semantic Memoryの実装には、IMemoryStoreインターフェースを実装したベクトルデータベースを選定する必要がある。
Azure AI Search、Qdrant、Redis、SQLiteなど、複数のバックエンドがサポートされており、要件に応じた選定が求められる。
エンタープライズ環境では、既存のAzureエコシステムとの親和性からAzure AI Searchが選ばれることが多いが、オンプレミスや開発環境ではQdrantやSQLiteの軽量性が有利である。

| ベクトルストア | 運用形態 | ハイブリッド検索 | 推奨場面 |
| Azure AI Search | フルマネージド | 全文検索と統合可能 | Azure基盤のエンタープライズ環境 |
| Qdrant | コンテナまたはセルフホスト | ベクトル検索に特化 | 柔軟なデプロイが必要な場合 |
| Redis | インメモリ | タグ付きベクトル検索 | 低レイテンシが要求されるキャッシュ層 |
| SQLite | 組み込み | 制限あり | ローカル開発や単体テスト |

builder.Services.AddSingleton<IMemoryStore>(sp =>
    new AzureAISearchMemoryStore(
        endpoint: builder.Configuration["AzureAISearch:Endpoint"]!,
        apiKey: builder.Configuration["AzureAISearch:ApiKey"]!));

builder.Services.AddSingleton<ISemanticTextMemory>(sp =>
{
    var memoryStore = sp.GetRequiredService<IMemoryStore>();
    var embeddingGenerator = sp.GetRequiredService<OpenAITextEmbeddingGenerationService>();
    return new SemanticTextMemory(memoryStore, embeddingGenerator);
});

テキストのベクトル化と保存フロー

RAGの品質は、テキストをどのように分割し、どのような粒度でベクトル化するかに大きく依存する。
単純に長大な文書をそのまま埋め込み生成しても、検索精度は低下する。
適切なチャンキング戦略、すなわち意味的な区切りに基づく分割とオーバーラップの確保が、検索時の再現率を左右する。

var memory = kernel.GetRequiredService<ISemanticTextMemory>();
var documentId = "enterprise-policy-001";
var content = "新しいセキュリティポリシーでは、すべてのAPIキーはAzure Key Vaultに保存される必要がある..."; // 実際には長文

await memory.SaveInformationAsync(
    collection: "enterprise-docs",
    id: documentId,
    text: content,
    description: "セキュリティポリシードキュメント");

保存時には、コレクション名による論理的分離を明確に行うべきである。
マルチテナント環境ではテナントIDをコレクション名に含めるか、メタデータにテナント情報を付与してフィルタリングを行う設計が必要である。
これはデータの分離とアクセス制御の観点から、セキュリティ上不可欠な考慮事項である。

検索結果のプロンプトへの動的注入

推論フェーズでは、ユーザーのクエリをベクトル化し、ベクトルストアから類似度の高いチャンクを検索する。
検索結果は、プロンプトテンプレート内の変数として動的に注入され、LLMはその文脈に基づいて回答を生成する。
このメカニズムにより、モデルは訓練データに存在しない最新の情報や社内文書の内容を参照できる。

var memory = kernel.GetRequiredService<ISemanticTextMemory>();
var query = "APIキーの保存要件を教えて";
var results = await memory.SearchAsync("enterprise-docs", query, limit: 3, minRelevanceScore: 0.8);
var contextText = string.Join("\n---\n", results.Select(r => r.Metadata.Text));

var arguments = new KernelArguments
{
    ["query"] = query,
    ["context"] = contextText
};

var response = await kernel.InvokePromptAsync(
    "以下の情報のみに基づいて回答してください:\n{{$context}}\n\n質問: {{$query}}",
    arguments);

この実装において重要なのは、minRelevanceScoreによる品質ゲートの設定である。
関連性の低い情報をプロンプトに混入させると、LLMの回答品質を低下させ、幻覚を誘発するリスクがある。
適切な閾値を設定し、検索結果が存在しない場合には「情報が見つかりませんでした」という明示的な分岐を実装することが、信頼性の高いRAGシステムの条件である。
あなたの実装は、単にベクトル検索を行うのではなく、検索結果の品質を検証し、不確実な情報を排除する設計になっているか。
そこに、実用的なRAGとデモ的なRAGの差異が存在する。

運用品質を担保する実装:フィルタ、テレメトリ、リトライ、エラーハンドリング

フィルタ、テレメトリ、リトライを統合した運用監視アーキテクチャ図

生成AIを統合したアプリケーションを本番環境で運用する際、機能の正しさだけでは不十分である。
観測可能性、回復性、そして実行フローに対する制御可能性が、エンタープライズソフトウェアとしての品質を決定する。
Semantic Kernelは非決定論的なLLM呼び出しを内包するため、従来のHTTPクライアント以上の防御的な設計が求められる。
コンピューターサイエンスにおける「fail-fast」と「graceful degradation」の原則は、ここでも有効である。
以下では、運用品質を支える4つの柱を実装レベルで解説する。

| 運用品質の観点 | 対応する実装コンポーネント | 本番環境における目的 |
| 実行フローの横断的制御 | FunctionFilter / PromptFilter | 認可、検証、監査ログの一元的な適用 |
| 分散トレーシングと診断 | OpenTelemetry | リクエスト単位での推論過程の可視化 |
| 外部通信の回復性 | Polly | レート制限や一時的障害からの自動回復 |
| 障害の局所化 | 構造化例外ハンドリング | カスケード障害の防止と診断情報の保全 |

FunctionFilterとPromptFilterの実装

FilterはSemantic KernelにおけるAspect Oriented Programming(AOP)に相当する機構である。
PromptRenderFilterはプロンプトが最終レンダリングされる直前に介入し、FunctionInvocationFilterは関数の実行前後に介入する。
これにより、認可チェック、機密情報のマスキング、実行時間の計測、あるいは特定条件下での呼び出し拒否を横断的に適用できる。

public class ApprovalFilter : IFunctionInvocationFilter
{
    public async Task OnFunctionInvocationAsync(
        FunctionInvocationContext context,
        Func<FunctionInvocationContext, Task> next)
    {
        if (context.Function.PluginName == "FinancePlugin" &&
            !context.Arguments.ContainsKey("approvalCode"))
        {
            throw new InvalidOperationException(
                "財務関連操作には承認コードが必要です。");
        }

        await next(context);
    }
}

このフィルタを依存性注入コンテナに登録することで、Kernelは自動的に解決し、すべての関数呼び出しに適用される。
ハードコードされたガード条件を各メソッドに散らばさせるのではなく、Filterとして分離することで、セキュリティポリシーの変更に対する耐性を高めることができる。

OpenTelemetryと構造化ログの統合

LLMとの対話はブラックボックス化しやすく、なぜ特定の応答が生成されたかを事後的に追跡することは困難である。
OpenTelemetryを統合することで、プロンプトのレンダリング、関数呼び出し、ベクトル検索、外部APIアクセスまでを一つのトレースとして関連付けられる。
.NETのLogging APIと組み合わせることで、構造化ログとして運用環境に出力することが可能である。

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddSource("Microsoft.SemanticKernel");
        tracing.AddAspNetCoreInstrumentation();
        tracing.AddOtlpExporter();
    });

builder.Logging.AddJsonConsole(options =>
{
    options.IncludeScopes = true;
    options.UseUtcTimestamp = true;
    options.JsonWriterOptions = new System.Text.Json.JsonWriterOptions
    {
        Indented = false
    };
});

構造化ログは単なるデバッグ出力ではなく、ログ蓄積基盤でのクエリ対象となる。
トレースIDを紐付けることで、特定のユーザーリクエストにおけるLLMの推論ステップを再構成でき、運用異常の根本原因分析に不可欠な情報となる。

Pollyによる回復性の高いHTTP戦略

OpenAIやAzure OpenAIへのHTTP呼び出しは、レート制限、タイムアウト、一時的なサービスUnavailableに常に晒されている。
Pollyを用いた回復性戦略は、こうした外部障害をアプリケーションの致命的なエラーに昇格させないための必須の防御層である。
特に指数バックオフを伴うリトライとサーキットブレーカーの組み合わせは、過負荷時のシステム保護と可用性の両立を実現する。

builder.Services.AddHttpClient("OpenAIResilientClient")
    .AddResilienceHandler("openai-pipeline", b =>
    {
        b.AddRetry(new HttpRetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            Delay = TimeSpan.FromSeconds(1),
            BackoffType = DelayBackoffType.Exponential,
            ShouldRetryAfterHeader = true
        });

        b.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            SamplingDuration = TimeSpan.FromSeconds(30),
            FailureRatio = 0.5,
            MinimumThroughput = 5,
            BreakDuration = TimeSpan.FromSeconds(15)
        });

        b.AddTimeout(new HttpTimeoutStrategyOptions
        {
            Timeout = TimeSpan.FromSeconds(30)
        });
    });

Semantic KernelのHttpClientをこの回復性の高いクライアントに統合することで、レート制限への対応をアプリケーションコードに散在させることなく、インフラストラクチャ層で解決できる。
運用環境において、エラーの発生は避けられない。
重要なのは、エラーが発生した際にシステムがどのように振る舞うかを設計することである。
あなたの実装は、単に成功時のハッピーパスだけを想定しているのではないか。
堅牢な運用品質は、障害を前提とした設計から生まれる。

まとめ:Semantic Kernelを使いこなすC#エンジニアの次の一歩

Semantic Kernelを使いこなすC#エンジニアの次のステップを示すロードマップ

本稿では、Semantic Kernelを用いたC#アプリケーション開発における中核概念から実装パターン、そして運用品質に至るまでの体系的なノウハウを解説してきた。
Kernelによる実行コンテキストの統合管理、Pluginによるプロンプトとネイティブ関数の抽象化、Plannerによる動的実行フローの制御、そしてSemantic Memoryを活用したRAGアーキテクチャの構築。
これらは単なる機能の羅列ではなく、生成AIを堅牢なエンタープライズソフトウェアに統合するための設計言語として機能する。
コンピューターサイエンスの基礎に立ち返れば、ここで示したすべてのパターンは、関心事の分離、抽象化、そして防御的設計という不変の原則の具体化に過ぎない。
SDKのAPIを覚えることよりも、これらの原則がなぜ適用されるのかを理解することが、長期的な技術的優位性を生む。

実務に取り組むにあたり、最も重要な視点は「生成AIの能力をどのように既存のアーキテクチャに組み込むか」ではなく、「既存の設計原則を生成AIの文脈でどう適用するか」という点にある。
依存性注入によるKernelのライフサイクル管理は、オブジェクトの共生とスコープに関する古典的な問いの延長線上にある。
Filterによる実行フローの横断的制御は、AOPやミドルウェアの概念がLLM時代に再解釈されたものである。
Pollyによる回復性戦略やOpenTelemetryによる観測性の確保も、分散システムにおける運用品質の要請が変わらないことの表れである。
技術の流行に流されることなく、第一原理に基づいて設計判断を行うことが、長期にわたる保守性と拡張性を担保する唯一の方法である。

次の一歩として、以下の3つの実践を強く推奨する。

  • 既存アプリケーションへの漸進的統合:生成AIを一から構築するのではなく、既存のレイヤードアーキテクチャの特定のユースケースにSemantic Kernelを導入し、小さく検証してから拡張するアプローチを採用する
  • プロンプトのバージョン管理と評価基盤の構築:プロンプトテンプレートをコードと同等の資産として扱い、変更の追跡と回帰テストを可能にするCIパイプラインを整備する
  • セキュリティとガバナンスの確立:LLMの出力検証、機密情報のフィルタリング、プランナーの許可リスト管理を、組織のセキュリティポリシーと整合させた運用ガイドラインとして文書化する

生成AIの技術進化は今後も加速し、モデルの能力とSDKの機能は日々更新されていく。
しかし、良質なソフトウェアを構築するための根本的な原理は変わらない。
型安全性、テスト可能性、モジュール性、運用性。
これらを妥協することなく、Semantic Kernelの豊かな機能を活用することが、C#エンジニアとしての真の価値を発揮する道である。
あなたの次のプロジェクトにおいて、本稿で論じた設計指針が、単なる実装の参考ではなく、意思決定の根拠となることを期待する。
生成AIは道具であり、その道具をいかにマスターして、信頼性の高いシステムを構築するか。
そこに、エンジニアリングの本質がある。
新しい技術への対応力と、揺るがない設計の原理原則を併せ持つことこそが、これからのエンジニアに求められる資質である。

コメント

タイトルとURLをコピーしました