Back to "Архітектура і внутрішні органи: Як вона насправді працює"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI AI-Article LLM Machine Learning RAG Semantic Search

Архітектура і внутрішні органи: Як вона насправді працює

Saturday, 22 November 2025

Вхід Частина 1Ви розумієте концепцію високого рівня: отримати потрібну інформацію, а потім використати її для створення відповідей.

Вступ

Навігація: Це частина 2 серії з РАГ:

Якщо ви ще не читали частини 1, я рекомендую вам розпочати з цього розуміння:

  • Що таке РАГ і чому це важливо
  • Журнал від пошуку за ключовими словами до семантичного розуміння
  • RAG проти fine- tuning та інших підходів

Ця стаття показує, що ви розумієте ці основи і зосереджуєтесь на них. технічна архітектура, реалізація та внутрішні елементи LLM.

Як працює RAG: повна картина

Давайте розіб'ємо точно те, що відбувається у системі RAPG, з моменту додавання документа до того часу, коли користувач отримає відповідь.

Індексування фази 1: (Завершення бази знань)

Перш ніж RAG зможе отримати будь- які відомості, вам слід індексувати вашу базу знань. Цей процес є одноразовим (хоча пізніше ви зможете додавати нові документи).

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

    B -.Metadata.-> E

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

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

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

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

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

Крок 1: Видобування тексту

Видобування звичайного тексту з ваших документів джерела. Це може бути:

  • Позначки файлів (на зразок дописів у моєму блозі)
  • PDFs (для документації)
  • HTML (для використання веб- зображень)
  • Записи баз даних
  • Ел. пошта, журнали балачки тощо.

Приклад з мого блогу:

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

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

    return plainText.Trim();
}

Крок 2: Розшифрування

У цьому випадку більшість реалізацій RAG зазнає невдачі. Ви не можете просто розділитися на межі абзаців - вам потрібні семантично з' єднані шматки.

Навіщо різати справи:

  • LLM має обмеження на ALM (контекстові вікна)
  • Менші шматки = точніше отримання
  • Але шматки повинні містити достатньо контексту, щоб мати значення

Погане різання шматків:

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

Хороший шматок:

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

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

Приклад з моєї реалізації семантичного пошуку:

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

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

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

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

        return chunks;
    }
}

Поширені стратегії розрізання шматків:

  • Фіксований розмір: Прості, але порушують семантичні межі
  • Заснований на реченні: Поважає граматику, але може бути замала.
  • Заснований на абзаці: Натуральний, але змінний розмір
  • Заснований на розділі: Найкраще для структурованого вмісту (за моїм уподобанням)
  • З' єднання вікна з перекриттям: Запевняє, що на кордонах не втрачено жодного контексту

Крок 3: Створити вбудовування

Вбудовування - це магія, яка уможливлює семантичний пошук. Вбудовування - це вектор (масив чисел), який відповідає значенню тексту.

Концепція ключа: Подібні значення → подібні вектори

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

Перші два вектори будуть "закриті" у векторному просторі (високий косинус), тоді як третій знаходиться далеко.

Спосіб створення вбудовування: Сучасні моделі вбудовування - це нейронні мережі, треновані на масивних текстових наборах даних для вивчення семантичних зв'язків. Популярні моделі:

  • all- MiniLM- L6- v2: 384 виміри, швидка, гарна якість (те, що я використовую у цьому блозі)
  • text- embow- 3- little (OpenAI): 1536 вимірів, дуже висока якість
  • База BGE: 768 вимірів, відкрите джерело екстазів

Приклад з моєї служби вбудовування 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);
}

Чому нормалізація важлива: Після нормалізації косинус стає простим продуктом крапки, що значно пришвидшує пошук.

Крок 4: Зберегти у базі даних векторів

Бази даних векторів оптимізовані для зберігання і пошуку високовимірних векторів. На відміну від традиційних баз даних, які використовують запити SQL, у базах даних векторів використовується пошук подібності.

Дії з ключами:

  • Upsert: Додати або оновити вектор з метаданими
  • Пошук: Знайти K найбільш подібні вектори для вектора запитів
  • Фільтр: Об' єднати векторний пошук з фільтрами метаданих

