Un chunk sin contexto es un chunk que no se encuentra
Trocear un documento para meterlo en un RAG tiene un efecto secundario del que casi nadie habla: cada chunk pierde de vista el documento del que salió.
Imagina un informe financiero en tu base de conocimiento y esta pregunta: “¿cuánto creció la facturación de ACME en el segundo trimestre de 2023?”. El chunk que responde dice, literal: “los ingresos crecieron un 3 % respecto al trimestre anterior”. Correcto y, a la vez, inútil. No menciona ACME. No menciona 2023. Ni el embedding ni BM25 tienen forma de saber que ese fragmento es la respuesta, porque el fragmento ya no sabe de qué habla.
Ese es el fallo silencioso de la mayoría de los RAG: no recuperan mal por culpa del modelo de embeddings, sino porque al trocear destruyeron el contexto que hacía recuperable cada trozo. La recuperación contextual, la técnica que Anthropic publicó en 2024, ataca justo ese punto. Y lo hace antes de indexar, no en tiempo de consulta.
La idea: contexto delante de cada chunk
La receta es casi vergonzosamente simple. Antes de generar el embedding y de construir el índice BM25, se antepone a cada chunk un par de frases que lo sitúan dentro de su documento.
El fragmento de ACME deja de ser un huérfano:
chunk_original = "Los ingresos crecieron un 3 % respecto al trimestre anterior."
chunk_contextualizado = (
"Este fragmento procede de un informe sobre los resultados de ACME "
"en el segundo trimestre de 2023; el trimestre anterior la facturación "
"fue de 314 millones. Los ingresos crecieron un 3 % respecto al "
"trimestre anterior."
)
Ahora “ACME”, “2023” y “trimestre” viven dentro del vector y dentro del índice léxico. La pregunta del usuario y el chunk comparten por fin vocabulario. Ese contexto de más, unos 50 a 100 tokens, es lo que separa un fragmento inencontrable de uno que aparece el primero.
Contextualizar a mano miles o millones de chunks no es viable, claro. La gracia está en que el propio contexto lo escribe un LLM barato, chunk a chunk.
Cómo se genera el contexto
Se pasa a un modelo pequeño el documento entero más el chunk concreto, y se le pide una frase que lo ubique. Nada más.
PROMPT_CONTEXTO = """<documento>
{documento}
</documento>
Este es el fragmento que queremos situar dentro del documento:
<fragmento>
{fragmento}
</fragmento>
Da un contexto breve y conciso para situar este fragmento dentro del
documento, con el fin de mejorar su recuperación en una búsqueda.
Responde solo con ese contexto, nada más."""
def contextualizar(documento: str, fragmento: str) -> str:
contexto = llm_barato.completar(
PROMPT_CONTEXTO.format(documento=documento, fragmento=fragmento)
)
return f"{contexto}\n\n{fragmento}" # el contexto va delante
El texto resultante se usa para dos cosas: se le calcula el embedding (contextual embeddings) y se mete en el índice léxico (contextual BM25). En consulta no cambia nada: el usuario pregunta igual, y ambos índices aciertan más porque los chunks por fin saben quiénes son.
Un matiz que ahorra dinero de verdad: como reprocesas el mismo documento para todos sus chunks, el prompt caching hace este paso casi gratis. Cargas el documento en caché una vez y lo referencias en cada llamada. Con chunks de 800 tokens y documentos de 8k, contextualizar sale por poco más de un dólar por millón de tokens de documento. Es preprocesamiento que pagas una sola vez, no coste por consulta.
Por qué también hace falta BM25
Los embeddings capturan significado, pero se les escapan las coincidencias exactas. Y en soporte técnico o documentación las coincidencias exactas son media batalla.
Si un usuario busca “código de error TS-999”, el embedding te traerá contenido sobre errores en general, pero puede fallar el TS-999 concreto. BM25, que es puro emparejamiento léxico sobre TF-IDF, encuentra ese identificador clavado. Por eso la recuperación contextual no elige entre semántica o léxico: contextualiza los dos índices y luego funde los resultados con rank fusion, quedándose con lo mejor de cada uno.
Semántica para la intención, léxico para los identificadores. Contexto para que ninguno de los dos se pierda.
Cuánto mejora, con números
Lo interesante de esta técnica es que los beneficios se apilan. Anthropic midió la tasa de fallo de recuperación como 1 − recall@20: el porcentaje de preguntas cuyo chunk correcto no aparece entre los veinte recuperados.
| Configuración | Fallo @20 | Reducción |
|---|---|---|
| Embeddings + BM25 (RAG clásico) | 5,7 % | — |
| + contextual embeddings | 3,7 % | −35 % |
| + contextual embeddings y contextual BM25 | 2,9 % | −49 % |
| + reranking encima | 1,9 % | −67 % |
Casi la mitad de los fallos de recuperación desaparecen solo con anteponer contexto, sin tocar el modelo de embeddings ni el reranker. Y si ya tienes un reranker en la tubería, el conjunto se lleva por delante dos tercios de los fallos. Son mejoras que se notan aguas abajo: un chunk que la búsqueda nunca trae es un chunk que el generador nunca podrá usar.
Qué vigilar al implementarlo
No es magia, y hay decisiones que cambian el resultado.
El tamaño y las fronteras del chunk siguen importando: contextualizar no arregla un troceado que parte una tabla por la mitad. El modelo de embeddings también manda; la técnica mejora todos, pero unos aprovechan el contexto más que otros. Y el prompt genérico funciona, pero un prompt afinado a tu dominio, con un glosario de términos que solo se definen en otros documentos, rinde aún más.
Un apunte para la generación de la respuesta, no solo la recuperación: al construir el prompt final conviene distinguir qué parte es el contexto añadido y qué parte es el chunk original, para que el modelo no confunda la anotación con la fuente.
Y la regla de siempre: mide. La recuperación contextual añade un paso de preprocesamiento y algo de latencia al indexar, no en consulta. Se justifica sola cuando ves tu recall@k subir sin haber cambiado el índice ni el modelo.
Cómo empezar mañana
No reconstruyas toda tu tubería de golpe. Coge veinte preguntas que tu RAG resuelve mal, mira los chunks que recupera y comprueba cuántos son huérfanos sin contexto propio. Si el patrón se repite, contextualiza solo esa colección con un modelo barato y prompt caching, y vuelve a medir el recall.
El mejor reranker del mundo no puede reordenar un chunk que la búsqueda nunca trajo, y la búsqueda no trae lo que no sabe nombrar. Devuélvele a cada fragmento la memoria de dónde venía y la mitad de tus fallos de recuperación se van solos.