
Seguro que te ha pasado alguna vez. Estás tranquilamente delante de la pantalla charlando con tu modelo de lenguaje favorito, necesitas trabajar con un documento personal o un informe confidencial de tu empresa y te surge ese escalofrío en la espalda. ¿De verdad vas a subir ese archivo con datos privados, cuentas o información estratégica a un servidor externo propiedad de una multinacional? ¿Quién te asegura que esos datos no acabarán alimentando el próximo entrenamiento de su modelo público? Además, si le preguntas a esa inteligencia artificial sobre un correo electrónico que te envió tu pareja hace diez días, o sobre un manual de instrucciones de un aparato hiperespecífico que compraste la semana pasada, lo más probable es que falle estrepitosamente. En el mejor de los casos te dirá que no tiene acceso a esa información. En el peor, empezará a inventarse datos con una seguridad tan aplastante que te lo creerás a pies juntillas hasta que lo verifiques por ti mismo. Esto es lo que en el mundillo llamamos alucinaciones.
Hoy vengo a traerte la solución definitiva a todos estos dolores de cabeza. Nos vamos a meter de lleno en el barro del cacharreo del bueno para explicarte cómo puedes exprimir todos tus documentos en tu propio servidor doméstico, de forma totalmente privada, gratuita y local. Te voy a enseñar a construir un RAG casero.
El problema original: Las costuras de los modelos de lenguaje
Un modelo de lenguaje, por muy potente y asombroso que parezca, tiene dos grandes límites físicos que determinan su comportamiento: su fecha de corte de entrenamiento y la falta de acceso a tus datos privados.
Cuando un modelo de lenguaje se entrena, se le suministra una cantidad ingente de información de internet. Pero ese proceso de entrenamiento requiere de un tiempo y un coste monumental. Una vez finalizado el entrenamiento, el modelo se queda congelado en el tiempo. Todo lo que ocurra un día después de esa fecha límite sencillamente no existe para él. A menos que utilicemos herramientas externas para conectarlo a la red, el modelo estará ciego ante la actualidad.
Por otro lado, están tus datos privados. Es físicamente imposible que un modelo público haya visto tus facturas, tus notas de estudio, tus manuales de configuración de tu servidor o tus diarios personales.
Para solucionar esto, muchos usuarios recurren a la opción de subir un documento directamente en la interfaz web de servicios comerciales. Pero esta solución es limitada por dos razones evidentes: no puedes subir una biblioteca de cien o mil archivos de golpe, y estás regalando tu privacidad en bandeja de plata.
Fine-Tuning frente a RAG: El combate definitivo
Para solventar estas limitaciones de conocimiento y dotar a nuestro asistente local de una base de conocimiento personalizada, la tecnología nos ofrece dos caminos muy distintos: el fine-tuning y el RAG.
El fine-tuning consiste en coger un modelo ya entrenado y someterlo a un reentrenamiento con un conjunto de datos específico que nosotros le proporcionemos. Aunque sobre el papel suena muy bonito, en el entorno doméstico es un auténtico dolor de cabeza por las siguientes razones:
- El coste de computación: Reentrenar un modelo, incluso uno pequeño, requiere de una potencia de cálculo brutal. Necesitas tarjetas gráficas con muchísima memoria de video (VRAM) que son caras y consumen mucha energía.
- La velocidad de actualización: Si tus datos cambian a diario (por ejemplo, si añades una nueva factura o modificas un tutorial en tu servidor), tendrías que volver a reentrenar el modelo todos los días. Esto es inviable.
- La fuga de privacidad: Si le prestas tu modelo reentrenado a un amigo o un compañero de trabajo, los datos con los que lo entrenaste van incrustados dentro del propio modelo. Si esa persona sabe cómo interrogarlo, podría extraer toda tu información confidencial.
- La precisión basada en memoria: El fine-tuning hace que el modelo intente memorizar los nuevos conceptos. Sin embargo, no garantiza que responda basándose en datos reales, por lo que sigue alucinando con relativa facilidad.
Frente a este titán complejo y costoso, emerge el RAG (Retrieval-Augmented Generation), que podríamos traducir como generación aumentada por recuperación.
El RAG es una técnica increíblemente inteligente y dinámica. No altera para nada los pesos del modelo de lenguaje; simplemente actúa como un intermediario que busca en tiempo real en una base de datos local los trozos de texto que contienen la respuesta a tu pregunta, los copia y se los entrega al modelo junto con tu consulta. Es el equivalente a presentarte a un examen con el libro abierto encima de la mesa: no necesitas memorizarlo todo, solo tienes que saber buscar la página correcta en cada momento.
¿Qué demonios es un RAG?
Para que se lo puedas explicar a tu abuela, un RAG es un sistema de búsqueda hipervitaminado. Imagina que tienes una biblioteca en tu casa con miles de papeles sobre cómo configurar tus contenedores con Podman. En lugar de leerte todos los papeles cada vez que tienes una duda sobre cómo configurar un volumen, tienes un asistente personal muy rápido que va corriendo a la estantería, saca los tres párrafos exactos donde se habla de esa configuración de volúmenes, te los lee en voz alta y luego tú redactas tu respuesta.
Eso es exactamente un RAG. El modelo de lenguaje se limita a leer la información que nuestro sistema de búsqueda ha rescatado de la base de datos local y redacta una respuesta coherente basándose única y exclusivamente en esos hechos reales. Si la información no está en tus papeles, el modelo te dirá educadamente que no lo sabe, evitando por completo inventarse historias inverosímiles.
Mi experiencia personal con modelos locales pequeños, como Llama 3.2 de tan solo 3 mil millones de parámetros (3B), es que combinados con un buen sistema de RAG local ofrecen respuestas infinitamente más precisas y útiles que los modelos más gigantescos del mercado cuando se les pregunta sin contexto. No es magia, es simplemente tener la información adecuada en el momento preciso.
La gran tubería: Anatomía de un RAG casero
Para construir este sistema en casa, necesitamos implementar lo que en ingeniería de datos llamamos un pipeline o tubería de procesamiento. Esta tubería consta de cinco etapas consecutivas:
1. La Ingesta de datos
El primer paso es recopilar toda nuestra información. El sistema debe ser capaz de leer diferentes formatos de archivo como documentos PDF, archivos de texto plano, páginas HTML o notas en formato Markdown.
Particularmente, yo me lo paso pipa trabajando con Markdown. ¿Por qué? Porque es un formato de texto estructurado limpísimo, que no contiene elementos extraños de formato ocultos y que resulta sumamente cómodo de procesar mediante scripts automáticos. Si tus documentos están en Markdown, la calidad de la lectura será impecable desde el primer minuto.
2. El troceado del texto (Chunking)
Los modelos de lenguaje tienen un límite en la cantidad de texto que pueden procesar de una sola vez (la ventana de contexto). Por lo tanto, no podemos pasarle un libro de mil páginas de golpe. Debemos trocear el documento original en porciones pequeñas, que solemos llamar chunks, de entre 500 y 1000 tokens (un token equivale aproximadamente a cuatro caracteres en español).
Encontrar el equilibrio en el tamaño del fragmento es todo un arte. Si haces los fragmentos demasiado pequeños, el modelo no tendrá suficiente contexto para entender de qué se habla. Si los haces demasiado grandes, introducirás mucho ruido innecesario y ralentizarás el tiempo de respuesta. Además, para evitar que una frase importante quede partida por la mitad al final de un fragmento, solemos configurar un pequeño solapamiento entre un trozo y el siguiente.
3. Los embeddings: La traducción matemática
¿Cómo sabe el sistema informático qué fragmentos de texto se parecen a la pregunta del usuario? Aquí es donde entra en juego la magia de los embeddings.
Un embedding es un vector matemático (una serie muy larga de números decimales, por ejemplo, 1024 o 1536 números continuos) que captura el significado semántico o conceptual de un fragmento de texto. Gracias a esto, si el usuario busca algo sobre mantenimiento de vehículos, el sistema sabrá asociarlo de forma conceptual con palabras como coche, aceite o motor, aunque esas palabras exactas no aparezcan en la consulta.
Con Ollama, que podemos instalar cómodamente en nuestro propio equipo de forma local, disponemos de modelos de embeddings excelentes y listos para usar sin pagar un céntimo:
- Nomic Embed Text: Un modelo de propósito general muy rápido y eficiente.
- MXBai Embed Large: Un modelo que ofrece una mayor precisión conceptual pero que requiere un poco más de potencia.
- BGE M3: Un modelo multilingüe excelente, ideal para textos en español, que es precisamente el que utilizo yo en mi día a día.
4. La Base de Datos Vectorial
Una vez transformados nuestros fragmentos de texto en vectores matemáticos, necesitamos guardarlos en una base de datos optimizada que permita buscar similitudes entre estos vectores de forma ultrarrápida. Aunque existen bases de datos diseñadas exclusivamente para este propósito, a mí me gusta utilizar PostgreSQL junto con la extensión pgvector.
Utilizar PostgreSQL tiene una ventaja indudable: es un motor de base de datos robusto, maduro, hiperprobado y del que ya conocemos prácticamente todos sus secretos. Al añadirle la extensión pgvector, convertimos nuestra base de datos tradicional en una base de datos vectorial de primerísimo nivel sin necesidad de añadir software extraño o experimental a nuestra infraestructura de red.
5. La recuperación y generación
Cuando haces una pregunta al sistema, el proceso fluye a la inversa de forma instantánea:
- El sistema convierte tu pregunta en un vector utilizando el mismo modelo de embeddings.
- Busca en la base de datos PostgreSQL los vectores de texto que estén más cerca geométricamente del vector de tu pregunta.
- Recupera los fragmentos de texto originales asociados a esos vectores coincidentes.
- Inserta esos trozos de texto dentro del prompt que le vamos a enviar a nuestro modelo de lenguaje local como contexto de referencia.
- El modelo de lenguaje redacta la respuesta definitiva basándose únicamente en ese contexto que le hemos inyectado.
El Stack Tecnológico Local: Montando nuestra infraestructura
Para levantar este proyecto en nuestro propio servidor o equipo local, vamos a apostar por un despliegue limpio y mantenible utilizando contenedores. Aunque muchos optan por Docker Compose, yo prefiero hacer uso de Podman con sus fantásticos quadlets, que nos proporcionan un control total sobre el ciclo de vida de nuestros contenedores integrándolos directamente con el sistema de inicio de Linux (systemd).
Aquí tienes la configuración detallada de un archivo docker-compose.yml para levantar todo el entorno de forma unificada si prefieres esta vía rápida de despliegue:
version: '3.8'
services:
postgres:
image: ankane/pgvector:latest
container_name: postgres_vector
environment:
POSTGRES_DB: rag_database
POSTGRES_USER: lorenzo
POSTGRES_PASSWORD: mi_super_contrasenia_privada
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
ollama:
image: ollama/ollama:latest
container_name: ollama_local
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
restart: unless-stopped
volumes:
pgdata:
ollama_data:
Si por el contrario eres de los míos y prefieres la elegancia y seguridad de Podman con quadlets, podemos definir los servicios de forma independiente creando los archivos de configuración en tu directorio de systemd local.
Por ejemplo, creamos el archivo postgres.container en el directorio ~/.config/containers/systemd/:
[Unit]
Description=Contenedor de PostgreSQL con soporte pgvector
After=network-online.target
[Container]
Image=ankane/pgvector:latest
ContainerName=postgres_vector
Environment=POSTGRES_DB=rag_database POSTGRES_USER=lorenzo POSTGRES_PASSWORD=mi_super_contrasenia_privada
PublishPort=5432:5432
Volume=postgres_vector_data:/var/lib/postgresql/data:Z
[Service]
Restart=always
[Install]
WantedBy=default.target
Y de igual manera, creamos el archivo ollama.container en el mismo directorio:
[Unit]
Description=Contenedor de Ollama para ejecucion de modelos locales
After=network-online.target
[Container]
Image=ollama/ollama:latest
ContainerName=ollama_local
PublishPort=11434:11434
Volume=ollama_local_data:/root/.ollama:Z
[Service]
Restart=always
[Install]
WantedBy=default.target
Una vez creados estos archivos, simplemente le indicamos a systemd que recargue su configuración y arrancamos nuestros nuevos y relucientes servicios de forma nativa:
systemctl --user daemon-reload
systemctl --user start postgres.service
systemctl --user start ollama.service
¡Listo! Ya tenemos toda la maquinaria de backend corriendo alegremente en nuestro servidor local de forma segura y aislada.
Para descargar el modelo de embeddings que utilizaremos, ejecutamos un comando rápido dentro del contenedor de Ollama:
podman exec -it ollama_local ollama pull bge-m3
Y para descargar el modelo de lenguaje de nuestra elección (por ejemplo, el ágil y veloz Llama 3.2):
podman exec -it ollama_local ollama pull llama3.2:3b
Configuración Detallada de la Base de Datos
Con los servicios en marcha, debemos preparar la estructura de datos que alojará los fragmentos de texto y sus representaciones matemáticas vectoriales. Nos conectamos a nuestra base de datos PostgreSQL y ejecutamos las siguientes sentencias SQL para activar la extensión vectorial y crear la tabla de almacenamiento:
-- Activamos la extension pgvector en la base de datos
CREATE EXTENSION IF NOT EXISTS vector;
-- Creamos la tabla que contendra nuestros documentos troceados
CREATE TABLE IF NOT EXISTS documentos_rag (
id SERIAL PRIMARY KEY,
nombre_archivo VARCHAR(255) NOT NULL,
fragmento_texto TEXT NOT NULL,
vector_embedding VECTOR(1024) -- 1024 es la dimension del modelo BGE-M3
);
Tipos de índice: ¿Flat o HNSW?
A la hora de buscar similitudes entre vectores en bases de datos muy grandes, la velocidad puede resentirse si no configuramos un índice adecuado. Disponemos de dos opciones de indexación principales en pgvector:
- Flat (Índice Plano): No realiza ninguna aproximación; busca comparando tu pregunta con absolutamente todos los registros de la base de datos de forma exacta. Es extremadamente preciso (no pierde ninguna coincidencia) pero resulta lento en bases de datos masivas. Para proyectos domésticos pequeños de unos pocos miles de fragmentos de texto, es más que suficiente.
- HNSW (Hierarchical Navigable Small World): Es un algoritmo avanzado que crea un grafo multicapa de caminos rápidos para buscar el vector más cercano de forma aproximada. Es sumamente veloz, incluso con millones de registros, aunque penaliza ligeramente la precisión de la búsqueda.
Si decides crear un índice del tipo HNSW para acelerar las consultas cuando tu biblioteca personal empiece a engordar considerablemente, puedes ejecutar la siguiente consulta:
CREATE INDEX ON documentos_rag USING hnsw (vector_embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
El arte del Chunking en Python
Ahora que nuestra infraestructura está lista, vamos a programar el script de Python que se encargará de leer un documento de texto, trocearlo de forma inteligente y guardar los fragmentos y sus vectores en nuestra base de datos.
Para este script, haremos uso de la biblioteca de Python llamada langchain_text_splitters, que nos proporciona un control exquisito sobre cómo partir los textos de forma recursiva respetando la integridad de los párrafos y las oraciones:
import os
import psycopg2
import requests
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Configuracion de conexion a Postgres y Ollama
DB_CONN = "dbname=rag_database user=lorenzo password=mi_super_contrasenia_privada host=localhost port=5432"
OLLAMA_API_URL = "http://localhost:11434/api/embeddings"
MODELO_EMBEDDING = "bge-m3"
def obtener_embedding(texto):
"""Llama a la API local de Ollama para obtener el vector del texto."""
payload = {
"model": MODELO_EMBEDDING,
"prompt": texto
}
respuesta = requests.post(OLLAMA_API_URL, json=payload)
respuesta.raise_for_status()
return respuesta.json()["embedding"]
def procesar_e_ingestar_archivo(ruta_archivo):
"""Lee un archivo, lo trocea y guarda los fragmentos con su vector en Postgres."""
with open(ruta_archivo, "r", encoding="utf-8") as f:
contenido = f.read()
# Configuramos el partidor recursivo de texto
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, # Tamano del trozo en caracteres aproximados
chunk_overlap=100, # Solapamiento para no perder el hilo entre fragmentos
length_function=len
)
trozos = splitter.split_text(contenido)
nombre_archivo = os.path.basename(ruta_archivo)
conexion = psycopg2.connect(DB_CONN)
cursor = conexion.cursor()
try:
for i, trozo in enumerate(trozos):
print(f"Procesando fragmento {i+1} de {len(trozos)} para {nombre_archivo}...")
# Generamos el vector conceptual del trozo
vector = obtener_embedding(trozo)
# Insertamos en la base de datos
cursor.execute(
"INSERT INTO documentos_rag (nombre_archivo, fragmento_texto, vector_embedding) VALUES (%s, %s, %s)",
(nombre_archivo, trozo, vector)
)
conexion.commit()
print("¡Ingesta de datos finalizada con exito!")
except Exception as e:
conexion.rollback()
print(f"Error durante la ingesta: {e}")
finally:
cursor.close()
conexion.close()
# Ejemplo de ejecucion con una nota personal
if __name__ == "__main__":
# Asegurate de tener un archivo de prueba llamado mi_servidor.md
procesar_e_ingestar_archivo("mi_servidor.md")
Este sencillo script lee el archivo, calcula los embeddings localmente pidiéndoselos a Ollama y los inyecta en nuestra base de datos PostgreSQL de forma totalmente automatizada. ¡Una auténtica gozada!
Búsqueda Híbrida: Cuando la semántica no es suficiente
La búsqueda semántica a través de vectores es una maravilla de la tecnología moderna, pero tiene un punto débil muy claro. Si estás buscando una cadena exacta muy concreta, como por ejemplo un identificador de error informático, una clave alfanumérica o el nombre preciso de un parámetro de configuración como Error 503, la búsqueda de similitud de vectores se puede quedar un poco corta e intentar buscar conceptos vagamente parecidos en lugar del término exacto.
Para solucionar esto de raíz, los profesionales de la informática apostamos por la búsqueda híbrida. Esta técnica consiste en realizar dos consultas simultáneas en la base de datos:
- Una búsqueda semántica pura por proximidad vectorial.
- Una búsqueda textual clásica de coincidencia exacta de términos.
Posteriormente, combinamos ambos resultados asignándoles un peso proporcional ajustable (por ejemplo, un 70% de importancia al resultado semántico y un 30% al resultado puramente textual). Esto nos garantiza que obtendremos siempre la información idónea sin importar cómo formulemos la pregunta.
El superpoder de ParadeDB
Para implementar esta búsqueda textual de primer nivel dentro de PostgreSQL sin salir de nuestra base de datos preferida, podemos hacer uso de una extensión fantástica llamada ParadeDB.
ParadeDB integra directamente dentro de PostgreSQL el algoritmo de búsqueda BM25, que es exactamente el mismo motor probabilístico que utiliza soluciones gigantescas del mercado como Elasticsearch. Al integrarse como un índice tradicional más de PostgreSQL, podemos realizar búsquedas textuales de altísimo rendimiento y combinarlas con las capacidades vectoriales de pgvector en una única consulta SQL limpia y elegante.
Si estás empezando, no te vuelvas loco queriendo instalar todo esto de golpe el primer día. Mi consejo es que comiences con una instalación básica de PostgreSQL y pgvector, y a medida que tu sistema crezca y veas que necesitas afinar las búsquedas textuales exactas, le añadas de forma progresiva las capacidades de la extensión de búsqueda textual integrada de Postgres o de ParadeDB. No se trata de complicarse la vida desde el inicio, sino de ir evolucionando tu solución poco a poco a medida que lo necesites.
El Re-ranking: El filtro de la excelencia
Cuando hacemos una consulta a nuestra base de datos vectorial, esta suele devolvernos un puñado amplio de fragmentos (por ejemplo, los 10 o 15 mejores trozos que se parezcan a la consulta). Sin embargo, introducir demasiados fragmentos en el prompt de nuestro modelo local no solo ralentizará la generación de la respuesta, sino que puede saturar su memoria de contexto.
Aquí es donde entra en juego la etapa del re-ranking (o reordenación). Un modelo de re-ranking es una pequeña inteligencia artificial especializada que analiza de forma pormenorizada los fragmentos recuperados de la base de datos junto con la consulta del usuario y los puntúa de nuevo, ordenándolos con precisión milimétrica de mayor a menor relevancia real.
Gracias a este paso adicional de refinamiento, podemos quedarnos únicamente con los 3 o 4 trozos con mayor puntuación de coincidencia conceptual y descartar el resto de fragmentos secundarios. El modelo de lenguaje recibirá un contexto depurado y responderá de forma impecable y directa.
La Interfaz de Usuario: Programando tu propia web con Streamlit
Tener el sistema funcionando en la terminal de Linux está genial, pero para que toda la familia pueda exprimir el sistema RAG o para utilizarlo de forma cómoda en tu día a día, lo mejor es dotarlo de una interfaz visual atractiva.
Con la librería de Python llamada Streamlit, podemos diseñar y poner en marcha una aplicación web completa y totalmente funcional en apenas unas pocas líneas de código.
A continuación te presento un script completo para interactuar con tu RAG local a través de una interfaz de chat web preciosa:
import streamlit as st
import psycopg2
import requests
# Configuracion del sistema local
DB_CONN = "dbname=rag_database user=lorenzo password=mi_super_contrasenia_privada host=localhost port=5432"
OLLAMA_API_EMB = "http://localhost:11434/api/embeddings"
OLLAMA_API_CHAT = "http://localhost:11434/api/chat"
MODELO_EMBEDDING = "bge-m3"
MODELO_LLM = "llama3.2:3b"
def obtener_embedding(texto):
"""Genera el vector de la consulta utilizando Ollama."""
payload = {"model": MODELO_EMBEDDING, "prompt": texto}
res = requests.post(OLLAMA_API_EMB, json=payload)
res.raise_for_status()
return res.json()["embedding"]
def buscar_contexto(pregunta, limite=3):
"""Busca los fragmentos mas similares en nuestra base de datos Postgres."""
vector_pregunta = obtener_embedding(pregunta)
conexion = psycopg2.connect(DB_CONN)
cursor = conexion.cursor()
# Realizamos la consulta de distancia del coseno utilizando pgvector
consulta_sql = """
SELECT fragmento_texto, nombre_archivo, (vector_embedding <=> %s::vector) AS distancia
FROM documentos_rag
ORDER BY distancia ASC
LIMIT %s;
"""
cursor.execute(consulta_sql, (vector_pregunta, limite))
resultados = cursor.fetchall()
cursor.close()
conexion.close()
return resultados
def consultar_llm(pregunta, contexto):
"""Envia la pregunta y el contexto a nuestro modelo local de Ollama."""
contexto_unificado = "\n\n".join([f"[Fuente: {res[1]}]\n{res[0]}" for res in contexto])
# Redactamos un prompt estricto para evitar alucinaciones
prompt_sistema = (
"Eres un asistente local estrictamente veraz. "
"Responde a la pregunta del usuario utilizando unicamente el contexto proporcionado a continuacion. "
"Si el contexto no contiene la respuesta, di claramente que no dispones de esa informacion, "
"pero no intentes inventar nada bajo ningun concepto.\n\n"
f"CONTEXTO DE REFERENCIA:\n{contexto_unificado}"
)
payload = {
"model": MODELO_LLM,
"messages": [
{"role": "system", "content": prompt_sistema},
{"role": "user", "content": pregunta}
],
"stream": False
}
res = requests.post(OLLAMA_API_CHAT, json=payload)
res.raise_for_status()
return res.json()["message"]["content"]
# Interfaz Grafica con Streamlit
st.set_page_config(page_title="Mi RAG Local y Privado", page_icon="🤖")
st.title("🤖 Mi Cerebro Local y Privado")
st.subheader("Haz preguntas a tus documentos personales de forma totalmente segura")
# Historial de chat en memoria de sesion
if "historial" not in st.session_state:
st.session_state.historial = []
# Mostramos el historial
for mensaje in st.session_state.historial:
with st.chat_message(mensaje["rol"]):
st.write(mensaje["contenido"])
# Entrada del usuario
if pregunta_usuario := st.chat_input("Escribe aqui tu pregunta..."):
# Mostramos la pregunta del usuario
with st.chat_message("user"):
st.write(pregunta_usuario)
st.session_state.historial.append({"rol": "user", "contenido": pregunta_usuario})
with st.spinner("Buscando en tus documentos locales..."):
# 1. Buscamos el contexto relevante en la base de datos vectorial
contexto_encontrado = buscar_contexto(pregunta_usuario)
# 2. Generamos la respuesta apoyandonos en Ollama
respuesta_ia = consultar_llm(pregunta_usuario, contexto_encontrado)
# Mostramos la respuesta de la IA
with st.chat_message("assistant"):
st.write(respuesta_ia)
# Mostramos de forma elegante de que documentos hemos extraido la informacion
with st.expander("Ver fuentes consultadas"):
for res in contexto_encontrado:
st.write(f"**Archivo:** *{res[1]}* (Distancia: *{res[2]:.4f}*)")
st.write(f"_{res[0]}_")
st.divider()
st.session_state.historial.append({"rol": "assistant", "contenido": respuesta_ia})
Para arrancar esta maravillosa interfaz web interactiva en tu navegador, tan solo tienes que guardar el código anterior en un archivo llamado app.py y ejecutar este comando en tu terminal de Linux:
streamlit run app.py
Automáticamente se abrirá una pestaña en tu navegador web donde podrás empezar a chatear con tus documentos de forma fluida, privada y visualmente impecable.
La alternativa sin código: OpenWeb UI
Si no te apetece ponerte a escribir código en Python o prefieres una herramienta que ya venga lista para usar de fábrica, te recomiendo encarecidamente que le eches un vistazo a OpenWeb UI.
Como ya te he comentado en episodios y artículos anteriores del blog, OpenWeb UI es una interfaz de usuario web espectacular, muy parecida a la de ChatGPT, diseñada específicamente para conectarse a tus instancias locales de Ollama de forma nativa. Lo mejor de todo es que OpenWeb UI ya incorpora un sistema de RAG totalmente integrado en su interfaz web.
La experiencia con OpenWeb UI es sumamente sencilla:
- Te descargas tus contenedores y accedes a la web.
- Subes tus archivos PDFs o notas directamente arrastrándolos sobre la pantalla de chat de la aplicación.
- Utilizas etiquetas rápidas en la caja de texto para indicarle a la IA que responda basándose en un documento concreto de tu biblioteca.
Ventajas y desventajas de OpenWeb UI
Esta aproximación sin código tiene cosas maravillosas pero también algunas pegas que debes conocer antes de decantarte por ella:
- Las ventajas: Tienes una interfaz de usuario pulida a nivel profesional, una excelente gestión integrada de múltiples usuarios, la posibilidad de cambiar de modelo de lenguaje local sobre la marcha y una sencillez absoluta para subir y organizar archivos desde el propio navegador sin tocar la terminal.
- Los inconvenientes: Pierdes por completo el control sobre las interioridades del proceso. No puedes decidir el algoritmo exacto para partir tus textos (chunking), no puedes personalizar de manera directa los parámetros de solapamiento del texto, ni tienes la libertad de diseñar una búsqueda híbrida compleja adaptada de forma milimétrica a tus necesidades como haríamos construyendo nuestra propia solución sobre PostgreSQL.
¿De verdad funciona? Evaluando el sistema
Una vez que tienes tu flamante RAG local montado y funcionando, llega el momento crucial: ¿cómo podemos comprobar científicamente si lo que hemos construido está haciendo bien su trabajo?
Existen marcos de evaluación automáticos muy potentes dentro de la comunidad de código abierto, como por ejemplo Ragas o TrueLens. Estas herramientas utilizan algoritmos complejos para medir parámetros específicos de calidad, como los siguientes:
- Faithfulness (Fidelidad): Mide si la respuesta generada por el modelo de lenguaje se basa única y exclusivamente en el contexto proporcionado por la base de datos o si sigue alucinando.
- Answer Relevance (Relevancia de la respuesta): Evalúa si la respuesta generada atiende de forma directa a la pregunta formulada por el usuario.
- Context Precision (Precisión del contexto): Comprueba si el sistema de búsqueda vectorial recuperó fragmentos verdaderamente relevantes para responder a la consulta o si rellenó el prompt de ruido inútil.
El método casero de las 20 preguntas
Aunque las herramientas automáticas de evaluación son fantásticas para proyectos a gran escala en producción, en el entorno de nuestro hogar yo prefiero aplicar un método mucho más artesanal pero endiabladamente efectivo: el método de las 20 preguntas.
Coge una hoja de papel y redacta una lista de 10 o 20 preguntas específicas de las que conozcas perfectamente la respuesta porque está escrita en tus documentos locales. Interroga a tu sistema y comprueba manualmente los resultados.
¿Ha acertado todas las respuestas basándose en los hechos reales de tus notas? ¡Perfecto! ¿Ha fallado en alguna de las preguntas? Entonces debes entrar en un proceso de ajuste interactivo donde irás modificando variables como el tamaño de los trozos de texto (chunk size), el solapamiento entre fragmentos (overlap) o el número total de fragmentos recuperados de la base de datos hasta que el sistema alcance un porcentaje de acierto impecable.
Errores típicos de principiante (y cómo solucionarlos)
A medida que vayas cacharreando con tu propio RAG casero, es muy probable que te encuentres con alguno de los siguientes tropiezos típicos de quien empieza en este mundillo:
- Un chunking incorrecto: Si configuras un tamaño de trozo extremadamente pequeño o no dejas suficiente solapamiento, las oraciones quedarán partidas por la mitad. Esto destruirá por completo el significado semántico del texto y tu base de datos guardará vectores inútiles. Ajusta siempre los parámetros del divisor de texto para garantizar que las ideas de tus apuntes queden bien cohesionadas en cada fragmento.
- Utilizar embeddings en inglés para procesar español: Muchos modelos de vectorización muy famosos de internet están entrenados casi exclusivamente con textos en inglés. Si los usas para procesar tus documentos personales redactados en español, las búsquedas por similitud semántica funcionarán realmente mal. Utiliza siempre modelos multilingües nativos de primer nivel como BGE M3 o Multilingual E5 Small para obtener los mejores resultados conceptuales en nuestro idioma.
- Subir PDFs «sucios» sin procesar: Cargar un manual en PDF lleno de tablas de datos, cabeceras repetitivas en cada página, pies de página con números de hojas o menús de navegación laterales puede empañar drásticamente el rendimiento de tu RAG. Antes de inyectar PDFs en la tubería de datos, es fundamental hacer una limpieza del texto extrayendo el contenido útil y descartando la paja estructural de las páginas.
- Olvidarse de reindexar de forma automática: De nada sirve que actualices una nota sobre la configuración de tu servidor si el sistema de búsqueda vectorial no se entera de los cambios. Diseña scripts que monitoricen las modificaciones en tu directorio de documentos e indexen de nuevo de forma transparente los archivos que hayan cambiado, eliminando de la base de datos los fragmentos obsoletos anteriores.
- Olvidar el re-ranking: Al confiar ciegamente en que la similitud de vectores resolverá todas tus consultas de forma mágica, puedes saturar el prompt del modelo con datos irrelevantes de fondo. Integrar un paso de re-ranking es un cambio radical para depurar los resultados y obtener respuestas nítidas y directas.
El horizonte: GraphRAG y RAG Agéntico
La tecnología avanza a pasos agigantados y el mundo del procesamiento del lenguaje natural no es ninguna excepción. Aunque el sistema de RAG clásico que te he detallado en esta guía es más que de sobra para solventar de forma óptima el 90% de los casos de uso domésticos, es sumamente emocionante asomarse a lo que viene en el horizonte tecnológico:
GraphRAG de Microsoft
Frente a la aproximación tradicional de buscar fragmentos planos de texto basándose en distancias vectoriales, Microsoft ha propuesto un concepto revolucionario llamado GraphRAG.
Este sistema procesa tus documentos para construir un grafo tridimensional complejo donde se identifican entidades específicas (personas, servidores, tecnologías, conceptos) y se establecen relaciones lógicas estructuradas entre ellas. Cuando haces una pregunta que requiere conectar diferentes ideas dispersas a lo largo de un libro, la inteligencia artificial navega a través del grafo de relaciones para construir una respuesta integral que ningún RAG clásico sería capaz de formular de forma coherente.
RAG Agéntico
En este paradigma, en lugar de ejecutar una tubería de datos rígida y secuencial preestablecida por nosotros, dotamos a nuestro modelo de lenguaje de autonomía para actuar como un agente capaz de decidir de forma independiente cómo abordar la resolución de tu consulta.
Un RAG agéntico tiene acceso a nuestra base de datos vectorial como si de una herramienta más se tratase dentro de su repertorio. El agente analiza tu pregunta, evalúa si necesita buscar información semántica, decide qué carpetas consultar, verifica si los resultados obtenidos son suficientes para dar una respuesta de calidad y, en caso contrario, reformula la consulta de forma automática para buscar datos complementarios en otras fuentes locales antes de entregarte la respuesta definitiva.
Conclusiones
Montar un sistema RAG en tu propia casa utilizando PostgreSQL con la extensión pgvector y apoyándote en la potencia local de Ollama es uno de los proyectos de cacharreo más gratificantes y útiles en los que te puedes embarcar hoy en día.
No solo estarás potenciando las capacidades de tu asistente inteligente personal dotándolo de una memoria impecable que de verdad conoce tus archivos de referencia, sino que estarás protegiendo el activo más valioso de tu vida digital: tu privacidad. Ninguno de tus datos privados saldrá jamás de los confines de tu red local, ni alimentarás servidores externos sin tu consentimiento expreso.
Te animo encarecidamente a que levantes tus propios contenedores, comiences a trocear tus notas y disfrutes de la apasionante experiencia de tener un cerebro artificial privado trabajando en exclusiva para ti.
Si te apasiona el mundo de la inteligencia artificial local, el self-hosting y la administración de sistemas Linux en general, te recomiendo visitar los siguientes recursos esenciales del ecosistema para profundizar en todo lo que hemos tratado en este artículo:
- Sitio oficial del proyecto Ollama para descargar el software y descubrir el catálogo de modelos disponibles.
- Repositorio oficial de la extensión pgvector en GitHub para conocer en profundidad todos los detalles técnicos y operaciones soportadas de la base de datos vectorial.
- Sitio web de Streamlit donde encontrarás la documentación oficial de la librería de Python y decenas de ejemplos inspiradores para crear tus propias interfaces web locales.
- Sitio oficial de OpenWeb UI para descubrir cómo levantar su espectacular entorno gráfico web de forma rápida utilizando contenedores Docker o Podman.
- Proyecto de ParadeDB si quieres llevar tu base de datos de PostgreSQL al siguiente nivel incorporando búsquedas textuales de máximo rendimiento integradas con Elasticsearch.
- La Red de Podcasts de Sospechosos Habituales donde compartimos de forma periódica podcasts fantásticos de tecnología, productividad y Linux en español.