RAG Arkkitehtuuri ja sisätilat: Miten se todella toimii (Suomi (Finnish))

RAG Arkkitehtuuri ja sisätilat: Miten se todella toimii

Saturday, 22 November 2025

//

23 minute read

Sisään Osa 1Käsittelimme RAG:n alkuperän, perusasiat ja sen, miksi sillä on merkitystä. Ymmärrät korkean tason konseptin: hae relevantti tieto ja käytä sitä vastausten tuottamiseen. Nyt sukellamme syvälle tekniseen arkkitehtuuriin - tarkalleen, miten RAG-järjestelmät toimivat konepellin alla, pilkkomisstrategioista LLM:n sisäisiin järjestelmiin, kuten rahakkeisiin ja KV-välilyönteihin.

Johdanto

Sarjan navigaatio: Tämä on aluetukisuuntaviivojen 2 osa:

Jos et ole lukenut osaa 1, suosittelen, että aloitat sen ymmärtämisen:

  • Mitä RAG on ja miksi sillä on merkitystä
  • Historia avainsanahausta semanttiseen ymmärrykseen
  • RAG vs hienosäätö ja muut lähestymistavat

Tässä artikkelissa oletetaan, että ymmärrät nämä perusasiat ja keskityt tekninen arkkitehtuuri, toteutustiedot ja LLM-sisätilat.

Miten RAG toimii: Täydellinen kuva

Puretaan tarkkaan, mitä RAG-järjestelmässä tapahtuu, siitä hetkestä, kun lisäät asiakirjan, siihen, kun käyttäjä saa vastauksen.

Vaihe 1: Indeksointi (tietopohjan valmistelu)

Ennen kuin RAG voi hakea mitään, sinun on indeksoitava tietopohjasi. Tämä on kertaluonteinen prosessi (joskin voit lisätä uusia asiakirjoja myöhemmin).

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

Vaihe 1: Tekstin poisto

Poista selkeä teksti lähdeasiakirjoistasi. Tämä voi olla:

  • Markdown-tiedostot (kuten blogikirjoitukseni)
  • PDF-muodossa (dokumentteja varten)
  • HTML (verkossa raaputtamiseen)
  • Tietokantatiedot
  • Sähköpostit, keskustelulokit jne.

Esimerkki blogistani:

// 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();
}

Vaihe 2: Nuuskiminen

Tässä suurin osa RAG:n toteutuksista epäonnistuu. Kappalerajaa ei voi vain jakaa, vaan tarvitaan semanttisesti johdonmukaisia kappaleita.

Miksi paloittelulla on merkitystä:

  • LLM:illä on nimelliset raja-arvot (konteksti-ikkunat)
  • Pienemmät kappaleet = tarkempi haku
  • Mutta palasten täytyy sisältää tarpeeksi kontekstia ollakseen merkityksellisiä

Huono löyly:

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."

Hyvä pilkkominen:

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"

Esimerkki semanttisesta hakutoteutuksestani:

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;
    }
}

Yhteiset leikkausstrategiat:

  • Kiinteä koko: Yksinkertainen mutta rikkoo semanttisia rajoja
  • Tuomio perustuu tuomioon: Kunnioittaa kielioppia, mutta voi olla liian pieni
  • Kappaleisiin perustuva: Luonnollinen, mutta vaihteleva koko
  • Erityisjaostoon perustuva: Paras jäsenneltyyn sisältöön (mieltymykseni)
  • Liukuikkuna päällekkäin: Varmistaa, ettei kontekstia hukata rajojen yli

Vaihe 3: Luo upotuksia

Upotus on taikaa, joka mahdollistaa semanttisen etsinnän. Upotus on vektori (lukujen säde), joka edustaa tekstin merkitystä.

Avainkäsite: Samanlaisia merkityksiä → samankaltaisia vektoreita

"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]

Ensimmäiset kaksi vektoria olisivat "lähellä" vektoriavaruudessa (korkean kosiinin samankaltaisuus), kun taas kolmas on kaukana.

