Inicio / Artículos / Elección y ajuste de modelos de embedding para sistemas RAG en producción.

Elección y ajuste de modelos de embedding para sistemas RAG en producción.

Aprende cómo los modelos de embedding convierten el texto en vectores buscables, por qué el vocabulario del dominio daña la búsqueda semántica, y cómo seleccionar, comprimir y ajustar los modelos para sistemas RAG en producción.

6521 palabras

Este es el quinto capítulo de una serie sobre la creación de sistemas de generación mejorada por recuperación de información de nivel profesional, que sigue el camino desde documentos en bruto hasta un sistema capaz de responder a preguntas reales. Las etapas anteriores de esta serie trataron sobre la limpieza y normalización del contenido extraído, y luego su división en unidades de recuperación mediante el procesamiento por bloques. Una vez obtenidos los bloques, el siguiente paso es transformarlos en algo que un índice de búsqueda pueda comparar realmente.

Imagínese al mismo gestor de relaciones mencionado anteriormente, aún atascado con una línea de crédito de 12 millones de euros para un cliente corporativo de alto riesgo. Para atender la solicitud es necesario localizar un requisito de diligencia debida mejorada oculto en una cláusula de la política, un umbral de aprobación indicado en una fila de tabla y una secuencia de control descrita en un diagrama de procesos. La etapa de procesamiento por bloques ya ha separado estos elementos en piezas de contenido independientes y rastreables.

Aun así, nada de eso puede ser buscado aún por un índice vectorial.

Un modelo de embedding es lo que convierte cada fragmento en un vector numérico de longitud fija. Luego, el mismo modelo transforma una consulta entrante en un vector, y el índice recupera aquellos fragmentos que se encuentren más cerca de él en ese espacio vectorial. No está garantizado que frases como “Aprobación del Comité de Crédito del Grupo” en un documento de política y “Umbral de aprobación del GCC” en la pregunta de un usuario terminen realmente cerca una de la otra; todo depende del modelo que se haya elegido y de lo que haya aprendido durante el entrenamiento.

Esa dependencia es precisamente de lo que trata esta parte de la serie.

Elegir un modelo de embedding equivale a apostar por cuánto vocabulario y formulación comparten sus documentos con las consultas de los usuarios. Si elige el incorrecto para su área de aplicación, perderá semanas intentando solucionar lo que parece un error de recuperación, pero que en realidad es un problema de representación: los vectores mismos están colocados incorrectamente, por lo que ninguna cantidad de ajuste del índice podrá solucionarlo.

Qué calcula realmente un modelo de embedding

En esencia, un modelo de embedding ingiere una secuencia de tokens y genera un vector denso, generalmente con entre 384 y 3072 dimensiones según el modelo específico. Ese vector tiene como objetivo ser una codificación comprimida de lo que significa la entrada.

La suposición básica detrás de la recuperación es que las entradas con significado similar terminan teniendo vectores cercanos entre sí en ese espacio. La proximidad se mide comúnmente con la similitud coseno, que analiza el ángulo que separa a dos vectores en lugar de su distancia bruta; esta propiedad hace que sea insensible a las diferencias en la longitud del texto.

Considere una regla de cumplimiento que establece que los clientes corporativos clasificados como de alto riesgo deben someterse a verificaciones de debida diligencia reforzada antes de que se pueda presentar una propuesta de crédito para su revisión. Un modelo versátil y capaz probablemente colocaría su vector cerca de textos similares sobre cumplimiento de otros bancos, cerca de material legal relacionado con las obligaciones de debida diligencia y cerca de directrices regulatorias para gestionar clientes de alto riesgo.

No obstante, un gestor de relaciones podría expresar la misma necesidad subyacente de manera muy diferente, preguntando algo como qué pasos se deben seguir antes de poder presentar una solicitud de crédito. Si esa forma de expresión cotidiana se acerca al enfoque formal de las políticas es, en realidad, una cuestión relacionada con la cantidad de datos de entrenamiento que ha vinculado preguntas operativas informales con lenguaje formal de cumplimiento. Los modelos entrenados principalmente en texto web general y de uso amplio a menudo nunca absorben esa combinación específica cuando el campo es especializado.

Esta brecha entre la redacción de las consultas cotidianas y la redacción de los documentos especializados se conoce como desajuste de vocabulario, y es la causa principal de los problemas de calidad en la recuperación de información en los sistemas RAG empresariales.

Tokenización y la ventana de contexto

Antes de que un modelo de embedding calcule algo, primero divide la entrada en tokens utilizando su propio vocabulario interno. El recuento de tokens no se corresponde directamente con el número de palabras o caracteres. Un bloque de 500 tokens en inglés podría equivaler a aproximadamente 350–400 palabras, mientras que el mismo límite de tokens en alemán — donde las palabras suelen ser compuestas — podría representar menos ideas distintas.

Cada modelo de embedding establece una longitud máxima de contexto; todo lo que sea más largo o bien se trunca o requiere un tratamiento especial. Los modelos Sentence-Transformers suelen tener un límite entre 256 y 512 tokens. Text-Embedding-3-Large de OpenAI puede manejar hasta 8,191 tokens. BGE-M3 alcanza hasta 8,192 tokens. Las embeddings v3 de Jina también soportan hasta 8,192 tokens.

La conclusión práctica para el RAG en entornos de producción es que los límites entre los fragmentos de texto definidos anteriormente en el proceso deben mantenerse dentro del límite de contexto impuesto por el modelo de embedding elegido. Cualquier fragmento que supere ese límite se corta silenciosamente, y el vector resultante solo refleja una parte del texto original; este es un modo de fallo que no aparecerá en los registros de tu pipeline.

El espacio semántico y cuándo falla

La mayoría de los modelos de embedding actuales son codificadores tipo transformer entrenados con un objetivo contrastivo: las parejas de textos que significan cosas similares se acercan en el espacio vectorial, mientras que las parejas dispares se alejan. Tras suficiente entrenamiento, el modelo adquiere una estructura geométrica en la que la proximidad sirve como indicador de relación semántica.

