RAG Arkitektur och interner: Hur det verkligen fungerar (Svenska (Swedish))

RAG Arkitektur och interner: Hur det verkligen fungerar

Saturday, 22 November 2025

//

24 minute read

Till Häfte 1, Vi täckte RAG ursprung, grunder, och varför det spelar roll. Du förstår högnivåkonceptet: hämta relevant information, sedan använda den för att generera svar. Nu dyker vi djupt in i den tekniska arkitekturen – exakt hur RAG system fungerar under huven, från bitning strategier till LLM internal som polletter och KV caches.

Inledning

på serienavigering: Detta är del 2 av RAG-serien:

Om du inte har läst del 1 rekommenderar jag att du börjar där för att förstå:

  • Vad RAG är och varför det betyder något
  • Historien från sökordssökning till semantisk förståelse
  • RAG mot finjustering och andra metoder

Den här artikeln förutsätter att du förstår dessa grunderna och fokuserar på Teknisk arkitektur, genomförandedetaljer och LLM-interner.

Hur RAG fungerar: Den kompletta bilden

Låt oss bryta ner exakt vad som händer i ett RAG-system, från det ögonblick du lägger till ett dokument till när en användare får ett svar.

Fas 1: Indexering (Förberedande av kunskapsbasen)

Innan RAG kan hämta något behöver du indexera din kunskapsbas. Detta är en engångsprocess (även om du kan lägga till nya dokument senare).

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

Steg 1: Textutdrag

Extrahera vanlig text från källdokumenten. Det här kan vara:

  • Markned filer (som mina blogginlägg)
  • PDF-filer (för dokumentation)
  • HTML (för webbskrapning)
  • Databasregister
  • E-post, chattloggar, etc.

Exempel från min blogg:

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

Steg 2: Knäppning

Det är här de flesta RAG-implementationer misslyckas. Du kan inte bara dela på punktgränser - du behöver semantiskt sammanhängande bitar.

Varför det är viktigt att stycka:

  • LLM:er har symboliska gränser (kontextfönster)
  • Mindre bitar = mer exakt hämtning
  • Men bitar måste innehålla tillräckligt med sammanhang för att vara meningsfull

Dålig bitning:

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

Bra bitning:

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"

Exempel från min semantiska sökimplementering:

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

Vanliga styckningsstrategier:

  • Fast storlek: Enkelt men bryter semantiska gränser
  • Påföljdsbaserad: Respekterar grammatiken men kan vara för liten
  • Punktbaserad: Naturlig men variabel storlek
  • Avsnittsbaserad: Bästa för strukturerat innehåll (min preferens)
  • Skjutfönster med överlappning: Säkerställer att inget sammanhang går förlorat vid gränserna

Steg 3: Generera inbäddningar

Inbäddningar är magin som gör semantisk sökning möjlig. En inbäddning är en vektor (array av siffror) som representerar betydelsen av text.

Nyckelbegrepp: Liknande betydelser → liknande vektorer

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

De två första vektorerna skulle vara "nära" i vektorrymden (hög cosinuslikhet), medan den tredje är långt borta.

Hur inbäddningar genereras: Moderna inbäddade modeller är neurala nätverk tränas på massiva textdataset för att lära semantiska relationer. Populära modeller:

  • Alla-MiniLM-L6-v2: 384 dimensioner, snabb, bra kvalitet (vad jag använder på den här bloggen)
  • Inbäddning av text till 3-små (OpenAI): 1536 dimensioner, mycket hög kvalitet
  • BGE-bas: 768 dimensioner, toppmodern öppen källkod

Exempel från min ONNX inbäddningstjänst:

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

Varför normalisering är viktigt: Efter L2 normalisering, cosinus likhet blir en enkel punkt produkt, vilket gör sökning mycket snabbare.

Steg 4: Förvaras i Vector-databasen

Vektordatabaser är optimerade för att lagra och söka högdimensionella vektorer. Till skillnad från traditionella databaser som använder SQL-frågor, vektordatabaser använder likhetssökning.

