“Gelişmiş Yapay Zeka Teknikleri: .NET’te Retrieval Augmented Generation(RAG) Kullanımı”

Turgay Kaya
LCW Digital

--

Öncelikle RAG kavramına tamamen başlamadan önce yapay zekâda bilinmesi gereken bazı kavramlar var.

Large Language Model (LLM): yapay zeka ile ilgili örneklerde ve makalelerde sıkça karşımıza çıkan yapay zeka destekli derin öğrenme modelleridir. Örnekler arasında OpenAI, Azure OpenAI gibi şu anda aktif olarak kullanılan uygulamaları sayabiliriz.

Prompt : Yapay zeka destekli bir sohbet robotuna verilen komut veya soru anlamına gelir. Daha açık ve anlaşılır promptlar, modelin istenen amaca uygun ve daha kaliteli yanıtlar üretmesine yardımcı olur.

Yapay zeka örneğinden yola çıkarak diğer kavramlara da değinebiliriz. Öncelikle, bir .NET 8 uygulaması oluşturalım.

(.Net6 ve .Net7'de için bazı ayarlamalar gerekebilir.)

dotnet new console -o chatapp
cd chatapp

Yeni bir proje oluşturduk ve şimdi yeni bir paket öğreniyoruz.

Semantic.Kernel : Bu paket mevcut kodları AI modellerine kolayca entegre etmeyi sağlamaktadır. Open source olan bu paket c#, phyton ve java kodlarınızı OpenAI, Azure OpenAI gibi modellerle birleştirerek soru girdi ve çıktı süreçlerini otomatize etmektedir.

Paketimizi yükleyelim.

dotnet add package Microsoft.SemanticKernel

Projemize başlarken bir LLM seçmemiz gerekiyor. OpenAI veya Azure OpenAI ikisi ile de çalışmaya müsade ediyor paketimiz. Ben OpenAI LLM ile ilerlemeyi tercih ediyorum.

LLM seçtikten sonra bize ihtiyaç olan 3 parçalık kısım var.

  1. API KEY
  2. Sohbet Modeli : gpt-3.5-turbo-0125

Bu konuda https://platform.openai.com/docs/overview openai sitesini inceleyebilirsiniz.

3. Embedding modeli : text-embedding-3-small

Bu kısma da sonlarda değinmiş olacağız.

Sohbet modelleri belirli bir token hesaplaması ile çalışır seçtiğimiz model 16k gibi bir token ile çalışıyor. Prompt ile sorular sorduğumuz da harcadığımız token değerli hale gelmekte. Bu yüzden sorunuzun token kısmını öğrenmek için https://platform.openai.com/tokenizer kullanabilirsiniz.

İpucu : Türkçe sorular yerine ingilizce soruyu tercih ederseniz daha az token tüketmiş olacaksınız.

Projemize geçelim.

using Microsoft.SemanticKernel;

string apikey = Environment.GetEnvironmentVariable("AI:OpenAI:APIKey")!;

// Initialize the kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion("gpt-3.5-turbo-0125", apikey)
.Build();

// Q&A loop
while (true)
{
Console.Write("Question: ");
Console.WriteLine(await kernel.InvokePromptAsync(Console.ReadLine()!));
Console.WriteLine();
}

Kodumuzu incelersek, API anahtarı (apikey) kısmını environment variable içinde tutarak güvenli hale getirdik. Kernel ile bağlantımızı kurduk ve gördüğünüz gibi basit bir şekilde sohbet modelimizi, LLM ve apikey ile hızlıca bağladık. Burada gördüğünüz while döngüsünde aslında bir ifade yer almaktadır. await kernel.InvokePromptAsync(Console.ReadLine()!) kısmı LLM ile olan iletişimin tamamıdır. Kullanıcıdan soruyu alır ve cevap döner.

Bu örnekte gördüğümüz gibi, Büyük Dil Modellerine (LLM) sorduğumuz sorulara cevaplar alabiliyoruz. Ancak, ‘şu an saat kaç?’ gibi bir soru sorduğumda, modelin tepkisi nasıl olur?

Eğer ‘şu anki saati’ sormak gibi zamanla ilgili bir soru yöneltirsek, Büyük Dil Modelleri (LLM) doğrudan ve güncel bir cevap veremeyebilir. Ancak, bu tür zaman odaklı sorular için çözüm yolları mevcuttur. İşte bu noktada, kernel eklentisi gibi araçlar devreye girer.

using Microsoft.SemanticKernel;

string apikey = Environment.GetEnvironmentVariable("AI:OpenAI:APIKey")!;

// Initialize the kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion("gpt-3.5-turbo-0125", apikey)
.Build();

// Create the prompt function as part of a plugin and add it to the kernel.
// These operations can be done separately, but helpers also enable doing
// them in one step.
kernel.ImportPluginFromFunctions("DateTimeHelpers",
[
kernel.CreateFunctionFromMethod(() => $"{DateTime.UtcNow:r}", "Now", "Gets the current date and time")
]);

KernelFunction qa = kernel.CreateFunctionFromPrompt("""
The current date and time is {{ datetimehelpers.now }}.
{{ $input }}
""");

// Q&A loop
var arguments = new KernelArguments();
while (true)
{
Console.Write("Question: ");
arguments["input"] = Console.ReadLine();
Console.WriteLine(await qa.InvokeAsync(kernel, arguments));
Console.WriteLine();
}

Kesinlikle, fonksiyonlar ve eklentiler (plugin’ler) kullanarak, ‘şu anki saat’ gibi zamanla ilgili sorulara doğru cevaplar sağlayabiliriz. Bu tür araçlar, Büyük Dil Modellerinin (LLM) sınırlamalarını aşmamıza ve kullanıcıların ihtiyaçlarına daha iyi hizmet etmemize olanak tanır. Örneğin, bir saat fonksiyonu veya eklentisi, kullanıcının bulunduğu zaman dilimine göre güncel saati hesaplayıp sağlayabilir. Bu, uygulamanın işlevselliğini genişletir ve kullanıcı deneyimini zenginleştirir.

Evet, şu ana kadar yaptığımız işlemlerle basit bir uygulama geliştirdik ve ‘şu anki saat’ gibi sorulara daha iyi cevaplar alabildik. Ancak, bu süreçte sohbet geçmişini kaydetme işlevini henüz gerçekleştirmedik. Bu özellik, kullanıcı deneyimini iyileştirebilir ve sohbetin bağlamını daha iyi anlamamıza yardımcı olabilir. Eğer sohbet geçmişini tutmak istiyorsanız, bu özelliği eklemek için bir sonraki adımları planlayabiliriz.

History İşleyişi: AI modeli, geçmişteki sohbetlerden elde ettiği bilgileri kullanarak yeni sorulara yanıt verir. Örneğin, model daha önce .NET 5.0'ın çıkış tarihini belirtmişse, bu bilgiyi kullanarak .NET 6.0'ın muhtemel çıkış tarihini tahmin edebilir. Bu süreç şu adımları içerir:

1. Geçmişteki Yanıtları İnceleme: Model, .NET 5.0 hakkında daha önce verdiği yanıtları history’den çeker.
2. Bilgi Güncelleme: Model, .NET’in yayınlanma sıklığı ve önceki sürümlerin çıkış tarihleri gibi kalıpları analiz eder.
3. Yeni Soruları Yanıtlama: Model, elde ettiği bilgileri kullanarak .NET 6.0'ın çıkış tarihine ilişkin bir tahminde bulunur.

History ile ilgili örneğimizi oluşturalım.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

string apikey = Environment.GetEnvironmentVariable("AI:OpenAI:APIKey")!;

// Initialize the kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion("gpt-3.5-turbo-0125", apikey)
.Build();

// Create a new chat
IChatCompletionService ai = kernel.GetRequiredService<IChatCompletionService>();
ChatHistory chat = new("You are an AI assistant that helps people find information.");

// Q&A loop
while (true)
{
Console.Write("Question: ");
chat.AddUserMessage(Console.ReadLine()!);

var answer = await ai.GetChatMessageContentAsync(chat);
chat.AddAssistantMessage(answer.Content!);
Console.WriteLine(answer);

Console.WriteLine();
}

Evet, IChatCompletionService servisi, yeni bir sohbet oturumu oluşturmanın yanı sıra, soruları ve yanıtları da bellekte tutarak, sohbetin devamlılığını ve bağlamsal tutarlılığını sağlar. Bu servis, kullanıcıların önceki sorularına ve verilen yanıtlara dayanarak daha kişiselleştirilmiş ve bağlamsal hizmet sunmayı mümkün kılar.

Soruları yanıtlamak için geçmiş bilgisi saklandığında, aynı soruya daha detaylı ve açıklayıcı bir bilgi sağlanabiliyor. Ancak, geçmiş bilgisiyle çalışırken yanıt süresinde gecikmeler yaşanabiliyor. Bu durumu iyileştirmek ve etkileşimi daha hızlı hale getirmek için kodumuza bazı eklemeler yapabiliriz.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using System.Text;

string apikey = Environment.GetEnvironmentVariable("AI:OpenAI:APIKey")!;

// Initialize the kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion("gpt-3.5-turbo-0125", apikey)
.Build();

// Create a new chat
IChatCompletionService ai = kernel.GetRequiredService<IChatCompletionService>();
ChatHistory chat = new("You are an AI assistant that helps people find information.");
StringBuilder builder = new();

// Q&A loop
while (true)
{
Console.Write("Question: ");
chat.AddUserMessage(Console.ReadLine()!);

builder.Clear();
await foreach (StreamingChatMessageContent message in ai.GetStreamingChatMessageContentsAsync(chat))
{
Console.Write(message);
builder.Append(message.Content);
}
Console.WriteLine();
chat.AddAssistantMessage(builder.ToString());

Console.WriteLine();
}

Önceki yapılarda yanıtlar tek seferde tam olarak yazılırdı. Ancak, akış (stream) kullanımı ile yanıtlar parça parça tamamlanarak yüklenmektedir.

Bu örnekte yanıt verme işleyişini görebilirsiniz. Artık soru gönderip cevap alabiliyoruz. Bu etkileşimlerin geçmişini tutabiliyor ve bu geçmiş bilgileri, cevapları etkilemek için kullanabiliyoruz. Ayrıca, sonuçlarımızı yayınlayabiliyoruz. Şu ana kadar olan kısım, LLM’in (Büyük Dil Modeli) eğitildiği veri setlerine veya proaktif olarak bizim sorulara verdiğimiz cevaplara dayanmaktadır. Ancak, LLM’in eğitilmediği veya bilgi bankasında olmayan konular hakkında sorular sorduğumuzda, model yanıltıcı, yararsız veya halüsinasyon içeren cevaplar verebilir. Örnek olarak:

GPT-3.5 Turbo modelinin bu sürümü eğitildikten sonra, “dotnet 9 ne zaman yayına alınacak?” diye sorarsak, bu konuda bilgisi olmadığı için bize 25 Aralık 2016 gibi alakasız bir cevap verebilir. Başka bir örnek ile bakarsak:

1. Soru, bu LLM eğitildikten sonra ortaya çıktığı için model güncel olmayan bir yanıt veriyor.

2. Soruya ise model halüsinasyon görerek bir şeyler uydurmaya başlıyor.

Şimdi yapmamız gereken, kullanıcının sorduğu soruları ona öğretmektir. Daha önce bunu yaptık ve bilgiyi geçmişe (history) ekleyerek ilerledik. Ancak, burada bir sorun olabilir. Eğer “Git ve bunu Microsoft’un devblog adresinden oku ve history’ye ekle” dersek, token limitimiz olan 16k’yı aşarak hata almaya başlayacağız.

Unhandled exception. Microsoft.SemanticKernel.AI.AIException: Invalid request: The request is not valid, HTTP status: 400
---> Azure.RequestFailedException: This model's maximum context length is 16384 tokens. However, your messages resulted in 155751 tokens. Please reduce the length of the messages.
Status: 400 (model_error)
ErrorCode: context_length_exceeded

İşte bu noktada, asıl konumuz olan RAG (Retrieval-Augmented Generation) devreye girerek kurtarıcı oluyor.

Burada karşılaşacağımız üç kavram;

Retrieval Modeli: Model, öncelikle büyük bir veri seti veya metin koleksiyonundan girdiyle alakalı tüm bilgiyi arar. Bu aşamada, modelin amacı, soruya en uygun ve en bilgilendirici metin parçalarını bulmaktır. Örneğin model, bilimsel makale ansiklopedi gibi kaynaklardan bilgi çekebilir.

Generative Modeli: Retrieval modeli tarafından çekilen bilgilere dayanarak, generative modeli kullanıcıya anlaşılır ve bilgilendirici bir yanıt üretir.

Embedding : Metin, resim, ses veya video gibi bilgileri temsil eden sayısal vektörler dizisidir. Vektör uzayında yakın anlamlı kelimeler yakın bulunurlar. Embedding bize bir modelden girdi için vektör oluşturacak ve bunu besleyen metni bu sayede veritabanında tutmamızı sağlamaya sağlayacaktır. Daha sonrasında benzerliklere göre veritabanına bakıp en uygun sonucu geri döndürmüş olacağız. Aramalar genellikle kosinüs benzerlerliği gibi bir mesafe ölçümü ile yapılır.

https://developers.google.com/machine-learning/crash-course/embeddings/translating-to-a-lower-dimensional-space
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.Numerics.Tensors;

#pragma warning disable SKEXP0011

string apikey = Environment.GetEnvironmentVariable("AI:OpenAI:APIKey")!;

var embeddingGen = new OpenAITextEmbeddingGenerationService("text-embedding-3-small", apikey);

string input = "What is an amphibian?";
string[] examples =
{
"What is an amphibian?",
"Cos'è un anfibio?",
"A frog is an amphibian.",
"Frogs, toads, and salamanders are all examples.",
"Amphibians are four-limbed and ectothermic vertebrates of the class Amphibia.",
"They are four-limbed and ectothermic vertebrates.",
"A frog is green.",
"A tree is green.",
"It's not easy bein' green.",
"A dog is a mammal.",
"A dog is a man's best friend.",
"You ain't never had a friend like me.",
"Rachel, Monica, Phoebe, Joey, Chandler, Ross",
};

// Generate embeddings for each piece of text
ReadOnlyMemory inputEmbedding = (await embeddingGen.GenerateEmbeddingsAsync([input]))[0];
IList> exampleEmbeddings = await embeddingGen.GenerateEmbeddingsAsync(examples);

// Print the cosine similarity between the input and each example
float[] similarity = exampleEmbeddings.Select(e => TensorPrimitives.CosineSimilarity(e.Span, inputEmbedding.Span)).ToArray();
similarity.AsSpan().Sort(examples.AsSpan(), (f1, f2) => f2.CompareTo(f1));
Console.WriteLine("Similarity Example");
for (int i = 0; i < similarity.Length; i++)
Console.WriteLine($"{similarity[i]:F6} {examples[i]}");

Şu anda embedding için text-embedding-3-small modelini kullanıyoruz. Examples dizisinde yer alan yazılar girdi olarak işlenir ve benzerliklerine göre sıralanır.

Similarity Example
1.000000 What is an amphibian?
0.937651 A frog is an amphibian.
0.902491 Amphibians are four-limbed and ectothermic vertebrates of the class Amphibia.
0.873569 Cos'è un anfibio?
0.866632 Frogs, toads, and salamanders are all examples.
0.857454 A frog is green.
0.842596 They are four-limbed and ectothermic vertebrates.
0.802171 A dog is a mammal.
0.784479 It's not easy bein' green.
0.778341 A tree is green.
0.756669 A dog is a man's best friend.
0.734219 You ain't never had a friend like me.
0.721176 Rachel, Monica, Phoebe, Joey, Chandler, Ross

Burada gömme işlemini manuel olarak gerçekleştirdik, ancak bu işlemi semantic.kernel paketi ile otomatik hale getirebiliriz.

dotnet add package Microsoft.SemanticKernel.Plugins.Memory --prerelease

Bu paketi kurarak işlemi otomatik hale getirebiliriz. Özetle, embedding’ler, verilerin semantik temsillerini oluştururken, RAG, bu embedding’leri kullanarak, bilgi tabanından ilgili bilgileri çeker ve bu bilgilere dayalı olarak yeni metinler üretir. Rag yapısını gösteren örneğimizle devam edersek:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Text;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;

#pragma warning disable SKEXP0003, SKEXP0011, SKEXP0052, SKEXP0055 // Experimental

string apikey = Environment.GetEnvironmentVariable("AI:OpenAI:APIKey")!;

// Initialize the kernel
IKernelBuilder kb = Kernel.CreateBuilder();
kb.AddOpenAIChatCompletion("gpt-3.5-turbo-0125", apikey);
kb.Services.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Trace));
kb.Services.ConfigureHttpClientDefaults(c => c.AddStandardResilienceHandler());
Kernel kernel = kb.Build();

