RAG Arquitectura e Internos: Cómo funciona realmente (Español (Spanish))

RAG Arquitectura e Internos: Cómo funciona realmente

Saturday, 22 November 2025

//

27 minute read

In Parte 1, cubrimos los orígenes de RAG, los fundamentos y por qué es importante. Comprendes el concepto de alto nivel: recuperar información relevante y luego usarla para generar respuestas. Ahora nos sumergimos profundamente en la arquitectura técnica—exactamente cómo funcionan los sistemas RAG bajo el capó, desde estrategias de troceado hasta internos LLM como tokens y cachés KV.

Introducción

Navegación de la serie: Esta es la parte 2 de la serie RAG:

Si no has leído la Parte 1, te recomiendo empezar por ahí para entender:

  • Qué es RAG y por qué importa
  • La historia de la búsqueda de palabras clave a la comprensión semántica
  • GCR frente a ajustes y otros enfoques

Este artículo asume que usted entiende esos conceptos básicos y se centra en arquitectura técnica, detalles de implementación e internos de LLM.

Cómo funciona RAG: La imagen completa

Vamos a desglosar exactamente lo que sucede en un sistema RAG, desde el momento en que se añade un documento a cuando un usuario obtiene una respuesta.

Fase 1: Indización (Preparación de la base de conocimientos)

Antes de que RAG pueda recuperar cualquier cosa, necesita indexar su base de conocimientos. Este es un proceso de una sola vez (aunque puede añadir nuevos documentos más adelante).

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

    B -.Metadata.-> E

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

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

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

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

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

Paso 1: Extracción de texto

Extraiga texto plano de sus documentos de origen. Esto podría ser:

  • Marcar archivos (como mis publicaciones de blog)
  • PDF (para documentación)
  • HTML (para el raspado web)
  • Registros de bases de datos
  • Correos electrónicos, registros de chat, etc.

Ejemplo de mi blog:

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

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

    return plainText.Trim();
}

Paso 2: Recorte

Aquí es donde la mayoría de las implementaciones de RAG fallan. No puede simplemente dividirse en los límites de los párrafos - necesita trozos semánticamente coherentes.

¿Por qué el troceado importa:

  • Los LLM tienen límites de token (ventanas de contexto)
  • Trozos más pequeños = recuperación más precisa
  • Pero los trozos deben contener suficiente contexto para ser significativos.

Mal troceado:

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

Buen troceado:

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

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

Ejemplo de mi implementación de búsqueda semántica:

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

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

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

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

        return chunks;
    }
}

Estrategias comunes de troceado:

  • Tamaño fijo: Simple pero rompe los límites semánticos
  • Pena basada en la pena: Respeta la gramática pero puede ser demasiado pequeña
  • Basado en párrafos: Tamaño natural pero variable
  • Seccionales: Mejor para el contenido estructurado (mi preferencia)
  • Ventana deslizante con solapamiento: Garantiza que no se pierda ningún contexto en los límites

Paso 3: Generar incrustaciones

Las incrustaciones son la magia que hace posible la búsqueda semántica. Una incrustación es un vector (array de números) que representa el significado del texto.

Concepto clave: Significados similares → vectores similares

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

Los dos primeros vectores serían "cerrados" en el espacio vectorial (alta similitud coseno), mientras que el tercero está muy lejos.

Cómo se generan las incrustaciones: Los modelos modernos de incrustación son redes neuronales entrenadas en conjuntos masivos de datos de texto para aprender relaciones semánticas.

  • all-MiniLM-L6-v2: 384 dimensiones, rápido, de buena calidad (lo que uso en este blog)
  • text-embedding-3-pequeño (OpenAI): 1536 dimensiones, muy alta calidad
  • BGE-base: 768 dimensiones, fuente abierta de última generación

Ejemplo de mi servicio de incrustación ONNX:

public async Task<float[]> GenerateEmbeddingAsync(string text)
{
    // Tokenize the input text
    var tokens = Tokenize(text);

    // Create input tensors for ONNX model
    var inputIds = CreateInputTensor(tokens);
    var attentionMask = CreateAttentionMaskTensor(tokens.Length);
    var tokenTypeIds = CreateTokenTypeIdsTensor(tokens.Length);

    // Run ONNX inference
    var inputs = new List<NamedOnnxValue>
    {
        NamedOnnxValue.CreateFromTensor("input_ids", inputIds),
        NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask),
        NamedOnnxValue.CreateFromTensor("token_type_ids", tokenTypeIds)
    };

    using var results = _session.Run(inputs);

    // Extract the output (sentence embedding)
    var output = results.First().AsTensor<float>();
    var embedding = output.ToArray();

    // L2 normalize the vector for cosine similarity
    return NormalizeVector(embedding);
}

