NUMAIN: Yapay zeka ve RAG ile ölümsüz olmak

Veee 100. yazıya geldik :) Blog yazmaya başlayalı 5 yıl oldu ve bu noktada blog yazılarımın toplamının benim görüşlerimin çoğunu yansıtabileceğini fark ettim. Yani benim bütün yazılarımı tek tek okursanız, hayat görüşüm ve tecrübelerim hakkında epeyce bilgi edinebilirsiniz. Fakat birine "benim bütün yazılarımı oku" demek ve 100 adet yazıyı okumasını beklemek anlamsız olacağı için blogda bir arama fonksiyonu hazırlamayı düşünüyordum. Bu özelliği geliştirme fikrini kafamda evirip çevirirken yapay zekanın bir parçası olan RAG yani Retrieval Augmented Generation konusunu keşfettim. Basitçe açıklamam gerekirse, yapay zekaya bir soru soruyorsunuz, sistem benim blog yazılarımda ilgili paragrafları buluyor ve sadece bu paragrafları göstermek yerine LLM mantığı ile benim tarzıma sadık kalarak bir cevaba çeviriyor. İsmi size saçma gelebilir ama adım Numan olduğu için bu sisteme NUMAIN adını verdim :) Benim yazdıklarımdan yola çıktığı için benim gibi konuşan bir soru-cevap ortamı oluştu. Bu yüzden bir bakıma ölümsüzlüğe benzetiyorum. Buradan test edebilirsiniz. Eğer RAG sisteminin teknik detaylarını merak ediyorsanız bu blog yazısında açıklayacağım.

RAG nedir?

Bugünlerde makine öğrenmesinin bir sonucu olarak yapay zeka hayatımıza girdi. Bu yapay zekanın da iyi yaptığı şey aslında Large Language Model, yani büyük dil modelleri, yani bildiği veri üzerinden insan gibi konuşabilen ve bazı çıkarımlar yapabilen sistemler. LLM modellerinin bazı seviyelerde eksikleri var tabi ve bu da normal ve beklenen birşey. Mesela yeterli bilgiye sahip olmadığı zaman halüsinasyona girebiliyorlar. Eğitim verileri yanlı ise yapay zekası da yanlı olabiliyor. Yanlış bilgi ile eğitildikleri zaman yanlış da olsa bildiklerinden %100 emin olabiliyorlar. ChatGPT gibi yapay zekaların web araması veya research özellikleri olsa da her konuda güncel bilgilere sahip değiller. Fakat bunların hepsi bir yana, Bilmedikleri bir soru sorulduğunda ya halüsinasyona girerler yada bilmiyorum der ve susarlar, otur sıfır. :) Mesela bir e-ticaret sitesinde bir ürüne yapılan yorumu bilemezler. Dünkü maçı kimin kazandığını bilemezler. Benim blogda en son yazımda ne yazdığımı bilemezler. Bu konularda agent mantığında veya çeşitli araçlar yardımı ile yapay zeka sizin sisteminize entegre edilebilir. Fakat bütün sistemi toptan yapay zekaya bağımlı kılmak yerine, yapay zekaya özel bilginizi verip o bilgi üzerinden çıkarımını yapmasını isteyebilirsiniz. Yani RAG dediğimiz şey yapay zekaya context olarak bilgi sunmak ve bunu değerlendirip LLM tarzında bir cevaba çevirmesini istemektir. Context sizin datanızdır ve datanızdaki ilgili kısımları siz arayıp bulursunuz ve yapay zekaya sunarsınız. İşin temeli budur.

