AIアプリケーションで複数のモデルやプロバイダーを利用すると、「簡単な処理は低コストモデルへ送りたい」「障害時は別プロバイダーへ切り替えたい」といった制御が必要になります。しかし、ルーティング判定、再試行、ストリーミング中の障害処理まで個別に実装すると、コードが複雑になりがちです。
Microsoft.Extensions.AI Version 10.9.0では、モデルやプロバイダーを選択するRoutingChatClientとSemanticRoutingChatClient、障害時の切り替えを行うFailoverChatClientとOrderedFailoverChatClientが追加されました。
実装方針は明確です。正常時の振り分けにはRouting、呼び出し失敗時の退避にはFailoverを使い、必要に応じて両者を組み合わせます。 ただし、これらはすべて診断IDMEAI001が付いたExperimental APIです。本番環境で利用する場合は、パッケージのVersion固定、API依存部分の分離、ストリーミングを含む障害テストが欠かせません。(Microsoft for Developers)
Microsoft.Extensions.AI 10.9.0で追加されたRoutingとFailover
Microsoft.Extensions.AIのRoutingとFailoverは、いずれもIChatClientとして扱えます。そのため、アプリケーション側は単一のチャットクライアントを利用する形を維持したまま、内部でモデルやプロバイダーを切り替えられます。(Microsoft for Developers)
| クラス | 主な役割 | 適しているケース |
|---|---|---|
RoutingChatClient | リクエストごとに利用するIChatClientを選択する | コスト、機能、リージョン、契約プランによる振り分け |
SemanticRoutingChatClient | メッセージの意味をEmbeddingで比較してルートを選択する | コーディング、文章作成、問い合わせ分類などの意味ベース振り分け |
FailoverChatClient | 失敗後に別のクライアントを選び直すための抽象基底クラス | ヘルス状態、遅延、クールダウンなどを考慮した独自Failover |
OrderedFailoverChatClient | 登録したクライアントを順番に試す | プライマリ、セカンダリ、緊急用という単純な優先順位切り替え |
RoutingChatClientはリクエストの実行前にクライアントを選びます。一方、FailoverChatClientは、選択したクライアントが出力開始前に失敗した場合に、別のクライアントを再選択します。OrderedFailoverChatClientは、そのFailover処理をすぐ使える形にした具象クラスです。(Microsoft for Developers)
RoutingとFailoverの違い
RoutingとFailoverは似ているように見えますが、利用目的が異なります。
| 比較項目 | Routing | Failover |
|---|---|---|
| 判断するタイミング | モデル呼び出し前 | モデル呼び出しの失敗後 |
| 主な判断材料 | 入力内容、必要機能、コスト、リージョン | 例外、出力開始の有無、試行回数 |
| 主な目的 | 最適なモデルやプロバイダーへ配置する | サービス停止や一時障害から退避する |
| 成功した低品質回答 | 別モデルへ切り替えない | 別モデルへ切り替えない |
| 複数モデルの同時実行 | 行わない | 行わない |
実務では、次のように整理すると判断しやすくなります。
- Routingは「この処理を最初からどこへ送るか」を決める
- Failoverは「送った先が失敗したとき、次にどこへ逃がすか」を決める
RoutingChatClientは1回のリクエストにつき、実行前に1つのクライアントを選択します。複数モデルへ同時送信して結果を投票するEnsemble、最初に返った応答を採用するHedging、回答品質が基準以下なら上位モデルへ送るModel Cascadingは、標準のRoutingやFailoverが直接担当する処理ではありません。(Microsoft for Developers)
Version 10.9.0を固定して導入する
パッケージをインストールする
Microsoft.Extensions.AI 10.9.0は、次のコマンドで追加できます。(NuGet)
dotnet add package Microsoft.Extensions.AI --version 10.9.0
ただし、Experimental APIを本番利用する場合は、単にVersion番号を指定するだけでなく、完全一致のVersion範囲にすることを推奨します。
<ItemGroup>
<PackageReference
Include="Microsoft.Extensions.AI"
Version="[10.9.0]" />
</ItemGroup>
NuGetのVersion指定では、Version="10.9.0"は範囲記法上「10.9.0以上」を意味します。通常は最小の適合Versionが優先されますが、Version="[10.9.0]"と記述すれば、10.9.0だけを許可する完全一致指定になります。(Microsoft Learn)
Central Package Managementを利用している場合は、Directory.Packages.props側を固定します。
<ItemGroup>
<PackageVersion
Include="Microsoft.Extensions.AI"
Version="[10.9.0]" />
</ItemGroup>
依存関係をロックする
直接参照しているパッケージだけでなく、推移的な依存関係も固定したい場合は、ロックファイルを有効にします。
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
復元するとプロジェクト直下にpackages.lock.jsonが生成されます。アプリケーションプロジェクトでは、このファイルをリポジトリへコミットします。
CIでは、次のようにLocked Modeで復元します。
dotnet restore --locked-mode
Locked Modeでは、ロックファイルと異なる依存関係が必要になった場合、暗黙に更新せず復元を失敗させられます。意図しない依存関係の変化を検出するうえで有効です。(Microsoft Learn)
MEAI001は狭い範囲で抑制する
RoutingとFailover関連の型には、Experimental("MEAI001")が付いています。ExperimentalAttributeは、将来APIが変更される可能性があることを示すための属性です。診断IDは必要に応じて抑制できます。(Microsoft Learn)
プロジェクト全体でMEAI001を無効にするより、クライアントを組み立てるFactoryやAdapterだけに範囲を限定する方が安全です。
#pragma warning disable MEAI001
// RoutingChatClientやOrderedFailoverChatClientの構築処理
#pragma warning restore MEAI001
例えば、次のような構成にします。
Application
└─ IAiChatService
└─ MicrosoftExtensionsAiClientFactory
├─ RoutingChatClient
├─ SemanticRoutingChatClient
└─ OrderedFailoverChatClient
Experimental APIをFactory内部へ閉じ込めておけば、将来コンストラクターやプロパティが変更されても、アプリケーション全体を修正せずに済みます。
RoutingChatClientでルールベースのModel Routingを実装する
RoutingChatClient.Createを使うと、選択用のコールバックだけで単純なルーターを構築できます。
次の例では、複雑なリクエストを高性能モデルへ、それ以外を低コストモデルへ送ります。powerfulClientとeconomicalClientは、あらかじめ構築したIChatClientです。
using Microsoft.Extensions.AI;
#pragma warning disable MEAI001
IChatClient router = RoutingChatClient.Create(
(context, cancellationToken) =>
new ValueTask<IChatClient>(
IsComplexRequest(context)
? powerfulClient
: economicalClient));
#pragma warning restore MEAI001
IsComplexRequestはMicrosoft.Extensions.AIが提供する判定ではなく、アプリケーション側で実装するルーティングポリシーです。公式APIは、判定結果に応じて選ばれたIChatClientへリクエストを転送します。(Microsoft for Developers)
実務で使えるRoutingの判断基準
| 判断条件 | 選択例 |
|---|---|
| 要約、分類、短い文章修正 | 低コスト・低遅延モデル |
| 複雑なコード生成、長文分析 | 高性能モデル |
| 画像入力が含まれる | Vision対応モデル |
| Tool Callingが必要 | Tool Calling対応モデル |
| JSONや構造化出力が必須 | 対応を検証済みのモデル |
| 長い会話履歴がある | 十分なContext Lengthを持つモデル |
| データ保管地域に制約がある | 要件を満たすリージョンのデプロイ |
| テナントごとに予算が異なる | 契約プランに対応したモデル |
入力文字数だけで複雑さを判定すると、短いが難しい質問を低性能モデルへ送る可能性があります。文字数、必要なツール、構造化出力、画像の有無、テナントの契約プランなどを組み合わせて判断するのが現実的です。
ChatOptionsの変更範囲に注意する
各リクエストでは、メッセージとChatOptionsのコピーを持つRoutingContextが作成されます。
context.ChatOptionsへ加えた変更は、そのリクエスト内の後続Failover試行にも引き継がれます。一方、特定ルートだけに適用したい設定は、選択するクライアント側へConfigureOptionsChatClientなどのラッパーとして設定することが推奨されています。(Microsoft for Developers)
例えば、次のように区別します。
- ユーザーが指定した出力形式や共通の最大トークン数は、リクエスト単位の設定
- 特定モデルだけで使う推論レベルや温度設定は、ルート単位の設定
- Failover先では利用できないProvider固有オプションは、各クライアントのラッパー側に設定
特定Provider専用の値をcontext.ChatOptionsへ直接書くと、Failover先にも設定が残り、互換性エラーを引き起こす可能性があります。
SemanticRoutingChatClientで意味ベースのRoutingを実装する
SemanticRoutingChatClientは、最後のユーザーメッセージをEmbeddingへ変換し、クライアントごとに登録した例文との類似度を比較します。基準値を超えたクライアントがなければ、defaultClientが選択されます。(Microsoft for Developers)
using Microsoft.Extensions.AI;
#pragma warning disable MEAI001
IChatClient semanticRouter = new SemanticRoutingChatClient(
embeddingGenerator,
clientProfiles:
new Dictionary<IChatClient, IReadOnlyList<string>>
{
[codingClient] = new[]
{
"C#のコードを書いて",
"この例外の原因を調査して",
"SQLクエリを最適化して",
"プログラムをリファクタリングして"
},
[writingClient] = new[]
{
"文章を読みやすく校正して",
"長い文章を要約して",
"記事の見出し案を作って",
"ビジネスメールを書き直して"
}
},
defaultClient: generalClient,
scoreThreshold: 0.3f,
topK: 1,
scoreAggregation:
SemanticRoutingChatClient.ScoreAggregation.Mean,
leaveOpen: true);
#pragma warning restore MEAI001
コンストラクターの既定値は、scoreThresholdが0.3、topKが1、集約方法がMeanです。leaveOpenの既定値はfalseで、ルーターを破棄すると、設定したクライアントとEmbedding Generatorも破棄されます。DIコンテナーなど別の場所がライフサイクルを管理する場合は、leaveOpen: trueを指定します。(Microsoft Learn)
Semantic Routingの精度を上げる手順
Semantic Routingは、例文を数件登録しただけで完成するものではありません。次の順序で調整します。
- 実際に入力されたプロンプトを、期待するルートごとに分類する
- 表現が異なる例文を各ルートへ登録する
- 正解ルート付きのテストデータを用意する
- 誤ったルートへ送られた割合を測定する
scoreThresholdとtopKを調整する- 本番投入後もルーティング結果を記録する
例えば「コードを書いて」だけでなく、次のような表現もコーディング用プロファイルへ含めます。
- この処理を高速化したい
- NullReferenceExceptionを直したい
- APIのレスポンスをクラスへ変換したい
- LINQの処理を読みやすくしたい
一方、「改善して」「作成して」のように用途を特定できない語句を複数ルートへ登録すると、誤判定が増えます。
公式ブログでは、十分な数の例文がある場合、topK: 5とMeanの組み合わせも初期候補として示されています。ただし、最適な閾値はEmbeddingモデル、言語、例文の分布によって変わるため、日本語の実データで調整する必要があります。プロファイルのEmbeddingは初回にまとめて生成され、その後はキャッシュされます。(Microsoft for Developers)
OrderedFailoverChatClientでProvider障害に備える
単純な優先順位で切り替える場合は、OrderedFailoverChatClientを使います。
using Microsoft.Extensions.AI;
#pragma warning disable MEAI001
var failoverClient = new OrderedFailoverChatClient(
new IChatClient[]
{
primaryClient,
secondaryClient,
emergencyClient
},
leaveOpen: true)
{
MaximumAttemptsPerRequest = 2
};
#pragma warning restore MEAI001
この例では、最初にprimaryClientを呼び出し、出力開始前に失敗した場合はsecondaryClientを試します。
MaximumAttemptsPerRequestが2なので、3番目のemergencyClientまでは進みません。すべての候補を試したい場合は、試行上限を候補数以上にするか、既定値のnullを利用します。MaximumAttemptsPerRequestは、1リクエスト当たりのクライアント呼び出し回数を制限するプロパティです。(Microsoft Learn)
Failoverが発生する条件
| 発生状況 | 別クライアントへ切り替えるか |
|---|---|
| 応答を返す前に例外が発生した | 切り替える |
| Streamingの最初の更新前に失敗した | 切り替える |
| Streaming出力を1回以上返した後に失敗した | 切り替えない |
| 呼び出し元がキャンセルした | 切り替えない |
| 応答には成功したが品質が低かった | 切り替えない |
| すべてのクライアントが失敗した | 最後の失敗を呼び出し元へ返す |
OrderedFailoverChatClientは登録順にクライアントを試しますが、Streaming出力が一度でも呼び出し元へ公開された後は、途中から別Providerの応答へつなぎ替えることはありません。キャンセル時にも再選択は行われません。(Microsoft Learn)
429や5xxだけを見ればよいわけではない
FailoverChatClientが扱うのは、選択したIChatClientの呼び出し失敗です。HTTPステータスを直接監視して切り替えているわけではありません。
そのため、次の点をProviderごとに確認します。
- 429がProvider SDK内部で再試行されるか
- 5xxが最終的に例外として表面化するか
- 認証エラーを別Providerへ切り替えるべきか
- タイムアウトが何秒で発生するか
- SDKの再試行とFailoverで待ち時間が二重にならないか
例えば、Provider SDKが30秒間再試行した後、さらに3つのFailover先を順番に試す設定では、ユーザーが長時間待たされます。Provider内部の再試行回数、クライアントのタイムアウト、MaximumAttemptsPerRequestをまとめて設計する必要があります。
RoutingとFailoverを組み合わせる
各RouterはIChatClientとして扱えるため、RoutingとFailoverを入れ子にできます。(Microsoft for Developers)
次の例では、通常処理用と高度処理用に、それぞれFailover経路を用意しています。
using Microsoft.Extensions.AI;
#pragma warning disable MEAI001
var fastRoute = new OrderedFailoverChatClient(
new IChatClient[]
{
fastPrimaryClient,
fastBackupClient
},
leaveOpen: true);
var deepRoute = new OrderedFailoverChatClient(
new IChatClient[]
{
deepPrimaryClient,
deepBackupClient
},
leaveOpen: true);
IChatClient resilientRouter = RoutingChatClient.Create(
(context, cancellationToken) =>
new ValueTask<IChatClient>(
IsComplexRequest(context)
? deepRoute
: fastRoute));
#pragma warning restore MEAI001
この構成では、最初にRoutingが処理の種類を判断します。その後、選ばれた経路内でProvider障害が発生するとFailoverが動作します。
簡単な処理
└─ 低コストProvider A
└─ 失敗時:低コストProvider B
複雑な処理
└─ 高性能Provider A
└─ 失敗時:高性能Provider B
重要なのは、Routing先ごとにFailoverグループを分けることです。
高性能モデルのFailover先に、必要な機能を持たない低価格モデルを登録すると、通信上は成功しても業務要件を満たせません。バックアップ先についても、次の互換性を確認します。
- Tool Calling
- Vision入力
- 構造化出力
- Context Length
- 推論レベル
- 利用可能なリージョン
- データ保持ポリシー
- レート制限
「応答できること」ではなく、「同じ業務を継続できること」をFailover先の選定基準にします。
複数ターンの会話ではSticky Routingを検討する
SemanticRoutingChatClientは最後のユーザーメッセージを使ってルートを判定します。そのため、会話の途中で毎回Routingすると、意図しないモデル変更が発生することがあります。
例えば、最初の入力が「このC#コードを修正して」で、次の入力が「もう少し短くして」だった場合、2回目のメッセージだけではコーディング用途と判定できない可能性があります。
推論モデルでは、Provider固有の継続情報や暗号化された推論データを利用する場合があります。また、途中でモデルやProviderを変更すると、既存のプロンプトキャッシュを再利用できない可能性があります。このため、同じ分類で会話を継続する場合は、最初に選んだルートを維持するSticky Routingが適しています。(Microsoft for Developers)
アプリケーション側のセッションIDを使う
Sticky RoutingのキーにChatOptions.ConversationIdをそのまま使うことは推奨されません。ConversationIdはProvider側の会話状態に属し、別のクライアントへ移行できない場合があるためです。
アプリケーション独自のセッションIDをAdditionalPropertiesへ設定します。
var options = new ChatOptions
{
AdditionalProperties = new()
{
["routing-session-id"] = sessionId
}
};
選択したルート名をRedisやデータベースへ保存し、同じセッションでは同じルートを返す構成にします。保存する値はクライアントインスタンスではなく、fastやdeepなどのルート名にすると、構成変更や再起動にも対応しやすくなります。(Microsoft for Developers)
Streamingでは途中切り替えできない
Streamingを利用する場合、最初のトークンや更新情報がユーザーへ届いた時点で出力がコミットされたと判断されます。それ以降にProviderが失敗しても、別クライアントへのFailoverは行われません。(Microsoft for Developers)
これは、Provider Aの回答途中にProvider Bの回答を接続すると、内容の重複や矛盾が生じるためです。
画面側では、次のような動作を設計します。
- 出力開始前の障害は自動Failoverする
- 出力開始後の障害は「回答が途中で停止しました」と表示する
- 再実行ボタンを用意する
- 再実行時は回答全体を新しく生成する
- 途中までの回答と再生成した回答を自動結合しない
Failoverの試験では、単に接続エラーを発生させるだけでは不十分です。「最初のStreaming更新前」と「最初のStreaming更新後」の両方で障害を注入し、期待した挙動になることを確認します。
Function Callingの内側と外側で動作が変わる
FunctionInvokingChatClientと組み合わせる場合は、Routerを配置する位置にも注意が必要です。
| 構成 | Routingの動作 |
|---|---|
RouterがFunctionInvokingChatClientの外側 | Tool Callingの一連の処理で同じクライアントを使う |
RouterがFunctionInvokingChatClientの内側 | ツール実行後の反復ごとに再選択される可能性がある |
ツールを呼び出す最初の推論だけ高性能モデルへ送り、ツール実行後の整形を低コストモデルへ送る設計では、Routerを内側へ配置する方法が考えられます。
一方、Provider固有のTool Call IDや会話状態を引き継ぐ必要がある場合は、途中でモデルを変えない方が安全です。標準的な実装では、まずTool Calling全体で同じクライアントを維持し、必要性を確認してから細かなRoutingを追加するとよいでしょう。Routerの配置によって、選択回数とTool Callingループ内のモデルが変わります。(Microsoft for Developers)
FailoverChatClientを継承するべきケース
OrderedFailoverChatClientは、固定された順番でクライアントを試す用途に適しています。一方、次の要件がある場合は、抽象クラスのFailoverChatClientを継承して独自ポリシーを実装します。
- 一定回数失敗したProviderを一時的に除外する
- 障害後にクールダウン時間を設ける
- 過去の応答時間から最速のProviderを選ぶ
- テナントの予算残高によって候補を変える
- 利用者の地域に近いリージョンを優先する
- 認証エラーと一時的なタイムアウトで処理を変える
- Providerごとのレート制限状況を考慮する
FailoverChatClientでは、派生クラスがSelectClientAsyncで次のクライアントを選択し、OnRoutingUpdateAsyncで試行結果を受け取れます。基底クラスが呼び出し、出力コミット、試行回数などを管理します。(Microsoft Learn)
OnRoutingUpdateAsyncで記録できる情報
FailoverChatClientAttemptには、次のような情報が含まれます。
| プロパティ | 確認できる内容 |
|---|---|
Client | 呼び出したIChatClient |
Duration | クライアント呼び出しに要した時間 |
Exception | 発生した例外 |
ResponseCompleted | 応答が最後まで正常に完了したか |
OutputCommitted | Streaming出力が呼び出し元へ届いたか |
TimeToFirstUpdate | 最初のStreaming更新までの時間 |
OnRoutingUpdateAsyncは、失敗時だけでなく、成功、失敗、途中放棄を含む各クライアント呼び出し後に実行されます。これらの値をOpenTelemetryやログ基盤へ送れば、Providerごとの成功率、遅延、Failover率を測定できます。(Microsoft for Developers)
ただし、OnRoutingUpdateAsync自身が例外を送出すると、Routing処理が停止します。ログ送信やメトリクス登録の失敗でAI応答まで失敗させないように、観測処理内の例外は適切に処理する必要があります。(Microsoft Learn)
また、OrderedFailoverChatClientはsealedクラスです。OrderedFailoverChatClientを継承して選択ロジックを変更することはできません。独自の試行順序やヘルス判定が必要な場合は、FailoverChatClientから実装します。(Microsoft Learn)
leaveOpenとDisposeの扱いに注意する
SemanticRoutingChatClientとOrderedFailoverChatClientは、既定では内部のクライアントを所有し、Routerが破棄されると内部クライアントも破棄します。(Microsoft for Developers)
次のように判断します。
| クライアントの管理方法 | leaveOpen |
|---|---|
| Routerがクライアントを生成し、Routerと同時に破棄する | false |
| DIコンテナーがSingletonとして管理する | true |
| 同じクライアントを複数Routerで共有する | true |
| テスト内でRouterだけがクライアントを所有する | false |
共有クライアントに対してleaveOpen: falseを指定すると、1つのRouterを破棄した際に、別のRouterが使用中のクライアントまで破棄される可能性があります。
反対に、すべてleaveOpen: trueにすると、所有者が不明確になりリソースが解放されないおそれがあります。どのオブジェクトがIChatClientとEmbedding Generatorを破棄するのかを、DI登録時に明確にしておきます。
本番導入前のチェックリスト
- [ ]
Microsoft.Extensions.AIを[10.9.0]で完全一致指定した - [ ]
packages.lock.jsonを生成してリポジトリへ追加した - [ ] CIで
dotnet restore --locked-modeを実行している - [ ]
MEAI001の抑制範囲をFactoryやAdapterへ限定した - [ ] Routing関連APIをアプリケーション本体から分離した
- [ ] 低コスト、高性能、Vision、Tool Callingなどの選択基準を文書化した
- [ ] Semantic Routingを実際の日本語プロンプトで評価した
- [ ] デフォルトルート率と誤ルーティング率を計測している
- [ ] Failover先が必要な機能とContext Lengthを備えている
- [ ] 429、5xx、タイムアウト、認証エラーの挙動を確認した
- [ ] Provider SDKの再試行とFailoverが重複していない
- [ ] Streaming出力前と出力後の障害テストを実施した
- [ ] 複数ターン会話でSticky Routingが必要か判断した
- [ ]
ConversationIdではなくアプリ独自のセッションIDを使っている - [ ]
leaveOpenとDisposeの所有関係を確認した - [ ] Provider別の成功率、所要時間、Failover率を記録している
- [ ] Package更新前にRelease NotesとAPI差分を確認する運用を決めた
まとめ:小さなRoutingから段階的に導入する
Microsoft.Extensions.AI 10.9.0のRoutingとFailoverを使うと、複数モデルや複数ProviderをIChatClientの共通インターフェース内で切り替えられます。
最初から複雑なSemantic Routingや動的ヘルス判定を実装する必要はありません。次の順序で導入すると、問題を切り分けやすくなります。
- Version 10.9.0と依存関係を固定する
RoutingChatClient.Createで単純なルールベースRoutingを実装する- 各ルートへ
OrderedFailoverChatClientを追加する - Streaming開始前後の障害をテストする
- Provider別の成功率と遅延を計測する
- 実際のプロンプトデータが蓄積してからSemantic Routingを追加する
- 必要になった段階で
FailoverChatClientによる独自ポリシーへ拡張する
特に重要なのは、Experimental APIだから利用しないことではなく、変更の影響範囲を限定した状態で利用することです。Version固定、Adapterへの分離、障害テスト、観測性をそろえれば、API変更へ備えながら、モデルのコスト最適化とProvider障害への耐性を同時に高められます。

コメント