Por qué la normalización importa: Después de la normalización L2, la similitud coseno se convierte en un simple producto punto, haciendo la búsqueda mucho más rápida.

Paso 4: Almacenar en la base de datos vectorial

Las bases de datos vectoriales están optimizadas para almacenar y buscar vectores de alta dimensión. A diferencia de las bases de datos tradicionales que utilizan consultas SQL, las bases de datos vectoriales utilizan búsquedas similares.

Operaciones clave:

  • Upsert: Añadir o actualizar un vector con metadatos
  • Buscar: Encontrar K vectores más similares a un vector de consulta
  • Filtro: Combine la búsqueda de vectores con los filtros de metadatos

Ejemplo de aplicación de Qdrant:

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

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

Bases de datos de vectores populares:

  • Qdrant: Rápido, auto-alojable, excelente soporte C# (mi elección)
  • pgvector: Extensión PostgreSQL (genial si ya estás usando Postgres)
  • Pinecona: Servicio gestionado (precioso pero bueno)
  • Tejido: rico en características, bueno para esquemas complejos
  • ChromaDB: Python-centrado, ligero

Vamos a explorar la creación de estas bases de datos en los próximos artículos.

Fase 2: Recuperación (Encontrando información relevante)

Cuando un usuario hace una pregunta, el sistema RAG necesita encontrar la información más relevante de la base de conocimientos.

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

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

Paso 1: Generar la integración de la consulta

La pregunta del usuario se convierte a un vector usando el mismo modelo de empotrado Esto es crítico - diferentes modelos producen vectores incompatibles.

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

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

    return results;
}

Paso 2: Búsqueda de similitudes

La base de datos de vectores calcula la similitud entre el vector de consulta y todos los vectores almacenados.

Similitud cosina (más popular para vectores normalizados):

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

Intervalo: -1 a 1 (más alto = más similar)

Distancia euclidiana (para vectores no normalizados):

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

Intervalo: 0 a Ł (inferior = más similar)

Producto de punto (cuando los vectores están prenormalizados):

similarity = A · B

Intervalo: -1 a 1 (más alto = más similar)

Ejemplo de mi servicio Qdrant:

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

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

Paso 3: Recalificación (opcional pero recomendada)

La recuperación inicial es rápida pero aproximada. Reordenar utiliza un modelo más sofisticado para revalorizar los resultados K superiores.

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

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

¿Por qué la recalificación ayuda a:

  • Modelos de inserción rápida optimizar para la velocidad, sacrificando algo de precisión
  • Los modelos de recalificación son más lentos pero más precisos
  • Equilibrios de aproximación en dos etapas velocidad y calidad

Ejemplo de reordenación de la aplicación:

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

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

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

Fase 3: Generación (Creando la respuesta)

Ahora que tenemos información relevante, la alimentamos al LLM junto con la pregunta del usuario.

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

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

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

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

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

Paso 1: Construcción inmediata

Aquí es donde RAG se convierte en un arte. Necesitas estructurar el prompt para que el LLM:

  • Utiliza el contexto proporcionado (no sus conocimientos internos)
  • Cites fuentes cuando sea posible
  • Admite cuando el contexto no contiene una respuesta
  • Mantiene un tono/estilo consistente

Ejemplo de plantilla prompt de mi sistema de abogado GPT:

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

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

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

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

    return sb.ToString();
}

Paso 2: Inferencia de LLM

El prompt construido va al LLM para la generación. Esto puede ser:

  • API en la nube: OpenAI, Antrópico Claude, Google PaLM
  • Modelo local: Usando llama.cpp, ONNX Runtime, o TorchSharp

Ejemplo usando LLM local:

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

    return result.Text.Trim();
}

Parámetros clave explicados:

  • Temperatura: Controla la aleatoriedad (0 = siempre escoge lo más probable, 1 = muestra al azar)
  • P superior: Muestreo de Núcleo - considere sólo tokens que componen la masa de probabilidad P superior
  • Max Tokens: Limite la longitud de la respuesta
  • Detener secuencias: Cuándo dejar de generar

Paso 3: Post-Procesamiento