Sadece bir arama fonksiyonu yerine RAG sistemi kurgulamamın özel bir sebebi var. Blogda benim yazdıklarıma bakarak ilgili ifadeleri bulup benim gibi konuşabilen bir soru-cevap sistemi oluşturmak istedim. Bunu muhtemelen yapay zekanın bağlandığı bir "tool" ile yapabilirsiniz. Bu araç benim bloguma bağlanır, arama yapar, sonuçları alır ve bunlar üzerinden konuşabilir. Fakat her soru için blogdaki 100 yazıya veya ileride de yazacaklarıma bakması epeyce maliyetli olur muhtemelen. Ayrıca cevapların kendi tonlaması ile değil benim tarzımla yazılmasını ve sadece benim yazdıklarıma sadık kalmasını da istiyorum. Ayrıca bütün sistemin benim sunucumda çalışmasını ve 3. parti erişiminin olmamasını istiyorum. Elinizde sadece 2 dosya ve Flask sunucusu çalıştıran 1 adet python script 'i varsa, benim sistemimi ayağa kaldırmanız mümkün. Size öncelikle yapılacaklar listesini sunayım ama unutmayın, her adımın önemli detayları var.

  • Yazmak istediklerinizi bütünlük içerisinde paragraflar halinde yazın
  • Paragraflardaki belirsiz referansları düzeltin, mesela "this, that, it, the above" gibi
  • Yapay zekaya yazılarınızı chunk 'lara ayırmasını söyleyin
  • Yapay zeka ile embedding vektörlerini çıkarın
  • Bir vektör veritabanı oluşturun, ben bunun için FAISS.index ve pickle dosyası kullandım
  • Kullanıcıdan soruyu alın ve sorunun da embedding vektör bilgisini yapay zekadan alın
  • FAISS vektör veritabanında soru için oluşturulan vektörü arayın
  • Vektör araması sonucunda ilgili chunk 'ları yani yazıları pkl dosyasından alın
  • Chunk 'ları yapay zekaya context olarak verin ve cevabı yorumlamasını isteyin

Herşey veri ile başlıyor

Bir yapay zeka sistemi yapmak veya yapay zekayı icat etmek istiyorsanız, veri sizin başlangıç noktanızdır. Hiçbir gözetim olmadan veri çöplüğü toparlayıp yapay zekanın sihirli bir güç ile doğru anlamı çıkarmasını bekleyemezsiniz. Bir RAG sisteminde ilk işiniz verinizi chunk 'lara ayırılabilecek hale getirmektir. RAG sistemlerinde genelde veriniz resmi veya ses yerine yazılı formatta olur. Bu yüzden yazılarınızın okuyucu için değil, RAG sistemi için anlamlı olması gerekir.

Genelde blog yazılarımı kendi içinde tutarlı ve bütünlüklü paragraflar halinde yazmaya çalışıyorum. Bu yüzden bir yazıda bir paragrafı okuduğunuzda belli bir anlam çıkarabilirsiniz veya bir bilgi edinebilirsiniz. Ama bazen de yazı yazarken başka paragraflardaki ifadelere referans verirsiniz. Mesela sosyal medya konusunda bir yazı yazarken "bu davranış tehlikelidir" diye bir ifade görseniz, diğer paragraflara bakarak "burada bir üstteki paragrafta özetlediği insan davranışından bahsediyor" gibi bir çıkarım yapabilirsiniz. Çünkü konunun yani context 'in geneli zaten hafızanızdadır.

Ama bir RAG sisteminde bir blog yazısının tamamını context olarak vermek çok anlamlı değildir. Çünkü gereksiz ve alakasız veriler sunarak yapay zekanın kafasının karışmasına sebep olabilir. Hatta belki de 5 farklı blog yazısında soru ile ilgili birşeyler yazılmış ise 5 farklı blog yazısı sunmak zorunda kalabilirsiniz. Bu yüzden size veri chunk 'ları yani parçacıkları gerekir. Veriyi chunkify etmenin farklı önerilen yolları var ve hepsi de bir cümle veya paragraf ile spesifik bir veri çıkarmaya uğraşıyor ama hepsinin kendi içinde bazı zayıf noktaları var. Zaten bu işlem aşırı sofistike bir işlem ve her zaman hatasız çalışan bir chunkify işlemi mümkün değil. Ben chunk oluşturma işlemini yapay zekaya devrettim. Ama bu bile bazı hazırlıklar gerektirdi. Çünkü dediğim gibi paragraflarda kendi içinde ne olduğu anlaşılmayan referanslar olabilir ve halüsinasyonlara sebep olabilir.