// Download a document and create embeddings for it
ISemanticTextMemory memory = new MemoryBuilder()
.WithLoggerFactory(kernel.LoggerFactory)
.WithMemoryStore(new VolatileMemoryStore())
.WithOpenAITextEmbeddingGeneration("text-embedding-3-small", apikey)
.Build();
string collectionName = "net7perf";
using (HttpClient client = new())
{
string s = await client.GetStringAsync("https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7");
List paragraphs =
TextChunker.SplitPlainTextParagraphs(
TextChunker.SplitPlainTextLines(
WebUtility.HtmlDecode(Regex.Replace(s, @"<[^>]+>|&nbsp;", "")),
128),
1024);
for (int i = 0; i < paragraphs.Count; i++)
await memory.SaveInformationAsync(collectionName, paragraphs[i], $"paragraph{i}");
}

// Create a new chat
var ai = kernel.GetRequiredService<IChatCompletionService>();
ChatHistory chat = new("You are an AI assistant that helps people find information.");
StringBuilder builder = new();

// Q&A loop
while (true)
{
Console.Write("Question: ");
string question = Console.ReadLine()!;

builder.Clear();
await foreach (var result in memory.SearchAsync(collectionName, question, limit: 3))
builder.AppendLine(result.Metadata.Text);
int contextToRemove = -1;
if (builder.Length != 0)
{
builder.Insert(0, "Here's some additional information: ");
contextToRemove = chat.Count;
chat.AddUserMessage(builder.ToString());
}

chat.AddUserMessage(question);

builder.Clear();
await foreach (var message in ai.GetStreamingChatMessageContentsAsync(chat))
{
Console.Write(message);
builder.Append(message.Content);
}
Console.WriteLine();
chat.AddAssistantMessage(builder.ToString());

if (contextToRemove >= 0) chat.RemoveAt(contextToRemove);
Console.WriteLine();
}

