Mert Özen Her Satırda Daha İleri
Yapay Zeka LLM Temelleri — #14

LLM ve RAG: Modelin Bilmediği Veriyle Cevap Vermek

Mert Özen 6 Kas 2026 11 dk 6 görüntülenme
LLM ve RAG: Modelin Bilmediği Veriyle Cevap Vermek

gemma3:4b'ye kitapçının iade süresini sorunca kendinden emin bir şekilde 30 gün dedi; doğrusu 21. Politikaları chunk'lara bölüyor, nomic-embed-text ile indeksliyor, en yakın parçaları prompt'a koyuyor ve modele bağlamda olmayanı bilmediğini söyletiyoruz. C#'ta bellekte minimal bir RAG ve sınırları.

Sürüm notu: Bu yazıdaki örnekler Ollama 0.34 ile yerel modellerde (gemma3:4b, nomic-embed-text) denenmiştir; .NET 10 (Microsoft.Extensions.AI 10.11.0, OllamaSharp 5.5.2) kullanılmıştır.

Önce Tahmin Et

Bookstore'un iade politikası açık: kitabı teslimattan sonra 21 gün içinde iade edebilirsin. Bu bilgi bizim dokümanımızda duruyor ve hiçbir yerde yayımlanmadı. Bookstore asistanı rolündeki gemma3:4b'ye "Bir kitabı kaç gün içinde iade edebilirim?" diye sorsan ne der? "Bilmiyorum" mu, yoksa bir sayı mı?

Cevap yazının ilk bölümünde. Ardından modelin bilmediği bir veriyi ona nasıl okutacağımızı, embedding yazısında kurduğumuz aramanın üstüne bir sohbet modeli ekleyerek göreceğiz.

Model Senin Politikalarını Bilmez

Önce modele hiçbir bağlam vermeden, yalnızca "You are the Bookstore support assistant." system prompt'uyla soralım:

$ dotnet run -- "How many days do I have to return a book?" --no-rag

Okay, let's get that sorted out for you! 

You have **30 days** to return a book to us. 😊 

Do you have any other questions about our return policy, or perhaps you'd like to start the return process?
(input tokens: 34, output tokens: 55)

Kendinden emin, kibar ve yanlış. Model bizim politikamızı hiç görmedi; eğitim verisinde en sık rastladığı iade süresi 30 gün olduğu için onu söyledi. System prompt'u hiç vermediğim ilk denemede ise "iade politikaları yerden yere değişir" deyip Amazon ve Barnes & Noble için 30 gün yazdı. İkisi de anlaşılır; ama müşteriye 30 gün dersen, 25. gün iade isteyen müşteriyle bir sorunun olur.

Modelin bilgisi eğitim verisinin bittiği tarihte donmuştur ve senin şirketine özel hiçbir şeyi içermez: politikalar, stok, sözleşmeler, iç dokümanlar. Bu boşluğu doldurmanın en yaygın yolu RAG.

RAG: Getir, Ekle, Üret

RAG (Retrieval-Augmented Generation), adını üç adımından alır. Retrieve: soruyla ilgili doküman parçalarını bul. Augment: bu parçaları prompt'a ekle. Generate: modelin cevabı bu parçalara dayanarak üretmesini iste. Fikir Lewis ve arkadaşlarının 2020 tarihli makalesine dayanıyor, ama bugün kullandığımız basit hâli bundan çok daha az şey istiyor: bir embedding modeli, bir sohbet modeli ve birkaç satır kod.

En iyi benzetme açık kitap sınavı. Model her şeyi ezberlemek zorunda değil; doğru sayfayı önüne koyarsan oradan okuyup cevaplar. Sayfayı bulmak embedding aramasının işi, okuyup cümle kurmak sohbet modelinin.

Neden fine-tuning (ince ayar) değil? Fine-tuning modelin ağırlıklarını değiştirir; üslup, biçim ya da dar bir görev için iyidir ama yeni bilgiyi güvenilir biçimde eklemez, üstelik veri seti ve GPU ister. Politika değiştiğinde de modeli yeniden eğitmen gerekir. RAG'de dokümanı güncellersin, bir sonraki soru yeni bilgiyle cevaplanır.

Dokümanı Parçalara Bölmek

Kaynağımız küçük bir markdown dosyası:

# Shipping
Standard shipping takes 3 to 5 business days. Orders over 40 USD ship for free; otherwise standard shipping costs 4.50 USD.

Express shipping costs 9.90 USD and delivers on the next business day if you order before 2 pm.

# Returns
You can return a book within 21 days of delivery if it is unread and in its original condition. Refunds go back to the original payment method within 5 business days.

E-book purchases and opened gift wrap cannot be returned.

