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.