Back to "RAG в практиці: будування програм реального світу"

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

RAG в практиці: будування програм реального світу

Saturday, 22 November 2025

Вхід Частина 1 і Частина 2 У цій серії ми обговорювали походження, фундаментальні принципи та технічну архітектуру. Ви зрозуміли, що таке RAG, чому це важливо, і як він працює під капотом. Тепер настав час вкласти ці знання в практику. Ця стаття показує вам, як будувати справжні системи RAG з кодом work C#, розв' язати спільні проблеми і використовувати додаткові методи з нещодавніх досліджень.

Вступ

Навігація: Це частина 3 серії RAG:

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

  • Що таке RAG і чому це важливо (частина 1)
  • Журнал від пошуку за ключовими словами до семантичного розуміння (частина 1)
  • Повна протока RAG: індексування, отримання, створення (частина 2)
  • Внутрішні елементи LLM: поля, кеш KV, контекстні вікна (Part 2)
  • RAG проти fine- tuning та інші підходи (частина 1)

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

Програми для RAG реального світу у цьому блозі

На цьому блозі я створив декілька специфічних особливостей РАГ.

Порада повісток

Кожен допис блогу може показувати " Послані повідомлення ," використовуючи семантичну подібність.

Як це працює:

  1. Кожен допис блогу буде вбудовано під час оприлюднення
  2. Переглядаючи пост, ми отримуємо його вбудовування у КДранті.
  3. Знайти 5 найбільш схожих дописів
  4. Показувати як " Послані повідомлення "

Чому це краще за мітки:

  • Мітки потребують вручнуї категоризації
  • Семантичний пошук знайдено концептуально пов' язані дописи, навіть без відповідних міток
  • "Докер Композитний" і "Континенційний оркестрування" є пов'язаними семантичними дописами

Фрагмент коду:

public async Task<List<SearchResult>> GetRelatedPostsAsync(
    string currentPostSlug,
    string language,
    int limit = 5)
{
    // Get the current post's embedding
    var currentPost = await _vectorStore.GetByIdAsync(currentPostSlug);

    if (currentPost == null)
        return new List<SearchResult>();

    // Find similar posts
    var similarPosts = await _vectorStore.SearchAsync(
        currentPost.Embedding,
        limit: limit + 1,  // +1 because result includes the current post
        filter: new Filter
        {
            Must =
            {
                new Condition
                {
                    Field = "language",
                    Match = new Match { Keyword = language }
                }
            },
            MustNot =
            {
                new Condition
                {
                    Field = "slug",
                    Match = new Match { Keyword = currentPostSlug }
                }
            }
        }
    );

    return similarPosts.Take(limit).ToList();
}

2. Семантичний пошук блогу

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

Досвід користувача:

  • Пошук " налаштування бази даних " → знаходить дописи про PostgreSQL, Frameturework, міграція
  • Пошук у " розділі " → знаходить дописи щодо Docker, hosting, CI/CD
  • Для точного пошуку ключових слів немає потреби

Впровадження: Я повідомлю про це у наступній статті про векторні бази даних.

3. Помічник писання Lavier GPT

Я створюю повну систему РАГ, щоб допомогти мені писати нові блоги.

Регістр використання: Коли я починаю писати про " завершення розпізнавання ядра ASP.NET ," системи:

  1. Вбудовує мій поточний чернеток
  2. Пошук у попередніх дописах пов' язаних даних щодо розпізнавання, ASP.NET, безпеки
  3. Пропонує відповідні фрагменти коду, які я використовував раніше
  4. Автоматично створювати внутрішні посилання на споріднені дописи
  5. Підтримує консистенцію з моїм стилем запису

Повнофункціональний трубопровод ПСГ:

public async Task<WritingAssistanceResponse> GetSuggestionsAsync(
    string currentDraft,
    string topic)
{
    // 1. Embed the current draft
    var draftEmbedding = await _embeddingService.GenerateEmbeddingAsync(
        currentDraft
    );

    // 2. Retrieve related past content
    var relatedPosts = await _vectorStore.SearchAsync(
        draftEmbedding,
        limit: 5
    );

    // 3. Build context for LLM
    var prompt = BuildWritingAssistancePrompt(
        currentDraft,
        topic,
        relatedPosts
    );

    // 4. Generate suggestions using local LLM
    var suggestions = await _llmService.GenerateAsync(prompt);

    // 5. Extract and format citations
    var response = ExtractCitations(suggestions, relatedPosts);

    return response;
}

Це RAG у дії - отримання (семантичний пошук) + доповнення (збільшення) + створення (пропозиції LLM).

Поширені труднощі та вирішення проблем

Розробка систем RAG - це не дріб'язкові проблеми, з якими я стикався, і спосіб їх розв'язання.

Трудність 1. Стратегія розпилювання

Проблема: Як розділити документи? Замало = втрату контексту. Завелика = несуттєва інформація.

Вирішення: Гибридне групування на основі структури документа.

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

        // Parse markdown into sections
        var document = Markdown.Parse(markdown);
        var sections = ExtractSections(document);

        foreach (var section in sections)
        {
            var wordCount = CountWords(section.Content);

            if (wordCount < MinChunkSize)
            {
                // Merge small sections
                MergeWithPrevious(chunks, section);
            }
            else if (wordCount > MaxChunkSize)
            {
                // Split large sections
                var subChunks = SplitSection(section);
                chunks.AddRange(subChunks);
            }
            else
            {
                // Just right
                chunks.Add(CreateChunk(section, sourceId));
            }
        }

        return chunks;
    }
}

Найкращі вправи:

  • Поважайте структуру документа (головки, абзаци)
  • Додати перетин між шматками (50- 100 слів)
  • Зберігати непідвладні блоки коду
  • Включити заголовки розділів до кожного шматка для контексту

Трудність 2. Вбудована якість

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

Вирішення:

Параметр 1: вбудовування якісного налаштування (порівняно)

# Using sentence-transformers in Python
from sentence_transformers import SentenceTransformer, InputExample, losses

model = SentenceTransformer('all-MiniLM-L6-v2')

# Create training examples from your domain
train_examples = [
    InputExample(texts=['Docker Compose', 'container orchestration'], label=0.9),
    InputExample(texts=['Entity Framework', 'ORM database'], label=0.9),
    InputExample(texts=['Docker', 'apple fruit'], label=0.1)
]

# Fine-tune
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
train_loss = losses.CosineSimilarityLoss(model)
model.fit(train_objectives=[(train_dataloader, train_loss)], epochs=1)

Варіант 2: Вбудовування гібриди (комбінувати декілька моделей)

public async Task<float[]> GenerateHybridEmbeddingAsync(string text)
{
    var semantic = await _semanticModel.GenerateEmbeddingAsync(text);
    var keyword = await _keywordModel.GenerateEmbeddingAsync(text);

    // Concatenate or weighted average
    return CombineEmbeddings(semantic, keyword);
}

Параметр 3: Додати фільтрування метаданих

var results = await _vectorStore.SearchAsync(
    queryEmbedding,
    limit: 10,
    filter: new Filter
    {
        Must =
        {
            new Condition { Field = "category", Match = new Match { Keyword = "ASP.NET" } },
            new Condition { Field = "date", Range = new Range { Gte = "2024-01-01" } }
        }
    }
);

Трудність 3: Керування контекстними вікнами

Проблема: LLM має обмеження на ALM. Як ви вписуєтеся у запит + context + information у вікні?

Вирішення: Динамічний вибір контексту і резюме.