Özetle, ilk yapmanız gereken kendi içinde tutarlı ve bütüncül paragraflar yazmaktır. Sonrasında yapay zeka yardımı ile belirsiz referansları düzelttirebilirsiniz veya kendiniz de düzeltebilirsiniz. Mesela "bu insanlar" yerine "insanların fakirliğinden faydalanan insanlar" şeklinde yazabilirsiniz. Normalde bu şekilde yazdığınızda okuyucu için çok saçma olacaktır. Çünkü "bu insanlar" dan daha önce bahsedilmiştir. Ama RAG için, bu uzun açıklama o paragrafın anlaşılır bir chunk haline dönüşmesi demektir. Yine de yazılarınızı kendi içinde tutarlı yazabiliyorsanız, bir blog postunu aşağıdaki prompt ile RAG-ready hale getirebilirsiniz:

You are a RAG optimization assistant. I am trying to make my blog posts RAG optimized. The following blog post will be stored as chunks with gpt4o, mostly 1 paragraph - 1 chunk. After that i will create embedding and faiss index files. So you get the idea. Your job is to identify and rewrite the vague or referential sentences (like those using 'he', 'she', 'this', 'that', 'the second', 'above', etc.) that would break context when chunked. DO NOT change any tags like [REF:], [IMAGE:], [ARCHETYPE:], [LAWARTICLE:] they are intentional. DO NOT add or remove paragraphs — replace vague sentences only. Return the entire blog post back to me after changing the problematic sentences in paragraphs. Don't explain or add new things. Here is the blog post:

Böylece chunk 'lara bölünebilecek bir blog postunuz olur. Aralarda "[REF:]" gibi ifadeler var. Bunlar zorunlu olmasa da blog yazıları arasında veya blog yazısının içinde referans verdiğim verilerin yakalanması için gerekli olmuştu. Bir sonraki adımımız blog yazısından chunk 'ları oluşturmak.

Chunkification

Chunk oluşturma işlemine chunkification diyorum :) Hiç python bilmememe rağmen aşağıdaki script 'lerin hepsini chatgpt 'ye yazdırabildim. Aşağıdaki script, "post.txt" isimli dosyayı okuyor. Bu dosyada blog yazıları "=== POST START: başlık ===" ve "=== POST END ===" ifadeleri ile birbirinden ayrışıyor. Çok ilkel bir yöntem ama çalışıyor. Bütün blog yazıları bulunuyor ve bir döngü ile yapay zekaya tek tek gönderiliyor. Burada blog yazısını chunk 'lara ayırmasını söyleyen bir prompt var. Yazıları bütüncül paragraflar halinde yazabilirseniz genelde 1 paragraf 1 chunk oluyor. Burada benim sistemim gpt-4o üzerine kurulu ve gpt-5 kullanırsanız api çağrıları farklı oluyor.

Bir de keyword 'ler yani anahtar kelimeler var. Her chunk için blog yazısının tamamını göz önünde bulundurarak anahtar kelimeler oluşturmasını istedim. Bu sayede vektör araması daha sağlıklı olabiliyor. Mesela "sorgulamak" gibi çok soyut bir kavram bile arasanız, bazı paragraflar dolaylı olarak o soyut kavramdan bahsediyor olabilir. Aşağıdaki scriptte chunk 'lara ayırdıktan sonra her chunk için anahtar kelime çıkaran kodlar da bulunuyor. Buradaki detayı kaçırmayın, chunk sadece kendi içinde değerlendirilmiyor. Bütün blog yazısı göz önüne alınıyor. Çünkü bir chunk kendisi dışında blog yazısında farklı referanslar veriyor olabilir. Dar bir bakış açısı ile anahtar kelime çıkarmak hatalı olur.

			
import json, re, time
from openai import OpenAI

client = OpenAI(api_key="sk-proj-xxxxx")
input_file = "post.txt"
output_file = "chunks.json"
model = "gpt-4o"

def split_posts(raw_text):
    return re.findall(r"=== POST START: (.*?) ===\n(.*?)=== POST END ===", raw_text, re.DOTALL)

