RAG: dándole tus datos al modelo
El conocimiento de un LLM se congeló en su fecha de corte de
entrenamiento; tus documentos cambian cada semana. La generación
aumentada por recuperación (RAG) lo descongela: en tiempo de consulta,
obtén las porciones relevantes de tus datos, pégalas en el prompt, y deja
que el modelo responda desde evidencia con citas. El modelo hace el
razonamiento; el índice hace la memoria.
La tubería, un ejemplo resuelto
Toma un manual de mantenimiento de 12,000 tokens y recórrelo de punta a
punta:
- Fragmenta. Parte primero por encabezados (las secciones sobreviven
intactas), luego re-parte cualquier sección por encima de ~400 tokens,
manteniendo ~60 tokens de solape (10–15%). Tamaños sensatos de
fragmento: 200–800 tokens. Resultado: unos 35 fragmentos. - Incrusta. Cada fragmento se vuelve un vector — bge-m3 emite 1,024
floats por fragmento. En CPU esto tarda segundos para todo el manual. - Indexa. Almacena vectores + texto + metadatos (archivo fuente,
ruta de encabezados) en un almacén vectorial. - Recupera. Incrusta la pregunta, obtén los k = 5 fragmentos
superiores por similitud coseno (4–10 es el rango sensato). - Genera. Ensambla el prompt: instrucciones, los 5 fragmentos con
etiquetas de ID, la pregunta.
Fragmentar por fronteras semánticas vence a las ventanas fijas. Una tabla
separada de su fila de encabezado, o un procedimiento numerado cortado en
el paso 4, es basura irrecuperable por bueno que sea el embedder.
Dos extras que se pagan solos de inmediato: guarda la ruta de encabezados
(manual › §4 › Pernos) como metadato para poder filtrar y citar con
precisión, e incrusta la consulta exactamente como la escribió el usuario
— la «limpieza» de preprocesado suele empeorar la recuperación, no
mejorarla.
Embeddings: el requisito multilingüe
El embedder decide qué significa «similar». Para coreano y corpus mixtos
coreano/inglés la elección es estrecha — los embedders solo-inglés fallan
en silencio con consultas en coreano, devolviendo fragmentos plausibles
pero equivocados:
| Modelo | Dims | Notas |
|---|---|---|
| bge-m3 | 1024 | multilingüe, coreano fuerte — el valor local seguro |
| nomic-embed-text-v1.5 | 768 | inglés sólido, contexto largo |
| all-MiniLM-L6-v2 | 384 | rápido, centrado en inglés, débil en coreano |
Los modelos de embedding son diminutos (0.1–0.6B parámetros); un núcleo
de CPU mastica miles de fragmentos — inferencia en CPU.
Incrusta el corpus una vez, incrusta las consultas al preguntar. Mantén
el embedder congelado por índice: cambiar de modelo a mitad de índice
produce vectores de espacios distintos, y las puntuaciones de similitud
se vuelven ruido.
Almacenes vectoriales
| Almacén | Cuándo |
|---|---|
| sqlite-vec | un solo usuario, hasta ~1M de vectores, cero ops — es un archivo |
| ChromaDB | prototipado local-first con filtros de metadatos |
| Qdrant | producción: filtrado, búsqueda híbrida, escala |
Los tres corren en local. Empieza con sqlite-vec; migra cuando de verdad
choques con su muro, no antes.
Ensamblar el prompt
Answer only from the excerpts below. Cite every claim as [n].
If the excerpts do not contain the answer, reply "not in the docs".
[1] (manual §4.2) Torque the M8 bolt to 12 Nm after lubrication...
[2] (manual §7.1) Do not re-use stretch bolts...
Question: what torque spec applies to the M8 bolt?
Las citas dejan a los usuarios verificar en vez de confiar. La salida de
«no está en los docs» importa: sin ella, el modelo inventa una respuesta
antes que fallar el formato.
Búsqueda híbrida y reranking
La búsqueda vectorial pura se pierde las cadenas exactas — códigos de
error, nombres de función, números de pieza — que la búsqueda por
palabras clave clava. Híbrido = BM25 + vectores, fusionado con fusión de
rango recíproco; es casi gratis y consistentemente mejor que cualquiera
por separado.
Para calidad, añade un reranker: recupera 20–50 candidatos, reordena
hasta el top 5 con un codificador cruzado (la familia bge-reranker — la
misma historia multilingüe que bge-m3). El reranker lee fragmento y
consulta juntos, lo que un embedder de un solo vector no puede. Receta
barata que cubre la mayoría de las necesidades: recuperación híbrida de
25, rerank a 5.
Diagnostica: ¿fallo de recuperación o de generación?
Antes de afinar nada, responde una pregunta: ¿se recuperó el fragmento
correcto?
- Imprime los fragmentos top-k de una consulta que falla, con
puntuaciones. - Si el fragmento con la respuesta no está en la lista, es un
problema de recuperación — arregla el fragmentado, los embeddings, o
sube k. - Si sí está en la lista pero la respuesta es equivocada, es un
problema de generación — modelo más grande, mejor prompt, rerank para
empujar fuera los distractores.
Esta única prueba ahorra horas de afinar la mitad equivocada de la
tubería.
Modos de fallo
| Síntoma | Causa | Arreglo |
|---|---|---|
| Documentos correctos recuperados, respuesta equivocada | distractores en el top-k | rerank; encoge k |
| La respuesta existe en los docs pero nunca sale | inanición del top-k o malas fronteras | sube k; fragmenta por encabezados |
| Respuestas confiadas pero caducas | índice más viejo que los docs | re-indexa al cambiar |
| Consultas en coreano no encuentran docs en inglés | embeddings monolingües | bge-m3 |
| Las citas apuntan al fragmento equivocado | el fragmento perdió sus metadatos de fuente | guarda fuente + encabezado por fragmento |
Mide, no improvises
Conserva un conjunto dorado de 20–50 preguntas reales con respuesta
conocida. Puntúa dos números por separado: tasa de acierto de
recuperación (¿está el fragmento correcto en el top-k?) y corrección de
la respuesta. Síguelos tras cada cambio de tamaño de fragmento, embedder
o k. El trabajo de RAG sin conjunto dorado es astrología.
Cuándo ajustar fino en su lugar
El ajuste fino cambia comportamiento — formato, estilo, terminología. Es
una mala forma de añadir hechos: el modelo no puede citar aquello sobre
lo que fue afinado, y los documentos frescos significan una corrida de
entrenamiento fresca. Regla rápida: hechos → RAG, comportamiento → LoRA,
y ambos solo cuando hayas agotado el prompting: ajuste
fino.
Pilas locales, de la más pequeña primero
Open WebUI trae subida de documentos y recuperación híbrida integradas —
empieza ahí y quizá nunca escribas código de tubería. El núcleo honesto
del RAG son ~50 líneas; escríbelo una vez para saber qué están haciendo
los frameworks:
chunks = chunk_by_header(read("manual.md"), max_tokens=400, overlap=60)
index = [(embed(c), c) for c in chunks] # hecho una vez
def answer(question):
hits = topk_by_cosine(embed(question), index, k=5)
ctx = "\n\n".join("[{}] {}".format(i, c) for i, (v, c) in enumerate(hits))
prompt = TEMPLATE.replace("{ctx}", ctx).replace("{q}", question)
return chat(prompt) # un modelo local pequeño es suficiente
Alcanza LlamaIndex o LangChain cuando necesitas permisos, refresco
programado o arneses de evaluación — no para el bucle de arriba. Para
documentos en coreano en específico, los embeddings multilingües (bge-m3)
no son opcionales, y el fragmentado por encabezados debería respetar la
estructura documental coreana (encabezados 장/절) igual que respeta los
encabezados markdown ##.
Relacionados: ajuste fino · prompting · salida estructurada · inferencia en CPU