RAG (Retrieval-Augmented Generation)
RAG es la alternativa más común al fine-tuning para el problema de “falta de conocimiento del dominio” que vimos en el Capítulo 1. En lugar de meter la información dentro de los pesos del modelo, le damos al modelo un “libro abierto” para que busque la respuesta antes de contestar.
El escenario: la misma empresa de soporte al cliente de capítulos anteriores quiere que su asistente responda preguntas de empleados sobre políticas internas — “¿cuántos días de vacaciones tengo si llevo 3 años en la empresa?”. A diferencia del problema de tono que resolvimos con fine-tuning en el Capítulo 4, aquí el problema es de información concreta y verificable, que además puede cambiar cuando la empresa actualice sus políticas — fine-tuning sería la herramienta equivocada, porque “hornear” información factual en los pesos es frágil y cada actualización requeriría reentrenar.
El flujo completo, con el escenario real
Esta fase ocurre una sola vez, al cargar o actualizar la base de conocimientos.
Esta fase ocurre cada vez que un usuario pregunta algo.
Imaginemos este fragmento en la base de conocimientos de la empresa:
Política de vacaciones — Extracto. Los empleados con menos de 2 años de antigüedad tienen derecho a 15 días hábiles de vacaciones al año. Los empleados con 2 a 5 años de antigüedad tienen derecho a 20 días hábiles. Los empleados con más de 5 años tienen derecho a 25 días hábiles.
-
Embeddings: el párrafo se convierte en un vector numérico, con el mismo mecanismo del capítulo de embeddings de Parte I.
-
Base de datos vectorial: ese vector se guarda junto con el texto original y metadatos (“documento: política de vacaciones, sección: antigüedad”) — el Capítulo 5b profundiza en esto.
-
La consulta: el empleado pregunta, y su pregunta también se convierte en un vector, con el mismo modelo de embeddings.
-
Retrieval: se calcula qué tan cerca está el vector de la pregunta del vector de cada fragmento guardado — literalmente la misma similitud por coseno de Parte I, solo que ahora comparando párrafos completos en vez de palabras individuales.
¿Cuántos fragmentos recuperar? El parámetro
top_k: en el código de este capítulo usamostop_k=3, pero ese número no es arbitrario — es una decisión de diseño con un trade-off real. Untop_kmuy bajo (por ejemplo, 1) arriesga no incluir toda la información relevante si está repartida en más de un fragmento de la base de conocimientos. Untop_kmuy alto (por ejemplo, 20) satura el contexto del modelo con fragmentos de relevancia decreciente, reintroduciendo el problema de “aguja en el pajar” que vimos en el Capítulo 1 — cuanto más contexto irrelevante rodea a la respuesta correcta, más le cuesta al modelo encontrarla. Un punto de partida frecuente en producción está entre 3 y 10, pero la forma correcta de afinarlo no es la intuición: es correr evals de RAG (RAGAS, Capítulo 3) probando distintos valores detop_ksobre el mismo conjunto de preguntas de prueba, y quedarse con el que maximice precisión y recall de contexto sin inflar innecesariamente el costo por consulta. -
Generación fundamentada (grounded generation): el sistema inyecta el fragmento recuperado en el prompt, con una instrucción explícita: “Responde usando ÚNICAMENTE la información proporcionada. Si no está aquí, di que no lo sabes.” El modelo responde correctamente no porque memorizó la política de esta empresa —nunca la vio en su preentrenamiento— sino porque la tiene justo frente a él, en su ventana de contexto, en el momento de generar. RAG no cambia el mecanismo fundamental de Parte I (predecir el token más probable dado el contexto) — cambia qué hay en el contexto para que la respuesta correcta se vuelva la más probable.
Fragmentación (chunking)
Partir documentos largos en fragmentos más pequeños antes de vectorizarlos y guardarlos. Como punto de partida, es común probar chunks de 300-500 tokens y ajustar su tamaño con evals según la estructura de los documentos y las preguntas reales.
Solapamiento entre chunks (overlap): es buena práctica que los chunks consecutivos se solapen parcialmente — típicamente 10-20% del tamaño del chunk, o unos 50 tokens en un chunk de 300-500. Esto asegura que si una idea importante queda justo en el límite entre dos fragmentos, al menos uno de los dos la contiene completa, en vez de que ambos la tengan partida a la mitad. Es un pequeño costo de almacenamiento redundante, a cambio de reducir el riesgo de perder contexto justo en las fronteras entre chunks.
Existen tres estrategias, de más simple a más costosa:
- Corte por tamaño fijo — rápido, pero puede partir una idea a la mitad.
- Corte recursivo/estructural — corta primero por párrafo; si un fragmento sigue siendo muy grande, retrocede a línea, luego a oración. Es un patrón de backoff, como el de los n-gramas en Parte I: usar la unidad más específica disponible, cayendo a una más genérica solo cuando hace falta.
- Corte semántico — usa embeddings o un LLM para agrupar oraciones que hablan del mismo tema. La más precisa, también la más cara.
Por qué es necesario — tres razones distintas:
| Razón | Dónde ocurre | Qué pasa si lo ignoras |
|---|---|---|
| Límite técnico del modelo de embeddings | Al vectorizar | Simplemente no puedes generar el vector de un texto demasiado largo |
| Dilución del vector | También al vectorizar, pero es un problema de calidad, no un límite duro | Un chunk gigante produce un vector “promedio borroso” que no representa bien ningún tema — la búsqueda pierde precisión |
| Ventana de contexto del generador | Después de recuperar, al armar el prompt final | El “aguja en el pajar” del Capítulo 1 — el modelo no encuentra bien la respuesta entre demasiado contexto |
Es importante conservar metadatos de ubicación (sección, página) para poder recuperar el chunk vecino si hace falta más contexto, y para referenciar la fuente después.
HyDE: incrustaciones de documentos hipotéticos
Un problema sutil: si el empleado pregunta de forma coloquial —“oye, si me voy de vacaciones tres semanas seguidas ¿me meto en problemas?”— ese texto usa un vocabulario (“me voy”, “meto en problemas”) muy distinto al de la política formal (“antigüedad”, “días hábiles”, “derecho a”). Ambos textos están conceptualmente relacionados, pero pueden terminar con una similitud vectorial más baja de lo ideal, simplemente por diferencia de registro.
HyDE resuelve esto insertando un paso intermedio, antes de tocar la base vectorial: se le pide al LLM que genere una respuesta hipotética completa —basada en su conocimiento general sobre el tema, no en perfilar a quién pregunta— redactada de forma natural en el registro típico del dominio. Se vectoriza esa respuesta hipotética, y se usa ese vector para buscar.
Un matiz importante: HyDE no reduce cuántos resultados obtienes — una búsqueda vectorial siempre entrega un ranking ordenado, sin importar qué tan vaga sea la consulta. Lo que hace HyDE es mejorar el ranking: el fragmento correcto, que con la pregunta original podía terminar mal posicionado por diferencias de vocabulario, sube hacia el primer lugar porque ahora se compara “registro formal contra registro formal”.
Y algo que vale la pena dejar explícito: la respuesta hipotética nunca se le muestra al usuario y no se usa para generar la respuesta final — es un “cebo” desechable que solo sirve para buscar. Una vez que encontró los fragmentos reales, se descarta. El prompt final de generación fundamentada usa los fragmentos reales recuperados junto con la pregunta original del usuario, no la respuesta hipotética.
¿Siempre se usa HyDE? No — es opcional, y tiene un costo real: una llamada adicional al LLM antes de siquiera tocar la base vectorial, con su propio time to first token y su propio costo por tokens (recordando el trilema de inferencia de Parte III). Vale la pena cuando hay un salto de registro conocido y consistente entre cómo pregunta la gente y cómo están escritos los documentos (preguntas coloquiales vs. reglamentos legales-formales); aporta poco cuando las preguntas ya se parecen al vocabulario de los documentos (por ejemplo, un sistema usado solo por especialistas). La forma correcta de decidirlo no es intuición — es correr evals de RAG (RAGAS, Capítulo 3) comparando el mismo conjunto de preguntas de prueba con y sin HyDE, y ver si la mejora en precisión y recall de contexto justifica el costo y la latencia adicional.
Contextual Retrieval: resolviendo el problema de chunking con más precisión todavía
Ya vimos el problema del chunking mal ajustado — un chunk demasiado agresivo puede perder el contexto que le da sentido al fragmento (recuerda el ejemplo de “los empleados con 2 a 5 años de antigüedad tienen derecho a 20 días hábiles” sin la oración anterior que aclara que habla de vacaciones). Anthropic publicó el 19 de septiembre de 2024 la técnica Contextual Retrieval en su blog oficial, Contextual Retrieval. Ataca este problema directamente, con resultados medidos y verificados, no solo teóricos.
El problema, con precisión: cuando trocean un documento largo (por ejemplo, un reporte financiero de varios trimestres), un chunk aislado como “los ingresos de la empresa crecieron un 3% respecto al trimestre anterior” pierde toda la información que le da sentido — ¿qué empresa? ¿qué trimestre? Esa pérdida de contexto puede hacer que un sistema RAG falle en encontrar la respuesta correcta, incluso cuando técnicamente el fragmento relevante sí está en la base de datos.
La solución, en dos técnicas combinadas:
-
Embeddings contextuales: antes de generar el embedding de cada chunk, se usa un LLM para generar un breve contexto (50-100 tokens) que sitúa ese fragmento dentro del documento completo — algo como “Este fragmento pertenece al reporte financiero del segundo trimestre de 2023 de la empresa ACME Corp, en la sección de resultados de ingresos” — y se antepone ese contexto al texto original del chunk antes de vectorizarlo. Ahora el embedding captura tanto el contenido como su ubicación dentro del documento más grande.
-
BM25 contextual: el mismo texto enriquecido con contexto (no solo el chunk original) se usa también para construir el índice de búsqueda por palabras clave (BM25, que ya vimos en el capítulo de Embeddings de Parte I) — combinando búsqueda semántica y por palabras clave sobre el mismo contexto enriquecido, no solo sobre el texto crudo original.
Los resultados, medidos por Anthropic sobre múltiples dominios de conocimiento (código, ficción, papers de ArXiv, papers científicos), usando como métrica la tasa de fallo en recuperar el fragmento correcto entre los primeros 20 resultados:
| Configuración | Tasa de fallo en recuperación | Reducción respecto al RAG tradicional |
|---|---|---|
| RAG tradicional (línea base) | 5.7% | — |
| + Embeddings contextuales | 3.7% | 35% menos fallos |
| + BM25 contextual (búsqueda híbrida completa) | 2.9% | 49% menos fallos |
| + Reranking (paso adicional, ver abajo) | 1.9% | 67% menos fallos |
¿Qué es el reranking? Un paso adicional, opcional pero efectivo: después de la recuperación inicial (rápida, usando similitud por coseno sobre los embeddings), un modelo especializado y más costoso computacionalmente —un reranker— vuelve a evaluar los mejores candidatos, comparando la pregunta directamente contra cada candidato de forma más profunda que una simple comparación de vectores precomputados. Es más lento que la búsqueda vectorial inicial, por eso solo se aplica sobre un conjunto ya reducido de candidatos, no sobre toda la base de datos — un filtro rápido primero, seguido de un juicio más cuidadoso solo sobre los finalistas. En el experimento citado, Anthropic recuperó inicialmente 150 chunks, los reordenó y conservó los 20 mejores.
Costo real de implementar esto: generar el contexto para cada chunk requiere una llamada adicional a un LLM durante la indexación (no en cada consulta, solo al construir la base de datos) — un costo que vale la pena gestionar con caché de prompts (el mismo concepto de Parte III) cuando se generan contextos para múltiples chunks del mismo documento fuente, y usando un modelo barato y rápido para esta tarea específica, no el modelo más costoso disponible.
Por qué esto importa para nuestro propio ejemplo de RR.HH.: si aplicáramos esta técnica a nuestra base de políticas de vacaciones, cada chunk de la política tendría, antepuesto, algo como “Este fragmento pertenece a la Política de Vacaciones vigente desde enero 2026, sección de cálculo por antigüedad” — resolviendo de raíz el problema que identificamos antes, donde un chunk mal cortado podía perder la referencia a “vacaciones” o a “antigüedad” que le daba sentido a la cifra de días.
Bajo el capó: el pipeline completo
# Preparación (una sola vez, al cargar la base de conocimientos)
chunks = trocear_documentos(documentos, tamaño_chunk=300)
for chunk in chunks:
vector = modelo_embeddings.encode(chunk.texto)
base_vectorial.guardar(vector, chunk.texto, chunk.metadata)
# En cada consulta del usuario
pregunta = "¿cuántos días de vacaciones tengo si llevo 3 años en la empresa?"
# (Opcional: HyDE) generar respuesta hipotética antes de buscar — se descarta después
respuesta_hipotetica = llm.generar(f"Responde de forma general: {pregunta}")
vector_busqueda = modelo_embeddings.encode(respuesta_hipotetica) # o encode(pregunta) sin HyDE
fragmentos_relevantes = base_vectorial.buscar(vector_busqueda, top_k=3)
# El prompt final usa la pregunta ORIGINAL, no la hipotética
prompt_final = f"""
Responde usando ÚNICAMENTE estos documentos. Si no está aquí, dilo.
{fragmentos_relevantes}
Pregunta: {pregunta}
"""
respuesta = llm.generar(prompt_final)
RAG resuelve “falta de conocimiento del dominio” y “falta de información actualizada” del Capítulo 1 — actualizar la política de vacaciones el próximo año es tan simple como reemplazar ese documento en la base vectorial, sin reentrenar nada. Pero para que el modelo pueda además actuar (tramitar la solicitud directamente, no solo informar), hace falta la pieza del Capítulo 5c: el Protocolo de Contexto de Modelo.
LinkedIn nelson.zepeda@simov.io SIMOV LABS
¿Encontraste un error? Sugerir una corrección