Miten upotukset syntyvät: Modernit upotusmallit ovat hermoverkkoja, jotka on koulutettu massiivisiin tekstiaineistoihin, jotta ne oppisivat semanttisia suhteita. Suosittuja malleja:

  • All-MiniLM-L6-v2: 384 kokoa, nopea, hyvä laatu (mitä käytän tässä blogissa)
  • tekstiin upotettava 3-pieni (OpenAI): 1536 kokoa, erittäin korkea laatu
  • BGE-pohja: 768 ulottuvuutta, viimeisintä avointa lähdekoodia

Esimerkki ONNX-yhdistämispalvelustani:

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);
}

Miksi normalisoinnilla on merkitystä: L2:n normalisoinnin jälkeen kosinan samankaltaisuudesta tulee yksinkertainen pistetuote, jolloin haku on paljon nopeampaa.

Vaihe 4: Säilytä Vectorin tietokannassa

Vektoritietokannat optimoidaan suuriulotteisten vektorien tallentamiseen ja etsimiseen. Toisin kuin perinteiset SQL-kyselyitä käyttävät tietokannat, vektoritietokannat käyttävät samankaltaisuushakua.

Avaintoiminnot:

  • Upsert: Lisää tai päivitä vektori metatietoineen
  • Etsi: Etsi K eniten samanlaisia vektoreita kuin kyselyvektori
  • Suodatin: Yhdistä vektorihaku metatietosuodattimiin

Esimerkki Qdrantin täytäntöönpanosta:

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 }
    );
}

Suositut vektoritietokannat:

  • Qdrant: Nopea, itseohjautuva, erinomainen C#-tuki (valintani)
  • pgvector: PostgreSQL-laajennus (hienoa, jos käytät jo Postgres-laajennusta)
  • Pinecone: Hallittu palvelu (kallista mutta hyvää)
  • Hellävarainen: Feature-rikas, hyvä monimutkaisille skeemoille
  • ChromaDB: Python-keskeinen, kevyt

Tutustumme näiden tietokantojen perustamiseen tulevissa artikkeleissa.

Vaihe 2: nouto (asian kannalta merkityksellisen tiedon löytäminen)

Kun käyttäjä esittää kysymyksen, RAG-järjestelmän on löydettävä tietopohjan tärkeimmät tiedot.

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

Vaihe 1: Luo kyselyn sulauttaminen

Käyttäjän kysymys muunnetaan vektoriksi käyttäen sama upotusmalli Käytetään indeksointiin. Tämä on tärkeää - eri mallit tuottavat yhteensopimattomia vektoreita.

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;
}

Vaihe 2: Samankaltaisuuden etsintä

Vektoritietokanta laskee kyselyvektorin ja kaikkien tallennettujen vektorien samankaltaisuuden. Yhteiset mittarit:

Cosinen samankaltaisuus (suosituin normalisoiduille vektoreille):

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

Vaihteluväli: -1–1 (suurempi = enemmän samanlainen)

Euklidinen etäisyys (normalisoitumattomien vektorien osalta):

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

Vaihteluväli: 0–0 (pienempi = enemmän samankaltaista)

Pistetuote (jolloin vektorit ovat ennakkonormalisoituja):

similarity = A · B

Vaihteluväli: -1–1 (suurempi = enemmän samanlainen)

Esimerkki Qdrant-palvelustani:

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();

Vaihe 3: Uudelleenasettaminen (valinnainen, mutta suositeltava)

Alkuperäinen haku on nopea mutta likimääräinen. Uudelleen rankingissa käytetään hienostuneempaa mallia, jonka avulla K-huipputulokset voidaan laskea uudelleen.

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

Miksi uudelleenasettaminen auttaa:

  • Nopeat upotusmallit optimoivat nopeuden ja uhraavat jonkin verran tarkkuutta
  • Uusintamallit ovat hitaampia mutta tarkempia
  • Kaksivaiheinen lähestymistapa tasapainottaa nopeutta ja laatua

Esimerkki täytäntöönpanon uudelleenjärjestämisestä:

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();
}

Vaihe 3: Sukupolvi (vastauksen luominen)

Nyt kun meillä on merkityksellistä tietoa, syötämme sen LLM:lle käyttäjän kysymyksen mukana.

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

Vaihe 1: Nopea rakentaminen

