
RAG Vectorial Local y Búsqueda Semántica con IA: Guía Paso a Paso (Proyecto Ingrid Daubechies)
En los sistemas de Inteligencia Artificial convencionales, hacer que un LLM consulte documentos propios suele requerir enviar datos a la nube o lidiar con búsquedas por palabras clave que fallan cuando el usuario no utiliza los términos exactos.
Para resolver este problema de manera privada, sostenible y matemáticamente precisa nace el Proyecto Ingrid Daubechies, enfocado en la Búsqueda Semántica Vectorial y RAG (Retrieval-Augmented Generation) sobre PDFs y documentos.
1. ¿Quién es Ingrid Daubechies y por qué da nombre a este proyecto?
Ingrid Daubechies es una matemática y física belga-estadounidense, una de las figuras más influyentes en el procesamiento de información del último siglo. Es famosa internacionalmente por desarrollar las ondículas de Daubechies (Daubechies wavelets), la base matemática que revolucionó el análisis de señales y la compresión de datos (utilizada, entre otros, en el estándar de imagen JPEG 2000).
Su trabajo demostró cómo descomponer información compleja en componentes matemáticos esenciales sin perder significado. Del mismo modo, el Proyecto Ingrid Daubechies transforma documentos de texto y archivos PDF en vectores matemáticos (embeddings), permitiendo al sistema encontrar el fragmento exacto que responde a una duda dentro de un mar de datos.
2. ¿Qué hace este sistema y cómo funciona?
Este proyecto implementa un pipeline de RAG Vectorial 100% Local sobre el puerto 8047:
- Extracción y Fragmentación (Chunking): Lee el documento cargado (archivos
.pdf,.txt,.md,.csv) y lo divide en bloques de texto manejables con solape para no perder contexto entre frases. - Generación de Embeddings: Envía cada bloque al modelo local
text-embedding-nomic-embed-text-v1.5cargado en LM Studio, convirtiendo el texto en un vector numérico de 768 dimensiones. - Búsqueda Geométrica (Similitud del Coseno): Convierte la pregunta del alumno en otro vector y calcula la distancia/ángulo en el espacio vectorial contra todos los bloques del documento.
- Respuesta Fundamentada (Grounding): Selecciona los bloques con mayor coincidencia matemática y los entrega como contexto estricto a Qwen 2.5 Instruct, garantizando que la IA responda únicamente con la información real del documento sin inventar datos (alucinaciones).

