Yazılım Mimarileri ve Tasarım Desenleri Üzerine

Semantic Kernel & SignalR İle Birlikte Canlı AI Yanıtlarını Gösteren Kendi Chat Uygulamamızı Yazalım

Merhaba,

Malumunuz, günümüz, anlık olarak yazılımsal ilkelerin sınırlarını aşan eşsiz devrimlerin söz konusu olduğu ilginç bir tarihsel döneme şahitlik etmektedir. Artık yazılımlar, salt algoritmalara dayalı ve önceden tarif edilebilir işleyiş mantıklarından ziyade kompleks bir altyapıya ve şuurumsu bir davranışa sahip yapay zekâlardan meydana gelmekte ya da hiç yoktan ucundan kıyısından yapay zeka desteğiyle varlıklarına, dünden ciddi farkla bu gün devam ederek geleneksel dönemlere nazaran radikal bir evrimleşme sürecinde ilerlemektedirler. Evet, bu bir dönüşümdür. Belki dışardan bakınca zahiren gibi gözükebilir ama esasında olayın kimyası dönüşmekte ve dönüşürken de ister istemez birçok teamül ile birlikte alışkanlıklar da değişmektedir. Bu dönüşüm; sadece yazılımların gereksinimlerini değiştirmekle kalmamakta, bir yandan da biz geliştiricilerin problem çözme biçimlerine de dokunuş yaparak, şekillendirmektedir.

Artık bir yazılım geliştiricisi için mesele yalnızca belirli algoritmaları tasarlamak ve inşa etmekten ibaret değildir. Aynı zamanda bu algoritma süreçlerindeki verileri yönetecek, işleyecek ve özellikle karar süreçlerini yönlendirecek yapay zekâ modellerini eğitmek ve dinamik sistemler inşa etmek gibi çok boyutlu bir sürecin zanaatkarı olması ve klasik tekniklerin ötesine geçmesi gerekmektedir. Evet, şahsen benim bizlerden beklentim budur. Bu geçiş, yakında ortalama bir yazılımda olmazsa olmaz denebilecek yapay zekâ sohbet uygulamalarının niteliksel olarak yazılımlara dahil edilmesiyle kendini mutlak gösterecektir. Kullanıcı deneyimini iyileştirmek için AI modelleriyle olan etkileşim sonucunda cevapların ‘canlı’ olarak ekrana yansıtılması büyük fark yaratacak ve birçok yazılımcıdan beklenen beceri olarak karşımıza çıkacaktır. Bizler bu içeriğimizde bu farkı .NET geliştiricileri olarak nasıl kendi uygulamalarımıza katabileceğimizi inceleyecek ve canlı AI yanıtlarını web uygulamasında yayınlayan kendi chat uygulamamızın nasıl geliştirileceğine odaklanacağız.

İçeriğimiz sürecinde OpenRouter üzerinden Google: Gemini Pro 2.0 Experimental (free) modelini kullanıyor olacağız.

OpenRouter, farklı yapay zekâ modellerine tek bir API üzerinden erişim sağlayan ve böylece özellikle yapay zekâ destekli uygulamalar geliştirenler için büyük kolaylıklar sunan bir yönlendirme platformudur. Bu platform sayesinde OpenAI, Anthropic, Mistral, Google Gemini, Meta vs. gibi farklı LLM provider’larını tek bir API çağrısıyla kullanabilir ve olası model değiştirme ihtiyaçlarına karşın kodu baştan yazmaktansa OpenRouter API parametresine küçük bir dokunuşta bulunarak anında istenilen modele geçiş yapılabilir. Ayrıca daha pahalı bir model yerine benzer işlev gören uygun fiyatlı alternatifleri de deneyerek maliyet optimizasyonu yapılabileceği gibi, bir yandan da farklı modellerin güçlü ve zayıf yönlerini merkezi bir şekilde test ederek projenizin gereksinimlerine uygun olanı seçip performansta artış ile birlikte esneklikte sağlanabilmektedir.

OpenRouter’ın sunmuş olduğu merkezi LLM hizmeti sayesinde kullanılan AI modelinde bir çökme durumu meydana gelir ya da kullanılamamazlık söz konusu olursa direkt platform üzerinden alternatif bir modelle hızlıca işlevselliğe devam edebilir ve böylece servis sürekliliğini de olabildiğince garanti altına alabilirsiniz. Evet, tüm bunlar tek bir API anahtarı ile gerçekleştirilmektedir.

Özetle; OpenRouter, yapay zekâ modellerinin uygulama kapsamında oldukça az maliyetle kolayca yönetilmesine ve sistem sürekliliğinin sağlanmasına imkan veren oldukça değerli bir platformdur.

Semantic Kernel İle SignalR Uygulaması

