RAG Αρχιτεκτονική και Εσωτερικά: Πώς Λειτουργεί Πραγματικά (ελληνικά (Greek))

RAG Αρχιτεκτονική και Εσωτερικά: Πώς Λειτουργεί Πραγματικά

Saturday, 22 November 2025

//

25 minute read

Το Μέρος 1Κατανοείτε την έννοια υψηλού επιπέδου: να ανακτήσει τις σχετικές πληροφορίες, στη συνέχεια να το χρησιμοποιήσετε για να δημιουργήσετε απαντήσεις. Τώρα βουτάμε βαθιά στην τεχνική αρχιτεκτονική;Ακριβώς πώς λειτουργούν τα συστήματα RAG κάτω από το καπό, από στρατηγικές κοπής σε LLM εσωτερικά, όπως μάρκες και KV caches.

Εισαγωγή

Πλοήγηση σειράς: Αυτό είναι το μέρος 2 της σειράς RAG:

Αν δεν έχετε διαβάσει το Μέρος 1, προτείνω να ξεκινήσετε από εκεί για να καταλάβετε:

  • Τι είναι το RAG και γιατί έχει σημασία
  • Η ιστορία από την αναζήτηση λέξεων-κλειδιών στη σημασιολογική κατανόηση
  • Ο παρών κανονισμός αρχίζει να ισχύει την εικοστή ημέρα από τη δημοσίευσή του στην Επίσημη Εφημερίδα της Ευρωπαϊκής Ένωσης.

Αυτό το άρθρο υποθέτει ότι καταλαβαίνετε αυτά τα βασικά και επικεντρώνεται σε τεχνική αρχιτεκτονική, λεπτομέρειες εφαρμογής και εσωτερικά LLM.

Πώς λειτουργεί το RAG: Η Πλήρης Εικόνα

Ας αναλύσουμε ακριβώς τι συμβαίνει σε ένα σύστημα RAG, από τη στιγμή που θα προσθέσετε ένα έγγραφο στο πότε ένας χρήστης παίρνει μια απάντηση.

Φάση 1: Ευρεσιτεχνία (Προετοιμασία της Βάσης Γνώσης)

Πριν RAG μπορεί να ανακτήσει οτιδήποτε, θα πρέπει να αξιολογήσετε τη βάση γνώσεων σας. Αυτή είναι μια διαδικασία μιας φοράς (αν και μπορείτε να προσθέσετε νέα έγγραφα αργότερα).

flowchart TB
    A[Source Documents] -->|1. Extract Text| B[Text Extraction]
    B -->|2. Split into Chunks| C[Chunking Service]
    C -->|3. Generate Embeddings| D[Embedding Model]
    D -->|4. Store Vectors| E[Vector Database]

    B -.Metadata.-> E

    subgraph "Example: Blog Post"
        F["Understanding Docker: A containerization platform..."]
    end

    subgraph "Chunks"
        G["Chunk 1: Title + Intro"]
        H["Chunk 2: Benefits Section"]
    end

    subgraph "Embeddings"
        I["0.234, 0.891, 0.567, ..."]
        J["0.445, 0.123, 0.789, ..."]
    end

    F --> G
    F --> H
    G --> I
    H --> J

    style D stroke:#f9f,stroke-width:2px
    style E stroke:#bbf,stroke-width:2px

Βήμα 1: Εξαγωγή κειμένου

Απόσπασμα απλού κειμένου από τα αρχικά σας έγγραφα.

  • Markdown αρχεία (όπως αναρτήσεις blog μου)
  • PDF (για τεκμηρίωση)
  • HTML (για ξύσιμο ιστού)
  • Αρχεία βάσης δεδομένων
  • Emails, αρχεία καταγραφής συνομιλίας, κλπ.

Παράδειγμα από το blog μου:

// From MarkdownRenderingService
public string ExtractPlainText(string markdown)
{
    // Remove code blocks
    var withoutCode = Regex.Replace(markdown, @"```[\s\S]*?```", "");

    // Convert markdown to plain text
    var document = Markdown.Parse(withoutCode);
    var plainText = document.ToPlainText();

    return plainText.Trim();
}

Βήμα 2: Τσάνκινγκ

Δεν μπορείτε απλά να χωρίσετε στα όρια των παραγράφων - χρειάζεστε σημασιολογικά συνεκτικά κομμάτια.

Γιατί το πνιγμό έχει σημασία:

  • LLMs έχουν συμβολικά όρια (παράθυρα κειμένου)
  • Μικρότερα κομμάτια = ακριβέστερη ανάκτηση
  • Αλλά τα κομμάτια πρέπει να περιέχουν αρκετά συμφραζόμενα για να έχουν νόημα

Κακό κουδούνισμα:

Chunk 1: "Docker is a containerization platform. It allows you"
Chunk 2: "to package applications with their dependencies. This"
Chunk 3: "ensures consistency across environments."

Καλό κουδούνισμα:

Chunk 1: "Docker is a containerization platform. It allows you to package applications with their dependencies. This ensures consistency across environments."

Chunk 2: "Benefits of Docker:
- Isolation: Each container runs in its own environment
- Portability: Containers run anywhere Docker is installed
- Efficiency: Lightweight compared to virtual machines"

Παράδειγμα από τη σημασιολογική μου εφαρμογή αναζήτησης:

public class TextChunker
{
    private const int TargetChunkSize = 500; // ~500 words
    private const int ChunkOverlap = 50;     // 50 words overlap

    public List<Chunk> ChunkDocument(string text, string sourceId)
    {
        var chunks = new List<Chunk>();

        // Split on section boundaries first (## headers in markdown)
        var sections = SplitOnHeaders(text);

        foreach (var section in sections)
        {
            // If section is small enough, keep it whole
            if (section.WordCount < TargetChunkSize)
            {
                chunks.Add(new Chunk
                {
                    Text = section.Text,
                    SourceId = sourceId,
                    SectionHeader = section.Header
                });
            }
            else
            {
                // Split large sections on sentence boundaries
                var subChunks = SplitOnSentences(section.Text, TargetChunkSize, ChunkOverlap);
                chunks.AddRange(subChunks.Select(c => new Chunk
                {
                    Text = c,
                    SourceId = sourceId,
                    SectionHeader = section.Header
                }));
            }
        }

        return chunks;
    }
}

