# RAG käytännössä: Real-World -sovellusten rakentaminen

> Sisään [Tämän aluetukisuuntaviivojen 1 osa](/blog/rag-primer)Nyt on aika toteuttaa tämä tieto käytännössä. Tässä artikkelissa näytetään, miten rakennetaan todellisia RAG-järjestelmiä, joissa käytetään C#-koodia, ratkaistaan yhteisiä haasteita ja hyödynnetään viimeaikaisen tutkimuksen kehittyneitä tekniikoita.

<datetime class="hidden">2025-11-22T10:00</datetime>

<!-- category -- AI, RAG, Machine Learning, Semantic Search, LLM, AI-Article -->
# Johdanto

Jos et ole lukenut [Osa 1: RAG Selitetty](/blog/rag-primer)Suosittelen, että aloitat siellä ymmärtääksesi peruskäsitteitä:

- Mitä RAG on ja miksi sillä on merkitystä
- Miten upotukset ja vektorihaku toimivat
- LLM-sisätilat (muistit, KV- välimuisti, kontekstiikkunat)
- RAG vs hienosäätö ja muut lähestymistavat

Tässä artikkelissa oletetaan, että ymmärrät nämä perusasiat ja keskityt **toteutus, optimointi ja reaaliaikaiset kuviot**.

[TOC]

# Real-World Region -sovellukset tähän blogiin

Olen rakentanut blogiin useita RAG-käyttöisiä ominaisuuksia. Näytän konkreettisia esimerkkejä.

## 1. Related Posts Recommendation

Jokainen blogikirjoitus voi näyttää "Related Posts" käyttäen semanttista samankaltaisuutta.

**Miten se toimii:**

1. Jokainen blogikirjoitus sulautetaan, kun se julkaistaan
2. Kun katselemme postia, haemme sen upotuksen Qdrantista
3. Etsi 5 samankaltaisinta postausta
4. Näytä "Related Posts"

**Miksi se on parempi kuin tagit:**

- Tunnisteet vaativat manuaalista luokittelua
- Semanttinen haku löytää käsitteellisesti toisiinsa liittyviä viestejä ilman vastaavia tageja
- "Docker Compose" ja "Container orkestration" -virat liittyvät semanttisesti

**Koodin näppäily:**