Nyckelåtgärder:

  • Uppvärmning: Lägg till eller uppdatera en vektor med metadata
  • Sök: Hitta K mest liknande vektorer till en frågevektor
  • Filter: Kombinera vektorsökning med metadatafilter

Exempel på genomförande av 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 }
    );
}

Populära vektordatabaser:

  • Qdrant Ordförande: Snabb, självupptagen, utmärkt C# stöd (mitt val)
  • Pgvector Ordförande: PostgreSQL-tillägg (bra om du redan använder Postgres)
  • Vinbär (röda, svarta och vita): Hanterad tjänst (dyr men bra)
  • Vävnad: Feature-rich, bra för komplexa scheman
  • ChromaDB: Python-fokuserad, lättviktig

Vi kommer att undersöka hur dessa databaser kan byggas upp i kommande artiklar.

Fas 2: Återhämtning (Hitta relevant information)

När en användare ställer en fråga måste RAG-systemet hitta den mest relevanta informationen från kunskapsbasen.

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

Steg 1: Generera frågebäddar

Användarens fråga konverteras till en vektor med hjälp av Samma modell för inbäddning används för indexering. Detta är kritiskt - olika modeller producerar inkompatibla vektorer.

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

Steg 2: Liknande sökningar

Vektordatabasen beräknar likheten mellan frågevektorn och alla lagrade vektorer.

Cosinuslikhet (mest populär för normaliserade vektorer):

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

Avstånd: -1 till 1 (högre = mer likartat)

Euclidean avstånd (för onormaliserade vektorer):

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

Avstånd: 0 till ∞ (lägre = mer likartat)

Punktprodukt (när vektorer är förnormaliserade):

similarity = A · B

Avstånd: -1 till 1 (högre = mer likartat)

Exempel från min Qdrant-tjänst:

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

Steg 3: Omställning (valfri men rekommenderad)

Den första hämtningen är snabb men ungefärlig. Reranking använder en mer sofistikerad modell för att markera de bästa K-resultaten.

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

Varför omplacering hjälper:

  • Snabb inbäddning modeller optimerar för hastighet, offrar viss noggrannhet
  • Uppställningsmodellerna är långsammare men mer exakta
  • Tvåstegsmetoden balanserar hastighet och kvalitet

Exempel på omklassificering av genomförandet:

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

Fas 3: Generation (skapa svaret)

Nu när vi har relevant information matar vi den till LLM tillsammans med användarens fråga.

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

Steg 1: Snabb konstruktion

Det är här RAG blir en konst. Du måste strukturera prompten så att LLM:

  • Använder det givna sammanhanget (inte dess interna kunskaper)
  • Cites källor när det är möjligt
  • Erkänner när sammanhanget inte innehåller ett svar
  • Bibehåller en konsekvent ton/ stil

Exempel på snabbmall från mitt GPT-system:

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

Steg 2: Slutsats om LLM

Den konstruerade prompten går till LLM för generation. Detta kan vara:

  • Moln API: OpenAI, Antropic Claude, Google PaLM
  • Lokal modell: Använda lama.cpp, ONNX Runtime, eller TorchSharp

Exempel på lokal 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();
}

Viktiga parametrar förklarade:

  • Temperatur: Kontroller slumpmässighet (0 = alltid plocka mest troligt, 1 = prov slumpmässigt)
  • Överst P: Nucleus provtagning - endast överväga polletter som utgör topp P sannolikhet massa
  • Max Tokens: Begränsa svarslängden
  • Stoppa sekvenser: När man ska sluta generera

Steg 3: Efterbearbetning

Efter att LLM genererat ett svar, måste vi ofta:

  • Extrahera citeringar och konvertera dem till länkar
  • Formatkodsblock
  • Lägg till metadata (källor, konfidenspoäng)
  • Logga interaktionen för felsökning

Exempel på efterbehandling:

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

Förstå LLM Internals: Tokens, KV Cache och Context Windows

Innan vi går över till praktiska tillämpningar är det viktigt att förstå hur LLMs fungerar internt. Denna kunskap hjälper dig att optimera RAG-system och undvika vanliga fallgropar.

Vad är Tokens?

Tokens är de grundläggande enheterna som LLMs bearbetar. Text matas inte direkt till modeller - den är först uppdelad i polletter.