Κοινές στρατηγικές πνιγμού:

  • Σταθερό μέγεθος: Απλό αλλά σπάει σημασιολογικά όρια
  • Με βάση την ποινή: Σέβεται τη γραμματική αλλά μπορεί να είναι πολύ μικρή
  • Με βάση την παράγραφο: Φυσικό αλλά μεταβλητό μέγεθος
  • Με βάση το τμήμα: Καλύτερο για δομημένο περιεχόμενο (προτίμησή μου)
  • Συρόμενο παράθυρο με επικάλυψη: Εξασφαλίζει ότι δεν χάνεται κανένα πλαίσιο στα όρια

Βήμα 3: Δημιουργία ενσωμάτωσης

Μια ενσωμάτωση είναι ένα διάνυσμα (συστοιχία αριθμών) που αντιπροσωπεύει την έννοια του κειμένου.

Βασική έννοια: Παρόμοιες έννοιες → παρόμοιοι φορείς

"Docker container" → [0.234, -0.891, 0.567, ..., 0.123]
"containerization platform" → [0.221, -0.903, 0.534, ..., 0.119]
"apple fruit" → [0.891, 0.234, -0.567, ..., -0.789]

Οι δύο πρώτοι φορείς θα είναι "κοντά" στο διανυσματικό χώρο (υψηλή ομοιότητα συνημίτονου), ενώ ο τρίτος είναι μακριά.

Πώς δημιουργούνται οι παρεμβολές: Μοντέρνα μοντέλα ενσωμάτωσης είναι νευρικά δίκτυα εκπαιδευμένα σε μαζικά σύνολα κειμένου για να μάθουν σημασιολογικές σχέσεις.

  • Όλα-MiniLM-L6-v2: 384 διαστάσεις, γρήγορη, καλή ποιότητα (τι χρησιμοποιώ σε αυτό το blog)
  • text-embedding-3-small (OpenAI): 1536 διαστάσεις, πολύ υψηλής ποιότητας
  • Βάση BGE: 768 διαστάσεις, state-of-the-art open source

Παράδειγμα από την υπηρεσία προσθήκης ONNX μου:

public async Task<float[]> GenerateEmbeddingAsync(string text)
{
    // Tokenize the input text
    var tokens = Tokenize(text);

    // Create input tensors for ONNX model
    var inputIds = CreateInputTensor(tokens);
    var attentionMask = CreateAttentionMaskTensor(tokens.Length);
    var tokenTypeIds = CreateTokenTypeIdsTensor(tokens.Length);

    // Run ONNX inference
    var inputs = new List<NamedOnnxValue>
    {
        NamedOnnxValue.CreateFromTensor("input_ids", inputIds),
        NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask),
        NamedOnnxValue.CreateFromTensor("token_type_ids", tokenTypeIds)
    };

    using var results = _session.Run(inputs);

    // Extract the output (sentence embedding)
    var output = results.First().AsTensor<float>();
    var embedding = output.ToArray();

    // L2 normalize the vector for cosine similarity
    return NormalizeVector(embedding);
}

Γιατί η ομαλοποίηση έχει σημασία: Μετά την ομαλοποίηση L2, η ομοιότητα συνημίτονου γίνεται ένα απλό προϊόν κουκκίδων, κάνοντας την αναζήτηση πολύ γρηγορότερα.

Βήμα 4: Κατάστημα σε βάση δεδομένων Vector

Σε αντίθεση με τις παραδοσιακές βάσεις δεδομένων που χρησιμοποιούν ερωτήματα SQL, οι διανυσματικές βάσεις δεδομένων χρησιμοποιούν αναζήτηση ομοιότητας.

Βασικές πράξεις:

  • Επάνω: Προσθήκη ή ενημέρωση φορέα με μεταδεδομένα
  • Αναζήτηση: Βρείτε K πιο παρόμοια διανυσματικά μέσα σε ένα διάνυσμα ερώτημα
  • Φίλτρο: Συνδυάστε διανυσματική αναζήτηση με φίλτρα μεταδεδομένα

Παράδειγμα εφαρμογής Qdrant:

public async Task IndexDocumentAsync(
    string id,
    float[] embedding,
    Dictionary<string, object> metadata)
{
    var point = new PointStruct
    {
        Id = new PointId { Uuid = id },
        Vectors = embedding,
        Payload =
        {
            ["title"] = metadata["title"],
            ["source"] = metadata["source"],
            ["chunk_index"] = metadata["chunk_index"],
            ["created_at"] = DateTime.UtcNow.ToString("O")
        }
    };

    await _client.UpsertAsync(
        collectionName: "blog_posts",
        points: new[] { point }
    );
}

Δημοφιλείς βάσεις δεδομένων διάνυσμα:

  • QdrantCity name (optional, probably does not need a translation): Γρήγορη, αυτο-ξεχωριστή, εξαιρετική υποστήριξη C# (η επιλογή μου)
  • pgvector: Επέκταση PostgreSQL (μεγάλη αν χρησιμοποιείτε ήδη Postgres)
  • Πινκόνη: Managed service (ακριβό αλλά καλό)
  • Υγρασία: Πλούσιο χαρακτηριστικό, καλό για σύνθετα σχήματα
  • ChromaDBCity name (optional, probably does not need a translation): Python-εστιασμένη, ελαφριά

Θα εξερευνήσουμε τη δημιουργία αυτών των βάσεων δεδομένων σε επερχόμενα άρθρα.

Φάση 2: Ανάκτηση (Εύρεση σχετικών πληροφοριών)

Όταν ένας χρήστης κάνει μια ερώτηση, το σύστημα RAG πρέπει να βρει τις πιο σχετικές πληροφορίες από τη βάση γνώσεων.