def chunkify(text, title="(unknown)", max_attempts=5, sleep_seconds=3):
    system_prompt = (
        "You are a JSON chunking assistant for a blog archive. "
        "Split the blog-style text into coherent, context-preserving JSON chunks. "
        "Each chunk must be a dictionary with keys: 'id', 'text', 'source', and 'section' (string). If no section header applies, set 'section' to ''.\n\n"
        "- Preserve paragraph boundaries.\n"
        "- Keep formatting like SECTION headers, [REF:], [IMAGE:], etc.\n"
        "- Do NOT summarize or modify meaning.\n"
        "- Use numeric chunk IDs like 'post-1-chunk-1'.\n"
        "- 'source' must contain the blog post title."
    )

    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": f"Split this blog post into JSON chunks:\n\n{text}"}
    ]

    for attempt in range(1, max_attempts + 1):
        try:
            response = client.chat.completions.create(
                model=model,
                messages=messages
            )
            raw_output = response.choices[0].message.content.strip()

            try:
                return json.loads(raw_output)
            except json.JSONDecodeError:
                pass

            raw_output = re.sub(r"^```json", "", raw_output, flags=re.MULTILINE).strip()
            raw_output = re.sub(r"```$", "", raw_output, flags=re.MULTILINE).strip()

            try:
                return json.loads(raw_output)
            except json.JSONDecodeError:
                pass

            objects = re.findall(r"\{.*?\}", raw_output, re.DOTALL)
            if not objects:
                print(f"⚠️ Attempt {attempt}: No JSON chunks found. Raw:\n{raw_output[:300]}")
                raise ValueError("No JSON chunks found in GPT output")

            return [json.loads(obj) for obj in objects]

        except Exception as e:
            print(f"❌ Chunkify attempt {attempt} failed: {e}")
            if attempt < max_attempts:
                time.sleep(sleep_seconds * attempt)
            else:
                raise

def extract_keywords(text, max_attempts=5, sleep_seconds=2):
    system_prompt = (
        "You are a keyword extraction assistant.\n"
        "Extract **between 1 and 6** concise keywords that reflect the main topics, concepts, or people in the text if possible.\n"
        "Avoid vague words like 'this', 'thing', or 'example'.\n"
        "Use singular forms only — e.g., write 'flower' instead of 'flowers'.\n"
        "Return the keywords **ONLY** as a JSON array of strings, with no explanations or extra text.\n"
    )

    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": text}
    ]

    for attempt in range(1, max_attempts + 1):
        try:
            response = client.chat.completions.create(
                model=model,
                messages=messages
            )
            content = response.choices[0].message.content.strip()
            content = re.sub(r"^```json", "", content, flags=re.MULTILINE).strip()
            content = re.sub(r"```$", "", content, flags=re.MULTILINE).strip()

            match = re.search(r"\[\s*(?:\"[^\"]*\"\s*,?\s*)+\]", content, re.DOTALL)
            if match:
                return json.loads(match.group(0))
            elif attempt == max_attempts:
                print(f"⚠️ Giving up on keywords after {attempt} attempts. Returning empty list.")
                return []

        except Exception as e:
            print(f"❌ Keyword attempt {attempt} failed: {e}")
            if attempt < max_attempts:
                time.sleep(sleep_seconds * attempt)
            else:
                print("⚠️ Final keyword extraction failure. Returning empty list.")
                return []

if __name__ == "__main__":
    with open(input_file, "r", encoding="utf-8") as f:
        raw_text = f.read()

    posts = split_posts(raw_text)
    chunks, global_id = [], 1

    for i, (title, body) in enumerate(posts, 1):
        print(f"✂️ Chunkifying: {title}")
        try:
            chs = chunkify(body, title)
            for idx, c in enumerate(chs):
                c["id"] = global_id
                c["source"] = title
                c["section"] = c.get("section", "")

                # 👇 Keywordify with full blog context
                keyword_prompt = (
                    "Given the following full blog post, extract 1–6 concise keywords for the INDIVIDUAL PARAGRAPH below.\n\n"
                    "Avoid vague words like 'thing', 'this', 'example'.\n"
                    "Use singular nouns, no extra text.\n\n"
                    "FULL POST:\n"
                    f"{body.strip()}\n\n"
                    "TARGET PARAGRAPH:\n"
                    f"{c['text'].strip()}"
                )

                c["keywords"] = extract_keywords(keyword_prompt)
                global_id += 1
                chunks.append(c)

        except Exception as e:
            print(f"❌ Failed on '{title}': {str(e)}")

    if chunks:
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(chunks, f, indent=2, ensure_ascii=False)
        print(f"✅ {output_file} written with {len(chunks)} chunks.")
    else:
        print("❌ No chunks generated.")
			
		