Después de que el LLM genera una respuesta, a menudo necesitamos:

  • Extraer citas y convertirlas a enlaces
  • Formato de bloques de código
  • Añadir metadatos (fuentes, puntuaciones de confianza)
  • Registrar la interacción para la depuración

Ejemplo de procesamiento posterior:

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

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

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

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

    return response;
}

Comprensión de los internos de LLM: Tokens, caché de KV y ventanas contextuales

Antes de pasar a aplicaciones prácticas, es esencial entender cómo funcionan internamente los LLMs. Este conocimiento le ayuda a optimizar los sistemas RAG y evitar los escollos comunes.

¿Qué son los tokens?

Los tokens son las unidades fundamentales que procesan los LLMs. El texto no se alimenta directamente a los modelos - primero se descompone en tokens.

Ejemplo de tokenización:

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

Diferentes modelos utilizan diferentes estrategias de tokenización:

  • Modelos GPT: Usar codificación Byte-Pair (BPE) con vocabulario ~50K
  • Claude: Enfoque similar BPE
  • Modelos de llama: SensaciónPiece tokenization

Por qué la tokenización es importante para RAG:

public class TokenCounter
{
    // Rough approximation: 1 token ≈ 0.75 words (English)
    public int EstimateTokens(string text)
    {
        var wordCount = text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
        return (int)(wordCount / 0.75);
    }

    public int EstimateTokensAccurate(string text, ITokenizer tokenizer)
    {
        // Use actual tokenizer for precision
        return tokenizer.Encode(text).Count;
    }
}

Límites de la ventana de contexto:

  • GPT-3.5: tokens de 16K
  • GPT-4: tokens 8K-128K (dependiendo de la variante)
  • Claude 3.5 Soneto: 200K tokens
  • Llama 3: tokens de 8K (aunque se puede extender)

En los sistemas RAG, debe caber:

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

Si su RAG recupera 10 documentos de 500 tokens cada uno, eso es 5.000 tokens sólo para contexto - antes de la consulta y la respuesta!

Gestión práctica de fichas RAG:

public class ContextWindowManager
{
    private readonly int _maxContextTokens;
    private readonly int _systemPromptTokens;
    private readonly int _responseBufferTokens;

    public ContextWindowManager(
        int totalContextWindow = 4096,
        int systemPromptTokens = 300,
        int responseBufferTokens = 500)
    {
        _maxContextTokens = totalContextWindow;
        _systemPromptTokens = systemPromptTokens;
        _responseBufferTokens = responseBufferTokens;
    }

    public List<SearchResult> FitContextInWindow(
        List<SearchResult> retrievedDocs,
        string query)
    {
        var queryTokens = EstimateTokens(query);

        // Available tokens for retrieved context
        var availableForContext = _maxContextTokens
            - _systemPromptTokens
            - queryTokens
            - _responseBufferTokens;

        var selectedDocs = new List<SearchResult>();
        var currentTokens = 0;

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

            if (currentTokens + docTokens <= availableForContext)
            {
                selectedDocs.Add(doc);
                currentTokens += docTokens;
            }
            else
            {
                break; // Context window full
            }
        }

        return selectedDocs;
    }

    private int EstimateTokens(string text)
    {
        // Rule of thumb: 1 token ≈ 4 characters
        return text.Length / 4;
    }
}

El caché de KV: el arma secreta de LLM

Cuando un LLM genera texto, no vuelve a procesarlo todo desde cero para cada token. Caché de valor clave (KV) para recordar lo que ya ha calculado.

Cómo funcionan los transformadores (simplificados)

Los transformadores usan un mecanismo de "atención" donde cada token "asiste" (mira) a todos los tokens anteriores para entender el contexto.

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

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

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

    D1 --> A2
    D2 --> A3

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

Sin caché KV:

  • Paso 1: Procesar 1 token → O(1)
  • Paso 2: Procesar 2 fichas desde cero → O(2)
  • Paso 3: Procesar 3 fichas desde cero → O(3)
  • Total: O(1 + 2 + 3 + ... + N) = O(N2)

Con caché KV:

  • Paso 1: Procesar 1 token, caché K,V → O(1)
  • Paso 2: Procesar 1 nuevo token, reutilizar en caché K,V → O(1)
  • Paso 3: Procesar 1 nuevo token, reutilizar en caché K,V → O(1)
  • Total: O(N)

Esto hace que la generación dramáticamente más rápido. - la diferencia entre 10 tokens/segundo y 100 tokens/segundo.