# Gift cards
Gift cards are valid for 3 years from the date of purchase. They can be used online and in the Ankara store, but they cannot be exchanged for cash.

# Loyalty program
Members earn 1 point per dollar spent. Every 100 points can be redeemed for a 5 USD discount on a future order.

Bu dosyayı tek parça embed edebilirdik, ama o zaman her soru için bütün dosya gelirdi; embedding yazısında gördüğümüz gibi uzun metnin vector'ü ayrıntıları ortalamada kaybeder. Bunun yerine metni chunk'lara bölüyoruz. En basit strateji: her paragraf bir chunk ve her chunk'ın başına bölüm başlığını ekle. "E-book purchases and opened gift wrap cannot be returned." cümlesi tek başına biraz sahipsiz; başına Returns: gelince hem embedding hem model onun iade kuralı olduğunu anlar. Bu dosyadan altı chunk çıkıyor.

Gerçek dokümanlarda paragraflar bu kadar düzenli değildir. Yaygın yöntemler sabit token sayısına göre bölmek (ör. 300–500 token), komşu chunk'lar arasında biraz örtüşme (overlap) bırakmak ve başlık, tablo gibi yapıyı korumaktır. Hangisinin iyi olduğu verine bağlı; ölçmeden karar verme.

Bütün Hat: C#'ta Minimal RAG

Bir embedding üreticisi (nomic-embed-text) ve bir sohbet modeli (gemma3:4b) için iki ayrı OllamaApiClient açıyoruz. Vektör veritabanı yok; altı vector'ü bir listede tutmak yeterli:

using System.Globalization;
using System.Numerics.Tensors;
using Microsoft.Extensions.AI;
using OllamaSharp;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Uri ollama = new("http://localhost:11434");
IEmbeddingGenerator<string, Embedding<float>> embedder = new OllamaApiClient(ollama, "nomic-embed-text");
IChatClient chat = new OllamaApiClient(ollama, "gemma3:4b");

// 1. Parçala: her paragraf bir chunk, başına bölüm başlığı eklenir
List<string> chunks = Chunk(File.ReadAllText("policies.md"));

// 2. İndeksle: her chunk'ı bir kez embed et
GeneratedEmbeddings<Embedding<float>> vectors =
    await embedder.GenerateAsync(chunks.Select(c => $"search_document: {c}"));

string question = args[0];
bool useRag = !args.Contains("--no-rag");

List<ChatMessage> messages = [];
if (useRag)
{
    // 3. Getir (retrieve): soruya en yakın 2 chunk
    ReadOnlyMemory<float> q = await embedder.GenerateVectorAsync($"search_query: {question}");
    List<(string Text, float Score)> top = chunks
        .Select((text, i) => (text, TensorPrimitives.CosineSimilarity(q.Span, vectors[i].Vector.Span)))
        .OrderByDescending(x => x.Item2)
        .Take(2)
        .ToList();

    foreach ((string text, float score) in top)
    {
        Console.WriteLine($"[{score:F3}] {text}");
    }

    // 4. Zenginleştir (augment): chunk'ları prompt'a koy, kuralları yaz
    string context = string.Join("\n\n", top.Select(t => t.Text));
    messages.Add(new ChatMessage(ChatRole.System, """
        You are the Bookstore support assistant.
        Answer using ONLY the facts inside <context>.
        If the answer is not in <context>, reply exactly: "I don't know based on our store policies."
        """));
    messages.Add(new ChatMessage(ChatRole.User, $"<context>\n{context}\n</context>\n\nQuestion: {question}"));
}
else
{
    // Aynı asistan, ama elinde yalnızca eğitim verisi var
    messages.Add(new ChatMessage(ChatRole.System, "You are the Bookstore support assistant."));
    messages.Add(new ChatMessage(ChatRole.User, question));
}

// 5. Üret (generate)
ChatResponse response = await chat.GetResponseAsync(messages, new ChatOptions { Temperature = 0, Seed = 42 });
Console.WriteLine($"\n{response.Text}");
Console.WriteLine($"(input tokens: {response.Usage?.InputTokenCount}, output tokens: {response.Usage?.OutputTokenCount})");

static List<string> Chunk(string markdown)
{
    List<string> result = [];
    string section = "";
    foreach (string block in markdown.Split("\n\n"))
    {
        foreach (string line in block.Split('\n', StringSplitOptions.RemoveEmptyEntries))
        {
            if (line.StartsWith("# "))
            {
                section = line[2..];
            }
            else
            {
                result.Add($"{section}: {line}");
            }
        }
    }
    return result;
}