Приклад реалізації Qdrant:

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

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

Популярні бази даних векторів:

  • Qdrant: Швидка, самопідтримка, чудова підтримка C# (Мій вибір)
  • pgvector: Суфікс PostgreSQL (завершений, якщо ви вже використовуєте Postgres)
  • Сосна: Керована служба (складна, але добра)
  • Тяжкі: Багатий на можливості, добрий для складних схем
  • ChromaDB: Фокусований Python, легкий

Ми дослідимо створення баз даних у наступних статтях.

Фаза 2: отримання (Пошукання актуальної інформації)

Коли користувач задає питання, система RAG повинна знайти найвідповіднішу інформацію з бази знань.

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

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

Крок 1: Створити вбудовування запиту

Питання користувача буде перетворено у вектор за допомогою та сама модель вбудовування використовується для індексування. Це критично: різні моделі створюють несумісні вектори.

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

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

    return results;
}

Крок 2: Пошук подібності

Векторна база даних обчислює подібність між вектором запиту і всіма збереженими векторами. Загальні виміри:

Подібність косину (більш популярний для нормалізованих векторів):

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

Діапазон: - 1 до 1 (високий = більш подібний)

Евклідова відстань (для ненормалізованих векторів):

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

Діапазон: 0 до ⇩ (точка = більш подібна)

Продукт крапки (якщо вектори є попередньо нормалізованими):

similarity = A · B

Діапазон: - 1 до 1 (високий = більш подібний)

Приклад з моєї служби Qdrant:

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

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

Крок 3: Перевищення (додатковий, але рекомендований)

Початкове отримання є швидким, але приблизним. Вирівнювання використовує більш складну модель для отримання найкращих результатів K.

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

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

Чому повторне чергування допомагає:

  • Моделі швидкого вбудовування оптимізовані для швидкості, з деякою точністю
  • Переробка моделей повільніша, але точніша
  • Двоступеневий підхід веде до балансу швидкості і якості

Приклад перевпорядкування:

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

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

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

Покоління (завершення відповіді)

Тепер, коли у нас є відповідна інформація, ми підставляємо її LLM разом з питанням користувача.

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

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

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

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

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

Крок 1. Спрощене будівництво

Тут RAG стає мистецтвом. Вам слід структурувати запрошення так, щоб це було LLM:

  • Використає вказаний контекст (не його внутрішні знання)
  • За можливості, коди Cites
  • Признає, якщо контекст не містить відповіді
  • Підтримує послідовний тон/ стиль

Приклад шаблона запиту з моєї адвокатської системи GPT:

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

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

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

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

    return sb.ToString();
}

Крок 2: Підсилення LLM

Спроектований рядок іде до LLM для створення. Це може бути:

  • Хмара API: OpenAI, Anthropic Claw, Google PALM
  • Локальна модель: Використання лам. cpp, ONK Runtime або TorchSap

Приклад з локальною LLM:

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

    return result.Text.Trim();
}

Пояснення параметрів ключів:

  • Температура: Контролює випадковість (0 = завжди найімовірніше, 1 = вибірка випадково)
  • Верхній P: Обчислення Nucleus - враховуються лише позначки, з яких складається верхня маса P ймовірності
  • Макс. кноп: Обмежити довжину відповіді
  • Зупинити послідовності: Коли припинити створення

Крок 3: Після обробки

Після того, як LLM створює відповідь, нам часто потрібно:

  • Видобування посилань і перетворення їх на посилання
  • Форматувати блоки коду
  • Додати метадані (описи, результати довіри)
  • Зареєструвати взаємодію для зневаджування

Приклад остаточної обробки:

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

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

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

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

    return response;
}

Розуміння внутрішніх елементів LLM: ключів, кешу KV і контекстних вікон

Перш ніж перейти до практичних програм, необхідно зрозуміти, як працює LLM. Це знання допомагає оптимізувати системи RAG і уникати типових пасток.

Що таке тонки?

Ключі є фундаментальними одиницями процесу LLM.

Приклад позначення:

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