La estructura del árbol de caché KV

La caché KV forma un "árbol" debido a cómo funciona la atención en los transformadores. Cada capa en el modelo tiene sus propias matrices K,V.

graph TB
    A[Input Tokens:<br/>'What is Docker?'] --> B[Layer 1 Attention]
    B --> C[Layer 1 KV Cache]

    B --> D[Layer 2 Attention]
    D --> E[Layer 2 KV Cache]

    D --> F[Layer 3 Attention]
    F --> G[Layer 3 KV Cache]

    F --> H[... up to Layer N]
    H --> I[Output: 'Docker is']

    C -.Key-Value pairs<br/>for all input tokens.-> C
    E -.Key-Value pairs<br/>for all input tokens.-> E
    G -.Key-Value pairs<br/>for all input tokens.-> G

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

Cada capa almacena:

  • Claves (K): Representaciones utilizadas para calcular los puntajes de atención
  • Valores (V): Representaciones que se mezclan en base a la atención

Para un modelo con:

  • 32 capas
  • 4096 dimensiones ocultas
  • 32 jefes de atención
  • Ventana de contexto 8K

La caché KV para una secuencia es:

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

Es por eso que las ventanas de contexto largas son intensivas en memoria.

Caché KV en sistemas RAG

Los sistemas RAG pueden aprovechar la optimización de la caché KV de maneras inteligentes:

Caché rápido (apoyado por algunas APIs como Anthropic Claude):

public class CachedRAGService
{
    // System prompt and retrieved context can be cached!
    public async Task<string> GenerateWithCachedContextAsync(
        string systemPrompt,          // Cached
        List<SearchResult> context,   // Cached
        string userQuery)             // Not cached, changes each time
    {
        var contextText = FormatContext(context);

        // The KV cache for systemPrompt + contextText is reused across queries
        var prompt = $@"
{systemPrompt}

CONTEXT:
{contextText}

QUERY: {userQuery}

ANSWER:";

        return await _llm.GenerateAsync(prompt, useCaching: true);
    }
}

Por qué esto es poderoso:

  • Primera consulta: Computa la caché KV para el sistema prompt + context (slow)
  • Consultas posteriores con el mismo contexto: Reutiliza KV en caché (10x más rápido!)
  • Sólo la porción de consulta del usuario necesita computación fresca

Ejemplo práctico:

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

Las tres consultas utilizan el mismo contexto recuperado, por lo que la caché KV para ese contexto se reutiliza.

Límites de token y estrategia de GCR

Entender tokens y caché KV informa sus decisiones de arquitectura RAG:

1. Tamaño de corte

Trozos más pequeños = recuperación más precisa, pero más arriba:

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

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

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

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

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

2. Utilización de ventanas contextuales

No max out the context window - deja espacio para la generación:

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

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

3. Conversaciones RAG multi-turn

En los chatbots, la historia de la conversación crece con cada turno:

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

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

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

Solución: Ventana deslizante con re-retrieval

public class ConversationalRAG
{
    private readonly int _maxHistoryTokens = 1000;

    public async Task<string> ChatAsync(
        List<ConversationTurn> history,
        string newQuery)
    {
        // Re-retrieve context based on current query
        var context = await RetrieveContextAsync(newQuery);

        // Keep only recent conversation history
        var relevantHistory = TrimHistory(history, _maxHistoryTokens);

        var prompt = BuildPrompt(context, relevantHistory, newQuery);

        return await _llm.GenerateAsync(prompt);
    }

    private List<ConversationTurn> TrimHistory(
        List<ConversationTurn> history,
        int maxTokens)
    {
        var trimmed = new List<ConversationTurn>();
        var currentTokens = 0;

        // Keep most recent turns
        foreach (var turn in history.Reverse())
        {
            var turnTokens = EstimateTokens(turn.Query) + EstimateTokens(turn.Response);

            if (currentTokens + turnTokens <= maxTokens)
            {
                trimmed.Insert(0, turn);
                currentTokens += turnTokens;
            }
            else
            {
                break;
            }
        }

        return trimmed;
    }
}

4. Optimización de los costos de referencia

Carga basada en API LLMs por token. RAG puede explotar los costos si no es cuidadoso:

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

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

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

        return inputCost + outputCost;
    }

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

Estrategias de reducción de costos:

  1. Recuperar menos documentos mejor clasificados
  2. Utilice caché rápido (Anthropic Claude: 90% más barato para tokens en caché)
  3. Utilice modelos más baratos para volver a clasificar, caros para la generación final
  4. Comprimir el contexto mediante la suma de datos

