Microsoft.Extensions.AI için Routing ve Failover
Microsoft.Extensions.AI, yapay zeka istemcileri arasında yönlendirme (routing) ve yedekleme (failover) yapmak için yeni deneysel türler getiriyor. Bu türler; birden fazla model veya sağlayıcı arasında istekleri içerik, sağlık durumu veya maliyet gibi ölçütlere göre dağıtmayı ve bir sağlayıcı yanıt vermediğinde başka bir istemciye geçmeyi IChatClient soyutlaması üzerinden mümkün kılıyor. Aşağıda bu yapıların ne yaptığına, hangi durumlarda tercih edilebileceğine ve tasarım sınırlarına kaynaktaki bilgilere sadık kalarak bakacağız.
Yeni tipler ve genel çerçeve
Kaynağa göre dört yeni deneysel tip tanımlanıyor:
- RoutingChatClient: Her istekte başka bir istemci seçip çağrıyı ona yönlendiren temel
IChatClient. - SemanticRoutingChatClient: İçeriğe göre yönlendirme yapan, uygulama tarafından sağlanan örnek ifadelerle embedding benzerliğini karşılaştıran
RoutingChatClienttürevi. - FailoverChatClient: Bir denemede hata olursa yeniden seçim yapıp tekrar deneyen soyut sınıf. Farklı failover stratejileri için temel oluşturuyor.
- OrderedFailoverChatClient: Kendisine verilen istemci listesini sırayla deneyen somut
FailoverChatClientuygulaması.
RoutingChatClient: temel yönlendirme
RoutingChatClient, her istekte SelectClientAsync çağrısı yapar, dönen istemciye isteği aktarır ve yanıtı çağırana iletir. En basit kullanım RoutingChatClient.Create ile bir geri çağrı vermektir:
var router = RoutingChatClient.Create((context, ct) =>
new(isComplexRequest(context) ? powerfulClient : cheapClient));
Durum tutan veya daha gelişmiş politikalar için sınıftan türetip SelectClientAsync metodu geçersiz kılınabilir:
class MyRouter : RoutingChatClient
{
protected override ValueTask<IChatClient> SelectClientAsync(
RoutingContext context, CancellationToken ct)
{
//...
}
}
Her GetResponseAsync veya GetStreamingResponseAsync çağrısı için istek mesajlarını ve ChatOptions‘ın bir kopyasını içeren yeni bir RoutingContext oluşturulur. Çağıranın kendi opsiyon nesnesi asla hedef istemciye verilmez; çağrı başladıktan sonra dışarıda yapılan değişiklikler de yansımaz. Bu tasarım opsiyonları iki katmana ayırır:
- İstek düzeyi opsiyonlar
context.ChatOptions‘ta yaşar ve olası yeniden denemeler dahil tüm istek boyunca kalıcıdır. - Rota düzeyi opsiyonlar istemcinin kendisine, tipik olarak istek opsiyonlarını klonlayıp kendi değerlerini üstüne uygulayan bir
ConfigureOptionsChatClientsarmalayıcısına aittir.
SemanticRoutingChatClient: anlama göre yönlendirme
Aurelio Labs’in semantic router projesinden esinlenen SemanticRoutingChatClient, mesajın anlamına göre yönlendirme yapar. Her istemci için örnek ifadeler tanımlanır, çalışma zamanında son kullanıcı mesajı embedding’e çevrilir ve bu profillere karşı eşleştirilir. Eşik değerini aşan en yüksek skorlu istemci kazanır; eşiği kimse aşamazsa defaultClient devreye girer.
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);
Profil embedding’leri tembel biçimde üretilir ve önbelleğe alınır: İlk yönlendirilen istekte tüm profil ifadeleri tek bir toplu çağrıyla embedding’e dönüştürülür, sonraki isteklerde yalnızca gelen mesajın embedding’i hesaplanır.
Önemli seçenekler
scoreThreshold: Profilli bir istemcinin seçilmesi için gereken minimum skor. AşılamazsadefaultClient‘a düşer.topK: Skorlar toplanırken dikkate alınacak en iyi eşleşme sayısı. Varsayılan1.scoreAggregation:MeanveyaSum. Geçerli eşik değerlerini etkiler.leaveOpen: Varsayılan olarakSemanticRoutingChatClientistemcilere ve embedding üreticisine sahip çıkar ve dispose eder; devre dışı bırakmak içintrueverilir.
Kaynağa göre bir başka makul başlangıç noktası, Aurelio Labs’in kendi varsayılanı olan topK: 5 ve Mean birleştirmesidir. Birden fazla eşleşmenin ortalamasını almak, tek bir yakın eşleşmeye bel bağlamaktan daha kararlıdır; Mean skorları -1 ile 1 ölçeğinde tutar, böylece topK ne olursa olsun 0.3 gibi bir eşik aynı anlama gelir. Her istemciye beş eşleşmenin gerçekten mümkün olacağı kadar örnek verilmesi öneriliyor.
Her turda yeniden yönlendirmeden önce
Yönlendirmeyi her mesajda tekrar yapmak her zaman doğru değildir. Kaynağın altını çizdiği iki neden var:
- Reasoning artefaktları: Muhakeme yapan modeller, muhakemesini sağlayıcıya özgü, sonraki turda geri verilmesi gereken şifreli içerik veya sağlayıcının kendi oturumuna bağlı bir devam belirteci olarak dönebilir. Konuşma ortasında sağlayıcı değiştirmek bu bağlamı çöpe atar.
- Prompt cache maliyeti: Yeni bir sağlayıcı veya aynı sağlayıcıda farklı bir model, sıcak bir önbellek olmadan taze bir istem demektir; her geçişte prefix’in tam hesap maliyetini yeniden ödersiniz.
Bir konuşma aynı sınıflandırmada kalacaksa, bunu bir kez belirleyip sabitlemek her turda yeniden yönlendirmekten daha mantıklıdır.
FailoverChatClient: yeniden deneme mantığı
FailoverChatClient, RoutingChatClient‘ı bir yeniden deneme döngüsüyle genişletir. Seçilen istemci, herhangi bir stream çıktısı çağırana ulaşmadan önce başarısız olursa yeniden SelectClientAsync çağrılır ve tekrar denenir. Çıktı akmaya başladıktan sonra ise hata terminal hale gelir; akış ortasında kurtarma yoktur. Stream olmayan yanıtlarda durum daha basit: Yarıda commit edilecek bir çıktı olmadığından deneme ya yanıt döndürür ya da tamamen başarısız olur.
Türetilen sınıflar SelectClientAsync‘i uygulayarak sıradaki istemciyi verir; her denemeyi gözlemlemek için OnRoutingUpdateAsync geçersiz kılınır. Bu güncelleme başarı, başarısızlık veya vazgeçme olsun her istemci çağrısından sonra bir FailoverChatClientAttempt ile tetiklenir. Bu nesne şunları içerir:
Client: ÇağrılanIChatClient.Duration: İstemcinin aktif olarak çağrıldığı süre (stream için çağıranın işleme süresi hariç).Exception: Gözlemlenen istisna, varsa.ResponseCompleted: Yanıtın başarıyla tamamlanıp tamamlanmadığı.OutputCommitted: Herhangi bir stream güncellemesinin çağırana ulaşıp ulaşmadığı.TimeToFirstUpdate: Uygunsa ilk stream güncellemesine kadar geçen süre.
Duration ve TimeToFirstUpdate bilgileri sağlayıcı performansını zaman içinde izlemek için değerlidir: Yavaş bir sağlayıcıyı devre dışı bırakmak, adayları geçmiş gecikmeye göre puanlamak veya gözlemlenebilirlik hattına beslemek gibi. Hook yalnızca hatalarda değil, her denemede tetiklenir; başarılar da veri setine yansır.
Metoda geçen isTerminal bayrağı, güncelleme döndükten sonra temel sınıfın tekrar seçim yapıp yapmayacağını söyler. Terminal olmayan bir güncelleme ardından yeni denemeler geleceği anlamına gelir; override içinde yapılan durum değişiklikleri sonraki SelectClientAsync çağrısında görünür.
Kendi kodunuzdaki istisnalar isteği rapor üretmeden sonlandırır. SelectClientAsync fırlatırsa hiçbir güncelleme oluşturulmaz. OnRoutingUpdateAsync terminal olmayan bir güncellemede fırlatırsa yeniden deneme yapılmaz ve başka güncelleme yayınlanmaz; her iki durumda da temizlik için geri arama alamayacağınızdan, istek başına tuttuğunuz durumu fırlatmadan önce serbest bırakmanız gerekir.
MaximumAttemptsPerRequest istek başına toplam çağrı sayısını sınırlar; isteğin iptal token’ı iptal edildiğinde yeniden seçim yapılmaz.
OrderedFailoverChatClient
Kutudan çıkar çıkmaz kullanılabilen OrderedFailoverChatClient, kendisine verilen sıralı listeyi baştan sona dener: İlki başarısız olursa ikinciye geçer ve böyle devam eder. Tüm istemciler başarısız olduğunda son istisna yeniden fırlatılır.
var failover = new OrderedFailoverChatClient([primaryClient, backupClient, lastResortClient]);
Aynı istemci listede birden fazla kez yer alabilir; her pozisyon için ayrı çağrı yapılır. MaximumAttemptsPerRequest ile listeyi erken kesebilirsiniz. Varsayılan olarak OrderedFailoverChatClient istemcilere sahip çıkar ve dispose eder; devre dışı bırakmak için leaveOpen: true verilir.
Her istek için taze bir RoutingContext oluşturulduğundan, bu nesne kendi başına istek kapsamlı bir anahtar gibi kullanılabilir; SelectClientAsync ile OnRoutingUpdateAsync arasında istek kimliği icat etmeden durum paylaşılabilir.
Uygulama örüntüleri
Sabit (sticky) seçim
Sticky yönlendirmede ChatOptions.ConversationId anahtarını kullanmak cazip gelebilir; ancak bu ID sağlayıcının durum tutan konuşmasına aittir ve başka istemciye taşınmayabilir. Uygulamanın kendine ait bir oturum ID’sini kullanmak daha doğrudur. Rotalara ad verilip uygulamanın oturum ID’si ChatOptions.AdditionalProperties üzerinden geçirilir ve seçilen isim örneğin IDistributedCache‘te (Redis gibi) saklanır:
var routes = new Dictionary<string, IChatClient>
{
["fast"] = fastClient,
["deep"] = deepClient,
};
var options = new ChatOptions
{
AdditionalProperties = new() { ["routing-session-id"] = sessionId },
};
class StickyRouter : FailoverChatClient
{
private readonly IReadOnlyDictionary<string, IChatClient> _routes;
private readonly ConcurrentDictionary<RoutingContext, string> _pending = new();
private readonly IDistributedCache _cache;
public StickyRouter(IReadOnlyDictionary<string, IChatClient> routes, IDistributedCache cache)
{
_routes = routes.ToDictionary(r => r.Key, r => r.Value);
_cache = cache;
MaximumAttemptsPerRequest = 1;
}
protected override async ValueTask<IChatClient> SelectClientAsync(
RoutingContext context, CancellationToken ct)
{
string route = await _cache.GetStringAsync(CacheKey(context), ct) ? Classify(context);
_pending[context] = route;
return _routes[route];
}
protected override async ValueTask OnRoutingUpdateAsync(
RoutingContext context, FailoverChatClientAttempt attempt, bool isTerminal, CancellationToken ct)
{
if (_pending.TryRemove(context, out string? route) && attempt.ResponseCompleted)
{
await _cache.SetStringAsync(CacheKey(context), route, ct);
}
}
private static string CacheKey(RoutingContext context) =>
context.ChatOptions?.AdditionalProperties?.TryGetValue("routing-session-id", out string? id) == true
? $"chat-route:{id}"
: throw new InvalidOperationException("A routing session ID is required.");
}
Burada Classify bir istemci değil bir rota adı döndürür; böylece önbellek, sınıflandırıcı ve sabitleme aynı string üzerinden konuşur. Sabitleme yalnızca yanıt başarıyla tamamlandığında yapılır, ilk turda hata veren bir istemci oturuma yapışıp kalmaz. Enumerasyondan erken çıkan bir çağıran için de ResponseCompleted false kalır ve hiçbir şey sabitlenmez.
Bu davranışın doğru elde edilmesi için router FailoverChatClient‘tan türer, ancak yeniden deneme döngüsü istenmediğinden MaximumAttemptsPerRequest = 1 ile devre dışı bırakılır. Tamamlanma takibi failover tipinde yaşadığı için denemeleri gözlemlemek, kullanılmayacak retry mekaniğini de miras almak anlamına gelir; kaynak, bu iki sorumluluğun ileride ayrılmasının faydalı olacağını belirtiyor.
Tek model, farklı reasoning seviyeleri
Prompt önbelleğini korumak isteyenler için tek modeli farklı reasoning efor seviyeleriyle sarmalayıp bu sarmalayıcılar arasında yönlendirmek etkili bir yol:
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();
var router = RoutingChatClient.Create((context, ct) =>
new(isComplexRequest(context) ? highEffort : lowEffort));
Her sarmalayıcı ayrı bir IChatClient olduğu için yönlendirme politikaları düşük ve yüksek eforu bağımsız izleyebilir.
Diğer örüntüler
- Gecikme ve sağlık bilinçli yönlendirme:
Duration,TimeToFirstUpdateve hataları kullanarak istemcileri gözlemlenen performansa göre sıralamak. Yeni istemciler için tohum tahminler veya bağımsız problar gerekir. - Devre kesici ve soğuma: Sağlıksız istemcileri seçimden çıkarıp bir süre sonra tekrar denemek. Timeout için saniyeler yetebilirken kimlik doğrulama hatası manuel müdahale gerektirebilir.
- Maliyet bilinçli yönlendirme: Rutin istekler için ucuz adayları tercih etmek, pahalı modelleri zor işlere saklamak veya oturum/tenant bazında bütçe uygulamak.
- Yetenek bilinçli yönlendirme: Görüntü, araç çağırma, yapılandırılmış çıktı, bağlam uzunluğu gibi gereksinimlere göre istemcileri filtrelemek.
- Bölge bazlı yönlendirme: Ağ gecikmesini azaltmak için yakın deployment’ı tercih etmek veya veri ikametgahı gereksinimlerini karşılayan bir bölgeyi seçmek.
- Router kompozisyonu: Her router bir
IChatClientolduğundan maliyet veya yetenek bilinçli bir router, bir failover zincirinin içine yerleşebilir veOnRoutingUpdateAsyncher denemeyi kaydedebilir.
Sınırlar
RoutingChatClient seçimi her zaman çağrıdan önce yapar: İstek başına önden seçilmiş tek bir istemci. Bu, bazı yönlendirme paradigmalarını kapsam dışında bırakır (bunlar Microsoft.Extensions.AI‘de imkansız değildir, sadece RoutingChatClient‘ın işi değildir):
- Model cascading:
FailoverChatClientyalnızca hata durumunda yeniden seçim yapar, başarılı ama kalite eşiğinin altındaki yanıtlar için değil. - Ensemble routing: Birden fazla istemciye dağıtıp yanıtları birleştirmek/oylamak için çoklu çağrı yapan bir istemci gerekir.
- Hedging: Birden fazla istemciyi yarıştırıp ilk cevap vereni almak, ekstra maliyet karşılığında tail latency’yi düşürür; bu da fan-out gerektirir.
Router’ın pipeline’daki yeri de önemli. Seçim istek başına bir kez yapıldığı için FunctionInvokingChatClient etrafında sarılı bir router, araç çağırma döngüsünün her iterasyonu boyunca aynı istemciyi kullanır. İlk reasoning’i güçlü bir modele, araç sonucu turlarını ucuz bir modele göndermek istiyorsanız router’ı FunctionInvokingChatClient etrafına değil içine koymanız gerekir.
Başlarken
Kaynağa göre RoutingChatClient, RoutingContext, FailoverChatClient, FailoverChatClientAttempt, OrderedFailoverChatClient ve SemanticRoutingChatClient tipleri Microsoft.Extensions.AI 10.9.0 sürümünde yer alıyor. Tümü [Experimental] olarak işaretli ve MEAI001 tanı kimliğiyle geliyor:
dotnet add package Microsoft.Extensions.AI
Microsoft ekibi özellikle failover’ın yeniden deneme ve deneme limiti davranışı, OnRoutingUpdateAsync‘in her denemede bildirdikleri, çerçevenin ne kadar durum tuttuğu, SemanticRoutingChatClient‘ın skor varsayılanları ve toplama seçenekleri ile deneme başına opsiyon şekillendirmenin farklı bir istemci seçmeden ifade edilip edilemeyeceği konularında geri bildirim bekliyor. Denediklerinizi dotnet/extensions üzerinde issue veya tartışma olarak paylaşabilirsiniz.
Kaynaklar ve İleri Okuma
- Routing and Failover for Microsoft.Extensions.AI —.NET Blog (Joshua Yue)
- dotnet/extensions#7662: Bu tipleri ekleyen PR
- Microsoft.Extensions.AI dokümantasyonu
- dotnet/extensions GitHub deposu
- Routing CLI Sample
- Aurelio Labs — semantic-router
- .NET Blog
- Microsoft Agent Framework 1.0: Ajanlar Artık Ciddileşti
- Microsoft Agent Framework ile.NET’te Ajan Kurmanın İncelikleri







Yorum gönder