Embeddings

Primero, la palabra. Un embedding es una lista de números que representa una pieza de texto y captura su significado, de modo que textos que significan cosas similares se convierten en números similares. Este capítulo trata sobre cómo convertir palabras en algo que una computadora pueda comparar, y es lo que permite que la búsqueda coincida con el significado en lugar de palabras clave.
Texto como vectores
¿Cómo sabe una computadora que "perro" y "cachorro" están relacionados, mientras que "perro" y "grapadora" no? Las palabras no comparten letras que sean importantes.
El truco es convertir el texto en números que capten su significado, de modo que significados similares terminen como números similares. Esa lista de números es un embedding, también llamado vector. Un modelo lee el texto y genera el vector.
No interpretas los números tú mismo. Lo que importa es la relación entre ellos.
Una imagen mental aproximada: imagina cada pieza de texto colocada como un punto en el espacio, donde las cosas que significan cosas similares están cerca una de la otra. "Perro" y "cachorro" aterrizan en el mismo vecindario. "Grapadora" está al otro lado de la ciudad. Los embeddings reales utilizan cientos o miles de dimensiones en lugar de dos o tres que puedas visualizar, pero la intuición se mantiene: la cercanía significa similitud.
¿De dónde vienen esos números y por qué se alinean con el significado? Un modelo de embedding es un primo cercano del modelo predictor de Cómo funcionan los LLM, entrenado en una enorme cantidad de texto. Para ser bueno en su tarea, tuvo que aprender qué palabras e ideas aparecen en contextos similares, la observación antigua de que conoces una palabra por la compañía que frecuenta.
"Perro" y "cachorro" aparecen en el mismo tipo de oraciones, así que el modelo aprendió a representarlos de manera similar. "Grapadora" frecuenta compañía diferente, así que se ubica en otro lugar. Los cientos de números son direcciones aprendidas de significado, cada una capturando algún eje de cómo varía el texto, ninguno de ellos etiquetado de una manera que un humano leyera. Nunca decodificas los números, solo mides qué tan cerca están dos vectores.
Quieres una búsqueda que coincida con el significado en lugar de la ortografía, así que "quiero mi dinero de vuelta" encuentra una línea sobre reembolsos. El movimiento es convertir cada pieza de texto en un vector, una lista de longitud fija de números donde significados similares se sientan cerca. Un modelo de embedding lee el texto y emite ese vector, y el punto completo es que la distancia entre vectores representa la similitud de significado.
El vector tiene una longitud establecida, su dimensión, a menudo algunos cientos a un par de miles de números. Cada texto que incrustes con el mismo modelo sale de la misma longitud, lo que te permite comparar cualquiera de ellos. Esas dimensiones son direcciones de significado aprendidas, no categorías etiquetadas por humanos, así que no lees la dimensión 47 como "formalidad". Tratas el vector como un punto opaco y mides distancias.
Antes de incrustar algo real, hay un paso que decide si todo funciona: chunking. Rara vez incrustas un documento completo como un vector, porque un vector para un manual de 20 páginas difumina cada tema en él en un único punto promedio que no coincide bien con nada. En su lugar, divides el documento en chunks, pasajes de aproximadamente un párrafo a una página, e incrustas cada chunk por su cuenta. Incrusta chunks, no documentos completos, para que cada vector contenga una idea enfocada.
El tamaño del chunk es un compromiso real que estableces deliberadamente. Los chunks demasiado grandes promedian varias ideas juntas y recuperan de manera floja. Los chunks demasiado pequeños pierden el contexto circundante que hizo que una oración tuviera sentido. Un punto de partida común es algunos cientos de tokens por chunk con un pequeño solapamiento entre vecinos, de modo que una oración dividida en un límite aún aparezca completa en uno de ellos, luego ajustas según lo que tu recuperación realmente devuelve.
La búsqueda que coincide con el significado se basa en una idea: mapear texto en un espacio vectorial (un espacio de coordenadas de dimensión fija donde cada punto es una lista de números) de modo que la similitud semántica se convierta en proximidad geométrica. Un modelo de embedding es el mapa: lee texto y devuelve un vector.
Todo lo que construyes encima, indexación, recuperación, ranking, es geometría sobre esos vectores. Si obtienes el mapeo incorrecto, nada después lo recupera.
El primer mecanismo de diseño es qué incrustas, y eso es chunking: dividir documentos fuente en pasajes antes de incrustar en lugar de incrustar archivos completos. La razón es que un embedding es una compresión con pérdidas de su entrada en un punto, así que cuantas más ideas distintas contiene un chunk, más se promedian en un centroide vago que no recupera nada afiladamente. Ajusta el tamaño del chunk a la unidad que un usuario preguntará, generalmente un pasaje de algunos cientos de tokens, con un pequeño solapamiento para que un hecho en el límite sobreviva en al menos un chunk.
La estrategia de chunking es donde se gana o se pierde la calidad de recuperación, más que la elección del modelo:
- Ventanas de tamaño fijo son económicas, pero cortan oraciones a mitad de pensamiento.
- División consciente de la estructura (en encabezados, párrafos o bloques de código) mantiene cada chunk coherente y tiende a recuperar mejor, al costo de un pipeline más complicado.
Cualquiera que elijas, almacena la fuente y posición del chunk junto a su vector, porque en el momento de la recuperación necesitas apuntar de vuelta a dónde vino un hecho, y un vector desnudo no puede decirte.
Una propiedad más para interiorizarte: las dimensiones de embedding no son ejes interpretables, por lo que no puedes depurar recuperación leyendo un vector. Depuras embeddings inspeccionando lo que recuperan, nunca leyendo los números. Cuando una consulta extrae el chunk incorrecto, la solución está en el chunking, el modelo o la consulta, examinados a través de los resultados recuperados, no a través de las coordenadas en sí.
Generando un embedding
Pides a un modelo de embedding el vector, muy similar a una llamada normal de modelo pero con un punto final diferente y un modelo diferente.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small" # punto de intercambio por el modelo de embedding
def embed(text):
response = client.embeddings.create(model=EMBED_MODEL, input=text)
return response.data[0].embedding # una lista de números
vector = embed("un perro pequeño y juguetón")
print(len(vector)) # p. ej. 1536 númerosEl resultado es una lista de números. Una regla importa más que el resto: siempre compara embeddings hechos por el mismo modelo. Los vectores de diferentes modelos viven en espacios diferentes y no son comparables, como mediciones en diferentes unidades. Mezclar modelos da puntuaciones sin sentido, cada vez.
Y como cada llamada de modelo, se te factura por tokens, así que incrustar una gran colección de documentos tiene un costo que vale la pena estimar por adelantado antes de ejecutarlo en miles de documentos.
La llamada en sí es un punto final diferente con la misma forma que una llamada de chat: envías texto, obtienes un vector de vuelta.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small"
def embed_batch(texts):
# envía muchos textos en una solicitud, obtén un vector por texto de vuelta
response = client.embeddings.create(model=EMBED_MODEL, input=texts)
return [item.embedding for item in response.data]
chunks = ["un perro pequeño y juguetón", "un perro joven", "un artículo de oficina"]
vectors = embed_batch(chunks) # un vector por chunk, mismo orden enLa forma varía según el proveedor, pero la idea de solicitud y respuesta es la misma en todas partes. Dos puntos prácticos más allá de la llamada básica. Primero, incrusta en lotes: la mayoría de los puntos finales aceptan una lista de textos en una sola solicitud y devuelven un vector cada uno, lo que es más rápido y económico que una llamada por chunk porque pagas menos gastos de solicitud y golpeas límites de velocidad menos. Cuando indexas un corpus grande, los lotes es lo que mantiene el costo y el tiempo de reloj cuerdo.
Segundo, la regla del mismo modelo tiene un compañero: modelo y dimensión son una opción, no un valor por defecto. Los modelos de embedding más grandes con más dimensiones capturan distinciones más finas pero cuestan más de calcular, almacenar y comparar. Elige el modelo más pequeño y la dimensión que siga separando tus datos.
Algunos modelos incluso te permiten solicitar un vector más corto para comerciar un poco de precisión por almacenamiento más económico y búsqueda más rápida. Lo que elijas se convierte en un contrato: cambiar el modelo después y cada vector almacenado tiene que ser regenerado.
La superficie de la API es pequeña. Envías texto, obtienes un vector flotante, y las decisiones interesantes se sientan alrededor de esa llamada en lugar de en ella.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small"
def embed_batch(texts):
# una solicitud, muchos vectores de vuelta, en orden de entrada
response = client.embeddings.create(model=EMBED_MODEL, input=texts)
return [item.embedding for item in response.data]La forma de solicitud y respuesta varía según el proveedor, pero cada punto final de embedding es este mismo contrato de lote-dentro, vectores-fuera. Los lotes no son una comodidad a escala, es la diferencia entre un trabajo de índice que termina y uno que acelera toda la tarde: cada solicitud lleva gastos fijos y cuenta contra un límite de velocidad, así que amortiza ambos enviando lotes grandes, luego retrocede e intenta de nuevo en los fracasos parciales que un trabajo grande golpeará.
Modelo y dimensión son un compromiso arquitectónico con una cola larga. Vectores de dimensión más alta almacenan más distinción pero cuestan más en cada eje que importa en producción: almacenamiento por vector, memoria en el índice y tiempo por comparación, todo se escala con la dimensión. Algunos modelos soportan embeddings Matryoshka (vectores que puedes truncar a una longitud más corta y seguir usándolos), lo que te permite mantener vectores completos para precisión y servir un prefijo truncado para búsqueda de primer paso económica. Vale la pena cuando tu corpus es lo suficientemente grande como para que el almacenamiento y la latencia de consulta, no la calidad del modelo, sean la restricción vinculante.
El compromiso que muerde después es el versionado. Un embedding almacenado está vinculado a una versión exacta de modelo para siempre.
Los vectores de dos versiones de modelo no son comparables, así que no puedes incrustar una nueva consulta con un modelo más nuevo y compararla contra un índice construido en el anterior. El resultado es silencioso: las puntuaciones se ven plausibles y los rankings son incorrectos. Trata la versión del modelo como parte del esquema de tu índice, almacénalo junto a los vectores, y planifica re-incrustar como una migración, cubierta en la discusión de frescura más adelante en este capítulo.
Midiendo similitud
Para encontrar qué tan relacionadas son dos piezas de texto, mides qué tan cerca están sus vectores. La medida estándar es similitud del coseno, que devuelve un número de -1 a 1: más cerca de 1 significa más similar, cerca de 0 significa no relacionado. No necesitas las matemáticas detrás para usarla. Piénsalo como comparar la dirección en que apuntan dos vectores.
La dirección es lo que lleva el significado aquí, por eso el coseno compara ángulo en lugar de distancia bruta. Dos textos sobre reembolsos apuntan de la misma manera ya sea una frase corta o un párrafo largo.
import math
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
mag_a = math.sqrt(sum(x * x for x in a))
mag_b = math.sqrt(sum(x * x for x in b))
return dot / (mag_a * mag_b)
dog = embed("un perro pequeño y juguetón")
puppy = embed("un perro joven")
stapler = embed("un artículo de oficina para encuadernar papel")
print(cosine_similarity(dog, puppy)) # más alto, los dos están cerca en significado
print(cosine_similarity(dog, stapler)) # más bajo, los dos no están relacionadosLos números confirman la intuición: perro y cachorro puntúan más alto, perro y grapadora puntúan más bajo. Comparar un vector de consulta contra vectores almacenados es el corazón de la búsqueda semántica. Esa única función es el movimiento en el que el resto del capítulo se construye.
Tienes vectores, ahora necesitas un número que diga qué tan cerca están dos de ellos. El estándar es similitud del coseno: mide el ángulo entre dos vectores, ignorando su longitud, y devuelve un valor de -1 a 1 donde mayor significa más parecido. Ángulo, no distancia, es lo que quieres, porque un pasaje largo y una frase corta sobre el mismo tema deberían puntuar como similar aunque sus vectores tengan magnitudes diferentes.
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
# si los vectores están normalizados a longitud 1, similitud del coseno es igual a su producto punto
score = dot(query_vector, doc_vector) # sin división necesariaAquí está el movimiento que paga a escala: normaliza tus vectores a longitud 1 cuando los almacenas. Una vez que cada vector tiene la misma longitud, la división en similitud del coseno desaparece y la puntuación se convierte en un producto punto simple, que es una multiplicación-y-suma por dimensión y mucho más rápida de ejecutar en un conjunto grande. Normaliza una vez al almacenar, luego compara con un producto punto. Muchos modelos de embedding devuelven vectores ya normalizados, pero no lo supongas, verifica.
El otro cambio del ejemplo de juguete: raramente quieres la coincidencia única más cercana. Quieres recuperación top-k, los k resultados con puntuación más alta, generalmente tres a diez. Un vector puede ser un casi-fallo mientras que el segundo y tercero juntos contienen la respuesta, y devolver un conjunto pequeño da al siguiente paso espacio para trabajar. Devolver solo el primero bota contexto que desearías haber mantenido.
Necesitas una métrica de similitud, y similitud del coseno (el coseno del ángulo entre dos vectores, que varía de -1 a 1) es el predeterminado porque ignora la magnitud, así que la longitud del documento no abruma el tema. Esa opción tiene una consecuencia de desempeño que vale la pena diseñar alrededor: el coseno sobre vectores brutos cuesta un producto punto más dos cálculos de magnitud por comparación, y lo pagas por cada vector en el índice en cada consulta.
import numpy as np
# normaliza al momento de escritura, una vez por vector
def normalize(v):
v = np.asarray(v, dtype=np.float32)
return v / np.linalg.norm(v)
# al momento de consulta, coseno sobre vectores normalizados es un único producto matriz-vector
def search(query_vec, matrix):
return matrix @ query_vec # una puntuación por fila, vectorizadoLa optimización es normalizar cada vector a longitud unitaria al momento de escritura, lo que hace que la similitud del coseno sea idéntica a un producto punto y te permite puntuar el corpus completo como un único producto matriz-vector en lugar de un bucle de Python. Normaliza al momento de escritura y la similitud es un producto punto sobre todo el índice a la vez. Este es el punto base de fuerza bruta: exacto, directo, y lineal en tamaño del corpus, que está bien hasta aproximadamente los pocos cientos de miles de vectores y se desmorona por encima de eso, la señal para pasar a un índice aproximado cubierto después.
Dos realidades de producción que la métrica de juguete oculta completamente. Primero, casi nunca quieres el argmax, quieres top-k con un umbral de puntuación, porque la brecha entre el primer y segundo resultado te dice qué tan segura es la coincidencia, y un resultado superior que aún puntúa bajo no significa nada en tu corpus respondió realmente la consulta. Devuelve eso como "sin coincidencia buena" en lugar de entregar la fila menos-mala. Segundo, la similitud del coseno es relativa, no absoluta: una puntuación de 0,4 es significativa solo contra la distribución de puntuaciones que tu corpus y modelo producen, así que calibra umbrales empíricamente por índice en lugar de copiar un número mágico de una entrada de blog.
Búsqueda semántica
Junta las piezas y puedes buscar por significado en lugar de palabras clave. Incrusta tus documentos una vez y almacena los vectores. Cuando llega una consulta, incrústala también, luego devuelve los documentos cuyos vectores están más cerca.
docs = [
"Restablece tu contraseña desde la página de configuración de cuenta.",
"Nuestra política de reembolso permite devoluciones dentro de 30 días.",
"Contacta soporte por correo electrónico para preguntas de facturación.",
]
# incrusta cada documento una vez, por adelantado
index = [{"text": text, "vector": embed(text)} for text in docs]
def search(query):
query_vector = embed(query)
scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
return sorted(scored, key=lambda x: x["score"], reverse=True)[0] # la coincidencia más cercana
print(search("¿cómo obtengo mi dinero de vuelta?"))
# devuelve la línea de política de reembolso, aunque no comparta palabras clave con la consultaLa consulta "¿cómo obtengo mi dinero de vuelta?" encuentra la línea de reembolso a pesar de no comparten palabras importantes con ella. La búsqueda semántica coincide por significado, no por ortografía. La búsqueda por palabras clave la habría extrañado completamente, porque la consulta y el documento no tienen palabras importantes en común.
La forma es siempre la misma: incrusta una vez y mantén los vectores, incrusta la consulta en tiempo de solicitud, puntúa, ordena, devuelve el más cercano. Ese es el bucle de búsqueda semántica, y lo reutilizarás para el resto de este manual.
La versión de juguete almacena vectores en una lista de Python y puntúa cada uno en cada consulta. Eso funciona para una demostración y se rompe en el momento en que tu corpus crece, porque un escaneo lineal vuelve a ejecutar la comparación completa sobre cada vector para cada consulta, y también estás reconstruyendo el índice en memoria en cada reinicio.
La solución es una base de datos vectorial: una tienda construida para mantener vectores y responder "encuentra los k más cercanos a este vector de consulta" rápidamente y desde el disco. En lugar de tu propia lista y bucle, escribes vectores una vez y la base de datos maneja la búsqueda, persistencia y actualización de entradas individuales.
# la forma varía según DB vectorial, pero el patrón es el mismo en todas partes
index.upsert([
{"id": "doc-1", "vector": embed(chunk), "metadata": {"source": "faq.md", "section": "refunds"}}
for chunk in chunks
])
results = index.query(vector=embed(user_question), top_k=5) # los 5 más cercanos, no solo 1Dos cosas que la base de datos te da que la lista no:
- Devuelve resultados top-k en una llamada, los varios más cercanos en lugar de un único mejor, que es lo que realmente alimentas al siguiente paso.
- Almacena metadatos junto a cada vector, el archivo fuente, sección y marca de tiempo, así que un resultado puede apuntar de vuelta a dónde vino y puedes filtrar por ello.
Almacena vectores en una DB vectorial, no en una lista de Python, en el momento en que tengas datos reales. La lista es para aprender el mecanismo, no para ejecutarlo.
El bucle de referencia, incrusta una vez, almacena, incrusta la consulta, puntúa, ordena, devuelve, es correcto y no sobrevive al contacto con la escala. Un escaneo de fuerza bruta es lineal en tamaño del corpus por consulta, así que en un millón de vectores y cualquier tasa de consulta real estás gastando un segundo de CPU por búsqueda que no tienes. Todo en búsqueda de producción se trata de no comparar la consulta contra cada vector.
Eso es lo que búsqueda aproximada de vecinos más cercanos te compra (ANN: algoritmos que encuentran los vectores más cercanos rápido aceptando una pequeña posibilidad de perder uno de los verdaderamente más cercanos). Dos familias dominan:
- HNSW (Pequeño Mundo Navegable Jerárquico) construye un gráfico en capas de vectores y lo camina de áspero a fino, dando baja latencia de consulta al costo de alta memoria.
- IVF (archivo invertido) agrupa vectores y busca solo los grupos más cercanos, usando menos memoria pero necesitando que sus centros de grupo se reentrenen a medida que los datos cambian.
Por encima de algunos cientos de miles de vectores, ANN no es opcional, es el índice. El compromiso que estás ajustando es recall (la fracción de los verdaderos vecinos más cercanos que el índice realmente devuelve) contra latencia y memoria: empuja recall hacia 1.0 y las consultas se ralentizan, relájalo y silenciosamente pierdes chunks relevantes.
El modo de fallo que el bucle de juguete oculta completamente es la frescura. Un índice es una instantánea, así que cuando un documento fuente cambia, su vector almacenado ahora está obsoleto y sigue siendo recuperado como si fuera actual. Necesitas un pipeline de re-incrustar disparado por cambio de documento: detecta la edición, re-chunifica y re-incrusta ese documento, y haz upsert de los nuevos vectores mientras eliminas los viejos, idealmente rastreados por un hash de contenido para que re-incrustes solo lo que realmente cambió en lugar del corpus completo en cada edición. Los vectores obsoletos son la forma silenciosa en que un sistema de recuperación comienza a mentir.
La última brecha es la coincidencia exacta. La búsqueda vectorial está construida para perderla: una consulta para orden A-10293 o un código de error se incrusta a una región difusa y puede no extraer el único documento que contiene esa cadena. Búsqueda híbrida ejecuta búsqueda por palabras clave (que clava tokens exactos) y búsqueda vectorial (que clava significado) juntas y fusiona los rankings, así que identificadores, nombres y términos raros no se pierden en difusión semántica. Alcanza por ella siempre que tu corpus contiene cosas que los usuarios escribirán exactamente.
Para qué no sirven los embeddings
Los embeddings son una herramienta de similitud, no una herramienta de pensamiento. Unos cuantos límites para tener en mente así alcanzas la herramienta correcta:
- No hacen coincidencia exacta. Para encontrar un número de orden exacto o un código de error, la búsqueda por palabras clave o base de datos simple es mejor.
- No razonan. Te dicen que dos cosas están relacionadas, no por qué, y no si una lógicamente sigue de la otra.
- Se vuelven obsoletos. Si tus documentos cambian, debes re-incrustarlos, o tu búsqueda apuntará a contenido antiguo.
Los embeddings encuentran lo que está relacionado, no juzgan qué es verdadero o actual. Cada una de estas brechas tiene una herramienta mejor, y saber cuál usar es la mitad de la habilidad.
La trampa con una herramienta que funciona tan bien es alcanzarla en todas partes. Tres límites marcan dónde detenerse y elegir algo más.
La coincidencia exacta es la primera. Los embeddings mapean un ID de orden o un código de error al mismo espacio semántico difuso que todo lo demás, así que A-10293 aterrizaría cerca de otras cadenas de forma de orden, no exactamente en sí mismo. Para identificadores, códigos y cualquier valor que un usuario escriba literalmente, la búsqueda por palabras clave o base de datos es la herramienta correcta, y búsqueda híbrida (búsqueda por palabras clave y vectorial ejecutadas juntas) es cómo obtienes ambas a la vez.
El razonamiento es el segundo. Similitud no es inferencia: dos chunks puntuando alto significa que son sobre cosas relacionadas, no que uno responda una pregunta o lógicamente siga del otro. El razonamiento ocurre en el modelo al que alimentas los chunks recuperados, no en la recuperación. La recuperación encuentra candidatos, el modelo hace el pensamiento.
La frescura es el tercero y el que muerde silenciosamente. Tu índice es una instantánea de siempre que lo incrustaste, así que los documentos editados mantienen devolución de su ortografía vieja hasta que la re-incrustes. Trata re-incrustar al cambio como parte del sistema, no algo que hagas a mano cuando alguien nota que las respuestas se volvieron incorrectas.
Saber dónde se detienen los embeddings es lo que mantiene un sistema de recuperación confiable, y tres límites importan en producción.
La coincidencia exacta es un punto ciego estructural, no un problema de ajuste. Los embeddings comprimen la entrada hacia vecindarios semánticos, así que identificadores, códigos, SKU y nombres propios raros se esparcen hacia cadenas de aspecto similar en lugar de aterrizar en sí mismos. La búsqueda vectorial pura clasificará un casi-fallo sobre la coincidencia exacta. La solución es recuperación híbrida que fusiona un índice léxico (búsqueda por palabras clave o BM25, un ranking clásico de frecuencia de término) con el índice vectorial, así que tokens exactos se coinciden exactamente mientras el significado se coincide semánticamente, y no apuestas una búsqueda de orden en geometría.
La deriva es el límite que se erosiona silenciosamente. Deriva de embedding es el desajuste que obtienes cuando la versión del modelo detrás de tu índice cambia: vectores de dos versiones ocupan espacios diferentes y no son comparables, así que una consulta incrustada con un modelo nuevo contra un índice construido en el anterior devuelve tonterías de aspecto plausible. Fija la versión del modelo en tu esquema de índice, y trata cualquier actualización de modelo como una migración de re-incrustar completa, no una sustitución fácil, porque no hay error lanzado, solo resultados silenciosamente peores.
La evaluación es el límite que la gente se salta hasta que la recuperación ya está fallando. No puedes examinar si la recuperación es buena, la mides: retén un conjunto de consultas con chunks conocidos como relevantes y puntúa recall@k (¿el chunk correcto aterrizó en el top k) y precisión (cuánto de lo que devolviste fue relevante). Si no estás midiendo recall@k, no sabes si la recuperación funciona.
Ese conjunto de evaluación es también lo que te dice si una estrategia de chunking, modelo o ponderación híbrida nueva realmente ayudó, en lugar de intercambiar un conjunto de fallos por otro que aún no has notado. La calidad de recuperación es la fundación en la que se para RAG, así que se gana una prueba real, no una comprobación de vibra.
En la práctica
El flujo completo vive en la función search anterior: incrusta un pequeño conjunto de entradas de FAQ una vez, luego responde consultas por significado. El patrón es el mismo en cualquier tamaño, solo el almacenamiento cambia a medida que creces.
En el próximo capítulo tomas las coincidencias más cercanas y las alimentas a un modelo, así que responde desde tus documentos en lugar de desde memoria. Esa técnica es RAG, y está construida directamente sobre lo que escribiste aquí.
El mecanismo está resuelto: chunificar, incrustar en lotes, almacenar en una DB vectorial, recuperar top-k por similitud. Lo que cambia cuando vas a producción es la fontanería circundante, la re-incrustar al cambio, los metadatos en los que filtras, la versión de modelo que fijas.
Los chunks recuperados no son el fin. En el próximo capítulo los alimentas a un modelo como contexto para que responda desde tus documentos en lugar de su entrenamiento congelado. Eso es RAG, y la calidad de sus respuestas está limitada por la recuperación que construyes aquí.
Todo aquí, chunificación, normalización, indexación ANN, recuperación híbrida, frescura y evaluación recall@k, es la capa de recuperación, y es la parte de un sistema de IA que silenciosamente determina si las respuestas son buenas. El modelo en la cima solo puede razonar sobre lo que le entrega la recuperación, así que una respuesta confidente incorrecta es usualmente un fallo de recuperación disfrazado de generación.
Ese es el puente hacia el próximo capítulo. RAG es esta capa de recuperación alimentando contexto fundamentado a un modelo, y cada modo de fallo aquí, vectores obsoletos, coincidencias exactas extrañadas, recall bajo, aparece allá como una respuesta incorrecta. Construye la recuperación para ser medida y fresca, y RAG tiene algo sólido en lo que pararse.

