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:
| Herramienta | Características |
|---|---|
| Chroma | La opción más sencilla para empezar con Python; puede guardar los datos en una carpeta local |
| SQLite + sqlite-vec | Un solo archivo, ideal para proyectos pequeños |
| PostgreSQL + pgvector | La opción preferida si ya usas PostgreSQL |
| Qdrant, Milvus | Para 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