RAG bugünlerde bazen kötü anılıyor. Bunun nedeni, insanların artık onu tamamen basit sanması (başlamak kolay, ölçeklemek o kadar değil) ya da yerini “ajan tabanlı sistemlere” bıraktığını düşünmesi (oysa çoğu durumda yüzeyi biraz kazıyınca bunlar hızla RAG'e benzemeye başlıyor…).
Bu blog yazısında, sık karşılaştığımız şu zorlukları nasıl ele aldığımızı bir iki uygulamalı örnekle gösteriyoruz:
Metin ve sayısal verileri birlikte işleme ve bunun basit RAG'i neden bozduğu: Anahtar kelimeler çakışır, sayılar ise anlamsal bir anlam taşımaz.
Önce özet yaklaşımıyla embedding tasarlamak neden yararlıdır: Her parça için kısa ve açıklayıcı bir özet oluşturun, ardından embedding ve sorgulama işlemlerini bu özet üzerinde yapın.
Bağlamsal özetler nasıl oluşturulur: Benzer yapıdaki istatistiklerin ayırt edilebilmesi için ana belge bağlamını ekleyin.
Ne zaman kod ve Pydantic modellerine başvurulmalı: İçeriğin aynen korunması gerektiğinde güvenilirlik için özel kodu ve/veya Pydantic modelini LLM çağrılarıyla birleştirin.
Temel bilgiler
RAG sistemleri, destek botlarından şirket içi bilgi asistanlarına kadar pek çok çözümün temelini oluşturur.
Arka planda genellikle şunları yaparsınız:
Kaynak belgelerinizi parçalara ayırırsınız
Her parçayı bir vektör uzayına gömersiniz
Sorgu sırasında en iyi K parçayı getirirsiniz
Bu parçaları temel alan bir yanıt üretirsiniz
LangChain, LlamaIndex ve OpenAI Filestore gibi popüler araç setleri bu adımları neredeyse zahmetsiz hâle getirir. Ancak gerçek dünyadaki veri hatlarında yalnızca yoğun metinden oluşmayan verilerle karşılaşırsınız ve temel RAG bunlarla başa çıkmakta zorlanabilir. İzleyen bölümlerde veriyle ilgili zorlukları somut örneklerle gösterecek ve karmaşıklık arttıkça çözümü adım adım geliştireceğiz.
Verileriniz yalnızca metinden oluşmadığında (aslında oldukça sık görülür)
Oyun bağlamındaki şu veri parçasını ele alalım:
JSON
Embedding'ler, kelimeler arasında anlamsal anlam ve dil bilgisi üzerinden öğrenilen ilişkiler sayesinde çalışır. Yukarıdaki veriler metin ve sayıların bir karışımıdır; bu bağlamın dışında sayılar ile kelimeler arasında herhangi bir ilişki yoktur. Dolayısıyla bu veri parçasının, biraz açıklayıcı birkaç kelime ile bunları izleyen rastgele sayıların birleşiminden ibaret olduğunu söyleyebiliriz.
Elimizde yalnızca bu tür veriler olsaydı bu aslında sorun yaratmazdı; mevcut birkaç açıklayıcı kelimenin embedding'leri üzerinden ilgili parçayı yine getirebilirdik (ya da doğrudan text-to-sql kullanabilirdik). Peki bu parça, aynı kelimelerin de geçtiği çok sayıda yoğun metin parçasının arasında gömülü olsaydı ne olurdu? Örneğin:
JSON
Şimdi “What is the attack range with Draconic Ascension?” sorgusuyla ilgili parçayı getirmek istediğimizi düşünün. İstediğimiz parçayı büyük olasılıkla getiremeyiz; çünkü aynı anahtar kelimeleri içeren diğer parçaların yarattığı gürültünün içinde kaybolmuştur.
Temel sorun, aynı konu hakkında farklı türde bilgiler içerseler bile bu veri parçalarını birbirinden yeterince iyi ayıramamamızdır. Peki bunu bir şekilde zenginleştirip geliştirebilir miyiz? Elbette yapabiliriz:smile:
Verilerinizi özetleyerek zenginleştirin; evet, yanlış okumadınız
Parçanın embedding'ini doğrudan oluşturmak yerine, önce verinin ne hakkında olduğunu açıklayan bir özet üretebilir; ardından embedding ve getirme işlemlerini bu özet üzerinden yapabiliriz. Üretim aşamasında ise özetle bağlantılı özgün verileri kullanmaya devam ederiz.
Yukarıda gösterilen iki parça örneği için şöyle özetler oluştururduk:
Menzil, hız ve hasara ilişkin saldırı istatistikleri (varsayılan ve Draconic Ascension etkinken).
Etkinleştirme koşulları, görsel efektleri ve hikâyesi dâhil olmak üzere Draconic Ascension'ın açıklaması ve ayrıntıları.
Ardından sorguyu da özetle “uyumlu” hâle getirecek şekilde zenginleştiririz. Örneğin “What is the attack range with Draconic Ascension?” sorgusunu “What is the statistics of attack range with Draconic Ascension?” sorgusuna dönüştürürdük.Bu, özellikle getirme sorgusunun teknik alanlar dışındaki kullanıcılar tarafından ~~“serbest tarzda”~~ normal insan diliyle yazıldığı durumlarda önemlidir. Sonuçta RAG'in kesinlik ve duyarlılığı en üst düzeye çıkarmak için nasıl çalıştığını bilmek onların ne uzmanlık alanıdır ne de önceliğidir.


Konuyu bağlamından koparmayın (genel olarak hayatta da geçerli)
Bir sonraki senaryoda ise aşağıdaki gibi birbirine benzeyen çok sayıda veri parçasıyla uğraşıyoruz:
Plain Text
Aynı yaklaşımı sürdürdüğümüzü ve “what is character X’s attack range?” diye sorduğumuzu düşünün. Yeni oluşturduğumuz bu özetler de birbirine çok benzeyeceği için şansa dayalı bir tahmin oyunu oynamış olurduk. Peki bunları nasıl ayırt edebiliriz?
Basit yanıt: Bağlam sağlayarak. Veri parçasına ana belgesine yönelik bir referans ekleyebiliriz; örneğin bu durumda {”character”: “X”}. Böylece Y ve Z karakterleri için aynı veriler mevcut olsa bile X karakterine ait doğru verileri isabetli biçimde getirebiliriz.
Ancak daha iyi ve genellenebilir bir yaklaşım, parçanın bağlamsal bir özetini oluşturmaktır. Başka bir deyişle, yalnızca veri parçasının özetini oluşturmak yerine hem ana belgeyi hem de parçayı modele ileterek genel bir bağlamsal özet oluşturabiliriz. Bu özette parçanın ana belgedeki yerini de açıklarız. Örneğin:
Bu parça, X karakterine ait … hakkında ayrıntılı istatistikler sunar. Bu parça, X'in saldırı hızındaki gücünü göstererek belgenin bütününü tamamlar…
Bu parça, Y karakterine ait … hakkında ayrıntılı istatistikler sunar. Bu parça, Y'nin özel yeteneğiyle güçlenen istatistiklerini göstererek belgenin bütününü tamamlar…
Bu parça, Z karakterine ait … hakkında ayrıntılı istatistikler sunar. Bu parça, Z'nin takım maçlarında tank rolüne çok uygun istatistiklerini göstererek belgenin bütününü tamamlar…
Kısmen Anthropic'ten esinlenen bu yöntem, yukarıdaki örnek için gereğinden fazla kapsamlı görünebilir. Ancak “bağlamından koparıldığında” yanlış yorumlanabilecek parçalar için son derece etkilidir. Üstelik tüm parçalarda çalışan birleşik bir yaklaşım sunarak düzenli bir mühendislik veri hattının korunmasını sağlar.


~~Kontrol manyağı~~ titiz olmanız gerektiğinde
Verileri genellikle bütün hâlinde alır ve bir RAG sistemi için parçalara böleriz. Bu örnekte biraz farklı bir durumu ele alıyoruz: Veriler parçalara ayrılmış, ancak kötü biçimde. Aslında yeniden bir araya getirilmesi gereken mantıksal bir parçanın rastgele bölümlerinden oluşuyorlar. Mantıksal parça; bir belgenin alt bölümü veya kendi içinde tutarlı bir paragraf gibi, doğal olarak bir arada bulunması gereken içerik bölümüdür.


Bu verilerle ilk denememizde her şeyi tek bir LLM çağrısına verip uygun gördüğü şekilde gruplandırmasını ve gruplanmış içeriği döndürmesini istedik. LLM'in bu konuda oldukça iyi olması gerekir, değil mi? Hem evet hem hayır.
Başka birçok durumda da gördük ki özellikle bağlam uzun olduğunda LLM'ler, içeriğin tamamını ve birebir aynısını istediğinizde tembel davranma eğiliminde oluyor ve güvenilir sonuç vermiyor. Bu da son derece anlaşılır. Ancak özgün içeriği kelimesi kelimesine, eksiksiz almamız gerektiğinden bu durum söz konusu kullanım senaryosu için kabul edilemezdi. Özetleme veya herhangi bir bölümü atlama söz konusu olamazdı. Hiçbir ayrıntıyı kaçırmamalıyız.
İşin “evet” kısmıysa bozuk parçaların anlam ve yapılarını çözmekte harika bir iş çıkarmasıydı. Tabii içeriği birebir aktarmayı reddetmediği sürece. Hay aksi:/
Peki bir LLM'in güçlü yönlerinden yararlanırken güvenilir olmadığı noktalardan nasıl kaçınabiliriz? Eski dostumuz koda başvurduk (özel Python fonksiyonu diye okuyun). Bir de “daha basiti olamaz” denecek bir Pydantic modeline. Çözüm şöyle:
Mevcut mantıksal parçayı koruyarak bölümleri sırayla işleyin
Her bölümde LLM'e şunu sorun: Bu bölüm mevcut mantıksal parçaya ait mi? (Pydantic modeline uygun olarak) evet veya hayır diye yanıtla.
Yanıt evetse bölümü parçaya ekleyin. Hayırsa tamamlanmış olan mevcut mantıksal parçayı olduğu gibi çıkarın ve bu bölümle yeni bir parça başlatın.


Elbette burada tüm içeriği tek geçişte işlemekten biraz daha fazla token kullanıyoruz. Ancak içeriği birebir korumanın en önemli öncelik olduğu bu kullanım senaryosunda, küçük ek maliyete kesinlikle değdi.
Bu çok basit bir çözüm olsa da önemli bir ilkeye dayanıyor: Titizlik gerektiğinde, olasılıksal yapıları nedeniyle yalnızca LLM'lere güvenmemeliyiz.
Özel kod ve fonksiyonlar ile Pydantic modelleri, LLM'lerin yeteneklerinden yararlanmaya devam ederken öngörülebilir ve güvenilir sonuçlar elde etmek için kullanılabilir.
Üretken yapay zekâ çözümü geliştirmek, yapay zekâ kadar mühendislik açısından da zorlu bir süreçtir. Bu örneklerin, kendinize özgü zorlukların üstesinden gelmeniz için size ilham verdiğini umuyoruz. Mühendislik öncelikli üretken yapay zekâ çözümleri hakkında daha fazla bilgi edinmek için yönlendirici tabanlı ajan sistemi tasarımı konulu blog yazımıza göz atın.