In Parte 1 y Parte 2 de esta serie, hemos cubierto los orígenes, fundamentos y arquitectura técnica de RAG. Usted entiende lo que es RAG, por qué importa, y cómo funciona bajo el capó. Ahora es el momento de poner en práctica ese conocimiento. Este artículo le muestra cómo construir sistemas reales RAG con código C# de trabajo, resolver desafíos comunes, y utilizar técnicas avanzadas de investigación reciente.
Navegación de la serie: Esta es la parte 3 de la serie RAG:
Si no has leído las partes 1 y 2, te recomiendo que empieces por ahí para entender:
Este artículo asume que usted entiende esos fundamentos y se centra en implementación, optimización y patrones del mundo real.
He construido varias características impulsadas por RAG en este blog. Permíteme mostrarte ejemplos concretos.
Cada entrada de blog puede mostrar "Related Posts" usando similitud semántica.
Cómo funciona:
Por qué es mejor que las etiquetas:
Fragmento de código:
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();
}
El cuadro de búsqueda en este blog utiliza la búsqueda semántica estilo RAG (aunque sin la parte de generación - es sólo la recuperación).
Experiencia del usuario:
Ejecución: Cubriré la construcción de esto en un próximo artículo sobre bases de datos vectoriales.
Estoy construyendo un sistema completo de RAG para ayudarme a escribir nuevos posts de blog.
Caso de uso: Cuando empiezo a escribir sobre "añadir autenticación a ASP.NET Core", el sistema:
Gasoducto RAG completo:
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;
}
Esto es RAG en acción - recuperación (búsqueda semántica) + aumento (añadiendo contexto) + generación (sugerencias LLM).
Los sistemas de producción de edificios RAG no son triviales. Estos son los desafíos que he encontrado y cómo resolverlos.
Problema: ¿Cómo dividir documentos? Demasiado pequeño = pérdida de contexto. Demasiado grande = información irrelevante.
Solución: Trozos híbridos basados en la estructura del documento.
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;
}
}
Prácticas óptimas:
Problema: Los modelos de incrustación genéricos pueden no capturar la semántica específica del dominio.
Soluciones:
Opción 1: Inserciones de ajuste fino (Avanzado)
# 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)
Opción 2: Incrustaciones híbridas (combina varios modelos)
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);
}
Opción 3: Añadir filtrado de metadatos
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" } }
}
}
);
Problema: Los LLMs tienen límites de token. ¿Cómo encaja la consulta + contexto + prompt en la ventana?
Solución: Selección y resumen dinámicos del contexto.
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);
}
Problema: Incluso con el contexto, los LLM a veces lo ignoran y alucinan.
Soluciones:
1. Ingeniería inmediata:
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. Validación posterior a la generación:
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. Refinamiento iterativo:
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.";
}
Problema: Al agregar nuevos documentos, la base de datos de vectores necesita mantenerse actualizada.
Solución: Gasoducto de indexación automatizado.
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
);
}
}
}
}
}
Exploremos técnicas RAG de vanguardia de investigaciones recientes.
Problema: Las consultas de los usuarios a menudo son cortas y están mal formadas. Los trozos de documento están detallados y bien escritos.
Solución: Generar un documento ideal hipotético que responda a la consulta, incrustar que, a continuación, buscar.
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);
}
Por qué funciona: La respuesta hipotética utiliza lenguaje y estructura similares a los documentos reales, mejorando la recuperación.
Problema: Las consultas de los usuarios a menudo mezclan búsqueda semántica con filtros de metadatos.
Ejemplo: "Publicaciones recientes sobre Docker" = semántica("Docker") + filtro(fecha > 2024-01-01)
Solución: Utilice LLM para analizar la consulta en filtros semánticos + metadatos.
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)
);
Problema: Una sola consulta podría perder los documentos pertinentes debido a la fraseación.
Solución: Generar múltiples variaciones de la consulta, buscar con todos, combinar los resultados.
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;
}
Problema: Los trozos recuperados contienen información irrelevante. Enviándolo a todos los residuos tokens.
Solución: Utilice un LLM más pequeño para comprimir el contexto recuperado sólo a partes relevantes.
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);
}
Problema: Las preguntas complejas requieren información de múltiples fuentes que necesitan ser conectadas.
Ejemplo: "¿Qué base de datos utiliza el blog y cómo se implementa la búsqueda semántica?"
Solución: Recuperación iterativa y síntesis.
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);
}
Problema: ¿Cómo se construyen sistemas de IA que recuerdan conversaciones de meses o años atrás? Los chatbots tradicionales pierden contexto después de cada sesión.
Solución: Combine RAG con resumen progresivo para crear memoria persistente y buscable.
Este es el enfoque utilizado en DiSE (Evolución sintética directa) - un sistema avanzado que estoy construyendo que utiliza memoria de contexto basada en RAG para mantener una historia de conversación compartida indefinidamente.
Escenario de ejemplo:
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..."
Cómo funciona:
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
Enfoque de aplicación:
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")
}
);
}
}
}
Por qué esto es poderoso:
Ejemplo del mundo real de DiSE:
DiSE utiliza este enfoque para recordar:
Esto crea un sistema de IA que realmente "aprende" de cada interacción y construye memoria institucional, en lugar de comenzar de nuevo cada sesión.
Retos a tener en cuenta:
Esta técnica transforma a RAG de "buscar mis documentos" en "recordar todo lo que hemos discutido" - un cambio de juego para los asistentes de IA a largo plazo.
RAG no siempre es la respuesta. Aquí está cuando para evitarlo:
1. Cuestiones relativas a los conocimientos generales
2. Escritura creativa
3. Necesidades de datos en tiempo real
4. Razonamiento matemático
5. Bases de conocimientos muy pequeñas
6. Cuando usted controla el entrenamiento de la LLM
¿Quieres construir tu propio sistema RAG? Aquí tienes un enfoque paso a paso.
Objetivo: Obtener recuperación básica trabajando sin 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
Objetivo: Escalar a colecciones de documentos reales.
Próximas medidas para aplicar:
Cubriré esto en detalle en un próximo artículo sobre bases de datos vectoriales.
Objetivo: Completa el gasoducto RAG.
// 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;
Una vez que lo básico funcione, migra a la inferencia local (lo cubriré en los próximos artículos):
RAG (Retrieval-Augmentated Generation) es una poderosa técnica para hacer que los LLMs sean más precisos, actualizados y confiables basando sus respuestas en documentos reales. En lugar de confiar solo en los datos de entrenamiento del modelo, los sistemas RAG:
Principales ventajas de los GCR:
Cuándo usar RAG:
Cuándo evitar RAG:
El campo está evolucionando rápidamente con técnicas avanzadas como HyDE, recuperación de múltiples consultas y compresión contextual, pero el concepto central sigue siendo simple: dar acceso a los LLMs a la información correcta en el momento adecuado.
Comience simple, mida los resultados e iterate. RAG es una de las maneras más prácticas de construir sistemas de IA confiables hoy en día.
Ahora has completado la serie RAG de tres partes:
Parte 1: Orígenes y fundamentos
Parte 2: Arquitectura e Interiores
Parte 3: Los GCR en la práctica (este artículo)
Ahora tienes la imagen completa: Desde entender los orígenes de RAG hasta construir sistemas de producción con optimizaciones avanzadas.
Ahora que usted entiende RAG de la teoría a la práctica, los próximos artículos le mostrarán cómo construir sistemas RAG completos, listos para la producción en C#:
Próximamente:
Estos artículos te llevarán de la teoría a la práctica, con código de trabajo completo, estrategias de implementación y optimizaciones del mundo real basadas en la ejecución de estos sistemas en producción en este blog.
¡Manténgase atento para las guías de implementación prácticas que convierten este conocimiento de RAG en sistemas de trabajo!
Documentos Fundacionales:
Herramientas y marcos:
Más información:
Esta serie RAG:
¡Feliz edificio!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.