Різні моделі використовують різні стратегії позначення:

  • Моделі GPT: Використовувати кодування байтів (BPE) зі словником ~50K
  • Клод.: Подібний підхід до BPE
  • Моделі Llama: Декламація речень

Навіщо підписувати питання 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;
    }
}

Обмеження контекстних вікон:

  • GPT- 3. 3: 16K- маркери
  • GPT- 4: 8K- 128 K- маркерів (залежить від варіанта)
  • Claude 3. 5 Sonnet: 200K tags
  • Llama 3: 8K marks (taw можна продовжити)

У системах RAG вам слід вписати:

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

Якщо ваш комп'ютер отримає 10 документів з 500 жетонів за кожен, то це 5000 жетонів лише для контексту, перед запитом і відповіддю!

Практичне керування ключами 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;
    }
}

Кеш KV: Секретна зброя LLM

Коли LLM створює текст, він не виконує всі дії з нуля для кожного ключа. Він використовує a Кеш значень ключів (KV) щоб запам'ятати те, що він вже обчислював.

Як працюють перетворення (спрощено)

Трансформатори використовують механізм " неуважності ," у якому кожен знак " відповідає " (див.) для розуміння контексту всі попередні позначки.

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

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

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

    D1 --> A2
    D2 --> A3

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

Без кешу KV:

  • Крок 1: Процес 1 ключ → О' 1)
  • Крок 2: Процес 2 маркери від нуля → О'2
  • Крок 3: Процес 3 маркери від нуля → О'3)
  • Всього: O'1 + 2 + 3 +... + N) = O'N2

З кешем KV:

  • Крок 1: Помітка 1 процесу, кеш K, V → O} 1)
  • Крок 2: Новий ключ процесу 1, повторне кешування K, V → O' 1)
  • Крок 3: Новий ключ процесу 1, повторне кешування K, V → O' 1)
  • Всього: O'N)

Це створює створення значно швидше - різницю між 10 марками/секундою і 100 жетонами/секундами.

Структура дерева кешу KV

Кеш KV створює " дерево " через те, як працює увага у трансформаторах. Кожен шар моделі має свої матриці K, V.

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

Кожен з сховищ шарів:

  • Ключі (K): Представлення, які буде використано для обчислення рахунку уваги
  • Значення (V): Представлення, які змішуються на основі уваги.

Для моделі з:

  • 32 шари
  • Приховані розміри 4096
  • 32 голови уваги
  • Контекстне вікно 8K

Кеш KV для однієї послідовності:

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

Ось чому довгі контекстні вікна впливають на пам'ять.

Кеш KV у системах RAG

Система RAG може використовувати оптимізацію кешу KV у кмітливий спосіб:

Запропонувати кешуванняThe role of the transaction, in past tense (підтриманий деякими API, як Антропічний Клод):

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

Чому це має силу?

  • Перший запит: обчислює кеш KV для системного запрошення + context (низько)
  • Наступні запити з таким же контекстом: Повторно кешує KV (10x швидше!)
  • Тільки частина запиту користувача потребує свіжих обчислень

Практичний приклад.

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

Всі три запити використовують однаковий контекст, отже кеш KV для цього контексту буде повторно використано.

Межі тону і стратегія RAG

Розуміння жетонів і кешу KV інформує ваші архітектурні рішення RAG:

Розмір 1

Менші шматки = точніше отримання, але більше надходження:

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

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

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

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

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

2. Утиліта контекстного вікна

Не перевищуйте контекст вікна - залиште місце для створення:

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

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

3. Розмови RAG з декількома назад

У chatbots історія спілкування зростає з кожним хідом:

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

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

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

Вирішення: З' єднання вікна з повторним показом

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. Оптимізація вартості тону

Заснований на API LLMs заряд на ключ. RAG може підірвати витрати, якщо не бути обережним:

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

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

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

        return inputCost + outputCost;
    }

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

Стратегія зниження вартості:

  1. Отримати менше, краще підготовлених документів
  2. Використовувати миттєве кешування (Антропічний Клод: 90% дешевше для кешованих елементів)
  3. Використовувати дешевші моделі для перевищення, дорогі для останнього покоління
  4. Стискання контексту за допомогою резюме