Ese diseño funciona bien siempre y cuando las consultas y los documentos compartan la terminología, el estilo de redacción y el enfoque conceptual con lo que se utilizó para entrenar al modelo. En el caso del RAG empresarial, tiende a fallar de varias maneras predecibles:

  • La terminología específica del dominio genera fallos que son fáciles de pasar por alto. Un analista que pregunte sobre el “umbral de presentación de SAR para la estructuración” podría no encontrar coincidencias con un fragmento de política redactado como “Criterios de presentación del Informe de Actividades Sospechosas para la estructuración de transacciones”, si el modelo nunca aprendió que estas dos formulaciones significan lo mismo.
  • Abreviaturas no se comportan de manera consistente. Términos como “GCC” (Group Credit Committee), “EDD” (Enhanced Due Diligence) y “RFI” (Request for Information) pueden ser incorporados por un modelo de uso general según sus significados más comunes en otros contextos: Consejo de Cooperación del Golfo, Entrega Electrónica de Documentos, Interferencia de Frecuencia de Radio.
  • Identificadores regulatorios no tienen un significado inherente para los modelos generales. Un código como CRD-EU-047, al ser procesado por un modelo de incorporación de uso general, se trata como una cadena arbitraria. Un modelo entrenado específicamente en texto regulatorio, en cambio, lo clasificaría junto a otros identificadores de regulación crediticia de la UE que pertenecen a la misma familia conceptual.
  • Límites numéricos son manejados solo parcialmente bien por los modelos generales. La expresión “10 millones de euros” por sí sola se incluye cerca de otras cifras monetarias. Cuando aparece junto a información sobre la autoridad de aprobación, un modelo ajustado específicamente para ese dominio puede capturar la relación entre esa cantidad concreta y el control de gobernanza que desencadena, algo que es menos probable que un modelo general codifique correctamente.
  • Reconocer exactamente dónde tienden a fallar los modelos de incrustación de uso general es tan importante como saber qué modelo lidera las listas de mejores rendimientos en pruebas públicas.

    Elegir un modelo de incrustación en 2025

    El campo de los modelos de incrustación se ha reducido considerablemente. La comparación que sigue abarca los modelos más relevantes para los sistemas RAG empresariales en banca y servicios financieros a mediados de 2025.

    No existe un ganador universal para todos los escenarios empresariales. Su elección depende de la combinación de idiomas que necesite soportar, del presupuesto de latencia y de la infraestructura a su disposición, de si la inferencia local es una opción viable para usted, y del tamaño de la brecha terminológica entre su dominio y los modelos de uso general en los que se entrenaron; dicha brecha determina si vale la pena realizar un ajuste fino.

    Recuperación asimétrica y indicar al modelo qué tipo de entrada está recibiendo

    Una distinción que muchos equipos pasan por alto es la recuperación asimétrica. Cuando se recuperan fragmentos de texto, la consulta y el fragmento con el que se compara son cosas estructuralmente muy diferentes. Las consultas suelen ser cortas, están formuladas como preguntas y a menudo carecen de gran parte del vocabulario que aparece en una respuesta correcta. En cambio, los fragmentos de texto son más largos, se presentan como hechos y están repletos de términos específicos del dominio.

    Ciertos modelos están diseñados para reconocer esta asimetría directamente. Los modelos E5 añaden un prefijo “query:” o “passage:” al texto de entrada para que el modelo sepa qué rol debe asignarle. Cohere’s Embed v3 expone esto a través de un parámetro input_type, cuyos valores aceptados incluyen “search_query”, “search_document”, “classification” y “clustering”.

    Proporcionar el tipo de entrada incorrecto en el momento del indexado o la consulta afecta silenciosamente las puntuaciones de similitud de manera difícil de rastrear hasta su causa raíz. Si incrustas un documento como si fuera una consulta, obtendrás un vector diseñado para la geometría de las consultas y no para la de los pasajes. La recuperación de información no falla por completo, pero pierde precisión de forma que es fácil pasarla por alto durante pruebas informales.

    En entornos de producción, no dependa de que los desarrolladores recuerden configurarlo correctamente: hágalo obligatorio a través de la configuración. Tanto la llamada de incrustación en el momento del indexado como la llamada de incrustación en el momento de la consulta deben declarar explícitamente su tipo de entrada siempre que el modelo que utilice soporte esa opción.

    import cohere
    from typing import List
    
    co = cohere.Client(api_key="your_api_key")
    
    def embed_documents(chunks: List[str]) -> List[List[float]]:
        """Embed document chunks for indexing with explicit document input type."""
        response = co.embed(
            texts=chunks,
            model="embed-english-v3.0",
            input_type="search_document",
            embedding_types=["float"]
        )
        return response.embeddings.float
    
    def embed_query(query: str) -> List[float]:
        """Embed a search query with explicit query input type."""
        response = co.embed(
            texts=[query],
            model="embed-english-v3.0",
            input_type="search_query",
            embedding_types=["float"]
        )
        return response.embeddings.float[0]
    

    Vectores dispersos: donde la coincidencia de palabras clave supera a la búsqueda semántica

    Las incrustaciones densas capturan el significado. En cambio, las representaciones dispersas indican qué términos están presentes y cuánto peso deben tener. Para una parte significativa de los tipos de consultas en sistemas RAG empresariales, la recuperación con vectores dispersos supera por completo a la recuperación con vectores densos, y en la mayoría de los sistemas de producción, combinar ambos es mejor que utilizar solo uno.

    BM25 como línea de base fiable

    BM25 sigue siendo el enfoque estándar para la recuperación basada en palabras clave. Evalúa la relevancia según la frecuencia con que aparece un término en un documento, cuán raro es ese término en todo el corpus y un factor de normalización que tiene en cuenta la longitud del documento. No hay modelo que entrenar, no se requiere GPU y no se necesita ninguna llamada a una API de embeddings.

    Considere una consulta como “CRD-EU-047 approval authority threshold”. BM25 dará una alta clasificación a cualquier fragmento que contenga esos términos literales. Por otro lado, un modelo denso podría no mostrar ese fragmento a menos que su corpus de entrenamiento haya establecido una fuerte asociación entre ese código de política específico y el concepto de autoridad de aprobación.

    from rank_bm25 import BM25Okapi
    import re
    from typing import List, Tuple
    
    def tokenise(text: str) -> List[str]:
        """Simple whitespace and punctuation tokeniser for BM25."""
        return re.findall(r'\b\w+\b', text.lower())
    
    class BM25Index:
        def __init__(self, documents: List[str]):
            self.documents = documents
            tokenised = [tokenise(doc) for doc in documents]
            self.bm25 = BM25Okapi(tokenised)
    
        def search(self, query: str, top_k: int = 10) -> List[Tuple[int, float]]:
            """Return (doc_index, score) pairs for the top_k results."""
            tokens = tokenise(query)
            scores = self.bm25.get_scores(tokens)
            ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
            return ranked[:top_k]
    

    La recuperación densa es eficaz para capturar similitudes conceptuales, mientras que la recuperación dispersa es útil para encontrar coincidencias exactas en términos e identificadores. Combinar ambas —recuperación híbrida— suele dar excelentes resultados, especialmente en corpus bancarios donde el lenguaje regulatorio es preciso y está repleto de identificadores.

    En corpus basados en un lenguaje normativo estable y definido con precisión, BM25 por sí solo a menudo logra un rendimiento similar al de la recuperación densa en búsquedas factuales específicas, con una fracción mínima de la carga adicional en infraestructura. Su principal limitación es la sinonimia: una consulta que utilice “requisitos EDD” no recuperará un pasaje que solo diga “requisitos de debida diligencia mejorada”, a menos que la frase exacta aparezca en algún lugar.

    SPLADE: Vectores dispersos que aprenden la expansión del vocabulario

    SPLADE (Sparse Lexical and Expansion Model) ocupa un punto intermedio entre la simple coincidencia de palabras clave y la recuperación densa completa. Durante el proceso de indexación, se utiliza un modelo de lenguaje enmascarado para enriquecer tanto los documentos como las consultas con un vocabulario semánticamente relacionado que no necesariamente está presente en la redacción original. El resultado es un vector disperso cuyas dimensiones corresponden a cada token del vocabulario, ponderadas según la importancia que ese token tiene para la entrada.

    Por lo tanto, un pasaje codificado con SPLADE que aborda los requisitos de EDD podría tener mayor peso en términos como “debida diligencia del cliente”, “evaluación de riesgos” e “verificación de identidad”, incluso cuando ninguna de esas frases exactas aparezca en el texto original. Esta expansión mejora la capacidad de recuperación en consultas que utilizan sinónimos, al tiempo que mantiene la eficiencia e interpretabilidad propias de las representaciones dispersas adecuadas para índices invertidos.

    El compromiso consiste en un costo adicional de inferencia al momento del índice y un mayor espacio de almacenamiento en comparación con BM25 tradicional. Pero en corpus de servicios financieros donde el mismo concepto normativo se describe de manera diferente según las jurisdicciones y revisiones de documentos, dicha expansión puede ampliar significativamente el alcance de la recuperación.

    Matryoshka Embeddings: Tamaño vectorial ajustable para el control de costos

    El aprendizaje por representación Matryoshka (MRL) crea embeddings en los que las N dimensiones iniciales ya forman una representación completa y autónoma de la entrada; las dimensiones adicionales añaden detalles más finos en lugar de sobrescribir lo anterior.

    La técnica toma su nombre de las muñecas rusas: un vector Matryoshka de 1536 dimensiones contiene una representación completamente funcional de 256 dimensiones en sus primeros 256 espacios, una representación funcional de 512 dimensiones en sus primeros 512 espacios, y así sucesivamente.

    La familia de modelos text-embedding-3 de OpenAI admite esto directamente mediante un parámetro de dimensiones.

    from openai import OpenAI
    from typing import List
    
    client = OpenAI()
    
    def embed_with_matryoshka(
        texts: List[str],
        dimensions: int = 256,
        model: str = "text-embedding-3-large"
    ) -> List[List[float]]:
        """
        Embed texts at a specified sub-dimension.
        Lower dimensions reduce storage and index cost.
        Measure retrieval quality drop before committing to a dimension.
        """
        response = client.embeddings.create(
            input=texts,
            model=model,
            dimensions=dimensions
        )
        return [item.embedding for item in response.data]
    

    Los embeddings Matryoshka anidan representaciones cada vez más detalladas dentro de un único vector: las primeras 256 dimensiones ya proporcionan una representación útil para la recuperación de información, y cada nivel adicional añade precisión semántica a un costo proporcional en almacenamiento.

    La verdadera ventaja en la producción es poder ajustar el equilibrio entre almacenamiento y calidad según sea necesario, sin tener que volver a entrenar un modelo ni reconstruir el índice desde cero. En el caso de un corpus relacionado con políticas bancarias, se pueden realizar pruebas de rendimiento con 256, 512, 1024 y 3072 dimensiones, y descubrir que 512 dimensiones permiten obtener el 97% de la recuperación total utilizando solo el 17% del almacenamiento que requeriría un vector completo.

    Ese equilibrio rara vez es tan claro en la práctica como parece en un gráfico de pruebas. Las sutiles diferencias entre dominios, especialmente entre conceptos regulatorios estrechamente relacionados, suelen encontrarse específicamente en la parte de mayor dimensión del vector. Pruebe con su propio corpus y patrones reales de consultas antes de decidirse por una dimensionalidad reducida para su uso en producción.

    Compresión de embeddings sin sacrificar demasiada precisión

    Los embeddings estándar almacenan cada dimensión como un número flotante de 32 bits. Si se eleva esa cantidad a un millón de fragmentos de documentos, cada uno con 1536 dimensiones, se obtienen aproximadamente 6 GB de vectores en bruto antes de que se añada cualquier carga adicional por indexación. A escala empresarial, ese espacio de almacenamiento y su costo en memoria dejan de ser un error de redondeo.

    La cuantización aborda este problema reduciendo la cantidad de bits utilizados para representar cada dimensión. En la práctica, tres técnicas son las más utilizadas: cuantización escalar (convierte float32 a int8), cuantización binaria (convierte float32 a un solo bit) y cuantización por producto (compresión de cada vector en un código más corto).

    Cuantización escalar: int8

    La cuantización escalar mapea el rango continuo de float32 a 256 valores enteros discretos. Cada dimensión pasa de ocupar 4 bytes a 1, lo que reduce el almacenamiento en un 75%. Dado que los modelos de embedding de alta dimensión distribuyen la información de manera dispersa en muchas dimensiones, ninguna dimensión por sí sola tiene mucho peso, por lo que la precisión perdida debido a este redondeo suele ser menor.

    import numpy as np
    from typing import Tuple
    
    def quantise_to_int8(
        embeddings: np.ndarray
    ) -> Tuple[np.ndarray, float, float]:
        """
        Scalar quantisation to int8.
        Returns quantised array plus the scale and zero_point needed for dequantisation.
        """
        min_val = embeddings.min()
        max_val = embeddings.max()
        scale = (max_val - min_val) / 255.0
        zero_point = -round(min_val / scale)
        quantised = np.clip(
            np.round(embeddings / scale) + zero_point,
            0, 255
        ).astype(np.uint8)
        return quantised, scale, zero_point
    
    def dequantise_from_int8(
        quantised: np.ndarray,
        scale: float,
        zero_point: float
    ) -> np.ndarray:
        """Reconstruct approximate float32 embeddings from int8."""
        return ((quantised.astype(np.float32) - zero_point) * scale)
    

    Cuantización binaria

    La cuantización binaria va aún más allá, reduciendo cada dimensión a un único bit que simplemente registra si el valor float original era positivo o negativo. Esto reduce el almacenamiento en aproximadamente un 97% en comparación con float32. Como la representación ya no es continua, la similitud se mide mediante la distancia de Hamming en lugar de la similitud coseno.

    Esta técnica funciona mejor en modelos cuyas distribuciones de salida están naturalmente bien centradas, de modo que para cualquier entrada dada aproximadamente la mitad de las dimensiones se encuentran en cada lado de cero. Si las dimensiones de un modelo están sesgadas en lugar de equilibradas, la cuantización binaria provoca una pérdida de calidad notablemente mayor. Cohere desarrolló Embed v3 teniendo en cuenta esta restricción, y la evaluación publicada por Anthropic de dicho modelo indica una degradación en la recuperación inferior al 1%, junto con una reducción del 97% en el almacenamiento en sus conjuntos de prueba. Considere esa cifra como un punto de partida y no como una garantía, y véalas validadas con su propio corpus antes de confiar en ellas.

    import numpy as np
    
    def quantise_to_binary(embeddings: np.ndarray) -> np.ndarray:
        """
        Binary quantisation: positive dimensions become 1, negative become 0.
        Packs 8 dimensions per byte using numpy packbits.
        """
        binary_matrix = (embeddings > 0).astype(np.uint8)
        return np.packbits(binary_matrix, axis=1)
    
    def hamming_similarity(
        query_binary: np.ndarray,
        corpus_binary: np.ndarray
    ) -> np.ndarray:
        """Compute normalised Hamming similarity for binary embeddings."""
        n_bits = corpus_binary.shape[1] * 8
        xor = np.bitwise_xor(
            query_binary,
            corpus_binary
        )
        hamming_distances = np.unpackbits(xor, axis=1).sum(axis=1)
        return 1.0 - (hamming_distances / n_bits)
    

    En entornos de producción, la cuantización binaria se suele utilizar como primera etapa de un proceso de recuperación en dos pasos: un índice binario permite una recuperación rápida y amplia de candidatos, mientras que un proceso de precisión total vuelve a calificar los resultados más relevantes. Este enfoque permite ahorrar la mayor parte del espacio de almacenamiento al mismo tiempo que se mantiene la precisión en el paso final de clasificación, donde es crucial.

    Ajuste fino para RAG específico del dominio

    El ajuste fino es la solución adecuada una vez que se ha confirmado que los embeddings de uso general no son suficientes para los datos específicos del dominio. El objetivo es enseñar al modelo que el vocabulario, las abreviaturas y los vínculos conceptuales propios de su área están cercanos entre sí en el espacio semántico.

    El ajuste fino no siempre es necesario, ni siempre constituye la solución adecuada. Si los problemas de recuperación se deben a una mala división en fragmentos, como se explicó anteriormente en esta serie, ajustar el modelo de embeddings no servirá de nada. Si la causa raíz radica en cómo está configurado el reordenamiento o en cómo se elaboran las instrucciones, el ajuste fino apunta erróneamente a una capa completamente distinta del sistema. Antes de invertir recursos en ello, analice sus fallos de recuperación por tipo de consulta para determinar dónde se encuentra realmente el problema.

    Cuando fallan los embeddings generales

    En el contexto específico de RAG en banca, algunos patrones recurrentes de fallo justifican la inversión en ajuste fino:

    Las abreviaturas específicas de un dominio suelen interpretarse erróneamente. Un modelo de uso general podría asociar “NPA” con la National Parks Association en lugar de con Activos que No Generan Ingresos, y podría vincular solo de forma débil “KYC” con los conceptos de cumplimiento y onboarding que en realidad predominan en las consultas bancarias.

    Faltan conexiones entre conceptos relacionados en diferentes documentos. Una búsqueda de “disposiciones para la reestructuración de facilidades” debería mostrar textos normativos sobre “marcos para la modificación de préstamos”, pero un modelo entrenado principalmente con contenido web general podría no haber visto nunca estas expresiones juntas lo suficiente como para establecer esa relación.

    Los códigos y identificadores regulatorios no reciben la importancia que merecen. Los números de versión de las políticas, los códigos regulatorios y los marcadores de jurisdicción deberían influir significativamente en la clasificación, pero los modelos de embedding generales tienden a tratarlos como tokens de bajo valor que transmiten poca información semántica.

    Los umbrales numéricos pierden su contexto regulatorio. Una cifra como “10 millones de euros” presentada por separado no debería relacionarse automáticamente con una consulta sobre “la autoridad competente para exposiciones grandes”; dicha conexión solo se establece si el modelo ha sido entrenado con datos del dominio que vinculan esa cifra a su significado regulatorio.

    Creación de pares de entrenamiento a partir de datos específicos del dominio

    El ajuste fino de los modelos sentence-transformers bajo un objetivo contrastivo depende de pares positivos: ejemplos que emparejan una consulta con un pasaje que el modelo debe aprender a tratar como relacionado. Los ejemplos negativos pueden seleccionarse manualmente o extraerse automáticamente del corpus circundante.

    En un contexto bancario de RAG, estos pares positivos pueden obtenerse de varias fuentes prácticas:

    Conjuntos existentes de preguntas y respuestas ya generados por los equipos de cumplimiento y crédito, donde cada pregunta está vinculada a su pasaje de origen.

    La estructura natural de los documentos normativos, donde un encabezado junto con el párrafo debajo forma un ejemplo positivo listo para usar.

    Consultas de analistas registradas emparejadas con los pasajes que realmente se recuperaron cuando la respuesta era correcta.

    Las consultas generadas por máquina por un LLM para cada fragmento de texto, utilizando dicho fragmento como el pasaje positivo de referencia.

    De estas, la generación de consultas sintéticas suele ser el enfoque más viable cuando ya existe poca información etiquetada.

    from openai import OpenAI
    import json
    from typing import List, Dict
    
    client = OpenAI()
    
    def generate_training_queries(
        chunk: str,
        chunk_metadata: Dict,
        n_queries: int = 3
    ) -> List[Dict]:
        """
        Generate synthetic query-passage pairs for fine-tuning.
        The chunk itself is the positive passage for each generated query.
        """
        prompt = f"""You are generating training data for a banking RAG system.
    Given the following policy passage, generate {n_queries} realistic questions
    that a credit analyst, compliance officer, or relationship manager might ask
    that this passage directly answers. Each question should use natural language
    and may use different terminology than the passage itself.
    
    Passage:
    {chunk}
    
    Return a JSON array of objects with keys "query" and "difficulty".
    Difficulty should be "narrow" (single fact) or "synthesis" (multiple facts).
    Return only the JSON array, no other text."""
    
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"}
        )
    
        try:
            result = json.loads(response.choices[0].message.content)
            queries = result.get("queries", result) if isinstance(result, dict) else result
            return [
                {
                    "query": q["query"],
                    "passage": chunk,
                    "document_id": chunk_metadata.get("document_id"),
                    "chunk_id": chunk_metadata.get("chunk_id"),
                    "difficulty": q.get("difficulty", "narrow")
                }
                for q in queries
            ]
        except (json.JSONDecodeError, KeyError):
            return []
    

    Entrenamiento contrastivo con pérdida de estilo tripleta

    El objetivo de entrenamiento más efectivo para los modelos de embedding orientados a la recuperación es el aprendizaje contrastivo, que utiliza negativos dentro del mismo lote o negativos difíciles seleccionados intencionadamente. Sentence-transformers admite este patrón a través de MultipleNegativesRankingLoss, el cual utiliza cada otro ejemplo en un lote de entrenamiento como negativo implícito para una pareja ancla-positivo dada.

    from sentence_transformers import SentenceTransformer, InputExample
    from sentence_transformers.losses import MultipleNegativesRankingLoss
    from torch.utils.data import DataLoader
    from typing import List, Dict
    import logging
    
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)
    
    def build_training_examples(
        pairs: List[Dict]
    ) -> List[InputExample]:
        """
        Convert query-passage pairs into InputExample objects.
        MultipleNegativesRankingLoss expects (anchor, positive) pairs.
        Negatives are sampled automatically from other items in the batch.
        """
        return [
            InputExample(texts=[pair["query"], pair["passage"]])
            for pair in pairs
            if pair.get("query") and pair.get("passage")
        ]
    
    def fine_tune_embedding_model(
        base_model_name: str,
        training_pairs: List[Dict],
        output_path: str,
        epochs: int = 3,
        batch_size: int = 16,
        warmup_steps: int = 100
    ) -> SentenceTransformer:
        """
        Fine-tune a sentence-transformers model on domain-specific query-passage pairs.
    
        base_model_name: HuggingFace model identifier or local path.
        training_pairs: List of dicts with "query" and "passage" keys.
        output_path: Directory to save the fine-tuned model.
        """
        model = SentenceTransformer(base_model_name)
        logger.info(f"Loaded base model: {base_model_name}")
        logger.info(f"Training on {len(training_pairs)} query-passage pairs")
    
        examples = build_training_examples(training_pairs)
        loader = DataLoader(examples, shuffle=True, batch_size=batch_size)
        loss = MultipleNegativesRankingLoss(model)
    
        total_steps = len(loader) * epochs
        logger.info(f"Training for {epochs} epochs, {total_steps} total steps")
    
        model.fit(
            train_objectives=[(loader, loss)],
            epochs=epochs,
            warmup_steps=warmup_steps,
            output_path=output_path,
            show_progress_bar=True,
            checkpoint_path=output_path,
            checkpoint_save_steps=len(loader)
        )
    
        logger.info(f"Fine-tuned model saved to: {output_path}")
        return model
    

    Resultados de las pruebas: un corpus bancario antes y después del ajuste fino

    Considere una prueba de rendimiento de recuperación realizada contra un corpus de políticas de crédito corporativo compuesto por 847 fragmentos extraídos de documentos normativos, matrices de aprobación, procedimientos mejorados de debida diligencia y directrices contra el lavado de dinero. El conjunto de evaluación está formado por 120 consultas que abarcan cuatro categorías: búsquedas factuales específicas, preguntas con umbrales, preguntas que requieren múltiples pruebas y preguntas de síntesis.

    La ventaja del ajuste fino del dominio es más evidente en consultas de tipo umbral, como aquellas que solicitan el umbral de aprobación aplicable a clientes corporativos de alto riesgo en la UE. Es aquí donde las abreviaturas bancarias y el vocabulario regulatorio difieren más marcadamente de lo que un modelo de uso general ha visto durante el preentrenamiento; por lo tanto, cerrar esa brecha léxica genera el mayor aumento en la capacidad de recuperación. Las consultas basadas en múltiples fuentes y las de síntesis mejoran menos con el ajuste fino por sí solo, pero ganan considerablemente una vez que se aplica la recuperación híbrida sobre ellas.

    Considere estos números como una ilustración de lo que puede lograrse con un proyecto de ajuste fino bien ejecutado en un conjunto de datos bancarios adecuadamente etiquetado, y no como una promesa. La composición de su propio corpus, la mezcla de consultas y la precisión en el etiquetado modificarán los resultados. Lo más importante es desglosar la calidad de la recuperación por tipo de consulta en lugar de presentar una sola puntuación combinada, ya que cada tipo de consulta tiende a fallar por razones diferentes.

    Consideraciones de rendimiento para el embejecimiento por lotes a gran escala

    Enviar 100,000 fragmentos a través de una API de embejecimiento uno por solicitud es lento y costoso. En cambio, las pipelines de embejecimiento de nivel profesional procesan sus entradas por lotes para aumentar el rendimiento, mantenerse dentro de los límites de velocidad, recuperarse eficazmente de las fallas y garantizar que la generación de resultados sea determinista.

    Procesamiento por lotes a través de APIs de proveedores

    El endpoint de embeddings de OpenAI permite hasta 2,048 entradas en una sola llamada. El endpoint Embed de Cohere tiene un límite de 96 textos por solicitud, a menos que se utilice su API por lotes dedicada para tareas más grandes. Ejecutar la inferencia localmente con sentence-transformers permite configurar tamaños de lote, limitados únicamente por la memoria de la GPU disponible.

    import time
    import logging
    from typing import List, Optional
    from openai import OpenAI, RateLimitError, APIError
    
    logger = logging.getLogger(__name__)
    client = OpenAI()
    
    def embed_in_batches(
        texts: List[str],
        model: str = "text-embedding-3-large",
        batch_size: int = 512,
        max_retries: int = 3,
        retry_delay: float = 2.0,
        dimensions: Optional[int] = None
    ) -> List[List[float]]:
        """
        Embed a large list of texts using batched API calls with retry logic.
    
        texts: Pre-chunked text strings. Caller is responsible for ensuring
               no text exceeds the model's token limit.
        batch_size: Number of texts per API call. Stay well below the API limit
                    to avoid hitting per-request token limits.
        dimensions: Optional Matryoshka dimension reduction for supported models.
        """
        all_embeddings: List[List[float]] = []
        total_batches = (len(texts) + batch_size - 1) // batch_size
    
        for batch_idx in range(0, len(texts), batch_size):
            batch = texts[batch_idx: batch_idx + batch_size]
            current_batch = batch_idx // batch_size + 1
            logger.info(f"Embedding batch {current_batch}/{total_batches} "
                        f"({len(batch)} texts)")
    
            kwargs = {
                "input": batch,
                "model": model
            }
            if dimensions is not None:
                kwargs["dimensions"] = dimensions
    
            attempt = 0
            while attempt < max_retries:
                try:
                    response = client.embeddings.create(**kwargs)
                    # Preserve input order: API returns items sorted by index
                    sorted_items = sorted(response.data, key=lambda x: x.index)
                    all_embeddings.extend([item.embedding for item in sorted_items])
                    break
    
                except RateLimitError:
                    attempt += 1
                    wait = retry_delay * (2 ** attempt)
                    logger.warning(f"Rate limit hit on batch {current_batch}. "
                                   f"Waiting {wait:.1f}s before retry {attempt}/{max_retries}")
                    time.sleep(wait)
    
                except APIError as e:
                    attempt += 1
                    logger.error(f"API error on batch {current_batch}: {e}. "
                                 f"Retry {attempt}/{max_retries}")
                    if attempt >= max_retries:
                        raise
                    time.sleep(retry_delay)
    
        logger.info(f"Embedding complete. Total vectors: {len(all_embeddings)}")
        return all_embeddings
    

    Ejecutar inferencia localmente con sentence-transformers

    Algunas organizaciones enfrentan restricciones de residencia de datos que impiden enviar documentos de política a una API externa. En esos casos, la inferencia local con sentence-transformers es la solución más adecuada.

    from sentence_transformers import SentenceTransformer
    import numpy as np
    from typing import List, Optional
    import logging
    
    logger = logging.getLogger(__name__)
    
    class LocalEmbeddingPipeline:
        """
        Production-ready local embedding pipeline using sentence-transformers.
        Suitable for data-residency-constrained banking environments.
        """
    
        def __init__(
            self,
            model_name_or_path: str,
            device: str = "cpu",
            batch_size: int = 64,
            normalise: bool = True
        ):
            self.model = SentenceTransformer(model_name_or_path, device=device)
            self.batch_size = batch_size
            self.normalise = normalise
            self.device = device
            logger.info(f"Loaded model: {model_name_or_path} on {device}")
    
        def embed(
            self,
            texts: List[str],
            show_progress: bool = True
        ) -> np.ndarray:
            """
            Embed a list of texts. Returns an (N, D) numpy array.
            Normalises to unit length if normalise=True (required for cosine similarity).
            """
            embeddings = self.model.encode(
                texts,
                batch_size=self.batch_size,
                show_progress_bar=show_progress,
                normalize_embeddings=self.normalise,
                convert_to_numpy=True
            )
            logger.info(f"Embedded {len(texts)} texts. "
                        f"Output shape: {embeddings.shape}")
            return embeddings
    
        def embed_query(self, query: str) -> np.ndarray:
            """Embed a single query. Returns a 1D array."""
            return self.embed([query], show_progress=False)[0]
    

    Verificar la longitud de los tokens antes de embeberlos

    Si un fragmento supera el límite de tokens del modelo, se trunca sin previo aviso. En un corpus de políticas, esa truncación silenciosa puede eliminar exactamente la cláusula o el umbral numérico que hizo que ese fragmento valiera la pena recuperar en primer lugar. Validar el conteo de tokens antes del proceso de incrustación permite detectar este problema antes de que llegue a su índice.

    from transformers import AutoTokenizer
    from typing import List, Tuple
    import logging
    
    logger = logging.getLogger(__name__)
    
    def validate_chunk_lengths(
        chunks: List[str],
        model_name: str,
        max_tokens: int,
        truncation_strategy: str = "warn"
    ) -> Tuple[List[str], List[int]]:
        """
        Validate that all chunks are within the model's token limit.
    
        truncation_strategy:
            "warn"  - Log a warning for oversized chunks and include them (will be truncated by model).
            "skip"  - Remove oversized chunks and return only valid ones.
            "raise" - Raise ValueError on the first oversized chunk.
    
        Returns (validated_chunks, oversized_indices).
        """
        tokeniser = AutoTokenizer.from_pretrained(model_name)
        oversized = []
    
        for idx, chunk in enumerate(chunks):
            token_count = len(tokeniser.encode(chunk, add_special_tokens=True))
            if token_count > max_tokens:
                oversized.append(idx)
                msg = (f"Chunk {idx} has {token_count} tokens, "
                       f"exceeds model limit of {max_tokens}. "
                       f"First 80 chars: {chunk[:80]!r}")
                if truncation_strategy == "raise":
                    raise ValueError(msg)
                else:
                    logger.warning(msg)
    
        if truncation_strategy == "skip" and oversized:
            valid = [c for i, c in enumerate(chunks) if i not in set(oversized)]
            logger.info(f"Removed {len(oversized)} oversized chunks. "
                        f"{len(valid)} chunks remain.")
            return valid, oversized
    
        return chunks, oversized
    

    Asociación de metadatos y procedencia a las incrustaciones

    Un vector de incrustación en bruto no es suficiente por sí solo para que un sistema RAG regulado funcione correctamente. Cada vector necesita metadatos estructurados asociados a él, de modo que las etapas de recuperación, reclasificación y generación posteriores puedan determinar la fuente del contenido, aplicar permisos de acceso, restringir los resultados según la jurisdicción y remitir al documento fuente autorizado.

    Volviendo al escenario de la propuesta de crédito de 12 millones de euros, cada fragmento incrustado debe incluir, como mínimo, los campos que se muestran aquí:

    from dataclasses import dataclass, field
    from typing import Optional, List
    import uuid
    
    @dataclass
    class EmbeddedChunk:
        """
        Production embedding record for a banking policy RAG system.
        The vector enables retrieval. The metadata enables everything else.
        """
        # Vector
        vector: List[float]
        vector_dimensions: int
        embedding_model: str
        embedding_model_version: str
    
        # Content
        text: str
        content_type: str          # "narrative", "table_row", "proposition", "image_description"
    
        # Provenance
        document_id: str
        document_version: str      # e.g. "7.2"
        policy_id: Optional[str]   # e.g. "CRD-EU-047"
        jurisdiction: Optional[str] # e.g. "EU"
        effective_date: Optional[str]
    
        # Chunk structure
        chunk_id: str = field(default_factory=lambda: str(uuid.uuid4()))
        parent_id: Optional[str] = None
        section: Optional[str] = None
        page_number: Optional[int] = None
        source_artifact_path: Optional[str] = None  # path to original image/table
    
        # Access control
        classification: str = "INTERNAL"            # "PUBLIC", "INTERNAL", "CONFIDENTIAL"
        permitted_roles: List[str] = field(default_factory=list)
    
        # Indexing
        indexed_at: Optional[str] = None
        indexing_pipeline_version: Optional[str] = None
    

    Aplicar estos metadatos de manera consistente en todas las etapas del proceso, desde la división en fragmentos hasta su incrustación y el indexado vectorial, no solo es una buena práctica. En un contexto bancario regulado, obtener una respuesta técnicamente correcta basada en una versión de política obsoleta se considera un incumplimiento normativo. La función del vector es encontrar el fragmento; la función de los metadatos es confirmar que dicho fragmento proviene de la versión correcta y actual del contenido original.

    Evaluación de la calidad de la incrustación

    Las listas de clasificación públicas como MTEB muestran las puntuaciones generales de recuperación en una amplia gama de conjuntos de datos académicos. Esos números son útiles para descartar modelos cuyo rendimiento es claramente deficiente. Sin embargo, resultan insuficientes cuando la tarea consiste en elegir el mejor modelo para un corpus especializado, como la biblioteca de políticas internas de un banco.

    La única métrica que realmente importa es el rendimiento de un modelo con respecto a sus propios documentos, utilizando sus propias consultas y evaluándolo según los criterios de relevancia que usted mismo haya definido.

    Creación de un conjunto de evaluación para recuperación

    Un conjunto de pruebas de recuperación diseñado para un pipeline de incrustación RAG en banca debe abarcar varios tipos de consultas:

    Búsquedas factuales específicas que se corresponden con un único fragmento fiable, como preguntar con qué frecuencia, como mínimo, deben someterse a revisión anual los clientes corporativos de alto riesgo.

    Preguntas basadas en umbrales que combinan una condición numérica específica con la regla de gobernanza asociada, como preguntar qué autoridad de aprobación se requiere cuando una instalación empresarial de la UE supera los 10 millones de euros.

    Preguntas con múltiples fuentes de información, donde la respuesta completa depende de reunir más de un elemento, como preguntar qué verificaciones deben realizarse antes de poder presentar siquiera una propuesta de crédito empresarial de alto riesgo.

    Preguntas de síntesis que extraen contenido de varias secciones al mismo tiempo, como solicitar una descripción completa del marco de control contra el lavado de dinero que rige los préstamos empresariales de alto riesgo.

    Preguntas entre documentos, relevantes siempre que las políticas se refieren mutuamente en documentos separados.

    import numpy as np
    from typing import List, Dict, Set
    
    def recall_at_k(
        retrieved_ids: List[str],
        relevant_ids: Set[str],
        k: int
    ) -> float:
        """
        Compute Recall@k for a single query.
        relevant_ids is the ground truth set of chunk identifiers.
        retrieved_ids is the ordered list of retrieved chunk identifiers.
        """
        if not relevant_ids:
            return 0.0
        top_k_retrieved = set(retrieved_ids[:k])
        return len(top_k_retrieved & relevant_ids) / len(relevant_ids)
    
    def mean_reciprocal_rank(
        retrieved_ids: List[str],
        relevant_ids: Set[str]
    ) -> float:
        """Compute MRR for a single query."""
        for rank, chunk_id in enumerate(retrieved_ids, start=1):
            if chunk_id in relevant_ids:
                return 1.0 / rank
        return 0.0
    
    def evaluate_embedding_model(
        model_name: str,
        evaluation_queries: List[Dict],
        corpus_chunks: List[Dict],
        k_values: List[int] = [1, 5, 10, 20]
    ) -> Dict:
        """
        Evaluate an embedding model on a labelled retrieval dataset.
    
        evaluation_queries: List of dicts with "query" and "relevant_chunk_ids" keys.
        corpus_chunks: List of dicts with "chunk_id" and "text" keys.
        Returns per-query-type and aggregate retrieval metrics.
        """
        from sentence_transformers import SentenceTransformer
    
        model = SentenceTransformer(model_name)
    
        corpus_texts = [c["text"] for c in corpus_chunks]
        corpus_ids = [c["chunk_id"] for c in corpus_chunks]
        corpus_embeddings = model.encode(corpus_texts, normalize_embeddings=True)
    
        results_by_type: Dict[str, List] = {}
        all_recall: Dict[int, List[float]] = {k: [] for k in k_values}
        all_mrr: List[float] = []
    
        for query_item in evaluation_queries:
            query = query_item["query"]
            relevant = set(query_item["relevant_chunk_ids"])
            query_type = query_item.get("query_type", "unspecified")
    
            query_embedding = model.encode(query, normalize_embeddings=True)
            scores = corpus_embeddings @ query_embedding
            ranked_indices = np.argsort(scores)[::-1]
            retrieved = [corpus_ids[i] for i in ranked_indices]
    
            mrr = mean_reciprocal_rank(retrieved, relevant)
            all_mrr.append(mrr)
    
            for k in k_values:
                r = recall_at_k(retrieved, relevant, k)
                all_recall[k].append(r)
    
            if query_type not in results_by_type:
                results_by_type[query_type] = {"mrr": [], "recall": {k: [] for k in k_values}}
            results_by_type[query_type]["mrr"].append(mrr)
            for k in k_values:
                results_by_type[query_type]["recall"][k].append(
                    recall_at_k(retrieved, relevant, k)
                )
    
        aggregate = {
            "model": model_name,
            "n_queries": len(evaluation_queries),
            "mrr": float(np.mean(all_mrr)),
            "recall": {k: float(np.mean(all_recall[k])) for k in k_values}
        }
    
        per_type = {
            qt: {
                "mrr": float(np.mean(data["mrr"])),
                "recall": {k: float(np.mean(data["recall"][k])) for k in k_values},
                "n_queries": len(data["mrr"])
            }
            for qt, data in results_by_type.items()
        }
    
        return {"aggregate": aggregate, "by_query_type": per_type}
    

    Las métricas de recuperación no deben evaluarse de forma aislada, sin considerar cómo afectan la calidad de la respuesta final. Supongamos que Recall@5 mejora en tres puntos, pero la tasa de fragmentos casi idénticos recuperados aumenta un 30 por ciento: ese equilibrio podría no beneficiar realmente al LLM, ya que proporcionarle tres pasajes casi idénticos en lugar de uno útil no aporta evidencia real.

    El enfoque correcto es evaluar toda la cadena, desde la consulta inicial hasta la respuesta final que se presenta al usuario. El papel de un modelo de embedding se limita a introducir la evidencia adecuada en la ventana de contexto. Si dicha evidencia conduce a una respuesta que sea al mismo tiempo precisa y conforme, eso lo determinan todas las etapas posteriores.

    La pipeline de embedding como infraestructura

    Una vez que un fragmento termina de pasar por el proceso de incrustación, debe salir del otro lado con un vector asociado, metadatos completamente completados y un identificador estable que permita recuperarlo, actualizarlo o eliminarlo posteriormente sin dañar los registros adyacentes en el índice.

    Se trata de una parte de la infraestructura, no de un script único. Los procesos de incrustación de nivel profesional para entornos bancarios regulados requieren lo siguiente:

    • Idempotencia. Si un fragmento se vuelve a incrustar debido al cambio de versiones del modelo, eso debería sobrescribir el registro existente, no generar un duplicado.
  • Rastreo de versiones. Cada vez que cambia el modelo de incrustación, debes reconstruir el índice desde cero o dividirlo de manera clara por versión del modelo. Permitir que los vectores de diferentes modelos coexistan en el mismo índice genera puntuaciones de similitud en las que no se puede confiar.
  • Propagación de los controles de acceso. Si un fragmento fue etiquetado como CONFIDENTIAL durante su procesamiento, esa etiqueta debe mantenerse intacta tras la incrustación y quedar incluida en el índice de vectores. Luego, la capa de recuperación debe respetarla.
  • Observabilidad. Debes registrar la latencia de incrustación por lote, el conteo de tokens, las tasas de error de la API y los fallos a nivel de fragmentos, todo ello con metadatos estructurados adjuntos. Una truncación que modifique silenciosamente el comportamiento de recuperación es exactamente el tipo de problema que tu sistema de monitoreo debe detectar, no algo invisible.
  • Seguimiento de costos. Al incorporar los costos a través de una API, estos se acumulan rápidamente cuando se opera a gran escala. Desglose los gastos por tipo de documento y por ejecución del pipeline, para que pueda evaluar realmente la elección del modelo y la reducción de dimensionalidad en función de costos reales, en lugar de basarse en conjeturas.
  • Nada de esto es opcional una vez que se está en producción. Estas son las características que diferencian un pipeline que solo funciona en una demostración de uno que se puede operar, auditar y mantener a lo largo del tiempo dentro de un entorno regulado.

    Volver a la propuesta de crédito de 12 millones de euros

    La pregunta original del gestor de relaciones no ha cambiado: ¿qué autoridad de aprobación necesita este acuerdo, y qué controles deben cumplirse antes de que pueda ser presentado?

    • La Parte 4 explicó cómo diseñar unidades de recuperación que mantengan esas respuestas intactas en su forma original: la cláusula de política EDD, la fila de la matriz de aprobación que especifica la autoridad del GCC y el diagrama de flujo de trabajo que detalla los controles previos a la presentación.
    • En esta parte, hemos convertido esas unidades de recuperación en vectores buscables, utilizando un modelo que fue probado con terminología específica del sector bancario, ajustado para comprender cómo las abreviaturas regulatorias se relacionan con su contexto de cumplimiento, e integrado junto con metadatos completos de procedencia que permiten confirmar posteriormente la versión de la política y la jurisdicción aplicable.

    Cuando llega la consulta, el índice vectorial localiza la fila de la matriz de aprobación que abarca exposiciones de alto riesgo superiores a 10 millones de euros, la cláusula EDD y el diagrama que muestra la secuencia de control; luego las devuelve junto con metadatos que confirman que los tres se relacionan con la póliza CRD-EU-047, versión 7.2, jurisdicción de la UE, con vigencia a partir del 15 de enero de 2026.

    Lo que llega a la capa de generación son pruebas precisas, completas y rastreables.

    Esa es la diferencia entre un pipeline de incrustación que simplemente carga fragmentos en una base de datos vectorial y uno que conserva todo lo necesario para obtener respuestas fiables y auditables.

    Antes de pasar a la indexación vectorial: una lista de verificación

    Antes de introducir los fragmentos incrustados en el índice vectorial, confirme lo siguiente:

    • ¿Está configurado correctamente el parámetro de tipo de entrada en el modelo de incrustación tanto para las llamadas de indexación como para las de consulta? Esta falta de coincidencia erosiona silenciosamente la precisión en la recuperación de información.
    • ¿Se ha verificado la longitud de los tokens de cada fragmento antes de incrustarlo? La truncación silenciosa modifica lo que realmente representa el texto incrustado, sin que se genere ningún error en ninguna etapa del proceso.
    • ¿Cada registro vectorial incluye toda la información de origen: versión del documento, ID de la política, jurisdicción y fecha de vigencia?
    • ¿Se probó realmente el modelo de incrustación con su propio corpus de dominio y patrones de consulta, en lugar de elegirlo únicamente basándose en las clasificaciones de pruebas públicas?
    • Si ajustó el modelo, ¿dispone de cifras de recuperación antes y después medidas en un conjunto de consultas separado?
  • ¿Están sus decisiones de cuantización respaldadas por resultados provenientes de su propio conjunto de evaluación de recuperación, en lugar de suposiciones tomadas de pruebas de rendimiento publicadas?
  • ¿Es el proceso idempotente, consciente de las versiones y observable, tal como lo exigen los sistemas de producción regulados por estándares?
  • Si falta alguno de estos requisitos, el índice vectorial no lo detectará por usted; almacenará sin problemas todo lo que le proporcione. Nada en la base de datos indicará si un vector fue creado a partir de un fragmento truncado, una consulta con el tipo de entrada incorrecto o un fragmento incluido utilizando una versión del modelo desincronizada con el resto del índice. Esos defectos no se manifiestan de inmediato; reaparecen más tarde como problemas en la calidad de la recuperación que, desde el exterior, parecen ser problemas del LLM.

    En una etapa anterior de esta serie, la Parte 4 mostró cómo convertir documentos limpios en unidades de recuperación manteniendo intacta su estructura. Esta parte ha explicado cómo transformar esas unidades de recuperación en vectores buscables sin alterar su origen.

    A continuación se aborda cómo esos vectores se integran realmente en el índice: búsqueda vectorial densa, recuperación dispersa, enfoques híbridos, algoritmos de vecino más cercano aproximado, y el filtrado de metadatos que determina qué vectores pueden ser considerados para la recuperación desde un principio.

    Lecturas relacionadas

  • Los modelos pequeños especializados están superrando en silencio a las gigantescas LLMs — Descubra cómo un modelo lógico de 3 mil millones de parámetros supera a uno de 120 mil millones en razonamiento formal con hardware común, y por qué la adecuación a la tarea es más importante que el tamaño bruto del modelo.