Böylece "python chunkify.py" komutu ile bu dosyayı isimlendirir ve çalıştırırsanız, aşağıdaki gibi chunk 'lardan oluşan bir json çıktınız olur. Aşağıdaki "source" kısımları blog yazısının başlığını içermektedir.

			
{
    "id": 123,
    "text": "Long story short, we started sharing fake things on social media. Then [REF: Algorithms] shaped our habits and we started sharing things that we don't ACTUALLY believe or have. Then we normalized it and built a life on top of it. Then we got used to fake content and behaviour and artificial intelligence fueled it. And we ended up having fake video and sound compositions, which is an awful state. There will soon be a point where we say \"Damn, i can't tell what is real or what is fake on the internet\". I have a very simple caution against this. I also call this the 2000's philosophy which is shaped by politicians and rich people. It is just three words: \"I don't believe\". Then i guess the next statement could be \"i question\". You know Descartes said i think therefore i am, or i doubt therefore i am. We could say i question because i don't f..ing believe.",
    "source": "Ordinarity of fake",
    "keywords": [
      "social media",
      "algorithm",
      "artificial intelligence",
      "fake content",
      "Descartes",
      "philosophy"
    ]
}
			
		

Index ve pickle dosyalarını oluşturmak

Böylece chunks.json dosyasını oluşturmuş olursunuz. Sırada bir vektör veritabanına vektörleri yazma işleminde. Bunun için chunk 'larınızı embedding vektörlerine çevirmeniz gerekiyor. Sonuçta oluşan verinin büyüklüğü orijinal text verisinden 10 kat falan büyük oluyor. Vektörleri saklayabilen veritabanları var ama ben sırf bu iş için veritabanı kullanmak istemedim. Zaten hayatım boyunca yazabileceğim şeylerin hepsini bile toplasanız, modern bir işlemciyi yoracak kadar vektör araması işlemi yapacak bir durum oluşamaz. Vektör işlemleri temelde matematiksel işlemler ve her zaman süper bir GPU gerektirmeyebiliyor. Benim yazılarımda birşeyler aramak minicik bir işlem. Ama embedding vektörü oluşturmak büyük bir iş ve yapay zekanın yapması gerekiyor. Embedding ile sizin bir chunk 'ınız, 3000 'den fazla boyutu olan bir vektöre dönüşebiliyor. Yapay zeka dünyasında her yapay zekanın kendi embedding 'i bulunabiliyor. Ben en güncel ve güvenilir olarak chatgpt 'nin "embedding large" olanını kullandım. Başka embedding 'ler de seçmek isteyebilirsiniz, size kalmış.

Aşağıdaki script ile index ve pickle dosyalarını oluşturuyorum. Burada dosya tabanlı FAISS yani (Facebook AI Similarity Search) kullanıyorum. FAISS alternatifi olarak HNSW (Hierarchical navigable small world) deneyebilirsiniz. Burada ayrıca pkl dosyasına ihtiyaç duymamın sebebi, FAISS index dosyasında sadece vektör index bilgisinin bulunması. Pickle dosyasında ise bütün yazılarım metin olarak saklanıyor. Yani index dosyasında vektör araması yapıyorsunuz ve bulunan id 'leri pickle dosyasında arayıp chunk 'ları getiriyorsunuz. Burada hatırlamadığım birşey var, bir sebepten dolayı chatgpt vektör normalizasyonu yapmamı tavsiye etti. Matematiksel birşey olduğuna eminim ama ne olduğundan emin değilim :)

			
import json, faiss, pickle, os
import numpy as np
from openai import OpenAI

client = OpenAI(api_key="sk-proj-xxxx")

def delete_file_if_exists(path):
    if os.path.exists(path):
        os.remove(path)
        print(f"🗑️ Deleted existing file: {path}")

def embed_openai(texts):
    response = client.embeddings.create(model="text-embedding-3-large", input=texts)
    return np.array([np.array(e.embedding, dtype="float32") for e in response.data])