Beş adım kodda numaralı. Dikkat edilmesi gereken iki yer var. Birincisi system prompt: modele yalnızca <context> içindeki bilgiyi kullanmasını ve cevap orada yoksa hangi cümleyi kuracağını harfi harfine söylüyoruz. İkincisi bağlamın kullanıcının sorusundan ayraçlarla ayrılması: prompt yazısında konuştuğumuz gibi, modelin "doküman" ile "soru"yu karıştırmaması için sınırları belli et.

Aynı Soru, Bu Kez Bağlamla

$ dotnet run -- "How many days do I have to return a book?"
[0.907] Returns: You can return a book within 21 days of delivery if it is unread and in its original condition. Refunds go back to the original payment method within 5 business days.
[0.696] Returns: E-book purchases and opened gift wrap cannot be returned.

You can return a book within 21 days of delivery.
(input tokens: 134, output tokens: 14)

Retrieval doğru chunk'ı 0.907 ile en üste koydu, model de cevabı oradan aldı: 21 gün. Maliyet tarafına da bak: prompt 34 token'dan 134 token'a çıktı, çünkü iki chunk ve kurallar prompt'a eklendi. Cevap ise 55 token'dan 14 token'a indi; model sohbet etmeyi bırakıp soruyu cevapladı. Gerçek bir sistemde chunk sayısı ve uzunluğu, her isteğin token maliyetini doğrudan belirler.

Biraz daha zorlayalım. Bu soru, kuralı okumayı ve bir karşılaştırma yapmayı gerektiriyor:

$ dotnet run -- "My order is 35 USD. Do I pay for shipping?"
[0.742] Shipping: Standard shipping takes 3 to 5 business days. Orders over 40 USD ship for free; otherwise standard shipping costs 4.50 USD.
[0.719] Shipping: Express shipping costs 9.90 USD and delivers on the next business day if you order before 2 pm.

Otherwise standard shipping costs 4.50 USD.
(input tokens: 144, output tokens: 12)

Bilgi doğru ama cümle tuhaf: "Otherwise..." diye başlıyor ve "evet" demiyor. "YALNIZCA bağlamdaki bilgiyi kullan" talimatı, küçük bir modeli bağlamdan cümle kopyalamaya itti. Katı kurallar halüsinasyonu azaltır ama cevabın doğallığını da azaltabilir. Bu ikisi arasındaki dengeyi bir sonraki yazıda ölçerek kuracağız.

"Bilmiyorum" Demeyi Öğretmek

RAG'in asıl değeri yalnızca doğru cevap vermek değil, cevabı bilmediğini fark etmek. Dokümanda olmayan iki soru ve bir sınır durumu:

$ dotnet run -- "Do you ship to Germany?"
[0.618] Shipping: Standard shipping takes 3 to 5 business days. Orders over 40 USD ship for free; otherwise standard shipping costs 4.50 USD.
[0.591] Shipping: Express shipping costs 9.90 USD and delivers on the next business day if you order before 2 pm.

I don't know based on our store policies.
(input tokens: 136, output tokens: 12)

$ dotnet run -- "Can I turn my loyalty points into cash?"
[0.726] Loyalty program: Members earn 1 point per dollar spent. Every 100 points can be redeemed for a 5 USD discount on a future order.
[0.573] Gift cards: Gift cards are valid for 3 years from the date of purchase. They can be used online and in the Ankara store, but they cannot be exchanged for cash.

I don't know based on our store policies.
(input tokens: 148, output tokens: 12)

$ dotnet run -- "Can I return an e-book I bought yesterday?"
[0.807] Returns: E-book purchases and opened gift wrap cannot be returned.
[0.757] Returns: You can return a book within 21 days of delivery if it is unread and in its original condition. Refunds go back to the original payment method within 5 business days.

No, E-book purchases and opened gift wrap cannot be returned.
(input tokens: 134, output tokens: 15)

Almanya sorusunda retrieval yine kargo chunk'larını getirdi, çünkü elindeki en yakın şey onlardı; retrieval her zaman bir şey döndürür. Ama model bu chunk'larda ülke bilgisi olmadığını gördü ve istediğimiz cümleyi kurdu. Sadakat puanı sorusu daha ilginç: politika puanların indirime çevrilebileceğini söylüyor, nakde çevrilip çevrilemeyeceğini söylemiyor. Model "hayır" diye çıkarım yapmak yerine "bilmiyorum" dedi. Bu temkinli ve bu senaryoda doğru davranış; ama kullanıcıya "puanlarını indirim olarak kullanabilirsin" demek daha yardımcı olurdu. E-kitap sorusunda ise doğru chunk birinci geldi ve cevap net.