flowchart LR
    A["User Query:<br/>'How do I use Docker Compose?'"] --> B[Generate Query Embedding]
    B --> C["Query Vector:<br/>[0.445, -0.123, ...]"]
    C --> D[Vector Search]
    D --> E[Vector Database]
    E --> F[Top K Similar Chunks]
    F --> G["Results:<br/>1. Docker Compose Basics 0.92<br/>2. Multi-Container Setup 0.87<br/>3. Service Configuration 0.83"]

    style B stroke:#f9f,stroke-width:3px
    style D stroke:#bbf,stroke-width:3px

Βήμα 1: Δημιουργία προσθήκης ερωτημάτων

Η ερώτηση του χρήστη μετατρέπεται σε ένα διάνυσμα με τη χρήση του το ίδιο μοντέλο ενσωμάτωσης Αυτό είναι κρίσιμο - διαφορετικά μοντέλα παράγουν ασύμβατους φορείς.

public async Task<List<SearchResult>> SearchAsync(string query, int limit = 10)
{
    // Same embedding model used for indexing
    var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query);

    // Search in vector store
    var results = await _vectorStoreService.SearchAsync(
        queryEmbedding,
        limit
    );

    return results;
}

Βήμα 2: Αναζήτηση ομοιότητας

Η διανυσματική βάση δεδομένων υπολογίζει την ομοιότητα μεταξύ του φορέα έρευνας και όλων των αποθηκευμένων διανυσμάτων.

Ομοιότητα συνημίτονου (Το πιο δημοφιλές για ομαλοποιημένα διανυσματικά):

similarity = (A · B) / (||A|| × ||B||)

Εύρος: -1 έως 1 (υψηλότερα = πιο παρόμοια)

Απόσταση Ευκλείδειας (για μη φυσιολογικούς φορείς):

distance = sqrt(Σ(Ai - Bi)²)

Εύρος: 0 έως ↔ (κατώτερο = πιο παρόμοιο)

Προϊόν κουκκίδας (όταν οι φορείς είναι προ-κανονισμένοι):

similarity = A · B

Εύρος: -1 έως 1 (υψηλότερα = πιο παρόμοια)

Παράδειγμα από την υπηρεσία Qdrant μου:

var searchResults = await _client.SearchAsync(
    collectionName: "blog_posts",
    vector: queryEmbedding,
    limit: (ulong)limit,
    scoreThreshold: 0.7f,  // Only return results with >70% similarity
    payloadSelector: true   // Include all metadata
);

return searchResults.Select(hit => new SearchResult
{
    Text = hit.Payload["text"].StringValue,
    Title = hit.Payload["title"].StringValue,
    Score = hit.Score,
    Source = hit.Payload["source"].StringValue
}).ToList();

Βήμα 3: Επανάκτηση (προαιρετική αλλά συνιστώμενη)

Η αρχική ανάκτηση είναι γρήγορη αλλά κατά προσέγγιση. Η επαναφορά χρησιμοποιεί ένα πιο εξελιγμένο μοντέλο για να ξανασκοράρει τα κορυφαία αποτελέσματα K.

flowchart LR
    A[Vector Search:<br/>Top 50 Results] --> B[Reranking Model]
    B --> C[Reranked:<br/>Top 10 Results]

    style B stroke:#f9f,stroke-width:3px

Γιατί η επανασύνδεση βοηθάει:

  • Γρήγορη ενσωμάτωση μοντέλων βελτιστοποιούν για την ταχύτητα, θυσιάζοντας κάποια ακρίβεια
  • Τα συγκλονιστικά μοντέλα είναι πιο αργά αλλά πιο ακριβή
  • Η προσέγγιση δύο σταδίων εξισορροπεί την ταχύτητα και την ποιότητα

Παράδειγμα επανεγγραφής της εφαρμογής:

public async Task<List<SearchResult>> SearchWithRerankAsync(
    string query,
    int initialLimit = 50,
    int finalLimit = 10)
{
    // Stage 1: Fast vector search
    var candidates = await SearchAsync(query, initialLimit);

    // Stage 2: Precise reranking
    var rerankedResults = await _rerankingService.RerankAsync(
        query,
        candidates
    );

    return rerankedResults.Take(finalLimit).ToList();
}

Φάση 3: Γενιά (Δημιουργία της Απάντησης)

Τώρα που έχουμε σχετικές πληροφορίες, τις δίνουμε στην LLM μαζί με την ερώτηση του χρήστη.

flowchart TB
    A[User Query] --> B[Retrieved Context 1]
    A --> C[Retrieved Context 2]
    A --> D[Retrieved Context 3]

    B --> E[Construct Prompt]
    C --> E
    D --> E
    A --> E

    E --> F["System: You are a helpful assistant...\n\nContext:\n1. Docker Compose allows...\n2. Services are defined...\n3. Volumes persist data...\n\nQuestion: How do I use Docker Compose?\n\nAnswer:"]

    F --> G[LLM]
    G --> H[Generated Answer with Citations]

    style E stroke:#f9f,stroke-width:2px
    style G stroke:#bbf,stroke-width:2px

Βήμα 1: Άμεση Κατασκευή

Αυτό είναι όπου RAG γίνεται μια τέχνη. Θα πρέπει να δομήσει την ώθηση έτσι το LLM:

  • Χρησιμοποιεί το παρεχόμενο πλαίσιο (όχι τις εσωτερικές του γνώσεις)
  • Cite πηγές, όταν είναι δυνατόν
  • Παραδέχεται όταν τα συμφραζόμενα δεν περιέχουν απάντηση
  • Διατηρεί σταθερό τόνο/στυλ

Παράδειγμα γρήγορου προτύπου από το σύστημα GPT του δικηγόρου μου:

public string BuildRAGPrompt(string query, List<SearchResult> context)
{
    var sb = new StringBuilder();

    sb.AppendLine("You are a technical writing assistant. Your task is to answer the user's question using ONLY the provided context from past blog posts.");
    sb.AppendLine();
    sb.AppendLine("CONTEXT:");
    sb.AppendLine("========");

    for (int i = 0; i < context.Count; i++)
    {
        sb.AppendLine($"[{i + 1}] {context[i].Title}");
        sb.AppendLine($"Source: {context[i].Source}");
        sb.AppendLine($"Content: {context[i].Text}");
        sb.AppendLine($"Relevance: {context[i].Score:P0}");
        sb.AppendLine();
    }

    sb.AppendLine("========");
    sb.AppendLine();
    sb.AppendLine("INSTRUCTIONS:");
    sb.AppendLine("- Answer the question using the provided context");
    sb.AppendLine("- Cite sources using [1], [2], etc.");
    sb.AppendLine("- If the context doesn't contain enough information, say so");
    sb.AppendLine("- Maintain the technical, practical tone of the blog");
    sb.AppendLine();
    sb.AppendLine($"QUESTION: {query}");
    sb.AppendLine();
    sb.AppendLine("ANSWER:");

    return sb.ToString();
}

Βήμα 2: Το συμπέρασμα της LLM

Η κατασκευασμένη προτροπή πηγαίνει στο LLM για γενιά. Αυτό μπορεί να είναι:

  • Cloud API: OpenAI, Anthropic Claude, Google PALM
  • Τοπικό μοντέλο: Χρήση llama.cpp, ONNX Runtime, ή TorchSharp

Παράδειγμα χρησιμοποιώντας τοπική LLM:

public async Task<string> GenerateResponseAsync(string prompt)
{
    var result = await _llamaSharp.InferAsync(prompt, new InferenceParams
    {
        Temperature = 0.7f,      // Creativity (0 = deterministic, 1 = creative)
        TopP = 0.9f,             // Nucleus sampling
        MaxTokens = 500,         // Response length limit
        StopSequences = new[] { "\n\n", "User:", "Question:" }
    });

    return result.Text.Trim();
}

Οι βασικές παράμετροι εξηγούνται:

  • Θερμοκρασία: Ελέγχει την τυχαιότητα (0 = πάντα επιλέξτε το πιο πιθανό, 1 = δείγμα τυχαία)
  • Top P: Nucleus δειγματοληψία - εξετάσει μόνο τα σημεία που αποτελούν κορυφαία P μάζα πιθανότητας
  • Μαξ ΤόκενςCity name (optional, probably does not need a translation): Μήκος απόκρισης ορίου
  • Σταματήστε τις ακολουθίες: Πότε να σταματήσει η παραγωγή

Βήμα 3: Μετά την Επεξεργασία

Μετά το LLM παράγει μια απάντηση, συχνά πρέπει να:

  • Εκχύλισέ τα και μετατρέποντάς τα σε συνδέσμους
  • Τμήματα κωδικών μορφοτύπου
  • Προσθέστε μεταδεδομένα (πηγές, βαθμολογίες εμπιστοσύνης)
  • Καταγραφή της αλληλεπίδρασης για αποσφαλμάτωση

Παράδειγμα μετά την επεξεργασία:

public RAGResponse PostProcess(string llmOutput, List<SearchResult> sources)
{
    var response = new RAGResponse
    {
        Answer = llmOutput,
        Sources = new List<Source>()
    };

    // Extract citations like [1], [2]
    var citations = Regex.Matches(llmOutput, @"\[(\d+)\]");

    foreach (Match match in citations)
    {
        int index = int.Parse(match.Groups[1].Value) - 1;
        if (index >= 0 && index < sources.Count)
        {
            var source = sources[index];
            response.Sources.Add(new Source
            {
                Title = source.Title,
                Url = GenerateUrl(source.Source),
                RelevanceScore = source.Score
            });
        }
    }

    // Convert markdown citations to hyperlinks
    response.FormattedAnswer = Regex.Replace(
        llmOutput,
        @"\[(\d+)\]",
        m => {
            int index = int.Parse(m.Groups[1].Value) - 1;
            if (index >= 0 && index < sources.Count)
            {
                var url = GenerateUrl(sources[index].Source);
                return $"[[{m.Groups[1].Value}]]({url})";
            }
            return m.Value;
        }
    );

    return response;
}

Κατανόηση Εσωτερικών LLM: Tokens, KV Cache, και το πλαίσιο Windows

Πριν προχωρήσουμε σε πρακτικές εφαρμογές, είναι απαραίτητο να κατανοήσουμε πώς λειτουργούν οι LLMs εσωτερικά. Αυτή η γνώση σας βοηθά να βελτιστοποιήσετε τα συστήματα RAG και να αποφύγετε τις κοινές παγίδες.

Τι είναι τα Τόκενς;

Tokens είναι οι βασικές μονάδες που LLMs διαδικασία. Το κείμενο δεν τροφοδοτείται απευθείας σε μοντέλα - είναι για πρώτη φορά χωρισμένο σε μάρκες.

Παράδειγμα σηματοδότησης:

Input:  "Understanding Docker containers"
Tokens: ["Under", "standing", " Docker", " containers"]

Διαφορετικά μοντέλα χρησιμοποιούν διαφορετικές στρατηγικές σήμανσης:

  • Μοντέλα GPT: Χρήση κωδικοποίησης Byte-Pair (BPE) με λεξιλόγιο ~50K
  • Claude: Παρόμοια προσέγγιση BPE
  • Μοντέλα Llama: SentencePiece markenization

Γιατί η σήμανση έχει σημασία για τις ΚΓΠΕ:

public class TokenCounter
{
    // Rough approximation: 1 token ≈ 0.75 words (English)
    public int EstimateTokens(string text)
    {
        var wordCount = text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
        return (int)(wordCount / 0.75);
    }

    public int EstimateTokensAccurate(string text, ITokenizer tokenizer)
    {
        // Use actual tokenizer for precision
        return tokenizer.Encode(text).Count;
    }
}

