Contenido

Tutorial de embeddings y RAG en Ollama: haz que el modelo responda usando tus documentos

Los modelos de lenguaje no conocen las normas de tu empresa, los manuales de productos ni tus notas personales. RAG (Retrieval-Augmented Generation, generación aumentada por recuperación) funciona así: primero busca en tus documentos los fragmentos relacionados con la pregunta y luego se los envía al modelo junto con la pregunta para que responda basándose en esa información.

Pregunta ──▶ Busca los fragmentos más relevantes ──▶ «Responde según la siguiente información: …» ──▶ Modelo ──▶ Respuesta

La tecnología clave para encontrar los «fragmentos relevantes» son los embeddings de texto (embeddings).

Qué son los embeddings de texto

Un modelo de embeddings convierte un texto en una serie de números (un vector). Los textos con significados parecidos tienen vectores parecidos, aunque usen palabras completamente diferentes:

  • «¿Cómo solicito un permiso?»
  • «¿Cómo pido vacaciones?»

Los vectores de estas dos frases serán muy similares. Así se puede realizar una «búsqueda semántica» que la búsqueda por palabras clave no puede ofrecer.

Descargar modelos de embeddings

ollama pull bge-m3            # Multilingüe, buen rendimiento en chino
ollama pull nomic-embed-text  # Ligero, principalmente para inglés

Los modelos de embeddings no sirven para chatear; solo generan vectores.

Generar embeddings

curl http://localhost:11434/api/embed -d '{
  "model": "bge-m3",
  "input": ["¿Cómo solicito un permiso?", "¿Cómo pido vacaciones?"]
}'
import ollama

result = ollama.embed(model="bge-m3", input=["¿Cómo solicito un permiso?", "¿Cómo pido vacaciones?"])
vectors = result.embeddings
print(len(vectors), len(vectors[0]))  # 2 elementos, 1024 dimensiones cada uno

input puede ser una cadena o una lista de cadenas. Procesarlas por lotes es mucho más rápido que enviarlas de una en una.

Calcular la similitud

La medida más usada es la similitud del coseno. Su valor está entre -1 y 1; cuanto más se acerca a 1, más similares son los textos:

import math


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))


print(cosine(vectors[0], vectors[1]))

Implementación: preguntas y respuestas sobre una base de conocimiento local

Supongamos que tienes un archivo con las normas de la empresa llamado handbook.txt. Este es el programa completo:

import math
import ollama

EMBED_MODEL = "bge-m3"
CHAT_MODEL = "qwen3:8b"


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))


# 1. Lee el documento y divídelo en fragmentos (separa por líneas en blanco)
text = open("handbook.txt", encoding="utf-8").read()
chunks = [c.strip() for c in text.split("\n\n") if c.strip()]

# 2. Genera embeddings para cada fragmento (solo hace falta una vez; puedes guardarlos y reutilizarlos)
chunk_vectors = ollama.embed(model=EMBED_MODEL, input=chunks).embeddings


def ask(question: str, top_k: int = 3) -> str:
    # 3. Busca los fragmentos más parecidos a la pregunta
    q_vec = ollama.embed(model=EMBED_MODEL, input=question).embeddings[0]
    ranked = sorted(zip(chunks, chunk_vectors), key=lambda p: cosine(q_vec, p[1]), reverse=True)
    context = "\n\n".join(c for c, _ in ranked[:top_k])

    # 4. Envía los fragmentos y la pregunta al modelo
    response = ollama.chat(
        model=CHAT_MODEL,
        messages=[
            {
                "role": "system",
                "content": "Eres el asistente interno de la empresa. Responde solo según la información proporcionada y en español."
                "Si la información no contiene la respuesta, di «La información no contiene datos relevantes»; no hagas suposiciones.",
            },
            {"role": "user", "content": f"Información:\n{context}\n\nPregunta: {question}"},
        ],
        options={"temperature": 0.2},
    )
    return response.message.content


while True:
    q = input("\nPregunta: ").strip()
    if not q:
        break
    print(ask(q))

Este es un sistema RAG básico, pero completo y funcional. Todos los datos permanecen en tu máquina.

Cómo mejorarlo

Fragmentación (chunking)

Si los fragmentos son demasiado grandes, incluirán contenido irrelevante; si son demasiado pequeños, perderán contexto. Algunas prácticas comunes:

  • Usa unos 300–800 caracteres por fragmento y solapa entre 50 y 100 caracteres entre fragmentos contiguos.
  • Es mejor dividir según la estructura del documento (títulos y párrafos) que hacerlo con un número fijo de caracteres.
  • Añade al principio de cada fragmento el título al que pertenece, por ejemplo, «Capítulo 3 Normas sobre permisos > Vacaciones anuales».

Usar una base de datos vectorial

Cuando hay muchos fragmentos, comparar cada uno con todos los vectores en cada consulta puede ser lento, y tendrás que volver a calcularlos cada vez que reinicies el programa. Puedes usar una base de datos vectorial para guardarlos y buscarlos:

HerramientaCaracterísticas
ChromaLa opción más sencilla para empezar con Python; puede guardar los datos en una carpeta local
SQLite + sqlite-vecUn solo archivo, ideal para proyectos pequeños
PostgreSQL + pgvectorLa opción preferida si ya usas PostgreSQL
Qdrant, MilvusPara grandes cantidades de datos y servicios independientes

Búsqueda híbrida

La búsqueda semántica no es buena para coincidencias exactas (por ejemplo, modelos de productos o nombres de personas). Combinar la búsqueda por palabras clave (BM25) con la búsqueda semántica suele dar mejores resultados.

Tener en cuenta la longitud del contexto

Al incluir varios fragmentos en el prompt, la longitud total no puede superar la ventana de contexto del modelo; si lo hace, se truncará el contenido inicial. Si hace falta, aumenta num_ctx; consulta Variables de entorno y configuración.

¿No quieres programarlo tú?

Open WebUI incluye funciones para subir documentos y crear bases de conocimiento. Puedes subir directamente archivos PDF o Word para que el modelo responda basándose en ellos.

Actualizado 12 de octubre de 2026