if __name__ == "__main__":
    # Clean up old files
    delete_file_if_exists("faissgpt.index")
    delete_file_if_exists("embeddings_faissgpt.pkl")

    with open("chunks.json", "r", encoding="utf-8") as f:
        chunks = json.load(f)

    print(f"🔄 Embedding {len(chunks)} chunks with OpenAI...")

    texts = [
    f"[SECTION: {chunk.get('section', '')}] {chunk['text']} Keywords: {', '.join(chunk.get('keywords', []))}"
    for chunk in chunks
    ]
    vectors = embed_openai(texts)

    faiss.normalize_L2(vectors)
    index = faiss.IndexFlatL2(vectors.shape[1])
    index.add(vectors)

    faiss.write_index(index, "faissgpt.index")
    with open("embeddings_faissgpt.pkl", "wb") as f:
        pickle.dump(chunks, f)

    print("✅ GPT FAISS index and metadata saved.")
			
		

İşin eğlenceli kısmı

Bu aşamaya kadar, eğer bütüncül paragraflar yazabiliyorsanız, güzel chunk 'larınız olacakır. Bu chunk 'ları rahatlıkla embedding vektörlerine çevirebilir ve index ve pkl dosyalarında saklayabilirsiniz. Çok fazla hata yapılacak nokta yok gibi. Fakat şimdi, sizin gibi konuşabilen bir yapay zeka oluşturmaya çalışacaksınız. Yani, sizin bildiğiniz şeyleri context olarak alıp bunlara sadık kalacak ve kendi bilgisini işin içine karıştırmayacak. Ayrıca yazılarınızdaki bazı gizli anlamları çıkarabilecek ama halüsinasyona da girmeyecek. Sizin tarzınızda sizin kelimelerinizle cevap verebilecek ama dümdüz "numan bunu dedi" şeklinde olmayacak. Bütün bunların çalışması için yapay zekaya rolünü söylemeniz, ilgili context 'i vermeniz ve hangi chunk 'ın ne kadar soru ile alakalı olduğu bilgisini de vermeniz gerekiyor. Aşağıdaki scripti inceleyebilirsiniz.

			
from flask import Flask, request, jsonify, send_from_directory
from openai import OpenAI
import faiss
import pickle
import numpy as np
import webbrowser
import threading
import os

# Constants
TOP_K = 50          # how many candidates to fetch
MAX_CHUNKS = 40     # target max chunks to send to GPT
MIN_CHUNKS = 20     # always keep at least this many

# Load memory
index = faiss.read_index("faissgpt.index")
with open("embeddings_faissgpt.pkl", "rb") as f:
    chunks = pickle.load(f)

# OpenAI client
client = OpenAI(api_key="sk-proj-xxxx")

# Flask app
app = Flask(__name__)

@app.route("/")
def serve_html():
    return send_from_directory(".", "index.html")

def open_browser():
    webbrowser.open_new("http://localhost:5000")