Візуалізація повного прямого потоку з ключами

Ось як впорядковуються маркери, кеш KV і RAG:

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

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

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

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

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

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

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

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

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

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

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

Розуміння ключових слів:

  1. Вхідні позначки (2, 812) обробляється один раз для збирання початкового кешу KV
  2. Створення відбувається одночасно з одним елементом, повторне використання кешу KV
  3. Кожен новий знак додає до кешу KV, до якого слід додати наступні позначки
  4. Всього VRAM потрібен = Вага моделі + кеш KV для всіх елементів
  5. Довший контекст = більший кеш KV = більший VRAM

Практичні вимоги до реанімації

Розуміння елементів і кешу KV дає змогу покращити компонування RAG:

1. Спільні контексти попереднього порівняння і кешу:

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

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

2. Оптимізувати межі шматка:

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

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

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

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

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

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

    return chunks;
}

3. Використання ключа спостереження у виробництві:

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

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

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

Висновки: Архітектура Mastery

Ми розглянули технічну архітектуру систем РАГ.

Індексування фази 1:

  • Текст, отриманий з різних джерел
  • Стратегії shunking (засновані на секціях, заснованих на реченні, з перекриттям)
  • Створення вбудовування (ONNX, служби API)
  • Векторне сховище (Qdrant, pgvector, Pincone)

Фаза 2: отримання

  • Вбудовування запиту (те саме, що і індексування!)
  • Пошук подібності (coine, evclidean, dot production)
  • Необов' язкове перенавантаження для кращої точності
  • Фільтрування метаданих для удосконалених результатів

Покоління фази 3:

  • Запитувати побудову (контекст + інструкції + запит)
  • LLMCExption (температура, верхня п, макс. маркери)
  • Після обробки (диспетчерська видобування, форматування)

Внутрішні елементи LLM

  • Корінці: фундаментальні одиниці (не символи!)
  • Кеш KV: Чому створення швидке (лінійне, а не квадратне)
  • Контекстні вікна: керування полями у RAG
  • Оптимізація вартості: кешування, стиснення, отримання кмітливості

Основне технічне розуміння:

  1. Та сама модель вбудовування для індексування і отримання (критичний!)
  2. Бійка має більше значення, ніж ви думаєте - Зберігає семантичні зв'язки.
  3. Перевпорядкування покращує точність коштом спізнення
  4. Важливим є керування ключами. - оцінка перед запитом
  5. Caching KV робить RAG cann - повторно використовувати допоміжні обчислення
  6. Швидко заповнюють контекстні вікна - 10 docs × 500 маркерів = 5 K

Продовжуйте працювати в частині 3: RAG на практиці

Тепер ви розумієте Як працює RAG Але теоретика лише поки що просуває вас, як ви побудуєте ці системи, з якими труднощами ви зіткнетеся, якими додатковими технологіями ви можете скористатися?

Вхід **Частина 3: ПСГ на практиці**Ми переходимо від архітектури до реалізації:

Програми з реального світу:

  • Рекомендація пов' язаних дописів у цьому блозі
  • Пошук семантичних блогів
  • Побудова помічника запису " Lawier GPT "

Звичайні виклики і рішення:

  • Розшифрування стратегій, що зберігають контекст
  • Покращення якості вбудовування для вашого домену
  • Керування контекстними вікнами динамічно
  • Запобігання галюцинаціям, незважаючи на контекст
  • Збереження індексу

Додаткові методи:

  • Вбудовування гіпотечного документа (HyDE)
  • Самоперевірка з фільтрами LLM-parsed
  • Багатоquery RAG для комплексних результатів
  • Контекстне стиснення, щоб зменшити використання ключа
  • Багатокористуватний RAG для складних запитів
  • Довгострокова розмовна пам' ять

Перші кроки:

  • План реалізації тижня за тижнем
  • Практичні приклади коду
  • Оптимізація стратегії
  • Якщо не використовувати RAG

Продовжити до частини 3: РАГ у практиці →

Ресурси

Фундаментальні папери:

Інструменти і функціонування кадрів:

Подальше читання:

Навігація серією:

Продовжити до частини 3 →

logo

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