El objetivo es una app que recibe un texto normal — «mañana tengo una reunión importante» — y lo devuelve hablado como Perico Delgado: coloquial, con refranes deformados y digresiones.
Podríamos resolverlo en veinte minutos metiendo treinta frases suyas en el prompt de un LLM — un large language model, el tipo de modelo que hay detrás de la API de Claude o de ChatGPT: le mandas texto y te devuelve texto. Ese atajo se llama few-shot: pegar un puñado de ejemplos en el propio prompt para que el modelo los imite.
Funciona regular, y se le ve el techo enseguida. Los ejemplos son fijos: van los mismos treinta escribas lo que escribas, aunque tu texto no se parezca a ninguno. No hay nada detrás guardando material, así que el sistema no mejora con el tiempo. Y cuando el prompt se llena, se acabó — más frases ya no caben, y añadir ejemplos deja de servir de nada.
Lo rechazamos a propósito, porque lo que vamos a construir es un corpus propio — la colección de textos que reúnes, limpias y guardas tú, y que es la materia prima de todo lo demás — y un pipeline RAG de verdad, es decir la cadena de pasos que lleva ese material desde la fuente hasta la respuesta final, cada paso leyendo lo que dejó escrito el anterior.
Qué es RAG
Retrieval-Augmented Generation: en vez de meterle al modelo siempre los mismos ejemplos, guardas miles de frases en una base de datos y, para cada petición, buscas y le pasas solo las relevantes.
El retrieval — la recuperación — es el paso que no existe en el few-shot, y es literalmente una búsqueda. Llega el texto del usuario, se consulta la base de datos, salen las diez o veinte frases que mejor encajan con esa petición, y son esas las que se pegan al prompt. El prompt acaba siendo igual de corto que antes; lo que cambia es que su contenido se elige en cada petición en vez de estar escrito a mano.
Ahí está la ventaja sobre meter ejemplos fijos: el corpus puede tener cincuenta mil frases sin que el prompt crezca ni un carácter, y añadir material al corpus mejora las respuestas sin tocar una línea de código.
La búsqueda no es por palabras clave, es por significado. Eso lo hacen los embeddings.
Qué es un embedding
Un embedding es la lista de números con la que un modelo representa un texto — un vector, en la jerga: unos cientos o miles de decimales. Piensa en un hash, pero al revés. Un hash está diseñado para que dos textos casi idénticos den resultados completamente distintos; un embedding está diseñado justo para lo contrario, para que dos textos que significan lo mismo den listas de números parecidas aunque no compartan ni una palabra.
Con eso, «buscar por significado» se convierte en aritmética. Se calcula el embedding de lo que escribió el usuario y se compara con el de cada frase guardada. La medida habitual es la similitud coseno: el ángulo entre los dos vectores, expresado como un número donde 1 es «apuntan exactamente al mismo sitio» y 0 es «no tienen nada que ver». Se ordena por esa cifra y se cogen las primeras.
El detalle de cómo se generan y dónde se guardan es la etapa 12. Aquí basta con la idea, porque de ella sale el problema de diseño que viene ahora.
La trampa que casi nos comemos
La búsqueda por embeddings recupera por tema, no por estilo. Si el usuario escribe «mañana tengo una reunión importante», la búsqueda intentará encontrar frases de Perico sobre reuniones. No existen. Devuelve ruido.
El corpus está lleno de estilo, pero se indexa por contenido — indexar es guardar el embedding de cada frase para poder buscar sobre él, y ese embedding recoge de qué habla la frase, no cómo suena. Los dos ejes no coinciden, y es el fallo de diseño más caro de descubrir tarde.
Para cada frase de Perico generamos con un LLM su paráfrasis en español llano — lo que en realidad estaba diciendo. Indexamos el embedding de la paráfrasis y guardamos la frase original al lado.
Así el texto normal del usuario casa contra español normal, y devolvemos la versión deformada. Los dos ejes quedan alineados y la recuperación aporta valor real.
El stack, y por qué
| Pieza | Elección | Motivo |
|---|---|---|
| Scraping y pipeline | Python | Scraping es bajar páginas ajenas y sacarles los datos a la fuerza, porque nadie te va a dar una API. El ecosistema de Python para eso y para machine learning no tiene rival. Es la parte que se aprende. |
| Base vectorial | Supabase + pgvector | Una base vectorial guarda embeddings y sabe ordenarlos por similitud. pgvector es la extensión que le añade eso a Postgres, y Supabase es Postgres alojado con su API delante: sin base de datos nueva que aprender. |
| Embeddings | text-embedding-3-small | El modelo que convierte cada frase en su vector. Multilingüe, sólido en español, céntimos por todo el corpus. |
| Generación | API de Claude | Buen español coloquial, que es justo lo que necesitamos. |
| API | FastAPI | Sigue en Python, sin cambiar de mundo a mitad de proyecto. |
| Frontend | React + TypeScript | La capa más convencional del proyecto: nada de lo que se hace ahí es específico de él. Se deja para el final a propósito. |