In Deel 1 en Deel 2 Je begrijpt wat RAG is, waarom het belangrijk is, en hoe het werkt onder de kap. Nu is het tijd om die kennis in de praktijk te brengen. Dit artikel laat je zien hoe je echte RAG-systemen kunt bouwen met werkende C#-code, gemeenschappelijke uitdagingen oplost en geavanceerde technieken uit recent onderzoek gebruikt.
Serienavigatie: Dit is deel 3 van de RAG-reeks:
Als je de delen 1 en 2 nog niet gelezen hebt, raad ik je aan om daar te beginnen om te begrijpen:
Dit artikel gaat ervan uit dat je die basisprincipes begrijpt en focust op implementatie, optimalisatie en real-world patronen.
Ik heb verschillende RAG-aangedreven functies gebouwd op deze blog. Laat me u concrete voorbeelden laten zien.
Elke blog post kan tonen "Verwante berichten" met behulp van semantische gelijkenis.
Hoe het werkt:
Waarom het beter is dan tags:
Code knipsel:
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();
}
Het zoekvak op deze blog maakt gebruik van RAG-stijl semantische zoekopdracht (hoewel zonder de generatie deel - het is gewoon ophalen).
Gebruikerservaring:
Uitvoering: Ik zal dit in een volgend artikel over vector databases verwerken.
Ik bouw een compleet RAG-systeem om me te helpen nieuwe blogberichten te schrijven.
Use case: Wanneer ik begin te schrijven over "toevoegen van authenticatie aan ASP.NET Core," het systeem:
Volledige RAG-pijpleiding:
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;
}
Dit is RAG in actie - retrieval (semantic search) + augmentation (add context) + generation (LLM suggestions).
Bouw productie RAG systemen is niet triviaal. Hier zijn uitdagingen die ik heb ondervonden en hoe ze op te lossen.
Probleem: Hoe deel je documenten? Te klein = verlies van context. Te groot = irrelevante informatie.
Oplossing: Hybride chunking op basis van documentstructuur.
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;
}
}
Beste praktijken:
Probleem: Generieke inbeddingsmodellen mogen geen domeinspecifieke semantiek vastleggen.
Oplossingen:
Optie 1: Fine-tune inbeddingen (geavanceerd)
# 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)
Optie 2: Hybride inbeddingen (meerdere modellen combineren)
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);
}
Optie 3: Metadatafiltering toevoegen
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" } }
}
}
);
Probleem: LLM's hebben tokenlimieten. Hoe past u query + context + prompt in het venster?
Oplossing: Dynamische contextselectie en -samenvatting.
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);
}
Probleem: Zelfs met context negeren LLM's het soms en hallucineren.
Oplossingen:
1. Prompt engineering:
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. Validatie na de generatie:
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. Iteratieve verfijning:
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.";
}
Probleem: Als je nieuwe documenten toevoegt, moet de vectordatabase actueel blijven.
Oplossing: Automatische indexeringspijplijn.
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
);
}
}
}
}
}
Laten we de geavanceerde RAG-technieken uit recent onderzoek onderzoeken.
Probleem: Gebruiker queries zijn vaak kort en slecht gevormd. Document brokken zijn gedetailleerd en goed geschreven. Deze mismatch doet het ophalen pijn.
Oplossing: Genereer een hypothetisch ideaal document dat de query zou beantwoorden, insluiten dat, dan zoeken.
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);
}
Waarom het werkt: Het hypothetische antwoord maakt gebruik van dezelfde taal en structuur als de feitelijke documenten, waardoor het opvragen wordt verbeterd.
Probleem: Gebruikersqueries mengen vaak semantisch zoeken met metadatafilters.
Voorbeeld: "Recente berichten over Docker" = semantisch("Docker") + filter(datum > 2024-01-01)
Oplossing: Gebruik LLM om de query te verwerken in semantische + metadata filters.
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)
);
Probleem: Een enkele query kan relevante documenten missen als gevolg van frasering.
Oplossing: Genereer meerdere variaties van de query, zoek met alles, combineer resultaten.
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;
}
Probleem: De opgehaalde stukken bevatten irrelevante informatie.
Oplossing: Gebruik een kleinere LLM om opgehaalde context te comprimeren naar alleen relevante delen.
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);
}
Probleem: Complexe vragen vereisen informatie uit meerdere bronnen die moeten worden aangesloten.
Voorbeeld: "Welke database gebruikt de blog en hoe wordt semantisch zoeken geïmplementeerd?"
Oplossing: Iteratieve ophaling en synthese.
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);
}
Probleem: Hoe bouw je AI-systemen die gesprekken van maanden of jaren geleden herinneren? Traditionele chatbots verliezen de context na elke sessie.
Oplossing: Combineer RAG met progressieve opsomming om persistent, doorzoekbaar geheugen te creëren.
Dit is de benadering die wordt gebruikt in DiSE (Directed Synthetic Evolution) - een geavanceerd systeem dat gebruik maakt van RAG-gebaseerde contextgeheugen om de gedeelde conversatiegeschiedenis voor onbepaalde tijd te behouden.
Voorbeeldscenario:
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..."
Hoe het werkt:
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
Uitvoering:
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")
}
);
}
}
}
Waarom dit krachtig is:
Real-world voorbeeld van DiSE:
DiSE gebruikt deze benadering om te onthouden:
Dit creëert een AI-systeem dat echt "leren" van elke interactie en opbouwt institutionele geheugen, in plaats van het starten van verse elke sessie.
Uitdagingen om na te denken:
Deze techniek transformeert RAG van "zoek mijn documenten" in "herinner alles wat we ooit hebben besproken" - een game-changer voor langdurige AI assistenten.
RAG is niet altijd het antwoord.
1. Algemene kennisvragen
2. Creatief schrijven
3. Real-time gegevensbehoeften
4. Wiskundige redenering
5. Zeer kleine kennisbases
6. Wanneer u de training van de LLM controleert
Wilt u uw eigen RAG-systeem bouwen? Hier is een stap-voor-stap benadering.
Doelstelling: Krijg basis retrieval werken met geen 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
Doelstelling: Schaal naar echte documentverzamelingen.
Volgende te implementeren stappen:
Ik zal dit in detail behandelen in een volgend artikel over vector databases.
Doelstelling: Voltooi de RAG-pijpleiding.
// 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;
Zodra de basis werkt, migreren naar lokale conclusie (ik zal dit behandelen in de komende artikelen):
RAG (Retrieval-Augmented Generation) is een krachtige techniek om LLM's nauwkeuriger, up-to-date en betrouwbaarder te maken door hun antwoorden aan de hand van feitelijke documenten. In plaats van te vertrouwen op de trainingsgegevens van het model alleen, RAG-systemen:
Belangrijkste voordelen van RAG:
Wanneer wordt RAG gebruikt:
Wanneer RAG te vermijden:
Het veld evolueert snel met geavanceerde technieken zoals HyDE, multiquery retrieval, en contextuele compressie, maar het kernconcept blijft eenvoudig: geef LLM's toegang tot de juiste informatie op het juiste moment.
Start eenvoudig, meet resultaten en itereer. RAG is een van de meest praktische manieren om betrouwbare AI-systemen te bouwen vandaag.
Je hebt nu de drie-delige RAG serie voltooid:
Deel 1: Oorsprongen en Fundamentelen
Deel 2: Architectuur en interne aangelegenheden
Deel 3: RAG in de praktijk (dit artikel)
Je hebt nu het volledige plaatje: Van het begrijpen van de oorsprong van RAG tot het bouwen van productiesystemen met geavanceerde optimalisaties.
Nu je RAG van theorie tot praktijk begrijpt, zullen de komende artikelen je laten zien hoe je complete RAG-systemen kunt bouwen in C#:
Binnenkort:
Deze artikelen brengen u van theorie naar praktijk, met volledige werkcode, implementatiestrategieën en real-world optimalisaties gebaseerd op het uitvoeren van deze systemen in productie op deze blog.
Blijf kijken voor hands-on implementatiehandleidingen die deze RAG kennis omzetten in werkende systemen!
Fundamentele papers:
Instrumenten en kaders:
Meer lezen:
Deze RAG-serie:
Gelukkig gebouw!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.