Etiqueta: RAG

  • Embedding models, vector stores y retrievers

    Embedding models

    Los embedding models son el componente que transforma el contenido obtenido de los document en representaciones vectoriales densas que capturan significado semántico. En sistemas modernos de RAG dentro de LangChain, los embeddings son la base sobre la que se construye todo el sistema de recuperación.

    En términos operativos, un embedding model proyecta texto en un espacio vectorial donde la distancia entre vectores refleja similitud semántica. Esto permite ejecutar búsquedas por similitud (normalmente usando cosine similarity o dot product) en bases de datos vectoriales. La calidad de esta proyección determina directamente la capacidad del sistema para recuperar contexto relevante.

    Relación entre embeddings y organización en el vector store

    Un aspecto crítico que suele pasarse por alto es que los embeddings no se almacenan como “un vector por documento”, sino como un vector por fragmento (chunk). Esto significa que un mismo documento genera múltiples embeddings que se indexan de forma independiente en el vector store.

    En sistemas como Chroma, todos estos vectores se almacenan dentro de una misma colección (índice), independientemente del documento de origen. No existen “tablas separadas por documento”; en su lugar, cada entrada del índice contiene:

    • el embedding
    • el contenido (page_content)
    • la metadata asociada
    • un identificador (ID)

    De forma conceptual:

    [vector, texto, metadata, id]

    Cuando se indexan múltiples documentos, todos sus chunks se mezclan dentro del mismo espacio vectorial:

    ID         | Texto     | Metadata
    -------------------------------------------
    docA_1     | chunk A1  | {source: docA}
    docA_2     | chunk A2  | {source: docA}
    docB_1     | chunk B1  | {source: docB}
    

    Esto implica que la “separación” entre documentos no es física, sino lógica, y se realiza mediante metadata.

    Tipos de embedding models

    En producción, no todos los embeddings son equivalentes; se diferencian por arquitectura, objetivo y rendimiento.

    General-purpose embeddings: Modelos optimizados para tareas generales de similitud semántica como OpenAI embeddings (text-embedding models), se usan en RAG estándar y búsqueda semántica.

    Instruction-tuned embeddings: Modelos ajustados con instrucciones para mejorar tareas específicas (query vs document alignment). Tiene la ventaja de hacer mejor matching entre pregunta y contexto

    Domain-specific embeddings: Entrenados en dominios concretos: legal, médico, código. Esto hace que mejoren el recall en casos especializados

    Multilingual embeddings: permiten búsqueda cruzada entre idiomas.

    Parámetros clave

    • Dimensionalidad: Más dimensiones → más capacidad semántica, pero mayor coste.
    • Normalización: Muchos modelos requieren normalización del vector para usar cosine similarity correctamente.
    • Tokenización: El embedding depende del tokenizer del modelo → afecta cómo se representa el texto.

    Decisiones críticas en producción

    • Query vs Document embeddings: En sistemas avanzados: embeddings distintos para queries y documentos
    • Chunking alignment: El rendimiento depende directamente de cómo has hecho el text splitting un chunk mal definido lleva a un embedding pobre y la consecuencia es un retrieval malo
    • Batch vs real-time:
      • Indexing → los documentos se procesan en batch para generar embeddings de forma eficiente y reducir coste computacional.
      • Queries → las consultas del usuario se embeben en tiempo real para realizar búsqueda semántica inmediata sobre la base vectorial.

    Errores comunes

    • Usar embeddings genéricos en dominios especializados → reduce la precisión semántica porque el modelo no captura bien el vocabulario ni las relaciones propias del dominio.
    • No normalizar vectores → provoca que métricas como cosine similarity den resultados incorrectos o inconsistentes en la búsqueda. (encode_kwargs={"normalize_embeddings": True})
    • Usar chunks demasiado grandes o pequeños → los grandes introducen ruido y los pequeños pierden contexto, degradando el retrieval.
    • No evaluar recall@k → impide medir si el sistema realmente recupera información relevante, ocultando fallos críticos en producción. Ver en retrievers.

    Vector stores

    Los vector stores son el componente encargado de almacenar y consultar embeddings de forma eficiente. En un sistema RAG dentro de LangChain, no basta con generar vectores; es necesario indexarlos en una estructura que permita búsquedas por similitud a gran escala con baja latencia.

    En términos operativos, un vector store gestiona tres elementos: el vector (embedding), el contenido original (texto) y la metadata asociada. A partir de ahí, permite ejecutar consultas semánticas donde una query se transforma en embedding y se compara contra el índice para recuperar los elementos más cercanos.

    Tipos, elección y uso en producción

    La elección del vector store no es trivial; afecta a latencia, escalabilidad, coste y operativa. En la práctica, la decisión se reduce a dónde se ejecuta (local vs gestionado) y qué volumen/latencia necesitas.

    Tipos de vector stores

    Local (on-device): Se ejecutan en la propia máquina, sin dependencias externas. Ideales para prototipos, entornos offline o setups local-first.

    • Ejemplos: Chroma, FAISS
    • Ventajas: cero coste de API, baja latencia local, control total
    • Limitaciones: escalabilidad y concurrencia limitadas

    Gestionados (API / SaaS): Servicios externos optimizados para indexación y búsqueda vectorial a gran escala.

    • Ejemplos: Pinecone, Weaviate (cloud), Qdrant Cloud
    • Ventajas: alta disponibilidad, escalado automático, operaciones gestionadas
    • Limitaciones: coste y dependencia de red

    Híbridos (self-hosted / cloud opcional): Permiten ejecutar localmente o desplegar en servidor propio con capacidades de escalado.

    • Ejemplos: Qdrant, Weaviate (self-hosted)
    • Ventajas: equilibrio entre control y escalabilidad
    • Limitaciones: necesitas gestionar infraestructura

    Comparativa rápida

    TipoEjemploLatenciaEscalabilidadTipoUso recomendado
    LocalChroma, FAISSMuy bajaBajagratisdesarrollo, local RAG
    APIPinecone, Weaviate CloudMediaAltapagoproducción a escala
    HíbridoQdrant, WeaviateBaja–MediaAltavariableproducción self-hosted

    Criterios de elección

    • Usa local (Chroma / FAISS) si: trabajas en desarrollo o laboratorio, necesitas privacidad total y los dataset son pequeños/medio
    • Usa API (Pinecone / Weaviate Cloud) si: necesitas alta disponibilidad, tienes mucho volumen de datos y múltiples usuarios concurrentes
    • Usa híbrido (Qdrant / Weaviate self-hosted) si: quieres control + escalabilidad, despliegas en servidor propio y buscas evitar costes SaaS

    Insight importante

    El vector store no mejora embeddings ni corrige errores de chunking solo acelera la búsqueda; la calidad depende del pipeline previo.

    Ingesta / Indexación (escritura en el vector store)

    La fase de ingesta es donde se construye el índice vectorial a partir de los datos ya preprocesados (cleaning + splitting). En este punto, los textos o documentos se transforman en embeddings y se almacenan junto con su metadata en el vector store.

    Esta operación suele ejecutarse en batch durante el indexing, no en tiempo real, y es crítica porque define la calidad y consistencia del sistema de retrieval: cualquier error aquí (mal chunking, embeddings inconsistentes, falta de metadata) se propagará al resto del pipeline.


    Métodos de ingesta

    MétodoDescripción
    add_textsConvierte textos en embeddings y los añade al índice existente.
    aadd_textsVersión asíncrona de add_texts.
    add_documentsAñade o actualiza documentos (incluyendo metadata) en el vector store.
    aadd_documentsVersión asíncrona de add_documents.
    from_textsCrea un vector store directamente a partir de una lista de textos.
    afrom_textsVersión asíncrona de from_texts.
    from_documentsInicializa un vector store desde documentos estructurados.
    afrom_documentsVersión asíncrona de from_documents.

    Crear vector store desde loaders + splitters

    from langchain_community.vectorstores import Chroma
    from langchain_community.embeddings import HuggingFaceEmbeddings
    
    embeddings = HuggingFaceEmbeddings(
        model_name="BAAI/bge-small-en",
        model_kwargs={"device": "cpu"},
        encode_kwargs={"normalize_embeddings": True}
    )
    
    vectorstore = Chroma.from_documents(
                  chunks, 
                  embedding=embeddings
    )
    

    Insight operativo

    En producción, estos métodos se utilizan en pipelines de indexing offline, donde grandes volúmenes de datos se procesan en batch para evitar latencia y optimizar costes.

    docsearch = Chroma.from_documents(
        chunks,
        embedding=embedding_model,
        persist_directory="./chroma_db"
    )

    Eliminación y gestión de datos

    La gestión de datos en un vector store es fundamental para mantener la coherencia del índice a lo largo del tiempo. A diferencia de bases de datos tradicionales, los embeddings no suelen actualizarse “en sitio”; en la práctica, cualquier cambio en el contenido implica eliminar los vectores antiguos y reindexar los nuevos. Por ello, las operaciones de eliminación no son solo tareas de limpieza, sino una parte crítica del ciclo de vida del dato en sistemas RAG.

    MétodoDescripción
    deleteElimina vectores del índice por ID o mediante condiciones sobre metadata.
    adeleteVersión asíncrona de delete.

    Uso típico: eliminar chunks específicos de un documento.

    vectorstore.delete(ids=["docA_1", "docA_2"])

    Elimina todos los vectores asociados a un documento completo.

    vectorstore.delete(where={"source": "docA"})

    Patrón de actualización (delete + reindex)

    # eliminar versión antigua
    vectorstore.delete(where={"source": "docA"})
    
    # volver a indexar contenido actualizado
    vectorstore.add_texts(
        texts=["nuevo contenido actualizado"],
        metadatas=[{"source": "docA"}]
    )
    

    Acceso directo por ID

    El acceso por ID permite recuperar documentos de forma determinista sin pasar por el proceso de búsqueda semántica. En lugar de calcular similitudes vectoriales, se accede directamente a los elementos almacenados en el índice usando sus identificadores únicos. Esta operación es clave para tareas de trazabilidad y control, ya que permite verificar exactamente qué contenido fue indexado o recuperado en etapas anteriores del pipeline.

    Métodos de acceso

    MétodoDescripción
    get_by_idsRecupera documentos directamente por sus IDs.
    aget_by_idsVersión asíncrona de get_by_ids.

    Recuperar documentos por ID

    docs = vectorstore.get_by_ids(["docA_1", "docB_2"])
    
    for doc in docs:
        print(doc.page_content, doc.metadata)

    Uso en debugging: Permite verificar el origen del documento, metadatos asociados y contenido extacto indexado.

    doc = vectorstore.get_by_ids(["docA_1"])[0]
    
    print(doc.metadata)

    Reconstrucción de contexto: reconstruir un documento original a partir de chunks, auditar resultados del retrieval.

    ids = ["docA_1", "docA_2"]
    
    chunks = vectorstore.get_by_ids(ids)
    
    full_text = " ".join([c.page_content for c in chunks])
    

    Búsqueda semántica (core del RAG)

    La búsqueda semántica es el núcleo de cualquier sistema RAG: es el mecanismo que permite recuperar información relevante a partir de una consulta, no por coincidencia exacta de palabras, sino por similitud en el espacio vectorial. En esta fase, la query se transforma en embedding y se compara contra el índice para encontrar los fragmentos más cercanos. Este proceso implementa, en la práctica, algoritmos de k-nearest neighbors (k-NN) o sus variantes aproximadas (ANN) para escalar a grandes volúmenes de datos.

    Búsqueda básica

    La forma más directa de retrieval es recuperar los documentos más similares a una query.

    MétodoDescripción
    similarity_searchDevuelve los documentos más similares a una query.
    asimilarity_searchVersión asíncrona de similarity_search.
    similarity_search_by_vectorRealiza la búsqueda usando un embedding ya calculado.
    asimilarity_search_by_vectorVersión asíncrona de búsqueda por vector.
    results = vectorstore.similarity_search(
        "What is a text splitter?",
        k=3
    )
    # k muy bajo → pierdes información relevante
    # k muy alto → el LLM recibe ruido
    
    for doc in results:
        print(doc.page_content)
    
    kUso
    1muy preciso, poco contexto
    3estándar
    5–10más contexto, más ruido

    Usar by_vector cuando ya tienes el embedding calculado y quieres optimizar rendimiento.

    query_vector = embeddings.embed_query("What is LCEL?")
    
    results = vectorstore.similarity_search_by_vector(query_vector)
    

    Búsqueda con scoring

    Estas variantes añaden información cuantitativa sobre la similitud, lo que permite evaluar y depurar el comportamiento del retrieval.

    MétodoDescripción
    similarity_search_with_scoreDevuelve documentos junto con su distancia o score técnico.
    asimilarity_search_with_scoreVersión asíncrona.
    similarity_search_with_relevance_scoresDevuelve scores normalizados entre 0 y 1.
    asimilarity_search_with_relevance_scoresVersión asíncrona.

    Se usa para evaluación de calidad (recall, ranking), análisis de relevancia y tuning del sistema

    results = vectorstore.similarity_search_with_relevance_scores(
        "What are embeddings?",
        k=3
    )
    
    for doc, score in results:
        print(score, doc.page_content)

    MMR (Max Marginal Relevance)

    MMR introduce un criterio adicional: no solo busca relevancia respecto a la query, sino también diversidad entre los resultados.

    MétodoDescripción
    max_marginal_relevance_searchSelecciona documentos optimizando relevancia y diversidad.
    amax_marginal_relevance_searchVersión asíncrona.
    max_marginal_relevance_search_by_vectorMMR usando embeddings en lugar de texto.
    amax_marginal_relevance_search_by_vectorVersión asíncrona.

    Ejemplo con: max_marginal_relevance_search. Evita resultados redundantes, mejora la cobertura del contexto y es útil cuando los documentos son similares entre sí.

    results = vectorstore.max_marginal_relevance_search(
        "Explain embeddings",
        k=3
    )

    Método genérico

    El método search permite unificar distintas estrategias bajo una sola interfaz.

    MétodoDescripción
    searchPermite elegir el tipo de búsqueda (similarity, MMR, etc.).
    asearchVersión asíncrona.

    search() permite seleccionar dinámicamente entre similarity, MMR, scoring.

    results = vectorstore.search(
        "What is RAG?",
        search_type="mmr"
    )

    Filtering (restricción por metadata)

    El filtering permite restringir el espacio de búsqueda en el vector store utilizando metadata asociada a cada chunk. A diferencia de la búsqueda semántica, que opera sobre embeddings, los filtros actúan como una condición lógica que limita qué subconjunto de datos se considera antes (o durante) el cálculo de similitud.

    Cómo funciona: Cada chunk indexado incluye metadata:

    Document(
        page_content="...",
        metadata={"source": "docA", "section": "intro"}
    )

    El filtro se aplica en la búsqueda: solo se evalúan los vectores que cumplen esa condición.

    results = vectorstore.similarity_search(
        "text splitting",
        k=3,
        filter={"source": "docA"}
    )

    Uso con retriever

    retriever = vectorstore.as_retriever(
        search_kwargs={
            "k": 3,
            "filter": {"source": "docA"}
        }
    )
    
    docs = retriever.invoke("What is a text splitter?")
    

    Casos de uso

    • separación entre documentos
    • sistemas multi-usuario (multi-tenant)
    • filtrado por secciones (capítulos, páginas)
    • control de contexto en RAG

    Consideraciones

    • Los filtros dependen de la metadata → si no la defines bien, no funcionan
    • No sustituyen la similitud → la complementan
    • Su implementación puede variar según el vector store

    Insight clave

    • embeddings → determinan relevancia
    • filtering → determina contexto

    Ambos son necesarios para un retrieval correcto.

    Atributos

    Los vector stores no solo almacenan datos, sino que también mantienen información sobre los componentes con los que fueron construidos. El atributo más relevante es embeddings, que hace referencia al modelo de embeddings asociado al índice.

    Este atributo permite garantizar que las operaciones de retrieval se realicen con el mismo espacio vectorial con el que se indexaron los datos, evitando inconsistencias difíciles de detectar.

    AtributoDescripción
    embeddingsModelo de embeddings asociado al vector store.
    print(vectorstore.embeddings)

    Para qué se utiliza

    • Consistencia → asegura que las queries se embeben con el mismo modelo usado en la indexación
    • Debugging → permite verificar qué modelo está realmente en uso
    • Auditoría → ayuda a rastrear configuraciones en sistemas complejos

    Caso práctico

    query_vector = vectorstore.embeddings.embed_query("What is RAG?")

    Error común

    Cambiar el modelo de embeddings sin reindexar da como resultado búsquedas incorrectas o incoherentes.

    index → creado con modelo A  
    query → generada con modelo B  

    Retriever

    El método as_retriever transforma el vector store en un componente de alto nivel que encapsula la lógica de búsqueda y lo hace directamente integrable en pipelines de RAG. En lugar de llamar manualmente a métodos como similarity_search, el retriever actúa como una interfaz estándar que recibe una query y devuelve los documentos relevantes, desacoplando la capa de almacenamiento de la lógica del sistema.

    Este patrón es clave en LangChain, ya que permite componer fácilmente pipelines donde el retrieval se integra como una pieza más dentro del flujo de datos hacia el LLM.

    MétodoDescripción
    as_retrieverConvierte el vector store en un retriever configurable para pipelines RAG.

    Ejemplo básico

    retriever = vectorstore.as_retriever(
        search_type="mmr",
        search_kwargs={"k": 3}
    )
    
    docs = retriever.invoke("What is a text splitter?")
    

    Qué está haciendo internamente el retriever: (Es un wrapper sobre similarity_search.)

    1. Recibe la query
    2. La convierte en embedding
    3. Ejecuta búsqueda en el vector store
    4. Devuelve los documentos más relevantes

    Insight estructural

    Aunque la interfaz de VectorStore en LangChain puede parecer extensa, en la práctica se reduce a tres operaciones fundamentales que reflejan el ciclo de vida del dato en un sistema RAG.

    Write (indexing)

    Corresponde a la fase de ingesta, donde los datos se transforman en embeddings y se insertan en el índice vectorial. Se ejecuta normalmente en batch durante el indexing offline.

    • add_* → inserción incremental
    • from_* → construcción inicial del índice

    Read (retrieval)

    Es la fase de consulta, donde se recupera información relevante a partir de una query. Es el núcleo del sistema RAG.

    • similarity_* → búsqueda por similitud
    • mmr_* → búsqueda con diversidad
    • search → interfaz general

    Manage (mantenimiento)

    Permite modificar o inspeccionar el índice. Es clave para actualización, limpieza y debugging.

    • delete → eliminación de vectores
    • get_by_ids → acceso directo por ID

  • Text Splitters

    Los text splitters en LangChain son un componente estructural dentro de cualquier pipeline de RAG: determinan cómo se segmenta el conocimiento antes de ser embebido, indexado y recuperado. En sistemas en producción, esta decisión no es neutra; el splitting define directamente la calidad del retrieval, la densidad semántica de los embeddings y, en última instancia, la precisión del sistema.

    A nivel de arquitectura, el flujo es conocido:

    ingesta → splitting → embeddings → indexación → retrieval

    pero lo relevante es que el splitter introduce la primera transformación irreversible del dato. Cualquier pérdida de coherencia en esta etapa se propaga aguas abajo.

    El punto de abstracción es la interfaz TextSplitter, que define un contrato simple: transformar texto en una lista de fragmentos. Sin embargo, en entornos reales, la elección de implementación no es intercambiable. El uso de CharacterTextSplitter, aunque históricamente común, ha quedado relegado a casos triviales debido a su naturaleza puramente mecánica: corta por longitud fija sin respetar unidades semánticas, lo que degrada la calidad de los embeddings y reduce la efectividad del retrieval.

    El estándar de facto en producción es RecursiveCharacterTextSplitter, precisamente porque introduce una heurística jerárquica que preserva la estructura del lenguaje el mayor tiempo posible. En lugar de imponer cortes arbitrarios, aplica una estrategia de degradación progresiva: intenta dividir por párrafos, luego por saltos de línea, después por frases y, solo cuando no hay alternativa, por caracteres. Este comportamiento minimiza la fragmentación semántica sin renunciar al control sobre el tamaño del chunk.

    from langchain_text_splitters import RecursiveCharacterTextSplitter
    
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200,
        separators=["\n\n", "\n", ".", " ", ""]
    )
    
    chunks = splitter.split_text(text)
    

    Un aspecto clave en todos los splitters es la configuración de chunk_size y chunk_overlap, donde el primero define el tamaño del fragmento y el segundo preserva contexto entre fragmentos; en práctica profesional, valores típicos oscilan entre 500–1500 tokens con un solapamiento del 10–30%.

    Los separators definen la jerarquía de cortes que el algoritmo intenta aplicar en orden, no son simplemente delimitadores; son una estrategia de degradación progresiva. En este caso, conceptualmente, le estamos diciendo a la función: “intenta dividir primero por unidades semánticas grandes; si no puedes cumplir el chunk_size, baja de nivel progresivamente hasta poder hacerlo”

    El algoritmo sigue esta lógica:

    1. Intenta dividir usando "\n\n" (párrafos)
    2. Si los chunks siguen siendo demasiado grandes → usa "\n" (líneas)
    3. Si aún no cabe → usa "." (frases)
    4. Luego " " (palabras)
    5. Finalmente "" (caracteres individuales, último recurso)

    Splitters

    TokenTextSplitter

    Cuando el control fino sobre tokens es crítico (coste o límites del modelo), se utiliza TokenTextSplitter, que trabaja directamente con tokenizadores:

    from langchain_text_splitters import TokenTextSplitter
    
    splitter = TokenTextSplitter(
        chunk_size=512,
        chunk_overlap=50
    )
    

    Internamente se apoya en una abstracción de Tokenizer, y también puedes usar funciones como:

    from langchain_text_splitters import split_text_on_tokens
    

    Esto garantiza que nunca excedas el contexto real del modelo.

    MarkdownHeaderTextSplitter

    Más allá de longitud, LangChain introduce splitters estructurales que respetan formato del documento; por ejemplo, MarkdownHeaderTextSplitter divide en función de headers, manteniendo jerarquía lógica:

    from langchain_text_splitters import MarkdownHeaderTextSplitter
    
    splitter = MarkdownHeaderTextSplitter(
        headers_to_split_on=[("#", "h1"), ("##", "h2")]
    )
    
    docs = splitter.split_text(markdown_text)
    

    Esto permite que cada chunk incluya metadata semántica (sección, subsección, etc.).

    HTMLHeaderTextSplitter

    En HTML, el equivalente es HTMLHeaderTextSplitter, que detecta etiquetas <h1>, <h2>, etc., y genera documentos jerárquicos con metadata asociada; si no encuentra headers, devuelve el documento completo, lo cual lo hace robusto ante inputs inconsistentes.

    from langchain_text_splitters import HTMLHeaderTextSplitter
    
    splitter = HTMLHeaderTextSplitter(
        headers_to_split_on=[
            ("h1", "section"),
            ("h2", "subsection"),
        ]
    )
    
    docs = splitter.split_text(html_string)

    Para casos más avanzados, HTMLSemanticPreservingSplitter mantiene estructura completa (incluyendo links, imágenes o multimedia), y solo recurre a splitting recursivo si se supera el tamaño máximo, priorizando integridad semántica.

    RecursiveJsonSplitter

    Cuando trabajas con datos estructurados, RecursiveJsonSplitter permite dividir JSON preservando jerarquía, lo cual es crítico en agentes que dependen de contexto estructurado:

    from langchain_text_splitters import RecursiveJsonSplitter
    
    splitter = RecursiveJsonSplitter(max_chunk_size=500)
    chunks = splitter.split_json(json_data)
    

    Aquí el objetivo no es solo dividir, sino mantener relaciones entre claves.

    Splitters específicos por lenguaje o dominio

    LangChain también incluye splitters específicos por lenguaje o dominio, lo cual es clave en sistemas profesionales; por ejemplo, PythonCodeTextSplitter divide respetando sintaxis de Python (funciones, clases), mientras que JSFrameworkTextSplitter extiende el splitting recursivo para entender JSX, Vue o Svelte, detectando componentes como separadores naturales:

    from langchain_text_splitters import PythonCodeTextSplitter
    
    splitter = PythonCodeTextSplitter()
    chunks = splitter.split_text(code)
    

    Esto evita romper bloques de código de forma incorrecta.

    Splitters para procesamiento de texto

    En procesamiento lingüístico más avanzado, existen integraciones con NLP clásico como SpacyTextSplitter o NLTKTextSplitter, que segmentan por oraciones usando modelos lingüísticos:

    from langchain_text_splitters import SpacyTextSplitter
    
    splitter = SpacyTextSplitter(pipeline="sentencizer")
    chunks = splitter.split_text(text)
    

    Para casos específicos, también existen splitters especializados como:

    • LatexTextSplitter → divide respetando estructura LaTeX
    • KonlpyTextSplitter → optimizado para coreano
    • SentenceTransformersTokenTextSplitter → alineado con tokenizadores de modelos de embeddings
    • ExperimentalMarkdownSyntaxTextSplitter → mantiene whitespace exacto y extrae metadata avanzada (headers, code blocks, reglas horizontales)

    Text splitters de langchain

    NombreDescripción
    TextSplitterInterfaz base para dividir texto en chunks; define el comportamiento común de todos los splitters.
    CharacterTextSplitterDivide texto por número de caracteres; simple, pero no respeta semántica.
    RecursiveCharacterTextSplitterDivide recursivamente usando separadores jerárquicos (párrafos, líneas, frases); estándar en producción.
    TokenTextSplitterDivide texto en función de tokens usando un tokenizador; útil para controlar límites de modelos.
    SentenceTransformersTokenTextSplitterVariante basada en tokenizadores de modelos de sentence-transformers; optimizada para embeddings.
    SpacyTextSplitterUsa Spacy para segmentar texto en oraciones; más preciso lingüísticamente.
    NLTKTextSplitterSegmenta texto utilizando NLTK; útil para procesamiento basado en frases.
    MarkdownTextSplitterDivide texto siguiendo la estructura Markdown (headers, secciones).
    MarkdownHeaderTextSplitterDivide Markdown basado en headers específicos, generando chunks con metadata jerárquica.
    ExperimentalMarkdownSyntaxTextSplitterSplitter avanzado que preserva formato original, whitespace y extrae metadata (headers, código, reglas).
    HTMLHeaderTextSplitterDivide HTML según etiquetas de encabezado (<h1>, <h2>, etc.), generando estructura jerárquica.
    HTMLSectionSplitterDivide HTML basado en tags y tamaños de fuente; requiere lxml.
    HTMLSemanticPreservingSplitterMantiene estructura semántica HTML completa (links, imágenes, etc.) y solo divide si es necesario.
    RecursiveJsonSplitterDivide JSON en fragmentos manteniendo su estructura jerárquica; útil para datos estructurados.
    PythonCodeTextSplitterDivide código Python respetando su sintaxis (funciones, clases).
    JSFrameworkTextSplitterDivide código de frameworks JS (React, Vue, Svelte) detectando componentes y sintaxis.
    LatexTextSplitterDivide texto respetando estructura de documentos LaTeX.
    KonlpyTextSplitterSplitter especializado para texto en coreano usando la librería KoNLPy.

    Finalmente, en sistemas complejos de agentes construidos con LangGraph, los text splitters no solo afectan al retrieval, sino a la memoria externa del agente; elegir un splitter adecuado implica decidir cómo el agente “percibe” el conocimiento disponible.

  • Data Cleaning en RAG

    En cualquier sistema de RAG, la calidad del resultado final depende directamente de la calidad del dato de entrada. Antes de aplicar técnicas de segmentación, generación de embeddings o recuperación de información, es imprescindible garantizar que el contenido extraído esté correctamente estructurado y libre de ruido.

    En la práctica, los datos rara vez llegan en un formato listo para ser utilizados. Ya provengan de documentos PDF, páginas web u otras fuentes, el contenido suele presentar problemas como fragmentación del texto, elementos irrelevantes o estructuras inconsistentes. Estos problemas no son visibles a simple vista en todos los casos, pero tienen un impacto directo en el rendimiento del sistema.

    Esto significa que, aunque el loader funcione correctamente, el resultado puede ser un texto difícil de utilizar en un sistema RAG como en este ejemplo:

    print(document[6].page_content[:300]) 
    zonas  premium:  cada  metro  valía  (y  vale)  mucho  más  en  Madrid,  Baleares  y  País  
    Vasco
     
    que
     
    en
     
    cualquier
     
    otra
     
    región.
     

    Por tanto, antes de avanzar hacia fases más avanzadas del sistema, es necesario validar y procesar adecuadamente el contenido extraído. Un pipeline sólido no comienza con el modelo, sino con datos bien preparados.

    Antes de generar embeddings, es fundamental aplicar una fase de limpieza del texto. El objetivo es reconstruir, en la medida de lo posible, una estructura natural del lenguaje.

    • Una estrategia básica de limpieza incluye:
    • Eliminación de saltos de línea innecesarios
    • Normalización de espacios
    • Unificación del texto en una secuencia continua

    Este enfoque elimina múltiples espacios, saltos de línea y fragmentaciones simples, devolviendo un texto más coherente. Para casos más complejos, se pueden aplicar reglas adicionales, como la eliminación de patrones repetitivos o la reconstrucción de párrafos.

    Métodos de limpieza de datos tras la carga (post-load)

    Una vez cargados los documentos mediante un loader, el siguiente paso crítico es aplicar técnicas de limpieza que permitan transformar el texto en una forma coherente y útil para el sistema RAG.

    No existe un único método universal. La limpieza debe adaptarse al tipo de fuente (PDF, web, datos estructurados), pero sí existen patrones comunes que se aplican en la mayoría de los casos:

    Normalización básica de espacios

    Eliminar espacios duplicados, saltos de línea y fragmentación simple. Se debe hacer siempre. Es el primer paso obligatorio.

    text = document.page_content
    
    clean_text = " ".join(text.split())

    Soluciona:

    • Saltos de línea (\n)
    • Espacios múltiples
    • Texto fragmentado

    Eliminación explícita de saltos de línea

    Reconstruir frases que han sido cortadas artificialmente. Se utiliza en PDFs con líneas rotas y texto extraído por coordenadas

    text = document.page_content
    
    clean_text = text.replace("\n", " ")
    clean_text = " ".join(clean_text.split())
    

    Eliminación de patrones repetitivos (headers/footers)

    Eliminar contenido repetido en todas las páginas. Como: “Página 1”, “Confidencial”, nombres de empresa, etc. Mejora la calidad del embedding.

    import re
    
    text = document.page_content
    
    clean_text = re.sub(r'Página \d+', '', text)
    clean_text = re.sub(r'Confidencial', '', clean_text)
    

    Filtrado de líneas irrelevantes

    Eliminar líneas demasiado cortas o sin valor semántico como títulos sueltos, fragmentos incompletos y ruido.

    lines = document.page_content.split("\n")
    
    clean_lines = [line for line in lines if len(line.strip()) > 30]
    
    clean_text = " ".join(clean_lines)
    

    Reconstrucción de párrafos

    Unir líneas que pertenecen al mismo párrafo. Esta acción mejora coherencia y contexto para embeddings

    lines = document.page_content.split("\n")
    
    paragraph = ""
    
    for line in lines:
        if line.strip():
            paragraph += line.strip() + " "
        else:
            paragraph += "\n"
    
    clean_text = paragraph
    

    Limpieza con expresiones regulares (regex)

    Eliminar patrones complejos o caracteres no deseados. Usalo en PDFs sucios, OCR y datos con simbolos.

    import re
    
    text = document.page_content
    
    # eliminar espacios múltiples
    text = re.sub(r'\s+', ' ', text)
    
    # eliminar caracteres raros
    text = re.sub(r'[^\w\s.,€%-]', '', text)
    

    Eliminación de contenido irrelevante por secciones

    Eliminar partes completas del documento (ej. índice). Uso en informes largos y documentos estructurados.

    text = document.page_content
    
    if "Índice" in text:
        text = text.split("Introducción")[-1]
    

    Limpieza específica para web (HTML)

    Eliminar etiquetas HTML y contenido no útil. Elimina etiquetas, scripts y navegacion.

    from bs4 import BeautifulSoup
    
    html = document.page_content
    soup = BeautifulSoup(html, "html.parser")
    
    text = soup.get_text()
    
    clean_text = " ".join(text.split())
    

    Pipeline de limpieza básico recomendado

    text = document.page_content
    text = text.replace("\n", " ")
    text = " ".join(text.split())
    

    Versión más robusta

    import re
    
    text = document.page_content
    
    text = text.replace("\n", " ")
    text = re.sub(r'\s+', ' ', text)
    text = re.sub(r'Página \d+', '', text)
    

    La limpieza de datos no es un paso opcional dentro de un sistema RAG, sino una fase crítica que determina la calidad del resto del pipeline.

    Aplicar técnicas básicas como la normalización de espacios o la eliminación de ruido puede mejorar significativamente la coherencia del texto y, por tanto, la precisión del sistema.

    En entornos reales, la combinación de varios métodos de limpieza es la práctica habitual, adaptándose siempre al tipo de documento y a la calidad del contenido extraído.

  • Entorno híbrido (Ollama + DeepSeek) con LangChain.

    Por qué un entorno RAG híbrido (local + API)

    Este entorno se ha diseñado como base de trabajo para realizar prácticas del curso IBM RAG and Agentic AI, pero desvinculando la implementación de la plataforma watsonx de IBM.

    El objetivo no es replicar la herramienta, sino replicar la arquitectura. En el curso, muchos de los ejercicios se apoyan en servicios gestionados en el ecosistema de IBM, lo que facilita el aprendizaje inicial, pero introduce una fuerte dependencia del proveedor. Para un aprendizaje más profundo y aplicable, es necesario trasladar esos mismos conceptos a un entorno abierto y controlado.

    Entorno: Conda + Python

    Se utiliza Anaconda como gestor de entornos por varias razones:

    • aislamiento de dependencias (evita conflictos entre librerías)
    • compatibilidad con librerías de data science (NumPy, Pandas, sklearn)
    • control de versiones reproducible
    • integración natural con notebooks y workflows analíticos

    Framework: LangChain

    Se utiliza LangChain como capa de orquestación:

    • Abstrae el uso del LLM
    • Permite cambiar de modelo sin cambiar el pipeline
    • Integra: loaders, chunking, embeddings, vector stores, chains

    Uso de IA local: Ollama

    Se utiliza Ollama para ejecutar modelos en local.

    VentajasLimitacionesRol en el sistema
    Independencia total de internet
    Coste cero
    Privacidad de datos
    Control completo del entorno
    Rendimiento limitado por hardware
    Modelos más pequeños
    Menor precisión en tareas complejas
    Desarrollo
    Testing
    Validación de arquitectura
    Entornos offline

    Uso de IA en API: DeepSeek

    Se utiliza DeepSeek como modelo en la nube.

    VentajasLimitacionesRol en el sistema
    Mayor calidad de respuesta
    Mejor razonamiento
    Contexto más amplio
    Coste bajo
    Dependencia de red
    Coste (aunque bajo)
    Menor control
    Validación de calidad
    Comparación con modelos locales
    Ejecución en escenarios reales

    Coste aproximado: Para el modelo por defecto que se va a usar (deepseek-chat):

    • Entrada: ~$0.14 – $0.28 por 1 millón de tokens
    • Salida: ~$0.28 – $0.42 por 1 millón de tokens

    Por qué un enfoque híbrido

    El uso combinado de ambos modelos permite:

    1. Separar arquitectura de proveedor. El sistema RAG se diseña una vez y el LLM se cambia según necesidad:
      • mismo pipeline → distinto modelo
    2. Optimizar coste vs rendimiento
      • local → coste 0
      • API → alta calidad cuando es necesario
    3. Comparación y evaluación. Permite evaluar:
      • calidad de respuestas
      • Impacto del modelo en RAG
      • Diferencias entre local y cloud

    Preparación del entorno RAG híbrido

    Creación del entorno (Anaconda)

    Se crea un entorno aislado para evitar conflictos de dependencias. En la consola de conda ejecuta:

    conda create -n rag_env python=3.10 -y
    conda activate rag_env

    Se utiliza Python 3.10 (máxima compatibilidad con LangChain ahora mismo).

    Instalación de dependencias

    En este entorno se combinan Conda y pip para la instalación de dependencias. Aunque mezclar ambos gestores puede generar conflictos si no se hace correctamente, se sigue un patrón controlado que evita problemas.

    Primero se utilizan paquetes instalados con Conda para la base científica (como NumPy, Pandas o Scikit-learn), ya que garantizan compatibilidad a nivel de sistema. A continuación, se emplea pip para instalar librerías más recientes del ecosistema de IA, como LangChain o herramientas de embeddings, que suelen estar más actualizadas fuera de Conda.

    La regla clave es mantener el orden: instalar primero con Conda y después con pip, evitando volver a usar Conda sobre el mismo entorno una vez que pip ha añadido dependencias. De esta forma, se consigue un entorno estable, reproducible y compatible con las necesidades del desarrollo de sistemas RAG.

    Base científica (Conda)

    conda install -c conda-forge numpy pandas scikit-learn -y

    Librerías de IA, Machine Learning, LangChain (pip)

    pip install langchain langchain-community langchain-core
    pip install -U langchain-ollama
    pip install langchain-openai
    pip install faiss-cpu
    pip install sentence-transformers
    pip install pypdf
    pip install python-dotenv
    pip install requests
    pip install ipykernel
    pip install BeautifullSoup4
    pip install chromadb

    Esta combinación evita problemas de compatibilidad.

    Crear proyecto

    1. Crea una carpeta del proyecto.
    2. En el IDE de preferencia crea el proyecto desde la carpeta creada.
    3. Selecciona el entorno creado en Anaconda
    4. Opcional y recomendable iniciar repositorio y conectar con github.
    5. Crear estructura base
    rag-local/
     ├── .env
     ├── main.py
     ├── data/
     └── notebooks/

    Configuración de IA local (Ollama)

    Descargar modelo:

    ollama pull qwen:4b

    Cuando termine la descarga del modelo, ejecuta:

    ollama run qwen:4b

    Si todo fue correcto, te aparece un prompt interactivo y puedes hacer cualquier pregunta para verificar que el modelo está instalado.

    Configuración de IA en API (DeepSeek)

    Visita https://platform.deepseek.com/ inicia sesión y crea una api_key.

    Crear archivo .env

    DEEPSEEK_API_KEY=tu_api_key

    Cargar las variables en main.py

    # Agrega al inicio
    from dotenv import load_dotenv
    import os
    
    # Cargar variables
    load_dotenv()
    api_key = os.getenv("DEEPSEEK_API_KEY")

    Configuración de LangChain

    Conexión a modelo local

    # Agrega al inicio
    from langchain_ollama import OllamaLLM
    
    
    # LLM local
    llm_local = OllamaLLM(model="qwen:4b")

    Conexión a modelo API

    # agregar al inicio
    from langchain_openai import ChatOpenAI
    
    
    # LLM API
    llm_api = ChatOpenAI(
        openai_api_key=api_key,
        openai_api_base="https://api.deepseek.com",
        model="deepseek-chat"
    )

    Selección de modelo

    Se define un selector para poder cambiar de modelo sin modificar el resto del sistema.

    # Selector de modelo
    usar_api = False
    
    # Elegir Modelo
    llm = llm_api if usar_api else llm_local

    Verificación del entorno

    Código mínimo de prueba al final de main.py:

    respuesta = llm.invoke("Explica qué es RAG en 2 líneas")
    print(respuesta)

    El modelo local responde con usar_api = False:

    RAG significa "Remo de Armas" en inglés.

    El modelo con API responde al usar_api = True

    content='RAG (Retrieval-Augmented Generation) es una técnica que combina la recuperación de información relevante desde una base de datos externa con un modelo de lenguaje, permitiendo generar respuestas más precisas y actualizadas sin necesidad de reentrenar el modelo.' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 57, 'prompt_tokens': 15, 'total_tokens': 72, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 15}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_058df29938_prod0820_fp8_kvcache_20260402', 'id': '27107c79-dc76-40ab-b461-32439139c445', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--019dcfd2-0298-7482-b39b-c069b9f5e30a-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 15, 'output_tokens': 57, 'total_tokens': 72, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}

    Las respuestas de un LLM no solo contienen el texto generado (content), sino también metadatos relevantes como el uso de tokens, el modelo utilizado y el estado de finalización. Esta información es fundamental para analizar costes, rendimiento y comportamiento del sistema, especialmente en arquitecturas RAG.

    Creación de un módulo reutilizable

    El objetivo de este módulo es centralizar la configuración de los modelos LLM, permitiendo utilizar indistintamente un modelo local (Ollama) o un modelo en API (DeepSeek), evitando así la duplicación de código en notebooks o scripts.

    En lugar de definir la configuración del modelo en cada punto del proyecto, se encapsula esta lógica en un único módulo reutilizable. Esto facilita el cambio de modelo en cualquier momento y mejora la organización del código.

    Este enfoque resulta especialmente útil en sistemas RAG, donde el LLM forma parte de múltiples componentes del pipeline. Al desacoplar su configuración, se consigue un entorno más flexible, mantenible y preparado para evolucionar sin necesidad de modificar cada parte del sistema.

    Aquí tienes una sección lista para integrar en tu artículo, con enfoque didáctico y alineada con tu entorno híbrido.

    Parámetros de generación en DeepSeek y Ollama

    Los modelos de lenguaje, son configurables desde la API con parametros de generación que determinan el estilo, longitud y comportamiento de las respuestas. En este entorno híbrido (Ollama + DeepSeek), vamos a unificar conceptos para poder experimentar sin fricciones.

    Visita la documentación de la API de cada LLM

    Parámetros comunes

    Estos parámetros existen en ambos sistemas:

    Temperature (temperatura)

    Controla el nivel de aleatoriedad del modelo.

    • 0.0 → 0.3 → respuestas deterministas (más exactas)
    • 0.5 → 0.7 → equilibrio
    • 0.8+ → creatividad alta (más variabilidad)
    Max tokens / longitud de salida

    Limita el tamaño de la respuesta. Los LLM que estamos trabajando utilizan denominaciones diferentes:

    • DeepSeek → max_tokens
    • Ollama → num_predict
    Top-p (nucleus sampling)

    Controla la diversidad de palabras considerando la probabilidad acumulada.

    • 0.1 → 0.3 → respuestas más conservadoras
    • 0.8 → 1.0 → mayor diversidad
    Top-k (solo en Ollama)

    Limita el número de posibles palabras candidatas. DeepSeek no soporta este parámetro.

    • Bajo → más determinista
    • Alto → más diversidad
    Parámetros de penalización

    Solo DeepSeek (API tipo OpenAI) permite controlar repetición:

    • frequency_penalty → evita repetir palabras
    • presence_penalty → fomenta introducir nuevos temas
    Modos de razonamiento (DeepSeek)

    DeepSeek introduce capacidades avanzadas que permiten separar razonamiento interno de respuesta final

    • deepseek-reasoner
    • modo thinking

    Parámetros claves resumidos

    ParámetroDeepSeekOllamaUso recomendado
    temperature✔️✔️siempre
    max_tokens✔️usar como estándar
    num_predict✔️interno (no usar directamente)
    top_p✔️✔️recomendado
    top_k✔️opcional (solo local)
    penalties✔️avanzado
    reasoning mode✔️avanzado

    Agregar los parámetros al módulo

    Para evitar complejidad innecesaria, usamos una interfaz común y luego traducimos internamente:

    • max_tokens → num_predict (Ollama)
    • top_k solo si usamos Ollama

    Implementacion

    Crea en la raíz del proyecto el fichero llm_config.py

    from dotenv import load_dotenv
    import os
    
    from langchain_ollama import OllamaLLM
    from langchain_openai import ChatOpenAI
    
    # cargar variables de entorno
    load_dotenv()
    
    def get_llm(llm="dsk" , params=None):
        """
        Devuelve un modelo LLM configurado.
        
        Parameters:
        - usar_api (bool): si True usa DeepSeek, si False usa Ollama
        
        Returns:
        - instancia de LLM
        """
        
        default_params = {
            "temperature": 1, # Aleatoriedad del modelo.
            "max_tokens": 50, # Longitud de la respuesta en DeepSeek
            "top_p": 1, # diversidad de palabras considerando la probabilidad acumulada 
            "top_k": 40, # Diversidad de palabras considerando la frecuencia 
            "frequency_penalty": 0, # Penalizacion de frecuencia - DeepSeek
            "presence_penalty": 0, # Penalizacion de presencia - DeepSeek
            "repeat_penalty": 1.1 # Penalizacion de repeticion - Ollama
        }
    
        if params:
            default_params.update(params)
        
        if llm == "dsk":
            # DeepSeek
            return ChatOpenAI(
                model="deepseek-chat", 
                openai_api_key=os.getenv("DEEPSEEK_API_KEY"),
                openai_api_base="https://api.deepseek.com",
                temperature=default_params["temperature"],
                max_tokens=default_params["max_tokens"],
                top_p=default_params["top_p"],
                frequency_penalty=default_params["frequency_penalty"],
                presence_penalty=default_params["presence_penalty"]
            )
    
        elif llm == "oll":
            # Ollama
            return OllamaLLM(
                model="qwen:4b",
                temperature=default_params["temperature"],
                num_predict=default_params["max_tokens"],
                top_p=default_params["top_p"],
                top_k=default_params["top_k"],
                repeat_penalty=default_params["repeat_penalty"]
            )

    Test del módulo en un cuaderno Jupyter

    En la carpeta notebooks crea un cuaderno de Jupyter rag_test.ipynb. Antes de poder trabajar en el entorno rag_env debes registrar el kernel en Jupyter desde la terminal.

    Selecciona el kernel del entorno y añade al cuaderno las rutas para añadir la raíz al path,

    import sys
    import os
    
    sys.path.append(os.path.abspath(".."))

    Carga las variables del entorno

    from dotenv import load_dotenv
    load_dotenv()

    Importa la configuración de LLM

    from llm_config import get_llm

    Elige entre modelos cambiando el valor de llm

    llm = get_llm(llm="dsk")

    Realiza el test

    respuesta = llm.invoke("Explica qué es RAG en IA en 2 líneas")
    print(respuesta)

    Con esta configuración se ha establecido un entorno de trabajo completo para el desarrollo de sistemas RAG híbridos, combinando modelos locales y modelos en API dentro de una misma arquitectura.

    A lo largo de este proceso se ha definido una base técnica que permite trabajar de forma independiente a plataformas cerradas, replicando los conceptos del curso IBM RAG and Agentic AI en un entorno abierto, controlado y extensible. La integración de herramientas como LangChain, junto con el uso de modelos locales mediante Ollama y modelos en la nube como DeepSeek, proporciona la flexibilidad necesaria para experimentar, comparar resultados y optimizar el sistema según las necesidades.

    Este entorno no solo permite realizar las prácticas del curso, sino que sienta las bases para desarrollar soluciones reales, donde es posible equilibrar coste, rendimiento y control de los datos.

    A partir de este punto, el siguiente paso consiste en implementar el pipeline RAG completo, donde todas las piezas configuradas comenzarán a trabajar de forma conjunta y se podrá observar el verdadero valor de esta arquitectura.

    Actualización de entorno: modelo de embeddings

    En el entorno actual ya contamos con modelos de lenguaje (LLM) como Qwen o DeepSeek, que están diseñados para generación de texto. Sin embargo, para trabajar con arquitecturas RAG es imprescindible incorporar un segundo tipo de modelo: los modelos de embeddings.

    Estos modelos no generan texto, sino que transforman fragmentos de información en vectores numéricos que representan su significado semántico. Gracias a esto, es posible realizar búsquedas inteligentes, identificar similitudes entre textos y recuperar información relevante de forma eficiente.

    A diferencia de los LLM, los modelos de embeddings suelen ser más ligeros, pero trabajan de forma intensiva con memoria, especialmente cuando se generan vectores de múltiples documentos. En tu caso, según los recursos disponibles (16 GB de RAM ), es importante elegir un modelo que mantenga un buen equilibrio entre rendimiento y consumo.

    Para este entorno, la opción más recomendable es:

    nomic-embed-text

    Este modelo destaca por:

    • Buen rendimiento semántico para RAG
    • Bajo consumo de recursos
    • Ejecución fluida en entornos locales
    • Integración directa con Ollama

    Como alternativa más potente (pero más exigente en recursos) está: mxbai-embed-large
    (ideal si más adelante amplías RAM o trabajas con menos carga simultánea)

    Instalación del modelo de embeddings en Ollama

    Ejecuta en tu terminal:

    ollama pull nomic-embed-text
    Verificación (opcional pero recomendable)
    ollama list

    Deberías ver algo como:

    NAME                       ID              SIZE      MODIFIED
    nomic-embed-text:latest    0a109f422b47    274 MB    2 minutes ago
    qwen:4b                    d53d04290064    2.3 GB    3 days ago

    Separación de responsabilidades en el código

    Siguiendo buenas prácticas, no es recomendable mezclar la configuración de embeddings con la de los modelos generativos. Por ello, se introduce un nuevo módulo específico dentro del proyecto:

    embeddings_config.py

    De esta forma, cada tipo de modelo queda encapsulado en su propia capa, facilitando mantenimiento, escalabilidad y pruebas.

    Implementación del módulo de embeddings

    Se crea el archivo embeddings_config.py con una función que permite instanciar el modelo de embeddings de forma centralizada:

    from langchain_community.embeddings import OllamaEmbeddings
    
    def get_embeddings(provider="oll", model=None):
        """
        Devuelve un modelo de embeddings configurado.
        """
    
        if provider == "oll":
            return OllamaEmbeddings(
                model=model or "nomic-embed-text"
            )
    
        else:
            raise ValueError(f"Proveedor de embeddings no soportado: {provider}")

    Este enfoque permite desacoplar completamente el sistema y facilita futuros cambios, como probar otros modelos de embeddings sin modificar el resto del código.

    Uso dentro del flujo de trabajo

    Una vez definido el módulo, el uso es directo desde cualquier parte del proyecto:

    from config.embeddings_config import get_embeddings
    
    embeddings = get_embeddings()
    
    vector = embeddings.embed_query("¿Qué es RAG?")

    Con esto, ya es posible generar representaciones vectoriales tanto de consultas como de documentos.

    Resultado en la arquitectura híbrida

    Tras esta actualización, el entorno queda estructurado de forma clara:

    Embeddings → Ollama (local)
    
    LLM → DeepSeek / Ollama

    Esta separación es fundamental y refleja cómo se diseñan los sistemas RAG en entornos reales de producción, donde cada componente cumple una función específica dentro del pipeline.