Microsoft.Extensions.AI 10.9.0のRoutingとFailover実装ガイド

AIアプリケーションで複数のモデルやプロバイダーを利用すると、「簡単な処理は低コストモデルへ送りたい」「障害時は別プロバイダーへ切り替えたい」といった制御が必要になります。しかし、ルーティング判定、再試行、ストリーミング中の障害処理まで個別に実装すると、コードが複雑になりがちです。

Microsoft.Extensions.AI Version 10.9.0では、モデルやプロバイダーを選択するRoutingChatClientSemanticRoutingChatClient、障害時の切り替えを行うFailoverChatClientOrderedFailoverChatClientが追加されました。

実装方針は明確です。正常時の振り分けには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は似ているように見えますが、利用目的が異なります。

比較項目RoutingFailover
判断するタイミングモデル呼び出し前モデル呼び出しの失敗後
主な判断材料入力内容、必要機能、コスト、リージョン例外、出力開始の有無、試行回数
主な目的最適なモデルやプロバイダーへ配置するサービス停止や一時障害から退避する
成功した低品質回答別モデルへ切り替えない別モデルへ切り替えない
複数モデルの同時実行行わない行わない

実務では、次のように整理すると判断しやすくなります。

  • 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を使うと、選択用のコールバックだけで単純なルーターを構築できます。

次の例では、複雑なリクエストを高性能モデルへ、それ以外を低コストモデルへ送ります。powerfulClienteconomicalClientは、あらかじめ構築した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

コンストラクターの既定値は、scoreThreshold0.3topK1、集約方法がMeanです。leaveOpenの既定値はfalseで、ルーターを破棄すると、設定したクライアントとEmbedding Generatorも破棄されます。DIコンテナーなど別の場所がライフサイクルを管理する場合は、leaveOpen: trueを指定します。(Microsoft Learn)

Semantic Routingの精度を上げる手順

Semantic Routingは、例文を数件登録しただけで完成するものではありません。次の順序で調整します。

  1. 実際に入力されたプロンプトを、期待するルートごとに分類する
  2. 表現が異なる例文を各ルートへ登録する
  3. 正解ルート付きのテストデータを用意する
  4. 誤ったルートへ送られた割合を測定する
  5. scoreThresholdtopKを調整する
  6. 本番投入後もルーティング結果を記録する

例えば「コードを書いて」だけでなく、次のような表現もコーディング用プロファイルへ含めます。

  • この処理を高速化したい
  • NullReferenceExceptionを直したい
  • APIのレスポンスをクラスへ変換したい
  • LINQの処理を読みやすくしたい

一方、「改善して」「作成して」のように用途を特定できない語句を複数ルートへ登録すると、誤判定が増えます。

公式ブログでは、十分な数の例文がある場合、topK: 5Meanの組み合わせも初期候補として示されています。ただし、最適な閾値は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を試します。

MaximumAttemptsPerRequest2なので、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やデータベースへ保存し、同じセッションでは同じルートを返す構成にします。保存する値はクライアントインスタンスではなく、fastdeepなどのルート名にすると、構成変更や再起動にも対応しやすくなります。(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応答が最後まで正常に完了したか
OutputCommittedStreaming出力が呼び出し元へ届いたか
TimeToFirstUpdate最初のStreaming更新までの時間

OnRoutingUpdateAsyncは、失敗時だけでなく、成功、失敗、途中放棄を含む各クライアント呼び出し後に実行されます。これらの値をOpenTelemetryやログ基盤へ送れば、Providerごとの成功率、遅延、Failover率を測定できます。(Microsoft for Developers)

ただし、OnRoutingUpdateAsync自身が例外を送出すると、Routing処理が停止します。ログ送信やメトリクス登録の失敗でAI応答まで失敗させないように、観測処理内の例外は適切に処理する必要があります。(Microsoft Learn)

また、OrderedFailoverChatClientsealedクラスです。OrderedFailoverChatClientを継承して選択ロジックを変更することはできません。独自の試行順序やヘルス判定が必要な場合は、FailoverChatClientから実装します。(Microsoft Learn)

leaveOpenとDisposeの扱いに注意する

SemanticRoutingChatClientOrderedFailoverChatClientは、既定では内部のクライアントを所有し、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や動的ヘルス判定を実装する必要はありません。次の順序で導入すると、問題を切り分けやすくなります。

  1. Version 10.9.0と依存関係を固定する
  2. RoutingChatClient.Createで単純なルールベースRoutingを実装する
  3. 各ルートへOrderedFailoverChatClientを追加する
  4. Streaming開始前後の障害をテストする
  5. Provider別の成功率と遅延を計測する
  6. 実際のプロンプトデータが蓄積してからSemantic Routingを追加する
  7. 必要になった段階でFailoverChatClientによる独自ポリシーへ拡張する

特に重要なのは、Experimental APIだから利用しないことではなく、変更の影響範囲を限定した状態で利用することです。Version固定、Adapterへの分離、障害テスト、観測性をそろえれば、API変更へ備えながら、モデルのコスト最適化とProvider障害への耐性を同時に高められます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次