Architecture et architecture interne de RAG: Comment il fonctionne réellement (Français (French))

Architecture et architecture interne de RAG: Comment il fonctionne réellement

Saturday, 22 November 2025

//

27 minute read

À l'intérieur Partie 1, nous avons examiné les origines de RAG, ' , les principes fondamentaux, , et la raison pour laquelle cela est important,. Vous comprenez le concept d’un niveau élevé, MSC5 M SK6 saisir l’information pertinente, M SK7 puis l’utiliser pour générer des réponses, МSK8 Nous plongons maintenant profondément dans l’architecture technique.

Introduction

Navigation de la série 📖 Il s'agit d'une partie 2 de la série RAGM SK1

Si vous n’avez pas lu la partie ', je recommande de commencer par comprendre celle-ci.

  • Qu'est-ce que le RAG et pourquoi il est important
  • L'histoire de la recherche par mots clés à la compréhension sémantique
  • RAG vs fine-tuning et autres approches

Cet article suppose que vous comprenez ces principes fondamentaux et se concentre sur architecture technique, détails de mise en œuvre, et internes LLM.

Comment fonctionne le RAG: Le tableau complet

Let's diviser exactement ce qui se passe dans un système RAG, du moment où vous ajoutez un document au moment où l’utilisateur reçoit une réponse

Phase 1: Indexage (Préparation de la base de connaissances)

Avant que RAG puisse récupérer quelque chose, vous devez indexer votre base de connaissances.

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

Étape 1: Extraction de texte

Extraire un texte simple de vos documents sources. Il pourrait s'agir de:

  • Dossiers de marquage (comme mes articles sur mon blog)
  • PDF (pour la documentation)
  • HTML (for web scraping)
  • Répertoires de bases de données
  • Courriels, registres de chatM SK1 etc.

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

Étape 2: activités de fabrication

C'est là où la plupart des mises en œuvre RAG échouent. Vous pouvez' ne pas simplement diviser sur les limites du paragraphe - vous avez besoin de chunks sémantiquement cohérents

Pourquoi la fragmentation est-elle importante:

  • Les LLM ont des limites de token ( fenêtres contextuelles)
  • Parties plus petites = récupération plus précise
  • Mais les chunks doivent contenir suffisamment de contexte pour être significatifs

Faible fragmentation:

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

Bonne fractionnage:

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"

Exemple de ma mise en œuvre de la recherche sémantique:

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

Stratégies de regroupement communes:

  • Fixe-dimension: Simple mais rompt les limites sémantiques
  • Sentence- fondée: Respecte la grammaire mais peut être trop petite
  • Paragraphe- fondé: Taille naturelle mais variable
  • Section- fondée: Suivant le contenu structuré (de mes préférencesM SK2
  • Vente à coucher avec chevauchement: Assure qu'aucun contexte n'est perdu aux frontières

Étape 3: Générer les embeddings

Les embeddings sont la magie qui rend possible une recherche sémantique.

Concept clé: Signes similaires → vecteurs semblables

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

Les deux premiers vecteurs seraient "closeM SK1 dans l'espace de vecteur (haute similitude cosine), alors que le troisième est loinMSC4

Comment les embeddings sont générés: Les modèles d’embedding modernes sont des réseaux neuronaux formés sur de vastes ensembles de données textuelles pour apprendre les relations sémantiques.

  • all-MiniLMM SK1L 6-v2: 384 dimensionsM SK2 rapides, bonne qualité ( ce que je vais utiliser sur ce blogMSC5
  • texte-embedding-3-small (OpenAIM SK1 1536 dimensions, très haute qualité
  • BGE-base: 768 dimensionsM SK2 état-deMSC4le-art source libre

Exemple de mon service d'embedding 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);
}

Pourquoi la normalisation est importante: Après L2 normalisation , la similitude des cosine devient un simple produit de pointesM SK2 rendant la recherche beaucoup plus rapide.

Étape 4: Entrer dans la base de données Vector

Les bases de données vectorielles sont optimisées pour stocker et rechercher des vecteurs hautement dimensionnels-Véctores dimensionnelles . Contrairement aux bases de datos traditionnelles qui utilisent les demandes SQLM SK2Les bases de base de données vectorales utilisent la recherche de similitude.

Opérations clés:

  • Ajouter: Ajouter ou mettre à jour un vecteur avec des métadonnées
  • Rechercher: Trouver des vecteurs K les plus similaires à un vecteur de demande
  • Filtre: Combinez la recherche par vecteur avec les filtres de métadonnées

Exemple de la mise en œuvre du 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 }
    );
}