Evet, artık örnek çalışmamıza geçebiliriz… Bunun için aşağıdaki her bir basamağın adım adım takip edilmesi yeterli olacaktır. O halde hadi başlayalım…

Artık inşa ettiğimiz bu temel üzerine Semantic Kernel’ın yeteneklerinden faydalanarak belli başlı geliştirmelerde bulunabiliriz. Şimdi gelin bunla ilgili bir kaç dokunuşta bulunalım…

Chat’e History Object İle Memory Özelliği Ekleme

AI modelleriyle gerçekleştirilmiş bir chat uygulamasının olmazsa olmazı konunun bağlamının hatırlanmasıdır. Evet, hatırlarsanız eğer Semantic Kernel Nedir? (DeepSeek R1 Eşliğinde .NET Açısından Derinlemesine Değerlendirelim) başlıklı makalemizde History object’i ile context’i chat sürecinde memory’de tutabileceğimizi ve böylece önceki mesajlarında AI modeli tarafından hatırlanıp, iletişim sürecine dahil edilebileceğini ifade etmiştik. Aşağıdaki çalışmada yukarıda geliştirdiğimiz altyapıya history niteliği kazandırmakta ve böylece chat sürecinde bağlam hafızaya atılarak önceki prompt’lar ve cevapları o anki sürece dahil edilmektedir;

    public static class HistoryService
    {
        private static readonly Dictionary<string, ChatHistory> _chatHistories = new();
        public static ChatHistory GetChatHistory(string connectionId)
        {
            ChatHistory? chatHistory = null;
            if (_chatHistories.TryGetValue(connectionId, out chatHistory))
                return chatHistory;
            else
            {
                chatHistory = new();
                _chatHistories.Add(connectionId, chatHistory);
            }
            return chatHistory;
        }
    }

İlk olarak görüldüğü üzere farklı client’ları birbirlerinden ayırt edebilmek için yukarıdaki HistoryService sınıfını tasarlıyoruz. Dikkat ederseniz, bir Dictionary içerisinde Hub ile bağlantı kurmuş olan tüm client’ların connection id değerlerine karşılık bir ChatHistory nesnesi tutuyoruz. GetChatHistory metodu sayesinde de, artık hangi connection id‘ye uygun History nesnesi lazımsa onu elde ediyoruz.

Devamında ise AIService‘i aşağıdaki gibi bu History yapılanmasıyla tekrardan restore edersek eğer;

    public class AIService(IHubContext<AIHub> hubContext, IChatCompletionService chatCompletionService)
    {
        public async Task GetMessageStreamAsync(string prompt, string connectionId, CancellationToken? cancellationToken = default!)
        {
            var history = HistoryService.GetChatHistory(connectionId);

            history.AddUserMessage(prompt);
            string responseContent = "";
            await foreach (var response in chatCompletionService.GetStreamingChatMessageContentsAsync(history))
            {
                cancellationToken?.ThrowIfCancellationRequested();

                await hubContext.Clients.Client(connectionId).SendAsync("ReceiveMessage", response.ToString());
                responseContent += response.ToString();
            }
            history.AddAssistantMessage(responseContent);
        }
    }

Evet, artık bu çalışma neticesinde chat sürecinde client’lar arasındaki farkı gözeterek konunun bağlamını AI modele hatırlatabilmekteyiz.

Plugin İle Chat’e Özelleştirilmiş Davranış Ekleme

Semantic Kernel’ın en büyük özelliklerinden birisi plugin oluşturarak AI modellerinin davranışlarını uygulamamızın gereksinimlerine göre özelleştirebilmemizi sağlamasıdır. Bunu ilgili Semantic Kernel makalemizde özellikle vurguladığımızı anımsayacaksınız. Oluşturduğumuz plugin’ler sayesinde AI modele gelen prompt’lara karşın üretilecek cevaplarda otomatik bir şekilde bu eklentileri devreye sokarak hem AI modelin kapsamını belirleyebiliyor, hem de vereceği cevabı manipüle edebilme şansı elde ediyoruz. Yeter ki, bu özelliği kullanılan AI modeli destekliyor olsun. Evet, bizim bu içeriğimizde kullandığımız Google: Gemini Pro 2.0 Experimental (free) AI modelimiz prompt içeriğine uygun olarak otomatik plugin devreye sokabilme desteğine sahiptir. Şimdi gelin buna özel bir plugin oluşturalım ve ardından AI model’i otomatik bir şekilde plugin’i kullanacak şekilde yapılandıralım;

Öncelikle plugin’i aşağıdaki gibi tasarlayalım.

    public class CalculatorPlugin
    {
        [KernelFunction("add")]
        [Description("İki sayısal değer üzerinde toplama işlemi gerçekleştirir.")]
        [return: Description("Toplam değeri döndürür.")]
        public int Add(int number1, int number2)
            => number1 + number2;
    }