Visualización del flujo completo de RAG con tokens

Así es como los tokens, el caché KV y el RAG encajan:

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

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

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

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

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

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

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

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

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

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

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

Perspectivas clave:

  1. tokens de entrada 2.812) se procesan una vez para crear caché KV inicial
  2. Generación sucede un token a la vez, reutilizando la caché KV
  3. Cada nuevo token añade a la caché de KV para futuros tokens para atender
  4. Total VRAM necesario = pesos de modelo + caché KV para todos los tokens
  5. Contexto más largo = caché KV más grande = más VRAM

Consecuencias prácticas para los GCR

La comprensión de tokens y caché KV conduce a un mejor diseño de RAG:

1. Pre-computar y caché contextos comunes:

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

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

2. Optimice los límites de los trozos:

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

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

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

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

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

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

    return chunks;
}

3. Supervisar el uso de tokens en la producción:

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

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

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

Conclusión: Maestría en Arquitectura

Hemos cubierto la arquitectura técnica completa de los sistemas RAG:

Fase 1: Indización

  • Extracción de textos de diversas fuentes
  • Estrategias de corte (basadas en secciones, basadas en oraciones, con superposición)
  • Generación de incrustaciones (ONNX, servicios API)
  • Almacenamiento de vectores (Qdrant, pgvector, Pinecone)

Fase 2: Obtención

  • Incrustación de preguntas (¡el mismo modelo que la indexación!)
  • Búsqueda de similitudes (cosina, euclidiana, producto de punto)
  • Recalificación opcional para una mejor precisión
  • Filtrado de metadatos para obtener resultados refinados

Fase 3: Generación

  • Construcción rápida (contexto + instrucciones + consulta)
  • Inferencia LLM (temperatura, top-p, tokens máx.)
  • Post-procesamiento (extracción de citas, formato)

Internos LLM

  • Tokens: Las unidades fundamentales (no caracteres!)
  • KV Cache: Por qué la generación es rápida (lineal, no cuadrática)
  • Ventanas de contexto: Gestión de límites de token en RAG
  • Optimización de costos: Caché, compresión, recuperación inteligente

Perspectivas técnicas clave:

  1. Mismo modelo de empotrado para la indexación y recuperación (crítica!)
  2. Chunking importa más de lo que crees. - preserva la coherencia semántica
  3. La recalificación mejora la precisión al costo de la latencia
  4. La gestión de tokens es esencial - estimación antes de la consulta
  5. El almacenamiento en caché de KV hace que RAG sea factible - reutilizar los cálculos rápidos
  6. Las ventanas contextuales se llenan rápidamente - 10 documentos × 500 tokens = 5K tokens

Continuar a la Parte 3: RAG en la práctica

Ahora lo entiendes. Cómo funciona RAG a nivel técnico. Pero la teoría sólo te lleva hasta ahora. ¿Cómo se construyen estos sistemas? ¿A qué retos te enfrentarás? ¿Qué técnicas avanzadas puedes utilizar?

In Parte 3: Los GCR en la práctica, pasamos de la arquitectura a la implementación:

Aplicaciones en el mundo real:

  • Recomendación de entradas relacionadas en este blog
  • Búsqueda de blogs semánticos
  • Construyendo un asistente de escritura "Abogado GPT"

Desafíos y soluciones comunes:

  • Recorte de estrategias que preserven el contexto
  • Mejora de la calidad de incrustación para su dominio
  • Gestión dinámica de las ventanas de contexto
  • Prevención de alucinaciones a pesar de tener contexto
  • Mantener su índice actualizado

Técnicas avanzadas:

  • Incrustaciones de documentos hipotéticos (HyDE)
  • Autobuscable con filtros de LLM
  • GCR multiconsultas para obtener resultados exhaustivos
  • Compresión contextual para reducir el uso de tokens
  • Multi-hop RAG para consultas complejas
  • Memoria conversacional a largo plazo

Primeros pasos:

  • Plan de aplicación semanal
  • Ejemplos prácticos de códigos
  • Estrategias de optimización
  • Cuándo NO usar RAG

Continuar a la Parte 3: RAG en la práctica →

Recursos

Documentos Fundacionales:

Herramientas y marcos:

Más información:

Navegación en serie:

Continuar a la Parte 3 →

Finding related posts...
logo

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