Örneğimizi yukarıdan aşağıya incelediğimizde:

  1. LLM olarak gpt-3.5-turbo-0125 modelini seçtik.
  2. API anahtarımızı tanımladık.
  3. Embedding modeli olarak text-embedding-3-small modelini seçtik.
  4. Veritabanı bağlantısını kullanabiliriz, MemoryStore yerine. Aynı şekilde log yapısını da entegre edebiliriz.
  5. Devblog içerisinden metni indirip parçalara ayırdık ve metinleri veritabanına embedding işlemi yaparak işlemi tamamladık.

Bu şekilde verinin kaydedildiğini gördük ve chat bağlantımızı yaptık.

Artık soru sorduğumuzda, veritabanına ekleniyor ve kullanıcıya en alakalı cevapları materyal olarak sunmaya başlıyoruz. Girdi sorgusunu inceleyerek, bu sorguya dayalı olarak ek içerik elde ediyoruz ve daha fazla bilgi sağlamak için LLM’e gönderilen istemi artırıyoruz. Böylece, bir vektör veritabanı ile embedding’in uçtan uca kullanımını uygulamış oluyoruz. RAG’nin yapısı budur.

Ancak, içeriğin alınabileceği başka yollar da vardır ve geliştirici olarak bunu her zaman sizin yapmanız gerekmez. Aslında, modellerin kendileri daha fazla bilgi istemek için eğitilebilir; örneğin OpenAI modelleri, araçları / işlev çağrılarını desteklemek için eğitilmiştir; burada, istemlerin bir parçası olarak, değerli gördükleri takdirde çağırabilecekleri bir dizi işlev hakkında bilgi verilebilir.
https://openai.com/blog/function-calling-and-other-api-updates

Kaynak : https://devblogs.microsoft.com/dotnet/demystifying-retrieval-augmented-generation-with-dotnet/

--

--