3. El Código Completo con Interfaz y Datos de Prueba (app_daubechies.py)
Crea un archivo llamado app_daubechies.py. En la propia interfaz web se ha añadido una caja inferior con un texto y preguntas de prueba para que el alumnado pueda realizar su primer test de inmediato.
Python
import io
import math
import time
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import HTMLResponse
from openai import OpenAI
from pypdf import PdfReader
app = FastAPI()
# Conexión al servidor local de LM Studio
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
def calcular_similitud_coseno(vec1: list[float], vec2: list[float]) -> float:
"""Calcula la similitud matemática del coseno entre dos vectores."""
producto_punto = sum(a * b for a, b in zip(vec1, vec2))
magnitud_v1 = math.sqrt(sum(a * a for a in vec1))
magnitud_v2 = math.sqrt(sum(b * b for b in vec2))
if not magnitud_v1 or not magnitud_v2:
return 0.0
return producto_punto / (magnitud_v1 * magnitud_v2)
def fragmentar_texto(
texto: str, tamano_palabras: int = 120, solape_palabras: int = 20
) -> list[str]:
"""Divide el documento en chunks de texto manejables con solape."""
palabras = texto.split()
if not palabras:
return []
chunks = []
i = 0
while i < len(palabras):
fin = i + tamano_palabras
chunk = " ".join(palabras[i:fin])
if chunk.strip():
chunks.append(chunk)
i += tamano_palabras - solape_palabras
return chunks
def obtener_embedding(texto: str) -> list[float]:
"""Solicita el vector embedding al modelo local nomic-embed-text en LM Studio."""
respuesta = client.embeddings.create(
model="text-embedding-nomic-embed-text-v1.5", input=texto
)
return respuesta.data[0].embedding
@app.post("/consultar-rag")
async def consultar_rag(
pregunta: str = Form(...), archivo: UploadFile = File(...)
):
tiempo_inicio = time.time()
contenido_bytes = await archivo.read()
nombre_archivo = archivo.filename.lower()
texto_documento = ""
# Extracción según tipo de archivo (PDF vs Texto Plano)
try:
if nombre_archivo.endswith(".pdf"):
reader = PdfReader(io.BytesIO(contenido_bytes))
paginas_texto = [
page.extract_text()
for page in reader.pages
if page.extract_text()
]
texto_documento = "\n".join(paginas_texto)
else:
texto_documento = contenido_bytes.decode("utf-8", errors="ignore")
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Error al procesar el archivo '{archivo.filename}': {str(e)}",
)
if not texto_documento.strip():
raise HTTPException(
status_code=400,
detail="No se pudo extraer texto legible del documento subido.",
)
# 1. Chunking / Fragmentación
chunks = fragmentar_texto(texto_documento)
if not chunks:
raise HTTPException(
status_code=400, detail="El texto extraído no contiene palabras."
)
# 2. Generar Embeddings para los Chunks
embeddings_chunks = []
for chunk in chunks:
emb = obtener_embedding(chunk)
embeddings_chunks.append({"chunk": chunk, "embedding": emb})
# 3. Vectorizar la Pregunta del Usuario
embedding_pregunta = obtener_embedding(pregunta)
# 4. Búsqueda Vectorial (Similitud del Coseno)
resultados_similitud = []
for item in embeddings_chunks:
score = calcular_similitud_coseno(
embedding_pregunta, item["embedding"]
)
resultados_similitud.append(
{
"chunk": item["chunk"],
"score": score,
"dimensiones": len(item["embedding"]),
}
)
# Ordenar por relevancia (mayor score primero)
resultados_similitud.sort(key=lambda x: x["score"], reverse=True)
# Seleccionar el mejor contexto (Top 2 chunks)
top_chunks = resultados_similitud[:2]
contexto_unido = "\n\n---\n\n".join([c["chunk"] for c in top_chunks])
# 5. Generación de Respuesta con el LLM
prompt_sistema = (
"Eres el asistente del Proyecto Ingrid Daubechies especializado en RAG Vectorial sobre PDFs y documentos. "
"Responde a la pregunta del usuario utilizando ÚNICAMENTE la información provista en el contexto recuperado. "
"Si la respuesta no está explícitamente en el contexto, indica amablemente que no figura en el documento."
)
prompt_usuario = f"CONTEXTO RECUPERADO DEL DOCUMENTO:\n{contexto_unido}\n\nPREGUNTA: {pregunta}"
respuesta_llm = client.chat.completions.create(
model="qwen2.5-3b-instruct",
messages=[
{"role": "system", "content": prompt_sistema},
{"role": "user", "content": prompt_usuario},
],
temperature=0.2,
)
texto_final = respuesta_llm.choices[0].message.content
tiempo_total = time.time() - tiempo_inicio
# Trazabilidad / Audit Logs
logs = {
"nombre_archivo": archivo.filename,
"total_chunks_generados": len(chunks),
"dimensiones_vector": len(embedding_pregunta),
"top_coincidencias": [
{
"score_similitud": f"{c['score']:.4f}",
"extracto": c["chunk"][:100] + "...",
}
for c in top_chunks
],
}
return {
"respuesta": texto_final,
"tiempo": f"{tiempo_total:.2f} s",
"logs": logs,
}
@app.get("/", response_class=HTMLResponse)
async def index():
return """
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>PROYECTO INGRID DAUBECHIES - RAG PDF Vectorial</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f6f9; margin: 0; padding: 25px; color: #333; }
.container { max-width: 1050px; margin: auto; }
h1 { color: #1a365d; margin-bottom: 2px; font-size: 26px; }
.subtitle { color: #4a5568; font-size: 14px; margin-top: 0; margin-bottom: 25px; }
.upload-section { background: white; border: 1px dashed #cbd5e0; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
.grid-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.file-box { border: 2px dashed #805ad5; background: #faf5ff; border-radius: 6px; padding: 20px; text-align: center; cursor: pointer; }
textarea { width: 100%; height: 100px; padding: 10px; border: 1px solid #cbd5e0; border-radius: 6px; font-family: inherit; font-size: 14px; box-sizing: border-box; }
button { background: #6b46c1; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-weight: bold; font-size: 15px; cursor: pointer; margin-top: 10px; width: 100%; }
button:hover { background: #553c9a; }
.results-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 25px; }
.panel { background: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; min-height: 250px; }
.panel-header { font-size: 15px; font-weight: bold; color: #6b46c1; border-bottom: 1px solid #edf2f7; padding-bottom: 10px; margin-bottom: 15px; }
.tiempo { font-size: 12px; color: #718096; margin-top: 15px; border-top: 1px dotted #e2e8f0; padding-top: 8px; }
.log-box { background: #f7fafc; border: 1px solid #e2e8f0; border-radius: 5px; padding: 10px; margin-bottom: 10px; font-family: monospace; font-size: 12px; color: #2d3748; }
.test-section { background: #edf2f7; border: 1px solid #cbd5e0; border-radius: 8px; padding: 20px; font-size: 13px; }
.test-title { font-weight: bold; color: #2d3748; margin-bottom: 8px; font-size: 14px; display: flex; align-items: center; gap: 6px; }
.code-sample { background: #ffffff; border: 1px solid #e2e8f0; padding: 12px; border-radius: 5px; font-family: monospace; white-space: pre-wrap; color: #2b6cb0; margin-top: 5px; }
footer { text-align: center; margin-top: 30px; font-size: 12px; color: #a0aec0; }
</style>
</head>
<body>
<div class="container">
<h1>PROYECTO INGRID DAUBECHIES</h1>
<p class="subtitle">Búsqueda Semántica Vectorial y RAG sobre PDFs / Documentos · MonteSteam / AIDARAC</p>
<div class="upload-section">
<div class="grid-inputs">
<div>
<label><strong>1. Carga un documento (.pdf, .txt, .md, .csv):</strong></label>
<div class="file-box" onclick="document.getElementById('archivo').click()">
📕 Haz clic para cargar PDF o texto
<input type="file" id="archivo" accept=".pdf,.txt,.md,.csv,.json" style="display:none;" onchange="actualizarNombre(event)">
</div>
<div id="nombre-archivo" style="margin-top: 8px; font-size: 13px; color: #6b46c1; font-weight: bold;"></div>
</div>
<div>
<label><strong>2. Pregunta sobre el contenido del documento:</strong></label>
<textarea id="pregunta" placeholder="Ejemplo: ¿Qué temperatura máxima alcanza la boquilla de la impresora 3D y cuáles son las normas de seguridad?"></textarea>
<button onclick="procesarRAG()">Ejecutar RAG Vectorial</button>
</div>
</div>
</div>
<div class="results-grid">
<div class="panel">
<div class="panel-header">1. RESPUESTA EXTRAÍDA DEL DOCUMENTO</div>
<div id="respuesta-agente">Carga un documento PDF o texto y realiza una consulta.</div>
<div id="tiempo-box"></div>
</div>
<div class="panel">
<div class="panel-header">2. AUDITORÍA VECTORIAL Y SIMILITUD DEL COSENO</div>
<div id="logs-agente">Sin ejecución previa.</div>
</div>
</div>
<!-- SECCIÓN DE DATOS DE PRUEBA RÁPIDA -->
<div class="test-section">
<div class="test-title">🧪 Datos de prueba rápida para la primera ejecución en clase:</div>
<p style="margin: 2px 0 8px 0; color: #4a5568;">Copia este texto de ejemplo en un archivo de bloc de notas guardado como <code>normativa_robotica.txt</code> para subirlo a la app:</p>
<div class="code-sample">NORMATIVA DE SEGURIDAD Y USO DEL LABORATORIO DE ROBÓTICA - IES MONTERROSO
1. Acceso y Horarios: El aula estará abierta durante los recreos para el alumnado inscrito en las iniciativas MonteSteam y AIDARAC.
2. Uso de Impresoras 3D: No se debe tocar la boquilla (nozzle) de la impresora 3D cuando esté en funcionamiento, ya que alcanza temperaturas superiores a 210°C.
3. Equipos informáticos: Todos los modelos de Inteligencia Artificial deben ejecutarse localmente usando LM Studio en los puertos asignados (8042 a 8047). Queda prohibida la transferencia de datos de alumnos a servicios externos en la nube.
4. Almacenamiento de componentes: Las placas Arduino y sensores deben guardarse en sus respectivas cajas etiquetadas al finalizar cada sesión de trabajo.</div>
<div style="margin-top: 12px; font-weight: bold; color: #2d3748;">Preguntas sugeridas para probar el buscador semántico:</div>
<ul style="margin: 4px 0 0 0; padding-left: 20px; color: #4a5568;">
<li><code>¿A qué temperatura trabaja la boquilla de la impresora 3D?</code></li>
<li><code>¿Por qué está prohibido subir datos a servicios en la nube según la normativa?</code></li>
<li><code>¿Dónde hay que guardar los materiales al terminar el taller?</code></li>
</ul>
</div>
<footer>PROYECTO INGRID DAUBECHIES · Puerto 8047 · Laboratorio MonteSteam</footer>
</div>
<script>
function actualizarNombre(event) {
const archivo = event.target.files[0];
if (archivo) {
document.getElementById('nombre-archivo').innerText = 'Archivo cargado: ' + archivo.name;
}
}
async function procesarRAG() {
const archivoInput = document.getElementById('archivo');
const preguntaInput = document.getElementById('pregunta');
if (!archivoInput.files[0]) {
alert('Por favor, selecciona un PDF o archivo de texto.');
return;
}
if (!preguntaInput.value.trim()) {
alert('Escribe una consulta.');
return;
}
const respBox = document.getElementById('respuesta-agente');
const timeBox = document.getElementById('tiempo-box');
const logsBox = document.getElementById('logs-agente');
respBox.innerHTML = '<em>Leyendo documento, extrayendo texto y calculando embeddings...</em>';
timeBox.innerHTML = '';
logsBox.innerHTML = '<em>Vectorizando consulta y calculando coseno...</em>';
const formData = new FormData();
formData.append('archivo', archivoInput.files[0]);
formData.append('pregunta', preguntaInput.value.trim());
try {
const res = await fetch('/consultar-rag', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok) {
respBox.innerHTML = `<p>${data.respuesta.replace(/\\n/g, '<br>')}</p>`;
timeBox.innerHTML = `<div class="tiempo">⏱️ Tiempo total pipeline RAG: <strong>${data.tiempo}</strong></div>`;
let htmlLogs = `
<div class="log-box">📄 <strong>Documento:</strong> ${data.logs.nombre_archivo}</div>
<div class="log-box">🧩 <strong>Chunks creados:</strong> ${data.logs.total_chunks_generados}</div>
<div class="log-box">📐 <strong>Dimensión vector:</strong> ${data.logs.dimensiones_vector} d</div>
<div style="margin-top:10px;"><strong>Top Coincidencias (Coseno):</strong></div>
`;
data.logs.top_coincidencias.forEach((item, index) => {
htmlLogs += `
<div class="log-box" style="margin-top:5px;">
<strong>#${index + 1} (Similitud: ${item.score_similitud})</strong><br>
<em>"${item.extracto}"</em>
</div>
`;
});
logsBox.innerHTML = htmlLogs;
} else {
respBox.innerHTML = `<span style="color:red;">Error: ${data.detail}</span>`;
logsBox.innerHTML = '-';
}
} catch (e) {
respBox.innerHTML = '<span style="color:red;">Error de conexión con el servidor FastAPI.</span>';
logsBox.innerHTML = '-';
}
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
import uvicorn
uvicorn.run("app_daubechies:app", host="127.0.0.1", port=8047, reload=True)
4. Puesta en Marcha
- Requisito de dependencias: Asegúrate de instalar
pypdfpara soportar la lectura directa de PDFs:
Bashpip install pypdf - Modelos en LM Studio: Mantén cargado el servidor en el puerto
1234con el modeloQwen2.5 3B Instruct(oLlama 3.2 3B Instruct). - Ejecución del servidor: Arranca el servicio en el puerto asignado mediante Uvicorn:
Bashuvicorn app_daubechies:app --reload --port 8047 - Accede a
[http://127.0.0.1:8047](http://127.0.0.1:8047)en tu navegador. Puedes copiar el texto de la sección inferior en un archivo de texto o subir cualquier documento PDF para iniciar las pruebas semánticas en el aula.