public string BuildContextAwarePrompt(
    string query,
    List<SearchResult> retrievedDocs,
    int maxTokens = 4096)
{
    var promptTemplate = GetPromptTemplate();
    var queryTokens = CountTokens(query);
    var templateTokens = CountTokens(promptTemplate);

    // Reserve tokens for: prompt + query + response
    var availableForContext = maxTokens - queryTokens - templateTokens - 500; // 500 for response

    // Add context until we hit limit
    var selectedContext = new List<SearchResult>();
    var currentTokens = 0;

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

        if (currentTokens + docTokens <= availableForContext)
        {
            selectedContext.Add(doc);
            currentTokens += docTokens;
        }
        else
        {
            // Try summarizing the doc if it's important
            if (doc.Score > 0.85)
            {
                var summary = await SummarizeAsync(doc.Text, maxTokens: 200);
                var summaryTokens = CountTokens(summary);

                if (currentTokens + summaryTokens <= availableForContext)
                {
                    selectedContext.Add(new SearchResult
                    {
                        Text = summary,
                        Title = doc.Title,
                        Score = doc.Score
                    });
                    currentTokens += summaryTokens;
                }
            }
        }
    }

    return FormatPrompt(query, selectedContext);
}

Трудність 4.

Проблема: Навіть з контекстом, LLM іноді ігнорує його і галюцинатив.

Вирішення:

Перша - агресивна інженерія:

var systemPrompt = @"
You are a technical assistant.

CRITICAL RULES:
1. ONLY use information from the provided CONTEXT sections
2. If the context doesn't contain the answer, say 'I don't have enough information in the provided context to answer that'
3. DO NOT use your training data to supplement answers
4. Always cite the source using [1], [2] notation
5. If you're unsure, say so

CONTEXT:
{context}

QUESTION: {query}

ANSWER (following all rules above):
";

Перевірка 2. після створення:

public async Task<bool> ValidateResponseAgainstContext(
    string response,
    List<SearchResult> context)
{
    // Check if response contains claims not in context
    var responseSentences = SplitIntoSentences(response);

    foreach (var sentence in responseSentences)
    {
        var isSupported = await IsClaimSupportedByContext(sentence, context);

        if (!isSupported)
        {
            _logger.LogWarning("Hallucination detected: {Sentence}", sentence);
            return false;
        }
    }

    return true;
}

3) Ітеративне уточнення:

public async Task<string> GenerateWithValidationAsync(
    string query,
    List<SearchResult> context,
    int maxAttempts = 3)
{
    for (int attempt = 0; attempt < maxAttempts; attempt++)
    {
        var response = await _llm.GenerateAsync(
            BuildPrompt(query, context)
        );

        var isValid = await ValidateResponseAgainstContext(response, context);

        if (isValid)
            return response;

        // Refine prompt for next attempt
        query = $"{query}\n\nPrevious attempt hallucinated. Stick strictly to the context.";
    }

    return "I couldn't generate a reliable answer. Please rephrase your question.";
}

Трудність 5. Підтримуйте індекс

Проблема: Як ви додаєте нові документи, векторна база даних має залишатися поточною.

Вирішення: Автоматизовано індексування трубопроводу.

public class BlogIndexingBackgroundService : BackgroundService
{
    private readonly IVectorStoreService _vectorStore;
    private readonly IMarkdownService _markdownService;
    private readonly ILogger<BlogIndexingBackgroundService> _logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await IndexNewPostsAsync(stoppingToken);

                // Check for updates every hour
                await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in indexing service");
            }
        }
    }

    private async Task IndexNewPostsAsync(CancellationToken ct)
    {
        var allPosts = await _markdownService.GetAllPostsAsync();

        foreach (var post in allPosts)
        {
            var existingDoc = await _vectorStore.GetByIdAsync(post.Slug);

            // Check if content changed
            var currentHash = ComputeHash(post.Content);

            if (existingDoc == null || existingDoc.ContentHash != currentHash)
            {
                _logger.LogInformation("Indexing updated post: {Title}", post.Title);

                var chunks = _chunker.ChunkDocument(post.Content, post.Slug);

                foreach (var chunk in chunks)
                {
                    var embedding = await _embeddingService.GenerateEmbeddingAsync(chunk.Text);

                    await _vectorStore.UpsertAsync(
                        id: $"{post.Slug}_{chunk.Index}",
                        embedding: embedding,
                        metadata: new Dictionary<string, object>
                        {
                            ["slug"] = post.Slug,
                            ["title"] = post.Title,
                            ["chunk_index"] = chunk.Index,
                            ["content_hash"] = currentHash
                        },
                        ct: ct
                    );
                }
            }
        }
    }
}