@app.route("/ask", methods=["POST"])
def ask():
    data = request.get_json()
    question = data.get("question", "")
    if not question:
        return jsonify({"error": "No question provided."}), 400

    # Step 1: Translate question to English (for embedding)
    translation_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "Translate the following question into English. If the question is already in English, just return it. Do not explain anything. Only return the answer."
            },
            {
                "role": "user",
                "content": question
            }
        ]
    )
    translated_question = translation_response.choices[0].message.content.strip()

    # Step 2: Embed question and normalize vector
    embed_response = client.embeddings.create(
        model="text-embedding-3-large",
        input=translated_question
    )
    q_vector = np.array(embed_response.data[0].embedding, dtype="float32").reshape(1, -1)
    faiss.normalize_L2(q_vector)

    # Step 3: Search FAISS (always return top K)
    D, I = index.search(q_vector, TOP_K)
    scored = sorted(zip(D[0], I[0]), key=lambda x: x[0])
    scored_with_relevance = [(1 - dist / 2, i) for dist, i in scored]

    # Filter by relevance >= 0.25
    filtered = [item for item in scored_with_relevance if item[0] >= 0.25]

    # Guarantee at least MIN_CHUNKS
    if len(filtered) < MIN_CHUNKS:
        filtered = scored_with_relevance[:MIN_CHUNKS]

    # Trim to MAX_CHUNKS
    ranked = filtered[:MAX_CHUNKS]

    # Step 4: Build context
    context = "\n\n".join(
        f"[{chunks[i].get('source', '')} | relevance={relevance:.4f}]\n{chunks[i]['text']}"
        for relevance, i in ranked
    )
    prompt = f"""
Use only the context below to answer it. Reason precisely. Respond in English. Be accurate, structured, and specific. If the context does not support an answer, say so clearly.

Context:
{context}

Question:
{translated_question}

Answer:"""

    # Step 5: Ask GPT
    chat_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "You are NUMAIN. You are a retrieval-based AI assistant that only responds based on the writings of Numan — a brutally honest but thoughtful thinker who avoids fluff and corporate talk. Numan's tone is blunt but kind, critical of modern systems and prefers logic and clear reasoning over emotional persuasion. He is helpful, he doesn't hesitate giving direct information. He tries to be like an open book. Never make things up. Never generalize beyond what Numan has written in the context part of the prompt. Prioritize realism and Numan’s grounded reasoning, but don’t ignore philosophical depth if it’s present in the source material. If the question yields to more details or other subjects, briefly hint them in the end of the answer. If the context allows, provide a thorough and detailed answer. Do not add anything that is not directly supported by the context. You can also mention at most 1 or 2 most related blog posts to read from the provided context, not more than 2 posts. If you don’t know the answer based on Numan’s writings, say so clearly. When chunks in the context include a relevance value between 0 and 1, prefer higher values when answering. If the user input is not a question but a clear topic or statement, respond with Numan’s related thoughts following the same rules. If nothing relevant is in the context, say so. You will always receive many context chunks: do not merely summarize or collapse them, and do not parrot them verbatim; instead, synthesize and explain in Numan’s own voice, preserving important details while keeping the response coherent and non-repetitive. When Numan’s wording carries hidden meaning, irony, or mockery, frame the answer so that the subtext is acknowledged, not just the surface meaning."},
            {"role": "user", "content": prompt}
        ],
        max_tokens=1000,
        temperature=0.4
    )

    answer = chat_response.choices[0].message.content.strip()
    return jsonify({"answer": answer})

if __name__ == "__main__":
    print("🔥 Starting NUMAIN backend...")
    
    if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
        threading.Timer(2, open_browser).start()
    app.run(debug=True, port=5000)
			
		

Bu script python ile Flask sunucusu ayağa kaldırıyor ve index.html dosyasını localhost:5000 adresinde sunuyor. Ayrıca otomatik olarak bu adreste browser açıyor. Böylece arkadaki "/ask" endpointini post isteği ile çağırabiliyorsunuz veya aşağıdaki gibi bir javascript kodu da yazabilirsiniz. Endpoint kısmı web tarafını ilgilendiriyor. Gerisi RAG sistemimiz.

Önce soruyu ingilizceye çeviriyorum. Çünkü blog yazılarım ingilizce tutuluyor ve sistemi de ingilizce kurguladım. Embedding vektörleri de ingilizce olarak alınıyor. Bu ekstra bir api çağrısı ama çok da maliyetli olmuyor. Sonra ingilizceye çevirilen sorunun da embedding vektörünü api ile alıyorum. Burada da önceki adımdaki gibi embedding large şeysini kullanıyorum. Sonra index dosyasında benzerlik araması yapıp pickle dosyasından chunk 'ları getiriyorum. Burada TOP_K yani maksimum alınacak chunk sayısını 50 olarak belirledim. Bu 50 chunk, çok farklı blog yazılarından gelebilir. Zaten en ilgililer orada mutlaka olacak. Burada yapay zekaya en ilgili chunk 'ları en üstte vermek için sıralama yaptım. Sonra en ilgisiz olan 10 tanesini otomatik olarak eledim. Buna mecbur değildim ama böyle yaptım böyle kaldı. Sonra benzerlik oranı 0.25 'in altında olan chunk 'ların çok az alakalı olduğunu fark ettim ve en son 20 tanesi kalana kadar en az ilgili olanları listeden çıkardım. Çünkü çok garip bir soruda bile matematiksel olarak benzer şeyler yazmış olabilirim.