Tässä kohtaa RAG:stä tulee taidetta. Sinun on rakennettava pikavippi niin, että LLM:

  • Käyttää tarjottua kontekstia (ei sen sisäistä osaamista)
  • Leikkauslähteet, jos mahdollista
  • Myöntää, kun asiayhteys ei sisällä vastausta
  • Säilyttää johdonmukaisen sävyn/tyylin

Esimerkki nopeasta mallista asianajajani GPT-järjestelmästä:

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();
}

Vaihe 2: LLM-päätelmä

Rakennettu pikavippi menee LLM:lle sukupolveksi. Tämä voi olla:

  • Pilvirajapinta: OpenAI, Antropical Claude, Google PaLM
  • Paikallinen malli: Käyttämällä lama.cpp, ONNX Runtime, tai TorchSharp

Esimerkki paikallisen LLM:n avulla:

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();
}

Tärkeimmät parametrit selitettiin:

  • Lämpötila: Kontrollit satunnaisuus (0 = aina poimitaan todennäköisin, 1 = näyte sattumanvaraisesti)
  • Ylhäällä P: Nucleus-näytteenotto - harkitse vain rahakkeita, jotka muodostavat P-todennäköisyysmassan
  • Max Tokens: Rajaa vasteen pituus
  • Pysäytä jaksot: Milloin lopettaa tuottaminen

Vaihe 3: Prosessin jälkeinen

LLM:n synnyttämän reaktion jälkeen meidän on usein

  • Ottaa lainauksia ja muuntaa ne linkeiksi
  • Muodon koodilohkot
  • Lisää metatiedot (lähteet, luottamuspisteet)
  • Kirjaa vuorovaikutus vianetsintään

Esimerkki jälkikäsittelystä:

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 Internalsin ymmärtäminen: Tokens, KV Cache ja Context Windows

Ennen kuin siirrymme käytännön sovelluksiin, on tärkeää ymmärtää, miten LLM:t toimivat sisäisesti. Tämä tieto auttaa optimoimaan RAG-järjestelmiä ja välttämään yhteisiä sudenkuoppia.

Mitä tokenit ovat?

Tokens on LLM:n prosessien perusyksikkö. Tekstiä ei syötetä suoraan malleille - se on ensin jaoteltu rahakkeiksi.

Esimerkkimerkitys:

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

Eri malleissa käytetään erilaisia sulautusstrategioita:

  • GPT-mallit: Käytä Byte-Pair Encodingia (BPE) ~50K sanastolla
  • Claude: Samanlainen BPE-lähestymistapa
  • Llama-mallit: Tuomionpiece tokenization

Miksi aluetukialueilla on merkitystä kuponkivoitolla:

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;
    }
}

Kontekstiikkunan rajat:

  • GPT-3.5: 16K-poletit
  • GPT-4: 8K-128K tokenit (variantista riippuen)
  • Claude 3.5 Sonnet: 200K kuponki
  • Llama 3: 8K kuponkeja (vaikka niitä voi pidentää)

RAG-järjestelmiin pitää mahtua:

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

Jos RAG hakee 10 asiakirjaa 500 kuponkia kustakin, se on 5 000 kuponkia vain asiayhteyteen - ennen kyselyä ja vastausta!

Käytännöllinen aluetukialue:

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;
    }
}

KV Cache: LLM:n salainen ase

Kun LLM tuottaa tekstiä, se ei käsittele kaikkea uudelleen tyhjästä jokaiselle rahakkeelle. Avainarvo (KV) välimuisti muistaa, mitä se on jo laskenut.

Miten muuntajat toimivat (yksinkertaistettu)

Transformerit käyttävät "huomiomekanismia", jossa jokainen merkki "tarkkailee" (katsoo) kaikkia aiempia kuponkeja ymmärtääkseen kontekstia.

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

Ilman KV- välimuistia:

  • Vaihe 1: Process 1 plake → O(1)
  • Vaihe 2: Prosessi 2 kuponkia tyhjästä → O(2)
  • Vaihe 3: Prosessi 3 kuponkia tyhjästä → O(3)
  • Yhteensä: O (1 + 2 + 3 + ... + N) = O (N2)

