Saltar al contenido principal

05 · Fundamentos de ML y LLMs

Embeddings y RAG

Chunking, búsqueda vectorial y por palabras, fusión de rankings, reranking y citas: darle al modelo conocimiento con el que nunca fue entrenado.

14 minCaso de estudio: Adapta (se abre en una ventana nueva)

Un solo sabe lo que vio durante su . No conoce tu manual interno ni la política de reembolsos que cambió el martes. Reentrenarlo cada vez que cambia un documento no es viable, y pegar todos tus documentos en el tampoco: no caben y, si cupieran, costarían demasiado.

(Retrieval-Augmented Generation) resuelve esto de una forma sencilla: antes de preguntarle al modelo, buscamos los fragmentos relevantes de tus documentos y se los damos como contexto, con instrucciones de citarlos. Los pesos del modelo no cambian. Veamos cómo se construye cada pieza y dónde suele romperse.

Embeddings y similitud

Un es un vector de números (cientos de dimensiones) que representa el significado de un texto. Un modelo de embeddings (un bi-encoder) se entrena para que textos con significado parecido queden cerca en ese espacio: "¿cuánto tarda el reembolso?" y "plazo de devolución del dinero" apuntan en direcciones similares aunque no compartan palabras.

La cercanía se mide casi siempre con similitud coseno: el coseno del ángulo entre dos vectores, de −1 a 1. Muchas bases vectoriales reportan la distancia coseno, que es 1 − similitud.

consulta ──► embedding ──► [0.12, -0.40, ..., 0.07]
                                   │ coseno
chunks   ──► embeddings ──► índice vectorial ──► top-N más cercanos

Un vector store guarda esos vectores con un índice de vecinos cercanos aproximados (HNSW es el más común) y los metadatos de cada fragmento: archivo de origen, posición, permisos.

Ingestión: parseo y chunking

La ingestión convierte archivos en fragmentos indexados: extraer texto (PDF, DOCX, HTML), partirlo en chunks, calcular embeddings y guardarlos. El importa más de lo que parece:

  • Chunks demasiado grandes diluyen el significado: el vector promedia varios temas y no se parece a ninguna pregunta concreta.
  • Chunks demasiado pequeños pierden contexto: "el plazo es de 30 días" sin saber de qué plazo habla.
  • Overlap: repetir un poco de texto entre chunks consecutivos evita que una idea quede partida justo en el borde.

Recuperación híbrida

La búsqueda vectorial falla con términos exactos: un SKU como QX-4471, un apellido o un número de artículo no tienen un "significado" que el embedding capture bien. La búsqueda por palabras clave hace justo lo contrario: encuentra coincidencias literales y no entiende sinónimos.

es el ranking clásico por palabras: premia los términos de la consulta que aparecen en el documento (con rendimiento decreciente si se repiten), pondera más los términos raros en el corpus (IDF) y penaliza documentos largos. Postgres full-text, Elasticsearch u OpenSearch ofrecen esta familia de rankings.

Reciprocal Rank Fusion

Los puntajes de BM25 y de coseno no son comparables, así que no se suman. RRF combina posiciones, no puntajes:

RRF(d) = Σ  1 / (k + rank_r(d))      k = 60, rank empieza en 1
         r

Cada lista r que contiene al documento d le aporta más cuanto más arriba esté. Un chunk que aparece alto en ambas listas gana. La constante k amortigua la ventaja del primer puesto.

Esta es la implementación real en Adapta (adapta/services/rag.py, sin el docstring):

def _rrf_fuse(*rank_lists: list[int], k: int = 60) -> list[int]:
    rrf: dict[int, float] = {}
    for ranks in rank_lists:
        for rank, idx in enumerate(ranks):
            rrf[idx] = rrf.get(idx, 0.0) + 1.0 / (k + rank + 1)
    return sorted(rrf, key=lambda i: rrf[i], reverse=True)

Reranking con cross-encoder

Un cross-encoder recibe el par (consulta, pasaje) junto y produce un puntaje de relevancia. Es mucho más preciso que el bi-encoder porque compara ambos textos a token, pero también mucho más lento, así que solo se aplica a los mejores candidatos que ya salieron de la fusión.

Grounding y citas

Recuperar no basta: el modelo tiene que usar lo recuperado. El patrón habitual es numerar los chunks en el system prompt, pedir que responda con ese contexto y cite por número, y devolver al cliente la lista de fuentes para verificar.

Cuando RAG falla

Antes de culpar al modelo, separa dos preguntas: ¿el fragmento correcto llegó al contexto? ¿El modelo lo usó bien?

Síntoma Causa probable Qué revisar
Respuesta inventada, citas irrelevantes Fallo de recuperación ¿El chunk correcto está en el top-k?
Chunk correcto en contexto, respuesta errónea Fallo de generación Prompt, tamaño del modelo, contexto ruidoso
Dato viejo Índice desactualizado Reindexación al cambiar el archivo
No encuentra un código exacto Solo búsqueda semántica Rama de palabras clave real
Resultados raros tras cambiar de modelo Vectores de dos modelos mezclados Reindexar todo al cambiar embeddings

Ojo con el último: vectores de dos modelos distintos no son comparables, así que cambiar de modelo exige reindexar todo.

Seguridad en la ingestión

Todo archivo subido es entrada hostil. Un zip bomb declara un tamaño pequeño y se descomprime a gigabytes; un zip slip usa rutas como ../../etc/cron.d/x para escribir fuera del directorio destino. Y el texto de un documento puede contener instrucciones dirigidas al modelo ( indirecta; lo vemos en Sandboxing y seguridad de agentes).

Para CTOs

  • Mide la recuperación por separado. Sin un de preguntas con su documento esperado, no sabrás si un cambio mejoró algo. Lo tratamos en Evaluar modelos.
  • Decide dónde viven los vectores. Si ya operas Postgres, pgvector evita un sistema más; un vector store dedicado se justifica por escala o funciones concretas.
  • Idioma. Muchos modelos de embeddings y rerankers por defecto están entrenados en inglés. Con contenido en español, elige y mide modelos multilingües.
  • Permisos. Si no todos pueden ver todos los documentos, el filtro de acceso va en la recuperación, no en el prompt.

Para llevar

  • RAG da conocimiento sin tocar pesos: recuperar, inyectar como contexto y citar.
  • La búsqueda vectorial capta significado; BM25 capta términos exactos. RRF los combina por posición y un cross-encoder reordena los mejores.
  • El chunking y la frescura del índice causan tantos fallos como el modelo.
  • Diagnostica separando fallo de recuperación de fallo de generación.
  • La ingestión es una superficie de ataque: limita tamaños, rutas y bytes reales.

Comprueba lo aprendido

¿Por qué RRF combina posiciones en lugar de sumar los puntajes de BM25 y coseno?

Porque los puntajes viven en escalas distintas y no comparables: BM25 no tiene tope y el coseno va de −1 a 1. Las posiciones sí son comparables entre listas.

En Adapta, ¿puede recuperarse un chunk que solo coincide por palabra exacta y que la búsqueda vectorial no trajo?

No. BM25 se calcula solo sobre los candidatos vectoriales, así que ese chunk nunca entra en la fusión. TODO.md (E3.2) lo registra como límite y planea una rama full-text sobre todo el corpus.

El chunk correcto aparece en el contexto, pero la respuesta es incorrecta. ¿Qué tipo de fallo es?

Un fallo de generación: la recuperación funcionó. Revisa el prompt, la cantidad de contexto ruidoso y la capacidad del modelo, no el índice.