```csharp
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. Semanttinen blogihaku

Tämän blogin hakulaatikossa käytetään RAG-tyylistä semanttista hakua (vaikkakin ilman sukupolven osaa - se on vain hakua).

**Käyttäjäkokemus:**

- Hae "tietokannan perustamista" → löydät postitse postitse, Entity Framework, migrations
- Etsi "deployment" → löytää viestit noin Docker, hosting, CI/CD
- Täsmällisiä hakusanoja ei tarvita

**Täytäntöönpano:** Käsittelen asiaa tulevassa vektoritietokantojen artikkelissa.

## 3. "Lakimies GPT:n" kirjoittava avustaja

Rakennan täydellistä RAG-järjestelmää, joka auttaa minua kirjoittamaan uusia blogikirjoituksia.

**Käyttötapaus:** Kun alan kirjoittaa ASP.NET Coreen lisävarmennuksesta, järjestelmä:

1. Upottaa nykyisen luonnokseni
2. Etsii aiempia viestejä liittyen tietoturvaan liittyvään sisältöön, ASP.NETiin, tietoturvaan
3. Ehdottaa merkityksellisiä koodin näppäimiä, joita olen käyttänyt aiemmin
4. Luodaan automaattisesti sisäisiä yhteyksiä asiaan liittyviin virkoihin
5. Pysyy johdonmukaisena kirjoitustyylini kanssa

**Täysi RAG-putki:**

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

Tämä on RAG in Action -haku (semanttinen haku) + advancation (lisää asiayhteys) + generation (LLM:n ehdotukset).

# Yhteiset aluekehityshaasteet ja -ratkaisut

Rakennustuotannon RAG-järjestelmät eivät ole vähäpätöisiä. Tässä on haasteita, joihin olen törmännyt ja miten ne ratkaistaan.

## Haaste 1: Huutelustrategia

**Ongelma:** Miten asiakirjat jaetaan? Liian pieni = asiayhteyden menetys. Liian suuri = epäolennainen tieto.

**Ratkaisu:** Hybridipoiminta perustuu asiakirjarakenteeseen.

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

**Parhaita käytäntöjä:**

- Kunnioita asiakirjarakennetta (otsikoita, kappaleita)
- Lisää päällekkäin kappaleiden välillä (50-100 sanaa)
- Säilöntäkoodin lohkot ehjänä
- Sisällytä osion otsikot kuhunkin osaan kontekstia varten

## Haaste 2: Sisällyttämisen laatu

**Ongelma:** Yleiset upotettavat mallit eivät välttämättä vangitse domain-kohtaisia semantiikkaa.

**Ratkaisut:**

**Vaihtoehto 1: Hienosäädöt** (edistynyt)

```python
# 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)
```

**Vaihtoehto 2: Hybridikytkennät** (yhdistetään useita malleja)

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

**Vaihtoehto 3: Lisää metatietojen suodatus**

```csharp
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" } }
        }
    }
);
```

## Haaste 3: Konteksti-ikkunan hallinta

**Ongelma:** LLM:illä on nimelliset raja-arvot. Miten sovit kyselyyn + kontekstiin + pikaiseen ikkunaan?

**Ratkaisu:** Dynaaminen kontekstin valinta ja yhteenveto.

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

## Challenge 4: Hallusinaatiot pinta-alasta huolimatta

**Ongelma:** Jopa asiayhteydestä huolimatta LLM:t joskus sivuuttavat sen ja hallusinoivat.

**Ratkaisut:**

**1. Nopea insinööritaito:**

```csharp
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. Sukupolven jälkeinen validointi:**

```csharp
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. Iteratiivinen hiominen:**

```csharp
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.";
}
```

## Haaste 5: Hakemiston pitäminen ajan tasalla

**Ongelma:** Kun lisäät uusia asiakirjoja, vektoritietokannan on pysyttävä ajankohtaisena.

**Ratkaisu:** Automaattinen indeksointiputki.

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

# Kehittyneet RAG-tekniikat

Tutkitaan tuoreen tutkimuksen huippuluokan RAG-tekniikoita.

## 1. Hypoteettinen asiakirja Upotukset (HyDE)

**Ongelma:** Käyttäjäkyselyt ovat usein lyhyitä ja huonosti laadittuja. Asiakirjan kappaleet ovat yksityiskohtaisia ja hyvin kirjoitettuja.

**Ratkaisu:** Luo hypoteettinen ihanteellinen asiakirja, joka vastaisi kyselyyn, upottaisi sen ja etsisi sitten.

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

**Miksi se toimii:** Hypoteettisessa vastauksessa käytetään samaa kieltä ja rakennetta kuin varsinaisissa asiakirjoissa, mikä parantaa hakua.

## 2. Itsekytkentä

**Ongelma:** Käyttäjäkyselyt usein sekoittavat semanttisen haun metatietosuodattimiin.

Esimerkki: "Viimeisimmät viestit Dockerista" = semanttinen("Docker") + suodatin(päivämäärä > 2024-01-01)

**Ratkaisu:** Käytä LLM:ää yhdistääksesi kyselyn semanttisiin + metatietosuodattimiin.

```csharp
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. Monitasoinen aluetukialue

**Ongelma:** Yksikin kysely voi jäädä pois asian kannalta merkityksellisistä asiakirjoista sanaston vuoksi.

**Ratkaisu:** Luo kyselyn useita variaatioita, etsi kaikkien kanssa ja yhdistä tulokset.

