El sistema funcionaba en la demo
Montas el RAG con un corpus de documentación técnica en inglés. Recall@10 de 0,91 en tu conjunto de evaluación. Enseñas la demo, la demo convence.
Entonces llega el corpus real del cliente: manuales, circulares y pliegos, todo en español. Mismo modelo de embeddings, mismo código de ingesta, mismo prompt. Recall@10 de 0,64.
La reacción habitual es cambiar de modelo de embeddings. Es la pieza más visible y la más cara de tocar. También suele ser la que menos culpa tiene. El idioma no afecta a una capa del pipeline: atraviesa tres, y dos de ellas ni siquiera son el modelo.
El tokenizador cobra más por lo mismo
Los tokenizadores BPE de los modelos generalistas se entrenaron sobre corpus donde el inglés manda. Eso significa que una palabra inglesa frecuente suele ser un token, mientras que su equivalente en español se parte en tres o cuatro piezas. “Settings” es un token; “configuraciones” rara vez lo es.
La consecuencia no es filosófica, es de presupuesto. Un chunk de 512 tokens guarda menos contenido en español que en inglés, así que troceas más fino sin haberlo decidido tú. Y el coste por documento sube al traducir el corpus aunque el número de páginas sea el mismo.
Antes de suponer nada, mídelo en tu texto real:
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
es = "Las configuraciones de seguridad se restablecen automáticamente."
en = "The security settings are automatically restored."
print(len(enc.encode(es))) # más tokens
print(len(enc.encode(en))) # menos, para el mismo significado
# Lo que importa no es este par de frases, sino la razón sobre tu corpus:
# tokens_es / tokens_en en una muestra de 200 documentos traducidos.
# Con esa razón ya puedes ajustar el tamaño de chunk en lugar de heredarlo.
El arreglo aquí no es cambiar de modelo. Es aceptar que tu tamaño de chunk venía de un tutorial escrito en inglés y recalcularlo.
La mitad léxica no sabe español
Si usas búsqueda híbrida —y deberías—, la mitad de tu recall viene de BM25 o de
tsvector. Esa mitad es sensible al idioma de una forma brutal, porque trabaja
con las formas de las palabras y no con su significado.
Dos fallos concretos, los dos silenciosos:
Los acentos. El documento dice “garantía”. La persona escribe “garantia”, porque está en el móvil y con prisa. Sin plegado de acentos, la coincidencia léxica es cero. El híbrido no falla con estrépito: degrada a búsqueda vectorial pura y tú lo ves como “el retriever está flojo hoy”.
La morfología. El español flexiona más que el inglés: género, número, conjugaciones, enclíticos. “Configurarlo”, “configuraciones” y “configuró” son la misma raíz para cualquiera menos para un índice sin stemmer en español.
En Elasticsearch el analizador spanish trae stopwords y stemmer, pero no pliega
acentos: asciifolding hay que añadirlo a mano. En PostgreSQL la configuración
spanish stemiza, y para los acentos necesitas la extensión unaccent metida en
la propia configuración de búsqueda:
CREATE EXTENSION IF NOT EXISTS unaccent;
-- Configuración propia: primero quita acentos, luego stemiza en español.
CREATE TEXT SEARCH CONFIGURATION es_rag ( COPY = spanish );
ALTER TEXT SEARCH CONFIGURATION es_rag
ALTER MAPPING FOR hword, hword_part, word
WITH unaccent, spanish_stem;
-- Ahora "garantia" y "garantías" caen en el mismo lexema.
SELECT to_tsvector('es_rag', 'período de garantías extendidas');
-- 'extend':4 'garant':3 'period':1
-- Indexa la columna generada, no la expresión suelta:
ALTER TABLE chunks ADD COLUMN tsv tsvector
GENERATED ALWAYS AS (to_tsvector('es_rag', texto)) STORED;
CREATE INDEX chunks_tsv_idx ON chunks USING GIN (tsv);
Una tarde de trabajo, coste de inferencia cero, y recupera consultas que hoy no devuelven nada.
El encoder multilingüe alinea traducciones
Los encoders multilingües se entrenan, entre otras cosas, para que una frase y su traducción caigan juntas en el espacio vectorial. Es exactamente lo que quieres para buscar en inglés sobre documentos en español.
Y es exactamente lo que te destroza el top-k cuando el corpus tiene el mismo manual en los dos idiomas. El vecino más cercano de un párrafo en español no es el párrafo que responde a la pregunta: es ese mismo párrafo en inglés. Pides diez resultados y recibes cinco pares, es decir, cinco contenidos distintos ocupando diez plazas.
El síntoma es un contexto que parece lleno y responde mal. La causa no es el
recall, es la diversidad del top-k. Se arregla con un metadato lang por chunk y
un filtro previo, o deduplicando pares traducidos en ingesta. Nada de esto exige
tocar el modelo.
Síntoma, causa, dónde se arregla
| Síntoma | Causa probable | Dónde se arregla |
|---|---|---|
| El coste por documento sube al traducir el corpus | el tokenizador parte el español en más piezas | tamaño de chunk |
| La consulta sin tildes no devuelve nada | el índice léxico no pliega acentos | analizador del índice |
| ”Garantías” no encuentra “garantía” | stemmer ausente o en inglés | analizador del índice |
| El top-10 son cinco contenidos duplicados | alineamiento entre idiomas del encoder | metadato de idioma y deduplicación |
| Recall bajo y sin patrón claro | el conjunto de evaluación está traducido | conjunto de evaluación |
El conjunto de evaluación traducido miente
Este es el error que más caro sale, porque esconde a todos los demás.
Coges tus 200 preguntas de evaluación en inglés, se las pasas a un LLM para que las traduzca y te declaras cubierto. El problema es que la traducción y el documento comparten registro: ambos salen de un texto formal, con el vocabulario del manual. Tus métricas salen razonables y el sistema sigue fallando en producción.
Las consultas reales en español no se parecen a eso. La gente escribe “cuanto tarda en llegar”, no “¿cuál es el plazo de entrega estimado?”. Sin tildes, sin signo de apertura, con el término coloquial en lugar del término del pliego.
Con 30 consultas copiadas del buzón de soporte mides mejor que con 500 traducidas. Y son las únicas que detectan los dos fallos léxicos de arriba, porque son las únicas que llegan mal escritas.
Por dónde empezar
El orden importa, y es casi el inverso del instinto:
- Reúne 30 consultas reales en español y mide recall@10 antes de tocar nada. Sin esta línea base, cualquier cambio posterior es fe.
- Arregla la capa léxica. Plegado de acentos y stemmer en español. Es lo más barato del pipeline y suele ser lo más roto.
- Etiqueta el idioma de cada chunk y filtra antes de buscar, si el corpus es mixto.
- Recalcula el tamaño de chunk con la razón de tokens de tu propio corpus.
- Y solo entonces, compara modelos de embeddings. Con una línea base decente,
sabrás si el cambio aporta algo o si estabas pagando por tapar un
unaccentque faltaba.
Cambiar de modelo es lo primero que se propone en la reunión y lo último que conviene hacer. En un corpus en español, casi todo el recall perdido está en la tubería, no en el encoder.