2026-09-21 — RAG sobre frontier: arquitectura y MLOps por tamaño de BDD

Contexto: pregunta de Nef (“cómo se hace RAG sobre frontier para BDD personales de diferentes tamaños”). Aplicado a sus repos reales: second-brain (grafo, ~cientos de nodos) y nef-silo (~2k items/día, SQLite+FTS5+RRF ya en producción en Linode).

Principio rector

RAG sobre frontier = tu BDD nunca entra al modelo; entra el top-k recuperado. Por lo tanto la calidad se decide en el pipeline (parsing, chunking, retrieval, evals), no en el prompt. El frontier solo redacta y razona sobre lo que le pusiste enfrente. “Mejorar el RAG” casi nunca es cambiar de modelo: es arreglar retrieval o evals.

Tabla de decisión por tamaño

ChunksStackANNEmbeddingsOps
<100kSQLite + FTS5 + sqlite-vec o LanceDB. Un archivo, cero servidorNo (fuerza bruta coseno <50ms)Local (bge-m3 / nomic-embed)Ninguna. Rebuild completo en segundos
100k–5MQdrant o LanceDB o pgvector single-nodeHNSWLocal batch o APIJobs incrementales, namespaces por usuario, snapshots
5M–100MQdrant/Milvus cluster o DiskANNHNSW + cuantización binaria + rescoreServicio de embeddings dedicado (GPU o batch API)Cola de ingesta, sharding por tenant/fecha, índice caliente/frío
>100MDiskANN/Vamana en disco, particiones por dominioTwo-stage obligatorioPipeline distribuidoInfra de evals offline, versionado de índice, pre-filtros duros

Regla de escalado real: el costo de ops escala con tasa de actualización y volumen de queries, no solo con tamaño. Un archivo estático de 10M chunks es más fácil que un feed de 50k que cambia cada hora. BDD personal casi siempre es Tier 0–1; saltar a cluster es el error clásico de over-build.

Pipeline canónico (idempotente)

fetch → dedupe (hash de contenido) → parse → chunk
  → embed (cache por hash) → upsert con IDs estables → índice

Reglas no negociables:

  1. IDs de chunk content-addressed: hash(texto + versión_parser + id_modelo_embedding). Re-ingesta = solo lo nuevo/cambiado se embebe. Nunca re-embebes el corpus completo por un cambio de código.
  2. Metadata por chunk: doc_id, sección, posición, fecha, tenant, versión del documento. Filtrable en ambas ramas (léxica y densa).
  3. Dos ramas siempre: BM25/FTS + denso → fusión RRF k=60 → rerank (cross-encoder) sobre top-50 → top-5/10 al LLM. Los filtros de faceta idénticos en ambas ramas.
  4. Chunking: 256–512 tokens para retrieval; expansión a “parent” (sección/párrafo completo) antes de mandar al LLM. En científico/técnico, partir por sección, nunca por tokens ciegos.

Ciclo de vida del índice (lo que casi todos ignoran)

  • El índice guarda: id_modelo_embedding, dims, normalización, versión de parser, fecha de build. Sin esto no hay migración posible.
  • Cambiar de modelo de embedding = dual-write: índice nuevo en paralelo, backfill por lotes, eval comparativa, swap de alias. No hay atajo.
  • Re-embedding es un job batch con checkpoint, no un script de una corrida.
  • Snapshots con fecha: poder volver al índice de ayer es tu rollback.

Evals (el 80% del valor MLOps)

  • Golden set: 100–200 pares query→doc relevante, construidos a mano sobre tu corpus. Mide recall@20 (target >0.9) y nDCG.
  • Faithfulness: LLM-as-judge sobre 30 respuestas muestreadas: ¿cada afirmación está en un pasaje citado? Es la métrica anti-alucinación.
  • Regresión en CI: cualquier cambio de chunker, embedder, reranker o prompt corre el golden set. Sin esto estás adivinando.
  • Guardar el golden set en el repo, no en la cabeza.