```csharp
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. Kontekstillinen paineistus

**Ongelma:** Palautetut kappaleet sisältävät epäolennaista tietoa.

**Ratkaisu:** Käytä pienempää LLM:ää pakataksesi noudetun kontekstin vain tärkeisiin osiin.

```csharp
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. Iteratiivinen haku (Multi-Hop RAG)

**Ongelma:** Monimutkaiset kysymykset vaativat tietoa useasta lähteestä, jotka on kytkettävä toisiinsa.

Esimerkki: "Mitä tietokantaa blogissa käytetään ja miten semanttinen haku toteutetaan?"

- Hop 1: Etsi mitä tietokantaa käytetään (PostgreSQL)
- Hop 2: Etsi, miten semanttinen haku toimii kyseisen tietokannan kanssa

**Ratkaisu:** Iteratiivinen haku ja synteesi.

```csharp
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. Pitkän aikavälin taustamuisti RAG:llä + yhteenveto

**Ongelma:** Miten rakentaa tekoälyjärjestelmiä, jotka muistavat kuukausia tai vuosia sitten käydyt keskustelut? Perinteiset chatbotit menettävät asiayhteyden jokaisen istunnon jälkeen.

**Ratkaisu:** Yhdistä RAG progressiiviseen tiivistelmään, jotta voit luoda itsepintaisen, hakukelpoisen muistin.

Tämä on lähestymistapa, jota käytetään **DiSE (Directed Synthetic Evolution)** - kehittyneen järjestelmän, jota rakennan ja joka käyttää RAG-pohjaista kontekstimuistia ylläpitääkseen yhteistä keskusteluhistoriaa loputtomiin.

**Esimerkki:**

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

**Miten se toimii:**

```mermaid
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
```

**Täytäntöönpanoon perustuva lähestymistapa:**

```csharp
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")
                }
            );
        }
    }
}
```

**Miksi tämä on voimakas:**

1. **Ääretön muistiväli** - Vuosien takaiset keskustelut ovat yhtä helposti lähestyttäviä kuin eilinen
2. **Semanttinen etsintä** - "Georgen spektaakkelit" pitävät keskustelua, vaikka se olisi tallennettu "Georgen silmävaatteiden reseptivaatimuksiin"
3. **Yhteyttä yhdistävä yhteisö** - Kaikki "Georgen" mainitsevat keskustelut liittyvät toisiinsa
4. **Yksityisyyden säilyttäminen** - Pystyy toteuttamaan käyttäjäkohtaisen muistin eristämisen
5. **Kustannustehokas** - Yhteenveto estää varastoinnin räjähdyksen säilyttäen samalla etsittävyyden

**Oikean maailman esimerkki DiSE:stä:**

DiSE käyttää tätä lähestymistapaa muistaakseen:

- **Työkalutyöt** kuukausien takaa (mikä toimi, mikä epäonnistui)
- **Koodikuviot** jotka onnistuivat aikaisemmissa projekteissa
- **Käyttäjän mieltymykset** aikaisemmissa keskusteluissa
- **Verkkoalueen tuntemus** kertynyt ajan myötä

Tämä luo tekoälyjärjestelmän, joka aidosti "oppii" jokaisesta vuorovaikutuksesta ja rakentaa institutionaalista muistia sen sijaan, että aloittaisi jokaisen istunnon uudelleen.

**Harkittavia haasteita:**

1. **Yksityisyys**: Vanhat keskustelut on eristettävä käyttäjäkohtaisesti
2. **Merkityksellinen hajoaminen**: Kaikki vanhat tiedot eivät pysy olennaisina
3. **Varastointikustannukset**: Mittakaavaan tarvitaan yhteenvetostrategia
4. **Johdonmukaisuus**: Yhteenvedot ovat alkuperäisten versioiden mukaisia
5. **Hakemisen tarkkuus**: Tasapainottaminen viimeaikaisen ja historiallisesti merkittävän kontekstin välillä

Tämä tekniikka muuttaa RAG:n "tutkia asiakirjojani" "muistaa kaiken, mitä olemme koskaan keskustelleet" - pitkän aikavälin tekoälyavustajien pelinvaihtajaksi.

# Kun RAGia ei saa käyttää

RAG ei aina ole vastaus.

**1. Yleistietoa koskevat kysymykset**

- "Mikä on Ranskan pääkaupunki?" - LLM tietää jo tämän
- Aluetukisuuntaviivojen mukaan latenssi ja kustannukset jäävät hyödyttömiksi

**2. Luova kirjoitus**

- Runoutta, fiktiota, aivoriihiä
- Aluekehitys rajoittaa luovuutta asettamalla nykyisen tekstin pohjalle

**3. Reaaliaikaiset tietotarpeet**

- Osakehinnat, live-urheilutulokset
- API-integraatiot palvelevat paremmin, ei RAG:tä

**4. Matemaattinen päättely**

- "Lahjoita 15 prosentin tippi 83,47 dollarilla."
- LLM pystyy laskemaan, RAG ei auta

**5. Hyvin pienet tietopohjat**

- Jos tietopohjasi sopii konteksti-ikkunaan, ota kaikki mukaan
- RAG:n yleiskustannukset eivät kannata viittä asiakirjaa

**6. Kun hallitset LLM:n harjoittelua**

- Jos harjoittelet mallia tyhjästä datassasi, hienosäätö voisi olla parempi vaihtoehto
- RAG on paras, kun käytät esikoulutettuja malleja, joita et hallitse

# Aloittaminen RAG:lla: Käytännöllinen opas

Haluatko rakentaa oman RAG-järjestelmän? Tässä on askel askeleelta.

## Viikko 1: Aloita yksinkertainen

**Tavoite:** Hanki perusrekrytointi ilman LLM:ää.

```csharp
// 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
```

## Viikko 2: Lisää Vectorin tietokanta

**Tavoite:** Skaalaa oikeat dokumenttikokoelmat.

Seuraavat täytäntöönpanovaiheet:

1. Suorita Qdrant Dockerissa
2. Hakemisto asiakirjoistasi
3. Täytä hakutulos

Käsittelen asiaa yksityiskohtaisesti tulevassa vektoritietokantoja käsittelevässä artikkelissa.

## Viikko 3: Lisää LLM-sukupolvi

**Tavoite:** Suorita RAG-putki.

```csharp
// 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;
```

## Viikko 4: Optimoi ja puola

- Lisää välimuisti (säästä API-kuluissa)
- Paranna paloittelustrategiaa
- Lisää metatietojen suodatus
- Toteuta hybridihaku
- Lisää viittausten uutto

## Going Local (valinnainen)

Kun perusasiat toimivat, siirry paikalliseen johtopäätökseen (käsitän tämän tulevissa artikkeleissa):

- Paikallinen LLM-vaikutus GPU-kiihtyvyyteen
- CPU-ystävällisiä upotuksia ONNX-ajoon

# Päätelmät

RAG (Retrieval-Augmented Generation) on tehokas tekniikka, jolla LLM:t saadaan tarkemmiksi, ajanmukaisemmiksi ja luotettavammiksi asettamalla vastaukset oikeisiin asiakirjoihin. Sen sijaan, että ne luottaisivat pelkästään mallin koulutustietoihin, RAG-järjestelmät:

1. **Hae** semanttista hakua käyttävän tietopohjan merkitykselliset tiedot
2. **Augment** LLM:n ripeys tämän nousseen kontekstin kanssa
3. **Luo** vastaukset, jotka perustuvat todellisiin lähteisiin viitaten

**Aluetukisuuntaviivojen keskeiset edut:**

- Aina ajan tasalla (päivitä vain tietopohjasi)
- Maaperän lähteet (vähentää hallusinaatioita)
- Selittävä (voi mainita lähteet)
- Kustannustehokas (ei kallista uudelleenkoulutusta)
- Yksityisyyden säilyttäminen (voi tapahtua kokonaan paikallisesti)
- Verkkoaluekohtainen (käytä omia asiakirjojasi)

**Milloin RAG:tä käytetään:**

- Osaamisintensiiviset tehtävät
- Päivitä tietoa usein
- Yksityis-/toimivaltaiset tiedot
- Lainausten tarve ja auditointikelpoisuus
- Kustannukset ja yksinkertaisuus

**Milloin vältät RAG:n:**

- Yleistietoa koskevat kysymykset
- Luova kirjoitus
- Reaaliaikaiset tiedot (käytä sen sijaan API-rajapintoja)
- Hyvin pienet tietopohjat

Kenttä kehittyy nopeasti kehittyneillä tekniikoilla, kuten HyDE:llä, moniquery-haulla ja kontekstikomplikaatiolla, mutta ydinkonsepti on edelleen yksinkertainen: LLM:n on saatava oikea tieto oikeaan aikaan.

Aloittaa yksinkertainen, mitata tuloksia ja iteroida. RAG on yksi käytännöllisimmistä tavoista rakentaa luotettavia tekoälyjärjestelmiä nykyään.

# Mitä seuraavaksi?

Nyt kun ymmärrätte molemmat perusasiat ([Osa 1](/blog/rag-primer)) ja käytännön sovelluksia RAG, tulevissa artikkeleissa Näytän, miten rakentaa valmiita, tuotantovalmiita RAG-järjestelmiä C#:ksi:

**Tulossa pian:**

- **CPU-ystävällinen semanttinen haku** - Rakentaa semanttista hakua ONNX-kytkimillä, jotka toimivat millä tahansa VPS:llä ilman GPU:n vaatimuksia
- **Omatoimiset vektoritietokannat** - Täydellinen asetusopas Qdrantille Dockerin kanssa, mukaan lukien indeksointi-, haku- ja optimointistrategiat
- **Laadinta-avustajan rakentaminen** - Täydellinen sarja tekoälyä hyödyntävän kirjoitusavustajan luomisesta, joka käyttää olemassa olevaa sisältöäsi tietopohjana

Nämä artikkelit vievät sinut teoriasta käytäntöön, jossa on täydelliset työkoodit, käyttöönottostrategiat ja reaalimaailman optimoinnit, jotka perustuvat näiden järjestelmien pyörittämiseen tuotannossa tässä blogissa.

Pysy kuulolla, kun näet käytännön toteutusoppaita, jotka muuttavat tämän RAG-tiedon työjärjestelmiksi!

## Resurssit

**Peruskirjat:**

- [Hakijasukupolvea osaamisintensiivisille NLP-tehtäville](https://arxiv.org/abs/2005.11401) - Alkuperäinen RAG-lehti
- [Ankkuripassin hakeminen avoimelle verkkokyselyvastaukselle](https://arxiv.org/abs/2004.04906) - DPR (hakusäätiö)
- [Huomio on kaikki, mitä tarvitset](https://arxiv.org/abs/1706.03762) - Muuntajat (kehittävät perustukset)

**Työkalut ja puitteet:**

- [Qdrant](https://qdrant.tech/) - Vektoritietokantaa käytän
- [ONNX-ajoaika](https://onnxruntime.ai/) - Paikallisille upotuksille
- [LLAMASharp](https://github.com/SciSharp/LLamaSharp) - Paikalliselle LLM-päätelmälle
- [Tuomionvaihtajat](https://www.sbert.net/) - Upotettavat mallit

**Lue lisää:**

- [Antropistinen: Kontekstihaku](https://www.anthropic.com/index/contextual-retrieval) - Kehittyneet RAG-tekniikat
- [Miten hermokonekäännös toimii](/blog/how-neural-machine-translation-works) - Ymmärtää tekoälymallit upotusten takana
- [Aluetukisuuntaviivojen 1 osa](/blog/rag-primer) - Peruslähtöinen pohjapiirtäjä

Iloinen rakennus!