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.
| Ventajas | Limitaciones | Rol 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.
| Ventajas | Limitaciones | Rol 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:
- Separar arquitectura de proveedor. El sistema RAG se diseña una vez y el LLM se cambia según necesidad:
- mismo pipeline → distinto modelo
- Optimizar coste vs rendimiento
- local → coste 0
- API → alta calidad cuando es necesario
- 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_envSe 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 -yLibrerí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 chromadbEsta combinación evita problemas de compatibilidad.
Crear proyecto
- Crea una carpeta del proyecto.
- En el IDE de preferencia crea el proyecto desde la carpeta creada.
- Selecciona el entorno creado en Anaconda
- Opcional y recomendable iniciar repositorio y conectar con github.
- Crear estructura base
rag-local/
├── .env
├── main.py
├── data/
└── notebooks/Configuración de IA local (Ollama)
Descargar modelo:
- Visita https://ollama.com/download, descarga y ejecuta el instalador.
- Ejecutar en la terminal (PowerShell) en este caso:
ollama pull qwen:4bCuando termine la descarga del modelo, ejecuta:
ollama run qwen:4bSi 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_keyCargar 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_localVerificació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
- Ollama: https://ollama.readthedocs.io/en/modelfile/#valid-parameters-and-values
- DeepSeek: https://api-docs.deepseek.com/api/create-chat-completion
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→ equilibrio0.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 conservadoras0.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 palabraspresence_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ámetro | DeepSeek | Ollama | Uso 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_ksolo 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_llmElige 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-textVerificación (opcional pero recomendable)
ollama listDeberí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 agoSeparació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.pyDe 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 / OllamaEsta 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.