Bases de données populaires sur les vecteurs:

  • Qdrant: FastM SK1 self-hostable , excellent CMST4 support MST5my choiceM ST6
  • pgvector: Extension PostgreSQL (excellent si vous utilisez déjà Postgres
  • Pinecone: Service général (prix mais bonM SK2
  • Favoriser: Caractéristique
  • ChromaDB: PythonM SK1focused, lightweight

Nous examinerons la mise en place de ces bases de données dans les prochains articles.

Phase 2: Recouvrement (Trouver des renseignements pertinents)

Lorsque l’utilisateur pose une question, le système RAG doit trouver les informations les plus pertinentes de la base de connaissances.

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

Étape 1: Générer l'intégration de la demande

La question de l’utilisateur' est convertie en vector à l’aide du le même modèle d'implantation utilisés pour indexer. Il s'agit d'un élément critique - les différents modèles produisent des vecteurs incompatiblesM SK2

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

Étape 2: Recherche de similitude

La base de données des vecteurs calcule la similitude entre le vecteur de demande et tous les vecteurs stockés.

Symétrie de cosine (le plus populaire pour les vecteurs normalisésM SK1

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

Étendue: M SK1 à 1

Distance Euclidienne (pour les vecteurs non normalisésM SK1

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

Étendue: M SK1 à ∞ ≥( inférieure MSC4 plus similaireMSc5

Produit de pointe (lors que les vecteurs sont préétablisM SK1normalisés ):

similarity = A · B

Étendue: M SK1 à 1

Exemple de mon service 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();

Étape 3: Reclassement (Optionnelle mais recommandée)

La recherche initiale est rapide mais approximative. Le reranking utilise un modèle plus sophistiqué pour réévaluer les résultats de K supérieurs.

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

Pourquoi le reclassement aide-t-il:

  • Les modèles d’embedding rapides optimisent la vitesse, en sacrifiant une certaine précision
  • Les modèles de reclassement sont plus lentes mais plus précis
  • Une approche à deux étapes de la - permet d’équilibrer la vitesse et la qualité

Exemple de mise en œuvre de reclassement:

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

Phase 3: Génération ( Création de la réponseM SK2

Maintenant que nous disposons de l’information pertinente,, nous la transmettons au LLM en même temps que la question posée par l’utilisateur '

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

Étape 1: Construction rapide

C'est là où le RAG devient un art. Vous devez structurer la prompte de façon à ce que le LLM

  • Utilise le contexte fourni (pas ses connaissances internesM SK1
  • Citer les sources lorsque possible
  • Admet que le contexte ne contient pas de réponse
  • Maintenir un ton cohérent/style

Exemple de template d'appel du système GPT de mon avocat:

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

Étape 2: Inférence LLM

Le prompt construit va au LLM pour la génération. Il peut s'agir de:

  • L'API Cloud: OpenAI, Anthropic ClaudeM SK2 Google PaLM
  • Modèle local: Utilisation de llamaM SK1cpp , OnNX Runtime, ou TorchSharp

Exemple en utilisant local 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();
}

Paramètres clés expliqués:

  • Température: Contrôle de l'aléacité (0 = choisissez toujours la probabilité la plus élevée
  • Haut de la page: Échantillonnage du noyau - considérer uniquement les tokens qui constituent la masse de probabilité supérieure P
  • Tokens maximaux: Longueur de réponse limitée
  • Poursuivre les séquences: Quand cesser de produire

Étape 3: Post-Procédure

Après que le LLM génère une réponse,, nous devons souvent

  • Extraire des citations et les convertir en liens
  • Modifier les blocs de code
  • Ajouter des métadonnées (sources,points de confianceM SK2
  • Enregistrez l'interaction pour le débugging

Exemple post-processing:

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

Comprendre les systèmes internes de LLM: Tokens, KV CacheM SK2 et Windows contextuels

Avant de passer aux applications pratiques, il est essentiel de comprendre comment les LLM fonctionnent à l’interne. Ce savoir vous aide à optimiser vos systèmes RAG et à éviter des pièges communs.

Quels sont les tokens?

Les tokens sont les unités fondamentales que les LLM traitent.

Exemple de tokenisation:

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

Divers modèles utilisent différentes stratégies de tokenisation:

  • Modèles GPT: Utilisez un ByteM SK1Encodage par pair (BPE) avec le vocabulaire de
  • Claude: Approche BPE similaire
  • Modèles Llama: Tokenisation de SentencePiece

Pourquoi la tokenisation est importante pour 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;
    }
}

Limites de la fenêtre contextuelle:

  • Tokens GPT-3.5: M SK1K
  • TÉU, TPAC, TPMD, TPG, TM, TMÉU, TC:
  • Claude 3.5 SonnetM SK1 200K tokens
  • Llama 3: 8K de cotes (qu'il est possible d'étendre les cotes)

Dans les systèmes RAG, vous devez s’adapter:

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

Si votre RAG reçoit 10 des documents de 500 tokens chacuneM SK2 que's 5,000 tokens seulement pour le contexte |- avant la demande et la réponse

Gestion pratique des tokens RAG:

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

Le cache KV: LLM' est une arme secrete

Lorsqu’un LLM génère un texte, il ne reprocesse pas de tout à partir de zéro pour chaque token . Il utilise une Key-Value M SK1KV) cache pour se rappeler ce qu’il a déjà calculé.

Comment fonctionnent les transformateurs (Simplifié)

Les transformateurs utilisent un mécanisme "attention" dans lequel chaque token "assiste à

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

Sans cache KV:

  • Étape 1: Processus 1 token → OM SK3
  • Étape 2: Processus 2 Tokens à partir de zéro
  • Étape 3: Processus 3 Tokens à partir de zéro
  • Total : OM SK1 + | | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11

Avec le cache KV:

  • Étape 1: Processus 1 token, cache KM SK3V → OMSC5
  • Étape 2: Processus 1 nouveau token, réutilisation cached KM SK3V → OMSC5
  • Étape 3: Processus 1 nouveau token, réutilisation cached KM SK3V → OMSC5
  • Total: OM SK1N )

C'est ce qui fait la génération de plus en plus rapides - la différence entre les valeurs suivantes :

Structure de l'arbre du cache KV

Le cache KV forme un "arbre" en raison de la façon dont l'attention fonctionne dans les transformateursM SK2 Chaque couche du modèle a ses propres matrices KMSC3VMNK4

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

Chaque couche de stockage:

  • clés (KM SK1: Représentations utilisées pour calculer les points d'attention
  • Valeur (VM SK1: Représentations qui se mélangent en fonction de l’attention

Pour un modèle avec:

  • 32 couches
  • 4096 dimensions cachées
  • 32 têtes d'attention
  • 8K fenêtre contextuelle

Le cache KV pour une séquence est:

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

C'est pourquoi les fenêtres contextuelles longues sont mémoire-intensive.

Cache KV dans les systèmes RAG

Les systèmes RAG peuvent optimiser le cache KV de façon intelligente:

Cache rapide (appuyé par quelques API comme 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);
    }
}

Pourquoi cela est puissant:

  • Première demande: Compute le cache KV pour la prompte du système M SK1 contexte (slow)
  • Demandes subséquentes avec le même contexte: Réutilise KV caché M SK1x plus rapides!)
  • Seule la partie de demande de l'utilisateur a besoin d'un nouveau calcul

Exemple pratique:

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

Toutes les trois demandes utilisent le même contexte récupéré, de sorte que la cache KV pour ce contexte est utilisée à nouveau.

Limites des tokens et stratégie RAG

La compréhension des tokens et du cache KV informe vos décisions d’architecture RAG:

1. Taille de fabrication

Parties plus petites = récupération plus précise, mais plus lourdeM SK2

// 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. Utilisation de la fenêtre contextuelle

Don't max out the context window - leave room for generationM SK2

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-Retourner les conversations RAG

Dans les chatbots,, l’histoire de la conversation augmente à chaque tour.

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!

Solution: fenêtre Sliding avec 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. Optimisation des coûts du token

API-Les LLM basés sur la tarification par token . Le RAG peut exploser les coûts s’il n’est pas prudent.

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

Stratégies de réduction des coûts:

  1. Récupérer moins de documents classés
  2. Utiliser le cache rapide (Anthropic ClaudeM SK1 90% moins coûteux pour les tokens cachés)
  3. Utiliser des modèles moins coûteux pour re-ranking, coûteuses pour la génération finale
  4. Comprimer le contexte en utilisant la sommation

Visualiser le flux RAG complet avec les tokens

Voici la façon dont les tokens, le cache KV, le RAG et le , passent ensemble.

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

Principaux points de vue:

  1. Tokens d'entrée (2,812) sont traités une fois pour construire le cache initial KV
  2. Génération se produit un token à la fois, en réutilisant le cache KV
  3. Chaque nouveau token Ajout au cache KV pour les tokens futurs à assister
  4. Total VRAM requis = Poids du modèle + Cache KV pour tous les tokens
  5. Contexte plus long = Cache KV plus grande = VRAM plus grand

Implications pratiques pour le RAG

La compréhension des tokens et du cache KV conduit à un meilleur design RAG:

1. Pré-contextes communs de calcul et de 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. Optimiser les limites des chunks:

// 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. Surveillance de l'utilisation des tokens dans la production

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

Conclusion: Maîtrise en architecture

Nous avons couvert l’architecture technique complète des systèmes RAG.

Indexage de la phase 1:

  • Extraction de texte à partir de diverses sources
  • Stratégies d’encouragement (section-basée sur la , sentenceM SK3basées sur la, avec chevauchement+)
  • Génie d'embedding (ONNXM SK1 Services API)
  • Entreposage de vecteurs (QdrantM SK1 pgvector, PineconeMSC3

Récupération de phase 2:

  • Intégration de la question (le même modèle que l'indexation!)
  • Recherche de similitude (cosine,euclideanMSC2 produit ponctuelM SK3
  • Remplacement facultatif pour une plus grande précision
  • Filtration des métadonnées pour les résultats raffinés

Production de phase 3:

  • Construction rapide (contexte
  • Inférence du LLM (températureM SK1 haut-p , tokens maximauxMSC4
  • Posts-processage (extraction de citationsM SK2formattage )

LLM Internals

  • Tokens: Les unités fondamentales (pas de caractèresM SK2
  • KV Cache: Pourquoi la génération est rapide M SK1linear, non quadratiqueMSC3
  • Contexte windows: Gestion des limites de cotes dans RAG
  • Optimisation des coûts: Caching, compressionM SK2 recherche intelligente

Principaux renseignements techniques:

  1. Le même modèle d'implantation pour l'indexation et la recherche (criticalM SK1
  2. Il est plus important que vous ne le croyez de s'entraîner - préserve la cohérence sémantique
  3. Le reclassement améliore la précision au prix de la latence
  4. La gestion des tokens est essentielle - estimation avant la demande
  5. Le caching KV rend possible le RAG - Renouvellement des calculs de prompt
  6. Les fenêtres de contexte remplissent rapidement - 10 pièces justificatives

Continuer à la partie 3: RAG dans la pratique

Vous comprenez maintenant Comment fonctionne le RAG à l’échelon technique. Mais la théorie ne vous mène que jusqu’à présent. Comment bâtir réellement ces systèmesM SK2 Quels sont les défis auxquels vous serez confrontés ? Quelles techniques avancées pouvez-vous appliquer?

À l'intérieur Partie 3: RAG dans la pratique, nous passerons de l’architecture à la mise en œuvre

Applications réelles-world applications:

  • Postes connexes Recommandation sur ce blog
  • Recherche sur le blog sémantique
  • Établissement d’un "avocat GPT" assistant à la rédaction

Difficultés et solutions communes:

  • Stratégies de recyclage qui préservent le contexte
  • Améliorer la qualité de l'implantation pour votre domaine
  • Gestion dynamique des fenêtres contextuelles
  • Prévenir les hallucinations malgré le contexte
  • Maintenir votre index à jour-àM SK1date

Techniques avancées:

  • Embeddings hypothétiques de documents (HyDE)
  • Self-querying with LLM-parsed filters
  • Multi-recherche RAG pour obtenir des résultats complets
  • Compression contextuelle pour réduire l'utilisation des tokens
  • Multi-hop RAG pour les demandes complexes
  • Mémoire de conversation à long terme

Pour commencer:

  • Plan de mise en œuvre hebdomadaire
  • Exemples de codes pratiques
  • Stratégies d'optimisation
  • Quand ne pas utiliser RAG

Continuer à la partie 3: RAG dans la pratique →

Ressources

Documents de base:

Outils et cadres:

Lire plus:

Navigation de série:

Continuer à la partie 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.