Όρια παραθύρου πλαισίου:

  • GPT-3.5: 16K μάρκες
  • GPT-4: 8K-128K μάρκες (ανάλογα με την παραλλαγή)
  • Claude 3.5 Sonnet: 200K μάρκες
  • Λάμα 3: 8K μάρκες (αν και μπορεί να επεκταθεί)

Στα συστήματα RAG, πρέπει να ταιριάζετε:

Total tokens = System prompt + Retrieved context + User query + Response buffer

Εάν RAG σας ανακτήσει 10 έγγραφα των 500 μάρκων το καθένα, αυτό είναι 5.000 μάρκες μόνο για το πλαίσιο - πριν από την ερώτηση και την απάντηση!

Πρακτικές ΚΓΠΕ συμβολική διαχείριση:

public class ContextWindowManager
{
    private readonly int _maxContextTokens;
    private readonly int _systemPromptTokens;
    private readonly int _responseBufferTokens;

    public ContextWindowManager(
        int totalContextWindow = 4096,
        int systemPromptTokens = 300,
        int responseBufferTokens = 500)
    {
        _maxContextTokens = totalContextWindow;
        _systemPromptTokens = systemPromptTokens;
        _responseBufferTokens = responseBufferTokens;
    }

    public List<SearchResult> FitContextInWindow(
        List<SearchResult> retrievedDocs,
        string query)
    {
        var queryTokens = EstimateTokens(query);

        // Available tokens for retrieved context
        var availableForContext = _maxContextTokens
            - _systemPromptTokens
            - queryTokens
            - _responseBufferTokens;

        var selectedDocs = new List<SearchResult>();
        var currentTokens = 0;

        foreach (var doc in retrievedDocs.OrderByDescending(d => d.Score))
        {
            var docTokens = EstimateTokens(doc.Text);

            if (currentTokens + docTokens <= availableForContext)
            {
                selectedDocs.Add(doc);
                currentTokens += docTokens;
            }
            else
            {
                break; // Context window full
            }
        }

        return selectedDocs;
    }

    private int EstimateTokens(string text)
    {
        // Rule of thumb: 1 token ≈ 4 characters
        return text.Length / 4;
    }
}

The KV Cache: Το μυστικό όπλο της LLM

Όταν ένα LLM παράγει κείμενο, δεν επεξεργάζεται τα πάντα από το μηδέν για κάθε μάρκα. Key-Value (KV) cache να θυμάμαι τι έχει ήδη υπολογίσει.

Πώς λειτουργούν οι Μετασχηματιστές (απλουστευμένοι)

Μετασχηματιστές χρησιμοποιούν έναν μηχανισμό "προσοχής" όπου κάθε σύμβολο "προσκολλάται" (βλέπει) όλες τις προηγούμενες μάρκες για να κατανοήσουν τα συμφραζόμενα.

flowchart TB
    subgraph "Generation Step 1: 'Docker'"
        A1[Input: 'Docker'] --> B1[Compute K,V for 'Docker']
        B1 --> C1[Store in KV Cache]
        C1 --> D1[Generate: 'is']
    end

    subgraph "Generation Step 2: 'is'"
        A2[Input: 'is'] --> B2[Compute K,V for 'is']
        B2 --> C2[Store in KV Cache]
        C2 --> E2[Retrieve KV for 'Docker']
        E2 --> F2[Attend: 'is' to 'Docker']
        F2 --> D2[Generate: 'a']
    end

    subgraph "Generation Step 3: 'a'"
        A3[Input: 'a'] --> B3[Compute K,V for 'a']
        B3 --> C3[Store in KV Cache]
        C3 --> E3[Retrieve KV for 'Docker', 'is']
        E3 --> F3[Attend: 'a' to all previous]
        F3 --> D3[Generate: 'container']
    end

    D1 --> A2
    D2 --> A3

    style C1 stroke:#f9f,stroke-width:3px
    style C2 stroke:#f9f,stroke-width:3px
    style C3 stroke:#f9f,stroke-width:3px

Χωρίς KV cache:

  • Βήμα 1: Διαδικασία 1 μάρκα → O(1)
  • Βήμα 2: Διαδικασία 2 σημεία από το μηδέν → O(2)
  • Βήμα 3: Διαδικασία 3 σημεία από το μηδέν → O(3)
  • Σύνολο: O(1 + 2 + 3 + ... + N) = O(N2)

Με KV cache:

  • Βήμα 1: Process 1 point, cache K,V → O(1)
  • Βήμα 2: Διαδικασία 1 νέο σύμβολο, επαναχρησιμοποίηση cached K,V → O(1)
  • Βήμα 3: Διαδικασία 1 νέο σύμβολο, επαναχρησιμοποίηση cached K,V → O(1)
  • Σύνολο: O(N)

Αυτό κάνει τη γενιά δραματικά γρηγορότερα - τη διαφορά μεταξύ 10 μάρκων/δευτερόλεπτο και 100 μάρκων/δευτερόλεπτο.

Η δομή KV Cache Δέντρο

Το KV cache σχηματίζει ένα "δέντρο" λόγω του πώς η προσοχή λειτουργεί στους μετασχηματιστές. Κάθε στρώμα στο μοντέλο έχει το δικό του K,V matrices.

graph TB
    A[Input Tokens:<br/>'What is Docker?'] --> B[Layer 1 Attention]
    B --> C[Layer 1 KV Cache]

    B --> D[Layer 2 Attention]
    D --> E[Layer 2 KV Cache]

    D --> F[Layer 3 Attention]
    F --> G[Layer 3 KV Cache]

    F --> H[... up to Layer N]
    H --> I[Output: 'Docker is']

    C -.Key-Value pairs<br/>for all input tokens.-> C
    E -.Key-Value pairs<br/>for all input tokens.-> E
    G -.Key-Value pairs<br/>for all input tokens.-> G

    style C stroke:#bbf,stroke-width:2px
    style E stroke:#bbf,stroke-width:2px
    style G stroke:#bbf,stroke-width:2px