Observabilidad y costos

  • Log por query: query, IDs recuperados + scores, respuesta, latencia, tokens in/out, costo, feedback (thumb). JSONL basta al inicio; Langfuse/Phoenix cuando haya volumen.
  • Prompt caching (Anthropic/OpenAI): instrucciones y prefijos estables en la parte cacheable; puede cortar costo de input hasta ~90% en consultas repetidas.
  • Batch API (≈50% descuento) para trabajo offline: resúmenes, metadata, embeddings vía API.
  • Routing de modelos: modelo barato para query rewrite / multi-query / HyDE / rerank; frontier solo para síntesis final.
  • Citations nativas: Anthropic Citations API y file_search de OpenAI devuelven grounding verificable — preferirlas antes de inventar tu propio esquema de citas.
  • Envelope realista BDD personal: 100k chunks embebidos una vez local = $0; query con ~10k tokens de contexto en frontier ≈ centavos. 1k queries/mes = decenas de USD. Verificar precios vigentes antes de prometer cifras.

Local-first (tu caso)

Mismo pipeline, dos switches:

  • Todo local: bge-m3 en Ollama + sqlite-vec/LanceDB + Qwen3 para síntesis. Costo marginal cero, calidad de redacción menor.
  • Híbrido (recomendado): retrieval y embeddings locales; frontier solo en la síntesis, con fallback local si no hay red. El corpus nunca sale de la máquina; solo salen los top-k pasajes.

Aplicado a tus repos

  • second-brain (cientos de nodos): Tier 0 puro. No hace falta vector DB: FTS + embeddings en SQLite y ya. El grafo de edges es tu reranker estructural (vecinos del nodo como boost).
  • nef-silo (~2k–20k items, feed diario): ya está en Tier 0/1 con lo correcto (FTS5 + RRF k=60 + embeddings sin torch). Siguiente paso natural es sqlite-vec para ANN cuando brute force pase de ~100ms p95, no antes. El rerank neural que está diferido es exactamente lo que un frontier no puede hacer por ti (es retrieval, no generación).

Open-weights: qué capa ocupa cada uno

Open-weights no compite con frontier “en general”; ocupa capas concretas del stack. Mapa por capa:

CapaOpen-weights (típico)¿Frontier?
Embeddingsbge-m3, nomic-embed, e5, SPECTER2 (científico)API (OpenAI/Cohere/Voyage), poco ventajoso
Rerankerbge-reranker-v2-m3, mxbai-rerank, Qwen3-RerankerCohere Rerank (API)
Query rewrite / multi-query / HyDE / extracción de filtrosQwen3-0.6B–1.7B, Llama-3.2-1B/3BOverkill y caro para esto
Clasificación / NER / metadata de PDFsModelos pequeños fine-tuneadosPrompting funciona pero a 100× costo
Síntesis finalQwen3-30B-A3B, Llama-70B, GLM, DeepSeek (si hay VRAM)Donde frontier gana hoy
Juez de evalsOpen-weights como juez masivoFrontier para calibrar una muestra

Patrones de relación (los tres reales):

  1. Routing / fallback: open-weights local primero; se escala a frontier solo cuando la tarea es difícil o el modelo local falla. En tu caso (System76 + Ollama) es el default natural, con frontier como excepción.
  2. Teacher-student (distilación): frontier genera etiquetas/trazas offline una vez → fine-tune de un modelo pequeño open-weights para la tarea estrecha (rewrite, extracción de citas, clasificación). Así el costo del frontier desaparece en runtime. Es el patrón MLOps de mayor ROI para BDD personales.
  3. Pipeline de dos velocidades: open-weights para los pasos de alta frecuencia y bajo juicio; frontier para el paso raro de alto juicio.

Lo que open-weights te da y una API no: fine-tuning real, quantización, offline, reproducibilidad (pin del checkpoint exacto), KV cache propio, logprobs/estados internos, batch gratis de madrugada, cero rate limits, y que el corpus nunca sale de la máquina. Lo que no te da: la calidad de síntesis y razonamiento de un frontier grande en contexto largo.

Conclusión operativa: el binario open/frontier es falso. La arquitectura estándar es open-weights en todas las capas excepto la última milla, y la última milla es pluggable (swap frontier↔local sin tocar el pipeline). Verificar SOTA vigente al elegir: cambia cada trimestre.

Checklist mínimo para arrancar

  1. Golden set de 50–100 queries antes de tocar el pipeline.
  2. IDs content-addressed + cache de embeddings.
  3. Híbrida con RRF desde el día uno (es ~20 líneas con FTS5).
  4. Log JSONL de queries y retrieved IDs.
  5. Recién entonces: elegir frontier, prompt caching y loop de agente.
  6. Interfaz de síntesis como switch (local/frontier) desde el inicio — no cablear un proveedor en el pipeline.