KV- välimuistilla:

  • Vaihe 1: Process 1 plake, välimuisti K,V → O(1)
  • Vaihe 2: Prosessi 1 uusi tunniste, uudelleenkäyttö välimuistissa K,V → O(1)
  • Vaihe 3: Prosessi 1 uusi tunniste, uudelleenkäyttö välimuistissa K,V → O(1)
  • Yhteensä: O(N)

Tämä tekee sukupolvesta rajusti nopeampi - ero 10 poletilla/sekunnilla ja 100 poletilla/sekunnilla.

KV Cache -puurakenne

KV:n välimuisti muodostaa "puun", koska se toimii muuntajissa. Mallin jokaisella kerroksella on omat K,V-matriisensa.

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

Jokainen kerros tallentaa:

  • Avaimet (K): Huomiopisteiden laskemiseen käytetyt edustustot
  • Arvot (V): Edustustot, jotka sekoittuvat huomion perusteella

Malli, jossa on

  • 32 kerrosta
  • 4096 piilotetut mitat
  • 32 tarkkaavaista päätä
  • 8K-kontekstiikkuna

Yhden sarjan KV- välimuisti on:

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

Tästä syystä pitkät konteksti-ikkunat ovat muistitiheitä.

KV Cache RAG-järjestelmissä

RAG-järjestelmät voivat käyttää KV- välimuistien optimointia ovelasti:

Välimuistin pikainen välimuisti (joidenkin API-rajapintojen, kuten Antropical Clauden, tukemana):

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);
    }
}

Miksi tämä on voimakas:

  • Ensimmäinen kysely: Laskei KV- välimuistin järjestelmän pikavisiitille + kontekstille (hidas)
  • Seuraavat kyselyt samassa yhteydessä: Käyttää uudelleen välimuistissa olevaa KV:tä (10x nopeammin!)
  • Vain käyttäjäkyselyn osuus tarvitsee uutta laskentaa

Käytännöllinen esimerkki:

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!)

Kaikki kolme kyselyä käyttävät samaa noudettua kontekstia, joten KV- välimuistia käytetään uudelleen.

Token Limits and Regage Strategy

Ymmärryskyltit ja KV:n välimuisti kertovat RAG-arkkitehtuurin päätöksistä:

1. Chunking Size

Pienemmät kappaleet = tarkempi noutaminen, mutta enemmän yläpuolella:

// 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. Kontekstiikkunan käyttö

Älä maksimoi kontekstiikkunaa - jätä tilaa sukupolvelle:

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. Monikäänteinen RAG-keskustelut

Chatboteissa keskusteluhistoria kasvaa vuorotellen:

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!

Ratkaisu: Sliding-ikkuna uudelleen noudettavalla

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. Tokenin kustannusoptimointi

API-pohjainen LLM-maksu per rahake. RAG voi räjähtää kustannuksia, jos ei ole varovainen:

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!
}

Kustannusten vähentämisstrategiat:

  1. Hae vähemmän ja parempiarvoisempia asiakirjoja
  2. Käytä pikaista välimuistia (Antropical Claude: 90 % halvempi välimuistissa)
  3. Käyttää halvempia malleja uudelleen paremmuusjärjestykseen, kalliita lopulliseen sukupolveen
  4. Purista kontekstia yhteenvetoa käyttäen

Tokens visualisoi kokonaisen aluekaavion

Näin poletit, KV-kätköt ja RAG sopivat yhteen:

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

Tärkeimmät näkemykset:

  1. Syöttömerkit (2.812) käsitellään kerran ensimmäisen KV- välimuistin rakentamisessa
  2. Sukupolvi tapahtuu yksi kuponki kerrallaan käyttäen uudelleen KV- välimuistia
  3. Jokainen uusi merkki lisää KV- välimuistiin tulevia kuponkeja varten
  4. VRAM-muisti yhteensä tarvitaan = Mallipainot + KV- välimuisti kaikille rahakkeille
  5. Pidempi konteksti = suurempi KV välimuisti = enemmän VRAM-muistia

Käytännöllisiä vaikutuksia aluetukiin

Tokenien ja KV-välimuistin ymmärtäminen johtaa parempaan RAG-suunnitteluun:

1. Laske ja tallenna yhteiset kontekstit:

// 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. Optimoi kokojen rajat:

// 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. Seuraa kuvankäyttöä tuotannossa:

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);
        }
    }
}

Johtopäätös: Arkkitehtuurin mastery

Olemme käsitelleet RAG-järjestelmien täydellisen teknisen arkkitehtuurin:

Vaihe 1: Indeksointi

  • Tekstin poiminta eri lähteistä
  • Nuuskimisstrategiat (osapohjainen, lausepohjainen, päällekkäinen)
  • Upotustuotanto (ONNX, API-palvelut)
  • Vektorivarasto (Qdrant, pgvector, Pinecone)

Vaihe 2: nouto

  • Kysely upotetaan (sama malli kuin indeksointi!)
  • Samankaltaisuushaku (kosiini, euklidinen, pistetuote)
  • Valinnainen uudelleenasettelu paremman tarkkuuden saavuttamiseksi
  • Metatietojen suodatus tarkennettuja tuloksia varten

Vaihe 3: Sukupolvi

  • Nopea rakentaminen (konteksti + ohjeet + kysely)
  • LLM-päätelmä (lämpötila, top-p, max tokenes)
  • Jälkikäsittely (sidontauutto, muotoilu)

LLM:n sisätilat

  • Tokens: Perusyksiköt (ei merkkejä!)
  • KV Cache: Miksi sukupolvi on nopea (lineaarinen, ei quadratic)
  • Kontekstiikkunat: Tunnusten raja-arvojen hallinta aluetukialueilla
  • Kustannusten optimointi: Välimuisti, pakkaus, älykäs nouto

Tärkeimmät tekniset oivallukset:

  1. Sama upotettava malli indeksointi ja noutaminen (kriittinen!)
  2. Huutelulla on enemmän merkitystä kuin luuletkaan - säilyttää semanttisen johdonmukaisuuden
  3. Uudelleenjärjestäminen parantaa tarkkuutta latenssin hinnalla
  4. Tokenin johtaminen on olennaista - arvio ennen kyselyä
  5. KV:n välimuisti tekee RAG:stä käyttökelpoisen - uusiokäyttö nopeat laskelmat
  6. Kontekstiikkunat täyttyvät nopeasti - 10 docs × 500 tokens = 5K tokens

Jatka osan 3: RAG käytännössä

Ymmärrät nyt RAG:n toiminta Teknisellä tasolla, mutta teorialla päästään vain tähän asti. Miten näitä järjestelmiä oikeasti rakennetaan? Mitä haasteita kohtaat? Mitä kehittyneitä tekniikoita voit käyttää?

Sisään **Osa 3: Aluetuki käytännössä**Siirrymme arkkitehtuurista toteutukseen:

Reaalimaailman sovellukset:

  • Related posts Recommendation on this blog
  • Semanttinen blogihaku
  • "Lakimies GPT" -kirjoitusavustajan rakentaminen

Yhteiset haasteet ja ratkaisut:

  • Kontekstin säilyttävät nuuskimisstrategiat
  • Oman alueen laadun parantaminen
  • Konteksti-ikkunoiden hallinta dynaamisesti
  • Hallusinaatioiden ehkäiseminen asiayhteydestä huolimatta
  • Hakemiston pitäminen ajan tasalla

Kehittyneet tekniikat:

  • Hypoteettinen asiakirja Upotukset (HyDE)
  • Itsevalitus LLM-korjatuilla suodattimilla
  • Moniosainen aluetukialue kattaville tuloksille
  • Kontekstillinen kompressio poletin käytön vähentämiseksi
  • Multi-hop-alue monimutkaisia kyselyitä varten
  • Pitkän aikavälin keskustelumuisti

Aloittaminen:

  • Viikkokohtainen toteutussuunnitelma
  • Käytännöllisiä koodiesimerkkejä
  • Optimointistrategiat
  • Kun EI saa käyttää RAG-ohjelmaa

Jatka kohtaan 3: RAG käytännössä →

Resurssit

Peruskirjat:

Työkalut ja puitteet:

Lue lisää:

Sarjanavigointi:

Jatka kohtaan 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.