Κάθε στρώμα αποθηκεύει:

  • Κλειδιά (Κ): Αντιπροσωπείες που χρησιμοποιούνται για τον υπολογισμό σκορ προσοχής
  • Τιμές (V): Αντιπροσωπείες που αναμειγνύονται με βάση την προσοχή

Για μοντέλο με:

  • 32 στρώματα
  • 4096 κρυφές διαστάσεις
  • 32 κεφάλια προσοχής
  • Παράθυρο πλαισίου 8K

Η κρύπτη KV για μία ακολουθία είναι:

2 (K and V) × 32 layers × 4096 dimensions × 8192 tokens × 2 bytes (FP16)
≈ 4.3 GB of VRAM!

Αυτός είναι ο λόγος για τον οποίο μακριά παράθυρα πλαίσιο είναι έντονη μνήμη.

KV Cache σε RAG Systems

Τα συστήματα RAG μπορούν να αξιοποιήσουν τη βελτιστοποίηση της μνήμης KV με έξυπνους τρόπους:

Απρόσκοπτη αποθήκευση (υποστηριγμένο από κάποιους APIs όπως ο Ανθρωπιστής Claude):

public class CachedRAGService
{
    // System prompt and retrieved context can be cached!
    public async Task<string> GenerateWithCachedContextAsync(
        string systemPrompt,          // Cached
        List<SearchResult> context,   // Cached
        string userQuery)             // Not cached, changes each time
    {
        var contextText = FormatContext(context);

        // The KV cache for systemPrompt + contextText is reused across queries
        var prompt = $@"
{systemPrompt}

CONTEXT:
{contextText}

QUERY: {userQuery}

ANSWER:";

        return await _llm.GenerateAsync(prompt, useCaching: true);
    }
}

Γιατί αυτό είναι ισχυρό:

  • Πρώτη ερώτηση: Computes KV cache για το σύστημα άμεσης + context (slow)
  • Επακόλουθε ερωτήσεις με το ίδιο πλαίσιο: Reuses cached KV (10x γρηγορότερα!)
  • Μόνο το τμήμα ερωτημάτων του χρήστη χρειάζεται φρέσκο υπολογισμό

Πρακτικό παράδειγμα:

Query 1: "How do I use Docker?" → 2 seconds (no cache)
Query 2: "What are Docker benefits?" → 0.2 seconds (cache hit!)
Query 3: "Docker vs VMs?" → 0.2 seconds (cache hit!)

Και τα τρία ερωτήματα χρησιμοποιούν το ίδιο ανακτημένο πλαίσιο, οπότε η μνήμη KV για αυτό το πλαίσιο ξαναχρησιμοποιείται.

Όρια αναφοράς και στρατηγική ΚΓΠΕ

Κατανόηση μάρκων και KV cache ενημερώνει τις αποφάσεις αρχιτεκτονικής RAG σας:

1. Μέγεθος κυνήγι

Μικρότερα κομμάτια = πιο ακριβής ανάκτηση, αλλά πιο πάνω:

// Option A: Small chunks (200 tokens each)
// Retrieve 20 chunks = 4,000 tokens
// Pro: Very precise, only relevant info
// Con: More KV cache entries, slower attention

// Option B: Larger chunks (500 tokens each)
// Retrieve 8 chunks = 4,000 tokens
// Pro: Better context coherence, fewer KV entries
// Con: More noise, less precise

public class AdaptiveChunker
{
    public int DetermineChunkSize(int contextWindowSize)
    {
        if (contextWindowSize <= 4096)
            return 200; // Small chunks for limited windows

        if (contextWindowSize <= 16384)
            return 500; // Medium chunks

        return 1000; // Large chunks for big windows
    }
}

2. Πλαίσιο χρήσης παραθύρου

Μην μεγιστοποιήσετε το παράθυρο πλαίσιο - αφήστε χώρο για την γενιά:

public class SafeContextManager
{
    public int GetSafeContextLimit(int totalContextWindow)
    {
        // Use only 75% for input, reserve 25% for output
        return (int)(totalContextWindow * 0.75);
    }

    // Example: 4K model
    // Total: 4096 tokens
    // Safe input: 3072 tokens
    // Reserved for output: 1024 tokens
}

3. Multi-Turn RAG Συνομιλίες

Σε chatbots, η ιστορία συζήτησης μεγαλώνει με κάθε στροφή:

Turn 1:
System + Context + Query1 = 3000 tokens
Response1 = 300 tokens
Total: 3300 tokens

Turn 2:
System + Context + Query1 + Response1 + Query2 = 3650 tokens
Response2 = 300 tokens
Total: 3950 tokens

Turn 3:
System + Context + Query1 + Response1 + Query2 + Response2 + Query3 = 4250 tokens
ERROR: Context window exceeded!

Λύση: Συρόμενο παράθυρο με re-retrieval

public class ConversationalRAG
{
    private readonly int _maxHistoryTokens = 1000;

    public async Task<string> ChatAsync(
        List<ConversationTurn> history,
        string newQuery)
    {
        // Re-retrieve context based on current query
        var context = await RetrieveContextAsync(newQuery);

        // Keep only recent conversation history
        var relevantHistory = TrimHistory(history, _maxHistoryTokens);

        var prompt = BuildPrompt(context, relevantHistory, newQuery);

        return await _llm.GenerateAsync(prompt);
    }

    private List<ConversationTurn> TrimHistory(
        List<ConversationTurn> history,
        int maxTokens)
    {
        var trimmed = new List<ConversationTurn>();
        var currentTokens = 0;

        // Keep most recent turns
        foreach (var turn in history.Reverse())
        {
            var turnTokens = EstimateTokens(turn.Query) + EstimateTokens(turn.Response);

            if (currentTokens + turnTokens <= maxTokens)
            {
                trimmed.Insert(0, turn);
                currentTokens += turnTokens;
            }
            else
            {
                break;
            }
        }

        return trimmed;
    }
}

4. Βελτιστοποίηση κόστους Token

API-based LLMs χρέωση ανά μάρκα. RAG μπορεί να εκραγεί το κόστος, εάν δεν είναι προσεκτικοί:

public class CostAwareRAG
{
    // OpenAI GPT-4 pricing (example):
    // Input: $0.03 per 1K tokens
    // Output: $0.06 per 1K tokens

    public decimal EstimateQueryCost(
        int systemPromptTokens,
        int retrievedContextTokens,
        int queryTokens,
        int expectedResponseTokens)
    {
        var inputTokens = systemPromptTokens + retrievedContextTokens + queryTokens;
        var outputTokens = expectedResponseTokens;

        var inputCost = (inputTokens / 1000m) * 0.03m;
        var outputCost = (outputTokens / 1000m) * 0.06m;

        return inputCost + outputCost;
    }

    // Example:
    // System: 300 tokens
    // Context: 3000 tokens (10 retrieved docs)
    // Query: 50 tokens
    // Response: 500 tokens
    //
    // Cost = ((300 + 3000 + 50) / 1000 * 0.03) + (500 / 1000 * 0.06)
    //      = (3350 / 1000 * 0.03) + (500 / 1000 * 0.06)
    //      = $0.1005 + $0.03
    //      = $0.1305 per query
    //
    // At 1000 queries/day = $130/day = $3,900/month!
}

Στρατηγικές μείωσης του κόστους:

  1. Ανακτήστε λιγότερα και καλύτερα έγγραφα
  2. Χρησιμοποιήστε άμεση caching (Ανθρωπικός Claude: 90% φθηνότερα για τα cached μάρκες)
  3. Χρησιμοποιήστε φθηνότερα μοντέλα για την επαναβαθμολόγηση, ακριβό για την τελική γενιά
  4. Πλαίσιο συμπίεσης με τη χρήση στροφαλοθαλάμου

Οραματισμός της Πλήρης ροή RAG με Tokens

Εδώ είναι πώς οι μάρκες, KV cache, και RAG ταιριάζουν μεταξύ τους:

flowchart TB
    A[User Query:<br/>'How does Docker work?'<br/>≈ 12 tokens] --> B[Generate Query Embedding]

    B --> C[Vector Search]
    C --> D[Retrieved Docs:<br/>5 docs × 500 tokens<br/>= 2,500 tokens]

    D --> E[Construct Prompt]
    A --> E

    E --> F["Complete Prompt:<br/>System: 300 tokens<br/>Context: 2,500 tokens<br/>Query: 12 tokens<br/>Total: 2,812 tokens"]

    F --> G[Tokenize Prompt]
    G --> H["Token IDs:<br/>[245, 1034, 8829, ...]<br/>2,812 token IDs"]

    H --> I[LLM Layer 1]
    I --> J[Compute K,V]
    J --> K[KV Cache Layer 1:<br/>2,812 K,V pairs]

    I --> L[LLM Layer 2]
    L --> M[Compute K,V]
    M --> N[KV Cache Layer 2:<br/>2,812 K,V pairs]

    L --> O[... Layers 3-32]
    O --> P[Generate Token 1: 'Docker']

    P --> Q[Add to KV Cache]
    Q --> R[Generate Token 2: 'is']
    R --> S[Add to KV Cache]
    S --> T[... until completion]

    T --> U["Response: 'Docker is a containerization platform...'<br/>≈ 400 tokens"]

    style K stroke:#f9f,stroke-width:2px
    style N stroke:#f9f,stroke-width:2px
    style Q stroke:#bbf,stroke-width:2px
    style S stroke:#bbf,stroke-width:2px

Βασικές γνώσεις:

  1. Σημάδια εισόδου (2,812) υποβάλλονται σε επεξεργασία μία φορά για την κατασκευή αρχικής μνήμης KV
  2. Γενιά συμβαίνει ένα σημείο τη φορά, ξαναχρησιμοποιώντας την κρύπτη KV
  3. Κάθε νέο σύμβολο προσθέτει στο KV cache για μελλοντικές μάρκες για να παρακολουθήσετε
  4. Σύνολο VRAM απαιτείται = Μοντέλο βάρους + KV cache για όλες τις μάρκες
  5. Μεγαλύτερο πλαίσιο = μεγαλύτερη μνήμη KV = μεγαλύτερη VRAM

Πρακτικές επιπτώσεις για τις ΚΓΠΕ

Κατανόηση σημάτων και KV cache οδηγεί σε καλύτερο σχεδιασμό RAG:

1. Προσυγκρούστε και cache κοινά συμφραζόμενα:

// Cache KV for frequently used system prompts + static context
var cachedSystemContext = await _llm.PrecomputeKVCache(systemPrompt + staticContext);

// Reuse for each query (much faster)
foreach (var query in userQueries)
{
    var response = await _llm.GenerateAsync(query, reuseKVCache: cachedSystemContext);
}

2. Βελτιστοποιήστε τα όρια κομματιών:

// Bad: Arbitrary 500-character chunks
var chunks = text.Chunk(500);

// Good: Chunk on sentence boundaries, measure in tokens
public List<string> ChunkByTokens(string text, int maxTokensPerChunk)
{
    var sentences = SplitIntoSentences(text);
    var chunks = new List<string>();
    var currentChunk = new StringBuilder();
    var currentTokens = 0;

    foreach (var sentence in sentences)
    {
        var sentenceTokens = EstimateTokens(sentence);

        if (currentTokens + sentenceTokens > maxTokensPerChunk && currentTokens > 0)
        {
            chunks.Add(currentChunk.ToString());
            currentChunk.Clear();
            currentTokens = 0;
        }

        currentChunk.Append(sentence).Append(" ");
        currentTokens += sentenceTokens;
    }

    if (currentTokens > 0)
        chunks.Add(currentChunk.ToString());

    return chunks;
}

3. Παρακολουθήστε τη χρήση σημάδι στην παραγωγή:

public class RAGTelemetry
{
    public void LogRAGQuery(
        string query,
        List<SearchResult> retrievedDocs,
        string response)
    {
        var queryTokens = EstimateTokens(query);
        var contextTokens = retrievedDocs.Sum(d => EstimateTokens(d.Text));
        var responseTokens = EstimateTokens(response);
        var totalTokens = queryTokens + contextTokens + responseTokens;

        _logger.LogInformation(
            "RAG Query: {Query} | Context: {ContextTokens} tokens from {DocCount} docs | " +
            "Response: {ResponseTokens} tokens | Total: {TotalTokens} tokens",
            query, contextTokens, retrievedDocs.Count, responseTokens, totalTokens
        );

        // Alert if approaching context limit
        if (totalTokens > _maxTokens * 0.9)
        {
            _logger.LogWarning("Approaching token limit: {TotalTokens}/{MaxTokens}",
                totalTokens, _maxTokens);
        }
    }
}

Συμπέρασμα: Αρχιτεκτονική Mastery

Έχουμε καλύψει την πλήρη τεχνική αρχιτεκτονική των συστημάτων RAG:

Φάση 1: Ευρεσιτεχνία

  • Εξαγωγή κειμένου από διάφορες πηγές
  • Στρατηγικές για το κυνήγι (με βάση το τμήμα, με βάση τις φράσεις, με επικάλυψη)
  • Παραγωγή ενσωμάτωσης (ONNX, API υπηρεσίες)
  • Αποθήκευση διάνυσμα (Qdrant, pgvector, Pinecone)

Φάση 2: Ανάκτηση

  • Ερωτηματογράφηση (ίδιο μοντέλο με ευρετηρίαση!)
  • Αναζήτηση ομοιότητας (cosine, euclidean, dot product)
  • Προαιρετική επανασύνδεση για καλύτερη ακρίβεια
  • Φίλτρο μεταδεδομένα για εκλεπτυσμένα αποτελέσματα

Φάση 3: Γενιά

  • Απρόσκοπτη κατασκευή (συνθήκη + οδηγίες + ερώτημα)
  • Συνέδρια LLM (θερμοκρασία, κορυφή-p, max μάρκες)
  • Μετά την επεξεργασία (εκχύλιση, μορφοποίηση)

LLM Internals

  • Tokens: Οι θεμελιώδεις μονάδες (όχι χαρακτήρες!)
  • KV Cache: Γιατί η γενιά είναι γρήγορη (γραμμική, όχι τετραδική)
  • Παράθυρα πλαισίου: Διαχείριση συμβολικών ορίων στις ΚΓΠΕ
  • Βελτιστοποίηση κόστους: Caching, συμπίεση, έξυπνη ανάκτηση

Βασικές τεχνικές γνώσεις:

  1. Το ίδιο μοντέλο ενσωμάτωσης για ευρετηρίαση και ανάκτηση (κρίσιμη!)
  2. Το κυνήγι μετράει περισσότερο απ' όσο νομίζεις. - διατηρεί σημασιολογική συνοχή
  3. Η αύξηση βελτιώνει την ακρίβεια σε σχέση με το κόστος της καθυστέρησης
  4. Η διαχείριση του Token είναι απαραίτητη - εκτίμηση πριν από την ερώτηση
  5. Η KV caching καθιστά τις ΚΓΠΕ εφικτές - τους γρήγορους υπολογισμούς επαναχρησιμοποίησης
  6. Τα παράθυρα πλαισίου γεμίζουν γρήγορα - 10 docs × 500 μάρκες = 5K μάρκες

Συνέχεια στο Μέρος 3: Οι ΚΓΠΕ στην Πρακτική

Τώρα καταλαβαίνεις. πώς λειτουργεί το RAG Πώς μπορείτε πραγματικά να οικοδομήσετε αυτά τα συστήματα; Ποιες προκλήσεις θα αντιμετωπίσετε; Ποιες προηγμένες τεχνικές μπορείτε να χρησιμοποιήσετε;

Το Μέρος 3: Οι ΚΓΠΕ στην πράξη, μετακινούμαστε από την αρχιτεκτονική στην εφαρμογή:

Αληθινές εφαρμογές:

  • Σχετικές δημοσιεύσεις σύσταση σε αυτό το blog
  • Σημαντική αναζήτηση blog
  • Κατασκευή βοηθού συγγραφής "Δικηγόρος GPT"

Κοινές προκλήσεις και λύσεις:

  • Στρατηγικές για το κυνήγι που διατηρούν το πλαίσιο
  • Βελτίωση της ποιότητας ενσωμάτωσης για τον τομέα σας
  • Διαχείριση παραθύρων πλαισίου δυναμικά
  • Εμποδίζοντας τις παραισθήσεις παρά το γεγονός ότι έχουν συμφραζόμενα
  • Κρατήστε ενημερωμένο το ευρετήριο σας

Προηγμένες τεχνικές:

  • Υποθετικά Έντυπα Εγκαταστάσεων Εγγράφων (HyDE)
  • Αυτοκατασκευή με φίλτρα LLM-parsed
  • ΠΟΛΛΑΠΛΟΙ ΛΟΓΑΡΙΑΣΜΟΙ ΓΙΑ ΠΕΡΙΣΣΟΤΕΡΑ ΑΠΟΤΕΛΕΣΜΑΤΑ
  • Πλαίσιο συμπίεσης για τη μείωση της χρήσης σημάτων
  • Πολυεπιχειρησιακές ΚΓΠΕ για περίπλοκα ερωτήματα
  • Μακροχρόνια μνήμη συζητήσεων

Ξεκινάμε:

  • Σχέδιο εφαρμογής της εβδομάδας ανά εβδομάδα
  • Πρακτικά παραδείγματα κώδικα
  • Στρατηγικές βελτιστοποίησης
  • Όταν ΔΕΝ χρησιμοποιούνται ΚΓΠΕ

Συνέχεια στο Μέρος 3: Οι ΚΓΠΕ στην Πρακτική →

Πόροι

Ιδρυτικές εφημερίδες:

Εργαλεία και πλαίσια:

Περαιτέρω ανάγνωση:

Πλοήγηση σειράς:

Συνέχεια στο Μέρος 3 →

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.