Exempel på tokenisering:

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

Olika modeller använder olika tokeniseringsstrategier:

  • GPT-modeller: Använd Byte-Pair kodning (BPE) med ~50K ordförråd
  • Claude Ordförande: Liknande BPE-strategi
  • Llamamodeller: SentencePiece tokenization

Varför tokenisering är viktigt för RAG:

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

Gränser för sammanhangsfönster:

  • GPT-3.5: 16K polletter
  • GPT-4: 8K-128K tokens (beroende på variant)
  • Claude 3.5 Sonnet: 200K tokens
  • Llama 3: 8K polletter (men kan förlängas)

I RAG-system måste du passa:

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

Om din RAG hämtar 10 dokument med 500 polletter var, det är 5000 polletter bara för sammanhang - innan frågan och svar!

Practical RAG token management:

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-cachen: LLM:s hemliga vapen

När en LLM genererar text, det inte rebehandlar allt från scratch för varje token. cache för nyckeltal (KV) för att komma ihåg vad den redan har räknat ut.

Hur Transformatorer fungerar (förenklat)

Transformatorer använder en "attention" mekanism där varje token "besöker" (tittar på) alla tidigare polletter för att förstå sammanhang.

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

Utan KV- cache:

  • Steg 1: Process 1 token → O(1)
  • Steg 2: Process 2 polletter från grunden → O(2)
  • Steg 3: Process 3 polletter från grunden → O(3)
  • Totalt: O(1 + 2 + 3 + ... + N) = O(N2)

Med KV- cache:

  • Steg 1: Process 1 token, cache K,V → O(1)
  • Steg 2: Process 1 ny token, återanvända cached K,V → O(1)
  • Steg 3: Process 1 ny token, återanvända cached K,V → O(1)
  • Totalt: O(N)

Detta gör generation dramatiskt snabbare - Skillnaden mellan 10 polletter/sekund och 100 polletter/sekund.

KV- cacheträdets struktur

KV cache bildar ett "träd" på grund av hur uppmärksamhet fungerar i transformatorer. Varje lager i modellen har sina egna K, V matriser.

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

Varje lager lagrar:

  • Nycklar (K): Representationer som används för att beräkna uppmärksamhet poäng
  • Värden (V): Representationer som blandas baserat på uppmärksamhet

För en modell med

  • 32 skikt
  • 4096 dolda mått
  • 32 uppmärksamhetshuvuden
  • 8K sammanhangsfönster

KV- cache för en sekvens är:

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

Därför är långa sammanhangsfönster minnesintensiva.

KV Cache i RAG-system

RAG-system kan utnyttja KV-cacheoptimering på smarta sätt:

Snabb cachning (som stöds av vissa API:er som Anthropic 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);
    }
}

Varför detta är kraftfullt:

  • Första frågan: Beräknar KV- cache för systemprompt + sammanhang (slow)
  • Följande frågor med samma sammanhang: Återanvänder cachad KV (10x snabbare!)
  • Endast användarens frågedel behöver ny beräkning

Praktiskt exempel:

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

Alla tre frågor använder samma hämtade sammanhang, så KV-cachen för det sammanhanget återanvänds.

Token Limits och RAG strategi

Förstå polletter och KV-cache informerar dina RAG arkitekturbeslut:

1. Knäpp storlek

Mindre bitar = mer exakt hämtning, men mer overhead:

// 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. Användning av sammanhangsfönster

Maximera inte det sammanhangsberoende fönstret - lämna plats för generation:

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. Flervarviga RAG-samtal

I chattrobotar växer konversationshistorien med varje vändning:

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!

Lösning: Skjutfönster med 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. Kostnadsoptimering

API-baserade LLMs laddning per token. RAG kan explodera kostnader om inte försiktig:

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

Strategier för kostnadsminskning:

  1. Hämta färre, bättre rangordnade dokument
  2. Använd snabb caching (Antropic Claude: 90% billigare för cachade polletter)
  3. Använd billigare modeller för omplacering, dyrt för sista generationen
  4. Komprimera sammanhang med hjälp av summering