En zor kısmı da yapay zekanın benim gibi konuşması oldu. Bazen tam olarak bilmediğimiz bir konuda aklımıza geleni söyleriz. Ama numain 'de böyle olmasını istemedim, bilmediği şeyler hakkında konuşmaması gerekiyor. Veya sorunun içinde küçük ipuçları varsa ve yapay zekanın bunları da yakalamasını istiyorum. Bazen benim yazdıklarımda alt-metin diyebileceğimiz anlamlar olabiliyor ve sistemin bunu da yakalamasını istiyorum. Bazen de soru sorulmasa bile söylenen ifade hakkında birşeyler yazdıysam cevap vermesini istiyorum. Bu scriptte "temperature" parametresi var. Bu parametre gpt-4o 'ya ne kadar katı olması gerektiğini söylüyor. 0 'a yakın olursa her zaman aynı cevabı veriyor ve context 'e aşırı bağlı kalıyor. 1 'e yakın olursa da kendi bildiği gibi konuşmaya başlıyor. Yani özetle, sadece context olarak yazdıklarımı tekrarlamayan ama halüsinasyona da girmeden ve sıradan bir özet yapmayan bir yapay zeka istedim. Bu yüzden soru, context ve prompt dışında gpt-4o 'ya mesaj kısmında rol olarak büyük bir ifade yazdım.

Böylece sistem ayağa kalktı ve sadece restful endpoint 'leri çalıştırmak kaldı. Aşağıdaki basit javascript kodu ile post isteğini gönderebilirsiniz. Backend kısmı bitmiş oldu.

			
try {
	const res = await fetch("/ask", {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ question })
	});

	const data = await res.json();
	answerDiv.innerText = data.answer;
	} catch (err) {
		answerDiv.innerText = "Error fetching answer.";
	}
			
		

YAŞIYOR !!!

Bu noktada parça parça Frankenstein oluşturmuş gibi hissettim kendimi :) Bütün bu sistemden öğrendiğim şu oldu: datanızı organize etmeli ve yapay zekadan mucize beklememelisiniz. Yapay zekanın herşeyi bilmesini beklemeden ve bilgisine bağımlı olmadan RAG sistemlerini güvenle kurgulayabilirsiniz. Yapay zekaya çok fazla direkt bağımlılık biraz tehlikeli geliyor bana.

Teknik olarak bakıldığında bu sistemi kurgulamak için kendi chunkification mantığınızı oluşturabilirsiniz. Ama ben önce biraz veri üzerinde uğraşıp chunk 'lara ayırma işlemini yapay zekaya bırakma taraftarıyım. text-embedding-3-large dışında başka yapay zeka modellerinin embedding vektörlerini kullanabilirsiniz. Ama 2 yerde de aynısını kullanmak zorundasınız. Vektör verilerini saklamak ve aramak için FAISS dışında yöntemler de var. Bunların üstüne zaten ChatGPT yerine istediğiniz yapay zeka LLM modelini kullanabilirsiniz. Tabi bu durumda python 'daki api çağrıları da değişiyor. Genel RAG çerçevesi bu şekilde. Daha farklı bir yöntem olarak milyarlarca parametre ile çalışan açık LLM modellerinden birisini kendi veriniz ile eğitebilir ve api çağırmadan tamamen offline çalışabilirsiniz. Ama bu vur dedik öldürdün türünde birşey olur. Çok küçük LLM modellerinden birisini kullanırsanız da sistem büyük modeller kadar güzel konuşamaz. Chatgpt üzerinde custom gpt oluşturabiliyorsunuz fakat o yöntemde çok fazla "kendisi gibi" konuşuyor veya inisiyatif alıyor.

Böylece, blogdaki 100. yazımı yazmaya yaklaştığım sırada AI dünyasında RAG sistemini görmüş ve çalıştırabilmiş oldum. Biraz da tesadüf oldu. Bu kadar yazıyı okuyamam, bana şunu söyle dediğiniz birşey varsa soru sormayı deneyebilirsiniz. Bir sonraki yazıda görüşmek üzere :)


Bir yorum yazabilirsiniz