Додаткові технології RAG

Давайте дослідимо методи порізного RAG з нещодавніх досліджень.

Вбудовування гіпотечного документа (HyDE)

Проблема: Запити користувачів часто є короткими і погано сформованими. Шматки документів докладно і добре написані. Таке невідповідність шкодить отриманню.

Вирішення: Створює гіпотетичний ідеальний документ, який би відповідав на запит, вбудував би його, а потім шукав би.

public async Task<List<SearchResult>> HyDESearchAsync(string query)
{
    // Generate hypothetical answer (even if hallucinated)
    var hypotheticalAnswer = await _llm.GenerateAsync($@"
        Write a detailed, technical paragraph that would perfectly answer this question:

        Question: {query}

        Paragraph:"
    );

    // Embed the hypothetical answer
    var embedding = await _embeddingService.GenerateEmbeddingAsync(
        hypotheticalAnswer
    );

    // Search using this embedding
    return await _vectorStore.SearchAsync(embedding);
}

Чому це працює: На гіпотетичній відповіді використовується подібна мова і структура для справжніх документів, що покращують отримання даних.

2. Самоуправління

Проблема: Запити користувача часто змішують семантичний пошук з фільтрами метаданих.

Приклад: " Спростити дописи про Docker " = secutive} "Docker" + filter} {date > 2024- 01- 01)

Вирішення: Скористайтеся LLM для обробки запиту на фільтри семантики + метаданих.

public async Task<SearchQuery> ParseSelfQueryAsync(string naturalLanguageQuery)
{
    var parsingPrompt = $@"
        Parse this search query into:
        1. Semantic search query (what the user is looking for)
        2. Metadata filters (category, date range, etc.)

        User Query: {naturalLanguageQuery}

        Output JSON:
        {{
            ""semantic_query"": ""the core concept"",
            ""filters"": {{
                ""category"": ""...",
                ""date_after"": ""..."",
                ""date_before"": ""...""
            }}
        }}
    ";

    var jsonResponse = await _llm.GenerateAsync(parsingPrompt);
    var parsed = JsonSerializer.Deserialize<SearchQuery>(jsonResponse);

    return parsed;
}

// Use parsed query
var parsedQuery = await ParseSelfQueryAsync("Recent ASP.NET posts about authentication");
// semantic_query: "authentication"
// filters: { category: "ASP.NET", date_after: "2024-01-01" }

var results = await _vectorStore.SearchAsync(
    embedding: await _embeddingService.GenerateEmbeddingAsync(parsedQuery.SemanticQuery),
    filter: BuildFilter(parsedQuery.Filters)
);

3. Багатофункціональне RAG

Проблема: Одному запиту може бути пропущено відповідні документи, що виникають через фрасування.

Вирішення: Створити декілька варіантів запиту, виконати пошук з усіма, результати комбінування.