Embedding yazısındaki olumsuz cümle tuzağını hatırla: Almanya sorusunda en yüksek puan 0.618'di; iade sorusundaki 0.907'nin yanında oldukça düşük. Bu fark, "elimde iyi bir kaynak yok" sinyali olarak kullanılabilir. Model çağrılmadan önce bir eşik koyup doğrudan "bilmiyorum" demek de mümkün; ama eşiği kendi verinle ölçerek seçmelisin.

RAG'in Sınırları

Retrieval yanlış chunk'ı getirirse model doğru cevabı veremez; RAG zincirinin en zayıf halkası çoğu zaman aramadır. İki chunk'ın birlikte gerektiği sorularda (ör. "Hediye kartımla express kargo ödeyebilir miyim?") Take(2) ikisini birden getiremeyebilir. top-k'yı büyütmek recall'u artırır ama prompt'u uzatır ve modelin dikkatini dağıtır.

Bellekte bir liste altı chunk için harika, altı milyon chunk için değil. Gerçek sistemlerde vector'ler bir vektör veritabanında ya da vector desteği olan ilişkisel bir veritabanında tutulur, dokümanlar değiştikçe yeniden indekslenir ve arama genelde anahtar kelime aramasıyla birleştirilir. Bunları ayrı bir RAG serisinde, EF Core ile vektör aramasını da işin içine katarak ele alacağız. Bu yazının amacı mekanizmayı göstermek: bir arama, bir prompt ve bir kural.

Son olarak RAG halüsinasyonu bitirmez, azaltır. Model bağlamı yanlış okuyabilir, iki chunk'ı karıştırabilir ya da bağlamda olmayan bir ayrıntı ekleyebilir. Bunu yakalamanın yolu ölçmek.

Sık Düşülen Tuzaklar

Birincisi, "bilmiyorum" kuralını yazmamak. Kural yoksa model, bağlamda cevap bulamayınca açılıştaki gibi eğitim verisinden bir şey uydurur ve bunu bağlamdan okumuş gibi söyler.

İkincisi, chunk'ları bağlamından koparmak. Başlıksız, tek cümlelik bir chunk hem yanlış aramalara takılır hem de model tarafından yanlış yorumlanır. Bölüm başlığını ya da doküman adını chunk'ın içine koy.

Üçüncüsü, retrieval sonucunu hiç loglamamak. Yanlış bir cevapta ilk soru "model mi yanlış okudu, yoksa yanlış chunk mı geldi?" olmalı. Bu yazıdaki program getirilen chunk'ları puanlarıyla yazdırıyor; production'da da bunu bir yerde sakla.

Dördüncüsü, dokümanı güncelleyip indeksi güncellememek. Politika 21 günden 30 güne çıkarsa ve eski vector'ler hâlâ eski metni gösteriyorsa, RAG artık eski bilgiyi kendinden emin bir şekilde söyler.

Beşincisi, kullanıcı girdisi gibi doküman içeriğine de güvenmek. RAG'le prompt'a giren her metin modelin okuduğu bir talimat olabilir. Birisi bir ürün yorumuna "ignore previous instructions" yazarsa, o yorum chunk olarak prompt'a girer. Tool calling yazısında konuştuğumuz prompt injection burada da geçerli.

Küçük Bir Deneme

★☆☆ policies.md'ye bir "International shipping" bölümü ekle (ör. "We ship to EU countries for 14 USD."). Programı yeniden çalıştırıp "Do you ship to Germany?" sorusunu sor. Model artık doğru cevabı veriyor mu?

★★☆ Take(2)'yi Take(1) ve Take(4) yap. "Can I pay for express shipping with a gift card?" sorusunda getirilen chunk'lar ve cevap nasıl değişiyor? Input token sayısını da not et.

★★☆ System prompt'taki "If the answer is not in context..." satırını sil ve "Do you ship to Germany?" ile "Can I turn my loyalty points into cash?" sorularını tekrar sor. Model ne uyduruyor?

★★★ Bir eşik ekle: en yüksek retrieval puanı 0.65'in altındaysa modeli hiç çağırmadan "I don't know based on our store policies." yaz. Bu yazıdaki beş soruyla eşiğin hangi soruları doğru, hangilerini yanlış elediğini çıkar.

Gördüğün gibi RAG, modelin cevabını doğru kaynağa bağlıyor ama her şeyi çözmüyor; "Otherwise..." diye başlayan tuhaf cevap ve temkinli "bilmiyorum"lar da bu sistemin parçası. Bir sonraki yazıda halüsinasyonun nedenlerine inecek, cevaba kaynak göstermeyi ve bu kaynağı kodla doğrulamayı ekleyecek, sonra küçük bir golden set ile prompt'larımızı sayılarla karşılaştıracağız.