Evet, basit bir toplama işlemi. Örnek verebilmek için gidip tulumbadan su çekmemi beklemiyordunuz herhalde 🙂

Şimdi bu plugin’i Semantic Kernel aracılığıyla uygulamaya dahil edelim.

.
.
.
builder.Services
    .AddKernel()
    .AddOpenAIChatCompletion(
        modelId: "google/gemini-2.0-pro-exp-02-05:free",
        openAIClient: new OpenAIClient(
            credential: new ApiKeyCredential("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"),
            options: new OpenAIClientOptions
            {
                Endpoint = new Uri("https://openrouter.ai/api/v1")
            })
    )
    .Plugins.AddFromType<CalculatorPlugin>();

Burada ufak ama gözden kaçabilecek bir noktaya dikkatinizi çekmek istiyorum. Oluşturulan plugin’i sisteme dahil edebilmek için .Plugins.AddFromType<CalculatorPlugin>() kodunu yazmamız gerekmektedir. Ee bunu da yazabilmek için AddOpenAIChatCompletion metodunu AddKernel() metodu üzerinden yapılandırmamız gerekmektedir. Aksi taktirde Plugins property’sine erişim söz konusu olmayacaktır.

Uygulamaya plugin’i ekledikten sonra AIService‘de aşağıdaki çalışmayı gerçekleştirerek chat sürecine de eklentiyi dahil edelim ve prompt içeriğine göre otomatik tetiklenebileceğinin yapılandırmasını gerçekleştirelim;

    public class AIService(IHubContext<AIHub> hubContext, IChatCompletionService chatCompletionService, Kernel kernel)
    {
        public async Task GetMessageStreamAsync(string prompt, string connectionId, CancellationToken? cancellationToken = default!)
        {
            OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
            {
                FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
            };

            var history = HistoryService.GetChatHistory(connectionId);

            history.AddUserMessage(prompt);
            string responseContent = "";
            await foreach (var response in chatCompletionService.GetStreamingChatMessageContentsAsync(history, executionSettings: openAIPromptExecutionSettings, kernel: kernel))
            {
                cancellationToken?.ThrowIfCancellationRequested();

                await hubContext.Clients.Client(connectionId).SendAsync("ReceiveMessage", response.ToString());
                responseContent += response.ToString();
            }
            history.AddAssistantMessage(responseContent);
        }
    }

Burada da dikkat edilirse eğer OpenAIPromptExecutionSettings referansı üzerinden eklentilerin otomatik çağrılabileceği yapılandırılmakta ve bu, GetStreamingChatMessageContentsAsync metodunun executionSettings parametresine verilerek AI modelinin davranışı şekillendirilmektedir. Ayrıca burada yine küçük bir detay var ki, o da, uygulamada kullanılan Kernel referansı dependency injection ile IoC container’dan talep edilmekte ve yine aynı metodun kernel parametresine verilmektedir. Aksi taktirde bu çalışma için türlü hatalar söz konusu olabilmektedir.

Şimdi yaptığımız bu çalışmayı derleyip teste tabi tutarsak aşağıdaki ekran görüntüsünde olduğu gibi prompt’un mahiyetine göre ilgili eklentinin çağrıldığını gözlemlemiş olacağız;Dikkat ederseniz ilk prompt normal nazik bi soruyken, ikincisi ise yarı sayısal yarı metinsel bir toplama sorusudur. İşte bu soruda AI modeli tarafından eklentimiz devreye sokulup, çalıştırılmaktadır. Bunu da break point’in tetiklenmesinden anlamaktayız.

Ee bu kadar kâfi diyelim 🙂

Evet, görüldüğü üzere Asp.NET Core’da geliştirdiğimiz bir Web API aracılığıyla Semantic Kernel üzerinden rahatlıkla Google: Gemini Pro 2.0 Experimental (free) AI modeliyle entegrasyon gerçekleştirmiş ve bir yandan da kullanıcıdan gelen prompt’lara karşı üretilen cevapların stream’ini tüketerek SignalR eşliğinde bu mesajları bir UI uygulamasına aktararak doğal bir chat havası oluşturmuş bulunuyoruz. Tüm bu süreçte history nesnesiyle chat’in bağlamını client’a özel memory’e almış ve bir yandan da custom plugin oluşturarak prompt’un mahiyetine göre otomatik devreye girecek şekilde uygulamamızın gereksinimlerini gözeterek manipülasyonlar ve kapsam sınırlaması gerçekleştirmiş bulunuyoruz.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek çalışmaya aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/SemanticKernel.SignalR.Streaming.Handler.Example

Exit mobile version