Visualisera det fullständiga RAG-flödet med tokens

Så här passar polletter, KV-cache och RAG ihop:

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

Nyckelinsikter:

  1. Inmatnings- polletter (2,812) bearbetas en gång för att bygga den ursprungliga KV-cachen
  2. Generering händer en token åt gången, återanvänder KV- cache
  3. Varje nytt tecken lägger till KV- cache för framtida polletter att titta på
  4. Totalt belopp för VRAM behövs = Modellvikter + KV-cache för alla polletter
  5. Längre sammanhang = större KV-cache = mer VRAM

Praktiska konsekvenser för RAG

Förstå polletter och KV-cache leder till bättre RAG-design:

1. Förkompilera och cache gemensamma sammanhang:

// 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. Optimera bitars gränser:

// 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. Övervaka tokenanvändning i produktionen:

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

Slutsats: Arkitekturmasteri

Vi har täckt hela den tekniska arkitekturen av RAG-system:

Fas 1: Indexering

  • Textutdrag från olika källor
  • Chunking strategier (avsnitt-baserade, meningsbaserade, med överlappning)
  • Inbäddning av produktion (ONNX, API-tjänster)
  • Vektorlagring (Qdrant, pgvector, Pinecone)

Fas 2: Återhämtning

  • Fråga inbäddning (samma modell som indexering!)
  • Likvärdighetssökning (kosin, euklidean, punktprodukt)
  • Valfri omplacering för bättre precision
  • Metadatafiltrering för förfinade resultat

Fas 3: Generering

  • Snabb konstruktion (text + instruktioner + fråga)
  • LLM-slutsats (temperatur, topp-p, max polletter)
  • Efterbehandling (citeringsextraktion, formatering)

Inhemska LLM-enheter

  • Tokens: De grundläggande enheterna (inte karaktärerna!)
  • KV Cache: Varför generation är snabb (linjär, inte quadratic)
  • Sammanhangsfönster: Hantera tokengränser i RAG
  • Kostnadsoptimering: Caching, komprimering, smart hämtning

Viktiga tekniska insikter:

  1. Samma modell för inbäddning för indexering och hämtning (kritiskt!)
  2. Knäpphet betyder mer än du tror - bevarar semantisk sammanhållning
  3. Omställning förbättrar precisionen på bekostnad av latency
  4. Token management är viktigt - uppskattning före förfrågan
  5. KV-cachelagring gör RAG genomförbar - återanvändning av snabba beräkningar
  6. Sammanhangsfönster fyller snabbt - 10 dokument × 500 polletter = 5K polletter

Fortsätt till del 3: RAG i praktiken

Du förstår nu hur RAG fungerar På teknisk nivå. Men teorin får dig bara så långt. Hur bygger du egentligen dessa system? Vilka utmaningar kommer du att möta? Vilka avancerade tekniker kan du använda?

Till Del 3: RAG i praktiken, vi går från arkitektur till genomförande:

Verkliga tillämpningar:

  • Relaterade inlägg rekommendation på den här bloggen
  • Semantisk bloggsökning
  • Bygga en "laglig GPT" skrivande assistent

Gemensamma utmaningar och lösningar:

  • Chunking strategier som bevarar sammanhanget
  • Förbättrar bäddkvalitet för din domän
  • Hantering av sammanhangsfönster dynamiskt
  • Förhindra hallucinationer trots att ha sammanhang
  • Hålla ditt index uppdaterat

Avancerade tekniker:

  • Hypotetiska dokument inbäddade (HyDE)
  • Självförfrågan med LLM-parserade filter
  • Multi-query RAG för övergripande resultat
  • Sammanhangskomprimering för att minska tokenanvändning
  • Multi-hop RAG för komplexa frågor
  • Långsiktigt samtalsminne

Komma igång:

  • Genomförandeplan varje vecka
  • Praktiska kodexempel
  • Optimeringsstrategier
  • När ska INTE RAG användas?

Fortsätt till del 3: RAG i praktiken →

Resurser

Grunddokument:

Verktyg och ramar:

Läs vidare:

Serienavigering:

Fortsätt till del 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.