public async Task<List<SearchResult>> MultiQuerySearchAsync(string query)
{
    // Generate query variations
    var variations = await _llm.GenerateAsync($@"
        Generate 3 different ways to phrase this search query:

        Original: {query}

        Variations (one per line):
    ");

    var queries = variations.Split('\n', StringSplitOptions.RemoveEmptyEntries)
        .Prepend(query) // Include original
        .ToList();

    // Search with all variations
    var allResults = new List<SearchResult>();

    foreach (var q in queries)
    {
        var embedding = await _embeddingService.GenerateEmbeddingAsync(q);
        var results = await _vectorStore.SearchAsync(embedding, limit: 10);
        allResults.AddRange(results);
    }

    // Deduplicate and merge scores
    var merged = allResults
        .GroupBy(r => r.Id)
        .Select(g => new SearchResult
        {
            Id = g.Key,
            Text = g.First().Text,
            Title = g.First().Title,
            Score = g.Max(r => r.Score) // Take best score
        })
        .OrderByDescending(r => r.Score)
        .ToList();

    return merged;
}

4. Контекстне стискання

Проблема: Отримання шматків містить несуттєву інформацію. Відсилання всіх елементів відходи.

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

public async Task<string> CompressContextAsync(
    string query,
    List<SearchResult> retrievedDocs)
{
    var compressed = new List<string>();

    foreach (var doc in retrievedDocs)
    {
        var compressionPrompt = $@"
            Extract only the sentences from this document that are relevant to answering the question.

            Question: {query}

            Document:
            {doc.Text}

            Relevant excerpts (maintain original wording):
        ";

        var relevantExcerpt = await _smallLLM.GenerateAsync(compressionPrompt);

        if (!string.IsNullOrWhiteSpace(relevantExcerpt))
        {
            compressed.Add($"From '{doc.Title}':\n{relevantExcerpt}");
        }
    }

    return string.Join("\n\n", compressed);
}

5. Ітеративне отримання (Multi- Hop RAG)

Проблема: Складні питання потребують інформації з декількох джерел, які мають бути з' єднані.

Приклад: "Яка база даних використовує блог і як реалізується семантичний пошук?"

  • Space 1: з'ясувати, яка база даних використовується (PostgreSQL)
  • Стрибок 2: з'ясувати, як семантичний пошук працює з цією базою даних

Вирішення: Ітеративне отримання та синтез.

public async Task<string> MultiHopRAGAsync(string complexQuery, int maxHops = 3)
{
    var currentQuery = complexQuery;
    var allContext = new List<SearchResult>();

    for (int hop = 0; hop < maxHops; hop++)
    {
        // Retrieve for current query
        var results = await SearchAsync(currentQuery, limit: 5);
        allContext.AddRange(results);

        // Check if we have enough information
        var synthesisPrompt = $@"
            Original question: {complexQuery}

            Context so far:
            {FormatContext(allContext)}

            Can you answer the original question with this context?
            If yes, provide the answer.
            If no, what additional information do you need? (be specific)
        ";

        var synthesis = await _llm.GenerateAsync(synthesisPrompt);

        if (synthesis.Contains("yes", StringComparison.OrdinalIgnoreCase))
        {
            // We have enough information
            return ExtractAnswer(synthesis);
        }

        // Extract what we need for next hop
        currentQuery = ExtractNextQuery(synthesis);
    }

    // Final synthesis with all gathered context
    return await GenerateFinalAnswerAsync(complexQuery, allContext);
}

6. Довга пам' ять контексту з опам' яттю RAG +

Проблема: Як створити системи ШІ, які пам'ятають розмови місяці або роки тому? Традиційні chatbots втрачають контекст після кожного сеансу.

Вирішення: Об' єднати RAG з прогресивним резюмем для створення постійної пам' яті з можливістю пошуку.

Такий підхід використовується у DISE (пряма синтетична еволюція) Удосконалена система, яку я будую, використовує оперативну пам'ять RAG для того, щоб підтримувати спільну розмовну історію нескінченно.

Скрипт прикладу:

User (Today): "Remember George's specs?"
AI: "Yes, you discussed George's prescription requirements in our conversation
     from 5 years ago (2019-03-15). He needed progressive lenses with..."

Як це працює:

flowchart TB
    A[User Message] --> B[Store in RAG Memory]
    B --> C[Extract Key Entities & Topics]
    C --> D[Link to Past Conversations]

    E[Periodic Summarization] --> F[Summarize Old Conversations]
    F --> G[Store Summary with High-Level Tags]
    G --> H[Keep Original for Retrieval]

    I[Future Query: 'George's specs'] --> J[Semantic Search in RAG]
    J --> K[Find: 2019 conversation]
    K --> L[Retrieve Original Context]
    L --> M[LLM generates response with 5-year-old context!]

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

Підхід до впровадження:

public class LongTermConversationalMemory
{
    private readonly IVectorStoreService _vectorStore;
    private readonly IEmbeddingService _embeddingService;

    public async Task StoreConversationAsync(
        string conversationId,
        string userId,
        List<ConversationTurn> turns,
        DateTime timestamp)
    {
        // Extract key entities and topics
        var entities = await ExtractEntitiesAsync(turns);
        var topics = await ExtractTopicsAsync(turns);

        // Create searchable representation
        var conversationText = string.Join("\n", turns.Select(t =>
            $"{t.Speaker}: {t.Message}"));

        // Generate embedding
        var embedding = await _embeddingService.GenerateEmbeddingAsync(
            conversationText);

        // Store in RAG with rich metadata
        await _vectorStore.IndexDocumentAsync(
            id: $"conv_{conversationId}",
            embedding: embedding,
            metadata: new Dictionary<string, object>
            {
                ["user_id"] = userId,
                ["timestamp"] = timestamp.ToString("O"),
                ["entities"] = entities,  // ["George", "specs", "prescription"]
                ["topics"] = topics,      // ["healthcare", "eyewear"]
                ["full_text"] = conversationText,
                ["turn_count"] = turns.Count
            }
        );
    }

    public async Task<List<PastContext>> RetrieveRelevantPastAsync(
        string currentQuery,
        string userId,
        int limit = 5)
    {
        // Embed the current query
        var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(
            currentQuery);

        // Search past conversations
        var results = await _vectorStore.SearchAsync(
            queryEmbedding,
            limit: limit,
            filter: new Filter
            {
                Must =
                {
                    new Condition { Field = "user_id", Match = new Match { Keyword = userId } }
                }
            }
        );

        return results.Select(r => new PastContext
        {
            ConversationId = r.Id,
            Timestamp = DateTime.Parse(r.Metadata["timestamp"].ToString()),
            Entities = (List<string>)r.Metadata["entities"],
            FullText = r.Metadata["full_text"].ToString(),
            Relevance = r.Score
        }).ToList();
    }

    // Periodic summarization to keep memory manageable
    public async Task SummarizeOldConversationsAsync(DateTime olderThan)
    {
        var oldConversations = await _vectorStore.FindByDateRangeAsync(
            endDate: olderThan);

        foreach (var conv in oldConversations)
        {
            // Generate summary using LLM
            var summary = await _llm.GenerateAsync($@"
                Summarize this conversation, preserving key facts and entities:

                {conv.FullText}

                Summary:");

            // Update document with summary while keeping original
            await _vectorStore.UpdateAsync(
                id: conv.Id,
                additionalMetadata: new Dictionary<string, object>
                {
                    ["summary"] = summary,
                    ["summarized_at"] = DateTime.UtcNow.ToString("O")
                }
            );
        }
    }
}

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

  1. Нескінченна тривалість пам' яті - Розмови багато років тому такі ж доступні, як вчорашні
  2. Семантичний пошук - "Гергівські специфіки" знаходять розмову, навіть якщо вона зберігається як "виписані вимоги до окулярах Джорджа"
  3. Зв' язок сутностей - Всі розмови про "Джордж" пов'язані.
  4. Збереження конфіденційності - Може реалізувати відокремлення пам' яті для користувача
  5. Коштовний ефект - Сумари запобігають вибуху під час збереження можливості пошуку.

Приклад з реального світу з ДіСЕ:

ДіСЕ використовує цей підхід, щоб запам'ятати:

  • Інтеграції інструментів з місяців тому (що спрацювало, що не вдалося)
  • Шаблони коду які були успішними у минулих проектах
  • Налаштування користувача організовані в ранніх розмовах
  • Відомості про домен накопичено з часом

Це створює систему комп'ютерного інтелекту, яка справді "навчить" з кожної взаємодії і будує інституційну пам'ять, замість того, щоб починати новий сеанс.

Труднощі, які слід взяти до уваги:

  1. Конфіденційність: Старі розмови слід належно відокремлювати від інших користувачів
  2. Розпад висоти: Не вся стара інформація залишається актуальною
  3. Вартість зберігання: Потрібна стратегія підсумовування для масштабу
  4. Зручність: Підсумовування у відповідності з оригіналами
  5. Точність отримання: Баланс між нещодавнім та історично важливим контекстом

Ця техніка перетворює RAPG від "пошукую мої документи" на "пам'ятайте про все, що ми коли-небудь обговорювали" - ігрового помічника для довгострокових асистентів.

Якщо НЕ використовувати RAG

Відповідь RAG не завжди є відповіддю. Ось, коли її слід уникати:

1. Питання щодо загального знання

  • "Яка столиця Франції?" - LLM вже знає це
  • RAG додає запізнення і вартість без жодної користі

2. Створений запис

  • Поезія, фантастика, розумова травма
  • RAG обмежує творчість, засновуючи в існуючому тексті

3. Потрібні дані у режимі реального часу

  • Ціни на фонди, спортивні оцінки в реальному житті
  • Краща інтеграція API, а не RAG

4. Математичні міркування

  • "Calculate 15% чайових на $83.47"
  • LLM може виконувати арифметичні функції; RAG не допомагає

5. Основи дуже малих знань

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

6. Коли ви контролюєте тренування LLM

  • Якщо ви тренуєте модель з нуля на ваших даних, то штраф може бути кращим.
  • RAG кращий за використання попередньо підготовлених моделей, які ви не контролюєте.

Початок роботи з РСГ. Практичний посібник

Хочешь построить свою собственную систему RAG?

Тиждень 1. Почніть з простого

Мета: Отримати базове отримання працює з без LLM.

// 1. Choose an embedding service (start with API for simplicity)
var openAI = new OpenAIClient(apiKey);

// 2. Embed a few test documents
var docs = new[]
{
    "Docker is a containerization platform",
    "Kubernetes orchestrates containers",
    "Entity Framework is an ORM for .NET"
};

var embeddings = new List<float[]>();
foreach (var doc in docs)
{
    var response = await openAI.GetEmbeddingsAsync(
        new EmbeddingsOptions("text-embedding-3-small", new[] { doc })
    );
    embeddings.Add(response.Value.Data[0].Embedding.ToArray());
}

// 3. Implement basic search (in-memory for now)
var query = "container orchestration";
var queryEmbedding = await GetEmbeddingAsync(query);

var results = embeddings
    .Select((emb, idx) => new
    {
        Text = docs[idx],
        Score = CosineSimilarity(queryEmbedding, emb)
    })
    .OrderByDescending(r => r.Score)
    .ToList();

// 4. Verify search works
foreach (var result in results)
{
    Console.WriteLine($"{result.Score:F3}: {result.Text}");
}
// Expected: Kubernetes scores highest

Тиждень 2: Додати базу даних векторів

Мета: Масштабувати до справжніх збірок документів.

Наступні кроки для реалізації:

  1. Запустити QDrant в Docker
  2. Індексувати ваші документи
  3. Впровадження кінцевої точки пошуку

Я детально опишу це у наступній статті про векторні бази даних.

Тиждень 3: Додати створення LLM

Мета: Заповни трубопровод CRAG.

// 1. Retrieve context
var context = await SearchAsync(query, limit: 3);

// 2. Build prompt
var prompt = $@"
    Answer the question using this context:

    {FormatContext(context)}

    Question: {query}

    Answer:";

// 3. Generate (start with API)
var response = await openAI.GetChatCompletionsAsync(new ChatCompletionsOptions
{
    Messages =
    {
        new ChatMessage(ChatRole.System, "You are a helpful assistant."),
        new ChatMessage(ChatRole.User, prompt)
    },
    Temperature = 0.7f,
    MaxTokens = 500
});

return response.Value.Choices[0].Message.Content;

Тиждень 4: Оптимізувати та польську

  • Додати кешування (збереження за вартістю API)
  • Покращення стратегії розрізання шматків
  • Додати фільтрування метаданих
  • Впровадження гібридного пошуку
  • Додати видобування цитат

Залишатися локальним (необов' язковим)

Після роботи основ, міграція на локальні висновки (Я повідомлю про це у наступних статтях):

  • Локальний варіант LLM з прискоренням GPU
  • Дружній до ЦП вбудовування з ONNX Runtime

Висновки

RAG (Retrival- Augmented Generation) - потужний метод створення LLM для точнішого, найновішого та надійного створення їх відповідей у реальних документах. Замість того, щоб покладатися лише на тренувальні дані моделі, система RAG:

  1. Отримати відповідна інформація з бази знань з використанням семантичного пошуку
  2. Аугмент запрошення LLM з отриманим контекстом
  3. Створити відповіді, які базуються на реальних джерелах з цитатами

Переваги клавіш від RAG:

  • Завжди застарілий (просто оновлюйте вашу базу знань)
  • Заснований у джерелах (відновлює галюцинації)
  • Пояснення (можна цитувати джерела)
  • Ефективний (не дороге повторення) Noun, a currency
  • Збереження конфіденційності (може виконуватися повністю локально)
  • Специфічні для домену (використовуйте власні документи)

При використанні RAG:

  • Застосовані до знань завдання
  • Часті оновлення інформації
  • Конфіденційні/патентовані дані
  • Потреба у цитатах і перевірці
  • Вартість і простота мають значення

Якщо слід уникати RAG:

  • Бази Gge

Поле швидко розвивається з такими додатковими технологіями, як HyDE, мультиquery recoding і contextulual- стискання, але основна концепція залишається простою: надати LLMs доступ до потрібної інформації у потрібний час.

Почніть з простого, вимірюйте результати і перезаряджайте. RAG - один з найпрактичніших способів побудови на сьогодні надійних комп' ютерних систем.

Висновки: Від теорії до виробництва

Тепер ви завершили серію з трьох частин RAG:

Частина 1. Походження і основи

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

Частина 2: архітектура і внутрішні властивості

  • Повнофункціональний канал RAG: індексування, отримання, створення
  • Внутрішні елементи LLM: поля, кеш KV, вікна контексту
  • Подробиці технічної реалізації з кодом C#

Частина 3: ПСГ у практиці (ця стаття)

  • Реалізація реального світу (Пошти, семантичний пошук, Помічник запису)
  • Загальні виклики і рішення (розбір, вбудовування якість, галюцинація)
  • Додаткові прийоми (HyDE, Self- Quing, Multiquery, Contextulual Striver, Довгострокова пам' ять)
  • Якщо не використовувати RAG
  • Як отримати початкове керівництво

Тепер ви маєте повну картину: Від розуміння походження РАГ до створення виробничих систем з додатковими оптимізаціями.

Що далі?

Тепер, коли ви зрозумієте RAG від теорії до практики, наступні статті покажуть вам, як будувати цілі, готові до виробництва системи RAG у C#:

Иду скоро.

  • Symantic Search- Friendly Symantic Seartic Search Search Search - Побудова семантичних пошуків з вбудовуваннями ONNX, які виконуються на будь-якій VPS без вимог GPU
  • Бази даних векторів, що самовідтворюються - Повний довідник з налаштування Qdrant з Docker, зокрема індексування, пошук і оптимізація
  • Побудова помічника запису RAG - Повна серія про створення досвідченого асистента з письма, який використовує ваш існуючий контент як базу знань

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

Будьте налаштовані для керування ручними інструментами, які перетворять ці знання RAG на робочі системи!

Ресурси

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

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

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

Серія RAG:

Счастливого здания!

logo

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