8月14日、Microsoft .NET Blogが「Routing and Failover for Microsoft.Extensions.AI」と題した記事を公開した。AIをプロダクションで運用する際、コスト・稼働率・レイテンシという三つの制約をどう同時に制御するかは、エンジニアリング上の現実的な課題だ。レート制限に達したとき、特定モデルが高コストすぎるとき、あるいは特定クエリには軽量モデルで十分なとき——これらを個別に作り込むのではなく、IChatClientの抽象レイヤーで統一的に扱える仕組みがMicrosoft.Extensions.AIに追加された。
なお、今回紹介する四つの型はいずれも実験的(Experimental)ステータスであり、APIは将来変更される可能性がある。プロダクション環境への採用にあたっては、この点を十分に考慮していただきたい。
四つの新しい型の全体像
追加された型は以下の四つで、それぞれが抽象クラスの継承関係を持つ。
RoutingChatClient: リクエストごとに転送先クライアントを選択する基底クラスSemanticRoutingChatClient: メッセージの意味的類似度に基づいてルーティングする実装FailoverChatClient: 失敗時に再選択するリトライループを持つ抽象クラスOrderedFailoverChatClient: リスト順にフォールバックするすぐに使える実装
RoutingChatClientとFailoverChatClientはそれぞれ抽象クラスであり、独自ロジックを組み込む際の拡張ポイントとして機能する。SemanticRoutingChatClientとOrderedFailoverChatClientは、それぞれの具体実装としてそのまま利用できる。
最も実用的:SemanticRoutingChatClient
四つの中で最もユニークなのがSemanticRoutingChatClientだ。Aurelio Labsのsemantic routerにインスパイアされており、ユーザーの最後のメッセージを埋め込みベクトルに変換し、あらかじめ登録したサンプル発話との類似度で転送先を決定する。
var router = new SemanticRoutingChatClient(
embeddingGenerator,
clientProfiles: new Dictionary<IChatClient, IReadOnlyList<string>>
{
[codingClient] = ["write code", "fix this bug", "refactor this function"],
[creativeClient] = ["write a story", "brainstorm names", "generate a poem"],
},
defaultClient: generalClient,
scoreThreshold: 0.3f);
プロファイルの埋め込みは遅延生成かつキャッシュ済みで、最初のリクエスト時に全サンプル発話をバッチ処理し、以降は受信メッセージの埋め込みとの比較のみを行う。
主要なパラメータは三つ。
scoreThreshold: これを下回るスコアのリクエストはdefaultClientに転送topK: スコア集計に使う上位一致数。デフォルトは1scoreAggregation:MeanまたはSum。topK: 5かつMeanの組み合わせがAurelio Labsのデフォルト設定でもあり、単一一致より安定する
ただし、マルチターンの会話では毎ターンの再ルーティングに注意が必要だ。推論モデルはプロバイダー固有のアーティファクトを返すことがあり、途中でプロバイダーを切り替えるとセッションが継続不能になる。また、プロバイダーを切り替えるたびにプロンプトキャッシュが無効化され、プレフィックスの計算コストが毎回発生する。長い会話は最初に分類し、その後は同じクライアントを使い続ける「Sticky selection(後述)」が現実的な対策だ。
フェイルオーバーの挙動
FailoverChatClientは、ストリーミング出力が呼び出し元に届く前に失敗した場合のみリトライを行う設計だ。一度でも出力が呼び出し元に流れ始めると、その失敗は回復不能なエラーとして扱われる。この制約はトレードオフの結果であり、部分的に届いたストリーミングレスポンスを透過的に巻き戻すことは一般に不可能なため、安全側に倒した設計といえる。
OnRoutingUpdateAsyncフックは各試行後に発火し、FailoverChatClientAttemptとして以下の情報を受け取れる。
Duration: クライアント呼び出しに費やした時間TimeToFirstUpdate: 最初のストリーミング更新までの時間Exception: 発生した例外ResponseCompleted: レスポンスが正常完了したかOutputCommitted: ストリーミング更新が呼び出し元に届いたか
DurationとTimeToFirstUpdateを記録し続けることで、低速なプロバイダーへのサーキットブレーカーや、レイテンシによるスコアリングといった高度な制御が可能になる。これらのメトリクスを時系列で蓄積すれば、プロバイダーごとの品質劣化を検知する基盤にもなりうる。
すぐに使えるOrderedFailoverChatClientは、渡したリストを順番に試し、全て失敗すると最後の例外を再スローする。
var failover = new OrderedFailoverChatClient([primaryClient, backupClient, lastResortClient]);
応用パターン:Sticky selectionとコスト対応ルーティング
記事では、マルチターン会話向けにRedisなどのIDistributedCacheでルート選択を永続化するStickyRouterの実装例が紹介されている。セッションIDをChatOptions.AdditionalProperties経由で渡し、最初のターンで分類したルート名をキャッシュに保存する。レスポンスが正常完了した場合のみキャッシュに書き込む設計のため、最初のターンで失敗したクライアントにセッションが固定されることはない。
また、同一モデルで推論の深さ(ReasoningEffort)を変えて複数のIChatClientラッパーを作り、それをルーティング対象にするパターンも紹介されている。プロバイダーを切り替えないのでプロンプトキャッシュが維持され、コストを抑えながら難易度に応じた使い分けが可能だ。
IChatClient lowEffort = baseClient.AsBuilder()
.ConfigureOptions(options =>
options.Reasoning = new ReasoningOptions { Effort = ReasoningEffort.Low })
.Build();
IChatClient highEffort = baseClient.AsBuilder()
.ConfigureOptions(options =>
options.Reasoning = new ReasoningOptions { Effort = ReasoningEffort.High })
.Build();
現時点の制約
RoutingChatClientはリクエストごとに1クライアントを選んで転送する設計であり、以下のパターンは現時点ではスコープ外だ。
- モデルカスケード: 品質が閾値未満のレスポンスをトリガーにした再選択。一次応答の評価コストと品質向上のトレードオフが伴うため、設計上の判断が必要な領域だ
- アンサンブルルーティング: 複数クライアントに同時に投げて結果をマージ・多数決。並列呼び出しのコスト増と引き換えに精度を高める手法で、現行の単一選択モデルとは設計思想が異なる
- ヘッジングリクエスト: 複数クライアントをレースさせて最初の応答を採用。レイテンシを最小化したい用途に有効だが、余剰リクエストによるコスト増が課題となる
これらの制約は現バージョン時点のものであり、実験的APIであるがゆえに今後のバージョンで拡張される可能性もある。ロードマップの変化はMicrosoft.Extensions.AIのGitHubリポジトリで追跡できる。
詳細はRouting and Failover for Microsoft.Extensions.AIを参照していただきたい。




