Facsímil 04 · Completo

La caja de herramientas

APIs, modelos locales, RAG, laboratorios, despliegue y herramientas de trabajo para convertir modelos en soluciones utilizables.

Contenido disponible
14 de 14 capítulos listos
Contenido completo, pendiente de revisión editorial final.
Estado editorial
Completo
Lectura web generada desde los capítulos Markdown originales.

Sobre esta edición

Esta página se genera desde capítulos Markdown propios del facsímil. Las fórmulas se renderizan con KaTeX, los mapas con Mermaid y las notas al pie se mantienen junto al texto para leer el facsímil como una pieza autónoma, no como una exportación del taller.

Capítulo 01

Facsímil 4 · La caja de herramientas

Capítulo 01: Elegir la intervención correcta: prompt, RAG, tool o ajuste

La herramienta no es el punto de partida

El error más fácil en IA aplicada es empezar por la herramienta. “Hagamos RAG”, “probemos fine-tuning”, “pongamos un agente”, “lo llevo a local”, “metamos una base vectorial”. Suena activo, pero puede ser una forma elegante de no diagnosticar.

Este facsímil va de herramientas, sí. Pero las herramientas no son trofeos. Son respuestas a síntomas concretos. Si el modelo no respeta un formato, quizá necesitas salida estructurada. Si no conoce normativa interna, quizá necesitas RAG. Si debe consultar stock real, quizá necesita una tool. Si responde bien pero con tono inestable en miles de casos repetidos, quizá tiene sentido ajustar. Si los datos no pueden salir de una máquina, quizá toca local.

La caja de herramientas empieza con una pregunta humilde: ¿qué parte del sistema falla y qué cambio mínimo la arregla mejor?

Estado del arte con fecha de corte

Fecha de corte: 25 de mayo de 2026.
Fuentes consultadas ese día: documentación oficial de Anthropic sobre prompt engineering, OpenAI sobre salidas estructuradas y function calling, Hugging Face PEFT/LoRA, documentación de Ollama API y Cloud, y el paper original de RAG.

Esta foto cambia rápido. Lo estable no es el nombre de una herramienta concreta, sino el patrón de decisión: contexto, evidencia, schema, tool, ajuste, local, evaluación y coste.

En 2026, las plataformas de modelos ya no ofrecen solo “texto dentro, texto fuera”. Las APIs modernas permiten schemas, tool calls, streaming, batch, cache y distintos modelos para coste o calidad. OpenAI documenta salidas estructuradas para forzar que una respuesta siga un schema.1 Anthropic mantiene guías de prompt engineering como primera capa de control antes de añadir arquitectura.2

También se consolidó una separación práctica: RAG para conocimiento externo, PEFT/LoRA para adaptar comportamiento con pocos parámetros y modelos locales o cloud local-compatible cuando la restricción está en privacidad, coste, portabilidad o experimentación.3 Ollama documenta una API local y una variante cloud con una experiencia parecida para modelos que no caben en hardware personal.45

Qué no es una caja de herramientas de IA

Una caja de herramientas no es una lista de marcas. Si no sabes qué síntoma resuelve cada pieza, el catálogo solo añade ruido. La persona que sabe usar herramientas no es quien instala más librerías, sino quien simplifica antes de complicar.

Tampoco es una escalera de prestigio. Prompt no es “poco serio” y fine-tuning no es “más profesional” por defecto. RAG no es superior a una consulta SQL si la pregunta exige datos exactos. Un modelo local no es mejor que una API si no cumple calidad o mantenimiento. Cada elección compra algo y paga algo.

Y una caja de herramientas no sustituye a evaluación. Puedes montar RAG, tool calling, LoRA y modelo local; si no tienes casos de prueba, no sabes si has mejorado o solo has construido una máquina más difícil de entender.

La pregunta correcta: qué quieres cambiar

Antes de elegir intervención, separa cuatro planos. El primero es entrada: quizá el modelo ya puede hacer la tarea si le das mejores instrucciones, ejemplos y formato esperado. El segundo es contexto: quizá necesita documentos, datos o memoria externa. El tercero es acción: quizá debe consultar una API, calcular algo o escribir en un sistema. El cuarto es comportamiento aprendido: quizá quieres que responda de una forma estable en una tarea repetida.

Esa separación evita mezclar herramientas:

Si el problema es...Primera intervención razonablePor qué
Formato irregularPrompt claro y salida estructuradaEl modelo sabe responder, pero falta contrato de salida.
Conocimiento vivoRAG o consulta a fuente externaEl dato cambia y debe poder citarse o verificarse.
Cálculo, estado o acciónTool con schema y permisosEl modelo no debe inventar resultados de sistemas externos.
Tono o taxonomía repetidaEjemplos, luego fine-tuning si escalaCambias comportamiento estable, no conocimiento vivo.
Privacidad u offlineModelo local o entorno privadoLa restricción vive en dónde se ejecuta.
Coste o latenciaModelo más pequeño, cache, batch o localEl problema puede estar en serving, no en “inteligencia”.

Para verlo con un caso cercano: una universidad quiere contestar dudas de matrícula. Si las reglas cambian cada curso, RAG o consulta a la base oficial. Si el problema es devolver siempre JSON para integrarlo en una app, salida estructurada. Si debe comprobar si una persona ya pagó, tool contra el sistema de pagos. Si cada respuesta debe tener tono institucional, ejemplos y quizá ajuste más adelante.

La fórmula mental del diagnóstico

No hace falta una ecuación para decidir, pero ayuda tener una forma compacta de pensar.

Ejemplo de fórmula. Como regla pedagógica de decisión, no como métrica universal, una intervención merece la pena cuando la mejora esperada supera su coste, su complejidad y su riesgo operativo:

U(intervencioˊn)ΔQCKMRU(\text{intervención}) \approx \Delta Q \cdot C - K - M - R
SímboloSignificadoEjemplo
ΔQ\Delta QMejora esperada de calidad útil.Más respuestas con cita correcta.
CCConfianza en que la mejora se mantenga.Eval con 100 casos reales.
KKCoste económico y de latencia.Tokens, GPU, vector DB, reintentos.
MMMantenimiento añadido.Índices, adapters, schemas, versiones.
RRRiesgo operativo.Datos, permisos, acciones, regresiones.

La fórmula no pretende dar una verdad exacta. Pretende impedir una mala costumbre: contar solo la mejora y olvidar todo lo que tendrás que operar después.

Las cinco intervenciones básicas

Primero hay que explicar el síntoma. Luego la herramienta empieza a tener sentido. Estas cinco piezas vuelven durante todo el facsímil.

La tabla siguiente es una síntesis didáctica, no una taxonomía oficial única. Se apoya en prompt engineering6 para controlar tarea, contexto y ejemplos; en salidas estructuradas7 para imponer contratos de respuesta; en RAG8 para añadir evidencia recuperada; en function calling9 para conectar el modelo con funciones externas; y en LoRA/PEFT10 para adaptar comportamiento mediante pocos parámetros entrenables.

IntervenciónCambiaSirve cuandoNo sirve para
Prompt y ejemplosLa entradaFalta claridad, formato o criterio de respuesta.Datos vivos grandes o acciones externas.
Salida estructuradaEl contrato de salidaNecesitas JSON, campos obligatorios o validación automática.Saber más contenido.
RAGEl contexto recuperadoHay documentos, políticas o conocimiento cambiante.Cambiar el estilo profundo del modelo.
ToolLa capacidad de consultar o actuarHay estado real, cálculo, base de datos o sistema externo.Sustituir permisos o validación.
Fine-tuning/LoRAAlgunos pesos o adaptadoresTarea repetida, estable y medible.Actualizar información que cambia cada día.

RAG nació como una forma de combinar memoria paramétrica con recuperación externa en tareas intensivas en conocimiento.11 LoRA propuso adaptar modelos grandes entrenando matrices pequeñas de bajo rango.12 QLoRA redujo aún más memoria al ajustar sobre modelos cuantizados.13

Para entenderlo antes de tocar código

Antes de escribir una línea de código conviene hacer una prueba mental muy simple: describe el fallo como lo vería una persona usuaria, no como lo nombraría una librería. Después tradúcelo a una capa del sistema. Esa traducción es el músculo que queremos entrenar en este capítulo.

La misma frase “el modelo falla” puede significar cinco cosas distintas. Puede fallar porque no recibió instrucciones claras, porque le falta evidencia, porque necesita consultar un sistema externo, porque debe repetir un criterio muy específico o porque el entorno de despliegue impone límites. Si llamas a todo “modelo malo”, acabarás cambiando piezas equivocadas.

CasoSeñal observableDiagnósticoPrimera intervenciónQué medir
Asesoría con normativa cambianteResponde bien hasta que cambia una norma.Falta evidencia viva y trazable.RAG o consulta a fuente oficial con cita.Porcentaje de respuestas con documento correcto y fecha vigente.
CRM que necesita campos exactosEl texto es bueno, pero el backend rompe al parsear.Falta contrato de salida.Salida estructurada con schema y validación.Campos válidos, errores de schema y casos que requieren revisión.
Tienda con stock realPreguntan por talla, precio o disponibilidad.Hace falta estado externo.Tool pequeña contra inventario o pedidos.Exactitud del dato, latencia de consulta y manejo de “no disponible”.
Soporte con miles de tickets parecidosResponde con criterio parecido, pero formato y tono varían.Conducta repetida poco estable.Prompt con ejemplos; si escala, SFT, LoRA o adapter.Consistencia de rúbrica, coste por ticket y regresiones.
Equipo con documentos sensiblesLa mejor API externa no puede recibir ciertos textos.Restricción de entorno.Modelo local, entorno privado o arquitectura híbrida.Calidad mínima aceptable, latencia, coste operativo y trazabilidad.

Fíjate en el tercer caso. Si una persona pregunta “¿quedan zapatillas talla 42?”, el modelo puede escribir una respuesta convincente, pero no conoce el almacén. La intervención correcta no es pedirle que “razone mejor”. Es darle una función con un contrato estrecho: consultar_stock(producto, talla, tienda). La inteligencia del sistema no está solo en el modelo; está en saber cuándo el modelo debe dejar de completar texto y pedir un dato.

Ahora mira el primer caso. Si la asesoría trabaja con normas que cambian, ajustar pesos puede incluso empeorar la situación: convierte conocimiento vivo en memoria opaca. RAG no entra porque sea una palabra de moda, sino porque necesitamos tres cosas muy concretas: recuperar el fragmento vigente, citarlo y poder actualizarlo sin entrenar de nuevo.

El caso del CRM enseña otra lección. A veces el modelo “entiende” perfectamente la tarea, pero el producto necesita datos que se puedan validar. Ahí no queremos literatura: queremos {categoria, prioridad, siguiente_paso, confianza} y una regla clara para rechazar respuestas inválidas. La salida estructurada no mejora el mundo interno del modelo; mejora el contrato entre el modelo y el software que lo rodea.

Y el equipo de soporte muestra cuándo empieza a tener sentido ajustar. Si el conocimiento ya está resuelto, los documentos aparecen bien y el schema se cumple, pero la respuesta no respeta una rúbrica interna en miles de ejemplos parecidos, entonces sí: quizá toca entrenar una adaptación pequeña. Pero esa decisión llega después de medir, no antes.

Una regla útil: si puedes arreglarlo cambiando entrada, contexto o contrato, no empieces cambiando pesos. Los pesos se tocan cuando quieres estabilizar una conducta repetida, tienes ejemplos buenos, sabes medir el resultado y aceptas mantener otra versión del sistema.

Criterios de elección: la matriz que decide contigo

Si esto fuera una asignatura universitaria, aquí no bastaría con decir “depende”. El “depende” tiene que descomponerse en variables observables. Una buena decisión técnica no es la que suena más moderna, sino la que explica qué restricción pesa más y qué evidencia aceptaríamos para cambiar de opinión.

La siguiente matriz sirve para discutir una intervención en clase, en un equipo o en una revisión de arquitectura. No da una respuesta automática, pero obliga a justificarla.

CriterioPregunta que debes hacerSi pesa mucho, suele empujar hacia...Evidencia mínima
Conocimiento cambiante¿La respuesta depende de datos que cambian con frecuencia?RAG, base de datos o tool.Documentos con fecha, fuente y casos donde el dato cambia.
Necesidad de cita¿La persona debe poder revisar de dónde sale la respuesta?RAG con citas o consulta verificable.Porcentaje de respuestas con evidencia correcta.
Estado real¿Hace falta mirar inventario, pagos, agenda, expediente o cálculo externo?Tool con schema estrecho.Función definida, entrada validada y salida comprobable.
Formato estricto¿Otro software consume la respuesta?Salida estructurada.JSON válido, campos obligatorios y tests de schema.
Conducta repetida¿La misma tarea aparece miles de veces con criterios estables?Prompt con ejemplos; si no basta, SFT, LoRA o adapter.Dataset pequeño pero limpio, rúbrica y comparación contra baseline.
Privacidad o despliegue¿Dónde puede ejecutarse el sistema y dónde pueden vivir los datos?Modelo local, entorno privado o arquitectura híbrida.Política de datos, latencia aceptable y calidad mínima medida.
Coste y latencia¿El problema es calidad o servirlo sin arruinar la experiencia?Modelo menor, cache, batch, cuantización o local.Coste por consulta, TTFT, tokens/s y percentiles de latencia.
Reversibilidad¿Podemos deshacer el cambio si empeora?Prompt, schema, RAG o tool antes que ajuste de pesos.Plan de rollback y comparación antes/después.

Esta matriz también evita discusiones tramposas. Si alguien propone fine-tuning para un problema de stock, puedes preguntar: “¿qué criterio de la tabla justifica cambiar pesos?”. Si nadie puede responder, todavía no hay diagnóstico.

Un mismo caso, cinco soluciones posibles

Tomemos un caso único para no perdernos: una universidad quiere un asistente de matrícula. El objetivo superficial parece uno solo, “responder dudas”, pero debajo hay varios problemas distintos. Según cuál duela, la intervención cambia.

Lectura del problemaSolución razonableQué mejoraQué no arregla
El alumnado pregunta de formas muy distintas y el sistema responde desordenado.Prompt con ejemplos y criterios de estilo.Claridad, tono, estructura inicial.Normativa nueva o datos personales.
La app necesita guardar la respuesta en una ficha.Salida estructurada con campos como tema, respuesta, fuente, confianza.Integración con software y validación.Saber si la norma está vigente.
Las normas de matrícula cambian cada curso.RAG sobre normativa oficial, con fecha y cita.Respuestas trazables y actualizables.Consultar si una persona concreta pagó.
El alumno pregunta “¿me falta pagar algo?”.Tool contra el sistema académico o de pagos.Estado real de esa persona.Explicar bien una norma general.
El equipo responde miles de tickets con una rúbrica estable.Ajuste ligero si prompt, schema y RAG ya no bastan.Consistencia en una tarea repetida.Conocimiento que cambia cada semana.

Lo importante es que ninguna fila “gana” siempre. En un producto real quizá uses varias: RAG para normativa, tool para expediente, salida estructurada para integrar y prompt para tono. Pero las añades por capas, no por entusiasmo.

Cómo evaluar cada intervención

Una intervención no está terminada cuando funciona en la demo. Está terminada cuando puedes repetir una prueba y decidir si mejoró. Si no sabes qué medir, la arquitectura se convierte en opinión.

IntervenciónMétrica principalPrueba mínimaSeñal de que no basta
Prompt y ejemplosTasa de respuestas útiles según rúbrica.30 casos reales antes/después.Mejora solo en ejemplos vistos o se rompe con variaciones simples.
Salida estructuradaValidez de schema y tasa de campos correctos.Tests con campos obligatorios, tipos y casos incompletos.El JSON es válido pero el contenido sigue siendo incorrecto.
RAGEvidencia correcta, cobertura y abstención cuando falta fuente.Preguntas con documento esperado y preguntas sin respuesta en corpus.Recupera texto parecido pero no el fragmento que justifica la respuesta.
ToolExactitud de llamada, validación de argumentos y latencia.Casos con entradas válidas, incompletas y ambiguas.La tool se invoca cuando no toca o con parámetros mal formados.
Fine-tuning/LoRAMejora contra baseline sin perder casos importantes.Eval fija antes/después y revisión de regresiones.Mejora el formato pero empeora factualidad o flexibilidad.
Modelo local o privadoCalidad mínima, latencia, coste y mantenimiento.Misma eval que la API base, con medición de recursos.Cumple privacidad pero no alcanza la calidad necesaria.

En clase, yo pediría siempre tres números antes de aceptar una propuesta: calidad, coste y reversibilidad. Calidad sin coste puede ser inviable. Coste sin calidad no sirve. Y una mejora que no puedes revertir exige mucha más evidencia.

Errores de diagnóstico que conviene detectar

Estos errores parecen razonables cuando tienes prisa, por eso conviene nombrarlos. No son fallos de principiante: aparecen en equipos buenos cuando el problema se describe demasiado pronto con el nombre de una herramienta.

Error de diagnósticoCómo se veCómo corregirlo
Confundir conocimiento con comportamiento“Ajustemos el modelo para que sepa la normativa nueva”.Si cambia con frecuencia, sácalo a documentos, base de datos o tool.
Confundir formato con inteligencia“El modelo no entiende”, cuando lo único que falla es el JSON.Primero schema, validación y ejemplos de salida.
Confundir búsqueda con respuestaEl retrieval trae documentos parecidos, pero no justifican la conclusión.Evaluar recuperación por fragmento correcto, no solo por similitud.
Confundir acción con textoEl modelo “dice” que ha comprobado algo, pero no ha consultado ningún sistema.Tool real, logs y permisos mínimos.
Confundir benchmark con caso propioSe elige modelo por ranking general.Eval con idioma, dominio, coste y latencia del proyecto.
Confundir privacidad con peor producto“Local” se acepta sin medir calidad.Comparar local, privado y API con la misma rúbrica.

El antídoto común es escribir una frase de diagnóstico antes de escribir una frase de solución: “El sistema falla porque...”. Si esa frase ya contiene el nombre de una herramienta, sospecha un poco.

Mini práctica de decisión

Para entrenar el criterio, resuelve estos cuatro casos sin programar. En cada uno escribe: síntoma, intervención principal, alternativa descartada y métrica de aceptación. Después compara con la solución modelo.

CasoSíntomaIntervención principalAlternativa descartadaMétrica de aceptación
Biblioteca universitaria con horarios cambiantes.Responde horarios antiguos.RAG o consulta a fuente oficial.Fine-tuning.Respuestas con horario vigente y fuente correcta.
App médica que necesita clasificar mensajes en tres colas internas.El texto es bueno, pero la integración falla.Salida estructurada.RAG.JSON válido y cola correcta según rúbrica humana.
Ecommerce con preguntas de disponibilidad por tienda.El modelo estima stock.Tool de inventario.Prompt más insistente.Stock correcto, latencia aceptable y manejo de ausencia de dato.
Equipo legal con contratos sensibles en portátiles sin conexión.No puede enviar documentos fuera y necesita asistencia básica.Modelo local cuantizado o entorno privado.API externa directa.Calidad mínima en una eval interna y tiempo de respuesta usable.

La solución modelo no pretende cerrar todos los matices. Pretende mostrar el razonamiento: cada respuesta identifica la capa que falla. Si cambias el enunciado, puede cambiar la intervención. Si la biblioteca también necesita guardar campos exactos, añadirías salida estructurada. Si el ecommerce además debe explicar políticas de devolución, quizá combinarías tool con RAG. La arquitectura final puede tener varias piezas; el diagnóstico inicial decide cuál entra primero.

Mapa visual de diagnóstico

Elegir la intervención correcta Diagnóstico antes de herramienta Cada intervención cambia una capa distinta del sistema. Síntoma observado respuesta mala, lenta, cara, sin cita o sin formato no elijas todavía Diagnóstico ¿falla entrada, contexto, acción, conducta o entorno? casos reales + baseline Cambio mínimo útil mejora suficiente con menor coste, menor complejidad y una evaluación que pueda repetirse Prompt instrucción ejemplos criterio Schema campos tipos validación RAG chunks retrieval citas Tool consulta cálculo acción Ajuste LoRA QLoRA SFT Local datos coste offline Puerta de decisión baseline · eval · coste · latencia · privacidad · mantenimiento · rollback Si no mejora la eval, se vuelve al diagnóstico. IA para gente curiosa / Facsímil 04 / Capítulo 01 / 686f6c61

En el día a día

En un equipo real, este capítulo se usa antes de abrir el editor. Pones encima de la mesa tres cosas: el caso de uso, diez ejemplos reales y una forma de medir. Solo entonces eliges herramienta.

Si un jefe de producto pide “un chatbot con todos los documentos”, tradúcelo: quizá quiere búsqueda con citas, quizá quiere navegación guiada, quizá quiere reducir tickets, quizá quiere extraer campos. Cada objetivo produce una arquitectura distinta.

Si un equipo técnico dice “hagamos RAG”, pregunta por el corpus, el tipo de preguntas, el criterio de cita, los permisos por documento y cómo sabremos que el retrieval encontró evidencia. Si esas respuestas no existen, todavía no hay arquitectura: hay deseo.

Por qué debería importarte

Porque cada intervención mal elegida deja deuda. Un RAG innecesario añade índices, chunks y evals. Un fine-tuning prematuro añade dataset, entrenamiento y versiones. Una tool amplia añade permisos y validación. Un modelo local añade soporte, hardware y medición de calidad.

La buena noticia es que una intervención bien elegida suele simplificar. Un schema puede eliminar cientos de líneas de parsing frágil. Una tool puede evitar que el modelo invente un dato. Un RAG con citas puede convertir una respuesta bonita en una respuesta revisable.

Dónde volverá a aparecer

Este capítulo es la brújula del facsímil. Cada capítulo posterior toma una rama del diagnóstico y la desarrolla.

Rama del diagnósticoDónde vuelveQué resolveremos allí
Salida estructuradaCapítulo 02.Mensajes, schemas, streaming y contratos de API.
Coste y contextoCapítulo 03.Tokens, cache, batch y presupuestos.
Modelos y licenciasCapítulo 04.Leer model cards sin dejarse llevar por rankings.
Local y cloudCapítulos 05 y 06.Ollama, LM Studio, GGUF, privacidad y despliegue.
RAG y vector DBCapítulos 07 a 11.Embeddings, bases vectoriales, RAG, evaluación y GraphRAG.
Herramientas de datosCapítulo 12.Text-to-SQL y consultas con validación.
Laboratorio mínimoCapítulo 13.Notebooks, casos, evals, trazas y decisión escrita.

Dónde solía tropezar yo

Estos tropiezos aparecen mucho al empezar proyectos con IA. Casi todos nacen de confundir síntoma con solución.

ErrorPor qué es un errorAntídoto
Empezar por RAG sin corpus claroSi no sabes qué documentos, preguntas y citas necesitas, el índice será decoración cara.Definir 30 preguntas reales antes de indexar.
Ajustar pesos para datos vivosEl dato que cambia mañana no debería quedar escondido en un adapter.Usar RAG, base de datos o tool para conocimiento cambiante.
Pedir formato con súplicas“Responde en JSON” no es contrato suficiente si el backend depende de campos exactos.Usar salida estructurada y validación.
Usar una tool para todoUna función gigante es difícil de validar y fácil de usar mal.Tools pequeñas, schemas claros y permisos mínimos.
Comparar modelos sin tareaUn ranking genérico no sabe qué coste, idioma, latencia o riesgo tiene tu producto.Comparar con tus casos, tus métricas y tus límites.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a convertir el diagnóstico en una matriz pequeña. El objetivo no es automatizar la decisión final. Es obligarte a escribir qué importa en tu caso antes de enamorarte de una herramienta.

intervenciones = {
    "prompt_schema": {
        "conocimiento_vivo": 0,
        "accion_externa": 0,
        "formato": 5,
        "conducta_repetida": 2,
        "privacidad_local": 1,
        "coste": 5,
    },
    "rag": {
        "conocimiento_vivo": 5,
        "accion_externa": 1,
        "formato": 2,
        "conducta_repetida": 2,
        "privacidad_local": 3,
        "coste": 3,
    },
    "tool": {
        "conocimiento_vivo": 4,
        "accion_externa": 5,
        "formato": 3,
        "conducta_repetida": 1,
        "privacidad_local": 3,
        "coste": 3,
    },
    "ajuste_lora": {
        "conocimiento_vivo": 1,
        "accion_externa": 0,
        "formato": 4,
        "conducta_repetida": 5,
        "privacidad_local": 3,
        "coste": 2,
    },
    "modelo_local": {
        "conocimiento_vivo": 1,
        "accion_externa": 0,
        "formato": 2,
        "conducta_repetida": 2,
        "privacidad_local": 5,
        "coste": 4,
    },
}

caso = {
    "conocimiento_vivo": 5,
    "accion_externa": 0,
    "formato": 3,
    "conducta_repetida": 2,
    "privacidad_local": 4,
    "coste": 3,
}

def score(intervencion, pesos):
    return sum(intervencion[criterio] * importancia for criterio, importancia in pesos.items())

ranking = sorted(
    ((nombre, score(valores, caso)) for nombre, valores in intervenciones.items()),
    key=lambda item: item[1],
    reverse=True,
)

for nombre, puntos in ranking:
    print(f"{nombre}: {puntos}")

Salida esperada:

rag: 56
tool: 52
modelo_local: 47
ajuste_lora: 45
prompt_schema: 38

Este caso favorece RAG porque hemos dicho que el conocimiento vivo pesa mucho y no necesitamos actuar sobre sistemas externos. Cambia accion_externa a 5 y verás subir tool. Cambia formato a 5 y conocimiento_vivo a 0, y verás que prompt/schema se vuelve competitivo. La matriz no decide por ti: te obliga a explicar tus prioridades.

Cómo encaja todo

graph TD
    subgraph "Capítulo 1: Elegir intervención"
        SINTOMA["Síntoma"]
        DIAG["Diagnóstico"]
        MATRIZ["Matriz de criterios"]
        PROMPT["Prompt y ejemplos"]
        SCHEMA["Salida estructurada"]
        RAG["RAG"]
        TOOL["Tool"]
        AJUSTE["Ajuste"]
        LOCAL["Local o cloud"]
        EVAL["Eval y coste"]
        RUBRICA["Métrica de aceptación"]
    end
    subgraph "Viene del facsímil 3"
        LLM["LLM, tokens y contexto"]
        SAMPLING["Logits y sampling"]
        FT["LoRA, QLoRA<br/>cuantización"]
        SERVING["Inferencia y hardware"]
    end
    subgraph "Resto del facsímil 4"
        API["APIs y schemas"]
        COSTE["Tokens y coste"]
        MODELOS["Model cards"]
        VECTOR["Embeddings y vector DB"]
        RAGCAP["RAG y evaluación"]
        DATA["Text-to-SQL"]
        LAB["Laboratorio mínimo"]
    end

    SINTOMA --> DIAG
    DIAG --> MATRIZ
    MATRIZ --> PROMPT
    MATRIZ --> SCHEMA
    MATRIZ --> RAG
    MATRIZ --> TOOL
    MATRIZ --> AJUSTE
    MATRIZ --> LOCAL
    PROMPT --> EVAL
    SCHEMA --> EVAL
    RAG --> EVAL
    TOOL --> EVAL
    AJUSTE --> EVAL
    LOCAL --> EVAL
    EVAL --> RUBRICA
    LLM --> DIAG
    SAMPLING --> PROMPT
    FT --> AJUSTE
    SERVING --> LOCAL
    SCHEMA --> API
    EVAL --> COSTE
    LOCAL --> MODELOS
    RAG --> VECTOR
    VECTOR --> RAGCAP
    TOOL --> DATA
    EVAL --> LAB

    style SINTOMA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style DIAG fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MATRIZ fill:#F5F5F5,stroke:#000000,stroke-width:2
    style PROMPT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SCHEMA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RAG fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TOOL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style AJUSTE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style LOCAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EVAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RUBRICA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style LLM stroke-dasharray: 5 5
    style SAMPLING stroke-dasharray: 5 5
    style FT stroke-dasharray: 5 5
    style SERVING stroke-dasharray: 5 5
    style API stroke-dasharray: 5 5
    style COSTE stroke-dasharray: 5 5
    style MODELOS stroke-dasharray: 5 5
    style VECTOR stroke-dasharray: 5 5
    style RAGCAP stroke-dasharray: 5 5
    style DATA stroke-dasharray: 5 5
    style LAB stroke-dasharray: 5 5

Vocabulario aprendido

Estas palabras aparecerán en todo el facsímil. Conviene que queden limpias desde el principio.

TérminoDefinición
IntervenciónCambio concreto en el sistema para mejorar un síntoma medible.
PromptEntrada que define tarea, contexto, ejemplos y criterio.
SchemaEstructura esperada de una salida o una tool.
Salida estructuradaRespuesta obligada a cumplir un formato validable.
RAGRecuperar evidencia externa y pasarla al modelo como contexto.
ToolFunción externa que consulta, calcula o modifica algo bajo reglas.
Fine-tuningAjustar pesos de un modelo con datos propios.
LoRAAjuste eficiente con matrices pequeñas de bajo rango.
Modelo localModelo ejecutado en una máquina o infraestructura controlada.
BaselineComparación mínima antes de añadir complejidad.
EvalPrueba repetible que mide calidad, coste, latencia o seguridad del sistema.
Matriz de decisiónTabla que obliga a justificar una elección con criterios observables.
ReversibilidadFacilidad para volver atrás si una intervención empeora el sistema.
AbstenciónCapacidad de reconocer que falta evidencia suficiente para responder.

Antes de pasar página

  • ¿Puedo explicar por qué no se empieza eligiendo herramienta?
  • ¿Sé distinguir si un problema es de entrada, contexto, acción, conducta o entorno?
  • ¿Puedo decir cuándo basta prompt/schema y cuándo hace falta RAG?
  • ¿Entiendo por qué fine-tuning no es buena memoria para datos vivos?
  • ¿Sé cuándo una tool es más adecuada que pedirle al modelo que “razone”?
  • ¿Puedo usar la fórmula UΔQCKMRU \approx \Delta Q \cdot C - K - M - R para discutir una decisión?
  • ¿He ejecutado la matriz y cambiado el caso para que gane otra intervención?
  • ¿Puedo justificar una intervención usando criterios como cita, estado real, formato, coste y reversibilidad?
  • ¿Sé resolver un mismo caso con prompt, schema, RAG, tool o ajuste según el síntoma?
  • ¿Puedo proponer una métrica mínima para evaluar cada intervención?
  • ¿Sé detectar cuándo estoy confundiendo conocimiento, formato, acción o despliegue?

En resumen

La caja de herramientas empieza por diagnóstico. Si eliges bien el problema, la herramienta suele volverse evidente.

Idea fuerzaDetalle
La herramienta no es el punto de partida.Primero síntoma, casos reales y baseline.
Prompt/schema arreglan contrato de entrada y salida.No hacen que el modelo sepa datos vivos.
RAG aporta evidencia externa.Sirve para documentos cambiantes y respuestas citables.
Tool conecta con estado real.Sirve para consultar, calcular o actuar sin inventar.
Ajustar pesos cambia comportamiento.Tiene sentido en tareas repetidas, estables y medibles.
Local/cloud son decisiones de entorno.Privacidad, coste, latencia y mantenimiento mandan.
Eval decide si la intervención se queda.Sin medición, solo hay impresión.
Una buena decisión debe poder explicarse.Criterio, alternativa descartada, métrica y rollback forman parte de la respuesta.

Para saber más

Anthropic. (2026). Prompt engineering overview. https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview

Dettmers, T. et al. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. Advances in Neural Information Processing Systems 36. https://arxiv.org/abs/2305.14314

Hugging Face. (2026). PEFT: LoRA developer guide. https://huggingface.co/docs/peft/developer_guides/lora

Hu, E. J. et al. (2022). LoRA: Low-Rank Adaptation of Large Language Models. International Conference on Learning Representations. https://arxiv.org/abs/2106.09685

Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474. https://papers.nips.cc/paper/2020/hash/6b493230205f780e1bc26945df7481e5-Abstract.html

Ollama. (2026). Introduction to the Ollama API. https://docs.ollama.com/api/introduction

Ollama. (2026). Ollama Cloud. https://docs.ollama.com/cloud

OpenAI. (2026). Function calling. https://developers.openai.com/api/docs/guides/function-calling

OpenAI. (2026). Structured model outputs. https://developers.openai.com/api/docs/guides/structured-outputs

Notas

  1. OpenAI. (2026). Structured model outputs. https://developers.openai.com/api/docs/guides/structured-outputs. Consultado el 25 de mayo de 2026. La guía distingue entre salidas estructuradas para respuesta final y function calling cuando el modelo se conecta a herramientas o datos.

  2. Anthropic. (2026). Prompt engineering overview. https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview. Consultado el 25 de mayo de 2026. La guía trata el prompt como mecanismo de especificación de tarea, contexto, ejemplos y restricciones.

  3. Hugging Face. (2026). PEFT: LoRA developer guide. https://huggingface.co/docs/peft/developer_guides/lora. Consultado el 25 de mayo de 2026. PEFT documenta LoRA y variantes para entrenar adaptadores pequeños sobre modelos base.

  4. Ollama. (2026). Introduction to the Ollama API. https://docs.ollama.com/api/introduction. Consultado el 25 de mayo de 2026. La documentación indica la URL local por defecto y la URL cloud compatible.

  5. Ollama. (2026). Ollama Cloud. https://docs.ollama.com/cloud. Consultado el 25 de mayo de 2026. La guía describe modelos cloud que se ejecutan sin requerir una GPU local potente.

  6. Anthropic. (2026). Prompt engineering overview. https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview. Consultado el 25 de mayo de 2026.

  7. OpenAI. (2026). Structured model outputs. https://developers.openai.com/api/docs/guides/structured-outputs. Consultado el 25 de mayo de 2026.

  8. Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474. https://papers.nips.cc/paper/2020/hash/6b493230205f780e1bc26945df7481e5-Abstract.html.

  9. OpenAI. (2026). Function calling. https://developers.openai.com/api/docs/guides/function-calling. Consultado el 25 de mayo de 2026.

  10. Hu, E. J. et al. (2022). LoRA: Low-Rank Adaptation of Large Language Models. International Conference on Learning Representations. https://arxiv.org/abs/2106.09685. Hugging Face. (2026). PEFT: LoRA developer guide. https://huggingface.co/docs/peft/developer_guides/lora. Consultado el 25 de mayo de 2026.

  11. Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474. https://papers.nips.cc/paper/2020/hash/6b493230205f780e1bc26945df7481e5-Abstract.html. El trabajo formuló modelos que recuperan documentos y los combinan con generación.

  12. Hu, E. J. et al. (2022). LoRA: Low-Rank Adaptation of Large Language Models. International Conference on Learning Representations. https://arxiv.org/abs/2106.09685.

  13. Dettmers, T. et al. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. Advances in Neural Information Processing Systems 36. https://arxiv.org/abs/2305.14314.

Capítulo 02

Facsímil 4 · La caja de herramientas

Capítulo 02: APIs de modelos: mensajes, streaming y salidas estructuradas

El modelo no vive solo en una caja de texto

Cuando usamos un chat en el navegador, parece que el sistema funciona así: escribes una pregunta y aparece una respuesta. Para aprender a construir productos con IA, esa imagen se queda corta. Entre tu aplicación y el modelo hay un contrato: qué modelo llamas, qué mensajes envías, qué herramientas están disponibles, qué formato esperas, si quieres streaming, qué harás si la salida no valida y cómo guardarás la traza.

Este capítulo baja un escalón desde el criterio del capítulo 01. Allí decidimos cuándo tiene sentido usar prompt, schema, RAG, tool o ajuste. Aquí aprendemos a convertir esa decisión en una petición concreta a una API de modelos.

La idea central es sencilla: una API de modelos no es un buzón de texto; es una frontera entre software probabilístico y software verificable.

Estado del arte con fecha de corte

Fecha de corte: 10 de junio de 2026.
Fuentes consultadas ese día: documentación oficial de OpenAI sobre Responses API, generación de texto, archivos, visión, salidas estructuradas, function calling, streaming, SDKs y Agents SDK; documentación de Anthropic sobre Messages API, visión, PDF, streaming y structured outputs; documentación de Google sobre Gemini API, documentos, visión, structured outputs, ADK, MCP y A2A; documentación oficial del protocolo A2A; JSON Schema; y la especificación WHATWG de Server-Sent Events.

La parte estable es el patrón: enviar una petición con modelo, instrucciones, entrada, herramientas opcionales, formato esperado y opciones de ejecución. La parte cambiante son los nombres exactos de endpoints, SDKs, modelos, campos y límites. Por eso conviene aprender la arquitectura mental antes que memorizar una firma de cliente.

OpenAI documenta la generación de texto y el uso de salidas estructuradas para pedir respuestas que cumplan un schema.12 También documenta function calling para que el modelo solicite llamadas a funciones definidas por la aplicación.3 Anthropic organiza su API alrededor de mensajes y documenta streaming y salidas estructuradas como patrones de construcción.456 Google documenta Gemini como una API de generación de contenido con entrada textual y multimodal, además de structured outputs basadas en schema.78

La revisión del 10 de junio no cambia la tesis del capítulo, pero sí refuerza una decisión de ingeniería: no acoples tu dominio al JSON exacto de un proveedor. OpenAI mantiene como piezas centrales Responses, structured outputs, function calling y streaming; Anthropic conserva Messages API, streaming y patrones de salida estructurada; Google añade además superficies recientes alrededor de Interactions API, ADK y protocolos de agentes.91011 La conclusión práctica es aburrida y muy importante: escribe un adaptador por proveedor, valida la salida en tu código, guarda trazas comparables y deja que tu aplicación hable en objetos propios, no en campos de moda.

Qué no es una API de modelos

Una API de modelos no es “el prompt por HTTP”. Si la tratas así, acabarás pegando instrucciones, datos, historial, formato esperado y lógica de negocio en una sola cadena. Eso funciona en una demo; se vuelve frágil cuando hay usuarios reales.

Tampoco es una promesa de verdad. La API puede devolver texto muy convincente, pero tu aplicación sigue necesitando validación, métricas, trazas y reglas de producto. Si pides JSON y no validas JSON, no tienes contrato: tienes esperanza.

Y no es una interfaz idéntica entre proveedores. Muchos conceptos se parecen, pero los detalles cambian: roles disponibles, nombre del campo de entrada, eventos de streaming, formato de tools, límites, modelos, errores y objetos de respuesta. Por eso el diseño de tu aplicación debería tener una capa propia que traduzca “lo que necesita mi producto” a “lo que espera este proveedor”.

Qué sí es: un contrato entre capas

Una API de modelos es un contrato entre tres mundos. El primero es tu aplicación: usuarios, permisos, pantallas, flujos y datos. El segundo es el proveedor del modelo: endpoint, modelo, tokens, eventos, herramientas y respuesta. El tercero es tu código de integración: validadores, retries, logs, evals y transformación a objetos de dominio.

Ejemplo de fórmula. Una petición mínima puede pensarse así:

r=(m, I, C, F, T, S)r=(m,\ I,\ C,\ F,\ T,\ S)
SímboloSignificadoEjemplo
rrRequest o petición completa.Una llamada para clasificar una solicitud.
mmModelo elegido.Un modelo rápido para clasificación.
IIInstrucciones y mensajes.Rol del sistema, contexto y pregunta del usuario.
CCContexto externo.Fragmentos RAG, datos de producto o historial relevante.
FFFormato de salida.JSON con categoria, prioridad y siguiente_paso.
TTTools disponibles.consultar_expediente(id_alumno).
SSOpciones de servicio.Streaming, límite de salida, metadata o timeout.

Esta forma de verlo ayuda a no mezclar piezas. Si falla FF, quizá necesitas schema. Si falla CC, quizá necesitas retrieval. Si falta TT, el modelo no debería inventar estado externo. Si falla SS, puede que el problema sea latencia, no inteligencia.

Qué APIs se usan hoy y quién las usa

A 10 de junio de 2026, cuando una empresa dice “vamos a llamar a un LLM desde nuestra app”, normalmente está hablando de una de estas familias. No son las únicas, pero sí sirven para entender el mercado: APIs directas de laboratorios, APIs cloud que empaquetan varios modelos, gateways que unifican proveedores y runtimes locales que imitan una interfaz remota.

OpenAI organiza gran parte de su construcción moderna alrededor de la Responses API: una llamada que puede recibir entrada multimodal, instrucciones, tools, formato de salida, streaming y opciones de ejecución.12 La usan equipos que quieren construir asistentes, clasificadores, flujos con herramientas, extracción estructurada, análisis de documentos, experiencias con visión o productos donde una respuesta textual debe convertirse en objeto de software.

Anthropic expone Claude principalmente mediante Messages API para conversaciones y turnos sin estado: envías una lista estructurada de mensajes y el modelo genera el siguiente mensaje.13 Es habitual en productos que necesitan lectura y escritura largas, análisis documental, asistentes internos, ayuda a programación, tutoría o flujos donde interesa controlar muy bien el historial enviado.

Google ofrece la Gemini API alrededor de generateContent, con entradas que pueden combinar texto, imágenes, vídeo y audio, además de configuración de generación y salidas estructuradas.14 Encaja especialmente en productos multimodales, prototipos integrados con el ecosistema Google, procesamiento de documentos y aplicaciones que quieren aprovechar modelos con ventanas largas o capacidades visuales.

Además existen plataformas como Amazon Bedrock, Vertex AI, Azure AI Foundry, Mistral, Cohere, Groq, Together, Fireworks, OpenRouter o Vercel AI SDK. La lección no es aprenderlas todas de memoria. La lección es que casi todas terminan pidiendo lo mismo con nombres distintos: modelo, entrada, instrucciones, parámetros de generación, herramientas, schema, streaming, límites y metadatos.

Los parámetros: no memorizar nombres, entender familias

Cuando un alumno ve por primera vez una referencia de API, se pierde porque parece una lista interminable de campos. La forma útil de estudiarla es agrupar parámetros por intención. Unos campos dicen qué modelo usamos; otros dicen qué entra; otros controlan cuánto y cómo responde; otros definen qué herramientas puede pedir; otros describen qué forma debe tener la salida; y otros sirven para operar el sistema.

Ejemplo de fórmula. La petición completa puede verse así:

r=(p, x, g, o, q)r=(p,\ x,\ g,\ o,\ q)
SímboloSignificadoEjemplo
ppProveedor y modelo.OpenAI con Responses API, Claude con Messages API o Gemini con generateContent.
xxEntrada del usuario y contexto.Texto, historial, documentos, imagen o fragmentos recuperados.
ggParámetros de generación.Límite de salida, temperatura, top_p, top_k o secuencias de parada.
ooContrato operativo.Streaming, tools, tool choice, metadata, traza y timeout.
qqContrato de calidad.Schema, validador, evals y reglas de producto.

La tabla siguiente no pretende congelar una API que cambia. Sirve para reconocer equivalencias:

FamiliaOpenAI Responses APIAnthropic Messages APIGemini API
Modelomodelmodelmodelo en models.generateContent o ruta REST.
Entradainput con mensajes y bloques de contenido.messages con contenido por turnos.contents con parts.
Instrucciones establesinstructions o mensajes equivalentes según SDK.system como campo superior.systemInstruction o configuración equivalente.
Límite de salidamax_output_tokens según modelo y endpoint.max_tokens.maxOutputTokens dentro de configuración.
Aleatoriedadtemperature, top_p cuando el modelo lo permita.temperature, top_p, top_k según modelo.temperature, topP, topK.
Paradasecuencias de parada si están disponibles.stop_sequences.stopSequences.
Salida estructuradaformato de texto/schema o Structured Outputs.output_config.format o tool estricta, según caso.responseMimeType y responseJsonSchema.
Toolstools y tool_choice.tools y tool_choice.tools y function calling.
Streamingstream y eventos.stream con eventos SSE.métodos de streaming del SDK o REST.
Metadatos y operaciónmetadata, trazas, user o campos de servicio cuando existan.metadata, uso de tokens y estado de parada.usageMetadata, configuración y metadatos del entorno.

El parámetro más peligroso no es siempre el más técnico. Muchas integraciones fallan por no fijar bien max_tokens o max_output_tokens, por mezclar instrucciones con datos del usuario, por no versionar el schema o por asumir que temperature=0 convierte una salida probabilística en una función matemática.

Entradas y salidas: qué ocurre en cada caso

Una API de modelos no devuelve siempre “texto”. Puede devolver texto, JSON, una petición de tool, eventos parciales o una combinación de bloques. Por eso conviene diseñar el flujo antes de escribir el cliente.

CasoEntraSaleQué debe hacer tu aplicación
Texto a textoPregunta, instrucciones y contexto breve.Respuesta natural.Mostrar, registrar y quizá evaluar calidad.
Texto a JSONTexto y schema.Objeto estructurado.Parsear, validar y transformar a objeto de dominio.
Documento a resumenArchivo, páginas o fragmentos.Resumen, citas o extracción.Conservar referencia a página, versión y documento original.
Imagen a explicaciónImagen más pregunta.Descripción, clasificación o lectura visual.Revisar límites visuales y pedir evidencia cuando importe.
Texto a tool callPetición y definición de función.Nombre de tool y argumentos.Validar argumentos, comprobar permisos y ejecutar código.
Tool result a respuestaResultado externo.Explicación final.Separar dato consultado de redacción generada.
StreamingPetición normal con stream.Eventos parciales.Acumular, cancelar, manejar errores y cerrar estado.

La salida estructurada y las tool calls se parecen porque usan schemas, pero no cumplen la misma misión. Una salida estructurada es el resultado final con forma de dato. Una tool call es una solicitud intermedia para que el sistema consulte, calcule o actúe. Si confundes ambas cosas, tu app termina ejecutando texto como si fuera una decisión cerrada.

Documentos e imágenes: multimodal no significa comprensión automática

Enviar una imagen o un PDF a un modelo multimodal no equivale a “el modelo lo sabe todo sobre ese archivo”. Equivale a darle una entrada rica que el modelo puede procesar dentro de sus límites. OpenAI documenta bloques como input_file para archivos y input_image para imágenes dentro de la entrada.1516 Anthropic documenta visión y soporte de PDF para Claude.1718 Gemini permite pasar imágenes como datos inline o mediante Files API, y documenta límites específicos para PDF.1920

Para una sola imagen, suele bastar con enviar la imagen y una pregunta clara: “lee esta factura y extrae fecha, proveedor e importe”. Para muchas imágenes o archivos repetidos, conviene subirlos una vez y referenciarlos. Para un PDF largo, hay que decidir si se manda entero, si se divide por páginas, si se indexa en un sistema RAG o si se usa una combinación: retrieval para localizar fragmentos y modelo multimodal para interpretar tablas, figuras o páginas concretas.

Hay cuatro cosas que no deberíamos olvidar:

CuidadoPor qué importaBuena práctica
Tamaño y costeImágenes y documentos consumen contexto y pueden aumentar latencia.Medir tokens, páginas, resolución y tiempo de respuesta.
ReferenciasUna respuesta sin página o fragmento es difícil de revisar.Pedir pagina, fragmento o evidencia en el schema.
PrivacidadLos documentos pueden contener datos personales o internos.Minimizar, redactar cuando sea posible y revisar política del proveedor.
Lectura visualUn modelo puede interpretar mal tablas, sellos o capturas pequeñas.Comprobar con OCR, reglas o revisión humana cuando importe.

La regla práctica: si el documento es fuente de verdad, no lo trates como “contexto decorativo”. Guárdalo con identificador, versión, fecha de carga y forma de recuperación. Si mañana alguien pregunta por qué la app contestó eso, debes poder reconstruir qué archivo vio, qué páginas entraron y qué schema validó la salida.

Mensajes: separar instrucciones, contexto y petición

La mayoría de APIs modernas no reciben solo una cadena. Reciben mensajes o una estructura equivalente. El objetivo no es teatralizar una conversación, sino separar responsabilidades: qué reglas gobiernan la tarea, qué dijo la persona usuaria, qué contestó antes el modelo y qué resultados devolvieron herramientas.

PiezaQué representaRiesgo si se mezcla
Instrucciones de sistema o desarrolladorComportamiento estable que quieres mantener.El usuario puede pisar reglas importantes con texto accidental.
Mensaje de usuarioLa petición concreta de esta interacción.El sistema no distingue objetivo de contexto.
Contexto recuperadoEvidencia externa añadida por la aplicación.El modelo no sabe qué parte citar o priorizar.
Mensaje del asistenteRespuesta anterior o salida actual.Se pierde trazabilidad en conversaciones largas.
Resultado de toolDato externo obtenido por código.Se confunde texto generado con dato comprobado.

Un patrón robusto consiste en construir la petición desde piezas separadas y solo al final traducirlas al formato del proveedor. Así puedes cambiar de modelo sin reescribir la lógica de negocio.

Para entenderlo: si una app universitaria pregunta “¿puede Ana matricularse de Sistemas Inteligentes?”, no deberías mandar solo esa frase. Deberías separar la política académica vigente, el identificador de Ana, las reglas de formato de salida y, si hace falta, la tool que consulta expediente.

Salidas estructuradas: cuando el texto debe convertirse en dato

Pedir “responde en JSON” es una intención. Usar un schema es un contrato. JSON Schema define vocabulario para describir tipos, propiedades, campos requeridos y reglas de validación sobre documentos JSON.21 Las APIs de modelos aprovechan esa idea para reducir la distancia entre respuesta natural y objeto que tu software puede consumir.

La métrica mínima de una salida estructurada es la tasa de conformidad:

validez=NvaˊlidasNtotal\operatorname{validez}=\frac{N_{\text{válidas}}}{N_{\text{total}}}
SímboloSignificadoEjemplo
NvaˊlidasN_{\text{válidas}}Respuestas que cumplen schema.97 de 100 respuestas.
NtotalN_{\text{total}}Respuestas evaluadas.100 casos de prueba.
validez\operatorname{validez}Proporción de salidas estructuralmente correctas.0,970{,}97.

Pero cuidado: una respuesta puede cumplir schema y seguir siendo mala. Si el schema pide prioridad, el valor puede ser válido como texto y estar mal como decisión. Por eso necesitamos dos validaciones:

ValidaciónPreguntaEjemplo
Estructural¿Cumple tipos, campos y restricciones?prioridad existe y vale alta, media o baja.
Semántica¿El contenido es correcto para el caso?El mensaje realmente requiere prioridad alta.

La salida estructurada arregla el contrato con el software. No reemplaza la evaluación del criterio.

Streaming: que la respuesta llegue por partes

Streaming significa que la aplicación no espera a tener toda la respuesta para empezar a recibir fragmentos. En la web moderna suele implementarse con eventos o flujos parecidos a Server-Sent Events, donde el servidor envía datos progresivamente al cliente.22 OpenAI y Anthropic documentan streaming para respuestas de modelos.2324

La razón no es solo estética. El streaming cambia la experiencia percibida.

Ejemplo de fórmula. Para explicarlo en una revisión de producto, puedes separar primer evento y lectura progresiva:

TpercibidoTprimer_evento+Tlectura_progresivaT_{\text{percibido}} \approx T_{\text{primer\_evento}} + T_{\text{lectura\_progresiva}}
SímboloSignificadoEjemplo
Tprimer_eventoT_{\text{primer\_evento}}Tiempo hasta recibir el primer fragmento.700 ms.
Tlectura_progresivaT_{\text{lectura\_progresiva}}Tiempo durante el que se van mostrando fragmentos.La respuesta aparece mientras se genera.
TpercibidoT_{\text{percibido}}Latencia que siente la persona usuaria.Menor que esperar todo el texto junto.

Streaming no hace que el modelo “piense mejor”. Hace que el producto pueda mostrar progreso, cancelar, actualizar UI y registrar eventos. También complica: debes ensamblar fragmentos, manejar cortes, distinguir eventos de texto y eventos de tool, y decidir cuándo una salida estructurada está lista para validarse.

Tool calls: cuando responder no basta

Una salida estructurada devuelve datos. Una tool call pide que tu aplicación ejecute algo. Esa diferencia parece pequeña y es enorme.

NecesidadSalida estructuradaTool call
Clasificar un mensajeDevuelve {categoria, prioridad}.Normalmente no hace falta.
Consultar stockPuede devolver intención de consulta.Llama consultar_stock(producto, talla).
Calcular una cuotaPuede proponer fórmula.Llama calcular_cuota(importe, plazo).
Abrir un ticketPuede redactar el contenido.Llama crear_ticket(...) si el usuario confirma.

La regla práctica: si el dato existe fuera del modelo, no lo conviertas en adivinanza. Define una tool pequeña, valida sus argumentos, ejecuta el código y devuelve el resultado como contexto para que el modelo lo explique.

Para entenderlo antes de tocar código

Pensemos en cuatro productos que parecen parecidos porque todos “usan IA”, pero que piden contratos distintos.

ProductoQué envía a la APIQué espera recibirPieza crítica
Clasificador de correos internosTexto del correo e instrucciones.JSON con cola, prioridad y motivo breve.Schema y validador.
Tutor universitarioPregunta, nivel del curso y rúbrica.Explicación paso a paso.Mensajes bien separados.
Asistente de matrículaPregunta y contexto normativo recuperado.Respuesta con cita y quizá tool de expediente.RAG, tool y trazabilidad.
Redactor con respuesta largaBrief, tono y ejemplos.Texto progresivo en pantalla.Streaming y cancelación.

El mismo modelo puede participar en los cuatro, pero la API no se usa igual. En uno importa más el schema; en otro, el streaming; en otro, tools; en otro, trazabilidad del contexto.

Buenas prácticas de integración

Una integración madura no empieza llamando al SDK desde cualquier pantalla. Empieza con un contrato propio de la aplicación. Ese contrato dice: “para clasificar una solicitud necesito estos campos, esta política, este schema, estas tools permitidas y esta forma de guardar trazas”. Después un adaptador traduce ese contrato al proveedor elegido.

El adaptador evita que el resto del producto sepa si por debajo hay OpenAI, Claude, Gemini, un modelo local o un gateway. También te obliga a decidir lo importante en un sitio: versionar prompts, schemas y modelos; convertir errores de proveedor en errores propios; medir coste; registrar identificadores; y probar la misma tarea con casos de evaluación.

PrácticaQué resuelveCómo se ve en código o producto
Adaptador por proveedorEvita acoplar pantallas a nombres de campos externos.crearRespuestaTutor(...) traduce a OpenAI, Claude o Gemini.
Schema versionadoPermite cambiar formato sin romper consumidores.respuesta_matricula.v2.json.
Validador propioNo delega toda la corrección al proveedor.Pydantic, Zod, JSON Schema o validación del backend.
Trazas mínimasPermite reproducir fallos sin guardar más de lo necesario.trace_id, modelo, versión de prompt, schema y resumen de entrada.
Timeouts y reintentosEvita dejar al usuario esperando sin cierre.Reintento solo en operaciones idempotentes.
Tools pequeñasReduce ambigüedad y facilita permisos.consultar_expediente(id) en vez de hacer_cosas(datos).
Evals antes de publicarMide si la integración mejora o empeora.Conjunto de casos fijos con salida esperada.
Streaming con máquina de estadosEvita UI a medias.iniciado, parcial, tool, completo, cancelado, error.

Ejemplo de fórmula. La integración más limpia que conozco tiene esta forma mental:

productocontrato propioadaptadorproveedorvalidadorproducto\text{producto} \rightarrow \text{contrato propio} \rightarrow \text{adaptador} \rightarrow \text{proveedor} \rightarrow \text{validador} \rightarrow \text{producto}
PiezaPregunta que responde
Producto¿Qué necesita conseguir la persona usuaria?
Contrato propio¿Qué datos, formato, tools y reglas exige nuestro flujo?
Adaptador¿Cómo se expresa eso en la API concreta?
Proveedor¿Qué modelo genera, pide tool o devuelve eventos?
Validador¿La salida cumple estructura y criterio mínimo?
Producto¿Mostramos, pedimos confirmación, guardamos o repetimos?

Cómo sería una API perfecta para integrar

Una API perfecta no sería “la que siempre acierta”. Eso no existe. Sería la que hace fácil construir software fiable alrededor de una capacidad probabilística. Si diseñáramos una interfaz ideal para una app profesional, tendría estas propiedades:

RasgoPor qué importa
Entrada multimodal tipadaTexto, imágenes y documentos no llegan como una cadena opaca.
Instrucciones separadasLas reglas estables no se mezclan con lo que escribe la persona usuaria.
Salida schema-firstEl contrato de datos se declara antes de generar.
Tools tipadas y pequeñasLa API distingue pedir una acción de ejecutar una acción.
Eventos normalizadosStreaming, tool calls y errores siguen una secuencia predecible.
Uso y coste visiblesLa respuesta trae tokens, latencia y modelo usado.
Versionado explícitoPrompt, schema, modelo y toolset se pueden congelar y comparar.
Errores tipadosLa app sabe si hubo límite, timeout, validación fallida o contenido incompleto.
IdempotenciaReintentar no duplica acciones sensibles ni crea registros repetidos.
Privacidad configurablePuedes decidir qué se guarda, qué se omite y durante cuánto tiempo.
Evals integradasEl mismo contrato puede probarse con casos antes de publicarse.

En pseudocódigo, una llamada ideal se parecería menos a “envía este texto” y más a esto:

respuesta = modelo.generar({
  tarea: "clasificar_solicitud_matricula",
  contrato: "solicitud_matricula.v2",
  entrada: {texto, documentos, usuario_contexto},
  salida: schema_respuesta,
  tools: [consultar_expediente],
  ejecucion: {stream: true, timeout_ms: 12000, trace_id},
  politica: {confirmar_antes_de_crear_ticket: true}
})

La clave no es que todos los proveedores adopten exactamente ese formato. La clave es que tu aplicación sí tenga esa claridad interna. Si tu dominio está bien modelado, cambiar de proveedor es una migración. Si tu dominio vive pegado al prompt, cambiar de proveedor es cirugía.

Mapa visual de una petición robusta

El diagrama resume la idea práctica del capítulo: la aplicación no debería hablar con el modelo como quien manda una frase suelta, sino como quien prepara un contrato que luego valida.

De la aplicación al contrato de API Una API de modelos es un contrato Entrada, formato, tools, streaming y validación viajan separados. Aplicación usuario · permisos pantalla · objetivo no manda texto sin contrato Contrato + adaptador modelo · entrada · contexto schema · tools · opciones traduce dominio a API Proveedor modelo · inferencia eventos · objetos Respuesta texto · JSON tool call · stream Mensajes roles separados historial controlado Multimodal texto · imagen documento · audio Schema campos tipos y límites Tools funciones pequeñas argumentos validados Streaming eventos cancelación Trazas evals logs Regla final El modelo genera; tu aplicación valida, ejecuta, registra y decide qué hacer después. IA para gente curiosa / Facsímil 04 / Capítulo 02 / 686f6c61

Mapa Mermaid: todo lo que viaja en una API

El SVG anterior da la intuición editorial. Ahora conviene ver la llamada como arquitectura técnica: qué construye tu aplicación, qué transforma el SDK o adaptador, qué recibe el proveedor y qué vuelve a tu sistema.

flowchart TD
    subgraph "Aplicación propia"
        UI["UI o backend"]
        DOMINIO["Objeto de dominio"]
        TRAZA_APP["Trace id y evals"]
    end

    subgraph "Contrato interno"
        REQ["Request canónica"]
        INSTR["Instrucciones"]
        ENTRADA["Texto, historial<br/>multimodal"]
        PARAMS["Parámetros de generación"]
        SCHEMA["Schema de salida"]
        TOOLS["Tools permitidas"]
    end

    subgraph "Adaptador o SDK"
        SDK["SDK: auth, tipos<br/>streaming"]
        MAPEO["Mapeo a proveedor"]
        ERRORES["Errores normalizados"]
    end

    subgraph "API del proveedor"
        OAI["OpenAI Responses"]
        CLAUDE["Claude Messages"]
        GEMINI["Gemini generateContent"]
    end

    subgraph "Ejecución"
        EVENTOS["Eventos stream"]
        TOOLCALL["Tool call"]
        MCP["MCP: tools y recursos"]
        VALIDAR["Validación estructural<br/>y semántica"]
    end

    subgraph "Capa agente opcional"
        AGENTSDK["Agents SDK o ADK"]
        A2A["A2A: Agent Card<br/>Task, Message, Artifact"]
    end

    UI --> DOMINIO
    DOMINIO --> REQ
    REQ --> INSTR
    REQ --> ENTRADA
    REQ --> PARAMS
    REQ --> SCHEMA
    REQ --> TOOLS
    REQ --> SDK
    SDK --> MAPEO
    MAPEO --> OAI
    MAPEO --> CLAUDE
    MAPEO --> GEMINI
    OAI --> EVENTOS
    CLAUDE --> EVENTOS
    GEMINI --> EVENTOS
    EVENTOS --> TOOLCALL
    TOOLCALL --> MCP
    MCP --> EVENTOS
    EVENTOS --> VALIDAR
    VALIDAR --> TRAZA_APP
    TRAZA_APP --> UI
    AGENTSDK --> REQ
    AGENTSDK --> MCP
    AGENTSDK --> A2A
    A2A --> AGENTSDK
    ERRORES --> TRAZA_APP
    SDK --> ERRORES

    classDef own fill:#F5F5F5,stroke:#000000,stroke-width:2,color:#111111
    classDef external fill:#FFFFFF,stroke:#000000,stroke-width:1.4,color:#111111,stroke-dasharray:5 5
    class UI,DOMINIO,TRAZA_APP,REQ,INSTR,ENTRADA,PARAMS,SCHEMA,TOOLS,SDK,MAPEO,ERRORES,EVENTOS,TOOLCALL,VALIDAR own
    class OAI,CLAUDE,GEMINI,MCP,AGENTSDK,A2A external

En el día a día

En un proyecto real, este capítulo aparece cuando alguien dice: “ya tenemos el prompt, vamos a integrarlo”. Ahí empieza el trabajo serio. Hay que decidir qué parte será configuración, qué parte será código, qué parte será schema y qué parte quedará en logs para poder depurar.

Si una respuesta alimenta otra pantalla, no basta con que “se lea bien”. Debe llegar como objeto fiable. Si una respuesta se muestra mientras se genera, debes pensar en streaming y cancelación. Si el modelo pide una tool, debes decidir quién ejecuta, con qué permisos, cómo se registra y qué ocurre si faltan argumentos.

La integración buena suele tener una capa intermedia: tu aplicación habla en términos de dominio, y esa capa traduce a la API concreta. Así evitas que cada pantalla dependa de detalles de un proveedor.

Por qué debería importarte

Porque una mala integración convierte una capacidad potente en un sistema difícil de mantener. Si guardas texto sin estructura, mañana no podrás medir. Si no validas schema, el backend se rompe tarde. Si no separas mensajes, no sabrás qué instrucción produjo qué comportamiento. Si no registras eventos de streaming y tools, no podrás explicar por qué una respuesta salió como salió.

La buena noticia: una API bien tratada como contrato permite cambiar modelos, añadir RAG, introducir tools y medir calidad sin rehacer todo el producto.

SDKs, ADK, MCP y A2A: cada cosa en su capa

Aquí suele nacer mucha confusión porque todo parece “la API”. No lo es. Una API es el contrato de red y datos. Un SDK es una biblioteca en tu lenguaje que envuelve esa API. Un framework de agentes es una capa que orquesta pasos, estado, tools y decisiones. Y un protocolo de interoperabilidad define cómo se comunican sistemas que quizá ni comparten proveedor ni framework.

OpenAI documenta SDKs oficiales para lenguajes como JavaScript/TypeScript y Python, pensados para llamar a la API desde código de aplicación.25 El SDK no cambia la semántica: si el endpoint espera input, tools, stream o un schema, el SDK lo expresa con tipos, métodos, helpers de streaming, subida de archivos y manejo de errores. Es cómodo, pero no sustituye al diseño del contrato.

OpenAI también documenta Agents SDK para casos donde tu servidor posee la orquestación, la ejecución de tools, el estado y las aprobaciones del flujo.26 Google ADK va en esa misma familia de herramientas de construcción de agentes: su documentación organiza piezas como agentes, equipos de agentes, workflows, ejecución, observabilidad, evaluación, tools, sesiones, memoria, artefactos, MCP y A2A.27 La propia documentación de ADK incluye guías para exponer agentes a otros sistemas y consumir agentes remotos mediante A2A.28

MCP y A2A resuelven problemas distintos. ADK describe MCP como un estándar para que LLMs y agentes se comuniquen con aplicaciones externas, fuentes de datos y herramientas mediante recursos, prompts y tools.29 A2A, en cambio, es para comunicación entre agentes: la documentación oficial lo presenta como un estándar abierto para interoperabilidad entre agentes construidos con distintos frameworks o proveedores.30

Técnicamente, A2A introduce piezas que no aparecen en una simple llamada a un modelo: AgentCard, Message, Part, Task, TaskStatus, Artifact, métodos para enviar mensajes, consultar tareas, cancelar, suscribirse a eventos y entregar resultados por streaming o notificaciones.31 La AgentCard dice qué agente hay al otro lado, qué capacidades ofrece y cómo se accede. Un Message lleva partes de contenido; una Task representa trabajo con estado; un Artifact es una salida producida por el agente remoto.

CapaObjeto principalQuién controla el estadoPara qué sirve
API de modelosRequest y response.Tu aplicación y el proveedor.Generar, estructurar, llamar tools o recibir eventos.
SDKCliente tipado del lenguaje.Tu aplicación.Autenticación, tipos, helpers, streaming y errores.
Agents SDK / ADKRun, sesión, agente, workflow, tool context.Tu servidor o runtime de agentes.Orquestar pasos, tools, memoria, evaluación y observabilidad.
MCPTool, recurso, prompt.Host de IA y servidor MCP.Conectar agentes o apps a herramientas y datos externos.
A2AAgent Card, Message, Task, Part, Artifact.Agente cliente y agente remoto.Delegar tareas entre agentes independientes y recibir progreso o resultados.

Para entenderlo con una situación concreta: si una app de la universidad pregunta a un modelo “clasifica esta solicitud”, quizá basta la API y un SDK. Si necesita consultar expediente, el modelo puede pedir una tool; esa tool puede venir de tu backend o de un servidor MCP. Si además hay un agente remoto especializado en normativa académica, tu agente podría descubrirlo mediante una AgentCard, enviarle un Message, recibir una Task en estado working, escuchar eventos y recoger un Artifact final. Ahí ya no estás “llamando a un modelo”: estás coordinando sistemas.

Dónde volverá a aparecer

Este capítulo es el puente entre diagnóstico y construcción. Lo usaremos varias veces:

ConceptoDónde vuelvePara qué
Tokens y contextoCapítulo 03.Calcular coste, límites y tamaño de entrada/salida.
Model cardsCapítulo 04.Elegir modelo según capacidades reales.
Modelos localesCapítulos 05 y 06.Traducir la misma idea de API a entornos locales o privados.
Embeddings y RAGCapítulos 07 a 10.Añadir contexto externo y evaluar si fundamenta la respuesta.
MultimodalidadCapítulo 11.Enviar documentos, imágenes o capturas con criterio técnico.
Text-to-SQLCapítulo 12.Convertir lenguaje natural en consultas validadas.
Agentes, MCP y A2AFacsímil 05.Pasar de llamadas aisladas a workflows con tools, agentes remotos y protocolos.

Dónde solía tropezar yo

Estos tropiezos aparecen cuando uno pasa de probar prompts a construir una aplicación que tiene que vivir.

ErrorPor qué es un errorAntídoto
Meter todo en un único promptInstrucción, contexto, formato y datos se vuelven inseparables.Construir mensajes y contexto por capas.
Validar solo que haya JSONUn objeto puede estar bien formado y contener una decisión mala.Separar validación estructural y semántica.
Confundir tool call con ejecuciónQue el modelo pida una función no significa que deba ejecutarse sin más.Validar argumentos y aplicar reglas de negocio antes de ejecutar.
Usar streaming sin estado claroSi se corta el flujo, puedes dejar la UI o la traza a medias.Diseñar estados: iniciado, parcial, completo, cancelado y error.
Mandar documentos sin referenciaLa respuesta queda desligada del archivo, página o versión que la produjo.Guardar identificador, páginas usadas y schema de extracción.
Acoplarse al proveedor demasiado prontoCada pantalla acaba hablando el dialecto de una API concreta.Usar un contrato propio y adaptadores por proveedor.
Confundir SDK con arquitecturaEl SDK facilita la llamada, pero no decide schemas, permisos, trazas ni evaluación.Diseñar primero contrato y flujo; elegir SDK después.
Mezclar MCP y A2AMCP conecta tools y recursos; A2A conecta agentes completos con tareas y artefactos.Dibujar qué sistema habla con qué sistema antes de integrar.
No guardar trazas mínimasSin request, respuesta, schema y versión de modelo no puedes reproducir fallos.Registrar lo necesario para depurar sin guardar datos innecesarios.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a construir una petición de API completa sin llamar a Internet. Es decir: no necesitamos clave, pero sí vamos a ver la forma mental correcta de una integración real. Prepararemos un contrato de producto, lo traduciremos a un payload de OpenAI Responses API y dejaremos equivalentes para Claude Messages API y Gemini API. El objetivo no es memorizar cada nombre de campo, sino ver dónde vive cada decisión.

Fíjate en algo importante: timeout, retry_policy e idempotency_key no son parámetros del modelo; son parte de tu cliente HTTP o SDK. En una integración seria también deben estar configurados, aunque no viajen dentro del JSON del proveedor.

import json
from copy import deepcopy

RESPUESTA_SCHEMA = {
    "type": "object",
    "additionalProperties": False,
    "required": [
        "categoria",
        "prioridad",
        "siguiente_paso",
        "confianza",
        "evidencias",
        "necesita_tool",
    ],
    "properties": {
        "categoria": {
            "type": "string",
            "enum": ["matricula", "pagos", "beca", "soporte", "otro"],
        },
        "prioridad": {
            "type": "string",
            "enum": ["baja", "media", "alta"],
        },
        "siguiente_paso": {"type": "string"},
        "confianza": {"type": "number", "minimum": 0, "maximum": 1},
        "evidencias": {
            "type": "array",
            "items": {
                "type": "object",
                "additionalProperties": False,
                "required": ["fuente", "detalle"],
                "properties": {
                    "fuente": {"type": "string"},
                    "detalle": {"type": "string"},
                },
            },
        },
        "necesita_tool": {"type": "boolean"},
    },
}

TOOL_CONSULTAR_EXPEDIENTE = {
    "type": "function",
    "name": "consultar_expediente",
    "description": "Consulta datos mínimos de matrícula para un alumno.",
    "parameters": {
        "type": "object",
        "additionalProperties": False,
        "required": ["id_alumno", "curso"],
        "properties": {
            "id_alumno": {"type": "string"},
            "curso": {"type": "string"},
        },
    },
}

contrato_producto = {
    "trace_id": "trc_matricula_2026_00042",
    "schema_version": "clasificacion_matricula.v2",
    "feature": "asistente_matricula",
    "usuario_hash": "usr_anon_8f31",
    "modelo": "modelo-multimodal-vigente",
    "temperatura": 0.2,
    "top_p": 0.9,
    "max_salida": 900,
    "stream": True,
    "timeout_segundos": 12,
    "reintentos": 2,
}

openai_responses_request = {
    "model": contrato_producto["modelo"],
    "instructions": (
        "Eres un asistente de matrícula. Clasifica la solicitud, "
        "usa herramientas solo si faltan datos de expediente y responde "
        "siempre con el schema indicado."
    ),
    "input": [
        {
            "role": "user",
            "content": [
                {
                    "type": "input_text",
                    "text": (
                        "Ana dice: he pagado la matrícula, pero el campus "
                        "sigue marcando la asignatura como pendiente."
                    ),
                },
                {
                    "type": "input_file",
                    "file_id": "file_normativa_matricula_2026",
                },
                {
                    "type": "input_image",
                    "image_url": "https://example.edu/captura-campus.png",
                },
            ],
        }
    ],
    "text": {
        "format": {
            "type": "json_schema",
            "name": "clasificacion_matricula",
            "strict": True,
            "schema": RESPUESTA_SCHEMA,
        }
    },
    "tools": [TOOL_CONSULTAR_EXPEDIENTE],
    "tool_choice": "auto",
    "temperature": contrato_producto["temperatura"],
    "top_p": contrato_producto["top_p"],
    "max_output_tokens": contrato_producto["max_salida"],
    "parallel_tool_calls": False,
    "stream": contrato_producto["stream"],
    "store": False,
    "metadata": {
        "trace_id": contrato_producto["trace_id"],
        "feature": contrato_producto["feature"],
        "schema_version": contrato_producto["schema_version"],
    },
}

cliente_http = {
    "method": "POST",
    "url": "https://api.openai.com/v1/responses",
    "headers": {
        "Authorization": "Bearer $OPENAI_API_KEY",
        "Content-Type": "application/json",
    },
    "json": openai_responses_request,
    "timeout_seconds": contrato_producto["timeout_segundos"],
    "retry_policy": {
        "max_attempts": contrato_producto["reintentos"],
        "retry_on_status": [429, 500, 502, 503, 504],
        "idempotency_key": contrato_producto["trace_id"],
    },
}

# Con un SDK real, esta sería la idea:
# client.responses.create(**openai_responses_request)

anthropic_messages_request = {
    "model": "claude-modelo-vigente",
    "max_tokens": contrato_producto["max_salida"],
    "system": openai_responses_request["instructions"],
    "messages": [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": openai_responses_request["input"][0]["content"][0]["text"]},
                {
                    "type": "document",
                    "source": {
                        "type": "base64",
                        "media_type": "application/pdf",
                        "data": "<pdf_base64>",
                    },
                },
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/png",
                        "data": "<png_base64>",
                    },
                },
            ],
        }
    ],
    "tools": [
        {
            "name": TOOL_CONSULTAR_EXPEDIENTE["name"],
            "description": TOOL_CONSULTAR_EXPEDIENTE["description"],
            "input_schema": TOOL_CONSULTAR_EXPEDIENTE["parameters"],
        }
    ],
    "tool_choice": {"type": "auto"},
    "temperature": contrato_producto["temperatura"],
    "top_p": contrato_producto["top_p"],
    "top_k": 40,
    "stop_sequences": [],
    "stream": contrato_producto["stream"],
    "metadata": {"user_id": contrato_producto["usuario_hash"]},
}

gemini_generate_content_request = {
    "systemInstruction": {
        "parts": [{"text": openai_responses_request["instructions"]}]
    },
    "contents": [
        {
            "role": "user",
            "parts": [
                {"text": openai_responses_request["input"][0]["content"][0]["text"]},
                {
                    "fileData": {
                        "mimeType": "application/pdf",
                        "fileUri": "files/normativa_matricula_2026",
                    }
                },
                {
                    "inlineData": {
                        "mimeType": "image/png",
                        "data": "<png_base64>",
                    }
                },
            ],
        }
    ],
    "generationConfig": {
        "temperature": contrato_producto["temperatura"],
        "topP": contrato_producto["top_p"],
        "topK": 40,
        "maxOutputTokens": contrato_producto["max_salida"],
        "responseMimeType": "application/json",
        "responseJsonSchema": RESPUESTA_SCHEMA,
        "stopSequences": [],
    },
    "tools": [
        {
            "functionDeclarations": [
                {
                    "name": TOOL_CONSULTAR_EXPEDIENTE["name"],
                    "description": TOOL_CONSULTAR_EXPEDIENTE["description"],
                    "parameters": TOOL_CONSULTAR_EXPEDIENTE["parameters"],
                }
            ]
        }
    ],
    "safetySettings": [],
}

respuesta_simulada = {
    "categoria": "matricula",
    "prioridad": "alta",
    "siguiente_paso": "Consultar expediente y comprobar conciliación del pago.",
    "confianza": 0.82,
    "evidencias": [
        {
            "fuente": "normativa_matricula_2026",
            "detalle": "La matrícula queda activa cuando pago y expediente coinciden.",
        }
    ],
    "necesita_tool": True,
}

def validar_schema_minimo(objeto, schema):
    faltan = sorted(set(schema["required"]) - set(objeto))
    sobran = sorted(set(objeto) - set(schema["properties"]))
    return {"faltan": faltan, "sobran": sobran, "valido": not faltan and not sobran}

print("endpoint:", cliente_http["method"], cliente_http["url"])
print("timeout:", cliente_http["timeout_seconds"])
print("reintentos:", cliente_http["retry_policy"]["max_attempts"])
print("openai_params:", sorted(openai_responses_request.keys()))
print("anthropic_params:", sorted(anthropic_messages_request.keys()))
print("gemini_params:", sorted(gemini_generate_content_request.keys()))
print("tool:", openai_responses_request["tools"][0]["name"])
print("schema_required:", RESPUESTA_SCHEMA["required"])
print("validacion:", validar_schema_minimo(deepcopy(respuesta_simulada), RESPUESTA_SCHEMA))

Salida esperada:

endpoint: POST https://api.openai.com/v1/responses
timeout: 12
reintentos: 2
openai_params: ['input', 'instructions', 'max_output_tokens', 'metadata', 'model', 'parallel_tool_calls', 'store', 'stream', 'temperature', 'text', 'tool_choice', 'tools', 'top_p']
anthropic_params: ['max_tokens', 'messages', 'metadata', 'model', 'stop_sequences', 'stream', 'system', 'temperature', 'tool_choice', 'tools', 'top_k', 'top_p']
gemini_params: ['contents', 'generationConfig', 'safetySettings', 'systemInstruction', 'tools']
tool: consultar_expediente
schema_required: ['categoria', 'prioridad', 'siguiente_paso', 'confianza', 'evidencias', 'necesita_tool']
validacion: {'faltan': [], 'sobran': [], 'valido': True}

Ahora quita evidencias de respuesta_simulada y vuelve a ejecutar. La API podría haber devuelto texto convincente, pero tu contrato dirá que falta una pieza obligatoria. Ese es el salto que buscábamos: no solo “recibir JSON”, sino diseñar una llamada con parámetros, operación, tools y validación.

Cómo encaja todo

Este mapa sitúa el capítulo dentro del facsímil. El capítulo anterior decide qué intervención toca; este convierte esa decisión en contrato de API.

graph TD
    subgraph "Capítulo 2: Contratos de API"
        APP["Aplicación"]
        ADAPTADOR["Adaptador de proveedor"]
        SDK["SDK"]
        MSG["Mensajes"]
        MULTI["Contenido multimodal"]
        PARAMS["Parámetros de generación"]
        SCHEMA["Schema"]
        STREAM["Streaming"]
        TOOL["Tool call"]
        VALIDAR["Validación"]
        TRAZA["Traza"]
    end
    subgraph "Viene de capítulos anteriores"
        DIAG["Diagnóstico<br/>de intervención<br/>(F4C1)"]
        LLM["LLM y contexto (F3)"]
        LOGITS["Salida probabilística<br/>(F3C4)"]
    end
    subgraph "Continuidad del facsímil 4"
        TOKENS["Tokens y coste (F4C3)"]
        MODELOS["Model cards (F4C4)"]
        LOCAL["Modelos locales<br/>(F4C5-06)"]
        RAG["RAG y evaluación<br/>(F4C7-10)"]
        MULTIFUT["Multimodalidad<br/>aplicada (F4C11)"]
        SQL["Text-to-SQL (F4C12)"]
    end
    subgraph "Continuidad en agentes"
        ADK["ADK y Agents SDK (F5)"]
        MCPNODE["MCP: tools y recursos (F5)"]
        A2ANODE["A2A: agentes remotos (F5)"]
    end

    DIAG --> APP
    LLM --> MSG
    LOGITS --> VALIDAR
    APP --> ADAPTADOR
    ADAPTADOR --> SDK
    SDK --> MSG
    SDK --> MULTI
    SDK --> PARAMS
    MSG --> SCHEMA
    MSG --> STREAM
    MSG --> TOOL
    MULTI --> SCHEMA
    PARAMS --> STREAM
    SCHEMA --> VALIDAR
    TOOL --> VALIDAR
    STREAM --> TRAZA
    VALIDAR --> TRAZA
    MSG --> TOKENS
    SCHEMA --> MODELOS
    ADAPTADOR --> LOCAL
    MSG --> RAG
    MULTI --> MULTIFUT
    TOOL --> SQL
    TOOL --> MCPNODE
    SDK --> ADK
    ADK --> A2ANODE

    style APP fill:#F5F5F5,stroke:#000000,stroke-width:2
    style ADAPTADOR fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SDK fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MSG fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MULTI fill:#F5F5F5,stroke:#000000,stroke-width:2
    style PARAMS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SCHEMA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style STREAM fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TOOL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style VALIDAR fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TRAZA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style DIAG stroke-dasharray: 5 5
    style LLM stroke-dasharray: 5 5
    style LOGITS stroke-dasharray: 5 5
    style TOKENS stroke-dasharray: 5 5
    style MODELOS stroke-dasharray: 5 5
    style LOCAL stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style MULTIFUT stroke-dasharray: 5 5
    style SQL stroke-dasharray: 5 5
    style ADK stroke-dasharray: 5 5
    style MCPNODE stroke-dasharray: 5 5
    style A2ANODE stroke-dasharray: 5 5

Vocabulario aprendido

Estos términos nos permiten hablar de integración sin mezclarlo todo en “el prompt”.

TérminoDefinición
API de modelosInterfaz para enviar entradas a un modelo y recibir salidas bajo un contrato técnico.
MensajePieza de conversación con rol y contenido.
RolEtiqueta que indica qué función cumple un mensaje dentro de la petición.
StreamingEntrega progresiva de la respuesta por eventos o fragmentos.
Salida estructuradaRespuesta obligada a cumplir un schema.
SchemaContrato que define campos, tipos y restricciones de una salida.
Tool callPetición del modelo para que el sistema ejecute una función externa.
ValidadorCódigo que comprueba si una salida cumple el contrato esperado.
Contenido multimodalEntrada formada por texto, imágenes, documentos, audio o vídeo según lo permita el modelo.
Adaptador de proveedorCapa que traduce el contrato propio de la aplicación al formato de una API concreta.
Tool choiceOpción que limita o fuerza qué herramienta puede pedir el modelo durante una llamada.
IdempotenciaPropiedad de repetir una operación sin duplicar efectos cuando hay reintentos.
SDKBiblioteca cliente que envuelve una API desde un lenguaje concreto.
ADKFramework para construir agentes con tools, sesiones, memoria, workflows y observabilidad.
MCPProtocolo para conectar aplicaciones de IA con herramientas, recursos y contexto externos.
A2AProtocolo para que agentes independientes se descubran, se envíen mensajes y coordinen tareas.
Agent CardDocumento de metadatos que publica identidad, endpoint, capacidades y requisitos de acceso de un agente.
TrazaRegistro mínimo de lo enviado, recibido y validado para poder depurar.

Antes de pasar página

  • ¿Puedo explicar por qué una API de modelos no es solo un prompt por HTTP?
  • ¿Sé comparar OpenAI Responses API, Claude Messages API y Gemini API sin memorizar cada SDK?
  • ¿Reconozco las familias de parámetros: modelo, entrada, generación, tools, schema, streaming y operación?
  • ¿Sé separar instrucciones, mensaje de usuario, contexto recuperado, tool y salida?
  • ¿Entiendo qué cambia cuando la entrada incluye documentos o imágenes?
  • ¿Puedo distinguir salida estructurada de tool call?
  • ¿Entiendo por qué JSON válido no significa decisión correcta?
  • ¿Sé explicar cuándo streaming mejora experiencia y qué complica?
  • ¿Puedo describir una capa de adaptador que proteja mi producto de cambios de proveedor?
  • ¿Puedo distinguir API, SDK, Agents SDK, ADK, MCP y A2A sin meterlos en el mismo saco?
  • ¿Sé explicar qué son AgentCard, Task, Message, Part y Artifact dentro de A2A?
  • ¿Puedo diseñar un schema mínimo para una respuesta que usará otro software?
  • ¿He ejecutado la práctica y probado un caso que el validador rechace?

En resumen

Una buena integración trata la API como contrato. El modelo produce una salida; tu aplicación decide cómo construir la petición, validar la respuesta y registrar lo necesario.

Idea fuerzaDetalle
La API no es una caja de texto.Es una frontera entre software probabilístico y software verificable.
Los proveedores cambian de dialecto, no de problema.OpenAI, Claude y Gemini piden piezas parecidas con nombres y límites distintos.
Los mensajes separan responsabilidades.Instrucciones, usuario, contexto y tools no deberían vivir en una sola cadena.
La multimodalidad exige trazabilidad.Si entran documentos o imágenes, guarda fuente, versión, página y criterio de validación.
El schema convierte texto en dato.Pero aún necesitas validar si el contenido tiene sentido.
Streaming mejora la experiencia percibida.También obliga a manejar estados parciales y cancelación.
Una tool call no es ejecución automática.La aplicación valida argumentos y decide qué hacer.
El adaptador protege tu producto.Tu dominio debería hablar su propio contrato y traducirlo al proveedor.
SDK y ADK no son lo mismo.El SDK llama APIs; ADK o Agents SDK orquestan agentes, tools, sesiones y workflows.
MCP y A2A resuelven fronteras distintas.MCP conecta herramientas y recursos; A2A conecta agentes completos mediante tareas y artefactos.
Sin trazas no hay depuración seria.Guardar contrato, versión y resultado ayuda a reproducir problemas.

Para saber más

Anthropic. (2026). Messages API. https://platform.claude.com/docs/en/api/messages

Anthropic. (2026). PDF support. https://platform.claude.com/docs/en/build-with-claude/pdf-support

Anthropic. (2026). Streaming messages. https://platform.claude.com/docs/en/build-with-claude/streaming

Anthropic. (2026). Structured outputs. https://platform.claude.com/docs/en/build-with-claude/structured-outputs

Anthropic. (2026). Vision. https://platform.claude.com/docs/en/build-with-claude/vision

A2A Protocol. (2026). Agent2Agent (A2A) Protocol. https://a2a-protocol.org/latest/

A2A Protocol. (2026). Overview specification. https://a2a-protocol.org/latest/specification/

Google. (2026). ADK with Agent2Agent (A2A) Protocol. https://adk.dev/a2a/

Google. (2026). Agent Development Kit. https://adk.dev/

Google. (2026). Document understanding. https://ai.google.dev/gemini-api/docs/document-processing

Google. (2026). Image understanding. https://ai.google.dev/gemini-api/docs/image-understanding

Google. (2026). Model Context Protocol (MCP). https://adk.dev/mcp/

Google. (2026). Structured outputs. https://ai.google.dev/gemini-api/docs/structured-output

Google. (2026). Text generation. https://ai.google.dev/gemini-api/docs/text-generation

JSON Schema. (2020). JSON Schema Validation: A Vocabulary for Structural Validation of JSON. https://json-schema.org/draft/2020-12/json-schema-validation

OpenAI. (2026). Create a model response. https://developers.openai.com/api/docs/api-reference/responses/create

OpenAI. (2026). File inputs. https://developers.openai.com/api/docs/guides/file-inputs

OpenAI. (2026). Function calling. https://developers.openai.com/api/docs/guides/function-calling

OpenAI. (2026). Images and vision. https://developers.openai.com/api/docs/guides/images-vision

OpenAI. (2026). Agents SDK. https://developers.openai.com/api/docs/guides/agents

OpenAI. (2026). SDKs and CLI. https://developers.openai.com/api/docs/libraries

OpenAI. (2026). Streaming API responses. https://developers.openai.com/api/docs/guides/streaming-responses

OpenAI. (2026). Structured model outputs. https://developers.openai.com/api/docs/guides/structured-outputs

OpenAI. (2026). Text generation. https://developers.openai.com/api/docs/guides/text

WHATWG. (2026). Server-sent events. https://html.spec.whatwg.org/multipage/server-sent-events.html

Notas

  1. OpenAI. (2026). Text generation. https://developers.openai.com/api/docs/guides/text. Consultado el 10 de junio de 2026.

  2. OpenAI. (2026). Structured model outputs. https://developers.openai.com/api/docs/guides/structured-outputs. Consultado el 10 de junio de 2026.

  3. OpenAI. (2026). Function calling. https://developers.openai.com/api/docs/guides/function-calling. Consultado el 10 de junio de 2026.

  4. Anthropic. (2026). Messages API. https://platform.claude.com/docs/en/api/messages. Consultado el 10 de junio de 2026.

  5. Anthropic. (2026). Streaming messages. https://platform.claude.com/docs/en/build-with-claude/streaming. Consultado el 10 de junio de 2026.

  6. Anthropic. (2026). Structured outputs. https://platform.claude.com/docs/en/build-with-claude/structured-outputs. Consultado el 10 de junio de 2026.

  7. Google. (2026). Text generation. https://ai.google.dev/gemini-api/docs/text-generation. Consultado el 10 de junio de 2026.

  8. Google. (2026). Structured outputs. https://ai.google.dev/gemini-api/docs/structured-output. Consultado el 10 de junio de 2026.

  9. Google. (2026). Interactions API overview. https://ai.google.dev/gemini-api/docs/interactions/interactions-overview. Consultado el 10 de junio de 2026.

  10. Google. (2026). Agent Development Kit. https://adk.dev/. Consultado el 10 de junio de 2026.

  11. A2A Protocol. (2026). Agent2Agent Protocol. https://a2a-protocol.org/latest/. Consultado el 10 de junio de 2026.

  12. OpenAI. (2026). Create a model response. https://developers.openai.com/api/docs/api-reference/responses/create. Consultado el 10 de junio de 2026.

  13. Anthropic. (2026). Messages API. https://platform.claude.com/docs/en/api/messages. Consultado el 10 de junio de 2026.

  14. Google. (2026). Text generation. https://ai.google.dev/gemini-api/docs/text-generation. Consultado el 10 de junio de 2026.

  15. OpenAI. (2026). File inputs. https://developers.openai.com/api/docs/guides/file-inputs. Consultado el 10 de junio de 2026.

  16. OpenAI. (2026). Images and vision. https://developers.openai.com/api/docs/guides/images-vision. Consultado el 10 de junio de 2026.

  17. Anthropic. (2026). Vision. https://platform.claude.com/docs/en/build-with-claude/vision. Consultado el 10 de junio de 2026.

  18. Anthropic. (2026). PDF support. https://platform.claude.com/docs/en/build-with-claude/pdf-support. Consultado el 10 de junio de 2026.

  19. Google. (2026). Image understanding. https://ai.google.dev/gemini-api/docs/image-understanding. Consultado el 10 de junio de 2026.

  20. Google. (2026). Document understanding. https://ai.google.dev/gemini-api/docs/document-processing. Consultado el 10 de junio de 2026.

  21. JSON Schema. (2020). JSON Schema Validation: A Vocabulary for Structural Validation of JSON. https://json-schema.org/draft/2020-12/json-schema-validation.

  22. WHATWG. (2026). Server-sent events. https://html.spec.whatwg.org/multipage/server-sent-events.html. Consultado el 10 de junio de 2026.

  23. OpenAI. (2026). Streaming API responses. https://developers.openai.com/api/docs/guides/streaming-responses. Consultado el 10 de junio de 2026.

  24. Anthropic. (2026). Streaming messages. https://platform.claude.com/docs/en/build-with-claude/streaming. Consultado el 10 de junio de 2026.

  25. OpenAI. (2026). SDKs and CLI. https://developers.openai.com/api/docs/libraries. Consultado el 10 de junio de 2026.

  26. OpenAI. (2026). Agents SDK. https://developers.openai.com/api/docs/guides/agents. Consultado el 10 de junio de 2026.

  27. Google. (2026). Agent Development Kit. https://adk.dev/. Consultado el 10 de junio de 2026.

  28. Google. (2026). ADK with Agent2Agent (A2A) Protocol. https://adk.dev/a2a/. Consultado el 10 de junio de 2026.

  29. Google. (2026). Model Context Protocol (MCP). https://adk.dev/mcp/. Consultado el 10 de junio de 2026.

  30. A2A Protocol. (2026). Agent2Agent (A2A) Protocol. https://a2a-protocol.org/latest/. Consultado el 10 de junio de 2026.

  31. A2A Protocol. (2026). Overview specification. https://a2a-protocol.org/latest/specification/. Consultado el 10 de junio de 2026.

Capítulo 03

Facsímil 4 · La caja de herramientas

Capítulo 03: Tokens, coste, contexto y caché

El presupuesto invisible de cada pregunta

Cuando una persona escribe “resúmeme este PDF” parece que está enviando una frase. En realidad puede estar enviando miles de tokens: instrucciones, historial, documento, herramientas disponibles, schema de salida y la propia pregunta. La factura no mira si la frase sonaba sencilla. Mira lo que entró, lo que salió, lo que se pudo cachear, el modelo elegido y cómo se sirvió la petición.

Este capítulo continúa el capítulo 02. Allí aprendimos a construir una llamada de API completa. Aquí hacemos la cuenta que decide si esa llamada cabe, cuánto cuesta, cuánto tarda y qué podemos reutilizar.

La idea central es esta: un sistema con IA no solo se diseña con prompts; se diseña con presupuestos de tokens.

Estado del arte con fecha de corte

Fecha de corte: 25 de mayo de 2026.
Fuentes consultadas ese día: documentación oficial de OpenAI sobre conteo de tokens, prompt caching, coste, Batch API, latencia y precios; documentación de Anthropic sobre token counting, prompt caching y ventanas de contexto; documentación de Google sobre token counting, long context, context caching y precios de Gemini API; el repositorio oficial de tiktoken; y artículos primarios sobre MoE, Switch Transformer, GShard y Mixtral.

Lo estable es el mecanismo: los modelos procesan tokens, la ventana de contexto es finita, la entrada y la salida se cobran de forma distinta, el streaming mejora la espera percibida, el batch puede abaratar trabajos diferidos y la caché solo ayuda cuando repites prefijos de forma reconocible.

Lo cambiante son los precios, modelos, nombres de campos, ventanas máximas, umbrales de cache, descuentos y límites. Para que las notas no aparezcan como una ristra de números pegados, las dejamos ordenadas por tema y proveedor:

ProveedorNota que conviene revisarPor qué importa
OpenAIConteo de tokens.1Estimar entrada, salida y compatibilidad de tokenizador.
OpenAIPrompt caching.2Diseñar prefijos repetibles y medir cache hit.
OpenAIOptimización de coste.3Separar coste de entrada, salida, cache y modelo.
OpenAIBatch API.4Pasar trabajos no interactivos a procesamiento diferido.
OpenAIOptimización de latencia.5Distinguir prefill, decode, streaming y tiempo percibido.
AnthropicToken counting.6Comparar conteos antes de migrar prompts.
AnthropicPrompt caching.7Pensar qué prefijos se reutilizan y durante cuánto tiempo.
AnthropicContext windows.8Saber qué entra, qué sale y qué se queda fuera.
GoogleConteo de tokens.9Medir prompts, archivos y respuestas antes de desplegar.
GoogleLong context.10Evaluar cuándo una ventana larga ayuda y cuándo añade ruido.
GoogleContext caching.11Revisar TTL, prefijos y reutilización de contexto.
GooglePricing de Gemini API.12Presupuestar con tarifas vigentes.
MoECapa sparsely-gated MoE.13Entender expertos, router y cómputo condicional.
MoESwitch Transformer.14Ver por qué top-1 simplifica routing, comunicación y entrenamiento.
MoEGShard.15Conectar MoE con sharding y entrenamiento distribuido.
MoEMixtral of Experts.16Ver un LLM sparse MoE moderno con routing top-2 por token.

Qué no es un token

Un token no es una palabra. “Universidad” puede ocupar un token o varios, según el tokenizer. Un emoji puede ocupar más de uno. Un espacio, una coma o una tilde pueden cambiar la cuenta. Por eso contar palabras en un documento no sirve para estimar coste con precisión.

Tampoco es una unidad humana de significado. Para el modelo, un token es una pieza de codificación aprendida para representar texto de forma eficiente. A veces coincide con algo que reconocemos; a veces es una sílaba, una terminación, un símbolo o un fragmento raro.

Y no es igual en todos los modelos. Cada familia puede usar tokenizer, reglas multimodales y contabilidad distinta. OpenAI mantiene tiktoken como tokenizador rápido para sus modelos, pero eso no implica que el mismo conteo valga para Claude, Gemini o un modelo local.17

Cómo se construye un tokenizador

Un tokenizador, o constructor de tokens, no es una lista escrita a mano con todas las palabras posibles. Es una pieza entrenada sobre un corpus: mira mucho texto, aprende piezas frecuentes y produce una tabla estable de texto -> id. El modelo se entrena después con esos ids. Por eso no puedes cambiar el tokenizador de un modelo ya entrenado como quien cambia una fuente tipográfica: cambiarías el idioma interno con el que ese modelo aprendió.

La familia más intuitiva para empezar es BPE, Byte Pair Encoding. En NLP moderno se popularizó para manejar palabras raras y vocabularios abiertos en traducción neuronal.18 La idea es sencilla: empezar con unidades pequeñas y fusionar pares frecuentes hasta construir piezas útiles.

El paso a paso mental es este:

PasoQué hacesDecisión de ingeniería
1Reúnes un corpus representativo.Si entrenas con textos legales, contarán mucho las piezas legales; si entrenas con código, contarán símbolos y nombres técnicos.
2Normalizas lo mínimo necesario.Minúsculas, Unicode, espacios y acentos cambian el vocabulario. No lo improvises.
3Partes el texto en unidades pequeñas.Caracteres, bytes o piezas iniciales. Los tokenizadores modernos suelen preferir variantes robustas a bytes.
4Cuentas pares vecinos.("c", "a"), ("a", "s"), ("s", "a"), etc.
5Fusionas el par más frecuente.Ese par pasa a ser una pieza nueva del vocabulario.
6Repites hasta llegar al tamaño de vocabulario.8 000, 32 000, 100 000 o lo que pida el modelo y el dominio.
7Guardas vocabulario y reglas de merge.Es un artefacto versionado, igual que pesos, configuración y model card.
8Codificas texto nuevo aplicando esas reglas.El resultado son ids numéricos que entran al modelo.

La regla de fusión se puede escribir así:

(u\*,v\*)=argmax(u,v)freq(u,v)(u^\*,v^\*)=\arg\max_{(u,v)} \operatorname{freq}(u,v)
SímboloSignificadoEjemplo
u,vu, vDos piezas vecinas candidatas.c y a.
freq(u,v)\operatorname{freq}(u,v)Frecuencia del par en el corpus.ca aparece 28 000 veces.
(u\*,v\*)(u^\*,v^\*)Par ganador que se fusiona.c + a pasa a ser ca.
argmax\arg\maxElige el candidato con mayor frecuencia.No devuelve la frecuencia, devuelve el par.

SentencePiece añade una idea muy útil para ingeniería multilingüe: puede entrenarse directamente sobre texto crudo y tratar los espacios como parte de la segmentación, en lugar de depender de un preprocesado específico de cada idioma.19 Esto importa porque “separar por espacios” funciona regular en inglés y español, pero se vuelve frágil con japonés, chino, emojis, código, nombres propios y formatos raros.

Detalles que un ingeniero debe tener muy presentes:

DecisiónQué rompe si se decide mal
Normalización UnicodeDos textos visualmente iguales pueden tokenizar distinto.
Tamaño de vocabularioVocabulario pequeño alarga secuencias; vocabulario enorme aumenta tabla y puede memorizar rarezas inútiles.
Tratamiento de espaciosCambia la cuenta de tokens y la reversibilidad del decode.
Tokens especialessystem, tool, separadores, imágenes o fin de texto deben tener ids reservados y documentados.
Dominio del corpusUn tokenizador entrenado en conversación puede ser torpe con código o biomedicina.
VersionadoModelo, tokenizer y plantilla de mensajes forman un paquete; si uno cambia, se revalida todo.

Qué sí es: la unidad que paga, cabe y tarda

Un token es la unidad que conecta tres preguntas de ingeniería:

PreguntaQué midePor qué importa
¿Cabe?Tokens de entrada más salida esperada frente a ventana de contexto.Si no cabe, tienes que resumir, trocear, recuperar o rechazar.
¿Cuesta?Tokens de entrada, salida, cache, batch y modelo.Dos prompts parecidos pueden tener facturas muy distintas.
¿Tarda?Tokens procesados en prefill y generados en decode.Una entrada enorme tarda antes de empezar; una salida larga tarda mientras se genera.

La llamada que hicimos en el capítulo anterior no estaba completa hasta mirar tokens. input, tools, schema, documentos e imágenes ocupan presupuesto. La respuesta también. Si pides “razona mucho y dame una respuesta larga”, no solo estás pidiendo calidad: estás comprando tokens de salida y tiempo de generación.

La cuenta mínima: entrada, salida y ventana

La primera fórmula es sencilla:

Ttotal=Tentrada+TsalidaT_{\text{total}}=T_{\text{entrada}}+T_{\text{salida}}
SímboloSignificadoEjemplo
TentradaT_{\text{entrada}}Tokens enviados al modelo.Instrucciones, historial, PDF, schema y pregunta suman 18 000 tokens.
TsalidaT_{\text{salida}}Tokens generados por el modelo.El resumen y el JSON final ocupan 900 tokens.
TtotalT_{\text{total}}Tokens de la llamada completa.18000+900=1890018\,000+900=18\,900.

Pero que el total exista no significa que quepa. Cada modelo tiene una ventana de contexto:

Tentrada+Tsalida_maxWT_{\text{entrada}}+T_{\text{salida\_max}} \leq W
SímboloSignificadoEjemplo
Tsalida_maxT_{\text{salida\_max}}Máximo de salida reservado.Reservas 1 500 tokens para contestar.
WWVentana de contexto del modelo elegido.Un modelo con 32 000 tokens de ventana.
\leqRestricción de cabida.18000+15003200018\,000+1\,500 \leq 32\,000.

El detalle importante: reservar salida también consume ventana. Si llenas toda la ventana con documentos, quizá el modelo no tiene espacio para responder. Por eso el presupuesto de contexto debe decidir cuánto se queda cada pieza.

PiezaQué suele ocuparDecisión práctica
InstruccionesPoco, pero se repite siempre.Mantenerlas cortas, estables y cacheables.
HistorialCrece sin pedir permiso.Resumir, compactar o guardar solo turnos relevantes.
DocumentosPuede dominar toda la llamada.Usar RAG, citas, páginas o troceo.
Tools y schemasNo parecen contenido, pero cuentan.Versionar y no inflar campos innecesarios.
SalidaSe paga y tarda.Limitar max_output_tokens con criterio.

Coste: la factura no mira la dificultad, mira el uso

Los proveedores suelen separar precio de entrada y precio de salida. Algunos añaden categorías para cache writes, cache reads, batch, prioridad, procesamiento flexible, audio, imagen o razonamiento. Por eso la fórmula útil no es “precio por pregunta”, sino “precio por componentes”.

Ejemplo de fórmula. Una forma general de estimarlo es:

C=TiPi+ToPo+TcrPcr+TcwPcw1000000C=\frac{T_iP_i+T_oP_o+T_{cr}P_{cr}+T_{cw}P_{cw}}{1\,000\,000}
SímboloSignificadoEjemplo
CCCoste estimado de la llamada.0,0068 euros o dólares, según tarifa.
TiT_iTokens de entrada frescos.8 000 tokens no cacheados.
PiP_iPrecio por millón de tokens de entrada.2,00 por millón en un ejemplo inventado.
ToT_oTokens de salida.700 tokens generados.
PoP_oPrecio por millón de tokens de salida.8,00 por millón en el ejemplo.
TcrT_{cr}Tokens leídos desde caché.12 000 tokens reutilizados.
PcrP_{cr}Precio por millón de tokens cacheados leídos.Menor que PiP_i si el proveedor descuenta cache read.
TcwT_{cw}Tokens escritos en caché.12 000 tokens de prefijo guardado.
PcwP_{cw}Precio por millón de cache write.Puede ser igual, mayor o no aplicar según proveedor.

No uses los números del ejemplo para presupuestar un producto real. Usa la fórmula y consulta la página oficial de precios el día que diseñes el sistema.20 Lo profesional no es saberse una tarifa de memoria; es guardar en configuración qué modelo, proveedor, fecha y precio estás usando para cada estimación.

Contexto: meter más no siempre ayuda

La ventana larga es una bendición cuando necesitas leer un expediente grande, comparar documentos o mantener una conversación compleja. Pero más contexto también compra coste, latencia y ruido. Un documento irrelevante dentro del prompt no es gratis: ocupa presupuesto y puede distraer.

Ejemplo de fórmula. El presupuesto de contexto puede pensarse así:

W=Binstrucciones+Bhistorial+Bdocumentos+Btools+BsalidaW = B_{\text{instrucciones}} + B_{\text{historial}} + B_{\text{documentos}} + B_{\text{tools}} + B_{\text{salida}}
SímboloSignificadoEjemplo
BinstruccionesB_{\text{instrucciones}}Presupuesto reservado a reglas estables.600 tokens.
BhistorialB_{\text{historial}}Presupuesto de conversación previa.2 000 tokens.
BdocumentosB_{\text{documentos}}Presupuesto para evidencia externa.20 000 tokens.
BtoolsB_{\text{tools}}Presupuesto para definiciones de herramientas y schemas.1 400 tokens.
BsalidaB_{\text{salida}}Presupuesto reservado para responder.1 500 tokens.

El error típico es decidir el contexto al final. En realidad conviene decidirlo antes de llamar a la API: “para esta tarea, el modelo puede ver tres fragmentos, no veinte; debe citar página; y si no cabe, se resume o se pregunta de nuevo”.

Contexto, memoria y KV cache no son lo mismo

En producto solemos llamar “contexto” a todo lo que acompaña a la pregunta: instrucciones del sistema, mensajes anteriores, documentos recuperados, resultados de herramientas, imágenes, tablas, preferencias del usuario y formato esperado. Para el modelo, eso no llega como recuerdos. Llega como una secuencia de ids de token en una llamada concreta.

El recorrido real es más técnico:

EtapaQué ocurreQué queda guardado
Texto a tokensEl tokenizador convierte texto y partes estructuradas en ids.La lista de ids de entrada.
Tokens a vectoresCada id se convierte en embedding y se combina con información de posición.Tensores de entrada para el transformer.
AtenciónCada capa calcula consultas, claves y valores: QQ, KK, VV.Activaciones temporales de la llamada.
PrefillEl servidor procesa todo el prefijo de entrada.Claves y valores listos para generar.
DecodeEl modelo genera un token, lo añade a la secuencia y repite.La KV cache crece token a token.
Fin de llamadaSe devuelve texto, JSON o eventos de streaming.Nada queda en los pesos del modelo por defecto.

La atención original del transformer compara cada consulta con claves y usa valores para mezclar información relevante.21 En inferencia autoregresiva, no queremos recalcular las claves y valores de todos los tokens anteriores cada vez que generamos uno nuevo. Por eso los servidores guardan una KV cache temporal.

La fórmula conceptual de atención es:

Attention(Q,K,V)=softmax(QKdk)V\operatorname{Attention}(Q,K,V)=\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V
SímboloQué significaIntuición
QQQueries o consultas.Qué está buscando el token actual.
KKKeys o claves.Qué ofrece cada token anterior para ser encontrado.
VVValues o valores.Información que se mezcla si la atención la considera útil.
dkd_kDimensión de las claves.Factor de escala para estabilizar el producto.

La KV cache guarda KK y VV ya calculados. No es memoria del usuario. Es memoria de cálculo durante la inferencia. Si una conversación tiene 20 000 tokens de entrada y genera 800 tokens de salida, el servidor va manteniendo claves y valores para evitar rehacer todo el prefijo en cada paso.

Ejemplo de fórmula. Una aproximación de memoria para KV cache es:

MKV2LBSHkvdheadbytesM_{\text{KV}}\approx 2 \cdot L \cdot B \cdot S \cdot H_{\text{kv}} \cdot d_{\text{head}} \cdot \text{bytes}
SímboloSignificadoQué mueve en la práctica
22Guardamos claves y valores.K y V, no solo una matriz.
LLNúmero de capas.Modelos más profundos consumen más cache.
BBBatch o secuencias simultáneas.Más usuarios concurrentes, más memoria.
SSLongitud de secuencia.Contexto largo y salida larga hacen crecer la cache.
HkvH_{\text{kv}}Cabezas KV.MQA/GQA reducen cabezas KV frente a atención multi-head clásica.22
dheadd_{\text{head}}Dimensión por cabeza.Depende de arquitectura.
bytes\text{bytes}Precisión usada.FP16, BF16, INT8 o variantes cuantizadas cambian memoria.

Esta fórmula explica por qué el contexto largo no es solo “más texto”. Es memoria de GPU, planificación de batch, colas y throughput. Sistemas como vLLM investigan precisamente cómo gestionar esa memoria de KV cache con menos desperdicio mediante PagedAttention.23

Ahora sí podemos separar conceptos que a menudo se mezclan:

ConceptoVive dóndeDura cuántoLo controla quiénPara qué sirve
Contexto de llamadaEn el payload enviado al modelo.Una petición.Tu aplicación.Dar instrucciones, evidencia y formato de salida.
HistorialEn tu base de datos o en el cliente.Hasta que lo borres o resumas.Tu producto.Reconstruir conversación útil.
Memoria de productoEn una base de datos, perfil, resumen o vector store.Persistente o con política de expiración.Tu producto y sus permisos.Recordar preferencias o hechos útiles entre sesiones.
KV cacheEn memoria del servidor de inferencia.Durante una secuencia o sesión gestionada por el runtime.El proveedor o tu servidor local.Acelerar decode y evitar recomputar prefijos.
Prompt cacheEn la infraestructura del proveedor o runtime.Según reglas, TTL y coincidencia de prefijo.Proveedor más diseño de prompt.Reutilizar trabajo de prefijos repetidos entre llamadas.

La frase correcta sería: “nuestro producto recuerda algo porque lo guardamos y lo volvemos a meter en el contexto”. El modelo base no actualiza sus pesos por leer un mensaje de un usuario. Para que “recuerde” en otra llamada, alguien tiene que almacenar, recuperar, filtrar y reinyectar esa información.

Un MoE capa a capa dentro de este proceso

Ahora metamos una arquitectura MoE en la misma película. No cambia la idea básica de este capítulo: entran tokens, se procesan dentro de una ventana, se genera salida y se mide coste/latencia. Lo que cambia está dentro de algunas capas del Transformer: en lugar de que todos los tokens pasen por el mismo MLP denso, un router aprende a mandar cada token a uno o varios expertos.

En un bloque Transformer denso simplificado suele pasar esto:

ut()=ht()+Attention()(LN(ht()))u_t^{(\ell)}=h_t^{(\ell)}+\operatorname{Attention}^{(\ell)}(\operatorname{LN}(h_t^{(\ell)})) ht(+1)=ut()+MLP()(LN(ut()))h_t^{(\ell+1)}=u_t^{(\ell)}+\operatorname{MLP}^{(\ell)}(\operatorname{LN}(u_t^{(\ell)}))
SímboloSignificadoEjemplo
ht()h_t^{(\ell)}Estado del token tt al entrar en la capa \ell.Vector del token “cache” en la capa 12.
Attention\operatorname{Attention}Parte que mezcla información del contexto mediante QQ, KK y VV.Lee tokens anteriores y usa la KV cache.
MLP\operatorname{MLP}Red feed-forward densa del bloque.La misma red para todos los tokens de esa capa.
LN\operatorname{LN}LayerNorm.Normaliza antes de atención o MLP.
ut()u_t^{(\ell)}Estado tras atención y residual.El token ya trae información del contexto.

En un MoE, la parte que se suele sustituir es el MLP. La atención sigue mezclando contexto; después entra el router:

st()=Wr()LN(ut())s_t^{(\ell)}=W_r^{(\ell)}\operatorname{LN}(u_t^{(\ell)}) pt()=softmax(st())p_t^{(\ell)}=\operatorname{softmax}(s_t^{(\ell)}) Ct()=TopK(pt(),k)C_t^{(\ell)}=\operatorname{TopK}(p_t^{(\ell)}, k) MoE()(ut)=iCt()αiEi()(LN(ut()))\operatorname{MoE}^{(\ell)}(u_t)=\sum_{i \in C_t^{(\ell)}}\alpha_i E_i^{(\ell)}(\operatorname{LN}(u_t^{(\ell)})) ht(+1)=ut()+MoE()(ut)h_t^{(\ell+1)}=u_t^{(\ell)}+\operatorname{MoE}^{(\ell)}(u_t)
SímboloSignificadoEjemplo
Wr()W_r^{(\ell)}Matriz aprendida del router en la capa \ell.Produce una puntuación por experto.
st()s_t^{(\ell)}Logits del router para el token tt.Ocho puntuaciones si hay ocho expertos.
pt()p_t^{(\ell)}Probabilidades de routing tras softmax.[0.02, 0.61, 0.04, 0.21, ...].
kkNúmero de expertos activados por token.Switch usa top-1; Mixtral usa top-2.
Ct()C_t^{(\ell)}Conjunto de expertos elegidos para ese token y esa capa.Expertos 2 y 4 en la capa 18.
Ei()E_i^{(\ell)}Experto ii, normalmente una red feed-forward.Un MLP independiente dentro de la capa.
αi\alpha_iPeso normalizado con el que se combina cada experto seleccionado.Si top-2, cada experto aporta una parte.

La película layer a layer queda así:

PasoQué le pasa al tokenQué importa para coste, contexto y caché
1El texto ya fue tokenizado y convertido en embeddings.El MoE no cambia el conteo de tokens de entrada.
2El token entra en la capa \ell con un vector ht()h_t^{(\ell)}.La secuencia sigue teniendo longitud SS.
3La atención calcula QQ, KK, VV y mezcla contexto.La KV cache sigue siendo de atención, no de “memoria del experto”.
4El resultado pasa por residual y normalización.El token ya trae información de tokens anteriores.
5El router calcula puntuaciones para expertos.Aparece cómputo extra pequeño: routing.
6Se eligen kk expertos.Cambian los parámetros activos por token.
7El runtime agrupa tokens por experto.En GPU distribuida puede haber comunicación entre dispositivos.
8Cada experto procesa los tokens que le tocaron.No se ejecutan todos los expertos para cada token.
9Se combinan las salidas de los expertos elegidos.En top-2 hay mezcla; en top-1 se simplifica.
10Se suma residual y el token pasa a la siguiente capa.En la capa siguiente puede elegir expertos distintos.

Visualmente, una capa MoE se entiende mejor si la dibujamos como una cinta de procesamiento: el token pasa por atención, el router decide, solo algunos expertos trabajan y sus salidas se recombinan.

Recorrido de un token en una capa MoE Una capa MoE vista token a token La atención mezcla contexto; el router decide expertos; solo una parte de los parámetros se activa. Token t entra en capa l hₜˡ Atención causal Q · K · V usa KV cache lee contexto previo Residual + LN prepara el vector uₜˡ sin cambiar tokens Router MoE scores para 8 expertos top-k: E2 y E6 Dispatch agrupa tokens por experto seleccionado; los demás expertos no se ejecutan para este token Banco de expertos de la capa l muchos parámetros almacenados; pocos parámetros activos por token E1 inactivo MLP E2 activo procesa hₜ E3 inactivo MLP E4 inactivo MLP E5 inactivo MLP E6 activo procesa hₜ E7 inactivo MLP E8 inactivo MLP Combinar α₂E₂(hₜ) + α₆E₆(hₜ) más residual sale una sola representación Token t entra en capa l+1 Lectura correcta Tokens y ventana no cambian · la KV cache vive en atención · expertos no guardan memoria de usuario. El coste interno depende de top-k, batch, distribución de expertos y comunicación entre dispositivos. IA para gente curiosa / Facsímil 04 / Capítulo 03 / 686f6c61

Esto explica una frase que en fichas técnicas suele confundirse: parámetros totales no son parámetros activos. Un MoE puede tener muchos parámetros almacenados porque tiene muchos expertos, pero cada token usa solo una parte. Mixtral, citado en el bloque de estado del arte, describe capas con ocho bloques feed-forward y selección de dos expertos por token y por capa; el propio artículo distingue entre parámetros accesibles y parámetros activos durante inferencia.

En serving, la parte delicada no es solo matemática. Es logística:

Problema operativoQué ocurre
Balanceo de cargaSi demasiados tokens van al mismo experto, ese experto se convierte en cuello de botella.
Capacidad por expertoLos runtimes suelen limitar cuántos tokens procesa cada experto por lote.
ComunicaciónSi los expertos viven en dispositivos distintos, los tokens o activaciones tienen que moverse.
Batch irregularDos peticiones con los mismos tokens de entrada no tienen por qué activar exactamente la misma distribución de expertos.
Métrica de costeEl usuario paga tokens, pero el proveedor opera con parámetros activos, routing, memoria y comunicación.

Por eso MoE es muy relevante para este capítulo: te obliga a leer “coste por token” con más finura. Desde fuera sigues viendo tokens de entrada, tokens de salida, ventana y precio. Por dentro, cada token atraviesa todas las capas, pero en las capas MoE solo activa una ruta dispersa. El contexto no se “guarda en los expertos”; la memoria temporal de contexto sigue estando en la atención y su KV cache. Los expertos transforman representaciones, no almacenan recuerdos de usuario.

Caché: repetir bien para pagar y esperar menos

Prompt caching aprovecha un hecho simple: muchas llamadas repiten prefijos. Las instrucciones del sistema, las herramientas, el schema, una normativa larga o un conjunto de ejemplos pueden ser iguales durante muchas peticiones. Si el proveedor reconoce ese prefijo, puede reutilizar trabajo ya hecho.

En las guías citadas al inicio, OpenAI describe caché para mensajes, imágenes, tool use y structured outputs, y recomienda colocar el contenido estático o repetido al principio y lo dinámico al final. Anthropic explica breakpoints explícitos y automáticos, con especial atención al orden tools, system y messages en el prefijo cacheable. Gemini distingue caché implícita y caché explícita con TTL configurable.

Hay dos cachés que se confunden mucho:

CachéQué reutilizaCuándo la notasQué mirar
KV cacheKK y VV ya calculados dentro de una secuencia.En decode, porque cada token nuevo no recalcula todo el prefijo.Memoria, longitud de secuencia, batch, throughput.
Prompt cache o context cacheUn prefijo repetido entre llamadas.En coste, latencia o campos de usage del proveedor.Orden estable del prompt, TTL, cache hit y contenido dinámico al final.

El diseño de prompt cache es casi diseño de APIs: si serializas un JSON con claves en orden aleatorio, metes timestamps arriba o cambias ejemplos sin necesidad, rompes coincidencias. Si colocas primero instrucciones, tools, schema y documentos estables, y dejas al final la pregunta concreta del usuario, aumentas la probabilidad de reutilización.

La métrica que queremos mirar es:

H=Tcache_hitTentradaH=\frac{T_{\text{cache\_hit}}}{T_{\text{entrada}}}
SímboloSignificadoEjemplo
HHProporción de entrada servida desde caché.0,720{,}72, es decir, 72 %.
Tcache_hitT_{\text{cache\_hit}}Tokens de entrada que fueron cache hit.14 400 tokens.
TentradaT_{\text{entrada}}Tokens de entrada totales.20 000 tokens.

Para entenderlo: si cada petición incluye una normativa de 15 000 tokens y solo cambia la pregunta final, la caché puede tener sentido. Si cada petición mete documentos distintos, timestamps dentro del prefijo y orden aleatorio de fragmentos, la caché probablemente no ayudará.

Latencia: prefill, decode y espera percibida

La latencia no es una sola cosa. Hay tiempo de red, cola, prefill, generación y renderizado. En modelos de lenguaje, una intuición útil es separar entrada y salida:

LL0+αTentrada_fresca+βTsalidaL \approx L_0 + \alpha T_{\text{entrada\_fresca}} + \beta T_{\text{salida}}
SímboloSignificadoEjemplo
LLLatencia total aproximada.4,8 segundos.
L0L_0Coste fijo de red, cola y preparación.350 ms.
α\alphaCoste medio por token de entrada fresca.Depende de modelo y hardware.
Tentrada_frescaT_{\text{entrada\_fresca}}Entrada no cubierta por caché.3 000 tokens.
β\betaCoste medio por token generado.Depende de decode.
TsalidaT_{\text{salida}}Tokens generados.900 tokens.

El streaming no reduce necesariamente el tiempo total, pero reduce el tiempo hasta ver el primer fragmento. Batch puede reducir coste o mejorar operación cuando no necesitas respuesta inmediata. La caché puede reducir prefill si el prefijo se reutiliza. Elegir un modelo menor puede reducir coste y latencia, pero quizá empeora calidad. Todo vuelve al triángulo: calidad, coste y tiempo.

Para entenderlo antes de tocar código

Pensemos en cuatro casos cercanos:

CasoQué pesaQué haría
Tutor que corrige respuestas cortasSalida estructurada y muchas llamadas.Modelo menor, schema corto, batch si no es interactivo.
Asistente de normativa universitariaDocumento largo repetido.Prefijo estable, cache, RAG si la normativa es grande.
Chat de soporte con historial largoHistorial que crece cada turno.Compactar, resumir y guardar solo lo útil.
Analizador de facturas con imágenesImagen, OCR, schema y salida JSON.Medir tokens/latencia multimodal y limitar campos.
Copiloto interno con preferenciasMemoria de producto y contexto recuperado.Guardar preferencias fuera del modelo y reinyectar solo las relevantes.
Servicio con muchas sesiones simultáneasKV cache, batch y cola.Vigilar memoria de inferencia, longitud media y tokens por segundo.
Modelo MoE en producciónRouting, expertos y parámetros activos.Medir latencia real; no comparar solo parámetros totales.

La pregunta útil no es “¿cuántos tokens acepta el modelo más grande?”. La pregunta útil es “¿cuántos tokens necesita esta tarea para dar una respuesta fiable sin pagar ruido?”.

Mapa visual de presupuesto de tokens

Presupuesto de tokens, coste, contexto y caché Una llamada empieza con un presupuesto Entrada, salida, ventana, cache, coste y latencia se deciden juntos. Entrada instr. historial docs tokens que se procesan antes de responder Ventana entrada + salida deben caber ≤ W Salida texto · JSON · tool result tokens generados uno a uno Coste entrada · salida cache · batch Caché prefijo estable pregunta dinámica al final Latencia prefill decode Decisión modelo RAG · batch · cache Regla final No optimices el prompt aislado: optimiza la llamada completa y mide cache hit, coste, salida y latencia. IA para gente curiosa / Facsímil 04 / Capítulo 03 / 686f6c61

En el día a día

En un proyecto real, este capítulo aparece cuando alguien pregunta: “¿por qué esto cuesta tanto?” o “¿por qué tarda tanto?”. Muchas veces la respuesta no está en cambiar de modelo, sino en mirar el payload completo.

Si cada llamada manda el mismo documento largo, quizá toca cache. Si cada llamada manda veinte fragmentos RAG y solo dos eran relevantes, toca mejorar retrieval. Si el usuario necesita respuesta inmediata, quizá no puedes batchar. Si el proceso es nocturno, batch puede ser perfecto. Si la salida siempre se alarga, limita tokens de salida y cambia el contrato.

Si eliges un modelo MoE, añade una pregunta más: ¿cuántos parámetros son totales y cuántos activos por token? No necesitas ver el router interno para operar una API comercial, pero sí necesitas entender que “47B parámetros” y “13B activos” no significan lo mismo, y que la latencia real dependerá también de routing, expertos, batch y hardware.

Una integración madura registra al menos: tokens de entrada, tokens de salida, tokens cacheados si el proveedor los devuelve, modelo, arquitectura si se conoce, latencia, coste estimado, schema y motivo de selección del modelo. Sin esos datos, optimizar es opinar con cara seria.

Por qué debería importarte

Porque los tokens convierten decisiones aparentemente literarias en decisiones de producto. “Añadamos más contexto” puede duplicar coste. “Respondamos con más detalle” puede multiplicar salida. “Metamos todos los documentos” puede romper ventana o empeorar la respuesta. “Usemos el modelo grande siempre” puede ser innecesario.

MoE añade otra trampa sana: un modelo puede ser enorme en parámetros totales y, aun así, activar solo una fracción por token. Eso puede mejorar capacidad sin multiplicar igual el cómputo, pero también complica serving, balanceo y lectura de fichas técnicas.

También importa para enseñar y aprender. Cuando entiendes tokens, dejas de pensar en la IA como una caja opaca y empiezas a verla como un sistema con restricciones medibles.

Dónde volverá a aparecer

Este capítulo será una pieza recurrente del facsímil:

ConceptoDónde vuelvePara qué
TokenizadoresCapítulo 07 del facsímil 3.Entender por qué decode, throughput y límites dependen de tokens, no de palabras.
KV cacheCapítulo 07 del facsímil 3.Relacionar contexto largo con memoria de inferencia y serving.
MoE y parámetros activosCapítulo 05 del facsímil 3.Leer arquitectura, expertos y routing sin confundirlos con memoria o herramientas.
Model cardsCapítulo 04.Comparar modelos por ventana, precio, latencia y capacidades.
Modelos localesCapítulos 05 y 06.Traducir tokens a VRAM, cuantización y throughput.
Embeddings y RAGCapítulos 07 a 10.Decidir chunking, top-k y presupuesto de evidencia.
Multimodalidad aplicadaCapítulo 11.Entender cómo archivos, imágenes y audio afectan coste y contexto.
Laboratorio mínimoCapítulo 13.Registrar trazas, evals, latencia y coste por caso.

Dónde solía tropezar yo

Estos tropiezos son muy comunes cuando se pasa de una demo a una aplicación con usuarios.

ErrorPor qué es un errorAntídoto
Contar palabras y no tokensLa factura y la ventana no entienden palabras humanas.Usar el contador del proveedor o tokenizer compatible.
Pensar que todos los tokenizadores son equivalentesUn mismo texto puede producir ids y longitudes distintas según modelo.Versionar tokenizer, plantilla de mensajes y modelo como un conjunto.
Llenar la ventana hasta el bordeSi no reservas salida, el modelo no tiene espacio para contestar.Separar presupuesto de entrada y salida máxima.
Meter contexto por tranquilidadEl contexto irrelevante cuesta, tarda y puede confundir.Recuperar menos, citar mejor y medir calidad.
Confundir contexto con memoriaEl modelo no recuerda por defecto lo que no le vuelves a enviar.Guardar memoria en producto y reinyectarla con permisos y criterio.
Leer parámetros totales como coste por tokenEn MoE, muchos parámetros existen, pero solo algunos expertos se activan por token.Mirar parámetros activos, routing y latencia medida.
Pensar que el experto MoE guarda conocimiento humano etiquetadoUn experto es una subred aprendida, no “el experto de matemáticas” de forma garantizada.Hablar de rutas internas y medir comportamiento, no inventar etiquetas humanas.
Esperar demasiado de la cachéLa caché solo ayuda si repites prefijos estables.Ordenar prompt: estable primero, dinámico al final.
Mezclar prompt cache y KV cacheUna ahorra trabajo entre llamadas; la otra acelera el decode dentro de una secuencia.Medir ambas con métricas distintas.
No registrar usageSin tokens reales no puedes explicar coste ni latencia.Guardar usage, cache hit, modelo y traza por llamada.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a hacer dos prácticas pequeñas. La primera construye un tokenizador BPE mínimo para que se vea el mecanismo. La segunda usa esos tokens como unidad de presupuesto para estimar cabida, coste y caché. No son librerías de producción; son maquetas para entender qué está pasando debajo.

1. Construir un tokenizador mínimo

Este ejemplo aprende ocho fusiones sobre un corpus diminuto. Sustituye los espacios por para que el espacio sea visible y reversible. En un tokenizador real habría más normalización, más corpus, más pruebas y artefactos versionados.

from collections import Counter

corpus = [
    "la casa es clara",
    "la causa es clara",
    "casa clara",
    "cazar cuesta",
]

def preparar(texto):
    return list(texto.replace(" ", "▁")) + ["</w>"]

def contar_pares(vocab):
    pares = Counter()
    for secuencia in vocab:
        pares.update(zip(secuencia, secuencia[1:]))
    return pares

def fusionar(secuencia, par):
    fusion = "".join(par)
    salida = []
    i = 0
    while i < len(secuencia):
        if i < len(secuencia) - 1 and (secuencia[i], secuencia[i + 1]) == par:
            salida.append(fusion)
            i += 2
        else:
            salida.append(secuencia[i])
            i += 1
    return salida

vocab = [preparar(texto) for texto in corpus]
merges = []

for _ in range(8):
    par, frecuencia = contar_pares(vocab).most_common(1)[0]
    merges.append((par, "".join(par), frecuencia))
    vocab = [fusionar(secuencia, par) for secuencia in vocab]

piezas = sorted({pieza for secuencia in vocab for pieza in secuencia})
ids = {pieza: i for i, pieza in enumerate(piezas)}

def encode(texto):
    secuencia = preparar(texto)
    for par, _, _ in merges:
        secuencia = fusionar(secuencia, par)
    return secuencia

ejemplo = encode("la casa cuesta")
ejemplo_ids = [ids.get(pieza, "<unk>") for pieza in ejemplo]

print("merges_aprendidos:")
for paso, (_, pieza, frecuencia) in enumerate(merges, start=1):
    print(paso, pieza, frecuencia)
print("vocabulario:", ids)
print("tokens:", ejemplo)
print("ids:", ejemplo_ids)

Salida esperada:

merges_aprendidos:
1 ▁c 6
2 la 5
3 a</w> 4
4 sa 3
5 es 3
6 ▁cla 3
7 ▁clar 3
8 ▁clara</w> 3
vocabulario: {'a': 0, 'a</w>': 1, 'c': 2, 'es': 3, 'la': 4, 'r': 5, 'sa': 6, 't': 7, 'u': 8, 'z': 9, '▁': 10, '▁c': 11, '▁clara</w>': 12}
tokens: ['la', '▁c', 'a', 'sa', '▁c', 'u', 'es', 't', 'a</w>']
ids: [4, 11, 0, 6, 11, 8, 3, 7, 1]

Lo importante no es que el corpus sea ridículamente pequeño, sino el patrón: corpus, normalización, pares frecuentes, merges, vocabulario, ids y encode. Si entrenas con otros textos, cambia el vocabulario. Si cambias el tokenizador, cambian los ids. Si cambian los ids, el modelo ya no está leyendo el mismo idioma interno.

2. Calcular presupuesto de una llamada

Ahora simulamos una calculadora de presupuesto. Usaremos precios inventados por millón de tokens para no depender de tarifas reales. Lo importante es la estructura: entrada fresca, entrada cacheada, salida, ventana, coste, cache hit y decisión de optimización.

from dataclasses import dataclass

@dataclass
class Tarifa:
    entrada: float
    salida: float
    cache_read: float
    cache_write: float

@dataclass
class Llamada:
    instrucciones: int
    historial: int
    documentos: int
    tools_schema: int
    salida_max: int
    salida_real: int
    cache_hit: int
    cache_write: int
    ventana: int

tarifa = Tarifa(
    entrada=2.00,
    salida=8.00,
    cache_read=0.20,
    cache_write=2.50,
)

llamada = Llamada(
    instrucciones=700,
    historial=1800,
    documentos=14000,
    tools_schema=1200,
    salida_max=1500,
    salida_real=650,
    cache_hit=12000,
    cache_write=0,
    ventana=32000,
)

entrada_total = (
    llamada.instrucciones
    + llamada.historial
    + llamada.documentos
    + llamada.tools_schema
)
entrada_fresca = max(entrada_total - llamada.cache_hit, 0)
tokens_reservados = entrada_total + llamada.salida_max
tokens_reales = entrada_total + llamada.salida_real

coste = (
    entrada_fresca * tarifa.entrada
    + llamada.cache_hit * tarifa.cache_read
    + llamada.cache_write * tarifa.cache_write
    + llamada.salida_real * tarifa.salida
) / 1_000_000

cache_ratio = llamada.cache_hit / entrada_total if entrada_total else 0
margen_ventana = llamada.ventana - tokens_reservados

print("entrada_total:", entrada_total)
print("entrada_fresca:", entrada_fresca)
print("tokens_reservados:", tokens_reservados)
print("tokens_reales:", tokens_reales)
print("margen_ventana:", margen_ventana)
print("cache_hit_ratio:", round(cache_ratio, 3))
print("coste_estimado:", round(coste, 6))

if margen_ventana < 0:
    print("decision: no cabe; resume, reduce documentos o usa RAG")
elif cache_ratio < 0.3 and llamada.documentos > 5000:
    print("decision: revisar cache o retrieval; hay mucho contexto fresco")
else:
    print("decision: cabe; medir calidad y latencia antes de cambiar modelo")

Salida esperada:

entrada_total: 17700
entrada_fresca: 5700
tokens_reservados: 19200
tokens_reales: 18350
margen_ventana: 12800
cache_hit_ratio: 0.678
coste_estimado: 0.019
decision: cabe; medir calidad y latencia antes de cambiar modelo

Ahora cambia cache_hit a 0. Verás que la llamada sigue cabiendo, pero cuesta más. Luego sube documentos a 35_000. Verás que el problema ya no es solo dinero: la llamada deja de caber. Esa diferencia es el corazón del capítulo.

Cómo encaja todo

Este mapa conecta tokens con las decisiones que ya venimos construyendo: APIs, schemas, RAG, elección de modelo y operación.

graph TD
    subgraph "Capítulo 3: Tokens, coste, contexto y caché"
        TOKENIZER["Tokenizador"]
        TOK["Tokens"]
        WIN["Ventana de contexto"]
        CONTEXT["Contexto de llamada"]
        MEMORY["Memoria de producto"]
        KVCACHE["KV cache"]
        ACTIVE["Parámetros activos"]
        COST["Coste"]
        CACHE["Prompt caching"]
        LAT["Latencia"]
        BATCH["Batch"]
        BUDGET["Presupuesto de tokens"]
        USAGE["Usage y trazas"]
    end
    subgraph "Viene de capítulos anteriores"
        API["Contrato de API (F4C2)"]
        SCHEMA["Schema y tools (F4C2)"]
        ATT["QKV y atención (F3C3)"]
        MOE["MoE y router (F3C5)"]
        LLM["LLM y decode (F3C7)"]
    end
    subgraph "Continuidad del facsímil 4"
        MODELCARD["Model cards (F4C4)"]
        LOCAL["Modelos locales<br/>(F4C5-06)"]
        RAG["RAG y chunking (F4C7-10)"]
        MULTI["Multimodalidad (F4C11)"]
        EVALS["Evals y trazas (F4C13)"]
    end

    API --> BUDGET
    SCHEMA --> CONTEXT
    TOKENIZER --> TOK
    LLM --> LAT
    ATT --> KVCACHE
    MOE --> ACTIVE
    MOE --> LAT
    TOK --> WIN
    TOK --> COST
    TOK --> LAT
    CONTEXT --> TOKENIZER
    CONTEXT --> WIN
    MEMORY --> CONTEXT
    WIN --> BUDGET
    KVCACHE --> LAT
    KVCACHE --> LOCAL
    ACTIVE --> COST
    ACTIVE --> MODELCARD
    CACHE --> COST
    CACHE --> LAT
    BATCH --> COST
    BUDGET --> USAGE
    USAGE --> MODELCARD
    COST --> MODELCARD
    WIN --> RAG
    TOK --> LOCAL
    TOK --> MULTI
    USAGE --> EVALS

    style TOKENIZER fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TOK fill:#F5F5F5,stroke:#000000,stroke-width:2
    style WIN fill:#F5F5F5,stroke:#000000,stroke-width:2
    style CONTEXT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MEMORY fill:#F5F5F5,stroke:#000000,stroke-width:2
    style KVCACHE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style ACTIVE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style COST fill:#F5F5F5,stroke:#000000,stroke-width:2
    style CACHE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style LAT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style BATCH fill:#F5F5F5,stroke:#000000,stroke-width:2
    style BUDGET fill:#F5F5F5,stroke:#000000,stroke-width:2
    style USAGE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style API stroke-dasharray: 5 5
    style SCHEMA stroke-dasharray: 5 5
    style ATT stroke-dasharray: 5 5
    style MOE stroke-dasharray: 5 5
    style LLM stroke-dasharray: 5 5
    style MODELCARD stroke-dasharray: 5 5
    style LOCAL stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style MULTI stroke-dasharray: 5 5
    style EVALS stroke-dasharray: 5 5

Vocabulario aprendido

Estos términos nos permiten hablar de coste y contexto sin quedarnos en “el prompt es largo”.

TérminoDefinición
TokenUnidad mínima que el modelo procesa; puede ser una palabra, fragmento, signo o espacio.
TokenizadorSoftware que convierte texto en ids de tokens y reconstruye texto desde esos ids.
Vocabulario de tokensTabla versionada que asigna piezas a identificadores numéricos.
BPETécnica que aprende subpalabras fusionando pares frecuentes.
SentencePieceEnfoque de tokenización de subpalabras que puede aprender desde texto crudo y tratar espacios explícitamente.
Ventana de contextoLímite de tokens que pueden viajar juntos en una llamada.
Token de entradaToken enviado al modelo antes de generar.
Token de salidaToken producido por el modelo durante la respuesta.
PrefillProcesamiento inicial de la entrada.
DecodeGeneración de salida token a token.
KV cacheClaves y valores de atención guardados temporalmente para acelerar inferencia.
Memoria de productoInformación persistida por la aplicación y reinyectada en contexto cuando toca.
MoEArquitectura con varios expertos donde cada token activa solo algunos en ciertas capas.
Router MoEMódulo aprendido que puntúa expertos y elige top-k para cada token y capa.
Parámetros activosParámetros usados realmente por un token, distintos de todos los parámetros almacenados.
Prompt cachingReutilización de un prefijo repetido cuando el proveedor lo soporta.
Cache hitTokens que coinciden con contenido cacheado.
BatchProcesamiento diferido de muchas peticiones.
Presupuesto de tokensReparto planificado entre instrucciones, historial, documentos, tools y salida.

Antes de pasar página

  • ¿Puedo explicar por qué un token no es una palabra?
  • ¿Sé construir mentalmente un tokenizador BPE mínimo: corpus, pares, merges, vocabulario e ids?
  • ¿Sé calcular Tentrada+Tsalida_maxWT_{\text{entrada}}+T_{\text{salida\_max}}\leq W?
  • ¿Entiendo por qué reservar salida también consume ventana?
  • ¿Puedo distinguir contexto, historial, memoria de producto, KV cache y prompt cache?
  • ¿Puedo explicar qué hace un MoE capa a capa: atención, router, top-k, expertos y combinación?
  • ¿Sé distinguir parámetros totales y parámetros activos en un modelo MoE?
  • ¿Puedo estimar coste separando entrada, salida, cache read y cache write?
  • ¿Sé cuándo la caché puede ayudar y cuándo no?
  • ¿Puedo distinguir latencia de prefill y latencia de decode?
  • ¿Sé cuándo batch tiene sentido y cuándo rompería la experiencia?
  • ¿He ejecutado las prácticas cambiando corpus, cache_hit y documentos?

En resumen

Los tokens son la contabilidad básica de una integración con modelos. No son solo un detalle técnico: determinan cabida, coste, latencia, diseño de contexto, selección de modelo y estrategia de operación.

Idea fuerzaDetalle
El token es la unidad que paga, cabe y tarda.Palabras, documentos y herramientas se traducen a tokens.
El tokenizador es parte del modelo.Si cambias tokenizer, vocabulario o plantilla de mensajes, cambias la entrada real.
La ventana de contexto se reparte.Instrucciones, historial, documentos, tools y salida compiten por el mismo espacio.
El modelo no recuerda por defecto.La memoria útil vive en producto y se vuelve a meter en contexto.
La KV cache no es memoria de usuario.Es memoria temporal de inferencia para no recalcular claves y valores.
MoE cambia el MLP, no la naturaleza de los tokens.Cada token sigue atravesando capas, pero activa solo algunos expertos en las capas MoE.
Parámetros totales no son parámetros activos.En un MoE hay que preguntar cuántos expertos se activan por token y cómo afecta a latencia.
El coste es por componentes.Entrada, salida, cache y batch pueden tener precios distintos.
La caché necesita prefijos estables.Lo repetido va al principio; lo dinámico al final.
Más contexto no siempre mejora.Puede aumentar ruido, coste y latencia.
Sin usage no hay optimización seria.Hay que registrar tokens, cache hit, modelo, coste y latencia.

Para saber más

Ainslie, J. et al. (2023). GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. https://arxiv.org/abs/2305.13245

Anthropic. (2026). Context windows. https://platform.claude.com/docs/en/build-with-claude/context-windows

Anthropic. (2026). Prompt caching. https://platform.claude.com/docs/en/build-with-claude/prompt-caching

Anthropic. (2026). Token counting. https://platform.claude.com/docs/en/build-with-claude/token-counting

Fedus, W., Zoph, B. y Shazeer, N. (2022). Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity. https://jmlr.org/papers/v23/21-0998.html

Google. (2026). Context caching. https://ai.google.dev/gemini-api/docs/caching

Google. (2026). Gemini Developer API pricing. https://ai.google.dev/gemini-api/docs/pricing

Google. (2026). Long context. https://ai.google.dev/gemini-api/docs/long-context

Google. (2026). Token counting. https://ai.google.dev/gemini-api/docs/tokens

Jiang, A. Q. et al. (2024). Mixtral of Experts. https://arxiv.org/abs/2401.04088

Kudo, T. y Richardson, J. (2018). SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing. https://aclanthology.org/D18-2012/

Kwon, W. et al. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention. https://arxiv.org/abs/2309.06180

Lepikhin, D. et al. (2021). GShard: Scaling Giant Models with Conditional Computation and Automatic Sharding. https://arxiv.org/abs/2006.16668

OpenAI. (2026). Batch API. https://developers.openai.com/api/docs/guides/batch

OpenAI. (2026). Cost optimization. https://developers.openai.com/api/docs/guides/cost-optimization

OpenAI. (2026). Counting tokens. https://developers.openai.com/api/docs/guides/token-counting

OpenAI. (2026). Latency optimization. https://developers.openai.com/api/docs/guides/latency-optimization

OpenAI. (2026). Pricing. https://developers.openai.com/api/docs/pricing

OpenAI. (2026). Prompt caching. https://developers.openai.com/api/docs/guides/prompt-caching

OpenAI. (2026). tiktoken. https://github.com/openai/tiktoken

Sennrich, R., Haddow, B. y Birch, A. (2016). Neural Machine Translation of Rare Words with Subword Units. https://aclanthology.org/P16-1162/

Shazeer, N. et al. (2017). Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer. https://arxiv.org/abs/1701.06538

Vaswani, A. et al. (2017). Attention Is All You Need. https://papers.nips.cc/paper/7181-attention-is-all-you-need

Notas

  1. OpenAI. (2026). Counting tokens. https://developers.openai.com/api/docs/guides/token-counting. Consultado el 25 de mayo de 2026.

  2. OpenAI. (2026). Prompt caching. https://developers.openai.com/api/docs/guides/prompt-caching. Consultado el 25 de mayo de 2026.

  3. OpenAI. (2026). Cost optimization. https://developers.openai.com/api/docs/guides/cost-optimization. Consultado el 25 de mayo de 2026.

  4. OpenAI. (2026). Batch API. https://developers.openai.com/api/docs/guides/batch. Consultado el 25 de mayo de 2026.

  5. OpenAI. (2026). Latency optimization. https://developers.openai.com/api/docs/guides/latency-optimization. Consultado el 25 de mayo de 2026.

  6. Anthropic. (2026). Token counting. https://platform.claude.com/docs/en/build-with-claude/token-counting. Consultado el 25 de mayo de 2026.

  7. Anthropic. (2026). Prompt caching. https://platform.claude.com/docs/en/build-with-claude/prompt-caching. Consultado el 25 de mayo de 2026.

  8. Anthropic. (2026). Context windows. https://platform.claude.com/docs/en/build-with-claude/context-windows. Consultado el 25 de mayo de 2026.

  9. Google. (2026). Token counting. https://ai.google.dev/gemini-api/docs/tokens. Consultado el 25 de mayo de 2026.

  10. Google. (2026). Long context. https://ai.google.dev/gemini-api/docs/long-context. Consultado el 25 de mayo de 2026.

  11. Google. (2026). Context caching. https://ai.google.dev/gemini-api/docs/caching. Consultado el 25 de mayo de 2026.

  12. Google. (2026). Gemini Developer API pricing. https://ai.google.dev/gemini-api/docs/pricing. Consultado el 25 de mayo de 2026.

  13. Noam Shazeer et al. (2017). Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer. ICLR. https://arxiv.org/abs/1701.06538.

  14. William Fedus, Barret Zoph y Noam Shazeer. (2022). Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity. Journal of Machine Learning Research, 23(120), 1-39. https://jmlr.org/papers/v23/21-0998.html.

  15. Dmitry Lepikhin et al. (2021). GShard: Scaling Giant Models with Conditional Computation and Automatic Sharding. ICLR. https://arxiv.org/abs/2006.16668.

  16. Albert Q. Jiang et al. (2024). Mixtral of Experts. https://arxiv.org/abs/2401.04088.

  17. OpenAI. (2026). tiktoken. https://github.com/openai/tiktoken. Consultado el 25 de mayo de 2026.

  18. Rico Sennrich, Barry Haddow y Alexandra Birch. (2016). Neural Machine Translation of Rare Words with Subword Units. ACL. https://aclanthology.org/P16-1162/.

  19. Taku Kudo y John Richardson. (2018). SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing. EMNLP. https://aclanthology.org/D18-2012/.

  20. OpenAI. (2026). Pricing. https://developers.openai.com/api/docs/pricing. Consultado el 25 de mayo de 2026.

  21. Ashish Vaswani et al. (2017). Attention Is All You Need. NeurIPS. https://papers.nips.cc/paper/7181-attention-is-all-you-need.

  22. Joshua Ainslie et al. (2023). GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. EMNLP. https://arxiv.org/abs/2305.13245.

  23. Woosuk Kwon et al. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention. SOSP. https://arxiv.org/abs/2309.06180.

Capítulo 04

Facsímil 4 · La caja de herramientas

Capítulo 04: Model cards y elección de modelos

Elegir modelo sin dejarse arrastrar por el escaparate

Elegir modelo parece una decisión de catálogo: abres una tabla, miras cuál aparece arriba y lo usas. En proyectos reales casi nunca funciona así. El modelo que gana un benchmark general puede ser caro, lento, excesivo, poco conveniente por licencia, incómodo para tu runtime o flojo justo en el idioma, formato y dominio que necesitas.

Venimos del capítulo 03, donde vimos que tokens, coste, contexto y caché convierten una llamada aparentemente sencilla en una decisión de ingeniería. Aquí añadimos la siguiente capa: cómo leer la ficha de un modelo y decidir con criterio.

La idea central es esta: no eliges el mejor modelo en abstracto; eliges el modelo que cumple una tarea, bajo restricciones, con evidencia suficiente.

Estado del arte con fecha de corte

Fecha de corte: 14 de junio de 2026.
Fuentes consultadas: documentación oficial de modelos y precios de OpenAI, Anthropic y Google Gemini API; documentación de Hugging Face sobre model cards; definición de Open Source AI y Open Weights de la Open Source Initiative; fichas/licencias de gpt-oss, Qwen, Mistral, DeepSeek, Gemma y Llama; artículos académicos sobre model cards, datasheets y evaluación holística; y benchmarks de sistemas de inferencia.

Lo estable es el método: documentar el modelo, leer uso previsto, capacidades, límites, datos, licencia, coste, contexto, evaluación y condiciones de despliegue. Lo cambiante son los nombres de modelos, ventanas máximas, precios, disponibilidad regional, versiones preview, modelos retirados, rate limits y capacidades por API.

Actualización de apertura de modelos: el 14 de junio de 2026 se revisó además la distinción entre modelos cerrados, pesos abiertos, código abierto y Open Source AI. Es una parte especialmente cambiante porque los proveedores publican nuevas familias, licencias y repositorios con mucha frecuencia. Por eso en esta sección no basta con decir “modelo abierto”: hay que decir qué está abierto, bajo qué licencia, qué puedes modificar, qué no puedes reproducir y qué KPI vas a medir.

Para no convertir el capítulo en una lista que caduca, dejamos las fuentes ordenadas por función:

FuenteQué aportaCómo usarla
Model Cards for Model Reporting.1Marco original para documentar modelos, usos, métricas y límites.Como plantilla mental de lectura.
Datasheets for Datasets.2Documentación de datos, motivación, composición, recogida y mantenimiento.Para no leer una model card sin preguntar por datos.
Hugging Face Model Cards.3Implementación práctica: README con metadatos, licencia, datasets, evaluación y texto explicativo.Para leer modelos abiertos o open weights.
OpenAI Models.4Catálogo vivo de modelos, modalidades y usos previstos.Como foto actual, no como verdad permanente.
OpenAI Pricing.5Precios por modalidad, entrada, salida, cache, batch y variantes de servicio.Para calcular coste por tarea, no solo coste por token.
Anthropic Models.6Comparación de modelos Claude, ventanas y capacidades declaradas.Para contrastar familia, contexto y capacidades.
Anthropic Pricing.7Coste de entrada, salida, cache y operaciones por lote.Para no confundir modelo potente con solución asumible.
Google Gemini Models.8Modelos Gemini, estados estable/preview/experimental y modalidades.Para vigilar estabilidad de versión y API concreta.
Google Gemini Pricing.9Precios por modelo, modalidad, contexto y uso de API.Para revisar el coste justo el día de la decisión.
HELM.10Evaluación amplia con múltiples escenarios, métricas y transparencia de resultados.Para desconfiar de rankings de una sola métrica.
MLPerf Inference.11Benchmark de sistemas completos de inferencia.Para recordar que modelo, runtime y hardware van juntos.

Modelo cerrado, pesos abiertos y código abierto no son lo mismo

Aquí conviene ir despacio, porque esta confusión aparece muchísimo. En software tradicional, “código abierto” suele significar que puedes inspeccionar, modificar, compilar y redistribuir el código bajo ciertas condiciones. En IA generativa, el objeto que usamos no es solo código. Hay arquitectura, pesos, tokenizer, datos de entrenamiento, filtros, scripts de entrenamiento, receta de post-training, evaluaciones, plantillas de chat, runtime y, a veces, una API gestionada que no expone nada de eso.

Por eso decir “este modelo es abierto” sin apellido es demasiado vago. Puede significar una de varias cosas:

  1. El modelo está disponible por API, pero los pesos son cerrados.
  2. Los pesos se pueden descargar, pero no se publican los datos ni la receta completa de entrenamiento.
  3. Hay código de inferencia abierto, pero los pesos o datos no lo son.
  4. Hay pesos, código y suficiente información de datos/proceso para estudiar y modificar el sistema de forma profunda.

La Open Source Initiative formaliza una definición de Open Source AI basada en libertades de uso, estudio, modificación y compartición. Además, exige acceso a la forma preferida para modificar el sistema: información suficientemente detallada de datos, código usado para procesar y entrenar, y parámetros o pesos bajo términos adecuados.12 Esa definición es más exigente que “puedo descargar un checkpoint”.

La propia OSI distingue también los open weights: publicar los pesos finales ayuda mucho, pero normalmente no incluye el código de entrenamiento completo, los datos, los filtros, los checkpoints intermedios ni la receta suficiente para reproducir el modelo.13 Dicho de forma cercana: tener los pesos es como tener el edificio terminado; ayuda a vivir dentro, reformar algunas habitaciones y medir su consumo, pero no te da necesariamente los planos completos, el origen de todos los materiales ni el diario de obra.

Qué son exactamente los pesos

Los pesos son los números aprendidos durante el entrenamiento. En una red neuronal, cada capa transforma vectores usando matrices y funciones no lineales. Esas matrices contienen parámetros. Después de entrenar, esos parámetros quedan fijados en archivos: safetensors, gguf, checkpoints de PyTorch, formatos optimizados o variantes cuantizadas. Cuando alguien dice “pesos abiertos”, normalmente quiere decir: “puedes obtener esos números y cargarlos en un runtime compatible”.

Pero los pesos no son el modelo completo en sentido práctico. Para usarlos bien necesitas, como mínimo:

PiezaPor qué importa
ArquitecturaDefine cómo se conectan los pesos: capas, atención, MoE, normalización, activaciones.
ConfiguraciónDefine dimensiones, número de capas, vocabulario, contexto, precisión y detalles de carga.
TokenizerConvierte texto en tokens. Si cambias tokenizer, cambias la entrada real.
Chat templateConvierte roles y mensajes en el formato que el modelo espera.
RuntimeEjecuta el modelo: Transformers, vLLM, SGLang, llama.cpp, Ollama, TensorRT-LLM, etc.
LicenciaDecide qué puedes hacer legalmente: usar, modificar, vender, redistribuir, servir.
EvalsTe dicen si ese modelo, con esa configuración, sirve para tu caso.

Por eso un modelo con pesos abiertos puede ser tremendamente útil y, aun así, no ser “Open Source AI” en sentido estricto. Puede que no puedas reproducir su entrenamiento, auditar todos sus datos, conocer sus filtros o verificar por qué aprendió ciertos sesgos. En ingeniería, eso cambia cómo lo documentas: no lo vendes como sistema transparente total, sino como artefacto ejecutable con más control operativo que una API cerrada.

Las cuatro categorías que conviene enseñar al alumno

CategoríaQué tienesQué no tienes necesariamenteEjemplos a 14 de junio de 2026Decisión de ingeniería
Modelo cerrado por APIEndpoint, documentación, precios, SLA o contrato, herramientas del proveedor.Pesos, datos, receta de entrenamiento, capacidad de servirlo tú.GPT-5.5/GPT-5.4 en OpenAI API, Claude en Anthropic API, Gemini en Gemini API.14Útil si necesitas capacidad alta, menor operación propia, herramientas integradas y contrato de servicio. Mide dependencia, coste, privacidad y plan de salida.
Pesos abiertos permisivosPesos descargables y licencia amplia, normalmente compatible con uso comercial.Datos completos, receta reproducible, garantías de seguridad o coste operativo bajo.gpt-oss-120b/20b bajo Apache 2.0; Qwen3.6 open-weight Apache 2.0; Mistral Large 3 bajo Apache 2.0; DeepSeek-R1 con licencia MIT.15Bueno para control, despliegue propio, fine-tuning, privacidad y coste a escala. Mide VRAM, throughput, equipo de operación y calidad en tu eval.
Pesos abiertos con licencia propiaPesos accesibles, documentación y permiso condicionado.Libertad tipo MIT/Apache. Puede haber restricciones de escala, uso, redistribución o política aceptable.Llama 4 bajo Llama 4 Community License; Gemma con open weights y términos propios de Google.16Técnicamente puede ser muy atractivo, pero legalmente no lo trates como “open source clásico”. Revisa licencia con el caso de uso concreto.
Open Source AI estrictoLibertades de uso, estudio, modificación y compartición, más forma preferida de modificación: datos/información de datos, código y parámetros.No siempre existe en modelos frontier modernos.La OSI define el criterio; muchos modelos llamados “abiertos” solo cumplen parte.Útil como vara conceptual y de auditoría. Pregunta siempre: ¿qué falta para reproducir, auditar y modificar de verdad?

La tabla ayuda, pero el cuerpo de la idea es este: cada nivel de apertura compra una libertad distinta y deja una deuda distinta. Una API cerrada compra velocidad de adopción, pero deja dependencia. Un open weight compra control, pero deja operación. Un open weight permisivo compra posibilidad de modificar y servir, pero no necesariamente transparencia científica. Un Open Source AI completo compraría auditabilidad profunda, pero todavía no es la norma en modelos frontier.

KPIs para decidir entre cerrado, pesos abiertos y open source

Una discusión madura no pregunta “¿abierto o cerrado?” como si fuera una preferencia moral. Pregunta qué KPI importa para el sistema. KPI aquí no significa poner números por ponerlos; significa elegir indicadores que cambian la decisión.

KPIQué mideCómo se calcula o se observaQué valor sería razonable
Calidad propiaSi resuelve tus casos, no el benchmark general.Eval con casos de tu dominio: exactitud, rúbrica, tasa de formato válido, revisión humana.Se fija por tarea: por ejemplo, >95 % JSON válido y >85 % acierto en casos críticos.
Coste por tarea completadaCoste real por respuesta útil, no por token aislado.Tokens de entrada + salida + cache + batch + repeticiones + fallos.El modelo barato que repite tres veces puede salir caro; mide coste por caso aprobado.
Latencia p95Tiempo que sufre el 5 % más lento de usuarios.Medición bajo carga con red, runtime, batch y contexto reales.Interactivo: quizá <2-5 s; batch: puede ser minutos si está justificado.
ThroughputCuántas peticiones o tokens procesas por segundo.Requests/s, tokens/s, concurrencia y cola.Importa mucho en open weights: una GPU infrautilizada destruye el TCO.
Coste de operaciónTrabajo humano y técnico de mantener el sistema.Horas de SRE/ML, actualizaciones, incidentes, monitorización, GPUs, parches.API cerrada suele bajar operación propia; self-hosting la sube.
Riesgo de licenciaProbabilidad de que el uso viole términos o bloquee producto.Revisión de licencia, restricciones de uso, redistribución, derivados, atribución.Si el caso es comercial o regulado, licencia dudosa es filtro duro, no penalización pequeña.
Control de datosQué ocurre con prompts, documentos, logs y salidas.Retención, región, entrenamiento con datos, cifrado, contratos, despliegue on-prem.Datos sensibles pueden empujar a self-hosting o contrato enterprise fuerte.
ReproducibilidadCapacidad de repetir la misma evaluación y volver a la misma versión.Versión exacta, commit, hash de pesos, configuración, seeds, prompt, dataset.En producción: modelo y configuración fijados; alias latest solo si aceptas cambio.
Capacidad de adaptaciónFacilidad para fine-tuning, LoRA, cuantización o routing.Acceso a pesos, licencia, tooling PEFT, soporte runtime.Open weights gana aquí si el equipo sabe medir degradación.
PortabilidadFacilidad de cambiar proveedor o mover despliegue.Compatibilidad OpenAI-like, vLLM/SGLang, formatos, prompts, schemas, evals.Cuanto más crítica la app, más importante mantener segundo candidato.
ObservabilidadCapacidad de ver fallos, costes y comportamiento.Logs, trazas, métricas, prompts, outputs, tokens, latencia, errores por slice.No uses un modelo que no puedes medir en el nivel que exige el riesgo.
GobernanzaCapacidad de auditar qué se usa, por qué y bajo qué condiciones.Model card interna, fecha, licencia, DPIA si aplica, evals, aprobaciones.En entornos profesionales, una decisión sin ficha interna es memoria frágil.

Estos KPIs no pesan igual siempre. Para un prototipo de clase, calidad y facilidad de ejecución pueden bastar. Para un asistente con expedientes privados, privacidad, región, trazas y licencia pesan mucho más. Para una herramienta de código interna, quizá throughput, coste por tarea y capacidad de adaptación sean los criterios principales.

Ejemplo de lectura. Si una organización compara GPT-5.5 por API, Claude por API, Gemini por API, gpt-oss-120b, Qwen3.6 y Mistral Large 3, no debería empezar por “cuál es más inteligente”. Debería construir una matriz:

  1. Filtros duros: privacidad, modalidad, licencia, región, presupuesto máximo.
  2. Eval propia: 100 casos reales con salida esperada o rúbrica.
  3. Medición operativa: p50/p95, tokens/s, coste por caso, tasa de retry.
  4. Riesgo: dependencia de proveedor, licencia, plan de salida, estabilidad de versión.
  5. Decisión: modelo principal, modelo alternativo, fecha de revisión y condiciones de cambio.

La conclusión puede ser híbrida. Por ejemplo: API cerrada para razonamiento difícil y multimodalidad; open weights para tareas repetitivas, datos sensibles o coste a escala; modelo pequeño local para clasificación barata; RAG para conocimiento vivo. Eso no es indecisión. Es arquitectura.

Qué significa “abierto” en una model card

Hugging Face permite declarar licencia en los metadatos de la model card y enlazar archivos LICENSE; también permite especificar datasets, pipeline_tag y resultados de evaluación estructurados.17 Eso está muy bien, pero hay que leerlo con cuidado.

Cuando veas una model card, separa estas preguntas:

PreguntaDónde mirarDecisión que cambia
¿Puedo descargar pesos?Files and versions, tamaño, formato, gated access.Self-hosting, fine-tuning, cuantización.
¿Qué licencia tiene?Metadata license, archivo LICENSE, términos externos.Uso comercial, redistribución, derivados.
¿Hay código de inferencia?README, ejemplos, config.json, runtime recomendado.Facilidad de ejecución y compatibilidad.
¿Hay código de entrenamiento?Paper, repo, scripts, argumentos, filtros.Reproducibilidad y auditoría profunda.
¿Hay datos o información de datos?Dataset card, README, paper, datasheets.Riesgo de sesgo, cumplimiento, trazabilidad.
¿Hay evals comparables?model-index, benchmark, paper, harness.Calidad relativa y gaps de evaluación.
¿Hay política de uso?Model card, terms, acceptable use policy.Riesgo de producto y cumplimiento interno.

Un ejemplo típico: un repo puede tener pesos Apache 2.0 y ser muy útil para producción, pero no publicar dataset completo ni receta reproducible. En el libro lo llamaríamos “pesos abiertos permisivos”, no “open source completo”. No es un desprecio; es precisión.

Cómo lo explicaría en una revisión técnica

Una frase mala sería: “usamos un modelo open source porque es gratis”.

Una frase profesional sería: “para esta tarea usamos un modelo con pesos abiertos bajo licencia Apache 2.0, servido con vLLM en infraestructura propia, porque necesitamos control de datos y coste estable a volumen. No afirmamos que sea Open Source AI completo: no tenemos todos los datos de entrenamiento ni la receta reproducible. Lo compensamos con eval propia, model card interna, revisión de licencia, medición de p95 y alternativa API si la calidad cae”.

Y otra frase igualmente profesional sería: “para esta tarea usamos un modelo cerrado por API porque la calidad multimodal y las herramientas integradas superan el coste operativo de servir pesos abiertos. Lo documentamos como dependencia de proveedor, fijamos versión cuando el proveedor lo permite, medimos coste por tarea completada y mantenemos una eval de regresión para migrar”.

La madurez no está en elegir siempre abierto o siempre cerrado. Está en poder explicar qué libertad compras, qué deuda aceptas y qué KPI vigila que la decisión siga siendo buena.

Qué no es una model card

Una model card no es marketing. Puede estar mejor o peor escrita, pero su función no es decir “este modelo es increíble”. Su función es permitir una decisión responsable: qué es, para qué se pensó, dónde se evaluó, qué límites declara, qué licencia tiene y qué condiciones debes revisar antes de usarlo.

Tampoco es una garantía. Que una ficha diga “razonamiento”, “multimodal”, “contexto largo” o “excelente en código” no te dice automáticamente si funcionará en tu flujo. Te da pistas para construir pruebas.

Y no es un benchmark suelto. Una tabla de MMLU, SWE-bench, HumanEval, MMMU o cualquier otra métrica puede ser útil, pero solo mide lo que mide. Si tu producto clasifica incidencias en castellano, resume expedientes internos o genera SQL contra tu esquema, el ranking general es una señal débil.

La trampa más común es leer una model card como si fuera un menú. En realidad hay que leerla como una ficha de compatibilidad: “¿encaja con mi problema, mis datos, mi latencia, mi presupuesto, mis permisos y mi forma de evaluar?”.

Qué sí es: una ficha para decidir sin autoengaño

Una buena model card responde a seis bloques de preguntas:

BloquePreguntas que debe responder
Identidad¿Qué modelo es, qué versión, qué familia, qué arquitectura y qué modalidad?
Uso previsto¿Para qué fue diseñado? ¿Qué usos desaconseja?
Entrada y salida¿Texto, imagen, audio, vídeo, tools, JSON, embeddings? ¿Qué límites tiene?
Evaluación¿Con qué benchmarks, datasets, idiomas y condiciones se midió?
Operación¿Contexto, coste, latencia, rate limits, runtime, hardware, versiones estables?
Condiciones¿Licencia, privacidad, retención de datos, restricciones y obligaciones de atribución?

La palabra “modelo” además puede esconder capas distintas:

NivelQué mirasEjemplo de pregunta
Modelo baseArquitectura y preentrenamiento.¿Es base, instruct, MoE, multimodal o de embeddings?
Modelo servido por APICapacidades y contrato del proveedor.¿Acepta documentos? ¿Devuelve JSON validable? ¿Tiene tools?
Modelo localPesos, formato, cuantización y runtime.¿GGUF, safetensors, vLLM, Ollama, TensorRT-LLM?
Sistema completoRAG, tools, memoria, permisos y evals.¿El fallo viene del modelo o del contexto que le damos?

Una elección madura empieza separando esos niveles. Cambiar de modelo no arregla una mala recuperación de documentos. Un modelo con contexto enorme no sustituye una política de permisos. Un benchmark alto no te exonera de evaluar tus casos.

El tamaño tampoco decide solo. Las leyes de escala ayudaron a entender cómo bajaba la pérdida al aumentar parámetros, datos y cómputo durante el entrenamiento.18 Después, el trabajo conocido como Chinchilla puso el foco en el equilibrio entre tamaño de modelo y cantidad de datos de entrenamiento.19 Para elegir en un producto, esa lección se traduce así: no preguntes solo “cuántos parámetros tiene”, pregunta si el modelo resuelve tu tarea con el coste, la latencia y la trazabilidad que puedes sostener.

La matriz mínima de decisión

Elegir con criterio exige convertir preferencias vagas en criterios comparables. No hace falta convertirlo todo en una hoja de cálculo infinita, pero sí conviene explicitar qué pesa más.

Ejemplo de fórmula. Una forma simple es puntuar cada modelo candidato con criterios normalizados:

S(m)=j=1nwjqj(m)k=1rλkpk(m)S(m)=\sum_{j=1}^{n} w_j \cdot q_j(m)-\sum_{k=1}^{r} \lambda_k \cdot p_k(m)
SímboloSignificadoEjemplo
S(m)S(m)Puntuación final del modelo mm.0,78 para el candidato A.
wjw_jPeso del criterio positivo jj.Calidad vale 0,35; coste vale 0,20.
qj(m)q_j(m)Valor normalizado del criterio para el modelo.Calidad propia 0,86; latencia 0,72.
λk\lambda_kPeso de una penalización.Penalizar licencia incompatible con 1,0.
pk(m)p_k(m)Penalización activada.1 si no cumple privacidad; 0 si cumple.

Esta fórmula no pretende aparentar precisión. Pretende obligarte a declarar tus prioridades. Si privacidad es obligatoria, no debe ser “un criterio más”: debe ser un filtro. Si latencia p95 tiene que ser menor de 2 segundos, el modelo que no lo cumple queda fuera aunque gane un benchmark.

El flujo práctico suele ser:

FaseQué hacesResultado
1. RequisitosDefines tarea, entrada, salida, usuarios, idioma, latencia y presupuesto.Lista de restricciones.
2. Filtros durosEliminas modelos que no cumplen licencia, modalidad, región, privacidad o contexto.Lista corta inicial.
3. Lectura de fichasRevisas model cards, docs de proveedor y versiones.Hipótesis de encaje.
4. Eval propiaPruebas casos representativos con salida esperada o rúbrica.Evidencia en tu tarea.
5. Coste y operaciónMides tokens, p50/p95, fallos, rate limits y mantenimiento.Coste total razonable.
6. DecisiónDocumentas modelo elegido, alternativa y fecha de revisión.Decisión trazable.

También conviene separar “calidad” de “fiabilidad”. Calidad es que responda bien cuando todo va bien. Fiabilidad es que falle de forma manejable cuando el caso es raro, falta contexto, la entrada está sucia o el formato de salida importa.

La ficha que yo leería antes de elegir

Cuando abras una model card o una página de modelos, no empieces por la frase grande. Empieza por esta lista:

DatoPregunta incómoda
Nombre exacto y versión¿Estoy usando una versión estable o un alias que puede cambiar?
Modalidades¿Texto solo, imagen, audio, vídeo, embeddings, tools?
Contexto¿Cuánto entra realmente y cuánto debo reservar para salida?
Salida máxima¿Puede devolver la respuesta completa o tengo que trocear?
Precio¿Entrada, salida, cache, batch, imágenes, audio, razonamiento?
Latencia esperada¿Me importa tiempo total o tiempo hasta primer token?
Datos y fecha de entrenamiento¿Hay conocimiento que no puede saber sin RAG?
Evaluación¿Los benchmarks se parecen a mi tarea?
Idiomas¿Se evaluó de verdad en castellano o solo se declara soporte?
Formato de chat¿Tiene plantilla de mensajes, system, tools, JSON, function calling?
Licencia¿Puedo usarlo en mi producto, modificarlo, redistribuirlo o servirlo?
Retención y privacidad¿Qué ocurre con los datos que envío?
Deprecación¿Hay fecha de retirada, migración o versión recomendada?

Google, por ejemplo, distingue modelos estables, preview, latest y experimentales en su documentación de modelos. Esa clasificación importa porque latest o preview puede ser útil para explorar, pero no siempre es lo que quieres fijar en producción. OpenAI y Anthropic mantienen páginas vivas de modelos y precios; por eso una decisión profesional debería guardar fecha de consulta y versión exacta, no solo “usamos el modelo bueno”.

Anatomía de una model card en Hugging Face

Hugging Face convierte una model card en una página viva: parte README, parte ficha técnica, parte repositorio de archivos y parte panel operativo. Por eso conviene leerla en capas. Primero miras la identidad del modelo. Después los metadatos. Después los archivos. Después cómo se ejecuta. Y solo al final miras los benchmarks.

La idea no es aprender un ritual de botones. Es saber qué pregunta de ingeniería hay detrás de cada etiqueta.

Zona de la páginaQué estás mirandoPregunta que debes hacer
owner/modelOrganización y nombre exacto del repositorio.¿Estoy mirando el modelo oficial, una copia, un fine-tune o una cuantización?
Tarea visibleEtiqueta como Text Generation, Image-Text-to-Text o Feature Extraction.¿La tarea coincide con mi caso o estoy forzando el modelo?
BibliotecaTransformers, Diffusers, Sentence Transformers, timm u otra.¿Con qué librería se espera cargar o servir?
FormatoSafetensors, GGUF, ONNX, PyTorch, TensorRT u otros.¿Es el formato que mi runtime puede abrir?
LicenciaMIT, Apache-2.0, llama, custom, research-only u otra.¿Puedo usarlo, modificarlo, servirlo o redistribuirlo en mi contexto?
TagsPalabras como conversational, fp8, eval results, long-context.¿Son metadatos útiles o solo señales que debo comprobar?
Downloads y likesPopularidad y uso reciente.¿Hay adopción o solo ruido? Nunca es una prueba de calidad.
Model treeRelación con fine-tunes, adapters y cuantizaciones.¿Estoy viendo el tronco principal o una rama derivada?
Files and versionsArchivos, commits, pesos, config, tokenizer, licencia e historial.¿Puedo auditar qué estoy descargando y cuándo cambió?
Inference ProvidersEmpresas que lo sirven desde la nube.¿La calidad y el coste vienen del modelo o del proveedor que lo sirve?
SpacesDemos o aplicaciones que usan el modelo.¿Es una demo útil o una evidencia técnica? Normalmente es lo primero.
Evaluation resultsResultados integrados desde model-index o evaluaciones enlazadas.¿Qué métrica, dataset, configuración y fuente produjo ese número?

La parte superior suele incluir metadatos que Hugging Face usa para buscar, filtrar y mostrar modelos. Algunos aparecen escritos en YAML dentro del README, otros se infieren desde archivos como config.json o desde la integración de la librería.

TérminoTraducción prácticaQué no debes asumir
pipeline_tagTarea principal del modelo. Decide filtros, widget y parte de la experiencia de inferencia.Que el modelo sea bueno en todas las tareas parecidas.
library_nameLibrería esperada para usarlo.Que otra librería lo cargue igual sin conversión.
licenseCondiciones de uso declaradas.Que todo lo derivado tenga automáticamente la misma licencia sin revisar.
languageIdiomas declarados o detectados.Que haya evaluación seria en todos esos idiomas.
datasetsDatasets de entrenamiento o evaluación que el autor declara.Que conozcas todo el corpus real de entrenamiento.
base_modelModelo del que parte un fine-tune, adapter o destilación.Que conserve exactamente las capacidades del modelo base.
new_versionRepositorio recomendado como versión posterior.Que puedas migrar sin repetir evals.
tagsSeñales de búsqueda: modalidad, precisión, dominio, familia, técnica.Que sean una especificación formal.
model-indexResultados de evaluación estructurados.Que el benchmark represente tu producto.
widgetEjemplo interactivo en la página.Que el prompt del widget sea tu contrato de producción.
extra_gated_fieldsCampos que el usuario acepta antes de acceder a un modelo restringido.Que aceptar la pantalla baste para resolver privacidad o permisos internos.

Ahora leamos un caso real. El 25 de mayo de 2026, la card de deepseek-ai/DeepSeek-V4-Pro aparece en Hugging Face como modelo de generación de texto, con etiquetas de Transformers, Safetensors, deepseek_v4, conversational, resultados de evaluación y precisión fp8; declara licencia MIT; y describe DeepSeek-V4-Pro como un modelo MoE de 1,6T parámetros totales, 49B parámetros activados y contexto de 1M tokens.20

Lo importante no es memorizar esos números. Lo importante es saber leerlos. Para no convertirlo en una pared, vamos término a término, con el ejemplo de DeepSeek-V4-Pro como caso de lectura. Cuando una ficha técnica menciona DeepSeek-V4, también conviene contrastar con la documentación de Transformers, porque ahí aparecen detalles de arquitectura como tipos de atención, max_position_embeddings y clases de carga.21

La regla de esta sección es: ningún término se queda en definición de diccionario. Para cada dato preguntamos qué mide, qué recurso toca, con qué se compara, qué sería razonable y qué prueba haría antes de creerlo.

Identidad, tarea y repositorio

Estos términos responden a una pregunta básica: “¿qué estoy mirando exactamente?”. Parece trivial, pero muchísimos errores empiezan por usar una variante distinta de la que se quería evaluar.

Término en la cardExplicación completaEjemplo con DeepSeek-V4-ProDecisión práctica
deepseek-ai/DeepSeek-V4-ProEs el identificador completo del repositorio: organización o usuario antes de /, nombre del modelo después. En Hugging Face no basta con decir “DeepSeek V4”; puede haber forks, quantizations, fine-tunes y mirrors.deepseek-ai indica la organización; DeepSeek-V4-Pro indica el repo concreto.Guarda este valor exacto en documentación, evals y configuración. Si pruebas Rooc/DeepSeek-V4-Pro, ya no estás probando el mismo repositorio.
NamespaceEs la parte izquierda del identificador. Puede ser una empresa, una comunidad, una persona o una organización académica.deepseek-ai no es lo mismo que nvidia, unsloth, mlx-community o una cuenta personal.Antes de descargar pesos, mira si el repo es oficial, derivado o una adaptación para otro formato.
RepositorioEs la carpeta pública del modelo: README, pesos, tokenizer, configuración, licencia, historial y discusiones.En un repo grande encontrarás README, archivos safetensors, configuración, tokenizer, licencia, scripts y carpetas auxiliares.Trata el repo como expediente técnico, no como una tarjeta de marketing.
Text GenerationEs la tarea principal declarada. Significa que el modelo genera texto token a token a partir de un contexto. No significa automáticamente “buen chat”, “buen agente” o “buen programador”.DeepSeek-V4-Pro se muestra como generación de texto.Si quieres embeddings, clasificación o visión, esta etiqueta por sí sola no basta. Busca el pipeline_tag correcto y evalúa tu tarea.
conversationalTag que sugiere uso conversacional. Normalmente indica que el modelo está pensado para turnos de usuario/asistente.Puede aparecer junto a Text Generation.Revisa la plantilla de chat. Un modelo conversacional mal formateado puede fallar por entrada, no por capacidad.
Eval ResultsSeñal de que Hugging Face puede mostrar resultados de evaluación asociados al modelo.La card puede enseñar métricas como GSM8K, SWE-bench, GPQA o benchmarks de contexto.Lee dataset, métrica, configuración y fuente. El número sin protocolo vale poco.

Ejemplo cercano: si en un proyecto interno dices “vamos a usar DeepSeek”, esa frase no basta. Una decisión trazable diría algo como: “probamos deepseek-ai/DeepSeek-V4-Pro, consultado el 25 de mayo de 2026, frente a una alternativa local cuantizada y una API comercial, con estos 80 casos de evaluación”.

Librería, formato y archivos

La siguiente capa responde a: “¿cómo se carga y qué estoy descargando?”. Aquí aparecen términos que parecen de infraestructura, pero deciden si el modelo se puede probar hoy o si necesitas una semana de entorno.

Término en la cardExplicación completaEjemplo con DeepSeek-V4-ProDecisión práctica
TransformersIndica integración con la librería de Hugging Face para tokenizers, configuración y modelos. No garantiza que tu portátil pueda cargarlo ni que tu versión instalada tenga soporte completo.La card enseña ejemplos con AutoTokenizer y AutoModelForCausalLM.Comprueba versión de transformers, memoria, trust_remote_code si aplica y soporte de arquitectura.
SafetensorsFormato de pesos basado en tensores, diseñado para ser simple, rápido y evitar dependencias de carga como pickle.22La zona lateral puede mostrar Safetensors.Bien para distribuir pesos; no implica que el modelo quepa en GPU ni que tu runtime soporte todas sus capas.
Files and versionsPestaña donde están los archivos reales y su historial. Es donde miras pesos, config.json, tokenizer, licencia, scripts y commits.Si ves una carpeta encoding, no la ignores: puede explicar cómo convertir mensajes en texto para el modelo.Para producción, fija commit o versión. No dependas solo del nombre del repo.
config.jsonArchivo que describe arquitectura, dimensiones, vocabulario, contexto, tipos de capas y parámetros de inferencia.Puede incluir max_position_embeddings o layer_types.Si un número de la card no cuadra, abre la configuración antes de asumir que la card está mal o bien.
TokenizerComponente que parte texto en tokens y reconstruye texto desde tokens.El mismo prompt puede convertirse en secuencias distintas según tokenizer.No cambies tokenizer entre evals salvo que quieras medir otra cosa.
Tensor typeTipos numéricos presentes en archivos: BF16, F32, FP8, enteros u otros.La zona lateral puede listar varios tipos a la vez.No leas “tensor type” como precisión única de inferencia. Puede mezclar pesos, escalas, índices y archivos auxiliares.
License: mitLicencia declarada para repo y pesos, según la card. MIT suele ser permisiva, pero debes leer el archivo de licencia.DeepSeek-V4-Pro declara MIT en la card.Comprueba LICENSE, condiciones internas de tu organización y si usas derivados con otra licencia.

Ejemplo cercano: si un compañero dice “está en Safetensors, lo cargamos fácil”, la respuesta de ingeniería es: “formato de archivo sí; ahora dime tamaño, precisión, runtime, memoria, tokenizer, plantilla y licencia”.

Tamaño, precisión, contexto y memoria

Esta parte responde a: “¿cuánto pesa operar esto?”. Aquí se confunden mucho los términos porque parecen números comparables, pero no todos miden lo mismo. Un número útil debe decirte tres cosas: qué recurso toca, contra qué lo comparas y qué decisión cambia.

La cuenta mínima que debe tener un ingeniero en la cabeza es esta:

MpesosNparametrosb8M_{\text{pesos}} \approx N_{\text{parametros}} \cdot \frac{b}{8}
SímboloQué significaEjemplo
MpesosM_{\text{pesos}}Memoria aproximada solo de pesos.No incluye KV cache, activaciones, runtime ni fragmentación.
NparametrosN_{\text{parametros}}Número de parámetros almacenados.7B, 70B, 1.6T.
bbBits por parámetro.F32 usa 32; BF16/F16 usa 16; FP8/I8 usa 8; FP4/I4 usa 4.

Regla de bolsillo: 1B parámetros ocupa unos 4 GB en F32, 2 GB en BF16/F16, 1 GB en FP8/INT8 y 0,5 GB en FP4/INT4, antes de sobrecostes. En producción añade memoria para KV cache, buffers, escalas de cuantización, runtime y margen de seguridad. Por eso “70B en 4-bit son 35 GB” es solo el principio de la conversación, no el dimensionamiento completo.

Comparación rápidaBF16/F16FP8/INT8FP4/INT4Lectura de ingeniería
Modelo denso 7B~14 GB~7 GB~3,5 GBEn local, 4-bit suele ser el punto de entrada; BF16 pide GPU más holgada.
Modelo denso 70B~140 GB~70 GB~35 GBNormalmente necesitas varias GPUs, servidor grande o cuantización fuerte.
MoE 1.6T con 49B activadosPesos totales enormesMenos memoria por pesoMenos memoria por pesoEl cómputo por token se parece más a los activados, pero tienes que almacenar y servir el total o repartirlo.

Lo “adecuado” no es universal. Para entrenamiento o referencia científica, BF16/F16 suele ser base razonable; F32 queda para partes sensibles, depuración o cálculos concretos. Para inferencia de producción en hardware moderno, FP8/INT8 puede ser un buen compromiso si el runtime lo soporta. Para local, demos y coste bajo, INT4/FP4/GGUF puede ser aceptable, pero solo después de eval propia. NVIDIA documenta el uso de BF16, FP8 y formatos más bajos como parte de entrenamiento e inferencia de baja precisión; Hugging Face y vLLM mantienen documentación específica para cuantización en inferencia.23

Término en la cardExplicación completaEjemplo con DeepSeek-V4-ProDecisión práctica
1.6T total paramsMide cuántos parámetros hay almacenados. Aporta una idea de memoria total, descarga, reparto entre GPUs y complejidad operativa. En MoE no equivale al cómputo por token.Si fueran 1,6T en BF16, solo pesos serían ~3,2 TB; con FP8/FP4 mixto baja, pero sigue siendo infraestructura seria.Compáralo con modelos densos 7B/70B. Si no tienes plan de serving distribuido o provider, este número ya te dice que no es “modelo local normal”.
49B activated paramsMide la parte aproximada que participa por token. Aporta una pista de coste de cómputo, no de memoria total.49B activados se parece más a servir un modelo grande denso por token, pero con pesos totales mucho mayores detrás.Úsalo para estimar latencia y throughput, pero no para decidir memoria de GPU. Memoria y cómputo son dos columnas distintas.
Model sizeEstimación que muestra el Hub. Aporta una lectura rápida, pero puede mezclar detección automática, ficheros publicados y metadatos.La UI puede resumir algo que el README detalla de otra manera.Si hay discrepancia, abre config.json, README y archivos. El valor adecuado es el que puedes reproducir desde el repo.
1M context lengthMide ventana máxima teórica. Aporta capacidad para meter muchos tokens, pero también presión sobre KV cache, latencia y calidad de recuperación.DeepSeek-V4-Pro declara contexto de 1M tokens.Adecuado solo si tu tarea necesita contexto largo y lo evalúas. Para muchos productos, RAG con buenos fragmentos gana a meter todo.
max_position_embeddingsMide posiciones máximas soportadas por la arquitectura/configuración. Aporta el límite estructural, no la promesa de experiencia barata.En DeepSeek-V4 la documentación de Transformers menciona 1048576, aproximadamente 1M posiciones.Compáralo con tu longitud real: si tus casos tienen 8K-32K tokens, 1M quizá no aporta nada salvo coste.
FP4 + FP8 MixedIndica precisión mixta: algunas partes usan 4 bits y otras 8 bits. Aporta reducción de memoria/ancho de banda manteniendo más precisión donde conviene.DeepSeek-V4-Pro declara expertos MoE en FP4 y la mayoría del resto en FP8.Adecuado cuando el modelo fue entrenado/publicado para ese formato y tu runtime lo soporta. No lo equipares a una cuantización casera hecha después.
BF16Flotante de 16 bits con rango amplio parecido a F32 y menos precisión fina. Aporta buena estabilidad con mitad de memoria que F32.7B en BF16 son ~14 GB solo en pesos; 70B son ~140 GB.Buen baseline de calidad para inferencia seria si tienes memoria. Si BF16 no cabe, cuantizas; si cabe, úsalo como referencia de comparación.
F32Flotante de 32 bits. Aporta máxima comodidad numérica, pero cuesta el doble que BF16 y cuatro veces más que FP8/INT8 en memoria.7B en F32 son ~28 GB solo pesos; 70B son ~280 GB.No suele ser adecuado para servir LLM grandes completos. Útil para partes sensibles, depuración, entrenamiento clásico o modelos pequeños.
FP8Flotante de 8 bits. Aporta ahorro de memoria y ancho de banda manteniendo comportamiento mejor que muchos enteros si está bien calibrado y soportado.70B en FP8 son ~70 GB de pesos, antes de sobrecostes.Adecuado en GPUs/runtimes modernos con soporte real. Evalúa porque FP8 no garantiza calidad: depende de escalas, kernels y arquitectura.
I8 / INT8Entero de 8 bits. Aporta compresión y aceleración si el runtime tiene kernels adecuados. Suele necesitar escalas para reconstruir valores.Un modelo de 70B en INT8 ronda ~70 GB de pesos más escalas y sobrecostes.Adecuado para inferencia cuando la degradación medida es pequeña. No asumas que todo INT8 conserva igual matemáticas, código o formato JSON.
FP4 / INT44 bits por peso. Aporta gran reducción de memoria, a costa de mayor riesgo de pérdida de calidad.Un 70B en 4-bit ronda ~35 GB de pesos más sobrecostes.Adecuado para local, coste bajo o prototipos cuando la eval propia lo confirma. Para extracción exacta, SQL, código o razonamiento duro, compara contra BF16/FP8.
Tensor typeLista tipos numéricos presentes en archivos. Aporta pistas, pero no dice por sí sola la precisión principal de inferencia.Puede aparecer BF16, F32, FP8, I64 o I8 porque hay pesos, escalas, índices y metadatos.No tomes el primer tipo como “el modelo corre en eso”. Pregunta: qué tensors son pesos, cuáles escalas, cuáles índices y qué usa el runtime.
CuantizaciónTécnica para representar pesos o activaciones con menos bits. Aporta reducción de memoria y coste, pero puede cambiar calidad, velocidad y compatibilidad.Puede aparecer como GGUF, GPTQ, AWQ, bitsandbytes, FP8, INT8, INT4 o repo derivado.Lo adecuado depende de tu restricción: BF16 para referencia, FP8/INT8 para producción eficiente, 4-bit para local/coste bajo si pasa evals.

Ejemplo cercano: si tienes un asistente que responde a documentos de 40 páginas, un contexto de 1M tokens quizá no es la primera solución. Puede ser mejor recuperar 10 fragmentos bien citados con RAG, como veremos en capítulos 09 y 10.

Arquitectura: MoE, atención y conexiones internas

Estos términos explican cómo está construido el modelo. No siempre necesitas dominarlos para usar una API, pero sí para entender por qué un modelo tiene ciertas necesidades de runtime.

Término en la cardExplicación completaEjemplo con DeepSeek-V4-ProDecisión práctica
MoEMixture of Experts. El modelo tiene varios expertos y un mecanismo de enrutamiento decide cuáles se usan para cada token.DeepSeek-V4-Pro es MoE: total enorme, activación parcial.Necesitas servir expertos, routing y paralelismo con cuidado. Un runtime pobre puede arruinar la ventaja.
ExpertosSubredes especializadas dentro de un MoE. No son “personas”; son bloques de parámetros.Un token puede activar ciertos expertos y no otros.En inferencia distribuida importa dónde vive cada experto y cuánto tráfico genera.
RoutingDecisión interna de qué expertos se activan.Cada token se enruta a una parte del modelo.Puede afectar latencia, balanceo de carga y reproducibilidad de rendimiento.
CSACompressed Sparse Attention. Atención comprimida y dispersa para manejar contexto largo de forma más eficiente.DeepSeek-V4 menciona CSA como parte de su atención híbrida.No basta con leer “1M tokens”; mide si recupera bien información lejana en tu tarea.
HCAHeavily Compressed Attention. Otra rama de atención comprimida orientada a señales de largo alcance.La documentación describe capas HCA y CSA intercaladas.Útil para contexto largo, pero exige pruebas de latencia, memoria y calidad.
mHCManifold-Constrained Hyper-Connections. Conexiones internas que sustituyen o refuerzan conexiones residuales tradicionales.Aparece como cambio arquitectónico de DeepSeek-V4.Interesa para entender estabilidad y diseño, pero no decide por sí solo si te sirve.
Sliding attentionAtención en ventana local. Mira solo un tramo cercano del contexto.En algunos bloques se usa una ventana local.Buena para eficiencia local; no sustituye por sí sola recuperación global.
KV cacheMemoria de claves y valores de atención durante generación. Ya la vimos en capítulo 03.Contexto largo puede disparar KV cache si no hay compresión.Para producto, KV cache es coste real: GPU, batch, latencia y throughput.

Ejemplo cercano: imagina una biblioteca. Un modelo denso abre todas las salas para cada consulta. Un MoE intenta abrir solo algunas salas especializadas. Eso ahorra trabajo por consulta, pero obliga a tener un edificio enorme disponible y un sistema de pasillos muy bien organizado.

Entrenamiento, ajuste y modos de razonamiento

Esta capa responde a: “¿qué se hizo para que el modelo se comporte así?”. Son términos de entrenamiento, no botones de producto.

Término en la cardExplicación completaEjemplo con DeepSeek-V4-ProDecisión práctica
PretrainingEntrenamiento base sobre grandes cantidades de texto, código u otros datos. Aprende patrones generales.La familia V4 declara preentrenamiento a gran escala antes del post-training.No esperes que pretraining conozca tus documentos privados. Para eso entra RAG o fine-tuning.
Post-trainingFase posterior para ajustar instrucciones, formato, razonamiento, preferencias o herramientas.La card habla de un pipeline posterior al pretraining.Afecta cómo responde, no solo cuánto sabe. Evalúa estilo, obediencia de formato y consistencia.
SFTSupervised fine-tuning: ajuste con pares entrada-salida. Enseña formato y comportamiento deseado.“Si el usuario pregunta X, responde con Y” en muchos ejemplos.Muy útil para tono y patrón de respuesta; no es buena vía para información que cambia cada día.
RLReinforcement learning. El modelo mejora usando señales de recompensa sobre sus respuestas.Se usa en modelos de razonamiento para reforzar soluciones mejores.Puede mejorar resolución, pero debes medir longitud, coste, formato y estabilidad.
GRPOGroup Relative Policy Optimization, variante usada en trabajos de DeepSeekMath para mejorar razonamiento con menor coste de memoria que PPO.24DeepSeek-V4 menciona RL con GRPO en su post-training.Interpreta “GRPO” como pista de entrenamiento, no como garantía de que tu problema saldrá bien.
on-policy distillationDestilación usando salidas generadas bajo la propia política del sistema durante el proceso de mejora.La card lo describe como consolidación de capacidades.Es relevante para entender la receta; tu aplicación sigue necesitando eval propia.
Muon optimizerOptimizador usado durante entrenamiento para estabilidad o convergencia.DeepSeek-V4 lo menciona como parte de sus mejoras.No cambia tu llamada API. Sirve para leer el informe técnico, no para configurar un chatbot.
Non-thinkModo de respuesta más directa, sin gran presupuesto de razonamiento.Útil para tareas rutinarias.Si la tarea es simple, evita pagar latencia extra por razonamiento largo.
ThinkModo con más análisis interno y respuesta más cuidadosa.Útil para planificación, código o problemas con varias restricciones.Mide coste y tiempo. No lo actives por defecto en todo.
Think MaxModo de esfuerzo máximo.Pensado para problemas difíciles o evaluación de frontera.Reserva para casos donde el coste adicional compense.

Ejemplo cercano: para clasificar tickets de soporte, Non-think puede bastar. Para revisar una migración de base de datos, quizá Think tenga sentido. Para explorar una demostración matemática o un problema de programación complejo, Think Max puede ser una prueba, no necesariamente el modo de producción.

Plantilla de chat, encoding y parámetros de generación

Aquí aparece una de las causas más tontas y más caras de errores: usar bien el modelo, pero formatear mal la entrada. Hugging Face documenta las chat templates como la forma de convertir mensajes en el formato exacto que el modelo vio durante entrenamiento.25

Término en la cardExplicación completaEjemplo con DeepSeek-V4-ProDecisión práctica
Chat TemplatePlantilla que convierte mensajes system, user y assistant en texto/tokens con separadores especiales.Algunas familias usan Jinja; DeepSeek-V4-Pro indica una carpeta encoding en lugar de una plantilla Jinja clásica.Si ignoras la plantilla, puedes convertir un buen modelo en un mal modelo.
encoding folderCarpeta con scripts para codificar mensajes y parsear salidas.Puede incluir funciones tipo “mensajes a string” y “texto generado a respuesta”.Señal de contrato de entrada. No improvises el prompt final sin leerla.
RolesEstructura de conversación: sistema, usuario, asistente y a veces herramientas.Un mensaje de sistema puede fijar comportamiento; uno de usuario contiene la tarea.Mantén roles consistentes entre eval y producción.
temperatureControla aleatoriedad de muestreo. Valores bajos suelen ser más conservadores; altos, más variados.La card puede recomendar temperature = 1.0.Para extracción JSON o clasificación, baja variación. Para ideación, quizá más variación.
top_pMuestreo por núcleo: limita candidatos a una masa de probabilidad acumulada.La card puede recomendar top_p = 1.0.No cambies temperature y top_p a ciegas; registra configuración en tus evals.
max_new_tokensMáximo de tokens que puede generar la respuesta.Si pides resumen largo con límite bajo, cortará la salida.Reserva salida suficiente. Contexto total no es solo entrada.
Stop tokensSecuencias que detienen generación.Un token especial puede cerrar un bloque de razonamiento o respuesta.Útiles para formato; peligrosos si cortan respuestas válidas.

Ejemplo cercano: dos equipos pueden “usar el mismo modelo” y obtener resultados distintos si uno aplica bien la plantilla de chat y otro manda un string plano. La diferencia no está en inteligencia; está en protocolo.

Runtimes, proveedores y despliegue

Esta parte responde a: “¿dónde corre y con qué contrato?”. El modelo no vive en el aire: necesita runtime, hardware, límites, observabilidad y presupuesto. vLLM y SGLang, por ejemplo, exponen servidores compatibles con APIs tipo OpenAI para servir modelos grandes.26

Término en la cardExplicación completaEjemplo con DeepSeek-V4-ProDecisión práctica
vLLMRuntime de inferencia orientado a throughput, batch, KV cache y API compatible con OpenAI.La card puede enseñar cómo servir el modelo con vllm serve.Mide p50, p95, tokens por segundo, memoria y límites de contexto.
SGLangFramework de serving para modelos de lenguaje y multimodales, con foco en baja latencia, throughput, cache y paralelismo.La card puede dar comandos sglang.launch_server.Útil si necesitas exprimir serving, paralelismo o flujos estructurados.
Docker Model RunnerForma de ejecutar modelos desde Docker usando comandos familiares y un endpoint gestionado por Model Runner.27La card puede mostrar docker model run hf.co/....Muy cómodo para probar; en producción revisa memoria, logs, versiones y despliegue real.
Inference ProvidersProveedores integrados en Hugging Face que sirven modelos alojados.28Puede aparecer Novita u otros proveedores para un modelo.Compara proveedor concreto, región, precio, latencia, privacidad y versión servida.
OpenAI-compatible APIAPI que imita contratos de OpenAI, normalmente /v1/chat/completions u otros endpoints parecidos.vLLM y SGLang pueden exponer endpoints compatibles.Compatible no significa idéntico: revisa streaming, tools, JSON, errores y parámetros soportados.
ThroughputCantidad de tokens o peticiones procesadas por unidad de tiempo.Un modelo puede ser bueno para batch y peor para chat interactivo.Si tienes muchos usuarios, throughput importa tanto como calidad.
Latencia p95Tiempo por debajo del cual acaba el 95 por ciento de peticiones.Una demo rápida puede esconder colas lentas.Decide con p95, no solo con “a mí me respondió rápido”.
BatchAgrupar peticiones para aprovechar hardware.Muy útil en procesos nocturnos o clasificación masiva.Puede mejorar coste, pero empeorar tiempo de espera interactivo.
ObservabilidadLogs, métricas, trazas, errores, coste y calidad en producción.Necesitas saber cuándo falla, cuánto cuesta y qué versión respondió.Sin observabilidad, elegir modelo es solo el inicio de una caja negra.

Ejemplo cercano: “lo sirve un provider” no responde a “¿me sirve a mí?”. Si tu aplicación trata documentos internos, necesitas saber retención, región, contrato, trazas, límites y cómo se comporta el proveedor cuando hay picos.

Evaluación y resultados

Las cards enseñan números porque necesitamos señales, pero cada número tiene una historia. Un benchmark sin protocolo es como una nota sin examen.

Término en la cardExplicación completaEjemplo con DeepSeek-V4-ProDecisión práctica
Evaluation ResultsBloque de resultados visibles en el Hub o enlazados desde la ficha.Puede mostrar GSM8K, SWE-bench, GPQA, Terminal Bench u otros.Úsalo para orientar, nunca para decidir sin eval propia.
BenchmarkPrueba estandarizada sobre un conjunto de tareas.HumanEval mide programación; MMLU conocimiento general; GPQA razonamiento científico.Pregunta si se parece a tu caso real.
DatasetConjunto de datos usado para medir.GSM8K contiene problemas matemáticos de primaria/secundaria.Si tu tarea es legal, médica o administrativa, GSM8K no te la resuelve.
MetricRegla que convierte respuestas en puntuación.Pass@1, EM, ACC, F1.La métrica define qué cuenta como acierto. Léela antes de comparar.
ShotsNúmero de ejemplos que se dan en el prompt.0-shot, 3-shot, 5-shot, 25-shot.Más ejemplos consumen tokens y pueden cambiar mucho el resultado.
HarnessCódigo y protocolo que ejecutan la evaluación.Puede controlar prompts, herramientas, timeouts y validación.Dos resultados en el mismo benchmark pueden no ser comparables si cambia el harness.
tools enabledLa evaluación permite usar herramientas externas, como terminal, navegador, ejecución de código o buscador.Algunos benchmarks de agentes lo indican explícitamente.No compares contra resultados sin herramientas como si fueran la misma prueba.
SourceOrigen del resultado: autor del modelo, leaderboard externo, paper o tercero.Hugging Face puede enlazar fuente de evaluación.Prefiere resultados reproducibles o, como mínimo, con protocolo claro.

Ejemplo cercano: si un modelo saca muy buena nota en SWE-bench, eso no significa que clasifique bien incidencias de alumnos. Significa que merece pasar a tu lista corta para tareas de código, si tus restricciones de coste y privacidad encajan.

Hay un detalle muy de ingenieros: las cards grandes a veces muestran números que no parecen encajar a primera vista. En DeepSeek-V4-Pro puedes ver una tabla del README con parámetros totales, activados, contexto y precisión, y también una zona lateral generada por Hugging Face con “model size” y “tensor type”. Si algo no cuadra, no lo ignores: abre Files and versions, busca config.json, README, LICENSE, tokenizer, scripts de encoding y notas del repositorio. La card no es un oráculo; es la entrada al expediente.

Las métricas también tienen vocabulario propio. Conviene leerlas como leerías un contrato: una palabra pequeña cambia lo que realmente se está midiendo.

MétricaQué mide aproximadamenteEjemplo sencilloCuidado
EMExact match: la respuesta generada debe coincidir exactamente con la esperada.Esperado: Madrid. Generado: Madrid. Acierta. Generado: La respuesta es Madrid. Puede fallar si el evaluador es estricto.Penaliza respuestas correctas con formato distinto. Es buena para respuestas cerradas, mala para explicaciones.
F1Solapamiento entre partes de la respuesta esperada y generada. Se usa mucho cuando hay varias palabras relevantes.Esperado: revisar matrícula y pago. Generado: revisar el pago de matrícula. Tiene bastante solapamiento.Puede dar buena nota aunque falte un detalle crítico. En tareas sensibles, mira también errores cualitativos.
Pass@1Si la primera solución generada pasa la prueba. Es común en código y problemas con verificador automático.El modelo escribe una función; los tests se ejecutan una vez; si pasan, cuenta como acierto.Depende muchísimo del harness, tests, timeout y formato esperado. No es “calidad general”.
ACCAccuracy: porcentaje de aciertos sobre el total.Clasifica 100 tickets; acierta 87; ACC = 0.87.No dice qué clase falla. Si la clase rara es la importante, accuracy puede engañar.
MMRAcrónimo que debes definir en el benchmark concreto. En recuperación suele ser Maximal Marginal Relevance; en algunas tablas puede aparecer con otro significado operacional.En búsqueda semántica puede premiar resultados relevantes pero no repetidos.Nunca asumas el significado por las siglas. Abre la ficha del benchmark.
EloPuntuación relativa por comparaciones entre modelos. Suele venir de duelos: respuesta A frente a respuesta B.Un evaluador humano o automático prefiere una respuesta; el ranking se actualiza.Depende de participantes, prompts, evaluador y protocolo. No es una unidad absoluta de inteligencia.
0-shotEl modelo responde sin ejemplos dentro del prompt.“Clasifica este ticket” sin mostrar tickets anteriores resueltos.Si tu producto sí usa ejemplos, este resultado quizá subestima tu caso.
few-shotEl prompt incluye algunos ejemplos antes de la tarea.Das 3 tickets con categoría correcta y luego pides clasificar uno nuevo.Puede mejorar mucho, pero consume contexto y puede sobreajustarse al formato de los ejemplos.
tools enabledLa evaluación permite usar herramientas externas: ejecución de código, terminal, buscador, base de datos o navegación controlada.Para resolver un bug, el sistema puede ejecutar tests en vez de responder solo de memoria.No compares con un resultado sin herramientas. Es otro sistema, no solo otro modelo.

Mi lectura práctica de una card de Hugging Face siempre termina con siete preguntas:

PreguntaDónde buscarla
¿Qué modelo exacto es?Nombre del repo, organización, commits y versiones.
¿Qué tarea dice resolver?pipeline_tag, tags, README y ejemplos.
¿Cómo se ejecuta bien?Use this model, runtime, chat template, tokenizer y scripts.
¿Qué coste operativo tendrá?Tamaño, precisión, contexto, parámetros activados, runtime y proveedores.
¿Qué evidencia trae?model-index, tablas de evaluación, paper y fuente del benchmark.
¿Qué condiciones tiene?Licencia, privacidad del proveedor, gating y restricciones internas.
¿Qué no me está diciendo?Datos de entrenamiento, idiomas evaluados, fallos conocidos, prompts exactos y límites reales.

Si una card no responde a varias de estas preguntas, no significa que el modelo sea malo. Significa que tu decisión tiene más incertidumbre. Y la incertidumbre se compensa con pruebas propias, límites claros y una alternativa preparada.

Benchmarks: útiles, pero no soberanos

Los benchmarks son necesarios porque evitan discutir solo con impresiones. Pero no todos los benchmarks sirven para todas las decisiones. HELM nació precisamente para evaluar modelos de lenguaje de forma más holística: no solo exactitud, también escenarios, métricas y transparencia de resultados. Esa idea es más importante que cualquier posición concreta en una tabla.

Tres preguntas ayudan:

PreguntaPor qué importa
¿Qué tarea mide?Matemáticas, código, lectura, conversación, visión, SQL o seguridad no son lo mismo.
¿Cómo se evaluó?Prompt, few-shot, temperatura, herramientas, idioma y versión pueden cambiar resultados.
¿Qué coste tuvo acertar?Un modelo puede ganar usando más tokens, más tiempo o más cómputo de inferencia.

El capítulo anterior nos da el antídoto: mide tokens, coste y latencia además de calidad. Una respuesta que mejora un 2 % en exactitud pero triplica coste y p95 quizá no es mejor para tu producto.

Y hay otra capa: benchmark de modelo no es benchmark de sistema. MLPerf Inference, por ejemplo, mide sistemas completos bajo escenarios definidos. En aplicaciones con IA, el sistema incluye modelo, runtime, hardware, batch, cache, red, RAG, herramientas, validadores y observabilidad. Si solo miras el modelo, te faltan piezas.

Coste total: no solo precio por millón de tokens

El precio público es una parte, no toda la decisión. En una API pagas tokens, modalidades, cache, batch o prioridad según proveedor. En local pagas GPUs, electricidad, memoria, mantenimiento, actualización y tiempo del equipo. En ambos casos pagas también integración, evaluación y cambios de versión.

Ejemplo de fórmula. Una estimación útil es:

TCO=Ctokens+Cinfra+Coperacion+CcambioTCO = C_{\text{tokens}} + C_{\text{infra}} + C_{\text{operacion}} + C_{\text{cambio}}
SímboloSignificadoEjemplo
TCOTCOCoste total de propiedad.Coste mensual real de servir una función.
CtokensC_{\text{tokens}}Coste de entrada, salida, cache y batch.Factura del proveedor.
CinfraC_{\text{infra}}Infraestructura propia o gestionada.GPU, CPU, memoria, almacenamiento, red.
CoperacionC_{\text{operacion}}Monitorización, fallos, evals y soporte.Tiempo del equipo y alertas.
CcambioC_{\text{cambio}}Migración entre versiones o proveedores.Adaptar prompts, schemas y evals.

Este coste cambia según el patrón de uso. Un asistente interactivo necesita p95 bajo. Un proceso nocturno puede aceptar batch. Una tarea con normativa fija puede aprovechar caché. Una tarea con documentos privados puede empujar hacia local o hacia una API con garantías contractuales concretas.

Para entenderlo: tres elecciones distintas

Pensemos en situaciones concretas:

CasoModelo tentadorDecisión más sensata
Chat de orientación universitariaEl modelo más capaz disponible.Modelo fiable, barato, con RAG, citas y buena evaluación en castellano.
Clasificador de tickets internosUn LLM grande generalista.Modelo menor con salida estructurada, eval propia y batch si no es interactivo.
Análisis de contratos extensosEl modelo con más contexto.Contexto largo si aporta valor; si no, RAG con citas y control de fragmentos.
Generación de SQLModelo de código muy alto en benchmark.Eval con tu esquema, permisos, consultas esperadas y validación antes de ejecutar.
Asistente local para datos sensiblesAPI más cómoda.Revisar privacidad, modelo local, cuantización y coste operativo real.

La pregunta no es “¿cuál es mejor?”. La pregunta es “¿qué falla si me equivoco?”. Si el fallo cuesta poco, puedes experimentar. Si el fallo rompe una decisión importante, necesitas más evaluación, trazas y límites.

Mapa visual de la decisión

Elegir modelos a partir de fichas, restricciones y evaluación propia Elegir modelo es filtrar, medir y documentar Una model card no decide por ti; te da preguntas para construir una decisión trazable. Requisitos tarea · idioma latencia · privacidad salida esperada Filtros duros licencia · región modalidad · contexto versión estable Model card uso previsto datos · límites benchmarks Lista corta 2-4 candidatos con versión exacta y fecha de lectura Eval propia casos reales rúbrica · salida esperada fallos observados Coste y operación tokens · p95 cache · batch · rate limits migración Decisión trazable modelo elegido alternativa fecha de revisión Regla final Un modelo queda elegido cuando cumple restricciones y gana en tus evals, no cuando gana una tabla general. IA para gente curiosa / Facsímil 04 / Capítulo 04 / 686f6c61

En el día a día

En un proyecto real, este capítulo aparece cuando alguien dice: “probemos con el modelo más potente”. A veces tiene sentido. Muchas otras veces la decisión correcta es un modelo más barato, una salida estructurada, RAG mejor hecho, caché, batch o una evaluación más honesta.

El trabajo profesional no es enamorarse de una familia de modelos. Es mantener una lista corta con versión exacta, fecha de consulta, coste estimado, eval propia y plan de salida si el proveedor cambia una versión o retira un endpoint.

Un equipo maduro guarda, junto al prompt y el código, una pequeña ficha interna: modelo elegido, alternativas descartadas, motivo, dataset de evaluación, resultados, costes, límites y próxima revisión. Esa ficha evita discusiones circulares cuando tres meses después alguien pregunta por qué no usamos “el nuevo”.

Por qué debería importarte

Porque la elección de modelo decide coste, experiencia de usuario, privacidad, mantenimiento y calidad. Si eliges solo por capacidad máxima, puedes construir un sistema que funciona en demo y duele en producción. Si eliges solo por precio, puedes ahorrar justo en la parte que sostenía la calidad.

También importa para aprender. Leer model cards te entrena a pensar como ingeniero: cada número pide una pregunta, cada benchmark pide contexto y cada promesa pide verificación.

Dónde volverá a aparecer

Este capítulo conecta la caja de herramientas con casi todo lo que viene después:

ConceptoDónde vuelvePara qué
Modelos localesCapítulo 05.Leer pesos, formato, cuantización, memoria y runtime.
Cloud frente a localCapítulo 06.Convertir elección de modelo en decisión de arquitectura.
EmbeddingsCapítulo 07.Elegir modelos de representación, no solo generativos.
RAGCapítulos 09 y 10.Decidir cuándo contexto externo pesa más que modelo mayor.
EvalsFacsímil 7.Convertir criterios en pruebas reproducibles.
OperaciónFacsímil 6.Medir p95, coste, fallos y cambios de versión.

Dónde solía tropezar yo

Estos errores aparecen mucho cuando la conversación se queda en nombres de modelos.

ErrorPor qué es un errorAntídoto
Elegir por ranking generalUn benchmark amplio no mide tu flujo, tus datos ni tu coste.Crear una eval propia pequeña antes de decidir.
No fijar versión exactaUn alias puede cambiar y romper comparabilidad.Guardar modelo, fecha, proveedor y configuración.
Comparar precio sin salidaUn modelo barato puede generar más tokens o fallar más.Medir coste por tarea completada, no solo por millón de tokens.
Confundir contexto largo con calidadMás ventana puede añadir ruido y latencia.Medir qué fragmentos son realmente necesarios.
Olvidar licencia o privacidadEl modelo puede funcionar técnicamente y no encajar legalmente.Revisar condiciones antes de hacer pruebas profundas.
No tener alternativaCuando cambia una versión, el producto queda atado.Mantener segundo candidato y eval de regresión.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a construir una matriz de decisión mínima. Usaremos datos inventados para evitar convertir el capítulo en una tabla de precios que caduca. Lo importante es la mecánica: filtros duros, criterios ponderados, penalizaciones y explicación de la decisión.

modelos = [
    {
        "nombre": "closed_api_frontier",
        "calidad": 0.94,
        "latencia": 0.62,
        "coste": 0.40,
        "contexto": 0.95,
        "control_datos": 0.55,
        "reproducibilidad": 0.50,
        "apertura": 0.10,
        "encaje_operativo": 0.90,
        "privacidad": True,
        "json": True,
        "licencia": True,
    },
    {
        "nombre": "closed_api_mini",
        "calidad": 0.84,
        "latencia": 0.86,
        "coste": 0.78,
        "contexto": 0.70,
        "control_datos": 0.55,
        "reproducibilidad": 0.55,
        "apertura": 0.10,
        "encaje_operativo": 0.95,
        "privacidad": True,
        "json": True,
        "licencia": True,
    },
    {
        "nombre": "open_weight_permissive",
        "calidad": 0.80,
        "latencia": 0.70,
        "coste": 0.82,
        "contexto": 0.68,
        "control_datos": 0.90,
        "reproducibilidad": 0.82,
        "apertura": 0.85,
        "encaje_operativo": 0.55,
        "privacidad": True,
        "json": True,
        "licencia": True,
    },
    {
        "nombre": "open_weight_license_propia",
        "calidad": 0.86,
        "latencia": 0.67,
        "coste": 0.75,
        "contexto": 0.82,
        "control_datos": 0.85,
        "reproducibilidad": 0.72,
        "apertura": 0.55,
        "encaje_operativo": 0.55,
        "privacidad": True,
        "json": True,
        "licencia": False,
    },
    {
        "nombre": "open_weight_quantized_no_json",
        "calidad": 0.72,
        "latencia": 0.80,
        "coste": 0.92,
        "contexto": 0.55,
        "control_datos": 0.95,
        "reproducibilidad": 0.75,
        "apertura": 0.75,
        "encaje_operativo": 0.62,
        "privacidad": True,
        "json": False,
        "licencia": True,
    },
]

filtros_duros = {
    "privacidad": True,
    "json": True,
    "licencia": True,
}

pesos = {
    "calidad": 0.22,
    "latencia": 0.12,
    "coste": 0.14,
    "contexto": 0.08,
    "control_datos": 0.16,
    "reproducibilidad": 0.12,
    "apertura": 0.10,
    "encaje_operativo": 0.06,
}

def cumple_filtros(modelo):
    return all(modelo[campo] == esperado for campo, esperado in filtros_duros.items())

def puntuacion(modelo):
    return sum(modelo[criterio] * peso for criterio, peso in pesos.items())

candidatos = [m for m in modelos if cumple_filtros(m)]
ordenados = sorted(candidatos, key=puntuacion, reverse=True)

for modelo in ordenados:
    print(modelo["nombre"], round(puntuacion(modelo), 3))

ganador = ordenados[0]
print("decision:", ganador["nombre"])

descartados = [m["nombre"] for m in modelos if not cumple_filtros(m)]
print("descartados_por_filtro:", descartados)

Salida esperada:

open_weight_permissive 0.79
closed_api_mini 0.674
closed_api_frontier 0.625
decision: open_weight_permissive
descartados_por_filtro: ['open_weight_license_propia', 'open_weight_quantized_no_json']

Ahora cambia el peso de calidad a 0.60 y reduce control_datos o apertura. Verás que puede ganar una API cerrada. Ese es el punto: la matriz no “descubre la verdad”; revela tus prioridades. Si cambias prioridades, cambia la decisión. Lo honesto es dejarlo escrito.

Cómo encaja todo

Este mapa conecta la elección de modelos con lo que ya vimos y con lo que viene en el facsímil.

graph TD
    subgraph "Capítulo 4: Model cards y elección"
        CARD["Model card"]
        HF["Card real en Hugging Face"]
        OPENNESS["Apertura real:<br/>API, pesos, código, datos"]
        KPIS["KPIs de selección"]
        FILTERS["Filtros duros"]
        MATRIX["Matriz de decisión"]
        EVAL["Eval propia"]
        COST["Coste total"]
        VERSION["Versión exacta"]
        DECISION["Decisión trazable"]
    end
    subgraph "Viene de capítulos anteriores"
        INTERV["Intervención correcta<br/>(F4C1)"]
        API["Contrato API (F4C2)"]
        TOKENS["Tokens y caché (F4C3)"]
        ARCH["Arquitecturas (F3)"]
    end
    subgraph "Continuidad"
        LOCAL["Modelos locales (F4C5)"]
        CLOUD["Cloud frente a local<br/>(F4C6)"]
        RAG["RAG y embeddings<br/>(F4C7-10)"]
        EVALS["Evals formales (F7)"]
        OPS["Operación (F6)"]
    end

    INTERV --> FILTERS
    API --> CARD
    TOKENS --> COST
    ARCH --> CARD
    CARD --> HF
    HF --> OPENNESS
    OPENNESS --> KPIS
    KPIS --> FILTERS
    HF --> FILTERS
    HF --> EVAL
    FILTERS --> MATRIX
    MATRIX --> EVAL
    EVAL --> DECISION
    COST --> DECISION
    VERSION --> DECISION
    DECISION --> LOCAL
    DECISION --> CLOUD
    DECISION --> RAG
    EVAL --> EVALS
    COST --> OPS

    style CARD fill:#F5F5F5,stroke:#000000,stroke-width:2
    style HF fill:#F5F5F5,stroke:#000000,stroke-width:2
    style OPENNESS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style KPIS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style FILTERS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MATRIX fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EVAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style COST fill:#F5F5F5,stroke:#000000,stroke-width:2
    style VERSION fill:#F5F5F5,stroke:#000000,stroke-width:2
    style DECISION fill:#F5F5F5,stroke:#000000,stroke-width:2
    style INTERV stroke-dasharray: 5 5
    style API stroke-dasharray: 5 5
    style TOKENS stroke-dasharray: 5 5
    style ARCH stroke-dasharray: 5 5
    style LOCAL stroke-dasharray: 5 5
    style CLOUD stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style EVALS stroke-dasharray: 5 5
    style OPS stroke-dasharray: 5 5

Vocabulario aprendido

Estos términos convierten “me gusta este modelo” en una conversación técnica.

TérminoDefinición
Model cardFicha técnica que documenta uso previsto, datos, evaluación, límites y condiciones.
System cardDocumento que describe un sistema completo, no solo el modelo aislado.
BenchmarkPrueba estandarizada bajo una metodología concreta.
Eval propiaConjunto de casos representativos de tu proyecto.
Latencia p95Tiempo que cubre el 95 por ciento de peticiones.
TCOCoste total de propiedad: tokens, infraestructura, operación y cambios.
Modelo estableVersión concreta pensada para producción.
Modelo previewVersión de avance rápido que puede cambiar antes.
Matriz de decisiónComparación ponderada de modelos según criterios explícitos.
pipeline_tagTarea principal declarada en Hugging Face.
SafetensorsFormato de pesos basado en tensores.
Parámetros activadosParte del modelo MoE usada para cada token.
Chat templatePlantilla que convierte mensajes en tokens.
model-indexMetadatos de evaluación que Hugging Face puede mostrar de forma estructurada.
CuantizaciónUso de menos bits para reducir memoria y coste operativo.
NamespaceOrganización o usuario propietario del repositorio en Hugging Face.
TokenizerComponente que convierte texto en tokens y tokens en texto.
Tensor typeTipo numérico de los tensores publicados o auxiliares.
BF16Formato de 16 bits usado como baseline de buena calidad cuando cabe en memoria.
F32Formato de 32 bits, cómodo numéricamente pero caro para servir LLM grandes.
FP8 / INT8Formatos de 8 bits para reducir memoria y ancho de banda con evaluación obligatoria.
FP4 / INT4Formatos de 4 bits para local o coste bajo, con mayor riesgo de pérdida de calidad.
MoEArquitectura con expertos y enrutamiento por token.
Modelo cerradoModelo usado como servicio o producto sin acceso a pesos ni receta completa de entrenamiento.
Pesos abiertosPesos descargables o accesibles para servir, ajustar o cuantizar un modelo según licencia.
Código abiertoCódigo disponible bajo licencia abierta; en IA no implica automáticamente datos, pesos y entrenamiento abiertos.
Open Source AISistema que ofrece libertades de uso, estudio, modificación y compartición junto con la forma preferida para modificarlo.
KPI de selecciónIndicador que cambia una decisión de modelo: calidad propia, coste por tarea, p95, licencia, reproducibilidad o control de datos.
GRPOVariante de optimización por refuerzo usada en trabajos de razonamiento.
temperatureParámetro que controla variación en la generación.
top_pMuestreo que limita candidatos por probabilidad acumulada.
ThroughputCapacidad de procesar tokens o peticiones por unidad de tiempo.
Pass@1Métrica que cuenta si la primera solución pasa el verificador.
0-shotEvaluación sin ejemplos dentro del prompt.
few-shotEvaluación con ejemplos dentro del prompt.

Antes de pasar página

  • ¿Puedo explicar por qué una model card no es marketing ni garantía?
  • ¿Sé separar modelo base, modelo servido por API, modelo local y sistema completo?
  • ¿Sé distinguir modelo cerrado, pesos abiertos, código abierto y Open Source AI?
  • ¿Puedo explicar qué son los pesos y por qué no equivalen a todo el proceso de entrenamiento?
  • ¿He definido KPIs de selección antes de discutir marcas de modelos?
  • ¿Puedo construir filtros duros antes de comparar puntuaciones?
  • ¿Sé leer una model card de Hugging Face sin confundirme con tags, likes o downloads?
  • ¿Distingo parámetros totales, parámetros activados, precisión, formato y runtime?
  • ¿Sé por qué un benchmark general no sustituye una eval propia?
  • ¿Puedo calcular una puntuación ponderada y explicar sus pesos?
  • ¿Distingo precio público de coste total de propiedad?
  • ¿Sé qué datos debo guardar para que la decisión sea trazable?
  • ¿He ejecutado la práctica cambiando los pesos de la matriz?

En resumen

Elegir modelo es una decisión de ingeniería, producto y operación. La model card no te da una respuesta automática, pero sí una lista de preguntas que evitan elegir por entusiasmo.

Idea fuerzaDetalle
No existe “el mejor modelo” sin contexto.Existe el modelo adecuado para una tarea, restricciones y evidencia.
La model card se lee como ficha de compatibilidad.Uso previsto, límites, datos, evaluación, licencia y operación importan.
“Abierto” necesita apellido.Modelo cerrado, pesos abiertos, licencia propia y Open Source AI no significan lo mismo.
Los pesos abiertos compran control, no magia.Puedes servir, adaptar o cuantizar según licencia, pero quizá no tienes datos ni receta reproducible.
Los KPIs mandan sobre las etiquetas.Calidad propia, p95, coste por tarea, licencia, privacidad y reproducibilidad pesan más que el eslogan.
Los benchmarks orientan, no deciden.Tu eval propia decide si el modelo funciona en tu caso.
La versión exacta importa.Aliases, previews y modelos retirados pueden romper comparaciones.
El coste real no es solo precio por token.Latencia, cache, batch, operación y migración entran en la cuenta.
La decisión debe quedar escrita.Modelo elegido, alternativas, fecha y próxima revisión evitan memoria frágil.

Para saber más

Anthropic. (2026). Models overview. https://platform.claude.com/docs/en/about-claude/models/overview

Anthropic. (2026). Pricing. https://platform.claude.com/docs/en/about-claude/pricing

Gebru, T. et al. (2021). Datasheets for Datasets. https://doi.org/10.1145/3458723

DeepSeek-AI. (2025). DeepSeek-R1. https://huggingface.co/deepseek-ai/DeepSeek-R1

DeepSeek-AI. (2026). deepseek-ai/DeepSeek-V4-Pro. https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro

Docker. (2026). docker model run. https://docs.docker.com/reference/cli/docker/model/run/

Google. (2026). Gemma 4 model overview. https://ai.google.dev/gemma/docs/core

Google. (2026). Gemini API: Models. https://ai.google.dev/gemini-api/docs/models

Google. (2026). Gemini Developer API pricing. https://ai.google.dev/gemini-api/docs/pricing

Hugging Face. (2026). Chat templates. https://huggingface.co/docs/transformers/chat_templating

Hugging Face. (2026). DeepSeek-V4. https://huggingface.co/docs/transformers/model_doc/deepseek_v4

Hugging Face. (2026). Inference Providers. https://huggingface.co/docs/inference-providers/en/index

Hugging Face. (2026). Model Cards. https://huggingface.co/docs/hub/model-cards

Hugging Face. (2026). Quantization. https://huggingface.co/docs/transformers/main_classes/quantization

Hugging Face. (2026). Safetensors. https://huggingface.co/docs/safetensors/en/index

Hoffmann, J. et al. (2022). Training Compute-Optimal Large Language Models. https://doi.org/10.48550/arXiv.2203.15556

Kaplan, J. et al. (2020). Scaling Laws for Neural Language Models. https://doi.org/10.48550/arXiv.2001.08361

Liang, P. et al. (2022). Holistic Evaluation of Language Models. https://arxiv.org/abs/2211.09110

Mitchell, M. et al. (2019). Model Cards for Model Reporting. https://doi.org/10.1145/3287560.3287596

Meta. (2025). Llama 4 Community License Agreement. https://github.com/meta-llama/llama-models/blob/main/models/llama4/LICENSE

Mistral AI. (2025). Introducing Mistral 3. https://mistral.ai/news/mistral-3/

MLCommons. (2026). MLPerf Inference: Datacenter benchmark. https://mlcommons.org/benchmarks/inference-datacenter/

NVIDIA. (2026). Transformer Engine: Low Precision Training. https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/features/low_precision_training/introduction/introduction.html

OpenAI. (2025). Introducing gpt-oss. https://openai.com/index/introducing-gpt-oss/

OpenAI. (2026). Models. https://developers.openai.com/api/docs/models

OpenAI. (2026). OpenAI open-weight models (gpt-oss). https://help.openai.com/en/articles/11870455-openai-open-weight-models-gpt-oss

OpenAI. (2026). Pricing. https://developers.openai.com/api/docs/pricing

Open Source Initiative. (2024). The Open Source AI Definition 1.0. https://opensource.org/ai/open-source-ai-definition

Open Source Initiative. (2026). Open Weights: not quite what you’ve been told. https://opensource.org/ai/open-weights

Qwen Team. (2026). Qwen3.6. https://github.com/QwenLM/Qwen3.6

SGLang. (2026). Welcome to SGLang. https://docs.sglang.io/index.html

Shao, Z. et al. (2024). DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models. https://arxiv.org/abs/2402.03300

vLLM. (2026). OpenAI-Compatible Server. https://docs.vllm.ai/en/latest/serving/openai_compatible_server/

vLLM. (2026). Quantization. https://docs.vllm.ai/en/stable/features/quantization/

Notas

  1. Margaret Mitchell et al. (2019). Model Cards for Model Reporting. Proceedings of the Conference on Fairness, Accountability, and Transparency, 220-229. https://doi.org/10.1145/3287560.3287596.

  2. Timnit Gebru et al. (2021). Datasheets for Datasets. Communications of the ACM, 64(12), 86-92. https://doi.org/10.1145/3458723.

  3. Hugging Face. (2026). Model Cards. https://huggingface.co/docs/hub/model-cards. Consultado el 25 de mayo de 2026.

  4. OpenAI. (2026). Models. https://developers.openai.com/api/docs/models. Consultado el 25 de mayo de 2026.

  5. OpenAI. (2026). Pricing. https://developers.openai.com/api/docs/pricing. Consultado el 25 de mayo de 2026.

  6. Anthropic. (2026). Models overview. https://platform.claude.com/docs/en/about-claude/models/overview. Consultado el 25 de mayo de 2026.

  7. Anthropic. (2026). Pricing. https://platform.claude.com/docs/en/about-claude/pricing. Consultado el 25 de mayo de 2026.

  8. Google. (2026). Gemini API: Models. https://ai.google.dev/gemini-api/docs/models. Consultado el 25 de mayo de 2026.

  9. Google. (2026). Gemini Developer API pricing. https://ai.google.dev/gemini-api/docs/pricing. Consultado el 25 de mayo de 2026.

  10. Percy Liang et al. (2022). Holistic Evaluation of Language Models. https://arxiv.org/abs/2211.09110.

  11. MLCommons. (2026). MLPerf Inference: Datacenter benchmark. https://mlcommons.org/benchmarks/inference-datacenter/. Consultado el 25 de mayo de 2026.

  12. Open Source Initiative. (2024). The Open Source AI Definition 1.0. https://opensource.org/ai/open-source-ai-definition. Consultado el 14 de junio de 2026.

  13. Open Source Initiative. (2026). Open Weights: not quite what you’ve been told. https://opensource.org/ai/open-weights. Consultado el 14 de junio de 2026.

  14. OpenAI. (2026). Models. https://developers.openai.com/api/docs/models. Consultado el 14 de junio de 2026. Anthropic. (2026). Models overview. https://platform.claude.com/docs/en/about-claude/models/overview. Consultado el 14 de junio de 2026. Google. (2026). Gemini API: Models. https://ai.google.dev/gemini-api/docs/models. Consultado el 14 de junio de 2026.

  15. OpenAI. (2025). Introducing gpt-oss. https://openai.com/index/introducing-gpt-oss/. Consultado el 14 de junio de 2026. OpenAI. (2026). OpenAI open-weight models (gpt-oss). https://help.openai.com/en/articles/11870455-openai-open-weight-models-gpt-oss. Consultado el 14 de junio de 2026. Qwen Team. (2026). Qwen3.6. https://github.com/QwenLM/Qwen3.6. Consultado el 14 de junio de 2026. Mistral AI. (2025). Introducing Mistral 3. https://mistral.ai/news/mistral-3/. Consultado el 14 de junio de 2026. DeepSeek-AI. (2025). DeepSeek-R1. https://huggingface.co/deepseek-ai/DeepSeek-R1. Consultado el 14 de junio de 2026.

  16. Meta. (2025). Llama 4 Community License Agreement. https://github.com/meta-llama/llama-models/blob/main/models/llama4/LICENSE. Consultado el 14 de junio de 2026. Google. (2026). Gemma 4 model overview. https://ai.google.dev/gemma/docs/core. Consultado el 14 de junio de 2026.

  17. Hugging Face. (2026). Model Cards. https://huggingface.co/docs/hub/model-cards. Consultado el 14 de junio de 2026.

  18. Jared Kaplan et al. (2020). Scaling Laws for Neural Language Models. https://doi.org/10.48550/arXiv.2001.08361.

  19. Jordan Hoffmann et al. (2022). Training Compute-Optimal Large Language Models. https://doi.org/10.48550/arXiv.2203.15556.

  20. DeepSeek-AI. (2026). deepseek-ai/DeepSeek-V4-Pro. https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro. Consultado el 25 de mayo de 2026.

  21. Hugging Face. (2026). DeepSeek-V4. https://huggingface.co/docs/transformers/model_doc/deepseek_v4. Consultado el 25 de mayo de 2026.

  22. Hugging Face. (2026). Safetensors. https://huggingface.co/docs/safetensors/en/index. Consultado el 25 de mayo de 2026.

  23. NVIDIA. (2026). Transformer Engine: Low Precision Training. https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/features/low_precision_training/introduction/introduction.html. Hugging Face. (2026). Quantization. https://huggingface.co/docs/transformers/main_classes/quantization. vLLM. (2026). Quantization. https://docs.vllm.ai/en/stable/features/quantization/. Consultados el 25 de mayo de 2026.

  24. Zhihong Shao et al. (2024). DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models. https://arxiv.org/abs/2402.03300.

  25. Hugging Face. (2026). Chat templates. https://huggingface.co/docs/transformers/chat_templating. Consultado el 25 de mayo de 2026.

  26. vLLM. (2026). OpenAI-Compatible Server. https://docs.vllm.ai/en/latest/serving/openai_compatible_server/. Consultado el 25 de mayo de 2026. SGLang. (2026). Welcome to SGLang. https://docs.sglang.io/index.html. Consultado el 25 de mayo de 2026.

  27. Docker. (2026). docker model run. https://docs.docker.com/reference/cli/docker/model/run/. Consultado el 25 de mayo de 2026.

  28. Hugging Face. (2026). Inference Providers. https://huggingface.co/docs/inference-providers/en/index. Consultado el 25 de mayo de 2026.

Capítulo 05

Facsímil 4 · La caja de herramientas

Capítulo 05: Modelos locales: Ollama, LM Studio, GGUF y cuantización

Cuando el modelo se queda en tu máquina

Hay una frase que suena sencilla: “lo corremos en local”. Parece que significa privacidad, control, coste bajo y ausencia de dependencias externas. A veces es verdad. Otras veces significa otra cosa: descargar un fichero enorme, pelear con memoria, descubrir que el contexto no cabe, que el modelo responde lento, que la cuantización cambia el comportamiento o que la API local está expuesta de una forma que nadie revisó.

Venimos del capítulo 04, donde aprendimos a leer model cards. Ahora bajamos un nivel: qué ocurre cuando eliges un modelo descargable y quieres ejecutarlo tú. Ya no basta con preguntar si el modelo es bueno. Hay que preguntar si cabe, si responde a tiempo, si el runtime entiende su formato, si la licencia encaja, si la calidad tras cuantizar sigue siendo suficiente y si puedes medirlo sin engañarte.

La idea central es esta: un modelo local no es solo un modelo; es la suma de pesos, formato, runtime, hardware, configuración, API, licencia y evaluación.

Estado del arte con fecha de corte

Fecha de corte: 10 de junio de 2026.
Fuentes consultadas ese día: documentación oficial de Ollama, LM Studio, Hugging Face Hub sobre GGUF, repositorio de llama.cpp y papers de cuantización LLM.int8, SmoothQuant, GPTQ, AWQ, QLoRA y cuantización entera clásica.

Lo estable es el mecanismo: descargar pesos, elegir formato, cargar en un runtime, repartir memoria entre CPU/GPU, configurar contexto y generación, exponer una API si hace falta, medir calidad y latencia. Lo cambiante son nombres de modelos, soporte de GPU, formatos concretos, variantes de cuantización, límites de contexto y compatibilidad de cada aplicación.

FuenteQué aportaQué decisión permite tomar
Ollama API.1API local por defecto, endpoints, librerías y compatibilidad básica.Saber si tu app puede llamar al modelo como servicio local.
Ollama Modelfile.2FROM, PARAMETER, TEMPLATE, SYSTEM, ADAPTER, LICENSE y MESSAGE.Saber qué parte de la conducta se fija en la definición del modelo.
Ollama context length.3Relación entre VRAM, contexto por defecto y memoria necesaria.No subir contexto sin calcular memoria.
Ollama hardware support.4Soporte de NVIDIA, AMD, Metal y Vulkan.Comprobar si tu máquina acelera o cae a CPU.
Ollama OpenAI compatibility.5Compatibilidad con parte de la API de OpenAI.Reutilizar clientes existentes sabiendo que “compatible” no significa idéntico.
LM Studio basics.6Flujo de descarga y ejecución local de modelos con pesos accesibles.Entender qué se descarga y qué significa correr un modelo desde una UI.
LM Studio REST API.7API nativa local y endpoints compatibles con OpenAI y Anthropic.Decidir si LM Studio será UI, servidor local o ambas cosas.
LM Studio load.8Carga con contexto, GPU offload, TTL y estimación de memoria.Probar si un modelo cabe antes de cargarlo.
Hugging Face GGUF.9GGUF como formato con tensores y metadatos; visor de metadata y tipos de cuantización.Leer un .gguf como fichero técnico, no como etiqueta comercial.
llama.cpp.10Runtime C/C++ base del ecosistema GGUF.Entender de dónde vienen muchas piezas de Ollama, LM Studio y herramientas locales.
LLM.int8.11Cuantización 8-bit cuidando valores atípicos en LLMs grandes.Entender por qué bajar bits no es solo redondear números.
SmoothQuant.12Reescalado de pesos y activaciones para cuantización post-entrenamiento.Entender por qué activaciones y pesos se tratan juntos en algunos despliegues.
GPTQ.13Cuantización post-entrenamiento de pesos usando información aproximada de segundo orden.Leer GPTQ como método de compresión medible, no como sufijo decorativo.
AWQ.14Cuantización orientada a pesos importantes según activaciones.Saber por qué algunas cuantizaciones conservan mejor calidad que otras.
QLoRA.15Fine-tuning eficiente sobre modelos cuantizados de 4 bits.Separar servir un modelo cuantizado de ajustar adaptadores sobre una base cuantizada.
Cuantización entera clásica.16Base de cuantización para inferencia eficiente con enteros.Recordar que cuantizar es aproximar cálculo, no comprimir un ZIP.

La revisión del 10 de junio añade un matiz importante: “uso Ollama” ya no equivale necesariamente a “todo corre en mi portátil”. Ollama documenta Cloud como una forma de usar modelos remotos grandes con herramientas locales, mientras que la API local sigue viviendo por defecto en localhost:11434.17 LM Studio documenta un servidor local que puede exponerse en la máquina o en red y que ofrece API nativa, compatibilidad OpenAI y compatibilidad Anthropic.18 llama.cpp, por su parte, documenta llama-server con endpoints compatibles, batching, métricas y salidas restringidas por schema.19

La consecuencia práctica es clara: cuando digas “modelo local”, documenta dónde se ejecuta realmente, qué puerto abre, si acepta tráfico de red, qué autenticación tiene, qué contexto reserva, qué cuantización usa, qué plantilla de chat aplica y qué versión del runtime lo sirve. La diferencia entre una práctica de clase y un incidente de seguridad puede ser una bandera de host mal puesta.

Qué no es correr un modelo en local

Correr un modelo en local no significa automáticamente que sea privado en sentido fuerte. Si descargas pesos desde un repositorio externo, ejecutas una app, expones un puerto, instalas extensiones o conectas herramientas, sigues teniendo superficie técnica que revisar. Local reduce ciertos problemas de envío de datos a un proveedor remoto, pero no elimina los problemas de licencia, procedencia de ficheros, logs, permisos, backups o red.

Tampoco significa “gratis”. Dejas de pagar por token a una API, pero pagas en hardware, electricidad, tiempo de instalación, memoria, mantenimiento, actualizaciones y evaluación. Si un portátil tarda 40 segundos en responder a una tarea que una API resuelve en 2 segundos por céntimos, puede que local no sea más barato para ese flujo.

Y no significa “igual que el modelo original, pero comprimido”. Un GGUF Q4 no es el mismo objeto operativo que un checkpoint BF16. Puede ser suficientemente bueno, pero hay que demostrarlo con casos propios. Si no mides, no has elegido cuantización; has elegido esperanza.

Qué sí es un modelo local

Un modelo local es un sistema de inferencia donde tú controlas la máquina que carga los pesos. Esa frase es más precisa que “lo tengo en mi ordenador”. Controlar la máquina implica decidir formato, runtime, memoria, configuración, API, permisos y actualización.

PiezaQué esQué pregunta responde
PesosFicheros con matrices aprendidas durante entrenamiento o ajuste.¿Qué modelo estoy ejecutando realmente?
FormatoGGUF, safetensors, ONNX, TensorRT u otro contenedor.¿Qué runtime puede abrirlo?
RuntimeOllama, LM Studio, llama.cpp, vLLM, SGLang, Transformers u otro.¿Quién gestiona memoria y generación?
HardwareCPU, GPU, NPU, VRAM, RAM, memoria unificada.¿Cabe y a qué velocidad responde?
ConfiguraciónContexto, cuantización, GPU offload, temperatura, top-p, stops.¿Cómo se comporta en cada llamada?
API localEndpoint HTTP o SDK para llamarlo desde una app.¿Cómo lo integro en un producto o notebook?
EvaluaciónCasos propios con calidad, latencia y memoria.¿Sirve para mi tarea, no solo para una demo?

La pregunta profesional no es “¿puedo ejecutarlo?”. Es: ¿puedo ejecutarlo con calidad, latencia, memoria, licencia y mantenimiento aceptables?

La pila local por dentro

Un modelo local atraviesa una pila. Si una pieza falla, el resultado final falla aunque el modelo sea bueno.

CapaQué mideQué aportaQué sería razonable
LicenciaPermisos de uso, modificación y redistribución.Reduce riesgo jurídico y de producto.Leer licencia del modelo y de variantes derivadas antes de automatizar.
ProcedenciaQuién publicó el fichero y con qué historial.Evita comparar copias, forks o variantes sin saberlo.Guardar repo, archivo exacto, hash o commit cuando sea posible.
FormatoCómo están empaquetados pesos y metadatos.Determina runtime compatible.GGUF para llama.cpp/Ollama/LM Studio; safetensors para ecosistema Transformers.
Bits por pesoMemoria aproximada de pesos.Estima si cabe y cuánta calidad podrías perder.BF16 como referencia; Q4/Q5 si necesitas local barato; Q8 si tienes memoria.
ContextoTokens que entran en memoria.Permite tareas largas, pero aumenta KV cache.No subir contexto sin medir VRAM/RAM y TTFT.
GPU offloadCapas o partes movidas a GPU.Reduce latencia si cabe en VRAM.Usar GPU para capas máximas sin expulsar KV cache ni forzar intercambio lento.
APIContrato de integración.Permite usarlo desde una app.Probar streaming, errores, JSON y límites antes de cambiar proveedor.
ObservabilidadLogs, tiempo, tokens/s, memoria, errores.Permite saber qué pasa cuando algo va lento.Medir p50/p95, TTFT, tokens/s y memoria por caso.

La segunda pasada obligatoria es preguntarse qué falta. En un primer intento solemos mirar solo “modelo y cuantización”. Falta comprobar licencia, procedencia, hash, contexto real, plantilla de chat, memoria de KV cache, parámetros de muestreo, endpoint, exposición de red, eval propia y plan de actualización. Si no está escrito, no existe.

La tercera pasada es rehacer la decisión como si tuviera que explicarse en una reunión: “elegimos este modelo local porque cabe con esta cuantización, responde en este p95, conserva esta calidad frente al baseline, usa esta licencia, se integra por esta API y tiene esta alternativa si falla”.

Memoria: la cuenta que evita la fantasía

Antes de descargar nada, haz una estimación. No será perfecta, pero evita decisiones imposibles.

La memoria mínima de pesos se aproxima así:

MpesosNparametrosbpeso8M_{\text{pesos}} \approx N_{\text{parametros}} \cdot \frac{b_{\text{peso}}}{8}
SímboloSignificadoEjemplo
MpesosM_{\text{pesos}}Memoria aproximada ocupada solo por pesos.Un 7B Q4 ronda 3,5 GB antes de sobrecostes.
NparametrosN_{\text{parametros}}Número de parámetros del modelo.7B, 14B, 32B, 70B.
bpesob_{\text{peso}}Bits por peso tras cuantizar.16, 8, 5, 4, 3.

Pero un modelo cargado no son solo pesos.

Ejemplo de fórmula. Una cuenta más honesta es:

MtotalMpesos+MKV+Mruntime+MmargenM_{\text{total}} \approx M_{\text{pesos}} + M_{\text{KV}} + M_{\text{runtime}} + M_{\text{margen}}
SímboloSignificadoPor qué importa
MKVM_{\text{KV}}Memoria de la KV cache.Crece con contexto, capas, batch y dimensiones de atención.
MruntimeM_{\text{runtime}}Memoria del programa, buffers y kernels.No aparece en el tamaño del fichero.
MmargenM_{\text{margen}}Colchón para sistema operativo, UI, navegador, otros procesos y picos.Si no hay margen, el sistema intercambia memoria y se vuelve lento.

Regla de bolsillo para pesos:

Modelo densoBF16/F16Q8/INT8Q5 aprox.Q4 aprox.Lectura práctica
3B~6 GB~3 GB~1,9 GB~1,5 GBBuen candidato para portátiles modestos.
7B~14 GB~7 GB~4,4 GB~3,5 GBPunto de entrada local serio.
14B~28 GB~14 GB~8,8 GB~7 GBEmpieza a exigir GPU/RAM holgada.
32B~64 GB~32 GB~20 GB~16 GBNormalmente workstation o servidor.
70B~140 GB~70 GB~44 GB~35 GBMulti-GPU, mucha RAM o paciencia.

Lo adecuado depende de la tarea:

SituaciónPunto de partida razonablePor qué
Notebook, aprendizaje, privacidad personal3B-8B en Q4/Q5.Cabe mejor y permite experimentar.
Clasificación o extracción sencilla7B-14B Q4/Q5 con salida estructurada.Calidad suficiente si la tarea está bien acotada.
Código, razonamiento o agente local14B-32B si cabe, comparado contra API.Estos casos sufren más con modelos pequeños.
Producción con usuarios simultáneosRuntime de servidor, medición p95 y modelo alternativo.El problema ya no es una conversación aislada.
Datos sensibles con restricción fuerteLocal o entorno privado con auditoría.El criterio principal es control, no solo calidad.

La frase “este modelo cabe” debe significar algo concreto: cabe con este contexto, esta cuantización, este batch, esta GPU offload, este margen de memoria y esta calidad medida.

Ollama: cómodo no significa invisible

Ollama es una forma práctica de ejecutar modelos y hablar con ellos por CLI, app o API. La documentación oficial indica que su API local se sirve por defecto en http://localhost:11434/api, y que también ofrece librerías oficiales para Python y JavaScript. Esa comodidad es valiosa porque permite convertir un modelo local en un servicio consumible por scripts, notebooks o aplicaciones.

Lo importante es no tratar Ollama como una caja opaca. Ollama decide cómo cargar el modelo, qué contexto usar, qué parámetros aplicar y cómo exponer endpoints. Debes saber leer esas decisiones.

TérminoQué mide o controlaEjemploDecisión práctica
ollama runConversación rápida por terminal.ollama run gemma3Bien para probar; no basta para evaluar producto.
API localContrato HTTP local.POST /api/generate o POST /api/chat.Útil para integrar en Python, JS o una app interna.
OpenAI compatibilityAdaptación parcial a clientes existentes./v1/chat/completions.Reutiliza SDKs, pero valida parámetros soportados y errores.
ModelfileReceta reproducible de modelo, plantilla y parámetros.FROM llama3.2, PARAMETER num_ctx 4096, SYSTEM ....No guardes solo “uso llama”; guarda el Modelfile.
FROMModelo base o fichero GGUF/safetensors.FROM ./modelo.gguf.Define qué pesos cargas realmente.
PARAMETER num_ctxTamaño de contexto.num_ctx 4096.Más contexto usa más memoria; no es gratis.
TEMPLATEPlantilla de prompt.Formato de roles y separadores.Si está mal, el modelo parece peor.
ADAPTERAdaptador LoRA/QLoRA.ADAPTER ./adapter.gguf.Solo encaja si base y adaptador corresponden.
LICENSETexto legal asociado.Licencia incluida en el Modelfile.Necesario si empaquetas o compartes.
ollama psModelos cargados, procesador y contexto.100% GPU, CONTEXT 65536.Comprueba si de verdad está en GPU y qué contexto usa.

Ollama documenta valores por defecto de contexto según VRAM: por debajo de 24 GiB, 4k; entre 24 y 48 GiB, 32k; desde 48 GiB, 256k. También recomienda contextos altos para tareas como web search, agentes y herramientas de código. La lectura correcta no es “sube contexto a 64k siempre”, sino “si la tarea lo requiere, calcula memoria y verifica ollama ps”.

Situación concreta: quieres usar un modelo local para revisar un repositorio. Si el agente mete muchos ficheros en contexto, 4k no alcanza. Pero subir a 64k puede llenar VRAM. La solución profesional no es elegir al azar: mide cuántos tokens entran, decide qué se recupera, sube contexto solo si aporta y comprueba TTFT y p95.

LM Studio: visual, local y medible

LM Studio entra por otro camino: hace cómoda la experiencia visual de buscar, descargar, cargar y probar modelos locales. Su documentación recuerda una distinción importante: para correr local necesitas acceso a los pesos, normalmente en formatos como .gguf o .safetensors.

Además de UI, LM Studio ofrece API REST local, endpoints compatibles con OpenAI y Anthropic, y CLI. En su REST API v1 aparecen endpoints como /api/v1/chat, /api/v1/models, /api/v1/models/load, /api/v1/models/unload y /api/v1/models/download. Eso convierte LM Studio en algo más que una app de chat: puede ser un servidor local de desarrollo.

TérminoQué mide o controlaEjemploDecisión práctica
Modelo descargadoFichero local con pesos.Un .gguf de Qwen, Mistral o Gemma.Comprueba licencia, tamaño, cuantización y procedencia.
lms loadCargar un modelo en memoria.lms load <model_key>.Separa descargar de cargar; no todo lo descargado está activo.
--context-lengthTokens de contexto al cargar.--context-length 8192.Mide memoria y calidad con ese contexto, no con el máximo teórico.
--gpuProporción de offload a GPU.--gpu 0.5, --gpu max, --gpu off.Si no usas GPU, la experiencia puede cambiar mucho.
--estimate-onlyEstimar memoria sin cargar.lms load --estimate-only <model_key>.Úsalo antes de romper la sesión por falta de memoria.
TTLDescargar de memoria tras inactividad.--ttl 3600.Evita dejar modelos ocupando RAM/VRAM todo el día.
API nativaEndpoint local propio./api/v1/chat.Útil si quieres capacidades específicas de LM Studio.
OpenAI-compatibleEndpoint familiar para clientes existentes./v1/chat/completions.Valida streaming, structured output y parámetros.

LM Studio también permite configurar parámetros de inferencia, como temperature, maxTokens y topP, y parámetros de carga, como longitud de contexto y GPU offload. La separación es crucial: temperature cambia cómo se elige el siguiente token; contextLength cambia cuánta memoria reserva el sistema al cargar.

Situación concreta: en una clase o equipo, LM Studio es excelente para enseñar porque el lector ve el modelo, el fichero, la carga, la conversación y el servidor. En un backend repetible, quizá prefieras Ollama o llama.cpp directamente. La decisión no es “cuál mola más”, sino qué necesitas: UI, script, servidor, compatibilidad, trazabilidad o control fino.

Una prueba local que sí se puede repetir

Una prueba local no debería consistir en abrir una ventana, hacer una pregunta y decidir por impresión. Eso sirve para orientarse, pero no para elegir. Una prueba mínima debe dejar huella: qué modelo era, qué fichero, qué cuantización, qué runtime, qué contexto, qué máquina, qué prompt, qué salida y qué métricas.

Con Ollama, una llamada mínima a la API local puede parecer así:

curl http://localhost:11434/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama3.2",
    "messages": [
      {"role": "system", "content": "Responde en español claro y devuelve JSON válido."},
      {"role": "user", "content": "Clasifica esta incidencia: no puedo entrar al campus virtual."}
    ],
    "stream": false,
    "options": {
      "temperature": 0.2,
      "num_ctx": 4096
    }
  }'

Con LM Studio en modo compatible con OpenAI, una llamada de integración puede tener esta forma:

curl http://localhost:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "modelo-local-cargado",
    "messages": [
      {"role": "system", "content": "Extrae campos con precisión. Si falta un dato, usa null."},
      {"role": "user", "content": "Factura 2026-05: total 184,30 EUR; proveedor Norte S.L."}
    ],
    "temperature": 0.1,
    "max_tokens": 300
  }'

Lo importante no es copiar esos comandos tal cual, sino entender qué contrato crean:

CampoQué controlaQué anotar
modelIdentificador que el runtime resuelve a pesos concretos.Nombre exacto, origen y, si es posible, hash o ruta.
messagesPlantilla de conversación convertida a tokens.System prompt, user prompt y versión del caso de prueba.
temperatureAleatoriedad de muestreo.Valor fijo durante eval; si cambia, cambia la comparación.
num_ctx / contextoVentana que reserva memoria.Contexto usado, no el máximo anunciado.
max_tokensLímite de salida.Evita comparar respuestas truncadas con completas.
streamSi la salida llega por partes.Afecta percepción de latencia y forma de medir TTFT.

Una tabla de evaluación mínima tendría estas columnas:

ColumnaQué significaPor qué importa
fechaDía de la prueba.Los modelos, runtimes y drivers cambian.
runtimeOllama, LM Studio, llama.cpp, vLLM, etc.El mismo fichero puede rendir distinto.
version_runtimeVersión instalada.Una actualización puede cambiar memoria o salida.
modelo_origenRepositorio o proveedor de pesos.Evita comparar copias distintas.
ficheroNombre exacto del GGUF/safetensors.Incluye cuantización y variante.
contextoTokens de contexto configurados.Cambia memoria y a veces calidad.
caso_idIdentificador del caso de prueba.Permite repetir y depurar.
ok_formatoSi cumple contrato de salida.Importa tanto como “suena bien”.
ok_contenidoSi la respuesta es correcta.Métrica principal de utilidad.
ttft_sTiempo hasta primer token.Mide sensación inicial de respuesta.
tokens_sTokens de salida por segundo.Mide velocidad de generación.
memoria_pico_gbPico observado de RAM/VRAM.Detecta candidatos que caben sin margen real.
observacionFallo o matiz concreto.Ayuda a saber qué mejorar.

La regla profesional es comparar siempre contra algo: contra una API fuerte, contra Q8, contra otro runtime o contra el mismo modelo con menos contexto. Un resultado aislado no dice si el sistema es bueno; solo dice que funcionó una vez.

Medir latencia y velocidad sin engañarse

En local se habla mucho de “va rápido” o “va lento”. Para que esa frase sirva, hay que separarla en métricas.

TTFT=tprimer tokentenvioTTFT = t_{\text{primer\ token}} - t_{\text{envio}} tokens/s=Ntokens salidatfintprimer token\text{tokens/s} = \frac{N_{\text{tokens\ salida}}}{t_{\text{fin}} - t_{\text{primer\ token}}} p95=percentil95(latencias)p95 = \text{percentil}_{95}(\text{latencias})
MétricaQué aportaQué valor es adecuado
TTFTCuánto espera la persona hasta ver que algo empieza.Bajo si hay interacción humana; menos crítico en batch.
Tokens/sRitmo de generación una vez arrancó.Depende de longitud de salida; compáralo en los mismos casos.
Latencia totalTiempo desde petición hasta respuesta completa.Importa cuando la salida debe estar cerrada para continuar.
p95Cómo se comporta el sistema en casos lentos.Más útil que el promedio si hay usuarios reales.
Memoria picoMáximo de RAM/VRAM observado.Debe dejar margen; si va al límite, el sistema será frágil.
Tasa de formato válidoPorcentaje de salidas parseables.Crucial en JSON, SQL, extracción o agentes.

Ejemplo: un modelo que da 35 tokens/s puede parecer mejor que otro de 22 tokens/s. Pero si el primero falla JSON en 8 de cada 100 casos y el segundo falla en 1, el segundo puede ser mejor para una integración. La métrica adecuada depende de qué duele más: espera, coste, errores de formato, memoria o mantenimiento.

GGUF: el fichero no es el modelo, es el contenedor operativo

GGUF aparece en muchas páginas de modelos y causa una confusión frecuente. No es una arquitectura. No es una familia. No es una licencia. Es un formato de fichero pensado para guardar pesos y metadatos de forma útil para runtimes como llama.cpp.

Hugging Face describe GGUF como un formato binario optimizado para carga y guardado eficiente de modelos; a diferencia de formatos solo de tensores, GGUF incorpora tensores y metadatos estandarizados. Además, el Hub ofrece visor de metadatos y tipos de tensor para archivos GGUF.

Dato en un GGUFQué mideQué aportaQué sería razonable
ArquitecturaFamilia que espera el runtime.Saber si puede cargarse.No forzar un runtime que no soporta esa arquitectura.
Contexto declaradoLongitud máxima o recomendada.Límite operativo inicial.Probar contexto real, no solo máximo.
Tipo de cuantizaciónBits y método de aproximación.Memoria y calidad esperada.Q4/Q5 para equilibrio, Q8 para calidad si cabe, Q2/Q3 solo con eval fuerte.
Tensor infoNombre, forma y precisión de tensores.Auditar qué hay dentro.Revisar si algo no cuadra entre card y fichero.
Tokenizer metadataCómo partir texto.Evitar prompts mal codificados.No mezclar tokenizer de otro modelo.
Chat templateFormato de conversación.Convertir roles a tokens correctos.Probar con plantilla del modelo, no inventarla.

Nombres como Q4_K_M, Q5_K_M o Q8_0 no son decoración. Indican una estrategia de cuantización. Aun así, no hay que tratarlos como una escala universal de calidad. Dos modelos distintos pueden perder capacidades distintas al pasar a Q4. Una tarea de resumen puede sobrevivir bien; una tarea de código o extracción exacta puede sufrir más.

Variante orientativaQué aportaCuándo probarlaQué comparar
F16/BF16Referencia de más calidad y más memoria.Si tienes RAM/VRAM suficiente.Sirve como baseline contra cuantizadas.
Q8Cerca de calidad alta con menos memoria.Si Q4 falla o la tarea es delicada.Calidad frente a F16/BF16 y latencia.
Q5Equilibrio entre memoria y calidad.Buen primer candidato si Q4 queda justo.Errores en casos difíciles.
Q4Mucho ahorro de memoria.Portátiles, demos, prototipos y tareas acotadas.Formato, razonamiento, código y alucinaciones.
Q2/Q3Ahorro agresivo.Solo si la restricción de memoria manda.Degradación fuerte con eval propia.

La pregunta “¿qué cuantización uso?” debería reformularse así: “¿cuál es la menor cuantización que mantiene calidad suficiente en mis casos, con latencia y memoria aceptables?”.

Cuantizar no es comprimir un archivo

Cuantizar significa aproximar números. En un modelo, los pesos son valores numéricos. Si los representas con menos bits, ocupan menos y pueden moverse más rápido, pero pierdes resolución. La cuestión es dónde se pierde, cuánto se pierde y si tu tarea lo nota.

La idea matemática mínima es esta. Un peso real xx, que antes vivía como FP16, BF16 o FP32, se guarda como un entero pequeño qq. Para volver a usarlo, el runtime reconstruye una aproximación x^\hat{x}:

q=clip(round(xs)+z, qmin, qmax)q = \operatorname{clip}\left(\operatorname{round}\left(\frac{x}{s}\right) + z,\ q_{\min},\ q_{\max}\right) x^=s(qz)\hat{x} = s \cdot (q - z)
SímboloQué significaQué aporta
xxValor original del peso o activación.Referencia de calidad.
qqEntero almacenado con pocos bits.Ahorro de memoria y movimiento de datos.
ssEscala de cuantización.Dice cuánto vale un paso entero en el mundo real.
zzZero-point.Permite desplazar el cero en cuantización asimétrica.
qmin,qmaxq_{\min}, q_{\max}Rango posible del entero.En 4 bits hay muchos menos valores que en 8 o 16.
x^\hat{x}Valor reconstruido.Lo que realmente usa el cálculo tras cuantizar.

Si cuantizas a 4 bits, cada valor no puede tomar infinitos matices: solo hay 16 códigos posibles por grupo. El truco está en elegir bien la escala, el grupo y qué tensores reciben más precisión. Por eso dos ficheros Q4 pueden comportarse diferente aunque ambos “sean Q4”.

La pérdida local de información se puede mirar así:

ϵW=WW^2W2\epsilon_W = \frac{\lVert W - \hat{W} \rVert_2}{\lVert W \rVert_2}

Pero esa fórmula no basta para decidir. Un error pequeño en una matriz puede afectar mucho a una tarea de código, y un error mayor puede ser tolerable en una tarea de clasificación sencilla. La calidad final no se certifica mirando solo el error de pesos; se certifica con casos de uso.

Qué se puede cuantizar

No todo se cuantiza igual. Esta distinción evita muchas confusiones:

Qué se cuantizaQué cambiaQué aportaRiesgo
PesosSe guardan matrices del modelo con menos bits.Reduce tamaño del fichero y memoria de carga.Puede degradar razonamiento, código o extracción fina.
ActivacionesSe aproximan valores intermedios durante inferencia.Puede acelerar cálculo si el hardware lo aprovecha.Más delicado: cambia señales que dependen de cada entrada.
KV cacheSe guarda la memoria de atención con menos precisión.Ahorra mucho en contextos largos.Puede empeorar recuperación de detalles lejanos.
AdaptadoresSe ajustan piezas pequeñas sobre una base cuantizada.Permite fine-tuning barato con QLoRA.No convierte una base pobre en una buena por sí solo.
EmbeddingsSe reducen vectores de representación.Ahorra almacenamiento y búsqueda.Puede afectar ranking semántico y vecinos cercanos.

En modelos locales de escritorio, muchas veces estás usando cuantización de pesos. Eso no significa que todas las operaciones internas sean enteras, ni que las activaciones estén cuantizadas, ni que el runtime use la GPU de la misma forma que un servidor optimizado. Esta frase es importante: peso cuantizado no significa inferencia entera de punta a punta.

Granularidad: dónde se decide el daño

Una misma cuantización de 4 bits puede ser muy distinta según cuántos pesos comparten escala.

GranularidadQué ocurreVentajaCoste
Por tensorToda la matriz comparte escala.Muy simple y pocos metadatos.Mala si hay valores con rangos muy distintos.
Por canal o filaCada fila/canal tiene su escala.Mejor preservación de matrices grandes.Más metadatos.
Por grupo o bloqueGrupos de 32, 64, 128 u otro tamaño comparten escala.Buen equilibrio en LLMs locales.Más complejo; cada formato lo concreta distinto.
Mixta por tensorAlgunos tensores reciben más bits que otros.Protege partes sensibles del modelo.El nombre del fichero ya no cuenta toda la historia.

La regla práctica: grupos más pequeños suelen conservar mejor la señal, pero añaden metadatos y complejidad. En GGUF, los sufijos como Q4_K_M resumen una receta; no sustituyen leer metadata, card del cuantizador y evaluación.

Familias importantes

LLM.int8 mostró que en LLMs grandes no basta con pasar todo a 8 bits de forma ingenua: hay valores atípicos que conviene tratar con cuidado. SmoothQuant redistribuye dificultad entre activaciones y pesos para hacer más viable la cuantización de activaciones. GPTQ propuso una ruta de cuantización post-entrenamiento para modelos generativos usando información aproximada de segundo orden. AWQ se fijó en qué pesos importan más según activaciones para preservar mejor comportamiento. QLoRA popularizó ajustar modelos usando una base cuantizada de 4 bits con adaptadores entrenables. La cuantización clásica de redes ya venía de antes, pero los LLMs hicieron visible que no todos los pesos duelen igual.

Método o familiaQué cambiaQué aportaQué prueba haría
INT8 / LLM.int8Baja a 8 bits cuidando valores atípicos.Menos memoria con pérdida pequeña si se hace bien.Comparar exactitud y formato contra BF16.
SmoothQuantReescala pesos y activaciones antes de cuantizar.Hace más manejable cuantizar activaciones.Medir latencia real en el runtime elegido.
GPTQCuantización post-entrenamiento de pesos.Ficheros pequeños y rápidos en ciertos runtimes.Probar código, matemáticas y extracción.
AWQConserva pesos relevantes según activaciones.Buen equilibrio para despliegue eficiente.Comparar frente a GPTQ/GGUF de mismo tamaño.
GGUF Q4/Q5/Q8Recetas prácticas para llama.cpp y derivados.Uso local amplio y sencillo.Medir calidad por tarea, no por sufijo.
QLoRA / NF4Fine-tuning eficiente sobre base cuantizada.Ajustar comportamiento con mucha menos memoria.No confundir ajustar con servir un modelo cuantizado.

Qué significa elegir Q4, Q5 o Q8

Los bits por peso reducen memoria, pero no de forma aislada. Hay metadatos, escalas, grupos y tensores con formatos distintos. Aun así, como intuición:

OpciónLectura técnicaCuándo suele encajarCuándo sospechar
BF16/F16Baseline de alta fidelidad.Evaluación de referencia y tareas delicadas.Si no cabe o la latencia es inaceptable.
Q8Aproximación conservadora.Cuando quieres ahorrar memoria sin perder demasiado.Si el ahorro no basta para tu máquina.
Q6/Q5Punto intermedio.Código, extracción y razonamiento moderado si Q4 falla.Si el modelo sigue quedando lento o justo de memoria.
Q4Compromiso local popular.Chat, resumen, clasificación acotada, aprendizaje y prototipos.Si hay errores de formato, cálculo, SQL o instrucciones largas.
Q3/Q2Compromiso agresivo.Solo cuando la máquina manda y la tarea tolera pérdida.En casi todo lo que requiera precisión sostenida.

Un ejemplo concreto: si un 7B Q4 cabe y responde fluido, puede ser buena elección para resumir tickets internos. Si el mismo modelo debe devolver JSON contractual con importes, fechas y campos obligatorios, Q5, Q8 o una API fuerte pueden ser más razonables. No porque Q4 sea “malo”, sino porque el coste del error cambió.

Cómo evaluar una cuantización

Ejemplo de fórmula. Una evaluación mínima compara al menos dos candidatos sobre los mismos casos:

Δcalidad=scorebaselinescorecuantizado\Delta_{\text{calidad}} = \text{score}_{\text{baseline}} - \text{score}_{\text{cuantizado}} ahorro=1McuantizadoMbaseline\text{ahorro} = 1 - \frac{M_{\text{cuantizado}}}{M_{\text{baseline}}}
PruebaQué detectaSeñal de alarma
Formato exactoSi respeta JSON, CSV, SQL o campos obligatorios.Respuestas bonitas pero no parseables.
Casos largosSi conserva información al crecer el contexto.Olvida restricciones del principio.
Cálculo simpleSi degrada operaciones numéricas.Errores en sumas, importes o comparaciones.
CódigoSi mantiene sintaxis y pruebas.Soluciones que parecen plausibles pero no ejecutan.
Recuperación de datosSi extrae hechos sin inventar.Cambia nombres, fechas o cantidades.
LatenciaSi el ahorro de memoria mejora la experiencia.Menos memoria, pero más lentitud por ruta de runtime.

La decisión final debería escribirse así: “frente a BF16/Q8, esta cuantización ahorra X GB, mantiene Y de calidad en nuestros casos, empeora Z, y aun así compensa porque la tarea tolera ese error”. Si no puedes completar esa frase, todavía no has elegido; solo has descargado un fichero.

Ejemplo cercano: si el modelo debe escribir primeras versiones de correos, Q4 puede ser suficiente. Si debe extraer importes exactos de contratos, Q4 puede fallar de forma cara. Si debe generar SQL, un pequeño error puede romper la consulta. La cuantización adecuada depende del coste del error.

El criterio de elección local

Antes de instalar nada, escribe la decisión como una matriz. No hace falta que sea perfecta; hace falta que obligue a pensar.

PreguntaSi la respuesta es síSi la respuesta es no
¿Necesito que los datos no salgan de mi máquina o red?Local gana peso.API puede ser más simple.
¿La tarea tolera algo menos de calidad?Cuantización agresiva puede valer.Baseline fuerte o API.
¿Necesito baja latencia interactiva?Mide TTFT y tokens/s local.Batch o API pueden bastar.
¿Tengo VRAM/RAM suficiente?Prueba Q5/Q8 o modelos mayores.Baja tamaño, baja contexto o usa cloud.
¿Necesito integrar en app?Ollama/LM Studio API/local server.UI puede ser suficiente para aprendizaje.
¿Necesito control fino de runtime?llama.cpp/vLLM/SGLang.Ollama o LM Studio simplifican.
¿Puedo mantener actualizaciones?Local es viable.API gestionada reduce carga.

Fíjate en que ninguna pregunta dice “¿qué modelo está de moda?”. El orden correcto es restricción, memoria, calidad, latencia, integración y mantenimiento.

Qué ocurre cuando cargas un modelo local

“Cargar un modelo” no es abrir un archivo. Es convertir un conjunto de ficheros en un proceso de inferencia que ocupa memoria, reserva contexto, aplica una plantilla de chat y queda disponible para recibir peticiones.

El recorrido real suele ser este:

PasoQué pasaQué puede fallar
1. Resolver el identificadorgemma3, llama3.2, un GGUF concreto o un modelo importado se traducen a ficheros locales.Creer que dos nombres parecidos son el mismo modelo.
2. Leer metadatosEl runtime mira arquitectura, tokenizer, cuantización, contexto y plantilla.Usar plantilla o tokenizer incorrectos.
3. Mapear pesosLos tensores se leen desde disco y se preparan para CPU, GPU o memoria unificada.El fichero cabe en disco, pero no en memoria.
4. Decidir offloadAlgunas capas o cálculos pasan a GPU si hay VRAM o memoria unificada suficiente.Parte cae a CPU y la latencia se dispara.
5. Reservar KV cacheEl runtime reserva memoria para claves y valores de atención según contexto.Subir contexto llena memoria aunque el modelo pese lo mismo.
6. Aplicar plantillaLos mensajes system, user y assistant se convierten a tokens con el formato esperado.El modelo “parece malo” porque se le habla con formato equivocado.
7. Generar tokensEl modelo predice token a token usando sampling, stops, temperatura y límites.La salida no respeta formato, tarda demasiado o consume más memoria de la prevista.

En Ollama, el servidor local expone una API en localhost:11434 y permite comprobar modelos cargados con ollama ps. En LM Studio, la app permite cargar desde interfaz y el CLI lms permite listar, cargar, descargar, iniciar servidor y ver qué está en memoria. En ambos casos hay una idea común: descargar no es cargar, cargar no es evaluar, evaluar no es integrar.

Hardware y dependencias que sí importan

El hardware no se resume en “tengo GPU”. Para modelos locales, importan memoria, ancho de banda, drivers, disco, sistema operativo y puerto de servicio.

RecursoQué mirarLectura práctica
DiscoEspacio para modelos, duplicados, cachés y versiones.Un proyecto local puede ocupar decenas o cientos de GB. No lo metas sin pensar en el disco del sistema.
RAMMemoria principal para CPU, buffers, runtime y partes no aceleradas.Si no hay margen, el sistema intercambia memoria y todo parece roto.
VRAM o memoria unificadaPesos, KV cache y buffers en GPU o Apple Silicon.Un modelo Q4 pequeño puede ir fluido; uno grande con contexto alto puede dejar de caber.
CPUFallback y preparación de datos.Sirve para ejecutar, pero puede ser demasiado lento para uso interactivo.
GPU y driversNVIDIA, AMD, Metal, ROCm, Vulkan o CPU.Asegura soporte antes de prometer latencia. Ollama documenta NVIDIA, AMD, Metal y Vulkan.
ContextoTokens máximos disponibles en memoria.Más contexto aumenta memoria; no es una barra “gratis”.
Puerto local11434 para Ollama, 1234 habitual en LM Studio.Revisa si escucha solo en localhost o si lo expones en red.
Herramientas de trabajoTerminal, curl, Python 3, runtime elegido y drivers.Sin medición por terminal, todo queda en sensación visual.

Dónde se guardan los modelos también importa. Ollama documenta rutas por defecto: macOS usa ~/.ollama/models, Linux usa /usr/share/ollama/.ollama/models y Windows usa C:\Users\%username%\.ollama\models; si necesitas moverlos, OLLAMA_MODELS define otra ubicación. LM Studio lo gestiona desde “My Models” y lms ls refleja el directorio de modelos configurado en la app.

Una instalación local mínima tiene estas dependencias conceptuales:

DependenciaPara qué sirveSeñal de que está bien
RuntimeCargar y ejecutar el modelo.ollama -v o lms --help responden.
Modelo descargadoTener pesos reales en disco.ollama list o lms ls muestran el modelo.
Modelo cargadoOcupar memoria para inferencia.ollama ps o lms ps muestran un modelo activo.
API localIntegrar con scripts o apps.curl localhost:11434 o curl localhost:1234 responde.
Driver GPUAcelerar inferencia.ollama ps indica GPU o lms load --estimate-only estima uso razonable.
Prueba propiaVer si el sistema sirve para tu tarea.Tienes métricas de latencia, memoria y formato válido.

Mi recomendación para clase o primer montaje: empieza con un modelo pequeño o mediano, una cuantización conservadora (Q4_K_M, Q5_K_M o equivalente), contexto 4096 u 8192, y una tarea concreta. Luego sube tamaño o contexto solo si puedes explicar qué ganaste.

Mapa visual del sistema local

Arquitectura completa de un despliegue local Modelo local: de fichero descargado a decisión medible La figura separa lo que eliges, lo que cabe, lo que ejecuta y lo que demuestras con una eval. 1 expediente 2 pesos 3 cuantización 4 runtime 5 eval Expediente antes de cargar model card licencia · tarea contexto · tokenizer benchmarks · versión identidad reproducible repo + fichero + hash + fecha Tensores del modelo BF16/F16, safetensors o GGUF no basta con el tamaño del fichero Cuantización por bloques x -> q -> x aproximado bloque 64 escala s 4/5/8 bits zero-point outliers GPTQ · AWQ · SmoothQuant · GGUF menor memoria, calidad a demostrar Runtime y API local Ollama LM Studio llama.cpp num_ctx · offload · template · sampling localhost: API, streaming y errores Presupuesto de memoria M_total = pesos + KV + runtime + margen pesos KV cache buffers margen subir contexto agranda la KV cache aunque el fichero pese igual Contrato de carga modelo: fichero exacto contexto: 4096 / 8192 / 32768 offload: CPU, GPU o mixto sampling: temperature, top_p, stops Banco de pruebas propio métrica baseline Q4/Q5 formato válido 99% 97% TTFT / tokens/s alto coste local memoria pico no cabe con margen Decisión documentada elige la menor cuantización que conserva calidad suficiente con memoria, latencia y contrato de salida aceptables modelo + fichero + cuantización + contexto + runtime + métricas + fecha de revisión IA para gente curiosa / Facsímil 04 / Capítulo 05 / 686f6c61

En el día a día

En un proyecto real, los modelos locales aparecen en cinco situaciones: prototipado rápido, privacidad, trabajo offline, coste por volumen y aprendizaje técnico. Cada una exige una lectura distinta.

Si prototipas, quieres fricción baja. Ollama o LM Studio te permiten probar rápido. Si el objetivo es privacidad, ya no basta con “local”: revisa qué app abre red, dónde quedan logs, qué permisos tiene el servidor y si el puerto escucha solo en localhost. Si el objetivo es coste, calcula coste de hardware y tiempo del equipo. Si el objetivo es aprendizaje, acepta modelos pequeños y mide para entender.

La señal de madurez no es correr el modelo más grande posible. Es poder decir: “en esta máquina, con este modelo, esta cuantización y este contexto, obtenemos esta calidad, este TTFT, estos tokens/s y esta memoria”.

Por qué debería importarte

Porque local cambia el equilibrio del sistema. En una API externa, pagas por uso y delegas hardware. En local, controlas pesos y datos, pero compras complejidad: memoria, runtime, temperatura, contexto, actualizaciones y fallos.

También importa porque los próximos capítulos dependen de esto. RAG local, embeddings, text-to-SQL, agentes y herramientas internas pueden apoyarse en modelos locales, pero solo si sabes cuándo local es suficiente y cuándo estás intentando ahorrar en el sitio equivocado.

Dónde volverá a aparecer

ConceptoDónde vuelvePara qué
Cloud frente a localCapítulo 06.Comparar privacidad, latencia, coste y operación.
Embeddings localesCapítulo 07.Ejecutar modelos de representación en tu máquina.
RAGCapítulos 09 y 10.Decidir si generación y recuperación corren local o por API.
Text-to-SQLCapítulo 12.Evaluar si un modelo local genera consultas fiables.
Laboratorio mínimoCapítulo 13.Registrar evals, trazas y métricas de cada candidato.
OperaciónFacsímil 6.Servir, monitorizar y actualizar modelos de forma responsable.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Confundir local con privado absolutoLocal reduce envío remoto, pero no elimina permisos, logs, red ni procedencia.Revisar puerto, logs, permisos, licencia y origen de pesos.
Elegir por tamaño descargadoEl fichero no incluye KV cache, runtime ni margen de memoria.Estimar pesos + KV + runtime + margen.
Subir contexto sin medirMás contexto puede llenar memoria y empeorar TTFT.Probar contexto real y mirar memoria durante la carga.
Comparar Q4 contra API sin baselineQuizá el fallo viene de la cuantización, no del modelo.Comparar contra Q8/BF16 o una API fuerte en los mismos casos.
Pensar que OpenAI-compatible significa igualPuede haber diferencias en herramientas, JSON, streaming, errores y parámetros.Ejecutar tests de contrato antes de cambiar backend.
No guardar configuraciónNo puedes reproducir un resultado si no sabes contexto, temperatura, offload y fichero.Guardar Modelfile, modelo exacto, cuantización y métricas.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

La práctica útil no es estimar de oído. La práctica útil es montar un sistema local pequeño, comprobar que el modelo se descarga, se carga, responde por API y devuelve una salida que una aplicación podría validar.

Paso 0: elegir ruta de montaje

Tienes dos rutas razonables para empezar:

RutaCuándo usarlaQué aprendes
OllamaQuieres CLI sencilla, API local rápida y un flujo fácil de automatizar.Modelo como servicio local en localhost:11434.
LM StudioQuieres UI, exploración de modelos, carga visual y servidor local.Modelo como app, servidor y CLI con lms.

No hace falta instalar las dos para aprender. Sí conviene conocer ambas porque aparecen mucho en equipos reales: una persona prototipa en LM Studio, otra integra con Ollama, y el problema profesional es que las dos decisiones sean trazables.

Paso 1: instalar y comprobar runtime

En macOS y Windows, Ollama se instala desde la app oficial. En Linux, la documentación oficial propone:

curl -fsSL https://ollama.com/install.sh | sh

Después comprueba que existe el binario y que el servidor responde:

ollama -v
ollama
curl http://localhost:11434/api/version

Para LM Studio, instala la app, ábrela una vez y comprueba el CLI:

lms --help
lms ls
lms ps

Si vas sin interfaz gráfica en Mac o Linux, LM Studio documenta instalación headless con:

curl -fsSL https://lmstudio.ai/install.sh | bash
lms daemon up

Paso 2: descargar y cargar un modelo pequeño

Empieza con un modelo pequeño o medio. No empieces por el más grande: primero quieres comprobar que el circuito funciona.

Con Ollama:

ollama pull gemma3
ollama run gemma3
ollama ps

Con LM Studio por CLI:

lms get <modelo>
lms load <modelo> --context-length=4096 --gpu=auto --identifier=local-lab
lms server start --port 1234
lms ps

Si usas la app de LM Studio, el equivalente es: buscar modelo, descargar, cargar en memoria, abrir el servidor local y fijarte en el identificador que usará la API.

Paso 3: registrar lo que montaste

Antes de programar, deja escrito esto:

CampoEjemploPor qué importa
RuntimeOllama o LM StudioCambia API, carga y memoria.
Versiónsalida de ollama -v o lms --helpUna actualización puede cambiar resultados.
Modelogemma3 o identificador exacto de LM StudioEs lo que llamarás desde código.
Fichero o varianteQ4, Q5, Q8, GGUF, MLX, safetensorsCambia memoria y calidad.
Contexto4096, 8192, 32768Cambia KV cache y latencia.
OffloadCPU, GPU, auto, maxCambia velocidad y consumo.
Puerto11434 o 1234Cambia integración y exposición local.

Paso 4: probar por API con código real

Este script no simula un modelo. Busca runtimes instalados, intenta llamar a Ollama y LM Studio, mide latencia, extrae el texto y comprueba si la respuesta es JSON válido. Si no tienes uno de los servidores levantado, te dice qué falta.

Guárdalo como local_llm_smoke_test.py y ejecútalo con python3 local_llm_smoke_test.py.

import json
import os
import platform
import shutil
import subprocess
import time
import urllib.error
import urllib.request


PROMPT = (
    "Devuelve solo JSON valido con estos campos: "
    "categoria, prioridad, siguiente_paso, confianza. "
    "Caso: un alumno no puede acceder al campus virtual antes de entregar una practica."
)


def command_exists(name):
    return shutil.which(name) is not None


def run_command(command):
    try:
        completed = subprocess.run(
            command,
            text=True,
            capture_output=True,
            timeout=8,
            check=False,
        )
        output = (completed.stdout or completed.stderr).strip()
        return completed.returncode, output[:600]
    except Exception as exc:
        return 1, str(exc)


def post_json(url, payload, token=None, timeout=60):
    body = json.dumps(payload).encode("utf-8")
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"

    request = urllib.request.Request(url, data=body, headers=headers, method="POST")
    started = time.perf_counter()
    with urllib.request.urlopen(request, timeout=timeout) as response:
        raw = response.read().decode("utf-8")
    elapsed = time.perf_counter() - started
    return json.loads(raw), elapsed


def parse_model_json(text):
    cleaned = text.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.strip("`").strip()
        if cleaned.startswith("json"):
            cleaned = cleaned[4:].strip()

    try:
        return json.loads(cleaned), True
    except json.JSONDecodeError:
        start = cleaned.find("{")
        end = cleaned.rfind("}")
        if start >= 0 and end > start:
            return json.loads(cleaned[start : end + 1]), False
        raise


def ollama_check():
    model = os.getenv("OLLAMA_MODEL", "gemma3")
    payload = {
        "model": model,
        "stream": False,
        "messages": [
            {
                "role": "system",
                "content": "Responde solo con JSON valido. Sin texto adicional.",
            },
            {"role": "user", "content": PROMPT},
        ],
        "options": {"temperature": 0.1, "num_ctx": 4096},
    }
    data, elapsed = post_json("http://localhost:11434/api/chat", payload)
    text = data["message"]["content"]
    parsed, exact = parse_model_json(text)
    return {
        "backend": "ollama",
        "model": model,
        "latency_s": round(elapsed, 3),
        "exact_json": exact,
        "parsed": parsed,
    }


def lm_studio_check():
    model = os.getenv("LMSTUDIO_MODEL", "local-lab")
    token = os.getenv("LM_API_TOKEN")
    payload = {
        "model": model,
        "messages": [
            {
                "role": "system",
                "content": "Responde solo con JSON valido. Sin texto adicional.",
            },
            {"role": "user", "content": PROMPT},
        ],
        "temperature": 0.1,
        "max_tokens": 220,
        "stream": False,
    }
    data, elapsed = post_json(
        "http://localhost:1234/v1/chat/completions",
        payload,
        token=token,
    )
    text = data["choices"][0]["message"]["content"]
    parsed, exact = parse_model_json(text)
    return {
        "backend": "lm-studio",
        "model": model,
        "latency_s": round(elapsed, 3),
        "exact_json": exact,
        "parsed": parsed,
    }


def print_preflight():
    print("sistema:", platform.platform())
    print("python:", platform.python_version())
    print("ollama_cli:", command_exists("ollama"))
    print("lms_cli:", command_exists("lms"))

    if command_exists("ollama"):
        code, output = run_command(["ollama", "ps"])
        print("ollama_ps:", code, output or "(sin modelos cargados)")

    if command_exists("lms"):
        code, output = run_command(["lms", "ps"])
        print("lms_ps:", code, output or "(sin modelos cargados)")


def try_backend(name, check):
    print(f"\n== {name} ==")
    try:
        result = check()
        print(json.dumps(result, ensure_ascii=False, indent=2))
    except urllib.error.URLError as exc:
        print("no responde la API local:", exc)
    except Exception as exc:
        print("la API respondio, pero la prueba no paso:", exc)


if __name__ == "__main__":
    print_preflight()
    try_backend("ollama", ollama_check)
    try_backend("lm-studio", lm_studio_check)

Una salida sana no es que el texto “suene bien”. Una salida sana es algo así:

ollama_cli: True
lms_cli: True

== ollama ==
{
  "backend": "ollama",
  "model": "gemma3",
  "latency_s": 1.842,
  "exact_json": true,
  "parsed": {
    "categoria": "acceso",
    "prioridad": "alta",
    "siguiente_paso": "Revisar credenciales y estado del campus virtual",
    "confianza": 0.82
  }
}

Si exact_json sale false, el modelo produjo texto extra y el script tuvo que rescatar el objeto. Eso no es un detalle menor: para una aplicación real, significa que tu contrato de salida todavía es débil. Puedes probar a bajar temperature, cambiar modelo, usar salida estructurada si el runtime la soporta o ajustar el prompt de sistema.

Paso 5: interpretar el resultado

Después de ejecutar la prueba, contesta:

PreguntaQué te dice
¿El servidor local responde sin exponer red externa?La integración básica está controlada.
¿El modelo aparece en ollama ps o lms ps?Está cargado en memoria, no solo descargado.
¿La latencia es tolerable para una persona?Sirve o no para interacción.
¿El JSON es exacto?Sirve o no para integración automática.
¿Qué contexto configuraste?Sabes cuánta KV cache estás provocando.
¿Qué cambiarías primero?Modelo, cuantización, contexto, prompt, runtime o hardware.

Este ejercicio deja una base real: un runtime local, un modelo cargado, una API comprobada, una métrica y un contrato de salida. A partir de ahí sí tiene sentido comparar cuantizaciones, subir contexto o pasar al capítulo de cloud frente a local.

Cómo encaja todo

Este mapa muestra dónde se coloca el capítulo dentro del facsímil. No intenta repetir todas las siglas; separa decisión, ejecución y evaluación.

graph TD
    subgraph "Capítulo 5: modelos locales"
        CARD["Model card"]
        WEIGHTS["Pesos descargables"]
        FORMAT["Formato GGUF o safetensors"]
        QUANT["Cuantización"]
        RUNTIME["Runtime local"]
        MEMORY["Memoria y contexto"]
        API["API local"]
        EVAL["Eval propia"]
        DECISION["Decisión local trazable"]
    end
    subgraph "Viene de antes"
        TOKENS["Tokens y KV cache (F4C3)"]
        MODELSEL["Elección de modelos (F4C4)"]
        INFER["Inferencia optimizada (F3C7)"]
    end
    subgraph "Sigue después"
        CLOUD["Cloud frente a local<br/>(F4C6)"]
        EMB["Embeddings (F4C7)"]
        RAG["RAG (F4C9-10)"]
        OPS["Operación (F6)"]
    end

    MODELSEL --> CARD
    CARD --> WEIGHTS
    WEIGHTS --> FORMAT
    FORMAT --> QUANT
    QUANT --> RUNTIME
    TOKENS --> MEMORY
    INFER --> MEMORY
    RUNTIME --> MEMORY
    RUNTIME --> API
    MEMORY --> EVAL
    API --> EVAL
    EVAL --> DECISION
    DECISION --> CLOUD
    DECISION --> EMB
    DECISION --> RAG
    DECISION --> OPS

    style CARD fill:#F5F5F5,stroke:#000000,stroke-width:2
    style WEIGHTS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style FORMAT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style QUANT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RUNTIME fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MEMORY fill:#F5F5F5,stroke:#000000,stroke-width:2
    style API fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EVAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style DECISION fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TOKENS stroke-dasharray: 5 5
    style MODELSEL stroke-dasharray: 5 5
    style INFER stroke-dasharray: 5 5
    style CLOUD stroke-dasharray: 5 5
    style EMB stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style OPS stroke-dasharray: 5 5

Vocabulario aprendido

TérminoDefinición
Modelo localModelo que se carga y ejecuta en una máquina controlada por ti.
Open weightsPesos descargables bajo licencia concreta.
RuntimePrograma que ejecuta la inferencia y gestiona memoria.
GGUFFormato que guarda tensores y metadatos para runtimes como llama.cpp.
GPU offloadMovimiento de capas o trabajo a GPU para acelerar inferencia.
VRAMMemoria de GPU usada por pesos, KV cache y buffers.
KV cacheMemoria que crece con contexto y generación autoregresiva.
CuantizaciónRepresentar pesos con menos bits para ahorrar memoria.
Escala de cuantizaciónFactor que convierte un entero pequeño en una aproximación del valor real.
Zero-pointDesplazamiento entero usado para representar el cero en cuantización asimétrica.
GranularidadTamaño del tensor, fila o bloque que comparte escala y metadatos.
PTQCuantización aplicada después de entrenar, sin reentrenar todo el modelo.
Q4_K_MVariante GGUF de 4 bits usada como equilibrio frecuente.
ModelfileReceta de Ollama para modelo, parámetros, plantilla y licencia.
Local APIEndpoint HTTP para llamar al modelo desde una app local.
TTFTTiempo hasta recibir el primer token.
Tokens por segundoVelocidad de generación durante la salida.

Antes de pasar página

  • ¿Puedo explicar por qué local no significa automáticamente privado, barato o mejor?
  • ¿Sé separar pesos, formato, runtime, hardware, contexto y API?
  • ¿Puedo estimar memoria de pesos con parámetros y bits?
  • ¿Sé por qué el contexto aumenta memoria aunque el fichero pese lo mismo?
  • ¿Distingo GGUF, safetensors, cuantización y arquitectura?
  • ¿Sé cuándo usar Ollama y cuándo LM Studio por su forma de trabajo?
  • ¿Sé dónde se guardan los modelos y cómo mover la ruta si hace falta?
  • ¿Puedo explicar qué mide ollama ps, lms ps o lms load --estimate-only?
  • ¿He levantado una API local y probado una petición real con curl o Python?
  • ¿Sé comparar Q4, Q5, Q8 y BF16 con una eval propia?
  • ¿He comprobado si la respuesta sirve para una aplicación, no solo para leerla?

En resumen

Idea fuerzaDetalle
Local es una pila completa.Pesos, formato, runtime, memoria, API, licencia y eval trabajan juntos.
El tamaño del fichero no basta.Hay que sumar KV cache, runtime, margen y contexto real.
GGUF es un contenedor operativo.Guarda tensores y metadatos para runtimes locales, no una garantía de calidad.
Cuantizar cambia el sistema.Puede ahorrar memoria y coste, pero debe medirse contra un baseline.
Ollama y LM Studio resuelven problemas distintos.Uno favorece flujo simple y API; el otro añade UI, gestión visual y servidor local.
Instalar no es integrar.Debes probar descarga, carga, API, latencia y contrato de salida.
La decisión local debe quedar escrita.Modelo, cuantización, contexto, hardware, métricas y alternativa.

Para saber más

Dettmers, T. et al. (2022). LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. https://doi.org/10.52202/068431-2198

Frantar, E. et al. (2022). GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. https://arxiv.org/abs/2210.17323

ggml-org. (2026). llama.cpp: LLM inference in C/C++. https://github.com/ggml-org/llama.cpp

Hugging Face. (2026). GGUF. https://huggingface.co/docs/hub/en/gguf

Jacob, B. et al. (2018). Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference. https://doi.org/10.1109/CVPR.2018.00286

Lin, J. et al. (2024). AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. https://arxiv.org/abs/2306.00978

Dettmers, T. et al. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. https://arxiv.org/abs/2305.14314

LM Studio. (2026). Configuring the Model. https://lmstudio.ai/docs/typescript/llm-prediction/parameters

LM Studio. (2026). Get started with LM Studio. https://lmstudio.ai/docs/app/basics

LM Studio. (2026). LM Studio API. https://lmstudio.ai/docs/developer/rest

LM Studio. (2026). LM Studio Developer Docs. https://lmstudio.ai/docs/developer

LM Studio. (2026). lms: LM Studio's CLI. https://lmstudio.ai/docs/cli

LM Studio. (2026). lms load. https://lmstudio.ai/docs/cli/local-models/load

Ollama. (2026). FAQ. https://docs.ollama.com/faq

Ollama. (2026). Linux. https://docs.ollama.com/linux

Ollama. (2026). macOS. https://docs.ollama.com/macos

Ollama. (2026). Quickstart. https://docs.ollama.com/quickstart

Ollama. (2026). Windows. https://docs.ollama.com/windows

Ollama. (2026). Context length. https://docs.ollama.com/context-length

Ollama. (2026). Hardware support. https://docs.ollama.com/gpu

Ollama. (2026). Introduction to the Ollama API. https://docs.ollama.com/api/introduction

Ollama. (2026). Modelfile Reference. https://docs.ollama.com/modelfile

Ollama. (2026). OpenAI compatibility. https://docs.ollama.com/api/openai-compatibility

Xiao, G. et al. (2023). SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. https://arxiv.org/abs/2211.10438

Notas

  1. Ollama. (2026). Introduction to the Ollama API. https://docs.ollama.com/api/introduction. Consultado el 10 de junio de 2026.

  2. Ollama. (2026). Modelfile Reference. https://docs.ollama.com/modelfile. Consultado el 10 de junio de 2026.

  3. Ollama. (2026). Context length. https://docs.ollama.com/context-length. Consultado el 10 de junio de 2026.

  4. Ollama. (2026). Hardware support. https://docs.ollama.com/gpu. Consultado el 10 de junio de 2026.

  5. Ollama. (2026). OpenAI compatibility. https://docs.ollama.com/api/openai-compatibility. Consultado el 10 de junio de 2026.

  6. LM Studio. (2026). Get started with LM Studio. https://lmstudio.ai/docs/app/basics. Consultado el 10 de junio de 2026.

  7. LM Studio. (2026). LM Studio API. https://lmstudio.ai/docs/developer/rest. Consultado el 10 de junio de 2026.

  8. LM Studio. (2026). lms load. https://lmstudio.ai/docs/cli/local-models/load. Consultado el 10 de junio de 2026.

  9. Hugging Face. (2026). GGUF. https://huggingface.co/docs/hub/en/gguf. Consultado el 10 de junio de 2026.

  10. ggml-org. (2026). llama.cpp: LLM inference in C/C++. https://github.com/ggml-org/llama.cpp. Consultado el 10 de junio de 2026.

  11. Dettmers, T., Lewis, M., Belkada, Y. y Zettlemoyer, L. (2022). LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. Advances in Neural Information Processing Systems 35. https://doi.org/10.52202/068431-2198.

  12. Xiao, G. et al. (2023). SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. Proceedings of ICML. https://arxiv.org/abs/2211.10438.

  13. Frantar, E., Ashkboos, S., Hoefler, T. y Alistarh, D. (2022). GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. https://arxiv.org/abs/2210.17323.

  14. Lin, J. et al. (2024). AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. Proceedings of Machine Learning and Systems. https://arxiv.org/abs/2306.00978.

  15. Dettmers, T., Pagnoni, A., Holtzman, A. y Zettlemoyer, L. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. Advances in Neural Information Processing Systems 36. https://arxiv.org/abs/2305.14314.

  16. Jacob, B. et al. (2018). Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference. Proceedings of CVPR, 2704-2713. https://doi.org/10.1109/CVPR.2018.00286.

  17. Ollama. (2026). Cloud. https://docs.ollama.com/cloud. Consultado el 10 de junio de 2026.

  18. LM Studio. (2026). Local Server. https://lmstudio.ai/docs/developer/core/server. Consultado el 10 de junio de 2026.

  19. ggml-org. (2026). llama-server. https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md. Consultado el 10 de junio de 2026.

Capítulo 06

Facsímil 4 · La caja de herramientas

Capítulo 06: Cloud frente a local: privacidad, latencia y coste

La decisión que no cabe en un eslogan

Después de montar un modelo local, aparece una tentación muy humana: convertirlo en bandera. “Local es privado”. “Cloud es escalable”. “Local es barato”. “Cloud es caro”. Ninguna de esas frases aguanta mucho si la llevas a producción.

Venimos del capítulo 05, donde vimos que un modelo local es una pila: pesos, runtime, memoria, API, configuración y evaluación. Ahora comparamos esa pila con una API gestionada o una plataforma cloud. No para escoger por ideología, sino para responder una pregunta de ingeniería: dónde debe ejecutarse esta inferencia, con estos datos, este volumen, esta latencia, este presupuesto y este nivel de operación.

La idea central es esta: local y cloud no son bandos; son posiciones distintas dentro de una arquitectura.

Estado del arte con fecha de corte

Fecha de corte: 10 de junio de 2026.
Fuentes consultadas ese día: documentación oficial de controles de datos y precios de OpenAI, Anthropic, Google Cloud Vertex AI y Amazon Bedrock; documentación oficial de latencia; y artículos académicos sobre latencia de cola y edge computing.

Lo estable es el método: mirar frontera de confianza, retención, región, latencia, coste total, elasticidad, operación y plan de salida. Lo cambiante son precios, modelos disponibles, regiones, controles de retención, límites de API, multiplicadores por residencia, descuentos y disponibilidad de hardware.

FuenteQué aportaCómo usarla
OpenAI data controls.1Explica uso de datos, retención por endpoint y controles como retención cero o monitorización modificada cuando aplica.Para no decir “API” sin revisar qué se guarda y durante cuánto tiempo.
OpenAI models API.2Devuelve modelos disponibles para una clave y metadatos básicos como id, created y owned_by.Para no usar identificadores copiados de ejemplos viejos.
OpenAI pricing.3Precios por tokens, cache, batch y modalidades.Para calcular coste por flujo real, no por intuición.
OpenAI latency optimization.4Criterios para reducir latencia de aplicaciones con modelos.Para separar tokens, streaming, modelo y arquitectura.
Anthropic data retention.5Diferencia arreglos de retención, alcance de ZDR y funciones que necesitan almacenamiento.Para preguntar qué modo contractual tienes, no qué marca usas.
Anthropic models API.6Lista modelos disponibles con paginación y fechas de creación.Para separar “familia Claude” de identificador concreto de API.
Anthropic pricing.7Precios por entrada, salida, cache y geografía.Para ver que cache y residencia también cambian coste.
Gemini models.8Lista modelos, modalidades y patrones de versión estable, preview, latest y experimental.Para fijar versiones estables y no depender de alias que cambian.
Vertex AI data governance.9Controles de gobernanza, retención y condiciones de servicios generativos.Para tratar cloud como contrato de datos, no solo endpoint.
Vertex AI pricing.10Precios por modelo, tokens, herramientas y modalidades.Para revisar token, imagen, vídeo, grounding y extras.
Amazon Bedrock data protection.11Modelo de responsabilidad compartida, cifrado, IAM, logs y separación con proveedores de modelos.Para entender qué controla AWS y qué sigues controlando tú.
Amazon Bedrock pricing.12Precios por modelo y modo de inferencia.Para comparar marketplaces, regiones y modalidades.
OpenRouter models API.13Expone modelos de muchos proveedores con context_length, modalidades, precios, parámetros soportados y enlaces a endpoints.Para construir inventario técnico de modelos cloud comparables.
OpenRouter routing.14Permite controlar proveedores, preferencias, rutas y sustitución cuando un proveedor no encaja.Para entender un gateway como capa de selección, no como “un modelo más”.
Ollama Cloud y API.15Mantiene herramientas locales mientras ejecuta modelos grandes en el servicio cloud de Ollama; la API local usa localhost:11434/api, cloud https://ollama.com/api y compatibilidad parcial OpenAI en /v1.Para distinguir “uso Ollama” de “la inferencia está en mi máquina”.
vLLM, llama.cpp, TGI y SGLang.16Runtimes para servir modelos propios con APIs compatibles, batching, plantillas de chat, GPU/CPU y opciones de producción.Para montar servidor local con parámetros, observabilidad y contrato, no solo “ejecutar un modelo”.
Alquiler de GPU.17Permite alquilar VM o instancia con acelerador, pagar por tiempo, reservar capacidad o usar capacidad con descuento bajo condiciones.Para separar coste de API de coste de infraestructura propia en cloud.
GPU clouds y VM GPU.18Documentan familias GPU, billing, almacenamiento, pods dedicados, contenedores y disponibilidad.Para entender que alquilar GPU incluye storage, imagen, datos, arranque y operación.
The Tail at Scale.19Explica por qué p95 y p99 importan más que el promedio en servicios interactivos.Para medir latencia como experiencia real, no media bonita.
Edge Computing: Vision and Challenges.20Sitúa edge/local como forma de acercar cómputo a datos y usuarios.Para entender local como ubicación arquitectónica, no como capricho.

La revisión del 10 de junio refuerza que cloud frente a local no se decide solo con precio por millón de tokens. OpenAI separa controles de datos, optimización de coste, optimización de latencia y selección de modelos; Anthropic documenta retención, residencia, uso/coste y límites de tasa; AWS Bedrock presenta la protección de datos desde responsabilidad compartida; y Vertex AI publica capacidades, release notes y SLA por servicio.21222324252627

La pregunta de ingeniería queda así: ¿puedo medir coste, latencia, retención, región, límites, errores y salida por proveedor con la misma rúbrica? Si no puedes, todavía no estás comparando local y cloud; estás comparando sensaciones. Un diseño serio incluye inventario de modelos, contrato de datos, presupuesto por flujo, p95/p99, plan de degradación, logs filtrados y salida de emergencia si un proveedor cambia modelo, precio o disponibilidad.

Qué no significa “privado”

Privado no significa “no usa internet”. Un portátil con un modelo local puede tener logs, copias de seguridad, extensiones, puertos abiertos, carpetas sincronizadas y usuarios con permisos amplios. Cloud no significa automáticamente “lo ve todo el proveedor”: puede haber contratos, controles de retención, cifrado, regiones, IAM, redes privadas y auditoría. La pregunta correcta no es si algo suena local o remoto; es dónde existe texto claro, quién puede acceder, cuánto tiempo queda guardado y qué contrato lo gobierna.

Tampoco conviene confundir privacidad con cumplimiento. Puedes proteger datos en local y aun así incumplir una política interna por falta de trazabilidad. Puedes usar cloud y cumplir mejor porque tienes auditoría, control de accesos y residencia definida. Depende del caso.

Y privacidad tampoco equivale a calidad. El modelo local más controlado puede no servir para la tarea. El modelo cloud más potente puede no encajar con los datos. La decisión empieza por la frontera de confianza, pero no termina ahí.

La frontera de confianza

Antes de hablar de coste, dibuja por dónde viajan los datos.

Lugar donde existe el datoPregunta que debes hacerSeñal de control
Navegador o app cliente¿El usuario escribe datos sensibles?Minimización antes de enviar.
Backend propio¿Qué se loguea antes de llamar al modelo?Logs filtrados, cifrado y permisos.
Proveedor cloud¿Qué retiene el endpoint usado?Contrato, región, DPA/BAA cuando aplique, controles de retención.
Runtime local¿Quién puede leer disco, memoria, logs y puerto?Usuario dedicado, permisos, localhost, rutas controladas.
Herramientas conectadas¿El modelo llama sistemas externos?Scopes mínimos, auditoría y validación de salida.
Caché o vector store¿Se guardan prompts, fragmentos o embeddings?TTL, borrado, cifrado y separación por cliente.

La frontera no es un punto; es una cadena. Si haces RAG local pero subes la respuesta completa a una API cloud para “mejorarla”, la frontera cambió. Si usas cloud pero anonimizas antes, haces hashing de identificadores y evitas enviar documentos completos, también cambió.

Latencia: no mires solo el promedio

Ejemplo de fórmula. La latencia total de una llamada a modelo puede descomponerse así:

Ltotal=Lred+Lcola+Lprefill(Tentrada)+Ldecode(Tsalida)+Lherramientas+LpostprocesoL_{\text{total}} = L_{\text{red}} + L_{\text{cola}} + L_{\text{prefill}}(T_{\text{entrada}}) + L_{\text{decode}}(T_{\text{salida}}) + L_{\text{herramientas}} + L_{\text{postproceso}}
TérminoQué significaQué cambia local/cloud
LredL_{\text{red}}Viaje entre cliente, backend y modelo.Local puede reducirlo; cloud depende de región y red.
LcolaL_{\text{cola}}Espera antes de ejecutar.Cloud puede absorber picos; local se satura antes.
LprefillL_{\text{prefill}}Procesar tokens de entrada.Crece con contexto. RAG y documentos largos lo disparan.
LdecodeL_{\text{decode}}Generar tokens de salida.Depende de modelo, hardware, cuantización y runtime.
LherramientasL_{\text{herramientas}}Consultas a bases de datos, APIs o buscadores.A veces domina más que el modelo.
LpostprocesoL_{\text{postproceso}}Validar JSON, guardar, renderizar o reintentar.Suele olvidarse en demos.

El promedio engaña. Si una app tiene 1000 usuarios y el 5 por ciento sufre esperas largas, ese 5 por ciento puede ser el que abandona. Dean y Barroso explican por qué la cola de latencias importa en sistemas a escala: no basta con que “normalmente vaya bien”.

MétricaQué mideDecisión
p50Experiencia típica.Sirve para ver sensación normal.
p95Casos lentos frecuentes.Útil para producto.
p99Cola larga.Útil para flujos críticos o muchos usuarios.
TTFTTiempo hasta ver el primer token.Mejora percepción si usas streaming.
tokens/sVelocidad de salida.Importa en respuestas largas.
timeoutsPeticiones que no terminan a tiempo.Marcan límites de arquitectura.

Local puede ganar si el usuario y los datos están cerca del modelo y el modelo cabe bien. Cloud puede ganar si necesita hardware optimizado, batch, escalado, modelos grandes, alta concurrencia o regiones específicas. La única forma seria de decidir es medir el mismo caso en ambas rutas.

Coste: token barato no significa sistema barato

Ejemplo de fórmula. La cuenta cloud mínima es:

Ccloud=N(Tentrada106Pentrada+Tsalida106Psalida)+Ccache+Cherramientas+Calmacenamiento+CobservabilidadC_{\text{cloud}} = N \cdot \left( \frac{T_{\text{entrada}}}{10^6} P_{\text{entrada}} + \frac{T_{\text{salida}}}{10^6} P_{\text{salida}} \right) + C_{\text{cache}} + C_{\text{herramientas}} + C_{\text{almacenamiento}} + C_{\text{observabilidad}}

Ejemplo de fórmula. La cuenta local mínima es:

Clocal=ChardwareMamortizacion+Cenergia+Coperacion+Cmantenimiento+CfallosC_{\text{local}} = \frac{C_{\text{hardware}}}{M_{\text{amortizacion}}} + C_{\text{energia}} + C_{\text{operacion}} + C_{\text{mantenimiento}} + C_{\text{fallos}}
CosteCloudLocal
TokensVisible y variable.No se paga por token, pero sí por capacidad.
HardwareIncluido en precio o instancia.Compra, alquiler o servidor propio.
PicosElasticidad bajo demanda.Necesitas sobredimensionar o aceptar cola.
MantenimientoDelegado en gran parte.Drivers, runtime, modelos, disco, seguridad, monitorización.
FallosSLA, regiones, límites y dependencia externa.Tú operas la pila.
PrivacidadContratos y controles del proveedor.Control físico/lógico, pero también responsabilidad propia.
Cambio de modeloMás fácil si la API lo ofrece.Depende de pesos, runtime y hardware disponible.

Ejemplo de fórmula. El punto de equilibrio aparece cuando ambas cuentas se igualan:

Nequilibrio=Clocal mensualTentrada106Pentrada+Tsalida106PsalidaN_{\text{equilibrio}} = \frac{C_{\text{local mensual}}} { \frac{T_{\text{entrada}}}{10^6} P_{\text{entrada}} + \frac{T_{\text{salida}}}{10^6} P_{\text{salida}} }

Ese número no decide solo, pero baja la conversación a tierra. Si necesitas 3000 peticiones al mes, quizá cloud sea más barato que comprar y mantener una GPU. Si necesitas 30 millones de peticiones homogéneas al mes, quizá local o infraestructura propia empiece a tener sentido. Si necesitas el mejor modelo para pocos casos delicados, cloud puede ser obvio aunque sea más caro por token.

Tres decisiones que se confunden

Cuando alguien dice “lo hacemos local o cloud”, en realidad hay tres decisiones mezcladas:

DecisiónOpcionesQué pregunta responde
Dónde corre la generaciónAPI cloud, servidor propio, portátil, edge, híbrido.¿Dónde se ejecuta el modelo que genera texto?
Dónde viven los datosBase propia, documentos locales, vector store cloud, almacenamiento regional.¿Dónde están los documentos antes y después de inferir?
Dónde se opera el productoApp local, backend propio, cloud gestionado, marketplace.¿Quién escala, observa, actualiza y responde cuando falla?

Puedes tener generación cloud con datos minimizados localmente. Puedes tener generación local con logs sincronizados a una nube corporativa. Puedes tener embeddings locales y generación cloud. Puedes tener RAG cloud y clasificación local. Lo profesional es nombrar la arquitectura exacta.

Estrategias reales de despliegue

No hay dos caminos; hay una familia de estrategias. La pregunta no es “¿cloud o local?”, sino qué capa quieres delegar y qué capa quieres controlar.

EstrategiaQué controlasQué delegasCuándo encajaCuidado principal
API directa a un laboratorioPrompt, contrato de salida, observabilidad de tu app.Modelo, servidor, escalado, optimizaciones de inferencia.Necesitas calidad alta, velocidad de desarrollo y modelos actuales.Dependencia de precios, límites, región y política de datos.
Plataforma cloud gestionadaRegión, IAM, redes, trazabilidad cloud, despliegue corporativo.Servir el modelo y actualizar infraestructura base.Empresa con cloud ya gobernada, auditoría y compras centralizadas.El contrato cloud no elimina tu responsabilidad de diseño.
Gateway de modelosUn endpoint común, selección de modelo, fallback, comparación rápida.Relación con muchos proveedores y normalización parcial de APIs.Quieres probar modelos o tener plan B sin reescribir toda la app.No todos los parámetros significan lo mismo en todos los modelos.
Ollama CloudHerramienta local, CLI/API Ollama, cambio suave desde modelos pequeños.Ejecución de modelos cloud de Ollama cuando no caben en tu equipo.Quieres seguir usando flujo Ollama con modelos más grandes.“Uso Ollama” no siempre significa “el cálculo ocurre localmente”.
Servidor propio de inferenciaRuntime, modelo exacto, hardware, red, logs, versionado y costes fijos.Poco: tú operas casi todo.Volumen estable, datos cerca, requisitos offline o control fino.Operación, capacidad, actualizaciones y degradación bajo carga.
Híbrida por flujoQué parte va local, qué parte va cloud, qué se cachea y qué se deriva.Solo las piezas elegidas.La mayoría de productos reales con distintas sensibilidades y costes.Sin reglas explícitas se convierte en una mezcla difícil de depurar.

OpenRouter entra en la tercera categoría: no es un modelo, es un router/gateway con una API compatible en la que eliges modelos de distintos proveedores. Su endpoint de modelos publica campos como id, context_length, modalidades, precios y parámetros soportados. Eso sirve para hacer inventario, pero no sustituye tu evaluación: dos modelos con la misma ventana de contexto pueden comportarse distinto con JSON, herramientas, español, razonamiento o latencia.

Ollama Cloud es otra cosa. Ollama puede seguir pareciendo local desde tu terminal, pero los modelos marcados como cloud se ejecutan en Ollama Cloud para poder usar modelos que no caben en tu GPU. Esto es cómodo para probar, pero cambia la frontera de confianza y el coste: la interfaz local no garantiza inferencia local.

Cómo saber qué modelos cloud tienes de verdad

Nunca elijas un modelo copiando un nombre de una entrada antigua, una captura o una demo. El identificador de modelo es una dependencia de producción. Tiene versión, fecha, capacidades, precio, modalidad, límites y, a veces, política de retirada.

Proveedor o gatewayCómo inventariarloQué mirar antes de usarlo
OpenAIGET https://api.openai.com/v1/models con tu clave.id, familia, endpoint soportado, precio actual, entrada multimodal, herramientas, structured outputs y modelo recomendado para tu tarea.
AnthropicGET https://api.anthropic.com/v1/models con anthropic-version.id, display_name, fecha de creación, ventana de contexto, coste, soporte de tools y modo de pensamiento si aplica.
Gemini APIPágina de modelos y API de listado cuando trabajas con clave.Si el nombre es estable, preview, latest o experimental; modalidades, herramientas, contexto, rate limits y fecha de retirada.
Bedrock o Vertex AICatálogo de modelos dentro de la región y cuenta.Modelo disponible por región, precio por modalidad, IAM, logging, residencia y quotas.
OpenRouterGET https://openrouter.ai/api/v1/models.id, proveedor, context_length, pricing, supported_parameters, modalidades y endpoints concretos.
Ollama localGET http://localhost:11434/api/tags o ollama list.Modelo descargado, tamaño, cuantización, fecha, template y si responde bien a tu contrato.
Ollama CloudCatálogo de modelos cloud y base URL https://ollama.com/api.Si el modelo se ejecuta cloud, cuenta/API key, precio, límites y qué datos salen de tu máquina.
Servidor local OpenAI-compatibleGET http://host:puerto/v1/models si el runtime lo expone.Nombre servido, plantilla de chat, límites de contexto, dtype, cuantización y parámetros aceptados.

Un inventario mínimo debería quedar así, aunque lo guardes en una hoja o en JSON:

CampoEjemploPor qué importa
provideropenai, anthropic, openrouter, local-vllmTe dice quién opera la inferencia.
model_idgpt-..., claude-..., meta-llama/...Es la dependencia exacta de código.
endpoint/v1/chat/completions, /v1/responses, /api/chatNo todos los modelos sirven en todos los endpoints.
context_tokens128000, 1000000, 4096Define cuánto texto puedes meter sin partir.
input_price y output_priceUSD por millón de tokensEl coste de salida suele ser más alto que el de entrada.
modalitiestexto, imagen, audio, embeddingsEvita elegir texto para un problema multimodal.
tools_json_schemasí/no/parcialAfecta agentes, validación y salidas estructuradas.
retention_regionpolítica y regiónAfecta cumplimiento y arquitectura.
version_policyestable, preview, latestAfecta reproducibilidad.
checked_at2026-06-10Hace explícito cuándo era verdad.

Comandos de inventario, no de producción:

# OpenAI: modelos accesibles por tu clave
curl https://api.openai.com/v1/models \
  -H "Authorization: Bearer $OPENAI_API_KEY"

# Anthropic: modelos accesibles por tu workspace
curl https://api.anthropic.com/v1/models \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01"

# OpenRouter: modelos, contexto, precios y parámetros soportados
curl https://openrouter.ai/api/v1/models \
  -H "Authorization: Bearer $OPENROUTER_API_KEY"

# Ollama local: modelos descargados en tu máquina
curl http://localhost:11434/api/tags

# Servidor local compatible con OpenAI, si lo expone
curl http://localhost:8000/v1/models \
  -H "Authorization: Bearer $LOCAL_API_KEY"

Lo importante es no mezclar “modelo que existe”, “modelo que mi cuenta puede usar”, “modelo que mi endpoint acepta” y “modelo que mi evaluación aprueba”. Son cuatro filtros distintos.

Servir inferencia local en serio

Bajar un GGUF, escribir ollama run y ver una respuesta es una prueba de vida. Un servidor de inferencia es otra cosa: es una pieza de infraestructura que debe cargar pesos, reservar memoria, aceptar peticiones, encolar trabajo, generar tokens, devolver errores comprensibles, medir latencia y sobrevivir a usos repetidos.

Ejemplo de fórmula. La memoria mínima no es solo el tamaño del archivo:

memoriatotalpesoscuantizados+KV cache(L,T,B)+runtime+margen\text{memoria}_{\text{total}} \approx \text{pesos}_{\text{cuantizados}} + \text{KV cache}(L, T, B) + \text{runtime} + \text{margen}

Donde LL son capas, TT tokens de contexto, BB peticiones simultáneas y la KV cache guarda claves y valores de atención para no recalcular todo en cada token. Por eso un modelo puede “caber” con una conversación corta y romperse cuando subes contexto, batch o concurrencia. En MoE tampoco basta mirar parámetros totales: importan parámetros activados por token, memoria de pesos, comunicación entre GPUs y eficiencia del runtime.

CapaDecisión técnicaQué mirar
HardwareCPU, GPU, VRAM, RAM, disco NVMe, red.VRAM útil, ancho de banda, drivers, consumo, refrigeración y margen.
ArtefactoGGUF, safetensors, AWQ/GPTQ/FP8/BF16, revisión exacta.Licencia, checksum, tokenizer, chat template y versión fija.
RuntimeOllama, llama.cpp, vLLM, SGLang, TGI, LM Studio.Batching, KV cache, cuantización, multi-GPU, tools, JSON y compatibilidad OpenAI.
Configuraciónmax_model_len, dtype, batch, concurrencia, tensor parallel, cache.Que el límite declarado sea sostenible con tus usuarios reales.
API/v1/chat/completions, /v1/embeddings, /api/chat, streaming.Contrato estable, errores, timeouts y parámetros aceptados.
EntradaPlantilla de chat, system prompt, roles, documentos, imágenes.Si la plantilla es incorrecta, el modelo parece peor de lo que es.
Operaciónproceso, reinicio, logs, métricas, health checks, despliegue.p50/p95/p99, TTFT, tokens/s, cola, VRAM, errores de JSON y coste eléctrico.
Accesobind de red, autenticación, TLS, rate limits, CORS.No publiques 0.0.0.0 sin proxy, clave, límites y logs útiles.
Evolucióncanary, rollback, evals, cambio de modelo.Cambiar cuantización o template puede cambiar respuestas aunque el nombre parezca igual.

Runtimes habituales:

RuntimeMejor paraPuntos técnicos
OllamaDesarrollo local, demos, herramientas personales, API sencilla.Muy cómodo; distingue local de cloud cuando uses modelos cloud.
LM StudioExploración visual, pruebas con modelos descargados, endpoint local.Bueno para aprender; no lo confundas con una plataforma multiusuario.
llama.cpp llama-serverGGUF, CPU/edge, GPU modesta, despliegues ligeros.Expone servidor HTTP compatible, opciones de host/puerto, GPU offload y endpoints de chat/embeddings.
vLLMAlto throughput en GPU, servicio multiusuario, OpenAI-compatible.Continuous batching, KV cache eficiente, tensor parallel, cuantización y --api-key.
SGLangBaja latencia, alto throughput, modelos grandes/multimodales.Runtime optimizado, OpenAI API, RadixAttention, prefix caching y gateway.
Hugging Face TGIServir modelos HF con API REST y Messages API.Streaming, tensor parallel, Prometheus/Grafana, despliegue cloud o propio.

Un arranque local mínimo con vLLM no debería terminar en “funciona”. Debería fijar modelo, dtype, contexto, nombre servido y clave:

python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen3-8B-Instruct \
  --served-model-name local-qwen3-8b \
  --host 127.0.0.1 \
  --port 8000 \
  --dtype auto \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.86 \
  --api-key "$LOCAL_API_KEY"

Y una prueba de vida técnica debería medir contrato, streaming y coste aproximado de tokens, no solo leer una respuesta bonita:

URL="http://127.0.0.1:8000/v1"

curl "$URL/chat/completions" \
  -H "Authorization: Bearer $LOCAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "local-qwen3-8b",
    "messages": [
      {"role": "system", "content": "Devuelve solo JSON valido."},
      {"role": "user", "content": "Clasifica: acceso al campus virtual."}
    ],
    "temperature": 0.1,
    "max_tokens": 160,
    "stream": false
  }'

Si esa llamada falla, no has “fallado con IA”; has descubierto una capa concreta: modelo no cargado, plantilla incorrecta, falta de VRAM, endpoint mal expuesto, clave ausente, límite de contexto, JSON inestable o timeout. Eso es mucho más útil que una demo.

Alquilar GPUs para inferencia

Entre una API gestionada y una GPU en tu mesa existe una opción muy usada: alquilar GPU en cloud y servir tú el modelo. Es cloud en infraestructura, pero local en responsabilidad técnica: eliges artefacto, runtime, contenedor, plantilla, métricas, límites y política de despliegue.

Hay tres formas habituales:

FormaQué alquilasQué controlasCuándo encajaRiesgo principal
VM GPU clásicaUna máquina con GPU, CPU, RAM, disco y red.Sistema operativo, Docker, runtime, modelo, API y logs.Servicio propio estable, pruebas serias, fine-tuning ligero o inferencia dedicada.Pagas mientras está asignada, aunque esté esperando peticiones.
Pod GPU especializadoUna instancia GPU más directa, a menudo con plantillas o contenedor propio.Imagen, volumen, librerías, runtime y arranque.Probar modelos abiertos, levantar endpoints rápidos, trabajos por horas.Disponibilidad por región/GPU y coste de almacenamiento persistente.
Serverless GPU o endpoint dedicadoWorkers que arrancan bajo demanda o endpoint gestionado con GPU detrás.Menos infraestructura; normalmente controlas imagen y escalado.Tráfico irregular, demos, colas batch, picos previsibles.Cold start, límites de ejecución, cola y menor control fino.

AWS EC2, Google Compute Engine y Azure permiten VMs con familias aceleradas por GPU. Runpod y proveedores similares simplifican el alquiler de pods con GPU y contenedores propios. Esto no sustituye a Bedrock, Vertex AI u OpenAI: es otra capa. En vez de comprar tokens de un modelo servido por otro, alquilas capacidad y corres tu pila de inferencia.

Ejemplo de fórmula. El coste mensual de una GPU alquilada se parece más a esto:

Cgpu=HactivaPhora+HidlePhora+Cvolumen+Cegress+Cimagenes+CoperacionC_{\text{gpu}} = H_{\text{activa}} P_{\text{hora}} + H_{\text{idle}} P_{\text{hora}} + C_{\text{volumen}} + C_{\text{egress}} + C_{\text{imagenes}} + C_{\text{operacion}}

Ejemplo de fórmula. Y el coste aproximado por millón de tokens generados se estima así:

C1MPhora3600Rtokens/sU106C_{1M} \approx \frac{P_{\text{hora}}}{3600 \cdot R_{\text{tokens/s}} \cdot U} \cdot 10^6

Donde Rtokens/sR_{\text{tokens/s}} es la velocidad útil del servidor y UU es la utilización real. Si la GPU solo está ocupada el 10 por ciento del tiempo, el coste efectivo por token se multiplica. Este es el punto que más se olvida: una GPU barata por hora puede ser cara por token si está casi vacía.

Para inferencia, la GPU no se elige por nombre bonito:

NecesidadGPU o acelerador típicoPor qué
Modelo 7B-13B cuantizado, baja concurrenciaL4, A10, RTX 4090/5090, RTX 6000, T4 si aceptas menor margen.Suele bastar para pilotos, herramientas internas y modelos pequeños.
Modelo 30B-70B, contexto mayor o más usuariosA100 80GB, H100, H200, B200 o multi-GPU.Más VRAM, ancho de banda y margen para KV cache.
Inferencia muy optimizada en AWSInf2 u otros aceleradores específicos.Pueden ser eficientes, pero exigen stack y compilación propios.
Tráfico irregularServerless GPU o workers autoscalados.Pagas menos espera, pero introduces arranque en frío.
Tráfico estableGPU dedicada con reserva o compromiso.Mejora disponibilidad y coste si sabes que la usarás muchas horas.

El tamaño de modelo no basta. Para servir bien necesitas medir:

SeñalQué mideDecisión
VRAM libre tras cargar pesosMargen para KV cache y batch.Si queda poco margen, baja contexto, cuantización o concurrencia.
TTFTTiempo hasta primer token.Si es alto, revisa cola, prefill, cold start o modelo demasiado grande.
tokens/s por peticiónVelocidad de generación.Compara runtime, cuantización y GPU.
throughput agregadoTokens/s o peticiones/s con concurrencia.Sirve para decidir batch, réplicas y autoscaling.
utilización de GPUPorcentaje real de uso.Si es baja, estás pagando idle; si es alta, aparecerá cola.
errores por memoriaPeticiones que fallan por VRAM/contexto.Define límites de entrada y concurrencia.
tiempo de arranqueDescargar imagen, montar volumen, cargar pesos.Crítico en serverless y pods efímeros.

Checklist mínimo antes de usar GPU alquilada para una API de inferencia:

  • Imagen Docker reproducible con CUDA, runtime, versión de Python y dependencias fijadas.
  • Modelo y tokenizer en volumen persistente o cache precalentada; no descargar 80 GB en cada arranque.
  • Health check que no diga “vivo” hasta haber cargado el modelo.
  • Warmup con una petición corta para inicializar kernels, plantilla y cache.
  • Límite de contexto y max_tokens por endpoint, no solo por buena voluntad del cliente.
  • Autenticación delante del runtime, aunque sea interno.
  • Métricas de p50, p95, p99, TTFT, tokens/s, cola, VRAM y errores.
  • Política de apagado: deallocated/delete cuando no se usa; “stopped” no siempre significa coste cero según proveedor.
  • Plan de fallback: otro modelo, otra región, API gestionada o cola de espera.

La regla práctica: si vas a usar una GPU alquilada como API, trátala como producto en producción desde el minuto uno. Si solo la enciendes para experimentar, trátala como laboratorio caro y pon alarma de apagado.

Cuándo elegir cada ruta

SituaciónRuta probablePor qué
Prototipo rápido con usuarios internosCloud o LM Studio local.Aprendes rápido sin comprar infraestructura.
Datos sensibles y flujo simpleLocal o cloud con controles contractuales fuertes.La decisión depende de política, no de marca.
Mucha concurrencia variableCloud.La elasticidad suele compensar.
Volumen estable y tarea acotadaLocal/propio puede competir.Puedes amortizar hardware y optimizar.
Necesitas modelo fronteraCloud.Los mejores modelos no suelen estar todos como pesos descargables.
Offline, aula, demo, entorno cerradoLocal.Funciona sin depender de red externa.
Latencia de milisegundos cerca del usuarioLocal/edge si el modelo cabe.La distancia de red importa.
Cumplimiento con región definidaCloud regional o local controlado.Se decide por residencia, auditoría y contrato.
Comparar muchos modelos sin reescribir clientesGateway como OpenRouter.Normaliza entrada y permite inventariar precios, contexto y parámetros.
Usar herramientas Ollama con modelos que no cabenOllama Cloud.Mantienes flujo Ollama pero cambias la ubicación real de inferencia.
Servir modelos abiertos con control técnicoGPU alquilada con vLLM, SGLang, TGI o llama.cpp.Controlas artefacto, runtime y API sin comprar hardware.
Tráfico con picos y largas pausasServerless GPU o endpoint autoscalado.Puede reducir idle, a cambio de cold start y menos control fino.

La ruta híbrida es común: clasificación local, RAG con datos propios, generación cloud para casos difíciles, cache de respuestas frecuentes y fallback local si la API externa no está disponible. Híbrido no significa improvisado; significa que cada pieza tiene una razón.

Mapa visual de decisión

Cloud frente a local: arquitectura técnica de decisión Decidir inferencia es diseñar dos planos: datos y operación El destino del modelo sale de políticas, presupuesto, latencia, evaluación, capacidad y salida de emergencia. Plano de datos entrada texto · imagen · docs minimización PII · secretos · logs contrato interno schema · tools · límites traza id · tenant · región gate permitir · derivar Policy router de inferencia No elige una marca: evalúa restricciones por petición y por producto. data_class pública · interna latencia p95 TTFT · tokens/s coste token · idle eval_score JSON · calidad capacidad GPU · cuota fallback cola · modelo B Modelo de coste cloud: tokens + cache + tools local: amortización + operación GPU: hora + idle + storage C_total = variable + fijo + riesgo C_1M ≈ P_hora/(tokens/s · U) si U baja, el token real sube Modelo de latencia red + cola + prefill + decode tools + postproceso + retries p50 no decide producto L_total = L_red + L_cola + ... medir p95, p99, TTFT, timeout colas largas rompen UX Destinos de inferencia API directa modelo frontera · SLA tokens · región · retención Gateway OpenRouter · fallback proveedor real trazado Ollama Cloud CLI local · ejecución cloud cambia frontera de datos GPU alquilada vLLM · SGLang · TGI idle · cold start · storage Servidor local llama.cpp · Ollama · LAN VRAM · permisos · backup Plano de operación inventario modelos eval suite canary/rollback cost ledger retención/logs alertas p95/p99 runbook plan de salida IA para gente curiosa / Facsímil 04 / Capítulo 06 / 686f6c61

En el día a día

Imagina una universidad que quiere clasificar incidencias de estudiantes. Si el texto contiene solo categorías generales, una API cloud puede dar calidad alta y mantenimiento bajo. Si el texto incluye expedientes, datos médicos o información contractual, quizá convenga anonimizar, resumir localmente o ejecutar todo en un entorno controlado.

Imagina una asesoría que analiza miles de facturas al mes. Si el volumen es bajo, una API potente evita comprar hardware. Si el volumen es constante, el modelo local puede ahorrar coste. Pero si cada error de extracción cuesta una llamada humana, el coste real no está en tokens: está en fallos.

Imagina una app de escritorio que debe funcionar en una fábrica sin red estable. Local gana por disponibilidad. Pero si el modelo debe razonar sobre documentos complejos y actualizados, quizá necesites sincronizar, cachear o derivar algunos casos a cloud. La arquitectura buena suele ser menos épica y más concreta.

Por qué debería importarte

Porque la elección local/cloud afecta a producto, privacidad, presupuesto, operación y experiencia de usuario. No es una decisión que pueda tomar solo compras, solo legal o solo ingeniería. Hay que poner a todos mirando la misma tabla.

También importa porque los próximos capítulos del facsímil se apoyan en esta decisión. Los embeddings, las bases vectoriales, el RAG y las herramientas de datos pueden correr local, cloud o híbrido. Si no sabes elegir ubicación, cada capítulo posterior se convierte en una colección de piezas sin arquitectura.

Dónde volverá a aparecer

ConceptoDónde vuelvePara qué
Embeddings locales o gestionadosCapítulo 07.Decidir dónde convertir texto en vectores.
Bases vectorialesCapítulo 08.Elegir almacenamiento local, servicio gestionado o híbrido.
RAGCapítulos 09 y 10.Separar documentos, recuperación y generación.
Agentes con herramientasFacsímil 05.Decidir qué herramientas pueden llamarse y desde dónde.
OperaciónFacsímil 06.Monitorizar coste, latencia, errores y cambios de modelo.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Decir “local” como sinónimo de seguroLocal también tiene logs, backups, puertos y permisos.Dibujar la frontera de confianza completa.
Decir “cloud” como sinónimo de caroPocas peticiones o modelos frontera pueden ser más baratos por API que operar hardware.Calcular coste mensual, no coste emocional.
Comparar latencia mediaEl promedio oculta esperas largas.Medir p50, p95, p99 y timeouts.
Olvidar el coste de operación localDrivers, actualizaciones, disco, monitorización y guardias cuestan tiempo.Incluir horas humanas y mantenimiento en el TCO.
No mirar la retención por endpointDistintas funciones pueden guardar estado de forma distinta.Revisar documentación y contrato antes de enviar datos reales.
Construir sin plan de salidaCambiar de proveedor o runtime tarde puede doler mucho.Mantener pruebas comunes y contrato de API propio.
Creer que un gateway es un modeloOpenRouter, por ejemplo, puede enrutar a modelos y proveedores distintos.Guardar modelo, proveedor, endpoint, precio, fecha y parámetros soportados.
Confundir Ollama con inferencia local siempreOllama Cloud mantiene la experiencia de herramienta local, pero ejecuta modelos cloud.Mirar si el modelo está descargado localmente o se ejecuta en cloud.
Montar servidor local sin contrato operativoUna respuesta en terminal no mide colas, contexto, JSON, p95 ni reinicios.Definir runtime, API, clave, métricas, límites, salud y rollback.
Alquilar GPU y olvidar el idleUna GPU por hora puede salir cara si espera vacía.Calcular coste por token útil con utilización real.
Confundir stopped con deallocatedEn algunos clouds parar una VM no libera todo el coste asignado.Revisar estado facturable, discos, IPs, snapshots y volúmenes.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a comparar dos backends con el mismo caso: uno local y uno cloud, ambos con contrato OpenAI-compatible cuando sea posible. No hace falta que tengas todos para aprender. Lo importante es repetir el mismo caso, registrar el endpoint exacto, guardar fecha de comprobación y no mezclar resultados de modelos distintos.

Primero decide qué backend vas a probar:

BackendBASE_URL típicoModelo de ejemploQué estás midiendo
LM Studio localhttp://localhost:1234/v1alias cargado en LM StudioModelo local con interfaz cómoda.
Ollama local OpenAI-compatiblehttp://localhost:11434/v1gemma3 o qwen3:8bModelo descargado y servido por tu máquina.
vLLM localhttp://localhost:8000/v1local-qwen3-8bServidor de inferencia multiusuario más cercano a producción.
llama.cpp localhttp://localhost:8080/v1nombre servido por llama-serverGGUF ligero, CPU/edge o GPU modesta.
OpenAIhttps://api.openai.com/v1modelo disponible por /v1/modelsAPI directa de laboratorio.
OpenRouterhttps://openrouter.ai/api/v1proveedor/modeloGateway con varios proveedores detrás.

Si usas servidor local, arranca una opción concreta y anota parámetros. No basta con “modelo cargado”; necesitas saber contexto, cuantización, nombre servido y clave.

# LM Studio
lms load <modelo> --context-length=4096 --gpu=auto --identifier=local-lab
lms server start --port 1234

# Ollama local compatible con partes de OpenAI
ollama pull gemma3
ollama serve

# vLLM local con contrato OpenAI-compatible
python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen3-8B-Instruct \
  --served-model-name local-qwen3-8b \
  --host 127.0.0.1 \
  --port 8000 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.86 \
  --api-key "$LOCAL_API_KEY"

# llama.cpp con GGUF
llama-server \
  -m ./models/modelo.gguf \
  --host 127.0.0.1 \
  --port 8080

Luego configura variables. Ajusta precios con la tabla oficial del proveedor el día de la prueba. Para local puedes poner precio por token a cero y un coste fijo mensual estimado; para cloud, usa precio de entrada y salida por millón de tokens.

export LOCAL_BASE_URL="http://localhost:8000/v1"
export LOCAL_API_KEY="local-dev-key"
export LOCAL_MODEL="local-qwen3-8b"
export LOCAL_INPUT_USD_PER_MTOK="0"
export LOCAL_OUTPUT_USD_PER_MTOK="0"
export LOCAL_FIXED_MONTHLY_USD="80"

export CLOUD_BASE_URL="https://api.openai.com/v1"
export CLOUD_API_KEY="..."
export CLOUD_MODEL="modelo-cloud"
export CLOUD_INPUT_USD_PER_MTOK="1.25"
export CLOUD_OUTPUT_USD_PER_MTOK="10"

Para OpenRouter cambia solo la base, la clave, el modelo y los precios:

export CLOUD_BASE_URL="https://openrouter.ai/api/v1"
export CLOUD_API_KEY="$OPENROUTER_API_KEY"
export CLOUD_MODEL="<id_devuelto_por_openrouter>"
# OpenRouter publica pricing.prompt y pricing.completion por token.
# Para este script multiplícalos por 1_000_000.
export CLOUD_INPUT_USD_PER_MTOK="<pricing.prompt * 1000000>"
export CLOUD_OUTPUT_USD_PER_MTOK="<pricing.completion * 1000000>"

Para Ollama local compatible con OpenAI:

export LOCAL_BASE_URL="http://localhost:11434/v1"
export LOCAL_API_KEY="ollama"
export LOCAL_MODEL="gemma3"

Guarda esto como comparar_local_cloud.py:

import json
import os
import time
import urllib.error
import urllib.request


PROMPT = (
    "Devuelve solo JSON valido con categoria, prioridad, "
    "siguiente_paso y confianza. "
    "Caso: clasificar 1200 incidencias mensuales con datos internos."
)


def env_float(name, default):
    try:
        return float(os.getenv(name, default))
    except ValueError:
        return float(default)


def post_chat(base_url, api_key, model):
    payload = {
        "model": model,
        "messages": [
            {
                "role": "system",
                "content": "Responde solo JSON valido.",
            },
            {"role": "user", "content": PROMPT},
        ],
        "temperature": 0.1,
        "max_tokens": 220,
        "stream": False,
    }
    headers = {"Content-Type": "application/json"}
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"

    request = urllib.request.Request(
        base_url.rstrip("/") + "/chat/completions",
        data=json.dumps(payload).encode("utf-8"),
        headers=headers,
        method="POST",
    )
    started = time.perf_counter()
    with urllib.request.urlopen(request, timeout=90) as response:
        data = json.loads(response.read().decode("utf-8"))
    elapsed = time.perf_counter() - started
    return data, elapsed


def extract_text(data):
    return data["choices"][0]["message"]["content"]


def usage_tokens(data, fallback_input=80, fallback_output=80):
    usage = data.get("usage") or {}
    input_tokens = (
        usage.get("prompt_tokens")
        or usage.get("input_tokens")
        or fallback_input
    )
    output_tokens = (
        usage.get("completion_tokens")
        or usage.get("output_tokens")
        or fallback_output
    )
    return int(input_tokens), int(output_tokens)


def parse_json(text):
    cleaned = text.strip()
    try:
        return json.loads(cleaned), True
    except json.JSONDecodeError:
        start = cleaned.find("{")
        end = cleaned.rfind("}")
        if start >= 0 and end > start:
            return json.loads(cleaned[start : end + 1]), False
        raise


def estimate_request_cost(
    input_tokens,
    output_tokens,
    input_price,
    output_price,
):
    return (input_tokens / 1_000_000 * input_price) + (
        output_tokens / 1_000_000 * output_price
    )


def measure_backend(prefix):
    base_url = os.getenv(f"{prefix}_BASE_URL")
    model = os.getenv(f"{prefix}_MODEL")
    api_key = os.getenv(f"{prefix}_API_KEY", "")

    if not base_url or not model:
        return {"backend": prefix.lower(), "status": "skipped"}

    input_price = env_float(f"{prefix}_INPUT_USD_PER_MTOK", "0")
    output_price = env_float(f"{prefix}_OUTPUT_USD_PER_MTOK", "0")

    data, elapsed = post_chat(base_url, api_key, model)
    text = extract_text(data)
    parsed, exact_json = parse_json(text)
    input_tokens, output_tokens = usage_tokens(data)
    request_cost = estimate_request_cost(
        input_tokens,
        output_tokens,
        input_price,
        output_price,
    )

    return {
        "backend": prefix.lower(),
        "status": "ok",
        "model": model,
        "latency_s": round(elapsed, 3),
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
        "request_cost_usd": round(request_cost, 6),
        "exact_json": exact_json,
        "parsed": parsed,
    }


def monthly_projection(result, monthly_requests, fixed_monthly):
    if result["status"] != "ok":
        return None
    variable = result["request_cost_usd"] * monthly_requests
    return round(variable + fixed_monthly, 2)


def main():
    monthly_requests = int(os.getenv("MONTHLY_REQUESTS", "1200"))
    local_fixed = env_float("LOCAL_FIXED_MONTHLY_USD", "0")
    cloud_fixed = env_float("CLOUD_FIXED_MONTHLY_USD", "0")

    results = []
    for prefix in ("LOCAL", "CLOUD"):
        try:
            results.append(measure_backend(prefix))
        except urllib.error.URLError as exc:
            results.append({
                "backend": prefix.lower(),
                "status": "unreachable",
                "error": str(exc),
            })
        except Exception as exc:
            results.append({
                "backend": prefix.lower(),
                "status": "failed",
                "error": str(exc),
            })

    for result in results:
        fixed = local_fixed if result["backend"] == "local" else cloud_fixed
        monthly_cost = monthly_projection(result, monthly_requests, fixed)
        result["monthly_cost_usd"] = monthly_cost

    print(json.dumps(
        {
            "monthly_requests": monthly_requests,
            "results": results,
            "decision_hint": (
                "elige despues de mirar coste, latencia, "
                "JSON exacto y frontera de datos"
            ),
        },
        ensure_ascii=False,
        indent=2,
    ))


if __name__ == "__main__":
    main()

La salida útil se parece a esto:

{
  "monthly_requests": 1200,
  "results": [
    {
      "backend": "local",
      "status": "ok",
      "model": "local-lab",
      "latency_s": 2.41,
      "input_tokens": 85,
      "output_tokens": 74,
      "request_cost_usd": 0.0,
      "exact_json": true,
      "monthly_cost_usd": 80.0
    },
    {
      "backend": "cloud",
      "status": "ok",
      "model": "modelo-cloud",
      "latency_s": 0.92,
      "input_tokens": 85,
      "output_tokens": 74,
      "request_cost_usd": 0.000846,
      "exact_json": true,
      "monthly_cost_usd": 1.02
    }
  ]
}

La interpretación no es “cloud gana porque cuesta 1,02” ni “local gana porque no manda datos”. La interpretación correcta es: para 1200 peticiones mensuales, si esos datos pueden salir bajo contrato, cloud parece más barato y rápido; si esos datos no pueden salir o la app debe funcionar sin red, local tiene sentido aunque el coste fijo sea mayor.

Cómo encaja todo

Este mapa conecta la decisión cloud/local con lo que ya vimos y con lo que viene. Fíjate en que no sale de “gusto por herramientas”, sino de restricciones medibles.

graph TD
    subgraph "Capítulo 6: cloud frente a local"
        DATA["Frontera de confianza"]
        INVENTORY["Inventario de modelos"]
        RETENTION["Retención y región"]
        LAT["Latencia p95"]
        COST["TCO y punto de equilibrio"]
        SERVER["Servidor de inferencia local"]
        ROUTER["Gateway o router de modelos"]
        RENTGPU["GPU alquilada"]
        OPS["Operación y mantenimiento"]
        ARCH["Arquitectura local, cloud o híbrida"]
    end
    subgraph "Viene de antes"
        TOKENS["Tokens, coste y caché (F4C3)"]
        MODELCARD["Elección de modelos (F4C4)"]
        LOCAL["Modelos locales (F4C5)"]
    end
    subgraph "Sigue después"
        EMB["Embeddings (F4C7)"]
        VECTOR["Bases vectoriales (F4C8)"]
        RAG["RAG (F4C9-10)"]
        AGENTS["Agentes con herramientas (F5)"]
        OPERATE["Construir y operar (F6)"]
    end

    TOKENS --> COST
    TOKENS --> LAT
    MODELCARD --> INVENTORY
    INVENTORY --> RETENTION
    INVENTORY --> ROUTER
    INVENTORY --> SERVER
    INVENTORY --> RENTGPU
    LOCAL --> OPS
    LOCAL --> SERVER
    SERVER --> OPS
    ROUTER --> COST
    RENTGPU --> SERVER
    RENTGPU --> COST
    RENTGPU --> OPS
    DATA --> RETENTION
    RETENTION --> ARCH
    LAT --> ARCH
    COST --> ARCH
    SERVER --> ARCH
    ROUTER --> ARCH
    RENTGPU --> ARCH
    OPS --> ARCH
    ARCH --> EMB
    ARCH --> VECTOR
    ARCH --> RAG
    ARCH --> AGENTS
    ARCH --> OPERATE

    style DATA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style INVENTORY fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RETENTION fill:#F5F5F5,stroke:#000000,stroke-width:2
    style LAT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style COST fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SERVER fill:#F5F5F5,stroke:#000000,stroke-width:2
    style ROUTER fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RENTGPU fill:#F5F5F5,stroke:#000000,stroke-width:2
    style OPS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style ARCH fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TOKENS stroke-dasharray: 5 5
    style MODELCARD stroke-dasharray: 5 5
    style LOCAL stroke-dasharray: 5 5
    style EMB stroke-dasharray: 5 5
    style VECTOR stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style AGENTS stroke-dasharray: 5 5
    style OPERATE stroke-dasharray: 5 5

Vocabulario aprendido

TérminoDefinición
Frontera de confianzaPunto o cadena donde los datos pasan a otro dominio técnico o contractual.
TCOCoste total de propiedad, incluyendo uso, infraestructura, operación y mantenimiento.
Latencia p95Tiempo por debajo del cual termina el 95 por ciento de peticiones.
TTFTTiempo hasta recibir el primer token.
ThroughputCapacidad de procesar peticiones o tokens por unidad de tiempo.
RegiónUbicación geográfica donde se procesa o guarda una carga.
RetenciónTiempo durante el que se conservan datos, logs o estado.
Residencia de datosRestricción sobre dónde deben vivir o procesarse los datos.
Capacidad elásticaCapacidad de escalar recursos bajo demanda.
Punto de equilibrioVolumen donde coste local y cloud se igualan bajo hipótesis dadas.
Gateway de modelosCapa que ofrece un endpoint común y enruta peticiones a distintos proveedores o modelos.
Servidor de inferenciaServicio que carga pesos, gestiona memoria, cola peticiones y expone una API para generar salidas.
GPU alquiladaAcelerador en cloud que pagas por tiempo para ejecutar tu propio runtime o contenedor.
KV cacheMemoria usada para guardar claves y valores de atención ya calculados durante la generación.
Modelo servidoNombre de modelo que expone tu API; puede ser distinto del repositorio o archivo interno.

Antes de pasar página

  • ¿Puedo explicar por qué local no significa automáticamente privado?
  • ¿Puedo explicar por qué cloud no significa automáticamente caro?
  • ¿Sé dibujar la frontera de confianza de un flujo completo?
  • ¿Sé inventariar modelos disponibles por endpoint y no por memoria?
  • ¿Sé distinguir API directa, plataforma cloud, gateway, Ollama Cloud y servidor propio?
  • ¿Sé calcular coste cloud con entrada, salida, cache y extras?
  • ¿Sé calcular coste local incluyendo hardware, energía y operación?
  • ¿Sé calcular coste de GPU alquilada incluyendo idle, storage, red y operación?
  • ¿Estoy midiendo p95 y no solo promedio?
  • ¿Sé qué datos se retienen y durante cuánto tiempo en el endpoint elegido?
  • ¿Sé qué runtime, contexto, cuantización y nombre servido tiene mi servidor local?
  • ¿Tengo una práctica comparando local y cloud con el mismo caso?

En resumen

Idea fuerzaDetalle
Local y cloud son posiciones arquitectónicas.No son bandos; responden a restricciones distintas.
Privacidad exige dibujar la frontera de confianza.Hay que saber dónde existe el dato, quién accede y cuánto se retiene.
La latencia útil se mide en p95 y p99.El promedio no captura la experiencia de usuarios lentos.
El coste real es TCO.Tokens, hardware, operación, mantenimiento, cache y herramientas cuentan.
Un gateway no es un modelo.OpenRouter u otros routers pueden cambiar proveedor, parámetros y precio detrás de un endpoint común.
Ollama Cloud no equivale a inferencia local.Conserva la experiencia Ollama, pero cambia ubicación, coste y frontera de datos.
Servir local en serio requiere infraestructura.Modelo, runtime, KV cache, API, claves, métricas, colas, límites y rollback forman parte del sistema.
Alquilar GPU es cloud con responsabilidad propia.Pagas tiempo, storage e idle; tú sirves, mides, actualizas y apagas.
La ruta híbrida suele ser la más realista.Minimiza datos localmente, escala cloud cuando aporta y mantiene una eval común.

Para saber más

Amazon Web Services. (2026). Amazon Bedrock pricing. https://aws.amazon.com/bedrock/pricing/

Amazon Web Services. (2026). Data protection in Amazon Bedrock. https://docs.aws.amazon.com/bedrock/latest/userguide/data-protection.html

Amazon Web Services. (2026). Amazon EC2 Pricing. https://aws.amazon.com/ec2/pricing/

Amazon Web Services. (2026). Specifications for Amazon EC2 accelerated computing instances. https://docs.aws.amazon.com/ec2/latest/instancetypes/ac.html

Anthropic. (2026). API and data retention. https://platform.claude.com/docs/en/manage-claude/api-and-data-retention

Anthropic. (2026). List Models. https://platform.claude.com/docs/en/api/models/list

Anthropic. (2026). Pricing. https://platform.claude.com/docs/en/about-claude/pricing

Dean, J. y Barroso, L. A. (2013). The Tail at Scale. Communications of the ACM, 56(2), 74-80. https://doi.org/10.1145/2408776.2408794

Google. (2026). Gemini API models. https://ai.google.dev/gemini-api/docs/models

Google Cloud. (2026). About GPU instances. https://docs.cloud.google.com/compute/docs/gpus/about-gpus

Google Cloud. (2026). GPU machine types. https://docs.cloud.google.com/compute/docs/gpus

Google Cloud. (2026). Vertex AI and zero data retention. https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance

Google Cloud. (2026). Vertex AI pricing. https://cloud.google.com/vertex-ai/generative-ai/pricing

Hugging Face. (2026). Text Generation Inference: HTTP API Reference. https://huggingface.co/docs/text-generation-inference/reference/api_reference

llama.cpp. (2026). llama-server. https://www.mintlify.com/ggml-org/llama.cpp/api/tools/llama-server

Microsoft Azure. (2026). Linux Virtual Machines Pricing. https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/

Microsoft Azure. (2026). Virtual machine sizes overview. https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/overview

Ollama. (2026). API introduction. https://docs.ollama.com/api/introduction

Ollama. (2026). Cloud. https://docs.ollama.com/cloud

Ollama. (2026). OpenAI compatibility. https://docs.ollama.com/api/openai-compatibility

OpenAI. (2026). Data controls in the OpenAI platform. https://developers.openai.com/api/docs/guides/your-data

OpenAI. (2026). Latency optimization. https://developers.openai.com/api/docs/guides/latency-optimization

OpenAI. (2026). List models. https://developers.openai.com/api/reference/resources/models/methods/list

OpenAI. (2026). Pricing. https://developers.openai.com/api/docs/pricing

OpenRouter. (2026). List all models and their properties. https://openrouter.ai/docs/api/api-reference/models/get-models

OpenRouter. (2026). Provider routing. https://openrouter.ai/docs/guides/routing/provider-selection

Runpod. (2026). Cloud GPU Instances for AI Workloads. https://www.runpod.io/product/cloud-gpus

Runpod. (2026). Pods pricing. https://docs.runpod.io/pods/pricing

Shi, W., Cao, J., Zhang, Q., Li, Y. y Xu, L. (2016). Edge Computing: Vision and Challenges. IEEE Internet of Things Journal, 3(5), 637-646. https://doi.org/10.1109/JIOT.2016.2579198

SGLang. (2026). Welcome to SGLang. https://docs.sglang.io/index.html

vLLM. (2026). OpenAI-Compatible Server. https://docs.vllm.ai/en/stable/serving/openai_compatible_server/

Notas

  1. OpenAI. (2026). Data controls in the OpenAI platform. https://developers.openai.com/api/docs/guides/your-data. Consultado el 10 de junio de 2026.

  2. OpenAI. (2026). List models. https://developers.openai.com/api/reference/resources/models/methods/list. Consultado el 10 de junio de 2026.

  3. OpenAI. (2026). Pricing. https://developers.openai.com/api/docs/pricing. Consultado el 10 de junio de 2026.

  4. OpenAI. (2026). Latency optimization. https://developers.openai.com/api/docs/guides/latency-optimization. Consultado el 10 de junio de 2026.

  5. Anthropic. (2026). API and data retention. https://platform.claude.com/docs/en/manage-claude/api-and-data-retention. Consultado el 10 de junio de 2026.

  6. Anthropic. (2026). List Models. https://platform.claude.com/docs/en/api/models/list. Consultado el 10 de junio de 2026.

  7. Anthropic. (2026). Pricing. https://platform.claude.com/docs/en/about-claude/pricing. Consultado el 10 de junio de 2026.

  8. Google. (2026). Gemini API models. https://ai.google.dev/gemini-api/docs/models. Consultado el 10 de junio de 2026.

  9. Google Cloud. (2026). Vertex AI and zero data retention. https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance. Consultado el 10 de junio de 2026.

  10. Google Cloud. (2026). Vertex AI pricing. https://cloud.google.com/vertex-ai/generative-ai/pricing. Consultado el 10 de junio de 2026.

  11. Amazon Web Services. (2026). Data protection in Amazon Bedrock. https://docs.aws.amazon.com/bedrock/latest/userguide/data-protection.html. Consultado el 10 de junio de 2026.

  12. Amazon Web Services. (2026). Amazon Bedrock pricing. https://aws.amazon.com/bedrock/pricing/. Consultado el 10 de junio de 2026.

  13. OpenRouter. (2026). List all models and their properties. https://openrouter.ai/docs/api/api-reference/models/get-models. Consultado el 10 de junio de 2026.

  14. OpenRouter. (2026). Provider routing. https://openrouter.ai/docs/guides/routing/provider-selection. Consultado el 10 de junio de 2026.

  15. Ollama. (2026). Cloud. https://docs.ollama.com/cloud. Consultado el 10 de junio de 2026. Véase también API introduction: https://docs.ollama.com/api/introduction y OpenAI compatibility: https://docs.ollama.com/api/openai-compatibility.

  16. vLLM. (2026). OpenAI-Compatible Server. https://docs.vllm.ai/en/stable/serving/openai_compatible_server/. Consultado el 10 de junio de 2026. llama.cpp. (2026). llama-server. https://www.mintlify.com/ggml-org/llama.cpp/api/tools/llama-server. Hugging Face. (2026). TGI HTTP API Reference. https://huggingface.co/docs/text-generation-inference/reference/api_reference. SGLang. (2026). Welcome to SGLang. https://docs.sglang.io/index.html.

  17. AWS. (2026). Amazon EC2 Pricing. https://aws.amazon.com/ec2/pricing/. Consultado el 10 de junio de 2026. AWS. (2026). Specifications for Amazon EC2 accelerated computing instances. https://docs.aws.amazon.com/ec2/latest/instancetypes/ac.html. Google Cloud. (2026). GPU machine types. https://docs.cloud.google.com/compute/docs/gpus. Google Cloud. (2026). About GPU instances. https://docs.cloud.google.com/compute/docs/gpus/about-gpus.

  18. Microsoft Azure. (2026). Virtual machine sizes overview. https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/overview. Microsoft Azure. (2026). Linux Virtual Machines Pricing. https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/. Runpod. (2026). Pods pricing. https://docs.runpod.io/pods/pricing. Runpod. (2026). Cloud GPU Instances for AI Workloads. https://www.runpod.io/product/cloud-gpus. Consultado el 10 de junio de 2026.

  19. Jeffrey Dean y Luiz André Barroso. (2013). The Tail at Scale. Communications of the ACM, 56(2), 74-80. https://doi.org/10.1145/2408776.2408794.

  20. Weisong Shi et al. (2016). Edge Computing: Vision and Challenges. IEEE Internet of Things Journal, 3(5), 637-646. https://doi.org/10.1109/JIOT.2016.2579198.

  21. OpenAI. (2026). Cost optimization. https://developers.openai.com/api/docs/guides/cost-optimization. Consultado el 10 de junio de 2026.

  22. OpenAI. (2026). Model selection. https://developers.openai.com/api/docs/guides/model-selection. Consultado el 10 de junio de 2026.

  23. Anthropic. (2026). Usage and Cost Admin API. https://platform.claude.com/docs/en/manage-claude/usage-cost-api. Consultado el 10 de junio de 2026.

  24. Anthropic. (2026). Data residency. https://platform.claude.com/docs/en/manage-claude/data-residency. Consultado el 10 de junio de 2026.

  25. Amazon Web Services. (2026). Data protection in Amazon Bedrock. https://docs.aws.amazon.com/bedrock/latest/userguide/data-protection.html. Consultado el 10 de junio de 2026.

  26. Google Cloud. (2026). Vertex AI Generative AI release notes. https://docs.cloud.google.com/vertex-ai/generative-ai/docs/release-notes. Consultado el 10 de junio de 2026.

  27. Google Cloud. (2026). Vertex AI Generative AI SLA. https://cloud.google.com/vertex-ai/generative-ai/sla. Consultado el 10 de junio de 2026.

Capítulo 07

Facsímil 4 · La caja de herramientas

Capítulo 07: Embeddings aplicados y búsqueda semántica

Cuando las palabras exactas no bastan

Imagina que alguien busca en una intranet: “no puedo entrar al campus virtual”. El documento que resuelve el problema quizá se titula “Restablecer acceso a Moodle con doble factor”. No comparte demasiadas palabras con la consulta, pero para una persona está claro que hablan de lo mismo.

La búsqueda clásica por palabras exactas funciona muy bien cuando el usuario sabe cómo se llama algo. Falla más cuando el usuario describe una necesidad, usa sinónimos, escribe con otro registro o mezcla conceptos. Aquí entran los embeddings: convertir textos en vectores para poder buscar por cercanía aproximada de significado.

Venimos del capítulo 03, donde hablamos de tokens, contexto y coste, y del capítulo 06, donde decidimos dónde ejecutar modelos. Ahora usamos esa base para montar la pieza que alimenta RAG, memoria de producto, recomendadores y buscadores internos.

Estado del arte con fecha de corte

Fecha de corte: 25 de mayo de 2026.
Fuentes consultadas ese día: documentación oficial de embeddings de OpenAI, Gemini, Cohere, Voyage AI y Sentence Transformers; y trabajos académicos sobre sentence embeddings, búsqueda vectorial, HNSW, FAISS y evaluación de recuperación.

Lo estable es el mecanismo: un modelo convierte entradas en vectores, esos vectores se comparan con una métrica y el sistema devuelve un ranking. Lo cambiante son modelos disponibles, dimensiones, precios, límites de contexto, soporte multimodal, compresión, tipos de salida, librerías y benchmarks.

FuenteQué aportaCómo usarla
OpenAI embeddings.1Explica embeddings como vectores de números y enumera usos como búsqueda, clustering, recomendaciones y clasificación.Para entender el contrato básico: texto entra, vector sale, distancia mide relación.
text-embedding-3-large.2Documenta un modelo de embedding concreto y su ficha de modelo.Para no hablar de “OpenAI embeddings” como si fuera un único artefacto.
Gemini embeddings.3Muestra cómo generar embeddings en Gemini API y usar salida vectorial para recuperación.Para comparar API, dimensiones, tareas y límites.
Cohere embeddings.4Introduce input_type, soporte multilingüe, embeddings de imagen, contenido mixto, Matryoshka y compresión.Para recordar que query y documento pueden tratarse de forma distinta.
Voyage embeddings.5Lista modelos, dimensiones, contexto e input_type para query/document.Para elegir modelos orientados a retrieval, código, legal, finanzas o uso general.
Sentence Transformers.6Ofrece una forma local y reproducible de generar embeddings y hacer búsqueda semántica.Para aprender el mecanismo sin depender de una API externa.
Sentence-BERT.7Populariza embeddings de frases eficientes para similitud semántica.Para ver por qué no basta usar cualquier vector interno de un modelo.
FAISS.8Muestra técnicas de búsqueda eficiente de vectores a gran escala, especialmente en GPU.Para entender por qué una base vectorial no compara siempre todo contra todo.
HNSW.9Describe un índice por grafo muy usado para vecinos aproximados.Para entender el intercambio entre rapidez, memoria y exactitud.
BEIR.10Propone evaluar retrieval en tareas diversas, no solo en un dataset cómodo.Para no validar un buscador con tres ejemplos elegidos a mano.

Qué no es un embedding

Un embedding no es una traducción secreta del texto. No contiene una definición legible de cada palabra. No es una base de datos comprimida con todos los documentos. Tampoco es una garantía de verdad: dos textos pueden estar cerca en el espacio vectorial y aun así no responder la pregunta correcta.

Tampoco es “memoria” por sí mismo. Guardar embeddings de documentos permite recuperar fragmentos parecidos, pero el sistema no entiende permisos, vigencia, autoría ni contexto de negocio a menos que tú lo diseñes. Un vector no sabe si un reglamento está derogado.

Y un embedding no sustituye a la búsqueda por filtros. Si el usuario pregunta por “normativa de matrícula 2025” y tu sistema devuelve un documento semánticamente parecido de 2021, el vector ha hecho parte del trabajo; falta metadata, filtros y evaluación.

Qué sí es: una coordenada útil

Un modelo de embeddings es una función:

e=fθ(x)Rde = f_{\theta}(x) \in \mathbb{R}^{d}
SímboloSignificadoEjemplo
xxEntrada que queremos representar.“Restablecer acceso al campus virtual”.
fθf_{\theta}Modelo de embeddings con parámetros aprendidos.Un modelo de Sentence Transformers o una API de embeddings.
eeVector resultante.[0,12,0,03,0,44,...][0{,}12,\,-0{,}03,\,0{,}44,\,...].
ddNúmero de dimensiones del vector.384, 768, 1024, 1536 o 3072 según modelo.
Rd\mathbb{R}^{d}Espacio de vectores reales de dimensión dd.Una tabla con dd columnas numéricas.

La intuición: textos que el modelo considera parecidos quedan cerca. Textos que el modelo considera distintos quedan lejos. Esa cercanía no aparece porque el modelo “sepa” como una persona; aparece porque durante entrenamiento aprendió a colocar ejemplos relacionados cerca y ejemplos no relacionados más lejos.

En búsqueda semántica hacemos lo mismo con documentos y consultas:

q=fθ(consulta)q = f_{\theta}(\text{consulta}) di=fθ(documentoi)d_i = f_{\theta}(\text{documento}_i)

Después comparamos qq con cada did_i y ordenamos.

Qué significa la dimensión

La dimensión de un embedding es el número de componentes del vector. Si un modelo devuelve un embedding de dimensión 384, cada texto se convierte en 384 números. Si devuelve 3072, cada texto se convierte en 3072 números. No es una “nota de inteligencia”; es el ancho de la representación.

Piensa en una tabla. Cada fila es un texto y cada columna es una coordenada aprendida por el modelo:

e=[e1,e2,e3,,ed]e = [e_1, e_2, e_3, \dots, e_d]
PiezaQué significaEjemplo
eeEmbedding completo de un texto.El vector de “no puedo entrar al campus”.
e1,e2,e3e_1, e_2, e_3Primeras coordenadas del vector.0.12, -0.31, 0.08.
ddNúmero total de coordenadas.384 columnas numéricas.

En un ejemplo pequeño de cuatro dimensiones, dos textos podrían quedar así:

Textoe1e_1e2e_2e3e_3e4e_4
“acceso al campus”0,200,81-0,100,33
“entrar en Moodle”0,180,77-0,080,29
“calendario de matrícula”-0,420,050,71-0,11

Los dos primeros textos se parecen porque sus coordenadas apuntan en una dirección parecida. El tercero queda más lejos porque su patrón numérico es distinto.

Conviene decirlo con cuidado: una dimensión no suele significar “Moodle”, “matrícula” o “problema técnico” de forma aislada. En embeddings modernos, el significado aparece distribuido entre muchas coordenadas a la vez. Una coordenada puede participar en varios patrones; un patrón puede necesitar cientos o miles de coordenadas. Por eso no miramos una dimensión suelta para interpretar el texto: comparamos el vector completo.

La dimensión importa por cuatro razones:

RazónQué cambiaConsecuencia práctica
MemoriaCada vector ocupa dd números.Más dimensión implica más RAM, disco, backup y red.
LatenciaComparar vectores cuesta más si dd crece.El ranking exacto y el índice trabajan más.
SeñalEl vector tiene más espacio para codificar matices.Puede mejorar retrieval, pero no siempre en tu dominio.
CompatibilidadTodos los vectores del índice deben tener la misma dimensión.Cambiar modelo o dimensión suele exigir reindexar.

La memoria bruta se calcula así:

Mvectores=NdbM_{\text{vectores}} = N \cdot d \cdot b
SímboloSignificadoEjemplo
MvectoresM_{\text{vectores}}Memoria bruta para guardar vectores.Bytes antes de índice y metadata.
NNNúmero de vectores guardados.1.000.000 chunks.
ddDimensión de cada vector.384, 1024 o 3072.
bbBytes por número.4 bytes para float32, 2 para float16.

Para 1 millón de vectores en float32:

DimensiónMemoria brutaLectura de ingeniería
3841,54 GBCómodo para prototipos y muchos casos internos.
7683,07 GBDobla memoria y cómputo respecto a 384.
15366,14 GBEmpieza a exigir pensar en índice, RAM y backups.
307212,29 GBPuede tener más señal, pero no sale gratis.

El coste de comparación exacta crece igual:

Ccomparar=O(Nd)C_{\text{comparar}} = O(N \cdot d)

Si duplicas dd, duplicas el trabajo bruto de comparar una consulta contra todos los vectores. Un índice aproximado reduce el número de comparaciones, pero no elimina que cada comparación tenga dd componentes.

Algunos modelos y proveedores permiten reducir dimensión de salida o usar representaciones tipo Matryoshka, donde los primeros bloques del vector intentan conservar señal útil al truncar dimensiones.11 Cohere, por ejemplo, documenta embeddings con Matryoshka y compresión para equilibrar calidad, memoria y coste.12 Eso no significa que puedas cortar cualquier vector arbitrariamente y esperar el mismo resultado: hay que evaluarlo.

Dimensión, coste y calidad en una sola imagen

Dimensiones de embeddings: coste, latencia y señal La dimensión es ancho de representación, no prestigio Más columnas pueden capturar más señal, pero también multiplican memoria, latencia e índice. Un texto se convierte en d números e = [x₁, x₂, ..., x_d] 384 dims menos memoria · búsqueda rápida 1536 dims más señal · más índice 3072 dims más coste · evaluar antes Coste de almacenamiento M = N · d · b 1M vectores · float32 384 → 1,54 GB · 1536 → 6,14 GB · 3072 → 12,29 GB sin contar HNSW, metadata, réplicas ni backups Coste de comparación O(N · d) duplicar d duplica trabajo bruto ANN reduce candidatos, no el ancho del vector mide p95, recall y coste por consulta La decisión se toma con evaluación, no con intuición menos coste punto útil más señal posible 384 768 1536 3072 elige la menor dimensión que mantenga recall, MRR y calidad de respuesta en tu eval Cambiar dimensión cambia almacenamiento, índice y comparabilidad: versiona y reevalúa. IA para gente curiosa / Facsímil 04 / Capítulo 07 / 686f6c61

La métrica que decide el ranking

La similitud coseno compara dirección, no tamaño bruto:

cos(q,di)=qdiqdi\operatorname{cos}(q, d_i) = \frac{q \cdot d_i}{\|q\|\,\|d_i\|}
SímboloSignificadoEjemplo
qqVector de la consulta.[0,2,0,8][0{,}2, 0{,}8].
did_iVector del documento ii.[0,1,0,9][0{,}1, 0{,}9].
qdiq \cdot d_iProducto punto: suma de productos componente a componente.0,20,1+0,80,9=0,740{,}2\cdot0{,}1 + 0{,}8\cdot0{,}9 = 0{,}74.
q\|q\|Norma o longitud del vector de consulta.0,22+0,82=0,824\sqrt{0{,}2^2+0{,}8^2}=0{,}824.
di\|d_i\|Norma del vector de documento.0,12+0,92=0,906\sqrt{0{,}1^2+0{,}9^2}=0{,}906.
cos(q,di)\operatorname{cos}(q,d_i)Similitud final.0,74/(0,8240,906)=0,9920{,}74/(0{,}824\cdot0{,}906)=0{,}992.

Si normalizas todos los vectores para que tengan norma 1, el coseno se convierte en producto punto:

q^=qq,di^=didi\hat{q} = \frac{q}{\|q\|}, \qquad \hat{d_i} = \frac{d_i}{\|d_i\|} cos(q,di)=q^di^\operatorname{cos}(q,d_i)=\hat{q}\cdot\hat{d_i}

Eso importa en producción porque muchas bases vectoriales trabajan más rápido con producto punto si tus vectores ya están normalizados.

El ranking top-k se expresa así:

TopK(q,D,k)={d(1),,d(k)},s(q,d(1))s(q,d(k))\operatorname{TopK}(q, D, k) = \{d_{(1)},\dots,d_{(k)}\}, \quad s(q,d_{(1)}) \ge \dots \ge s(q,d_{(k)})
SímboloSignificadoEjemplo
DDColección de documentos vectorizados.50.000 fragmentos de una intranet.
kkNúmero de resultados que queremos devolver.5.
s(q,di)s(q,d_i)Función de puntuación.Coseno, producto punto o distancia negativa.
TopK\operatorname{TopK}Devuelve los identificadores con mayor puntuación.Documentos 17, 42, 8, 91 y 3.

El proceso completo

Una búsqueda semántica mínima tiene dos fases: indexación y consulta. En indexación conviertes documentos en vectores y guardas esos vectores con sus metadatos. En consulta conviertes la pregunta en otro vector, buscas vecinos cercanos y devuelves resultados.

PasoQué ocurreDecisión técnica
1. Preparar documentosLimpias, separas, titulas o partes contenido.Qué unidad se busca: documento entero, sección, párrafo o chunk.
2. Generar embeddingsCada unidad pasa por el modelo.Modelo, dimensión, idioma, coste, privacidad y batch.
3. NormalizarOpcionalmente reescalas vectores.Coseno/producto punto y compatibilidad con el índice.
4. GuardarVector + texto + metadata + versión.Base vectorial, tabla propia o índice en memoria.
5. Embedding de consultaLa pregunta se transforma con el mismo modelo o modelo compatible.input_type=query si el proveedor lo usa.
6. Recuperar top-kBuscas vecinos exactos o aproximados.Exactitud, latencia, memoria y filtros.
7. ReordenarPuedes aplicar reranking, filtros o MMR.Mejorar precisión y diversidad.
8. Usar resultadosMostrar documentos o pasarlos a un LLM.Búsqueda, RAG, recomendación o clasificación.

La unidad de búsqueda es decisiva. Si indexas documentos enormes, el resultado puede ser “parecido” pero poco accionable. Si indexas frases demasiado cortas, pierdes contexto. Si indexas chunks sin título, una frase como “plazo máximo” puede quedar huérfana.

Una imagen mental del pipeline

Embeddings aplicados a búsqueda semántica Buscar por significado es construir un ranking vectorial El embedding no responde: coloca consulta y documentos en un espacio comparable. Documentos políticas · tickets · FAQs texto + metadata unidad: sección o chunk Modelo embedding fθ(texto) → vector dimensión · idioma · coste query/document si aplica Vectores guardados id · vector · texto metadata · versión Índice exacto o aproximado HNSW · FAISS · DB latencia vs recall Consulta “no puedo entrar” → vector q mismo espacio que documentos Comparar y ordenar coseno(q, dᵢ) top-k resultados filtros · MMR · reranking Resultado útil fragmentos recuperados con puntuación y fuente sirve a búsqueda o RAG La calidad se decide en la unidad indexada, la métrica, los filtros y la evaluación, no solo en el modelo. IA para gente curiosa / Facsímil 04 / Capítulo 07 / 686f6c61

Exacto, aproximado y lo que cuesta

Si tienes pocos documentos, puedes comparar la consulta con todos los vectores. Eso se llama búsqueda exacta por fuerza directa. Si tienes millones de vectores, comparar contra todos puede ser caro y lento; entonces aparecen índices aproximados.

El coste de comparar una consulta contra NN documentos de dimensión dd es aproximadamente:

Cexacto=O(Nd)C_{\text{exacto}} = O(N \cdot d)
SímboloSignificadoEjemplo
NNNúmero de vectores guardados.1.000.000 chunks.
ddDimensión de cada vector.1024.
O(Nd)O(N\cdot d)Trabajo proporcional a comparar NN vectores de dd números.Unos 1.024 millones de multiplicaciones/sumas por consulta.

Los índices ANN reducen latencia buscando candidatos probables, no revisando todo. HNSW lo hace con un grafo de vecinos navegable; FAISS agrupa varias técnicas como índices planos, cuantización y búsqueda en GPU. La palabra aproximado no significa “malo”: significa que aceptas una probabilidad de no encontrar exactamente el vecino más cercano a cambio de velocidad.

La memoria también importa:

Mvectores=NdbM_{\text{vectores}} = N \cdot d \cdot b
SímboloSignificadoEjemplo
MvectoresM_{\text{vectores}}Memoria bruta para guardar vectores.Bytes antes de índice y metadata.
NNNúmero de vectores.1.000.000.
ddDimensión.1536.
bbBytes por número.4 bytes si usas float32.

Con N=1.000.000N=1.000.000, d=1536d=1536 y float32, solo los vectores ocupan:

1.000.00015364=6.144.000.000 bytes1.000.000 \cdot 1536 \cdot 4 = 6.144.000.000\ \text{bytes}

Eso son unos 6,1 GB antes de contar índices, texto, metadata, réplicas y backups. Si además guardas 10 millones de chunks, el problema deja de ser “llamar a embeddings” y pasa a ser arquitectura de almacenamiento.

Elegir modelo de embedding

Elegir un modelo de embeddings no es elegir “el más grande”. Es elegir el que recupera mejor tus documentos con tu idioma, tu dominio, tu latencia y tu presupuesto.

CriterioQué mirarPor qué
IdiomaEspañol, multilingüe, mezcla de idiomas.Un modelo fuerte en inglés puede perder matices en español.
DominioGeneral, código, legal, financiero, médico, soporte.El vocabulario y las relaciones cambian.
Dimensión384, 768, 1024, 1536, 3072...Afecta memoria, coste y velocidad de búsqueda.
ContextoTokens máximos por entrada.Documentos largos se truncarán o habrá que trocearlos.
input_typeQuery/document si el proveedor lo distingue.Algunas APIs optimizan consultas y documentos de forma distinta.
ModalidadTexto, imagen, documentos mixtos.Para PDFs visuales o capturas quizá no baste texto plano.
Local o cloudPrivacidad, latencia, coste y operación.Conecta directamente con el capítulo 06.
EvaluaciónRecall@k, MRR, precisión, nDCG.El benchmark externo orienta; tu caso decide.

Un detalle importante: si reindexas con otro modelo, los vectores antiguos y nuevos normalmente no son comparables. Cambiar de embedding model puede implicar recalcular todo el índice. Por eso conviene guardar embedding_model, embedding_version, dimension, normalization, created_at y source_hash junto a cada vector.

Cómo trabajar con embeddings sin romper producción

Trabajar con embeddings no es solo llamar una API y guardar un array. Es construir una cadena reproducible: preparar texto, generar vectores, versionarlos, guardarlos, consultarlos y evaluar si siguen sirviendo cuando cambian documentos, modelos o permisos.

TareaCómo hacerloQué comprobar
Preparar entradaAñadir título, sección, ruta y texto limpio.Que el fragmento sea entendible fuera del documento original.
Generar por lotesEnviar batches razonables y controlar reintentos.Coste, rate limits, errores parciales y orden de resultados.
VersionarGuardar modelo, dimensión, normalización y hash de fuente.Poder saber qué vector corresponde a qué texto exacto.
NormalizarReescalar si usarás coseno como producto punto.No mezclar vectores normalizados y sin normalizar.
Guardar metadataCurso, cliente, permiso, fecha, idioma, tipo de documento.Poder filtrar antes o después de recuperar.
ReindexarPlanificar jobs idempotentes y reanudables.No duplicar vectores ni mezclar versiones.
EvaluarMantener consultas reales con positivos y hard negatives.Que cambios de modelo o dimensión no degraden el ranking.
MonitorizarMedir latencia, top-k vacío, drift y feedback.Detectar documentos obsoletos o consultas nuevas.

Una regla sencilla: el texto que guardas junto al vector debe ser suficiente para explicar por qué salió ese resultado. Si solo guardas el vector y un identificador, depurar será una tortura tranquila.

Búsqueda semántica no es RAG

Búsqueda semántica recupera candidatos. RAG usa candidatos para construir una respuesta generada. Esta diferencia parece pequeña, pero cambia cómo evalúas.

SistemaSalidaQué evalúas
Búsqueda semánticaLista de documentos o fragmentos.Si el resultado correcto aparece arriba.
RAGRespuesta generada con contexto.Si la respuesta está fundamentada en los fragmentos correctos.
RecomendadorElementos parecidos o útiles.Si el usuario acepta, compra, lee o resuelve.
Clasificación por similitudEtiqueta más cercana.Si la etiqueta elegida es correcta.

Si el retrieval falla, el generador no puede arreglarlo de forma fiable. Puede escribir una respuesta bonita con evidencia equivocada. Por eso los próximos capítulos separan bases vectoriales, RAG y evaluación de RAG.

En el día a día

En una universidad, embeddings pueden servir para que una persona encuentre normativa aunque no conozca el nombre exacto del trámite. En soporte interno, pueden agrupar tickets parecidos y detectar respuestas repetidas. En producto, pueden recomendar documentación relacionada. En una base de conocimiento, pueden recuperar fragmentos para que un LLM responda con contexto.

La parte delicada es que una búsqueda semántica buena no depende solo del modelo. Depende de cómo partes documentos, qué metadata guardas, si filtras por permisos, si separas versiones antiguas, si reordenas resultados y si mides con preguntas reales.

Un caso cercano: tienes 800 artículos de ayuda. Si alguien pregunta “me han bloqueado la cuenta”, un buscador por palabra puede priorizar artículos que contienen “bloqueado”. Un embedding puede recuperar “Restablecer acceso tras demasiados intentos”. Pero si el artículo correcto está obsoleto y falta metadata de versión, el sistema seguirá pareciendo inteligente mientras devuelve una respuesta mala.

Por qué debería importarte

Embeddings son la puerta de entrada a casi todo lo que se vende como “IA con tus datos”. Si no entiendes esta pieza, no sabes si tu RAG falla por el modelo generativo, por el chunking, por la base vectorial, por la métrica o por la evaluación.

También importan por coste. Embeddings se calculan al indexar, se guardan durante meses, se consultan muchas veces y ocupan memoria. Una mala decisión de dimensión, chunking o modelo puede multiplicar almacenamiento y latencia sin mejorar recuperación.

Medir si recupera bien

Un buscador semántico debe evaluarse con consultas y respuestas esperadas. No hace falta empezar con un benchmark gigante: puedes crear 30 consultas reales, marcar qué documento debería aparecer y medir.

Recall@k:

Recall@k=consultas con al menos un resultado correcto en top-ktotal de consultas\operatorname{Recall@k} = \frac{\text{consultas con al menos un resultado correcto en top-k}} {\text{total de consultas}}
SímboloSignificadoEjemplo
kkNúmero de resultados que miras.3.
Top-kPrimeros kk documentos devueltos.Los tres primeros resultados.
Resultado correctoDocumento marcado como relevante.El artículo que resuelve el problema.

MRR mide en qué posición aparece el primer resultado correcto:

MRR=1Qj=1Q1rankj\operatorname{MRR} = \frac{1}{Q} \sum_{j=1}^{Q} \frac{1}{\operatorname{rank}_j}
SímboloSignificadoEjemplo
QQNúmero de consultas evaluadas.30.
rankj\operatorname{rank}_jPosición del primer resultado correcto para la consulta jj.1 si sale primero, 3 si sale tercero.
1/rankj1/\operatorname{rank}_jPenalización por aparecer más abajo.1, 0,5, 0,333...

Si el documento correcto aparece siempre en posición 8 y tú solo pasas top-3 al LLM, tu RAG no verá la evidencia aunque “el buscador la tenía”. Ese detalle es muy de ingeniería y muy poco de demo.

Evaluar embeddings, no solo el buscador

Evaluar embeddings no es preguntar “¿me gusta el primer resultado?”. Hay que separar varias preguntas:

PreguntaMétrica útilQué detecta
¿Aparece algún documento correcto entre los primeros?Recall@kSi el sistema encuentra evidencia suficiente.
¿Aparece arriba o enterrado?MRRSi el primer resultado útil llega pronto.
¿Ordena bien varios relevantes?nDCG@kSi documentos muy relevantes suben más que los medianos.
¿Se degrada al reducir dimensión?Curva dimensión-métricaSi puedes ahorrar memoria sin perder calidad.
¿Funciona en todos los grupos?Métricas por idioma, dominio, tipo de consulta.Si el promedio oculta fallos por segmento.
¿Distingue parecidos peligrosos?Hard negativesSi confunde textos casi iguales pero incorrectos.

BEIR y MTEB existen porque un embedding puede ir bien en una tarea y flojo en otra.13 Para un proyecto real, el benchmark externo sirve para elegir candidatos, pero tu evaluación interna decide.

nDCG@k se usa cuando no todos los documentos relevantes valen lo mismo:

DCG@k=i=1k2reli1log2(i+1)\operatorname{DCG@k} = \sum_{i=1}^{k} \frac{2^{rel_i}-1}{\log_2(i+1)} nDCG@k=DCG@kIDCG@k\operatorname{nDCG@k} = \frac{\operatorname{DCG@k}}{\operatorname{IDCG@k}}
SímboloSignificadoEjemplo
relirel_iRelevancia del resultado en posición ii.2 si responde, 1 si ayuda, 0 si no sirve.
DCG@k\operatorname{DCG@k}Ganancia acumulada con descuento por posición.Premia relevancia arriba.
IDCG@k\operatorname{IDCG@k}DCG ideal si el ranking fuera perfecto.Mejor orden posible para esa consulta.
nDCG@k\operatorname{nDCG@k}DCG normalizado entre 0 y 1.1 significa orden ideal.

Una evaluación seria de embeddings debería guardar:

CampoEjemploPor qué
query_idq-014Permite repetir y auditar resultados.
query_text“no puedo entrar al campus”La consulta real que hizo una persona o un caso diseñado.
positive_ids["doc-01"]Documentos que deben aparecer.
graded_relevance{"doc-01": 2, "doc-05": 1}Diferencia evidencia principal de apoyo parcial.
hard_negative_ids["doc-03"]Documentos parecidos que no responden.
filters{"curso": "2026"}Condiciones de producto o permisos.
embedding_modelall-MiniLM-L6-v2La evaluación depende del modelo.
dimension384Cambiar dimensión puede cambiar ranking.
top_k3, 5, 10Define qué ve usuario o LLM.

La parte más útil suele ser mirar errores, no solo el número final. Si una consulta falla porque el documento está mal troceado, el embedding no era el problema. Si falla porque hay dos documentos casi idénticos y uno está obsoleto, necesitas metadata y filtros. Si falla solo al bajar de 384 a 64 dimensiones, quizá encontraste el límite de compresión de tu caso.

Dónde volverá a aparecer

ConceptoDónde vuelvePara qué
Índices vectorialesCapítulo 08.Guardar y buscar millones de vectores con filtros.
ChunkingCapítulo 09.Elegir la unidad que recupera el RAG.
GroundednessCapítulo 10.Ver si la respuesta se apoya en fragmentos correctos.
Memoria de agentesFacsímil 05.Recuperar recuerdos útiles sin meter todo en contexto.
EvaluaciónFacsímil 07.Medir ranking, calibración y calidad real.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Pensar que más similitud siempre significa mejor respuestaLa similitud mide cercanía aproximada, no utilidad ni vigencia.Mirar documento, fecha, permisos y pregunta concreta.
Indexar documentos enormesRecuperas un bloque parecido pero poco accionable.Probar secciones o chunks con título y metadata.
Cambiar modelo sin reindexarLos vectores nuevos pueden no ser compatibles con los antiguos.Versionar modelo, dimensión, normalización y fecha.
Elegir dimensión por tamaño aparente3072 dimensiones no garantizan mejor producto que 768 en tu corpus.Comparar dimensión contra Recall@k, MRR, nDCG, coste y latencia.
Evaluar con tres consultas bonitasEl sistema parece bueno solo porque las pruebas eran fáciles.Crear un set de consultas reales con respuestas esperadas.
Olvidar filtrosUn resultado semánticamente cercano puede ser de otro curso, cliente o versión.Combinar similitud con metadata y permisos.
Subir top-k sin pensarMás resultados pueden meter ruido en el LLM y subir coste.Medir recall@k y calidad de respuesta final.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a construir un buscador semántico mínimo con sentence-transformers. El objetivo no es montar una base vectorial todavía; eso viene en el capítulo 08. Aquí queremos ver el mecanismo: documentos, embeddings, normalización, coseno, top-k, MMR, evaluación y comparación de dimensiones.

La práctica recorta el vector a 32, 64, 128 y 384 dimensiones para enseñar el intercambio entre memoria y calidad. En producción solo deberías reducir dimensión si el modelo o el proveedor lo soporta, o si tu evaluación demuestra que el recorte no rompe tu caso.

Instalación:

python -m pip install -U sentence-transformers numpy

Guarda esto como buscar_semanticamente.py:

from sentence_transformers import SentenceTransformer
import numpy as np


DOCUMENTOS = [
    {
        "id": "doc-01",
        "titulo": "Restablecer acceso al campus virtual",
        "texto": (
            "Si no puedes entrar, revisa doble factor "
            "y recupera contraseña."
        ),
        "curso": "2026",
    },
    {
        "id": "doc-02",
        "titulo": "Solicitar certificado académico",
        "texto": "El certificado se descarga desde secretaría virtual.",
        "curso": "2026",
    },
    {
        "id": "doc-03",
        "titulo": "Problemas con el correo institucional",
        "texto": (
            "Para recuperar el correo, actualiza contraseña "
            "y verifica MFA."
        ),
        "curso": "2026",
    },
    {
        "id": "doc-04",
        "titulo": "Calendario de matrícula",
        "texto": (
            "La matrícula ordinaria se abre en julio "
            "y la ampliación en septiembre."
        ),
        "curso": "2025",
    },
    {
        "id": "doc-05",
        "titulo": "Activar cuenta de estudiante",
        "texto": (
            "La cuenta se activa con DNI, código de admisión "
            "y teléfono."
        ),
        "curso": "2026",
    },
]

EVAL = [
    {
        "consulta": "no puedo entrar al campus",
        "relevantes": {"doc-01"},
    },
    {
        "consulta": "necesito el certificado de notas",
        "relevantes": {"doc-02"},
    },
    {
        "consulta": "se me ha bloqueado el correo",
        "relevantes": {"doc-03"},
    },
]

DIMENSIONES = [32, 64, 128, 384]


def normalizar(matriz):
    normas = np.linalg.norm(matriz, axis=1, keepdims=True)
    return matriz / np.maximum(normas, 1e-12)


def top_k(consulta_vec, documento_vecs, k):
    scores = documento_vecs @ consulta_vec
    orden = np.argsort(-scores)[:k]
    return [(int(i), float(scores[i])) for i in orden]


def mmr(consulta_vec, documento_vecs, candidatos, k, lambda_igualdad=0.75):
    elegidos = []
    candidatos = list(candidatos)

    while candidatos and len(elegidos) < k:
        mejor = None
        mejor_score = -10**9

        for idx in candidatos:
            relevancia = float(documento_vecs[idx] @ consulta_vec)
            diversidad = 0.0
            if elegidos:
                diversidad = max(
                    float(documento_vecs[idx] @ documento_vecs[j])
                    for j in elegidos
                )
            score = (
                lambda_igualdad * relevancia
                - (1 - lambda_igualdad) * diversidad
            )
            if score > mejor_score:
                mejor = idx
                mejor_score = score

        elegidos.append(mejor)
        candidatos.remove(mejor)

    return elegidos


def recall_at_k(resultados, relevantes, k):
    recuperados = {doc_id for doc_id, _score in resultados[:k]}
    return bool(recuperados & relevantes)


def reciprocal_rank(resultados, relevantes):
    for posicion, (doc_id, _score) in enumerate(resultados, start=1):
        if doc_id in relevantes:
            return 1 / posicion
    return 0.0


def limitar_dimension(matriz, dimension):
    recortada = matriz[:, :dimension]
    return normalizar(recortada)


def memoria_gb(num_vectores, dimension, bytes_por_numero=4):
    bytes_totales = num_vectores * dimension * bytes_por_numero
    return bytes_totales / 1_000_000_000


def evaluar_dimension(modelo, doc_vecs_full, dimension):
    doc_vecs = limitar_dimension(doc_vecs_full, dimension)
    recalls = []
    reciprocal_ranks = []

    for caso in EVAL:
        consulta_full = modelo.encode(
            [caso["consulta"]],
            convert_to_numpy=True,
        )
        consulta_vec = limitar_dimension(consulta_full, dimension)[0]
        ranking = top_k(consulta_vec, doc_vecs, k=3)
        resultados = [
            (DOCUMENTOS[idx]["id"], score)
            for idx, score in ranking
        ]
        recalls.append(recall_at_k(resultados, caso["relevantes"], k=3))
        reciprocal_ranks.append(
            reciprocal_rank(resultados, caso["relevantes"])
        )

    return {
        "dimension": dimension,
        "recall@3": sum(recalls) / len(recalls),
        "mrr": sum(reciprocal_ranks) / len(reciprocal_ranks),
        "gb_1m_float32": memoria_gb(1_000_000, dimension),
    }


def main():
    modelo = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
    textos = [f"{d['titulo']}. {d['texto']}" for d in DOCUMENTOS]

    doc_vecs = modelo.encode(textos, convert_to_numpy=True)
    doc_vecs = normalizar(doc_vecs)

    print("Dimensión:", doc_vecs.shape[1])
    print()

    aciertos = 0
    for caso in EVAL:
        consulta_vec = modelo.encode(
            [caso["consulta"]],
            convert_to_numpy=True,
        )
        consulta_vec = normalizar(consulta_vec)[0]

        ranking = top_k(consulta_vec, doc_vecs, k=4)
        reranked = mmr(
            consulta_vec,
            doc_vecs,
            [idx for idx, _score in ranking],
            k=3,
        )

        resultados = [
            (DOCUMENTOS[idx]["id"], score)
            for idx, score in ranking
        ]
        aciertos += recall_at_k(resultados, caso["relevantes"], k=3)

        print("Consulta:", caso["consulta"])
        print("Top por coseno:")
        for idx, score in ranking[:3]:
            doc = DOCUMENTOS[idx]
            print(" ", round(score, 3), doc["id"], doc["titulo"])

        print("Top con MMR:")
        for idx in reranked:
            doc = DOCUMENTOS[idx]
            print(" ", doc["id"], doc["titulo"])
        print()

    print("Recall@3:", round(aciertos / len(EVAL), 3))
    print()
    print("Comparación por dimensión")

    for fila in [
        evaluar_dimension(modelo, doc_vecs, dimension)
        for dimension in DIMENSIONES
    ]:
        print(
            fila["dimension"],
            "dims",
            "recall@3=",
            round(fila["recall@3"], 3),
            "mrr=",
            round(fila["mrr"], 3),
            "GB/1M=",
            round(fila["gb_1m_float32"], 3),
        )


if __name__ == "__main__":
    main()

Salida esperada aproximada:

Dimensión: 384

Consulta: no puedo entrar al campus
Top por coseno:
  0.62 doc-01 Restablecer acceso al campus virtual
  0.39 doc-05 Activar cuenta de estudiante
  0.27 doc-03 Problemas con el correo institucional
Top con MMR:
  doc-01 Restablecer acceso al campus virtual
  doc-05 Activar cuenta de estudiante
  doc-03 Problemas con el correo institucional

Recall@3: 1.0

Comparación por dimensión
32 dims recall@3= 1.0 mrr= 0.833 GB/1M= 0.128
64 dims recall@3= 1.0 mrr= 1.0 GB/1M= 0.256
128 dims recall@3= 1.0 mrr= 1.0 GB/1M= 0.512
384 dims recall@3= 1.0 mrr= 1.0 GB/1M= 1.536

Los números exactos pueden variar según versión de modelo y librería. Lo que no debe variar es la lectura: si 64 dimensiones mantienen recall y MRR para tu caso, quizá no necesitas guardar 384; si al bajar aparecen errores con documentos parecidos, el ahorro no compensa.

Prueba cuatro cambios: filtra curso == "2026", sube k de 3 a 5, añade un documento obsoleto muy parecido y cambia DIMENSIONES para incluir 16 o 256. Si el ranking mejora pero la respuesta de producto empeora, acabas de ver por qué búsqueda semántica, dimensión y gobernanza de datos tienen que ir juntas.

Cómo encaja todo

graph TD
    subgraph "Capítulo 7: embeddings y búsqueda semántica"
        INPUT["Texto, consulta<br/>o documento"]
        MODEL["Modelo de embeddings"]
        DIM["Dimensión d"]
        VECTOR["Vector en R^d"]
        METRIC["Coseno o producto punto"]
        TOPK["Ranking top-k"]
        EVAL["Recall@k, MRR y nDCG"]
        MMR["Diversidad y MMR"]
    end
    subgraph "Viene de antes"
        TOKENS["Tokens y coste (F4C3)"]
        CARDS["Model cards (F4C4)"]
        CLOUD["Local, cloud o GPU (F4C6)"]
        TRANS["Embeddings internos<br/>(F3C2)"]
    end
    subgraph "Sigue después"
        VECTORDB["Bases vectoriales (F4C8)"]
        RAG["RAG básico (F4C9)"]
        RAGEVAL["Evaluar RAG (F4C10)"]
        AGMEM["Memoria de agentes (F5)"]
    end

    TOKENS -->|"limitar contexto y coste de"| INPUT
    CARDS -->|"ayudar a elegir"| MODEL
    CLOUD -->|"decidir dónde ejecutar"| MODEL
    TRANS -->|"explicar origen de"| VECTOR
    INPUT -->|"se transforma con"| MODEL
    MODEL -->|"fija"| DIM
    DIM -->|"determina ancho de"| VECTOR
    VECTOR -->|"se compara mediante"| METRIC
    METRIC -->|"ordena en"| TOPK
    TOPK -->|"se mide con"| EVAL
    TOPK -->|"puede diversificarse con"| MMR
    TOPK -->|"alimenta"| VECTORDB
    TOPK -->|"aporta contexto a"| RAG
    EVAL -->|"prepara"| RAGEVAL
    VECTOR -->|"puede ser memoria de"| AGMEM

    style INPUT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MODEL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style DIM fill:#F5F5F5,stroke:#000000,stroke-width:2
    style VECTOR fill:#F5F5F5,stroke:#000000,stroke-width:2
    style METRIC fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TOPK fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EVAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MMR fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TOKENS stroke-dasharray: 5 5
    style CARDS stroke-dasharray: 5 5
    style CLOUD stroke-dasharray: 5 5
    style TRANS stroke-dasharray: 5 5
    style VECTORDB stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style RAGEVAL stroke-dasharray: 5 5
    style AGMEM stroke-dasharray: 5 5

Vocabulario aprendido

TérminoDefinición
EmbeddingVector que representa una entrada para compararla con otras.
Búsqueda semánticaBúsqueda que recupera por cercanía aproximada de significado.
Similitud cosenoMedida de alineación entre dos vectores.
Dimensión de embeddingNúmero de componentes numéricos que tiene cada vector.
Top-kPrimeros kk resultados según una puntuación.
Vector normalizadoVector reescalado para tener norma 1.
ANNBúsqueda aproximada de vecinos cercanos.
HNSWÍndice por grafo usado para búsqueda vectorial aproximada.
FAISSBiblioteca de Meta para búsqueda y clustering de vectores.
Recall@kMétrica que mira si aparece un resultado correcto entre los kk primeros.
MRRMétrica que premia que el primer resultado correcto aparezca arriba.
nDCG@kMétrica que evalúa orden y grados de relevancia en los primeros kk resultados.
Hard negativeDocumento parecido pero incorrecto que prueba si el embedding discrimina bien.
MMRTécnica para equilibrar relevancia y diversidad.

Antes de pasar página

  • ¿Puedo explicar qué es un embedding sin decir “significado puro”?
  • ¿Puedo explicar por qué la dimensión afecta memoria, coste y latencia?
  • ¿Puedo calcular un coseno sencillo entre dos vectores?
  • ¿Sé por qué normalizar vectores puede convertir coseno en producto punto?
  • ¿Sé distinguir búsqueda semántica de RAG?
  • ¿Sé qué campos versionar junto a un vector?
  • ¿Sé por qué cambiar de modelo obliga normalmente a reindexar?
  • ¿Sé medir Recall@k, MRR y nDCG con consultas reales?
  • ¿Sé comparar varias dimensiones antes de pagar más almacenamiento?
  • ¿Sé cuándo usar búsqueda exacta y cuándo mirar ANN?
  • ¿Sé por qué filtros y metadata son tan importantes como la similitud?

En resumen

Idea fuerzaDetalle
Un embedding es una coordenada útil.Convierte texto u otros objetos en vectores comparables, no en verdad garantizada.
La dimensión tiene coste de ingeniería.Más dimensiones implican más memoria, cómputo, latencia e índice; solo compensan si mejoran métricas reales.
La búsqueda semántica es ranking.Consulta y documentos se vectorizan, se comparan y se ordenan por una métrica.
La unidad indexada decide mucho.Documento, sección, párrafo o chunk cambian la calidad del resultado.
La escala obliga a elegir índice.Exacto es simple; ANN reduce latencia a cambio de aproximación.
Sin evaluación solo hay intuición.Recall@k, MRR, nDCG y hard negatives separan demo bonita de sistema útil.

Para saber más

Cohere. (2026). Introduction to Embeddings at Cohere. https://docs.cohere.com/v2/docs/embeddings

Google. (2026). Gemini API: Embeddings. https://ai.google.dev/gemini-api/docs/embeddings

Johnson, J., Douze, M. y Jégou, H. (2019). Billion-Scale Similarity Search with GPUs. IEEE Transactions on Big Data, 7(3), 535-547. https://doi.org/10.1109/TBDATA.2019.2921572

Kusupati, A. et al. (2022). Matryoshka Representation Learning. https://arxiv.org/abs/2205.13147

Malkov, Y. A. y Yashunin, D. A. (2020). Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs. IEEE TPAMI, 42(4), 824-836. https://doi.org/10.1109/TPAMI.2018.2889473

Muennighoff, N., Tazi, N., Magne, L. y Reimers, N. (2023). MTEB: Massive Text Embedding Benchmark. https://arxiv.org/abs/2210.07316

OpenAI. (2026). text-embedding-3-large. https://developers.openai.com/api/docs/models/text-embedding-3-large

OpenAI. (2026). Vector embeddings. https://platform.openai.com/docs/guides/embeddings

Reimers, N. y Gurevych, I. (2019). Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. Proceedings of EMNLP, 3982-3992. https://doi.org/10.18653/v1/D19-1410

Sentence Transformers. (2026). Semantic Search. https://sbert.net/examples/applications/semantic-search/README.html

Thakur, N., Reimers, N., Rücklé, A., Srivastava, A. y Gurevych, I. (2021). BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS Datasets and Benchmarks. https://arxiv.org/abs/2104.08663

Voyage AI. (2026). Text Embeddings. https://docs.voyageai.com/docs/embeddings

Notas

  1. OpenAI. (2026). Vector embeddings. https://platform.openai.com/docs/guides/embeddings. Consultado el 25 de mayo de 2026.

  2. OpenAI. (2026). text-embedding-3-large. https://developers.openai.com/api/docs/models/text-embedding-3-large. Consultado el 25 de mayo de 2026.

  3. Google. (2026). Gemini API: Embeddings. https://ai.google.dev/gemini-api/docs/embeddings. Consultado el 25 de mayo de 2026.

  4. Cohere. (2026). Introduction to Embeddings at Cohere. https://docs.cohere.com/v2/docs/embeddings. Consultado el 25 de mayo de 2026.

  5. Voyage AI. (2026). Text Embeddings. https://docs.voyageai.com/docs/embeddings. Consultado el 25 de mayo de 2026.

  6. Sentence Transformers. (2026). Semantic Search. https://sbert.net/examples/applications/semantic-search/README.html. Consultado el 25 de mayo de 2026.

  7. Reimers, N. y Gurevych, I. (2019). Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. Proceedings of EMNLP, 3982-3992. https://doi.org/10.18653/v1/D19-1410.

  8. Johnson, J., Douze, M. y Jégou, H. (2019). Billion-Scale Similarity Search with GPUs. IEEE Transactions on Big Data, 7(3), 535-547. https://doi.org/10.1109/TBDATA.2019.2921572.

  9. Malkov, Y. A. y Yashunin, D. A. (2020). Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs. IEEE TPAMI, 42(4), 824-836. https://doi.org/10.1109/TPAMI.2018.2889473.

  10. Thakur, N., Reimers, N., Rücklé, A., Srivastava, A. y Gurevych, I. (2021). BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS Datasets and Benchmarks. https://arxiv.org/abs/2104.08663.

  11. Kusupati, A. et al. (2022). Matryoshka Representation Learning. https://arxiv.org/abs/2205.13147. El trabajo propone aprender representaciones que funcionan a varias longitudes anidadas.

  12. Cohere (2026), documentación de embeddings citada en la tabla de estado del arte.

  13. Thakur et al. (2021) proponen BEIR para evaluar recuperación en tareas heterogéneas. Muennighoff, N., Tazi, N., Magne, L. y Reimers, N. (2023). MTEB: Massive Text Embedding Benchmark. https://arxiv.org/abs/2210.07316. MTEB compara embeddings en clasificación, clustering, retrieval, reranking, similitud semántica y otras tareas.

Capítulo 08

Facsímil 4 · La caja de herramientas

Capítulo 08: Bases vectoriales, filtros y búsqueda híbrida

Cuando el vector deja de ser el problema

En el capítulo 07 construimos la pieza básica: convertir textos en vectores, compararlos y ordenar resultados. Eso ya permite hacer una búsqueda semántica pequeña. Pero en cuanto el sistema deja de ser una demo, aparece otra pregunta: dónde viven esos vectores, cómo se filtran, cómo se actualizan, cómo se borran y cómo sabemos que el índice no está devolviendo resultados bonitos pero equivocados.

Imagina una universidad con miles de documentos internos. Hay normativa de 2024, 2025 y 2026; manuales para estudiantes y profesorado; documentos públicos y documentos solo visibles para equipos concretos. La consulta "no puedo entrar a Moodle con doble factor" no puede devolver cualquier texto parecido. Debe devolver documentos vigentes, del curso correcto y visibles para la persona que pregunta.

Una base vectorial no es "una carpeta de embeddings". Es el lugar donde se cruzan similitud, filtros, permisos, versiones, índices, latencia, borrado y evaluación. Si esta pieza está mal diseñada, el RAG del capítulo 09 heredará el problema aunque el modelo generativo sea excelente.

Estado del arte con fecha de corte

Fecha de corte: 25 de mayo de 2026.
Fuentes consultadas ese día: documentación oficial de Qdrant, pgvector, Weaviate, Milvus y Pinecone; y trabajos académicos sobre FAISS, HNSW, cuantización de producto, BM25 y fusión de rankings.

Lo estable es la arquitectura: guardar vectores con identificadores, texto y metadata; construir índices; filtrar candidatos; combinar señales densas y léxicas; medir recall y latencia. Lo cambiante son APIs concretas, parámetros, límites por plan, soporte de filtros, algoritmos de índice, modelos integrados y costes de almacenamiento.

FuenteQué aportaCómo usarla
FAISS.1Muestra técnicas de búsqueda eficiente de vectores a gran escala.Para entender por qué no basta hacer producto punto contra todo cuando crece el corpus.
HNSW.2Formaliza un índice por grafo usado por muchas bases vectoriales.Para entender el intercambio entre memoria, construcción, rapidez y recall.
Qdrant.3Explica que índice vectorial e índice de payload resuelven partes distintas del problema.Para no confundir "tengo HNSW" con "mis filtros ya van bien".
pgvector.4Integra búsqueda vectorial dentro de PostgreSQL con operadores de distancia e índices HNSW e IVFFlat.Para proyectos donde transacciones, SQL, joins y vectores conviven en la misma base.
Weaviate.5Documenta búsqueda híbrida que combina vector y BM25F mediante fusión configurable.Para ver el patrón denso + léxico sin construir todo a mano.
Milvus.6Distingue filtrado estándar e iterativo en búsqueda vectorial con metadata.Para razonar sobre filtros complejos y latencia.
Pinecone.7Compara usar un índice híbrido único frente a índices densos y dispersos separados.Para entender que "híbrido" también es una decisión de arquitectura.

Qué no es una base vectorial

Una base vectorial no arregla embeddings malos. Si el modelo coloca cerca documentos que no deberían estarlo, el índice solo acelerará ese error. Tampoco arregla un mal troceado: si guardas párrafos sin título, sin sección y sin fecha, recuperarás fragmentos pobres con mucha rapidez.

Tampoco es una base de conocimiento completa. El vector no sustituye al texto original, a las citas, a las reglas de acceso, al historial de versiones ni al sistema que decide si un documento está vigente. La base vectorial guarda una representación para buscar; la verdad documental sigue viviendo en el contenido y en la metadata.

Y no es siempre la herramienta adecuada. Para cien documentos, una búsqueda exacta en memoria puede bastar. Para datos relacionales con filtros complejos, PostgreSQL con pgvector puede ser suficiente. Para millones de fragmentos, alta concurrencia, filtros frecuentes o múltiples señales de ranking, conviene pensar en una base vectorial dedicada o en un buscador que combine índice invertido y vectores.

Qué sí es: un contrato de recuperación

Ejemplo de fórmula. Un registro vectorial debería parecerse a esto:

ri=(idi,  vi,  textoi,  mi,  versioni)r_i = (id_i,\; v_i,\; texto_i,\; m_i,\; version_i)
SímboloSignificadoEjemplo
rir_iRegistro número ii.Fragmento de una normativa.
idiid_iIdentificador estable.normativa-2026#sec-04.
viv_iVector del fragmento.768 números float32.
textoitexto_iTexto recuperable o puntero al texto.Párrafo que se pasará al RAG.
mim_iMetadata o payload.curso=2026, rol=estudiante, vigente=true.
versioniversion_iVersión de embedding y documento.embed-v3-large@2026-05-25.

La búsqueda con filtro se expresa así:

TopK(q,C,F,k)={r(1),,r(k)}\operatorname{TopK}(q, C, F, k) = \{r_{(1)}, \dots, r_{(k)}\} r(j)C,F(m(j))=1,s(q,v(1))s(q,v(k))r_{(j)} \in C,\quad F(m_{(j)}) = 1,\quad s(q, v_{(1)}) \ge \dots \ge s(q, v_{(k)})
SímboloSignificadoEjemplo
qqVector de la consulta.Embedding de "acceso a Moodle".
CCColección donde buscamos.Fragmentos de documentación interna.
FFFunción de filtro sobre metadata.curso == 2026 and vigente == true.
m(j)m_{(j)}Metadata del resultado en posición jj.Curso, idioma, rol, fuente.
s(q,v)s(q,v)Puntuación de similitud.Coseno o producto punto.
kkNúmero de resultados devueltos.8 fragmentos para un RAG.

Esta fórmula tiene una lección importante: el filtro no es decoración posterior. Forma parte de lo que significa "resultado válido". Si el sistema encuentra el fragmento más parecido del mundo pero no cumple F, ese resultado no debería existir para la consulta.

El coste real: vectores, índices y payload

En el capítulo anterior calculamos el coste bruto de guardar vectores.

Ejemplo de fórmula. Ahora añadimos lo que suele olvidarse:

MtotalNdb+Mıˊndice+Mpayload+MreˊplicasM_{\text{total}} \approx N \cdot d \cdot b + M_{\text{índice}} + M_{\text{payload}} + M_{\text{réplicas}}
SímboloSignificadoEjemplo
NNNúmero de registros vectoriales.10 millones de chunks.
ddDimensión del vector.768 dimensiones.
bbBytes por componente.4 bytes en float32, 2 en float16.
MıˊndiceM_{\text{índice}}Memoria del índice ANN.Grafo HNSW o listas IVF.
MpayloadM_{\text{payload}}Metadata, texto corto, ids y estructuras auxiliares.Fechas, permisos, fuente, idioma.
MreˊplicasM_{\text{réplicas}}Copias por disponibilidad o rendimiento.Dos réplicas duplican parte del coste.

Con 10 millones de vectores de 768 dimensiones en float32, solo el bloque vectorial ocupa alrededor de 30,72 GB. Eso no incluye índice, payload, logs, réplicas, snapshots ni espacio temporal para reconstruir índices. Por eso la dimensión del capítulo 07 y la operación del capítulo 06 vuelven aquí con fuerza.

La selectividad del filtro también importa:

σ(F)={riC:F(mi)=1}C\sigma(F) = \frac{|\{r_i \in C : F(m_i)=1\}|}{|C|}
SímboloSignificadoEjemplo
σ(F)\sigma(F)Fracción de la colección que pasa el filtro.0,02 si quedan 2 de cada 100.
$C$
${...}$

Un filtro con σ=0,9\sigma=0{,}9 apenas reduce el problema. Uno con σ=0,001\sigma=0{,}001 puede romper supuestos del índice aproximado si no está bien planificado. Las bases vectoriales serias dedican mucha ingeniería a combinar índice vectorial e índice de metadata porque las dos piezas tiran en direcciones distintas.

Cómo funciona por dentro

Una base vectorial tiene dos rutas principales: ingesta y consulta. En ingesta recibe texto, genera o recibe embeddings, valida el esquema, guarda payload y actualiza índices. En consulta recibe una pregunta, genera el vector de consulta, aplica filtros, busca candidatos, fusiona señales si hay búsqueda híbrida y devuelve resultados con puntuaciones y metadata.

Base vectorial: similitud, filtros y búsqueda híbrida El resultado válido no es solo cercano: debe cumplir metadata, versión, permisos y evaluación. 1. Ingesta Documento fuente y versión Chunk unidad buscable Embedding vector denso Payload curso, rol, fecha 2. Almacenamiento Registro: id + vector + texto + payload + versión el id permite upsert, borrado, trazas y auditoría Índice ANN HNSW, IVF, PQ Índice payload fecha, rol, tenant BM25 léxico 3. Operación Snapshots recuperación Reindexar nuevo modelo Borrado sin residuos Trazas consulta y ids 4. Consulta híbrida con filtros Pregunta texto + usuario Vector denso semántica Términos BM25 / sparse Plan de filtro curso, rol, fecha selectividad Top vectorial ANN o exacto Top léxico BM25 Fusion RRF o pesos Rerank citas, permisos evidencia final Medir contra búsqueda exacta: recall@k, nDCG@k, p95, resultados borrados, resultados fuera de filtro y coste por consulta. IA para gente curiosa / Facsímil 04 / Capítulo 08 / 686f6c61

La imagen resume el punto central: una consulta real atraviesa dos mundos. Por un lado está la cercania semántica; por otro, el contrato operativo que decide si ese resultado se puede usar. El producto final no debería aceptar un resultado solo porque su vector está cerca.

Índices: exacto, HNSW, IVFFlat y compresión

La búsqueda exacta compara la consulta con todos los vectores. Es fácil de razonar y sirve como referencia de calidad, pero su coste crece con NdN \cdot d. A partir de cierto tamaño, necesitamos índices aproximados de vecinos cercanos.

OpciónIdeaParámetros típicosQué se mide
ExactaComparar contra todos los vectores que pasan el filtro.Sin índice ANN.Calidad máxima, latencia base.
HNSWNavegar un grafo de vecinos por capas.M, ef_construction, ef_search.Recall frente a memoria y p95.
IVFFlatDividir vectores en listas y buscar solo algunas.lists, probes.Recall frente a velocidad y coste de build.
PQComprimir vectores por subespacios.Número de subcuantizadores y bits.Ahorro de memoria frente a pérdida de precisión.

HNSW suele dar buen equilibrio de recall y latencia, pero no es gratis: guarda conexiones entre vectores y consume memoria adicional.8 IVFFlat puede construir más rápido y ocupar menos, pero requiere elegir listas y probes con cuidado; la documentación de pgvector lo explica como un intercambio entre rendimiento y recall.9 La cuantización de producto reduce memoria representando subespacios con códigos compactos, pero introduce aproximación adicional.10

Un criterio práctico: conserva siempre un modo de evaluación exacta, aunque sea sobre una muestra. Si no puedes comparar el índice aproximado contra el ranking exacto, no sabes si ganar latencia te está costando evidencias importantes.

Filtros: dónde se gana o se rompe la recuperación

Los filtros parecen sencillos hasta que crece el corpus. Filtrar por curso=2026 es fácil; filtrar por tenant, rol, vigente, idioma, producto, region, tipo_documento y fecha ya obliga a planificar.

Hay tres patrones frecuentes:

PatrónCómo funcionaRiesgo
Filtrar antesPrimero reduce candidatos por metadata y luego busca vectores.Si queda muy poco, el índice ANN puede no aportar mucho.
Buscar antesPrimero recupera muchos vecinos y luego descarta por metadata.Puede perder resultados válidos si estaban fuera del primer lote.
Filtrado integradoEl índice combina metadata y navegación vectorial.Requiere crear buenos índices de payload y entender selectividad.

Qdrant lo dice de forma muy clara: el índice vectorial acelera la búsqueda vectorial y los índices de payload aceleran filtros; hacen trabajos distintos.11 Milvus también separa filtrado estándar e iterativo porque filtros complejos pueden cambiar mucho la latencia.12

Para entenderlo, piensa en una biblioteca. Si buscas "reglamento de prácticas" en toda la biblioteca y luego tiras los libros antiguos, quizá no veas el reglamento correcto porque no entró en el primer top-k. Si primero entras en la estantería "2026" y después buscas por significado, reduces ruido. Pero si la estantería tiene solo tres documentos, un recorrido exacto puede ser mejor que un índice sofisticado.

Búsqueda híbrida: cuando exactitud léxica y semántica se necesitan

Los embeddings son fuertes con sinónimos e intención. BM25 es fuerte con palabras raras, siglas, códigos, nombres propios y errores donde la palabra exacta importa. La búsqueda híbrida combina ambas señales.

BM25, simplificado, puntúa una consulta QQ sobre un documento DD así:

BM25(D,Q)=tQIDF(t)f(t,D)(k1+1)f(t,D)+k1(1b+bDavgdl)\operatorname{BM25}(D,Q) = \sum_{t \in Q} \operatorname{IDF}(t) \frac{f(t,D)(k_1 + 1)} {f(t,D) + k_1(1-b+b\frac{|D|}{avgdl})}
SímboloSignificadoEjemplo
ttTérmino de la consulta.MFA, Moodle, matrícula.
f(t,D)f(t,D)Veces que aparece el término en el documento.3 apariciones de Moodle.
IDF(t)\operatorname{IDF}(t)Peso de rareza del término en la colección.MFA pesa más que el.
$D$
avgdlavgdlLongitud media de documentos.220 tokens.
k1,bk_1, bParámetros de saturación y longitud.Valores habituales: k1=1,2k_1=1{,}2, b=0,75b=0{,}75.

BM25 viene de la familia probabilística de recuperación de información y sigue siendo una base fuerte para búsqueda léxica.13 La parte densa recupera significado; la parte léxica protege términos que no deberían diluirse.

Una forma sencilla de fusionar rankings es RRF:

RRF(d)=j=1S1k0+rankj(d)\operatorname{RRF}(d) = \sum_{j=1}^{S} \frac{1}{k_0 + \operatorname{rank}_j(d)}
SímboloSignificadoEjemplo
ddDocumento candidato.doc-01.
SSNúmero de sistemas que devuelven ranking.Vectorial y BM25: S=2S=2.
rankj(d)\operatorname{rank}_j(d)Posicion del documento en el sistema jj.1 en BM25, 5 en vectorial.
k0k_0Constante que suaviza el peso de la posición.60 es un valor comun en RRF.

RRF funciona bien porque no exige que las puntuaciones de BM25 y embeddings estén en la misma escala.14 Eso es práctico: un coseno de 0,72 y un BM25 de 11,4 no son directamente comparables, pero sus posiciones en rankings sí pueden combinarse.

ConsultaVector denso ayudaBM25 ayudaHibrido evita
"no puedo entrar al campus"Encuentra "restablecer acceso a Moodle".Poco, si no comparte palabras.Quedarse solo con sinónimos.
"error SAML 403 Moodle"Puede entender "login".Protege SAML y 403.Perder códigos exactos.
"matrícula 2026 septiembre"Relaciona matrícula con calendario.Protege 2026 y septiembre.Devolver normativa antigua.
"API pagos webhook reintentos"Relaciona integración y eventos.Protege webhook.Mezclar artículos de producto.

Weaviate expone búsqueda híbrida como combinacion de resultados vectoriales y BM25F por fusión configurable.15 Pinecone documenta dos caminos: índice híbrido único o índices densos y dispersos separados, cada uno con sus ventajas operativas.16 La idea pedagógica es la misma: no hay que elegir religión entre vector y palabras exactas; hay que medir cuál combina mejor en tu corpus.

Diseñar el esquema de una colección

Antes de indexar, conviene escribir el contrato. Una colección debería responder estas preguntas:

DecisiónPregunta técnicaMala señal
IdentificadorQué id estable permite reindexar sin duplicar?IDs aleatorios sin relación con fuente y sección.
TextoGuardamos texto completo o puntero?Resultados sin cita recuperable.
MetadataQué campos se filtran de verdad?Guardar metadata bonita que nunca se indexa.
VersiónQué modelo, dimensión y fecha genero el vector?Mezclar embeddings incompatibles.
PermisosEl filtro de acceso vive en la consulta?Filtrar después de mostrar candidatos.
VigenciaCómo caduca o se reemplaza un documento?Resultados de años anteriores en top-k.
BorradoQué significa borrar: vector, payload, texto y cache?Quedan fragmentos recuperables por accidente.

Si usas PostgreSQL con pgvector, el esquema puede ser explícito:

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE chunks (
  id text PRIMARY KEY,
  source_id text NOT NULL,
  chunk_text text NOT NULL,
  embedding vector(768) NOT NULL,
  curso integer NOT NULL,
  rol text NOT NULL,
  vigente boolean NOT NULL,
  embedding_model text NOT NULL,
  indexed_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX chunks_embedding_hnsw
ON chunks USING hnsw (embedding vector_cosine_ops);

CREATE INDEX chunks_metadata
ON chunks (curso, rol, vigente);

SELECT id, chunk_text
FROM chunks
WHERE curso = 2026
  AND rol IN ('estudiante', 'publico')
  AND vigente = true
ORDER BY embedding <=> $1
LIMIT 8;

El detalle importante no es memorizar esta sintaxis. El detalle es que el campo vectorial, los filtros y la versión viven juntos. Si cambias el modelo de embedding o la dimensión, no estás "actualizando una columna"; estás cambiando el espacio de búsqueda.

Cómo trabajar con bases vectoriales con criterio

Una base vectorial en producción necesita disciplina operativa. La parte difícil no es insertar el primer vector; es mantener el sistema correcto cuando cambian documentos, permisos, modelos y volumen.

PrácticaQué hacesPor qué importa
IDs deterministasDerivas el id de fuente, sección y versión.Permite upsert idempotente y evita duplicados.
Versionado de embeddingsGuardas modelo, dimensión, normalización y fecha.Permite reindexar y comparar variantes.
Doble índice temporalConstruyes el nuevo índice junto al anterior.Evita cortar servicio mientras migras.
Borrado verificableCompruebas que ids borrados no vuelven en top-k.Evita respuestas con contenido retirado.
Filtros obligatoriosEl backend añade filtros de permisos siempre.El cliente no decide qué puede ver.
Evaluación continuaMides recall, p95, coste y errores por segmento.Detecta degradación antes que usuarios y usuarias.
Trazas de retrievalGuardas consulta, filtros, ids, scores y versión.Permite explicar por qué se recuperó algo.

Una buena pregunta de ingeniería: si mañana cambiamos de all-MiniLM-L6-v2 a otro modelo de 1024 dimensiones, ¿qué pasos exactos hay que hacer? Si la respuesta no incluye reindexado, evaluación, cambio de versión y plan de retirada del índice anterior, falta diseño.

Evaluar una base vectorial

Aquí evaluamos dos capas: la calidad de recuperación y la calidad operativa. La primera pregunta es "encuentro lo correcto?". La segunda es "lo encuentro dentro del contrato de producto?".

MétricaQué mideCómo se calcula
Recall ANN@kCuánto se parece el índice aproximado al exacto.Resultados ANN frente a búsqueda exacta.
Recall con filtro@kSi aparecen documentos correctos cumpliendo metadata.Casos con filtros obligatorios.
nDCG@kSi los documentos más útiles suben arriba.Relevancia graduada por posición.
p50, p95, p99Latencia normal y de cola.Tiempos por consulta real.
Tasa de resultados retiradosCuántos resultados ya no deberían aparecer.Hits con vigente=false o versión antigua.
Cobertura de permisosSi cada consulta aplica el filtro correcto.Trazas con usuario, rol y condición.
Coste por consultaCPU, memoria, GPU o precio cloud.Coste mensual dividido por consultas útiles.

Una prueba mínima compara tres rankings para las mismas consultas: exacto filtrado, ANN filtrado e híbrido filtrado. Si el ANN pierde documentos que el exacto encuentra, ajustas parámetros o cambias índice. Si el híbrido mejora consultas con siglas pero empeora consultas naturales, ajustas fusión o decides cuándo activarlo.

Dónde volverá a aparecer

ConceptoDónde vuelvePara qué
ChunkingCapítulo 09.Elegir qué unidades guardamos en la base vectorial.
Citas y abstenciónCapítulo 09.No basta recuperar; hay que responder con evidencia.
Evaluación de RAGCapítulo 10.Conectar retrieval con calidad final de respuesta.
Agentic RAGCapítulo 11.Decidir cuándo hacer varias búsquedas o rutas.
Memoria de agentesFacsímil 05.Guardar recuerdos recuperables con filtros y caducidad.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Creer que vector DB equivale a RAGLa base recupera candidatos; no decide chunking, citas, abstención ni respuesta final.Separar retrieval, contexto y generación.
Filtrar después de recuperar pocoSi pides top-10 global y luego descartas por permisos, puedes quedarte sin el resultado correcto.Aplicar filtros como parte del plan de búsqueda.
No versionar embeddingsMezclar modelos o dimensiones hace que las distancias dejen de significar lo mismo.Guardar embedding_model, dimensión y fecha en cada registro.
Olvidar términos exactosSiglas, códigos, IDs y nombres propios pueden perderse en búsqueda solo densa.Probar búsqueda híbrida con BM25 o sparse vectors.
Medir solo latencia mediap95 o p99 pueden ser malos aunque la media parezca aceptable.Medir percentiles y separar consultas con filtros complejos.
No probar borradosUn índice puede seguir devolviendo contenido retirado si el flujo de borrado falla.Crear tests de ids retirados y verificar que no aparecen.
Elegir herramienta por modaCada base cambia filtros, operación, costes, backup y SQL disponible.Comparar con una matriz de requisitos reales.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a construir una mini base vectorial en memoria. No pretende competir con Qdrant, pgvector o Milvus; sirve para entender el contrato: documentos, metadata, filtros, ranking denso, BM25, fusión RRF y evaluación.

La práctica usa embeddings deterministas muy simples para no depender de una API externa. En un proyecto real, sustituirías vector_de_texto por el modelo de embeddings del capítulo 07, y el almacenamiento en memoria por una base vectorial real.

Guarda esto como mini_base_vectorial.py:

from collections import Counter, defaultdict
import hashlib
import math
import re
import unicodedata


DIM = 32
K_RRF = 60

DOCUMENTOS = [
    {
        "id": "doc-01",
        "titulo": "Acceso al campus virtual con doble factor",
        "texto": "Moodle requiere MFA y recuperacion de contraseña.",
        "curso": 2026,
        "rol": "estudiante",
        "vigente": True,
    },
    {
        "id": "doc-02",
        "titulo": "Calendario de matricula 2026",
        "texto": "La ampliacion de matricula se abre en septiembre.",
        "curso": 2026,
        "rol": "publico",
        "vigente": True,
    },
    {
        "id": "doc-03",
        "titulo": "Correo institucional y doble factor",
        "texto": "El correo se desbloquea revisando MFA y contraseña.",
        "curso": 2026,
        "rol": "estudiante",
        "vigente": True,
    },
    {
        "id": "doc-04",
        "titulo": "Acceso antiguo al campus virtual",
        "texto": "Procedimiento obsoleto para Moodle en 2024.",
        "curso": 2024,
        "rol": "estudiante",
        "vigente": False,
    },
    {
        "id": "doc-05",
        "titulo": "Manual de Moodle para profesorado",
        "texto": "Crear cuestionarios, bancos de preguntas y rubricas.",
        "curso": 2026,
        "rol": "profesorado",
        "vigente": True,
    },
]

CASOS = [
    {
        "consulta": "no puedo entrar a moodle con mfa",
        "filtro": {"curso": 2026, "vigente": True},
        "esperado": {"doc-01"},
    },
    {
        "consulta": "fechas de matricula septiembre",
        "filtro": {"curso": 2026, "vigente": True},
        "esperado": {"doc-02"},
    },
    {
        "consulta": "correo bloqueado doble factor",
        "filtro": {"curso": 2026, "vigente": True},
        "esperado": {"doc-03"},
    },
]

SINONIMOS = {
    "moodle": "campus",
    "aula": "campus",
    "virtual": "campus",
    "mfa": "doble_factor",
    "factor": "doble_factor",
    "2fa": "doble_factor",
    "entrar": "acceso",
    "acceder": "acceso",
    "bloqueado": "desbloqueo",
    "desbloquear": "desbloqueo",
}


def normalizar_texto(texto):
    texto = texto.lower()
    texto = unicodedata.normalize("NFD", texto)
    texto = "".join(c for c in texto if unicodedata.category(c) != "Mn")
    tokens = re.findall(r"[a-z0-9_]+", texto)
    return [SINONIMOS.get(t, t) for t in tokens]


def vector_token(token):
    digest = hashlib.sha256(token.encode("utf-8")).digest()
    valores = []
    for i in range(DIM):
        byte = digest[i % len(digest)]
        valores.append((byte / 255.0) * 2 - 1)
    return valores


def normalizar_vector(vector):
    norma = math.sqrt(sum(x * x for x in vector)) or 1.0
    return [x / norma for x in vector]


def vector_de_texto(texto):
    vector = [0.0] * DIM
    for token in normalizar_texto(texto):
        base = vector_token(token)
        vector = [a + b for a, b in zip(vector, base)]
    return normalizar_vector(vector)


def producto_punto(a, b):
    return sum(x * y for x, y in zip(a, b))


def cumple_filtro(doc, filtro):
    return all(doc.get(campo) == valor for campo, valor in filtro.items())


def construir_indice(documentos):
    textos = [d["titulo"] + ". " + d["texto"] for d in documentos]
    tokens_por_doc = [normalizar_texto(t) for t in textos]
    df = defaultdict(int)
    for tokens in tokens_por_doc:
        for token in set(tokens):
            df[token] += 1
    return {
        "vectores": [vector_de_texto(t) for t in textos],
        "tokens": tokens_por_doc,
        "df": df,
        "avgdl": sum(len(t) for t in tokens_por_doc) / len(tokens_por_doc),
    }


def bm25_score(query_tokens, doc_tokens, df, avgdl):
    k1 = 1.2
    b = 0.75
    total_docs = len(DOCUMENTOS)
    frecuencias = Counter(doc_tokens)
    score = 0.0

    for token in query_tokens:
        if token not in frecuencias:
            continue
        numerador = total_docs - df[token] + 0.5
        denominador_idf = df[token] + 0.5
        idf = math.log(1 + numerador / denominador_idf)
        tf = frecuencias[token]
        longitud = len(doc_tokens)
        denominador = tf + k1 * (1 - b + b * longitud / avgdl)
        score += idf * (tf * (k1 + 1)) / denominador

    return score


def ranking_denso(consulta, documentos, indice, filtro):
    consulta_vec = vector_de_texto(consulta)
    filas = []
    for pos, doc in enumerate(documentos):
        if not cumple_filtro(doc, filtro):
            continue
        score = producto_punto(consulta_vec, indice["vectores"][pos])
        filas.append((doc["id"], score))
    return sorted(
        filas,
        key=lambda fila: fila[1],
        reverse=True,
    )


def ranking_bm25(consulta, documentos, indice, filtro):
    query_tokens = normalizar_texto(consulta)
    filas = []
    for pos, doc in enumerate(documentos):
        if not cumple_filtro(doc, filtro):
            continue
        score = bm25_score(
            query_tokens,
            indice["tokens"][pos],
            indice["df"],
            indice["avgdl"],
        )
        filas.append((doc["id"], score))
    return sorted(
        filas,
        key=lambda fila: fila[1],
        reverse=True,
    )


def rrf(rankings):
    acumulado = defaultdict(float)
    for ranking in rankings:
        for posicion, (doc_id, _score) in enumerate(ranking, start=1):
            acumulado[doc_id] += 1 / (K_RRF + posicion)
    return sorted(
        acumulado.items(),
        key=lambda fila: fila[1],
        reverse=True,
    )


def recall_at_k(ranking, esperados, k):
    recuperados = {doc_id for doc_id, _score in ranking[:k]}
    return bool(recuperados & esperados)


def main():
    indice = construir_indice(DOCUMENTOS)
    aciertos = 0

    for caso in CASOS:
        denso = ranking_denso(
            caso["consulta"],
            DOCUMENTOS,
            indice,
            caso["filtro"],
        )
        lexico = ranking_bm25(
            caso["consulta"],
            DOCUMENTOS,
            indice,
            caso["filtro"],
        )
        hibrido = rrf([denso, lexico])
        aciertos += recall_at_k(hibrido, caso["esperado"], k=3)

        print("Consulta:", caso["consulta"])
        print("Filtro:", caso["filtro"])
        print("Top denso:", denso[:3])
        print("Top BM25:", lexico[:3])
        print("Top hibrido:", hibrido[:3])
        print()

    print("Recall hibrido@3:", round(aciertos / len(CASOS), 3))

    sin_filtro = ranking_denso(
        "entrar a moodle",
        DOCUMENTOS,
        indice,
        filtro={},
    )
    print("Sin filtro de vigencia:", sin_filtro[:3])


if __name__ == "__main__":
    main()

Salida esperada aproximada:

Consulta: no puedo entrar a moodle con mfa
Filtro: {'curso': 2026, 'vigente': True}
Top denso: [('doc-01', ...), ('doc-03', ...), ...]
Top BM25: [('doc-01', ...), ...]
Top hibrido: [('doc-01', ...), ...]

Recall hibrido@3: 1.0
Sin filtro de vigencia: [('doc-01', ...), ('doc-04', ...), ...]

La última línea es el aprendizaje. El documento antiguo puede parecer cercano porque habla de Moodle y acceso. Sin filtro de vigencia, el sistema puede recuperar algo semánticamente razonable y funcionalmente incorrecto.

Prueba tres cambios: añade rol="profesorado" al filtro, cambia K_RRF, y crea un documento con el código exacto SAML 403. Verás cuándo BM25 salva una consulta y cuándo el filtro cambia el ranking más que el embedding.

Cómo encaja todo

graph TD
    subgraph "Capítulo 8: bases vectoriales"
        COLLECTION["Colección"]
        RECORD["Registro: id, vector<br/>texto, payload"]
        VINDEX["Índice vectorial"]
        PINDEX["Índice de payload"]
        SPARSE["Índice léxico<br/>o sparse"]
        FILTER["Filtro obligatorio"]
        HYBRID["Búsqueda híbrida"]
        FUSION["Fusión RRF<br/>o pesos"]
        OPS["Upsert, borrado<br/>reindexado"]
        EVAL["Recall, nDCG<br/>p95 y coste"]
    end
    subgraph "Viene de antes"
        EMB["Embeddings (F4C7)"]
        DIM["Dimensión y coste (F4C7)"]
        API["APIs y contratos (F4C2)"]
        LOCAL["Cloud o local (F4C6)"]
    end
    subgraph "Sigue después"
        CHUNK["Chunking (F4C9)"]
        RAG["RAG básico (F4C9)"]
        RAGEVAL["Evaluar RAG (F4C10)"]
        AGMEM["Memoria de agentes (F5)"]
    end

    EMB -->|"generar"| RECORD
    DIM -->|"dimensionar"| VINDEX
    API -->|"definir contrato de"| OPS
    LOCAL -->|"decidir despliegue de"| COLLECTION
    COLLECTION -->|"contener"| RECORD
    RECORD -->|"alimentar"| VINDEX
    RECORD -->|"alimentar"| PINDEX
    RECORD -->|"alimentar"| SPARSE
    PINDEX -->|"aplicar"| FILTER
    VINDEX -->|"devolver candidatos"| HYBRID
    SPARSE -->|"aportar señales exactas"| HYBRID
    FILTER -->|"limitar candidatos"| HYBRID
    HYBRID -->|"combinar con"| FUSION
    FUSION -->|"medir con"| EVAL
    OPS -->|"mantener"| COLLECTION
    COLLECTION -->|"guardar unidades de"| CHUNK
    FUSION -->|"proveer contexto a"| RAG
    EVAL -->|"preparar"| RAGEVAL
    RECORD -->|"servir como memoria para"| AGMEM

    style COLLECTION fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RECORD fill:#F5F5F5,stroke:#000000,stroke-width:2
    style VINDEX fill:#F5F5F5,stroke:#000000,stroke-width:2
    style PINDEX fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SPARSE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style FILTER fill:#F5F5F5,stroke:#000000,stroke-width:2
    style HYBRID fill:#F5F5F5,stroke:#000000,stroke-width:2
    style FUSION fill:#F5F5F5,stroke:#000000,stroke-width:2
    style OPS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EVAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EMB stroke-dasharray: 5 5
    style DIM stroke-dasharray: 5 5
    style API stroke-dasharray: 5 5
    style LOCAL stroke-dasharray: 5 5
    style CHUNK stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style RAGEVAL stroke-dasharray: 5 5
    style AGMEM stroke-dasharray: 5 5

Vocabulario aprendido

TérminoDefinición
Base vectorialSistema que guarda vectores y permite recuperarlos por similitud.
ColecciónConjunto de registros con el mismo contrato de vector y metadata.
PayloadMetadata asociada al vector, usada para filtrar y explicar resultados.
Índice vectorialEstructura que acelera la búsqueda de vecinos cercanos.
HNSWÍndice por grafo navegable para búsqueda aproximada.
IVFFlatÍndice que divide el espacio en listas y busca solo algunas.
FiltroCondición que limita qué registros pueden entrar en el ranking.
BM25Ranking léxico basado en frecuencia, rareza y longitud de documento.
Búsqueda híbridaCombinación de ranking vectorial y ranking léxico.
RRFFusión de rankings basada en posiciones.
UpsertInserción o actualización idempotente de un registro.

Antes de pasar página

  • ¿Puedo explicar por qué una base vectorial no arregla embeddings malos?
  • ¿Sé qué debe guardar un registro vectorial además del vector?
  • ¿Puedo calcular el coste bruto de memoria para NN, dd y bytes por componente?
  • ¿Sé explicar qué mide la selectividad de un filtro?
  • ¿Sé distinguir búsqueda exacta, HNSW, IVFFlat y PQ?
  • ¿Sé por qué filtrar después de recuperar poco puede fallar?
  • ¿Sé cuándo BM25 aporta algo que el embedding puede perder?
  • ¿Puedo explicar RRF sin comparar puntuaciones incompatibles?
  • ¿Sé qué campos versionar para reindexar con seguridad?
  • ¿Sé qué métricas mirar además de recall: p95, coste y resultados retirados?

En resumen

Idea fuerzaDetalle
Una base vectorial es un contrato operativo.Guarda vectores, texto, ids, metadata, versiones e índices.
El filtro forma parte de la respuesta correcta.Un resultado cercano pero fuera de curso, rol o vigencia no es válido.
El índice aproximado debe compararse con exacto.Sin baseline exacto, no sabes cuánto recall pierdes por ganar latencia.
La búsqueda híbrida une dos señales.Embeddings capturan significado; BM25 protege términos exactos.
Operar importa tanto como buscar.Upsert, borrado, snapshots, reindexado y trazas deciden si el sistema aguanta.
La evaluación debe incluir producto.Recall, nDCG, filtros, p95, coste y documentos retirados cuentan juntos.

Para saber más

Cormack, G. V., Clarke, C. L. A. y Buettcher, S. (2009). Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR, 758-759. https://doi.org/10.1145/1571941.1572114

Jégou, H., Douze, M. y Schmid, C. (2011). Product Quantization for Nearest Neighbor Search. IEEE Transactions on Pattern Analysis and Machine Intelligence, 33(1), 117-128. https://doi.org/10.1109/TPAMI.2010.57

Johnson, J., Douze, M. y Jégou, H. (2019). Billion-Scale Similarity Search with GPUs. IEEE Transactions on Big Data, 7(3), 535-547. https://doi.org/10.1109/TBDATA.2019.2921572

Malkov, Y. A. y Yashunin, D. A. (2020). Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs. IEEE TPAMI, 42(4), 824-836. https://doi.org/10.1109/TPAMI.2018.2889473

Milvus. (2026). Filtered Search. https://milvus.io/docs/filtered-search.md

pgvector. (2026). pgvector: Open-source vector similarity search for Postgres. https://github.com/pgvector/pgvector

Pinecone. (2026). Hybrid search. https://docs.pinecone.io/docs/hybrid-search-and-sparse-vectors

Qdrant. (2026). Indexing. https://qdrant.tech/documentation/manage-data/indexing/

Robertson, S. y Zaragoza, H. (2009). The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 3(4), 333-389. https://doi.org/10.1561/1500000019

Weaviate. (2026). Hybrid search. https://docs.weaviate.io/weaviate/search/hybrid

Notas

  1. Johnson, J., Douze, M. y Jégou, H. (2019). Billion-Scale Similarity Search with GPUs. IEEE Transactions on Big Data, 7(3), 535-547. https://doi.org/10.1109/TBDATA.2019.2921572.

  2. Malkov, Y. A. y Yashunin, D. A. (2020). Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs. IEEE TPAMI, 42(4), 824-836. https://doi.org/10.1109/TPAMI.2018.2889473.

  3. Qdrant. (2026). Indexing. https://qdrant.tech/documentation/manage-data/indexing/. Consultado el 25 de mayo de 2026.

  4. pgvector. (2026). pgvector: Open-source vector similarity search for Postgres. https://github.com/pgvector/pgvector. Consultado el 25 de mayo de 2026.

  5. Weaviate. (2026). Hybrid search. https://docs.weaviate.io/weaviate/search/hybrid. Consultado el 25 de mayo de 2026.

  6. Milvus. (2026). Filtered Search. https://milvus.io/docs/filtered-search.md. Consultado el 25 de mayo de 2026.

  7. Pinecone. (2026). Hybrid search. https://docs.pinecone.io/docs/hybrid-search-and-sparse-vectors. Consultado el 25 de mayo de 2026.

  8. Malkov y Yashunin, 2020.

  9. pgvector, 2026.

  10. Jégou, H., Douze, M. y Schmid, C. (2011). Product Quantization for Nearest Neighbor Search. IEEE TPAMI, 33(1), 117-128. https://doi.org/10.1109/TPAMI.2010.57.

  11. Qdrant, 2026.

  12. Milvus, 2026.

  13. Robertson, S. y Zaragoza, H. (2009). The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 3(4), 333-389. https://doi.org/10.1561/1500000019.

  14. Cormack, G. V., Clarke, C. L. A. y Buettcher, S. (2009). Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR, 758-759. https://doi.org/10.1145/1571941.1572114.

  15. Weaviate, 2026.

  16. Pinecone, 2026.

Capítulo 09

Facsímil 4 · La caja de herramientas

Capítulo 09: RAG básico: chunking, retrieval, citas y abstención

El momento en que buscar ya no basta

En el capítulo 08 aprendimos a guardar fragmentos, buscarlos con vectores, filtrarlos por metadata y combinar señal densa con BM25. Eso devuelve candidatos. Un RAG hace algo más delicado: convierte esos candidatos en una respuesta útil, citada y capaz de decir “no tengo evidencia suficiente”.

Imagina un asistente para alumnado. La persona pregunta: “¿puedo ampliar matrícula en septiembre si tengo pagos pendientes?”. El sistema no debería inventar una política general. Debe recuperar normativa vigente, decidir qué fragmentos entran en contexto, responder solo con lo que esos fragmentos sostienen y enseñar las fuentes.

RAG significa retrieval-augmented generation: generación aumentada por recuperación. La idea fue formulada como una forma de combinar modelos generativos con memoria no paramétrica recuperada desde un índice externo.1 En producto, RAG no es una palabra bonita para “chat con PDFs”. Es una arquitectura para que el modelo responda con información que no tiene por entrenamiento, que cambia con el tiempo o que pertenece a una organización concreta.

Cuando decimos “memoria no paramétrica” estamos diciendo algo muy concreto: el conocimiento no está guardado dentro de los pesos del modelo. Está fuera, en documentos, tablas, índices, bases de datos o sistemas que podemos actualizar sin volver a entrenar el modelo.

Estado del arte con fecha de corte

Fecha de corte: 25 de mayo de 2026.
Fuentes consultadas ese día: documentación oficial de OpenAI File Search, LangChain, LlamaIndex, Haystack, Google Vertex AI RAG Engine, Azure AI Search, Pinecone, Weaviate, Qdrant y pgvector; y trabajos académicos sobre RAG, BM25 y fusión de rankings.

Lo estable es el patrón: ingestión, partición, indexado, recuperación, construcción de contexto, generación, citas y evaluación. Lo cambiante son proveedores, límites, APIs, modelos de embeddings, formatos soportados, coste, residencia de datos, rerankers y herramientas gestionadas.

FuenteQué aportaCómo usarla
RAG original.2Formaliza el uso de documentos recuperados como memoria externa para generación.Para entender que RAG no es solo “meter documentos en un prompt”.
OpenAI File Search.3Herramienta gestionada de la Responses API para buscar archivos en vector stores y devolver contexto al modelo.Para montar rápido un RAG alojado sin programar todo el retrieval.
LangChain Retrieval.4Describe loaders, splitters, embeddings, vector stores, retrievers y arquitecturas 2-step, agentic e híbridas.Para componer aplicaciones RAG con piezas intercambiables.
LlamaIndex RAG.5Ordena RAG en loading, indexing, storing, querying y evaluation; introduce Documents, Nodes y retrievers.Para proyectos centrados en ingestión de datos y gestión de índices.
Haystack pipelines.6Modela RAG como grafo de componentes con ramas, validación y flujos de indexado/consulta.Para equipos que quieren pipelines explícitos y desplegables.
Vertex AI RAG Engine.7Servicio gestionado de RAG dentro de Vertex AI.Para entornos Google Cloud donde operación, permisos y plataforma pesan.
Azure AI Search.8Combina búsqueda tradicional, vectorial, semántica, filtros e integración con escenarios generativos.Para organizaciones ya montadas sobre Azure y Microsoft Learn.
Pinecone y Weaviate.9Bases vectoriales gestionadas con patrones RAG, búsqueda semántica, filtros y opciones híbridas.Para delegar índice y escalado sin entregar el diseño del sistema.
BM25 y RRF.10Dan bases sólidas para retrieval léxico y fusión de rankings.Para no construir RAG solo con embeddings.

Qué no es RAG

RAG no es subir un PDF a un chat y confiar. Si el PDF se trocea mal, si la búsqueda trae el fragmento equivocado o si el prompt permite responder sin evidencia, el sistema puede fallar con mucha seguridad aparente.

Tampoco es fine-tuning. Ajustar un modelo puede enseñar formato, tono o una tarea repetida; RAG aporta contexto externo en tiempo de consulta. Si la información cambia cada semana, suele ser mejor actualizar documentos e índice que reentrenar.

RAG tampoco elimina la necesidad de permisos. Recuperar un fragmento privado y luego pedir al modelo que “no lo mencione” es una mala frontera. El filtrado debe ocurrir antes de que el texto entre en contexto.

RAG, memoria y entrenamiento no son lo mismo

Esta diferencia es fundamental. Si la mezclamos, acabamos usando RAG para lo que pide entrenamiento, fine-tuning para lo que pide documentos, o “memoria” para cosas que deberían ser permisos, trazas o contexto recuperado.

El modelo base tiene conocimiento en sus pesos. Eso viene del entrenamiento: grandes cantidades de datos, mucho cálculo y una actualización difícil de repetir para una aplicación pequeña. Cuando haces fine-tuning o LoRA, sigues tocando comportamiento aprendido: formato, estilo, patrones de respuesta, especialización en una tarea repetida. RAG, en cambio, no cambia los pesos. Recupera evidencia externa en el momento de la pregunta.

MecanismoDónde vive la informaciónCuándo cambiaPara qué sirve
Entrenamiento baseEn los pesos del modelo.Antes de que tú uses el modelo.Capacidades generales: lenguaje, razonamiento, código, patrones del mundo.
Fine-tuning / LoRAEn pesos ajustados o adaptadores.Cuando entrenas con ejemplos.Formato, tono, clasificación estable, tareas repetidas y medibles.
RAGEn un corpus externo recuperable.Cuando cambian documentos o índices.Conocimiento privado, vigente, citable o cambiante.
Memoria de conversaciónEn mensajes previos o resúmenes guardados.Durante una sesión o entre sesiones si se persiste.Preferencias, contexto personal y continuidad conversacional.
CachéEn una capa técnica de reutilización.Mientras sea válida.Ahorrar coste o latencia; no añade conocimiento nuevo.
ToolEn un sistema externo consultado o ejecutado.En tiempo real.Calcular, buscar estado vivo, escribir en sistemas o consultar APIs.

Ejemplo sencillo: si una universidad cambia la normativa de matrícula, no quieres reentrenar un modelo. Quieres actualizar el documento, reindexar y que las respuestas citen la normativa nueva. Si, en cambio, el problema es que el modelo siempre devuelve un JSON con campos mal nombrados, RAG no lo arregla por sí solo: quizá necesitas mejor contrato de salida, ejemplos, validación o ajuste.

La memoria conversacional tampoco equivale a RAG. Si el usuario dice “soy estudiante de segundo”, eso puede vivir como memoria o como contexto de sesión. Pero si pregunta “¿qué dice la normativa vigente sobre ampliación?”, eso debe salir del corpus, con fecha y cita. La memoria puede ayudar a formular la consulta; no debe sustituir la fuente.

Por qué usar un RAG

Usamos RAG cuando responder bien exige recuperar evidencia externa. No es una moda de arquitectura; es una respuesta a una limitación práctica: los modelos no traen dentro todos tus documentos, no conocen todos los cambios recientes y no pueden demostrar por sí solos de dónde sale una afirmación.

MotivoQué problema resuelveSeñal de que RAG encaja
Información cambianteEl conocimiento se actualiza sin tocar pesos.Normativas, catálogos, precios internos, políticas y manuales vivos.
Conocimiento privadoEl modelo no vio tus documentos durante entrenamiento.Intranets, tickets, expedientes, documentación de producto.
Citas y auditoríaLa respuesta puede revisarse contra fuentes.Necesitas enseñar página, sección, fecha o documento.
PermisosCada persona puede recuperar solo lo que le corresponde.Hay roles, grupos, tenants, cursos, clientes o áreas.
Coste de actualizaciónReindexar suele ser más barato que reentrenar.El corpus cambia más que el comportamiento deseado.
Especialización ligeraEl modelo general se apoya en contexto de dominio.La tarea pide lenguaje natural más conocimiento concreto.
DepuraciónPuedes ver qué documentos entraron en la respuesta.Necesitas saber si falló búsqueda, contexto o generación.

También hay casos donde RAG no es la primera respuesta.

SituaciónMejor primera herramienta
El modelo no sigue el formato.Prompt, ejemplos, salida estructurada o fine-tuning.
La respuesta depende de cálculo exacto.Tool o código, no solo documentos.
La información vive en una base de datos transaccional.SQL, API o tool; quizá luego RAG para explicar resultados.
La tarea es siempre igual y estable.Fine-tuning/LoRA puede ser más eficiente.
El corpus está desordenado o sin dueño.Gobernanza documental antes de RAG.

Qué sí es: una cadena verificable

Un RAG mínimo tiene dos procesos separados. El primero prepara el conocimiento; el segundo responde una pregunta.

ProcesoOcurre cuándoQué produce
IndexaciónAntes de la consulta, cuando entran o cambian documentos.Chunks con embeddings, texto, metadata y versión.
ConsultaCuando una persona pregunta.Respuesta con citas, o abstención si no hay evidencia suficiente.

Ejemplo de fórmula. La consulta completa puede expresarse así:

q=fθ(x)q = f_{\theta}(x) Rk=TopK(q,C,F,k)R_k = \operatorname{TopK}(q, C, F, k) y=gϕ(x,Contexto(Rk))y = g_{\phi}(x, \operatorname{Contexto}(R_k))
SímboloSignificadoEjemplo
xxPregunta original.“¿puedo ampliar matrícula en septiembre?”.
fθf_{\theta}Modelo de embeddings.Convierte la pregunta en vector.
qqVector de la pregunta.768 números.
CCColección de chunks indexados.Normativa, FAQs y manuales.
FFFiltros obligatorios.curso=2026, vigente=true, rol=estudiante.
RkR_kFragmentos recuperados.Top 6 chunks después de filtros.
gϕg_{\phi}Modelo generativo.LLM que redacta la respuesta.
yyRespuesta final.Texto con citas o abstención.

La parte peligrosa está en Contexto(Rk)\operatorname{Contexto}(R_k). No todos los chunks recuperados deben entrar en el prompt. Hay que ordenar, recortar, quitar duplicados, proteger permisos, preservar citas y dejar hueco para instrucciones y respuesta.

Términos que no podemos dar por sabidos

La jerga de RAG engaña porque muchas palabras parecen pequeñas y en realidad esconden decisiones de sistema. “Chunk”, “metadata” o “top-k” no son etiquetas académicas: son puntos donde puedes ganar o perder calidad, coste, privacidad y capacidad de depuración.

La primera familia de términos aparece antes de preguntar nada. Es la parte de preparación del conocimiento.

TérminoQué significa de verdadEn el ejemplo de matrícula
CorpusConjunto de fuentes que el sistema tiene permiso para consultar. No es “todo lo que existe”; es lo que has decidido meter en el sistema.Normativa 2026, FAQ de secretaría, calendario académico y manual de trámites.
FuenteDocumento o sistema original del que sale la información. Puede ser un PDF, una web, una tabla o una base de datos.normativa_matricula_2026.pdf.
DocumentoRepresentación interna de una fuente. Suele incluir texto, título, fecha, URL, propietario y versión.La normativa convertida a texto con su fecha de publicación.
ParserPrograma que extrae texto y estructura. Si el parser lee mal una tabla, el RAG recuperará texto defectuoso.Sacar de un PDF el artículo, el título y los apartados en orden correcto.
OCRReconocimiento óptico de caracteres. Convierte una imagen o escaneo en texto.Una normativa escaneada que no permite seleccionar texto.
ChunkFragmento recuperable. Debe ser suficientemente pequeño para buscar bien y suficientemente completo para citarlo.Un artículo completo sobre ampliación de matrícula.
TokenUnidad interna aproximada de texto que usan modelos y muchos contadores de coste. No siempre coincide con palabra.“matrícula” puede ocupar más de un token según el tokenizador.
SolapeParte repetida entre dos chunks consecutivos para no cortar ideas.Repetir el encabezado y la frase anterior cuando un artículo pasa de una ventana a otra.
MetadataDatos sobre el chunk, no necesariamente contenido para responder. Sirve para filtrar, citar y operar.curso=2026, vigente=true, seccion=matricula, pagina=12.
ACLReglas de acceso. Indican quién puede recuperar un chunk antes de que llegue al modelo.Alumnado ve normativa pública; personal interno ve notas de gestión.
HashHuella calculada del texto. Si el documento cambia, cambia el hash.Detectar que se subió una normativa nueva aunque mantenga el mismo nombre de archivo.
Versión de corpusIdentificador de qué conjunto de documentos e índices se usó. Es clave para reproducir una respuesta.matricula-2026-v3, usado el 25 de mayo de 2026.

La segunda familia aparece cuando alguien pregunta. Aquí conviene separar “buscar parecido” de “encontrar evidencia”.

TérminoQué significa de verdadQué decisión exige
QueryConsulta que entra al retrieval. Puede ser la pregunta original o una versión reescrita.Decidir si buscas literalmente “pagos pendientes” o reformulas con sinónimos.
Query rewriteReescritura de la pregunta para recuperar mejor. Es útil, pero debe registrarse.Convertir “¿puedo ampliar?” en “ampliación de matrícula septiembre pagos pendientes”.
EmbeddingVector numérico que representa un texto para comparar significado aproximado.Elegir modelo, dimensión, coste y cuándo recalcular vectores.
DimensiónNúmero de coordenadas del embedding. Más dimensión no garantiza mejor resultado; cambia memoria, coste e índice.Un embedding de 768 dimensiones ocupa menos que uno de 3.072, pero puede rendir distinto.
ÍndiceEstructura que permite buscar rápido. Sin índice, compararías contra todo el corpus en cada pregunta.Crear índice vectorial, índice léxico o ambos.
Vector storeAlmacén que guarda vectores, texto y metadata para buscar por similitud y filtros.Qdrant, pgvector, Pinecone, Weaviate o File Search.
FTSFull-text search: búsqueda textual clásica por palabras, operadores y relevancia.Encontrar “artículo 14” o “pagos pendientes” aunque el embedding no lo priorice.
BM25Fórmula de ranking léxico. Premia términos relevantes y penaliza documentos donde una palabra aparece por aparecer.Si la pregunta dice “septiembre”, BM25 ayuda a subir chunks que contienen esa palabra exacta.
ANNApproximate nearest neighbors. Técnica para buscar vectores parecidos sin comparar todos contra todos.Acelerar búsqueda en miles o millones de chunks aceptando una aproximación controlada.
Top-kNúmero de candidatos devueltos. k=5 trae cinco chunks antes de rerank o contexto.Si k es bajo, quizá pierdes evidencia; si es alto, sube ruido y coste.
ScorePuntuación de similitud o relevancia. No siempre es comparable entre métodos.No comparar sin más un coseno 0,78 con un BM25 12,4.
RRFFusión por posiciones. Combina rankings usando el puesto de cada documento, no su score bruto.Mezclar búsqueda vectorial y BM25 sin calibrar escalas distintas.
RerankerModelo o regla que reordena candidatos después de una primera búsqueda rápida.Pasar de 50 candidatos baratos a 6 fragmentos buenos para el prompt.
FiltroRestricción obligatoria antes o durante la búsqueda. No es una preferencia.vigente=true, curso=2026, rol=estudiante.

La tercera familia aparece cuando ya hay candidatos y el sistema debe responder. Aquí es donde un RAG deja de ser buscador y se convierte en producto.

TérminoQué significa de verdadQué se revisa en producción
Context builderPieza que decide qué chunks entran en el prompt y con qué formato.Deduplicación, orden, citas, presupuesto y prioridad de fuentes.
Presupuesto de contextoLímite de tokens disponible para instrucciones, pregunta, evidencia y respuesta.No gastar 90% del contexto en fragmentos repetidos.
CitaEnlace entre una afirmación y el fragmento que la sostiene.Que [F1] apunte a source_id, página, sección y hash.
GroundingGrado en que la respuesta está apoyada en la evidencia recuperada.Si la frase importante se puede subrayar en una fuente.
AbstenciónRespuesta correcta cuando falta evidencia suficiente.Decir qué dato falta en vez de rellenarlo con probabilidad.
Umbral τ\tauValor mínimo de soporte para responder. No se elige a ojo; se calibra con evaluación.Responder si soporte >= 0,65 y abstenerse si queda por debajo.
TrazaRegistro completo de una consulta. Sin traza, no sabes dónde falló.Pregunta, filtros, top-k, scores, prompt, respuesta, modelo y coste.
Recall@kMétrica: si la evidencia necesaria aparece entre los k primeros resultados.Si la respuesta correcta necesitaba un artículo y no aparece en top 10, falló retrieval.
nDCGMétrica que premia que los resultados más útiles aparezcan arriba.No basta con traer el chunk correcto; conviene que llegue en primeras posiciones.

Una forma sencilla de recordarlo: el corpus decide qué puede saber el sistema; el retrieval decide qué encuentra; el context builder decide qué lee el modelo; y la abstención decide qué no debe fingir.

Elementos importantes de un RAG

Un RAG no es una sola pieza. Es una cadena. Si una parte falla, el resultado final puede parecer correcto y estar mal apoyado. Por eso conviene nombrar los elementos con precisión.

ElementoPregunta que respondeError típico
Corpus¿Qué fuentes entran y cuáles quedan fuera?Indexar documentos sin vigencia, duplicados o sin propietario.
Ingesta¿Cómo entran los documentos al sistema?Subir archivos manualmente sin versiones ni borrado.
Parsing¿El texto extraído conserva estructura?Perder tablas, encabezados, notas o páginas.
Chunking¿Cuál es la unidad recuperable?Cortar ideas por tamaño fijo sin respetar secciones.
Metadata¿Cómo filtro, cito y opero cada chunk?Guardar solo texto y no poder filtrar por fecha, rol o fuente.
Embeddings¿Cómo busco por significado aproximado?Usar un modelo sin evaluar idioma, dominio, dimensión y coste.
Índice léxico¿Cómo busco palabras exactas?Confiar solo en vectores y perder códigos, fechas o términos raros.
Retrieval híbrido¿Cómo combino significado, palabras y filtros?Mezclar scores incompatibles sin fusión ni trazas.
Reranking¿Cómo reordeno candidatos prometedores?Meter al prompt los primeros resultados sin segunda revisión.
Context builder¿Qué evidencia entra al prompt?Pasar demasiados chunks, repetidos o sin citas.
Generación¿Cómo redacta el modelo con restricciones?Permitir respuesta sin evidencia o sin formato de cita.
Abstención¿Cuándo no se responde?Contestar siempre aunque el corpus no contenga la respuesta.
Evaluación¿Cómo sé si mejora?Medir solo “me gusta” y no recall, groundedness, coste o latencia.
Observabilidad¿Cómo depuro cada consulta?No guardar query, filtros, ranking, contexto y respuesta.

El orden importa. No tiene sentido discutir modelos grandes si el parser rompe tablas. No tiene sentido ajustar el prompt si el retrieval no trae el artículo correcto. No tiene sentido comprar una base vectorial si nadie sabe qué documentos están vigentes.

Dimensiones en un RAG

En RAG, “dimensión” puede significar dos cosas. La primera es matemática: la dimensión del embedding. La segunda es de diseño: las dimensiones que debes controlar para que el sistema funcione.

La dimensión matemática es el número de coordenadas del vector. Si un chunk se convierte en un embedding de dd dimensiones, queda así:

e(c)=[e1,e2,e3,,ed]e(c) = [e_1, e_2, e_3, \dots, e_d]
SímboloSignificadoEjemplo
ccChunk que queremos representar.Artículo sobre ampliación de matrícula.
e(c)e(c)Embedding del chunk.Vector guardado en el índice.
ddNúmero de dimensiones.768, 1.024, 1.536 o 3.072, según modelo.
eie_iValor de una coordenada.Un número flotante aprendido por el modelo.

Estas dimensiones no son columnas humanas como “matrícula”, “pago” o “septiembre”. Son coordenadas aprendidas. El significado está distribuido por muchas posiciones a la vez. Por eso no se interpreta una dimensión aislada como si fuera una etiqueta; se compara el vector completo con otros vectores.

La similitud suele medirse con coseno:

sim(a,b)=abab\operatorname{sim}(a,b)= \frac{a \cdot b}{\lVert a \rVert \lVert b \rVert}
IdeaQué implica
Más dimensiones no significa automáticamente mejor RAG.Puede mejorar representación, pero también memoria, latencia y coste.
No mezcles modelos de embeddings en el mismo índice sin control.Dos modelos pueden tener dimensiones y geometrías incompatibles.
Si cambias de modelo de embeddings, normalmente reindexas.Los vectores antiguos ya no viven en el mismo espacio.
La dimensión afecta almacenamiento.Más coordenadas por chunk implica más bytes y más trabajo para el índice.
La dimensión afecta recuperación, no generación directamente.Ayuda a encontrar evidencia; no hace que el LLM razone mejor por sí sola.

El coste bruto de guardar vectores puede aproximarse así:

bytesN×d×b\operatorname{bytes} \approx N \times d \times b
SímboloSignificadoEjemplo
NNNúmero de chunks.100.000 chunks.
ddDimensiones por embedding.1.536 dimensiones.
bbBytes por valor.4 bytes en float32, 2 bytes en float16.

Con 100.000 chunks, 1.536 dimensiones y float32, solo los vectores ocupan aproximadamente 614 MB antes de contar metadata, texto, índices auxiliares y réplicas. Esta cuenta no decide la arquitectura, pero te obliga a pensar como ingeniero: dimensión, volumen, tipo numérico, latencia y presupuesto van juntos.

La segunda lectura de “dimensiones” es de diseño. Un RAG se optimiza mirando varias dimensiones a la vez:

Dimensión de diseñoPregunta
Calidad del corpus¿Los documentos son correctos, vigentes y no duplicados?
Recuperación¿La evidencia necesaria aparece en top-k?
Contexto¿El prompt recibe lo justo, ordenado y citado?
Generación¿El modelo responde con contrato, citas y abstención?
Evaluación¿Sabemos medir si mejora o empeora?
Operación¿Podemos actualizar, borrar, auditar y controlar coste?

Chunking: partir sin romper el significado

Un chunk es la unidad que el sistema puede recuperar. Si es demasiado pequeño, pierde contexto. Si es demasiado grande, arrastra ruido y ocupa mucho prompt. La unidad correcta depende del documento y de la pregunta.

Ejemplo de fórmula. Si partimos un documento de LL tokens en ventanas de tamaño ww con solape oo, una aproximación del número de chunks es:

n1+max(0,Lw)won \approx 1 + \left\lceil \frac{\max(0, L-w)}{w-o} \right\rceil
SímboloSignificadoEjemplo
LLLongitud del documento.2.400 tokens.
wwTamaño de chunk.350 tokens.
ooSolape entre chunks.60 tokens.
wow-oAvance real de cada ventana.290 tokens.
nnNúmero aproximado de chunks.9 chunks.

El solape evita cortar una idea justo en la frontera. Pero el solape también duplica texto, embeddings y coste. Si todo solapa demasiado, el índice se llena de fragmentos casi iguales y el top-k pierde diversidad.

Tipo de documentoChunk inicial razonableQué cuidar
FAQ cortaUna pregunta-respuesta por chunk.Mantener la pregunta original en el texto.
NormativaArtículo, sección o bloque con título.Guardar fecha, versión, capítulo y vigencia.
Manual técnicoSección con pasos completos.No separar requisito, comando y salida esperada.
Contrato o políticaCláusula completa con encabezado.Preservar definiciones y excepciones.
CódigoFunción, clase o bloque lógico.Mantener ruta, lenguaje y dependencias cercanas.

Un buen chunk no es “350 tokens”. Un buen chunk es una pieza que, leída sola, todavía puede sostener una respuesta concreta.

Soluciones de terceros: qué comprar, qué montar y qué no delegar

Hay varias formas de montar RAG. La decisión no es “framework sí o no”. La decisión es qué parte quieres delegar y qué parte necesitas controlar.

FamiliaEjemplosTe quita trabajo enTe deja responsable de
RAG gestionado por proveedorOpenAI File Search, Vertex AI RAG Engine.Vector store, búsqueda, integración con modelo, parte de la operación.Calidad de documentos, permisos, evaluación, costes y trazabilidad.
Framework de orquestaciónLangChain, LlamaIndex, Haystack.Loaders, splitters, retrievers, pipelines, integraciones.Diseño del flujo, selección de componentes y despliegue.
Base vectorial / buscadorQdrant, pgvector, Pinecone, Weaviate, Milvus, Elasticsearch/OpenSearch.Almacenamiento, índice, filtros, latencia de búsqueda.Chunking, generación, citas, abstención y evaluación final.
Parsing y preparaciónExtractores PDF, OCR, conversores HTML, pipelines ETL.Sacar texto de documentos difíciles.Validar tablas, orden de lectura, duplicados y metadatos.
Observabilidad y evaluaciónLangSmith, evaluadores propios, trazas internas.Registro, comparación y análisis de runs.Definir qué significa “respuesta correcta” en tu dominio.

OpenAI File Search es útil si quieres empezar rápido con archivos y vector stores gestionados dentro de la Responses API.11 LangChain encaja cuando quieres componer loaders, splitters, retrievers, vector stores y varios estilos de RAG.12 LlamaIndex brilla cuando el centro del problema es ingestión, nodos, índices y consulta sobre datos propios.13 Haystack es especialmente claro si quieres pensar en pipelines como grafos de componentes conectados y validables.14

La regla práctica: compra o usa framework para acelerar, pero no delegues el criterio. Ninguna herramienta sabe por defecto qué documentos están vigentes, qué permiso tiene cada persona, qué cita es suficiente o cuándo conviene abstenerse.

Antes de elegir una solución de terceros, conviene escribir una pequeña ADR técnica. No hace falta una novela; hace falta que nadie confunda “funciona en demo” con “lo podemos operar”.

Pregunta de ingenieríaPor qué importaSeñal de buena respuesta
¿Cómo actualiza y borra documentos?RAG falla mucho cuando el índice conserva versiones antiguas.Hay upsert, borrado por documento, reindexado y versión de corpus.
¿Dónde aplico permisos?El texto no autorizado no debe entrar al contexto.Filtros por usuario, grupo, tenant, vigencia y clasificación antes del top-k.
¿Puedo combinar vector, BM25 y filtros?Muchas preguntas reales mezclan significado, palabras exactas y metadata.Retrieval híbrido con scores visibles y filtros que no rompen latencia.
¿Qué devuelve como cita?Sin source_id, página, sección o hash, revisar una respuesta es difícil.Cada fragmento trae identificador estable, título, fecha y localización.
¿Puedo ver trazas?Si no ves chunks, scores y prompt, no puedes depurar.Logs por query, ranking, contexto final, coste y respuesta.
¿Puedo cambiar de modelo?El embedding de hoy puede no ser el de mañana.Índices versionados por modelo y dimensión; migración repetible.
¿Qué pasa con tablas e imágenes?Mucha documentación útil no es texto plano.Parsing verificable, OCR cuando toca y preservación de estructura.
¿Cómo se evalúa?Una demo bonita no mide recall ni groundedness.Set de preguntas, respuestas esperadas, citas esperadas y regresión automática.
¿Cómo salgo de la herramienta?El bloqueo aparece cuando tus datos solo viven en su formato.Exportación de chunks, metadata, vectores y trazas.

Una ruta razonable para empezar es esta:

Contexto del equipoRuta inicialCuándo cambiar
Quieres validar una idea en días.File Search o RAG gestionado equivalente.Cuando necesites permisos complejos, índices propios o trazas más finas.
Tienes app Python/TypeScript y datos variados.LangChain, LlamaIndex o Haystack con Qdrant, pgvector, Pinecone o Weaviate.Cuando el framework esconda demasiado o el flujo ya sea estable.
Ya estás en Google Cloud o Azure.Vertex AI RAG Engine o Azure AI Search.Cuando residencia, permisos, facturación y operación sean prioridad.
Necesitas control total y bajo coste.pgvector/Qdrant autogestionado, pipeline propio y evaluación propia.Cuando el volumen o el equipo pidan servicio gestionado.

Arquitectura mínima de un primer RAG

Primer RAG serio: recuperar, responder, citar y abstenerse Dos flujos separados: preparar conocimiento y contestar con evidencia trazable. A. Indexación offline Se ejecuta cuando cambia el corpus. Fuentes PDF · HTML · DB Parser / OCR orden de lectura Chunking sección + solape Metadata ACL · fecha · hash Embedding modelo + dimensión Índice léxico BM25 / FTS Salida de indexación chunk_id · source_id · vector · tokens metadata · versión · hash de documento Almacén recuperable vector store + texto + BM25 + filtros Vectores coseno / ANN Texto BM25 / FTS Filtros rol · fecha Versiones modelo · corpus B. Consulta online Se ejecuta en cada pregunta. Pregunta texto + usuario Consulta rewrite + filtros Top-k híbrido vector + BM25 RRF / Rerank orden final Contexto presupuesto LLM respuesta Contrato de salida respuesta + citas verificables o abstención si soporte < τ C. Lo que debe quedar trazado ranking: dense score · BM25 · RRF · rerank · filtros aplicados cita: source_id · chunk_id · página/sección · hash · fecha de corpus evaluación: recall@k · nDCG · groundedness · latencia · coste decisión: si soporte(Rk, x) < τ, no se responde inventando Un RAG serio no promete saberlo todo: promete enseñar de dónde sale lo que dice. IA para gente curiosa / Facsímil 04 / Capítulo 09 / 686f6c61

El diagrama separa lo que suele mezclarse. Indexar es preparar el material. Consultar es decidir qué evidencia entra. Responder es redactar, citar y abstenerse si el material no basta.

Cómo montar un primer RAG de verdad

Un primer RAG serio no empieza por el modelo. Empieza por el contrato de respuesta.

PasoDecisiónSalida verificable
1. Elegir corpusQué documentos entran y cuáles no.Lista de fuentes, versiones y propietarios.
2. Extraer textoCómo leer PDF, HTML, Markdown o base de datos.Texto limpio con orden de lectura revisado.
3. Partir en chunksUnidad recuperable y citable.Chunks con source_id, sección, fecha y hash.
4. IndexarEmbeddings, BM25, filtros y vector store.Índice versionado y reproducible.
5. RecuperarTop-k, filtros y búsqueda híbrida.Lista de chunks con scores y metadata.
6. Construir contextoQué entra al prompt y con qué formato.Contexto numerado con fuentes.
7. GenerarInstrucciones para responder con evidencia.Respuesta con citas o abstención.
8. RegistrarGuardar query, chunks, respuesta y versión.Traza para depurar y evaluar.

Un prompt mínimo de RAG debe separar instrucciones y contexto. El modelo no debe tratar los documentos como órdenes, sino como material de consulta:

Responde usando solo el contexto incluido.
Si el contexto no contiene evidencia suficiente, responde:
"No tengo evidencia suficiente en las fuentes disponibles."

Pregunta:
{pregunta}

Contexto:
[F1] {fragmento_1}
[F2] {fragmento_2}

Formato:
- Respuesta breve.
- Citas entre corchetes, por ejemplo [F1].
- Si hay duda, explica qué dato falta.

La cita no es decoración. Es una interfaz de confianza: permite revisar si la frase que el modelo escribió está realmente en las fuentes.

Citas y abstención

Ejemplo de fórmula. Podemos definir una regla simple:

responder(x)={gϕ(x,Rk),si soporte(Rk,x)τ abstenerse,si soporte(Rk,x)<τ\operatorname{responder}(x)= \begin{cases} g_{\phi}(x, R_k), & \text{si } soporte(R_k, x) \ge \tau \ \operatorname{abstenerse}, & \text{si } soporte(R_k, x) < \tau \end{cases}
SímboloSignificadoEjemplo
xxPregunta.“¿Puedo ampliar matrícula con pagos pendientes?”.
RkR_kFragmentos recuperados.Seis chunks tras filtros.
soporte(Rk,x)soporte(R_k,x)Evidencia disponible para responder.0,82 si hay fragmentos claros.
τ\tauUmbral mínimo para responder.0,65 en una primera prueba.
gϕg_{\phi}Modelo que redacta.LLM con prompt de citas.

Ese soporte puede empezar siendo una regla: top score mínimo, al menos una fuente vigente y presencia de términos críticos. Más adelante puede ser un evaluador aprendido o un evaluador con rúbrica. Lo importante es que la abstención no sea una vergüenza; es una conducta correcta cuando falta evidencia.

SituaciónQué debe hacer el RAGPor qué
Hay una fuente vigente y clara.Responder y citar.La evidencia sostiene la respuesta.
Hay fuentes parecidas pero de años distintos.Responder solo si el filtro de vigencia lo resuelve.El parecido semántico no basta.
Hay dos fuentes que se contradicen.Mostrar conflicto o abstenerse.Elegir una en silencio rompe confianza.
No aparece evidencia directa.Abstenerse y decir qué falta.Mejor que rellenar huecos con probabilidad.
La pregunta pide acción externa.Recuperar contexto, pero delegar la acción a una tool.RAG informa; no ejecuta trámites por sí mismo.

Cómo optimizar bien un RAG

Optimizar RAG no significa tocar un parámetro al azar hasta que una demo parezca mejor. Significa separar dónde falla el sistema y medir cada etapa. Si una respuesta sale mal, la causa puede estar en el corpus, el parser, el chunking, el embedding, el índice, el reranker, el contexto, el prompt o la ausencia de una regla de abstención.

La primera regla es crear un pequeño conjunto de evaluación antes de optimizar. No hace falta empezar con mil preguntas. Puedes empezar con 30 o 50 preguntas reales, cada una con la fuente esperada y una explicación de qué debería responder el sistema. Sin ese conjunto, cada cambio se evalúa con intuición y la intuición se cansa rápido.

CapaQué optimizarCómo medirlo
CorpusFuentes vigentes, sin duplicados y con propietario.Porcentaje de documentos con fecha, versión, dueño y estado.
ParsingTexto correcto, tablas preservadas, orden de lectura.Revisión manual de muestras y errores por tipo de documento.
ChunkingUnidad completa, citable y no demasiado ruidosa.Recall@k por tamaño de chunk y tasa de chunks duplicados.
MetadataFiltros útiles y citas revisables.Porcentaje de chunks con source_id, página, sección, fecha y ACL.
EmbeddingsModelo adecuado a idioma, dominio y coste.Recall@k, latencia, coste por millón de chunks y memoria.
Retrieval híbridoCombinar significado, palabras exactas y filtros.Comparar vector solo, BM25 solo, híbrido y RRF.
RerankingSubir la evidencia buena antes del prompt.nDCG@k, MRR y coste añadido por consulta.
Context builderMeter evidencia suficiente sin ruido.Precisión de citas, tokens de contexto y duplicados.
PromptResponder con contrato, citas y abstención.Groundedness, formato válido y tasa de abstención correcta.
OperaciónActualizar, borrar, trazar y controlar coste.Tiempo de reindexado, errores, latencia p95 y coste por respuesta.

Una receta práctica para optimizar sería:

  1. Congela un corpus pequeño y versionado.
  2. Escribe preguntas de evaluación con sus fuentes esperadas.
  3. Mide retrieval antes de mirar la respuesta del LLM.
  4. Ajusta chunking y metadata hasta que la evidencia aparezca arriba.
  5. Añade BM25 o búsqueda híbrida si pierdes términos exactos.
  6. Añade reranker si recuperas bien pero ordenas mal.
  7. Recorta contexto y deduplica antes de tocar el prompt.
  8. Obliga a citar y abstenerse con un contrato de salida.
  9. Guarda trazas de cada consulta para comparar versiones.
  10. Cambia una cosa cada vez; si cambias cinco, no sabes qué funcionó.

Hay una trampa frecuente: intentar arreglar con prompt lo que es un fallo de retrieval. Si el chunk correcto no entra en contexto, el modelo no puede citarlo. Otra trampa es subir top-k sin control. Traer más chunks puede mejorar recall, pero también mete ruido, sube coste y aumenta la probabilidad de mezclar fuentes.

SíntomaDiagnóstico probablePrimer ajuste
La respuesta suena bien pero no cita la fuente correcta.Retrieval o context builder fallan.Revisar top-k, filtros, metadata y orden de contexto.
Recupera documentos antiguos.Falta filtro de vigencia o borrado.Añadir vigente=true, versión de corpus y política de retirada.
Pierde códigos, nombres propios o fechas.Vector solo no basta.Añadir BM25/FTS y fusión RRF.
Recupera muchos chunks parecidos.Solape excesivo o duplicados.Deduplicar por hash, sección o similitud entre chunks.
Responde cuando no sabe.Falta abstención o umbral.Definir soporte mínimo y respuesta de insuficiencia.
Es lento.Índice, reranker o contexto demasiado grandes.Medir p95, reducir candidatos, cachear embeddings y ajustar ANN.

¿Solo texto? Qué más puede entrar en un RAG

Texto es el punto de partida porque el LLM consume tokens y porque muchos documentos terminan convertidos a texto. Pero RAG no tiene por qué limitarse a texto plano. Lo importante es convertir cada fuente en una representación recuperable, filtrable y citable.

Tipo de informaciónCómo entra al RAGQué hay que cuidar
PDF y documentosTexto extraído, páginas, títulos y chunks.Orden de lectura, notas, tablas, encabezados y versión.
TablasFilas, columnas, celdas relevantes o resumen estructurado.No perder unidades, claves, fechas ni relación fila-columna.
Bases de datosResultados de SQL o vistas preparadas.Permisos, frescura, consultas reproducibles y explicación del resultado.
ImágenesOCR, descripciones, embeddings multimodales o regiones anotadas.Distinguir texto visible, objetos, gráficos y metadatos.
AudioTranscripción, marcas de tiempo y hablantes.Errores de transcripción, idioma, ruido y citas por minuto/segundo.
VídeoTranscripción, fotogramas clave, escenas y marcas temporales.Recuperar el momento exacto, no solo un resumen genérico.
CódigoFunciones, clases, rutas, tests y documentación cercana.Mantener dependencias, imports, versión y lenguaje.
Logs y ticketsEventos normalizados, campos, tiempos y etiquetas.Ruido, duplicados, retención y datos sensibles.
Grafos u ontologíasNodos, relaciones, triples y consultas de grafo.No convertir relaciones precisas en texto ambiguo.
Resultados de toolsSalidas de APIs, cálculos o búsquedas vivas.Separar evidencia recuperada de acciones ejecutadas.

Cuando una fuente no es texto, tienes dos estrategias. La primera es traducirla a texto fiel: OCR, transcripción, descripción de imagen, explicación de tabla. La segunda es usar embeddings específicos: embeddings de imagen, multimodales, de audio o de código. En sistemas reales se mezclan ambas: una imagen puede tener OCR, una descripción y un vector multimodal; una tabla puede tener filas indexadas y, además, una consulta SQL cuando hace falta precisión.

La pregunta de ingeniería no es “¿puedo meterlo en RAG?”, sino “¿puedo recuperar la parte correcta, respetar permisos, citarla y comprobarla?”. Si no puedes citar una celda, una página, una región de imagen, un minuto de audio o una fila de base de datos, todavía no tienes una evidencia robusta.

Ruta rápida con una solución gestionada

Si quieres montar algo rápido sobre archivos, una opción es usar File Search con vector stores gestionados. La idea es: crear vector store, subir archivos y dejar que la herramienta de búsqueda recupere contexto para la llamada al modelo.15

from openai import OpenAI

client = OpenAI()

store = client.vector_stores.create(name="normativa-universidad")

client.vector_stores.files.upload_and_poll(
    vector_store_id=store.id,
    file=open("normativa_matricula_2026.pdf", "rb"),
)

respuesta = client.responses.create(
    model="gpt-4.1-mini",
    input=(
        "¿Puedo ampliar matrícula en septiembre "
        "si tengo pagos pendientes? Cita las fuentes."
    ),
    tools=[
        {
            "type": "file_search",
            "vector_store_ids": [store.id],
            "max_num_results": 6,
        }
    ],
)

print(respuesta.output_text)

Esto sirve para prototipar o para casos donde te compensa delegar infraestructura. Pero incluso aquí quedan decisiones tuyas: qué archivos subes, cómo versionas, cómo retiras documentos antiguos, qué permisos aplicas, cómo revisas citas y cómo evalúas si la respuesta es correcta.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Ahora montamos un primer RAG local, sin depender de APIs. No será un LLM completo: el generador será extractivo para que puedas verificar cada paso. A cambio, verás lo esencial: chunking, retrieval híbrido, contexto, citas y abstención.

Guarda esto como rag_minimo_citado.py:

from collections import Counter, defaultdict
import hashlib
import math
import re
import unicodedata


DIM = 32
K_RRF = 60
TOP_K = 4
SIN_EVIDENCIA = (
    "No tengo evidencia suficiente en las fuentes disponibles."
)

DOCUMENTOS = [
    {
        "id": "norm-2026",
        "titulo": "Normativa de matrícula 2026",
        "texto": (
            "La ampliación de matrícula se abre en septiembre. "
            "El estudiante puede solicitar ampliación si no mantiene "
            "pagos pendientes vencidos. La solicitud se revisa desde "
            "secretaría virtual."
        ),
        "curso": 2026,
        "vigente": True,
    },
    {
        "id": "faq-campus",
        "titulo": "Acceso al campus virtual",
        "texto": (
            "Si no puedes entrar al campus virtual, revisa el doble "
            "factor y restablece la contraseña desde la página "
            "de acceso."
        ),
        "curso": 2026,
        "vigente": True,
    },
    {
        "id": "norm-2024",
        "titulo": "Normativa antigua de matrícula",
        "texto": (
            "En 2024 la ampliación de matrícula no revisaba pagos "
            "pendientes antes de enviar la solicitud."
        ),
        "curso": 2024,
        "vigente": False,
    },
]

SINONIMOS = {
    "moodle": "campus",
    "virtual": "campus",
    "entrar": "acceso",
    "ampliar": "ampliacion",
    "matricula": "matricula",
    "matrícula": "matricula",
    "pago": "pagos",
    "pendiente": "pendientes",
}

STOPWORDS = {
    "a",
    "al",
    "como",
    "con",
    "cual",
    "cuando",
    "de",
    "del",
    "desde",
    "el",
    "en",
    "es",
    "la",
    "las",
    "lo",
    "los",
    "me",
    "no",
    "o",
    "para",
    "por",
    "puedo",
    "que",
    "se",
    "si",
    "un",
    "una",
    "y",
}


def normalizar(texto):
    texto = texto.lower()
    texto = unicodedata.normalize("NFD", texto)
    texto = "".join(
        c for c in texto if unicodedata.category(c) != "Mn"
    )
    tokens = re.findall(r"[a-z0-9_]+", texto)
    return [SINONIMOS.get(t, t) for t in tokens]


def tokens_de_contenido(texto):
    return [t for t in normalizar(texto) if t not in STOPWORDS]


def partir_en_chunks(documento, max_palabras=42, solape=8):
    palabras = documento["texto"].split()
    avance = max_palabras - solape
    chunks = []

    for inicio in range(0, len(palabras), avance):
        bloque = palabras[inicio: inicio + max_palabras]
        if not bloque:
            continue
        chunk_id = f"{documento['id']}#c{len(chunks) + 1}"
        chunks.append(
            {
                "id": chunk_id,
                "source_id": documento["id"],
                "titulo": documento["titulo"],
                "texto": " ".join(bloque),
                "curso": documento["curso"],
                "vigente": documento["vigente"],
            }
        )
    return chunks


def vector_token(token):
    digest = hashlib.sha256(token.encode("utf-8")).digest()
    return [
        ((digest[i % len(digest)] / 255.0) * 2 - 1)
        for i in range(DIM)
    ]


def normalizar_vector(vector):
    norma = math.sqrt(sum(x * x for x in vector)) or 1.0
    return [x / norma for x in vector]


def vector_texto(texto):
    vector = [0.0] * DIM
    for token in normalizar(texto):
        base = vector_token(token)
        vector = [a + b for a, b in zip(vector, base)]
    return normalizar_vector(vector)


def producto_punto(a, b):
    return sum(x * y for x, y in zip(a, b))


def construir_indice(documentos):
    chunks = []
    for doc in documentos:
        chunks.extend(partir_en_chunks(doc))

    tokens = [
        normalizar(c["titulo"] + " " + c["texto"])
        for c in chunks
    ]
    df = defaultdict(int)
    for fila in tokens:
        for token in set(fila):
            df[token] += 1

    return {
        "chunks": chunks,
        "tokens": tokens,
        "vectores": [
            vector_texto(c["titulo"] + " " + c["texto"])
            for c in chunks
        ],
        "df": df,
        "avgdl": sum(len(t) for t in tokens) / len(tokens),
    }


def bm25(query_tokens, doc_tokens, df, avgdl, total):
    frecuencias = Counter(doc_tokens)
    score = 0.0
    k1 = 1.2
    b = 0.75

    for token in query_tokens:
        if token not in frecuencias:
            continue
        numerador = total - df[token] + 0.5
        denominador = df[token] + 0.5
        idf = math.log(1 + numerador / denominador)
        tf = frecuencias[token]
        largo = len(doc_tokens)
        denom = tf + k1 * (1 - b + b * largo / avgdl)
        score += idf * (tf * (k1 + 1)) / denom
    return score


def cumple_filtro(chunk, filtro):
    return all(
        chunk.get(campo) == valor
        for campo, valor in filtro.items()
    )


def ranking_vectorial(pregunta, indice, filtro):
    q = vector_texto(pregunta)
    filas = []
    for i, chunk in enumerate(indice["chunks"]):
        if cumple_filtro(chunk, filtro):
            score = producto_punto(q, indice["vectores"][i])
            filas.append((chunk["id"], score))
    return sorted(filas, key=lambda x: x[1], reverse=True)


def ranking_lexico(pregunta, indice, filtro):
    query_tokens = normalizar(pregunta)
    filas = []
    for i, chunk in enumerate(indice["chunks"]):
        if cumple_filtro(chunk, filtro):
            score = bm25(
                query_tokens,
                indice["tokens"][i],
                indice["df"],
                indice["avgdl"],
                len(indice["tokens"]),
            )
            filas.append((chunk["id"], score))
    return sorted(filas, key=lambda x: x[1], reverse=True)


def fusion_rrf(rankings):
    scores = defaultdict(float)
    for ranking in rankings:
        for pos, (chunk_id, _score) in enumerate(ranking, start=1):
            scores[chunk_id] += 1 / (K_RRF + pos)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)


def recuperar(pregunta, indice, filtro):
    vectorial = ranking_vectorial(pregunta, indice, filtro)
    lexico = ranking_lexico(pregunta, indice, filtro)
    ranking = fusion_rrf([vectorial, lexico])[:TOP_K]
    por_id = {c["id"]: c for c in indice["chunks"]}
    return [(por_id[chunk_id], score) for chunk_id, score in ranking]


def generar_respuesta(pregunta, evidencias):
    if not evidencias:
        return SIN_EVIDENCIA

    mejor_chunk, mejor_score = evidencias[0]
    tokens_pregunta = set(tokens_de_contenido(pregunta))
    tokens_texto = set(tokens_de_contenido(mejor_chunk["texto"]))
    cobertura = len(tokens_pregunta & tokens_texto)

    if mejor_score < 0.02 or cobertura < 2:
        return SIN_EVIDENCIA

    cita = f"[{mejor_chunk['id']}]"
    return (
        f"{mejor_chunk['texto']} {cita}\n\n"
        f"Fuente: {mejor_chunk['titulo']}."
    )


def preguntar(pregunta, filtro):
    indice = construir_indice(DOCUMENTOS)
    evidencias = recuperar(pregunta, indice, filtro)

    print("Pregunta:", pregunta)
    print("Evidencias:")
    for chunk, score in evidencias:
        print(" ", chunk["id"], round(score, 4), chunk["titulo"])
    print()
    print(generar_respuesta(pregunta, evidencias))


if __name__ == "__main__":
    preguntar(
        "¿Puedo ampliar matrícula en septiembre con pagos pendientes?",
        {"curso": 2026, "vigente": True},
    )
    print("\n---\n")
    preguntar(
        "¿Cuál es el horario de cafetería?",
        {"curso": 2026, "vigente": True},
    )

Salida esperada aproximada:

Pregunta: ¿Puedo ampliar matrícula en septiembre con pagos pendientes?
Evidencias:
  norm-2026#c1 0.0328 Normativa de matrícula 2026
  faq-campus#c1 0.0317 Acceso al campus virtual

La ampliación de matrícula se abre en septiembre...
[norm-2026#c1]

Fuente: Normativa de matrícula 2026.

---

Pregunta: ¿Cuál es el horario de cafetería?
Evidencias:
  faq-campus#c1 0.0323 Acceso al campus virtual
  norm-2026#c1 0.0317 Normativa de matrícula 2026

No tengo evidencia suficiente en las fuentes disponibles.

Este código no pretende ser el RAG final. Pretende que puedas señalar cada pieza. Si sustituyes vector_texto por un modelo real de embeddings, generar_respuesta por una llamada a un LLM y construir_indice por Qdrant, pgvector o File Search, la arquitectura sigue siendo la misma.

Cómo encaja todo

graph TD
    subgraph "Capítulo 9: primer RAG"
        DOCS["Fuentes documentales"]
        CHUNK["Chunking"]
        INDEX["Índice"]
        RET["Retrieval"]
        CTX["Context builder"]
        GEN["Generación"]
        CITES["Citas"]
        ABST["Abstención"]
        TRACE["Trazas"]
    end
    subgraph "Viene de antes"
        API["APIs y schemas (F4C2)"]
        EMB["Embeddings (F4C7)"]
        VECTORDB["Base vectorial (F4C8)"]
        HYBRID["Búsqueda híbrida (F4C8)"]
    end
    subgraph "Sigue después"
        EVAL["Evaluar RAG (F4C10)"]
        AGENTIC["Agentic RAG (F4C11)"]
        SQL["Text-to-SQL (F4C12)"]
        MEMORY["Memoria de agentes (F5)"]
    end

    DOCS -->|"partir en"| CHUNK
    CHUNK -->|"vectorizar y guardar"| INDEX
    INDEX -->|"consultar mediante"| RET
    RET -->|"seleccionar para"| CTX
    CTX -->|"alimentar"| GEN
    GEN -->|"producir"| CITES
    GEN -->|"decidir"| ABST
    CITES -->|"registrar en"| TRACE
    ABST -->|"registrar en"| TRACE
    API -->|"definir contrato de"| GEN
    EMB -->|"crear vectores para"| INDEX
    VECTORDB -->|"almacenar"| INDEX
    HYBRID -->|"mejorar"| RET
    TRACE -->|"preparar"| EVAL
    RET -->|"puede iterar en"| AGENTIC
    CTX -->|"puede incluir resultados de"| SQL
    CHUNK -->|"puede alimentar"| MEMORY

    style DOCS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style CHUNK fill:#F5F5F5,stroke:#000000,stroke-width:2
    style INDEX fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RET fill:#F5F5F5,stroke:#000000,stroke-width:2
    style CTX fill:#F5F5F5,stroke:#000000,stroke-width:2
    style GEN fill:#F5F5F5,stroke:#000000,stroke-width:2
    style CITES fill:#F5F5F5,stroke:#000000,stroke-width:2
    style ABST fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TRACE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style API stroke-dasharray: 5 5
    style EMB stroke-dasharray: 5 5
    style VECTORDB stroke-dasharray: 5 5
    style HYBRID stroke-dasharray: 5 5
    style EVAL stroke-dasharray: 5 5
    style AGENTIC stroke-dasharray: 5 5
    style SQL stroke-dasharray: 5 5
    style MEMORY stroke-dasharray: 5 5

Vocabulario aprendido

El vocabulario de este capítulo no conviene memorizarlo como lista. Conviene leerlo como piezas de una máquina: cada término responde a una pregunta concreta.

TérminoResponde aDefinición útil
Corpus¿Qué puede consultar el sistema?Conjunto de fuentes aceptadas para el RAG, con permisos, versión y propietario.
Fuente¿De dónde salió la evidencia?Documento, web, tabla o base de datos original.
Parser¿Cómo convierto la fuente en texto usable?Pieza que extrae texto, títulos, tablas y orden de lectura.
OCR¿Qué hago si el documento es una imagen?Técnica que convierte escaneos o imágenes en texto recuperable.
Chunk¿Cuál es la unidad mínima recuperable?Fragmento que puede buscarse, meterse en contexto y citarse.
Metadata¿Cómo filtro y explico un chunk?Datos como curso, vigencia, sección, página, rol, hash y fecha.
ACL¿Quién puede recuperar cada fragmento?Regla de acceso aplicada antes de construir contexto.
Hash¿Cómo sé si cambió una fuente?Huella calculada del texto o archivo para detectar cambios.
Embedding¿Cómo comparo significado aproximado?Vector numérico que representa una pregunta o fragmento.
Vector store¿Dónde guardo vectores y metadata?Almacén preparado para buscar por similitud y filtros.
FTS¿Cómo busco palabras exactas?Búsqueda textual clásica sobre términos, frases y campos.
BM25¿Cómo ordeno resultados por relevancia léxica?Ranking que combina frecuencia de términos y rareza informativa.
ANN¿Cómo busco vectores a escala?Búsqueda aproximada de vecinos cercanos para no comparar contra todo.
Top-k¿Cuántos candidatos saco?Número de resultados que pasan a rerank, contexto o evaluación.
RRF¿Cómo mezclo rankings distintos?Fusión por posiciones; útil para combinar BM25 y embeddings.
Reranker¿Cómo ordeno mejor candidatos ya encontrados?Modelo o regla más lenta que reevalúa resultados prometedores.
Context builder¿Qué lee finalmente el modelo?Pieza que selecciona, ordena, recorta y etiqueta evidencia.
Cita¿Qué fuente sostiene esta frase?Referencia trazable a chunk, documento, página, sección y versión.
Grounding¿La respuesta está apoyada en evidencia?Grado en que cada afirmación importante sale de los chunks recuperados.
Abstención¿Qué hago si no hay evidencia suficiente?Responder que falta soporte en vez de inventar.
Traza¿Cómo depuro una respuesta?Registro de pregunta, filtros, rankings, contexto, modelo, coste y salida.
File Search¿Qué delego si uso una solución gestionada?Herramienta alojada para subir archivos, buscar en vector store y pasar contexto al modelo.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Llamar RAG a cualquier chat con documentosPuede no haber filtros, citas, trazas ni abstención.Exigir contrato de evidencia desde el principio.
Trocear por tamaño sin mirar estructuraCortas definiciones, excepciones o pasos completos.Partir por secciones, títulos y unidades citables.
Meter demasiado contextoEl modelo recibe ruido y puede mezclar fuentes.Medir top-k, deduplicar y respetar presupuesto.
Confiar en la cita generada sin validarlaEl modelo puede citar un fragmento que no sostiene la frase.Construir citas desde ids recuperados, no desde memoria del modelo.
No abstenerse nuncaEl sistema responde incluso cuando no hay evidencia.Definir umbrales y respuesta estándar de insuficiencia.
Olvidar permisos en retrievalEl texto ya entró al contexto aunque luego no lo muestres.Aplicar filtros antes de recuperar y antes de construir contexto.
Evaluar solo la respuesta finalNo sabes si falló retrieval, chunking o generación.Guardar trazas por etapa; el capítulo 10 entra ahí.

Antes de pasar página

  • ¿Puedo explicar la diferencia entre base vectorial y RAG?
  • ¿Sé distinguir RAG, memoria conversacional, caché, tool y entrenamiento?
  • ¿Sé justificar por qué usar RAG en vez de fine-tuning?
  • ¿Sé separar indexación y consulta?
  • ¿Sé qué metadata mínima debe llevar un chunk citable?
  • ¿Sé explicar corpus, parser, OCR, ACL, hash, top-k y reranker sin esconderme en siglas?
  • ¿Sé explicar qué significa la dimensión de un embedding y cómo afecta a memoria, coste e índice?
  • ¿Puedo calcular cómo cambia el número de chunks con tamaño y solape?
  • ¿Sé optimizar retrieval antes de tocar el prompt?
  • ¿Sé qué añadiría si el RAG debe trabajar con tablas, imágenes, audio, vídeo, código o bases de datos?
  • ¿Sé cuándo usar una solución gestionada y cuándo montar componentes?
  • ¿Sé construir un prompt que separe instrucciones, pregunta y contexto?
  • ¿Sé por qué la abstención es una salida correcta?
  • ¿Puedo explicar qué parte cambiaría para pasar del ejemplo local a un LLM real?
  • ¿Sé qué trazas guardar para evaluar el sistema en el capítulo 10?

En resumen

Idea fuerzaDetalle
RAG une recuperación y generación.El modelo responde con contexto externo recuperado en tiempo de consulta.
RAG no es memoria ni entrenamiento.No cambia pesos; recupera evidencia externa y actualizable.
Usamos RAG cuando necesitamos fuentes.Información cambiante, privada, citable o filtrada por permisos.
Las dimensiones importan.Afectan representación, almacenamiento, latencia, coste y reindexado.
El chunk es la unidad de confianza.Si no puedes citarlo, no deberías usarlo como evidencia.
Las soluciones de terceros aceleran, no deciden.Delegan infraestructura o composición, pero no sustituyen evaluación y permisos.
Optimizar exige medir por capas.Corpus, parsing, chunking, retrieval, rerank, contexto y generación fallan de formas distintas.
RAG no es solo texto plano.Puede incorporar tablas, imágenes, audio, vídeo, código, grafos, bases de datos y tools si son recuperables y citables.
Citar exige diseño.La cita debe apuntar a una fuente concreta y vigente.
Abstenerse es parte del producto.Cuando falta evidencia, responder menos es responder mejor.
El primer RAG debe dejar trazas.Query, filtros, chunks, scores, prompt y respuesta permiten depurar.

Para saber más

Cormack, G. V., Clarke, C. L. A. y Buettcher, S. (2009). Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR, 758-759. https://doi.org/10.1145/1571941.1572114

deepset. (2026). Pipelines. https://docs.haystack.deepset.ai/docs/pipelines

Google Cloud. (2026). Vertex AI RAG Engine overview. https://docs.cloud.google.com/vertex-ai/generative-ai/docs/rag-engine/rag-overview

LangChain. (2026). Retrieval. https://docs.langchain.com/oss/python/langchain/retrieval

Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS 33, 9459-9474. https://papers.nips.cc/paper/2020/hash/6b493230205f780e1bc26945df7481e5-Abstract.html

LlamaIndex. (2026). Introduction to RAG. https://docs.llamaindex.ai/en/stable/understanding/rag/

OpenAI. (2026). File search. https://platform.openai.com/docs/guides/tools-file-search/

Qdrant. (2026). Indexing. https://qdrant.tech/documentation/manage-data/indexing/

Robertson, S. y Zaragoza, H. (2009). The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 3(4), 333-389. https://doi.org/10.1561/1500000019

Notas

  1. Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS 33, 9459-9474. https://papers.nips.cc/paper/2020/hash/6b493230205f780e1bc26945df7481e5-Abstract.html.

  2. Lewis et al., 2020.

  3. OpenAI. (2026). File search. https://platform.openai.com/docs/guides/tools-file-search/. Consultado el 25 de mayo de 2026.

  4. LangChain. (2026). Retrieval. https://docs.langchain.com/oss/python/langchain/retrieval. Consultado el 25 de mayo de 2026.

  5. LlamaIndex. (2026). Introduction to RAG. https://docs.llamaindex.ai/en/stable/understanding/rag/. Consultado el 25 de mayo de 2026.

  6. deepset. (2026). Pipelines. https://docs.haystack.deepset.ai/docs/pipelines. Consultado el 25 de mayo de 2026.

  7. Google Cloud. (2026). Vertex AI RAG Engine overview. https://docs.cloud.google.com/vertex-ai/generative-ai/docs/rag-engine/rag-overview. Consultado el 25 de mayo de 2026.

  8. Microsoft. (2026). Retrieval-augmented generation in Azure AI Search. https://learn.microsoft.com/en-us/azure/search/retrieval-augmented-generation-overview. Consultado el 25 de mayo de 2026.

  9. Pinecone. (2026). Build a RAG chatbot. https://docs.pinecone.io/guides/get-started/build-a-rag-chatbot. Consultado el 25 de mayo de 2026. Weaviate. (2026). Retrieval Augmented Generation (RAG). https://docs.weaviate.io/weaviate/search/generative. Consultado el 25 de mayo de 2026.

  10. Robertson, S. y Zaragoza, H. (2009). The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 3(4), 333-389. https://doi.org/10.1561/1500000019. Cormack, G. V., Clarke, C. L. A. y Buettcher, S. (2009). Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR, 758-759. https://doi.org/10.1145/1571941.1572114.

  11. OpenAI, 2026.

  12. LangChain, 2026.

  13. LlamaIndex, 2026.

  14. deepset, 2026.

  15. OpenAI, 2026.

Capítulo 10

Facsímil 4 · La caja de herramientas

Capítulo 10: Evaluar RAG: retrieval, groundedness y abstención

La demo no cuenta como evaluación

En el capítulo 09 montamos el primer RAG serio: corpus, chunks, embeddings, retrieval, contexto, citas y abstención. Ahora viene la parte que separa una demo de un sistema profesional: demostrar que mejora, saber dónde falla y decidir cuándo no debe responder.

Un RAG puede fallar de formas muy distintas. Puede no recuperar el documento correcto. Puede recuperarlo y no meterlo en el contexto final. Puede meterlo y redactar algo que no está sostenido. Puede citar una fuente que no dice eso. Puede contestar cuando debería abstenerse. Si solo miras la respuesta final, llegas tarde: ves el síntoma, pero no el órgano que falló.

Evaluar RAG es medir la cadena completa:

  1. ¿La pregunta está en un conjunto de evaluación representativo?
  2. ¿El corpus contiene la evidencia necesaria?
  3. ¿El retrieval trae esa evidencia entre los primeros resultados?
  4. ¿El context builder la mete en el prompt sin ahogarla en ruido?
  5. ¿La respuesta está apoyada por el contexto?
  6. ¿Las citas apuntan a fragmentos que sostienen lo dicho?
  7. ¿El sistema se abstiene cuando no hay evidencia?
  8. ¿El coste, la latencia y la operación siguen siendo aceptables?

La evaluación no es una fase final. Es una pieza del producto.

Estado del arte con fecha de corte

Fecha de corte: 25 de mayo de 2026.
Fuentes consultadas ese día: documentación de Ragas, TruLens, LangSmith, LlamaIndex, Phoenix y OpenAI Graders; y referencias académicas sobre RAG, retrieval y benchmarks como BEIR/MTEB.

RAGAS propuso evaluar RAG separando componentes como recuperación, relevancia y fidelidad de la respuesta.1 La documentación actual de Ragas organiza métricas de RAG como context precision, context recall, response relevancy, faithfulness y métricas multimodales.2 TruLens populariza una tríada muy práctica: relevancia del contexto, groundedness y relevancia de la respuesta.3 LangSmith, LlamaIndex y Phoenix empujan una idea parecida desde producto: datasets, experimentos, trazas, evaluadores y comparación entre versiones.4

OpenAI Graders documenta la idea de evaluadores configurables con criterios, escalas y umbrales, incluyendo verificaciones de texto, similitud, evaluadores de modelo y ejecución de código.5 La lección importante para nuestro libro no es “usa esta herramienta”, sino “define la rúbrica y conserva la traza”.

FamiliaQué mideCuándo usarla
Métricas clásicas de retrievalSi los chunks esperados aparecen y en qué posición.Antes de mirar la respuesta del LLM.
Métricas de contextoSi el contexto final es útil, completo y no ruidoso.Cuando el retrieval trae candidatos pero la respuesta falla.
Métricas de groundednessSi las afirmaciones están sostenidas por el contexto.Para respuestas citadas, informes y asistentes de documentación.
Métricas de abstenciónSi responde cuando debe y se calla cuando toca.En dominios donde inventar cuesta confianza.
Métricas de operaciónLatencia, coste, errores, cobertura y deriva.Cuando el RAG ya vive en una aplicación.

Qué significa evaluar por capas

Un RAG se evalúa por capas porque cada capa tiene una pregunta distinta. Si solo mides “respuesta correcta”, no sabes si mejorar embeddings, chunking, prompt, reranker o corpus.

CapaPreguntaEvidencia que necesitas
Corpus¿Existe la fuente correcta?Documento vigente, versión, propietario y fecha.
Parsing¿El texto extraído es fiel?Comparación contra PDF, HTML, tabla o fuente original.
Chunking¿La unidad recuperable sostiene una respuesta?Chunks con título, sección, página y hash.
Retrieval¿Aparece la evidencia en top-k?Ranking de chunks y qrels.
Reranking¿Sube la evidencia buena?Ranking antes/después y relevancia graduada.
Context builder¿El modelo recibe lo necesario y no demasiado ruido?Contexto final enviado al LLM.
Generación¿La respuesta contesta con contrato?Respuesta, citas, formato y abstención.
Groundedness¿Cada afirmación está sostenida?Claims separados y evidencia por claim.
Operación¿El sistema aguanta uso real?Trazas, latencia, coste, errores y feedback.

La regla de ingeniería es sencilla: primero evalúa retrieval sin LLM; luego evalúa generación con contexto fijo; después evalúa el sistema completo. Si mezclas todo desde el principio, cada fallo parece misterioso.

Dataset de evaluación: el corazón del sistema

Un dataset de evaluación no es un conjunto de preguntas bonitas. Es un contrato de verdad para comparar versiones. Debe contener preguntas que representen el uso real, preguntas que el sistema debe responder, preguntas que debe rechazar por falta de evidencia y preguntas donde los filtros importan.

Un ejemplo de fila mínima:

CampoPara qué sirve
idIdentificador estable de la pregunta.
preguntaLo que una persona o sistema preguntaría.
answerableSi el corpus contiene evidencia suficiente.
gold_chunksChunks esperados o aceptables.
gold_answerRespuesta de referencia, si existe.
gold_citationsCitas esperadas.
filtrosCurso, rol, vigencia, idioma, cliente o tenant.
tipoSingle-hop, multi-hop, tabla, código, imagen, temporal, etc.
dificultadFácil, media, difícil, o escala propia.
criterioQué debe ocurrir para considerar la respuesta válida.

No todas las preguntas necesitan una respuesta literal de referencia. Para retrieval basta con qrels: juicios de relevancia entre pregunta y chunks. Para groundedness necesitas contexto y respuesta. Para abstención necesitas casos sin evidencia suficiente.

Tipo de preguntaEjemploQué prueba
Directa“¿Cuándo se abre la ampliación de matrícula?”Retrieval simple y cita directa.
Con filtro“¿Qué aplica al curso 2026?”Metadata y vigencia.
Multi-hop“¿Puedo ampliar si tengo pagos pendientes y cómo se solicita?”Recuperar más de un fragmento.
Tabla“¿Qué plazo corresponde a segunda matrícula?”Parsing y estructura tabular.
No respondible“¿Cuál es el horario de cafetería?” si no está en corpus.Abstención.
Contradicción documentalDos fuentes con fechas distintas.Vigencia, prioridad y explicación.
Texto largoPreguntas que requieren contexto distribuido.Recall, deduplicación y presupuesto.

Ejemplos de datasets de evaluación que puedes construir en un proyecto real:

DatasetQué contienePor qué merece existir
FAQ realPreguntas frecuentes, respuesta esperada y fuente exacta.Mide si el RAG resuelve lo que más se pregunta.
Normativa vigentePreguntas con curso, fecha, rol y chunks esperados.Mide filtros y prioridad documental.
Casos sin evidenciaPreguntas plausibles cuya respuesta no está en el corpus.Mide abstención correcta.
Multi-hopPreguntas que exigen dos o más fuentes.Mide si el contexto compone evidencia sin mezclar.
TablasPreguntas sobre filas, columnas, importes, plazos o unidades.Mide parsing y recuperación estructurada.
Citas difícilesRespuestas donde una cita parcial no basta.Mide si la cita sostiene la afirmación completa.
RegresiónCasos que ya fallaron en producción y se corrigieron.Evita reintroducir errores.
Segmentos críticosPreguntas por idioma, área, producto, cliente o perfil.Evita que la media esconda fallos locales.

Si vienes del facsímil 3, capítulo 06, la diferencia es esta: datasets como FLAN, Dolly-15K, HH-RLHF o LAION-5B pueden entrenar o adaptar modelos; este dataset de evaluación no debería entrenar el sistema que estás midiendo. Su trabajo es ponerle un espejo fiable.

El dataset debe crecer desde producción. Las primeras 30 preguntas sirven para empezar. Las siguientes 300 salen de dudas reales, tickets, búsquedas sin respuesta, feedback de usuarios y revisiones de profesorado o equipo de dominio.

Qrels y relevancia graduada

Un qrel es un juicio de relevancia. Dice que una pregunta necesita tal documento o tal chunk. Puede ser binario o graduado.

RelevanciaSignificado
0No ayuda a responder.
1Relacionado, pero insuficiente.
2Útil para parte de la respuesta.
3Evidencia central.

Ejemplo:

PreguntaChunkRelevancia
q1 ampliación con pagos pendientesnorm-2026#c13
q1 ampliación con pagos pendientesfaq-campus#c10
q2 acceso al campusfaq-campus#c13
q3 horario de cafeteríaningún chunkno respondible

Esta tabla permite evaluar retrieval sin llamar al LLM. Eso ahorra coste y te dice si la base del sistema funciona.

Métricas de retrieval

Las métricas de retrieval responden a una pregunta: ¿la evidencia correcta aparece en el ranking?

Sea GqG_q el conjunto de chunks relevantes para una pregunta qq, y sea Rk(q)R_k(q) la lista de los kk primeros chunks recuperados.

Precision@k(q)=Rk(q)Gqk\operatorname{Precision@k}(q)= \frac{|R_k(q) \cap G_q|}{k} Recall@k(q)=Rk(q)GqGq\operatorname{Recall@k}(q)= \frac{|R_k(q) \cap G_q|}{|G_q|} Hit@k(q)={1,si Rk(q)Gq 0,si Rk(q)Gq=\operatorname{Hit@k}(q)= \begin{cases} 1, & \text{si } R_k(q) \cap G_q \ne \varnothing \ 0, & \text{si } R_k(q) \cap G_q = \varnothing \end{cases}
MétricaQué te diceCuidado
Precision@kDe lo que recuperas, cuánto sirve.Puede ser baja si necesitas traer contexto amplio.
Recall@kDe lo que necesitabas, cuánto aparece.Puede subir trayendo demasiado ruido.
Hit@kSi aparece al menos una evidencia.No mide si aparece toda la evidencia.
MRRQué tan pronto aparece la primera evidencia.No basta para preguntas multi-hop.
nDCG@kSi lo más relevante aparece arriba.Requiere relevancia graduada.

MRR se calcula con la posición de la primera evidencia relevante:

RR(q)=1rankq\operatorname{RR}(q)= \frac{1}{\operatorname{rank}_q}

Si el primer chunk relevante aparece en posición 1, RR vale 1. Si aparece en posición 5, vale 0,2. Si no aparece, vale 0. El MRR es la media de RR en muchas preguntas.

nDCG usa relevancia graduada:

DCG@k=i=1k2reli1log2(i+1)\operatorname{DCG@k} = \sum_{i=1}^{k} \frac{2^{rel_i}-1}{\log_2(i+1)} nDCG@k=DCG@kIDCG@k\operatorname{nDCG@k} = \frac{\operatorname{DCG@k}}{\operatorname{IDCG@k}}
SímboloSignificado
relirel_iRelevancia del resultado en posición ii.
DCGDCGGanancia descontada por posición.
IDCGIDCGDCG ideal si los mejores resultados estuvieran arriba.

Precision@k y recall@k vienen de la tradición de recuperación de información. BEIR y MTEB son recordatorios útiles: un retriever puede brillar en un benchmark y fallar en tu dominio.6 Por eso el benchmark público orienta, pero el dataset interno decide.

Métricas de contexto

El retrieval devuelve candidatos. El contexto final es lo que realmente lee el modelo. Entre una cosa y otra puede haber deduplicación, recorte, reordenación, filtros, prioridad de fuentes y presupuesto de tokens.

Ragas llama context precision y context recall a dos ideas útiles.7 Las traduzco de forma operativa:

MétricaPregunta
Context precision¿El contexto incluido es útil o está lleno de ruido?
Context recall¿El contexto contiene toda la evidencia necesaria?

Ejemplo de fórmula. Puedes empezar con una versión sencilla:

ContextPrecision=chunks uˊtiles en contextochunks en contexto\operatorname{ContextPrecision} = \frac{\text{chunks útiles en contexto}}{\text{chunks en contexto}} ContextRecall=evidencias esperadas presentesevidencias esperadas\operatorname{ContextRecall} = \frac{\text{evidencias esperadas presentes}}{\text{evidencias esperadas}}

La diferencia con retrieval es sutil pero importante. Retrieval@k mide el ranking bruto. Context precision/recall mide el paquete de evidencia que llegó al prompt.

FalloRetrievalContexto
El chunk bueno no aparece en top 20.Falló retrieval.No tiene oportunidad.
El chunk bueno aparece en top 5 pero se recorta.Retrieval bien.Falló context builder.
Hay cinco chunks repetidos.Retrieval dudoso.Falló deduplicación.
Entra una fuente antigua y otra vigente.Filtros dudosos.Falló prioridad o explicación.

Groundedness, faithfulness y citas

Groundedness significa que la respuesta está apoyada por el contexto recuperado. Faithfulness suele usarse de forma cercana: la respuesta no añade hechos que no se desprenden del contexto. TruLens lo conecta con la tríada: contexto relevante, respuesta apoyada en el contexto y respuesta relevante para la pregunta.8

La forma práctica de evaluarlo es separar la respuesta en afirmaciones.

RespuestaClaims
“La ampliación se abre en septiembre y no puede haber pagos vencidos.”1. La ampliación se abre en septiembre. 2. No puede haber pagos vencidos.

Cada claim se evalúa contra el contexto:

ClaimEvidenciaResultado
La ampliación se abre en septiembre.Chunk norm-2026#c1.Sostenido.
No puede haber pagos vencidos.Chunk norm-2026#c1.Sostenido.
Se aprueba automáticamente.No aparece en contexto.No sostenido.

Ejemplo de fórmula. Una métrica simple:

Groundedness=claims sostenidosclaims totales\operatorname{Groundedness} = \frac{\text{claims sostenidos}}{\text{claims totales}}

La cita añade otra capa. No basta con que la respuesta sea cierta: debe citar el fragmento correcto.

CitationPrecision=citas vaˊlidas usadascitas usadas\operatorname{CitationPrecision} = \frac{\text{citas válidas usadas}}{\text{citas usadas}} CitationRecall=evidencias citadasevidencias necesarias\operatorname{CitationRecall} = \frac{\text{evidencias citadas}}{\text{evidencias necesarias}}
CasoGroundednessCitasDiagnóstico
Respuesta correcta y cita correcta.Alta.Alta.Bien.
Respuesta correcta sin cita.Alta.Baja.Falta trazabilidad.
Respuesta correcta con cita equivocada.Puede parecer alta.Baja.Interfaz de confianza rota.
Respuesta inventada con cita real.Baja.Engañosa.El chunk citado no sostiene la frase.

Un evaluador LLM puede ayudar a evaluar groundedness, pero no es oráculo. Debe recibir rúbrica clara, contexto, respuesta y, si existe, respuesta de referencia. Sus resultados deben compararse con revisión humana en una muestra. OpenAI Graders documenta distintos tipos de evaluadores y la idea de devolver una puntuación numérica contra criterios.9

Evaluar abstención

Abstenerse no significa que el sistema sea torpe. En RAG, abstenerse puede ser la respuesta correcta. Hay preguntas que el corpus no cubre, fuentes contradictorias, permisos insuficientes o evidencia demasiado débil.

Podemos modelar la decisión:

d(q)={responder,si s(q)τ abstenerse,si s(q)<τd(q)= \begin{cases} \operatorname{responder}, & \text{si } s(q) \ge \tau \ \operatorname{abstenerse}, & \text{si } s(q) < \tau \end{cases}
SímboloSignificado
qqPregunta.
s(q)s(q)Soporte estimado: evidencia, scores, citas, groundedness.
τ\tauUmbral mínimo para responder.
d(q)d(q)Decisión final.

La matriz de abstención:

RealidadEl sistema respondeEl sistema se abstiene
Hay evidencia suficiente.Respuesta evaluable.Abstención innecesaria.
No hay evidencia suficiente.Respuesta no sostenida.Abstención correcta.

Métricas útiles:

Coverage=preguntas respondidaspreguntas totales\operatorname{Coverage} = \frac{\text{preguntas respondidas}}{\text{preguntas totales}} CorrectAbstentionRate=abstenciones correctaspreguntas no respondibles\operatorname{CorrectAbstentionRate} = \frac{\text{abstenciones correctas}}{\text{preguntas no respondibles}} UnsupportedAnswerRate=respuestas sin soportepreguntas no respondibles\operatorname{UnsupportedAnswerRate} = \frac{\text{respuestas sin soporte}}{\text{preguntas no respondibles}}

Subir cobertura no siempre es bueno. Si responde más a costa de inventar más, el sistema empeora. El umbral debe calibrarse con curvas: cuánto ganas en cobertura y cuánto pierdes en precisión de respuesta.

Arquitectura de evaluación

Evaluar RAG es medir la cadena, no solo la respuesta Dataset, experimento, métricas por capa, gates de publicación y trazas comparables. 1. Dataset preguntas reales qrels y gold chunks casos sin evidencia filtros y permisos versionado 2. Experimento pipeline candidato línea base anterior mismos inputs misma semilla si aplica comparar versiones 3. Trazas por pregunta retrieval ranking + scores contexto chunks usados respuesta citas + decisión Sin traza no hay depuración: solo opiniones sobre una salida. 4. Métricas por capa Retrieval Recall@k · MRR nDCG · Hit@k qrels Contexto precision · recall deduplicación tokens usados Grounding claims sostenidos citas válidas faithfulness Abstención coverage abstención correcta sin soporte Operación latencia p95 coste · errores deriva 5. Gate de publicación Publica solo si mejora o mantiene baseline en métricas críticas. Ejemplo: Recall@5 ≥ 0,85 · Groundedness ≥ 0,90 · sin soporte ≤ 0,02 · p95 ≤ 2,5s Sin dataset y trazas, optimizar RAG es mover piezas a oscuras. IA para gente curiosa / Facsímil 04 / Capítulo 10 / 686f6c61

El diagrama tiene una idea central: cada experimento debe producir trazas comparables. Si cambias embeddings, chunking o prompt, no basta con ver una respuesta bonita. Comparas contra baseline y miras qué capa se movió.

Gates: decidir si una versión publica

Un gate es una regla de publicación. Evita que un cambio que mejora una métrica rompa otra más importante.

MétricaUmbral ejemploQué protege
Recall@50,85\ge 0,85Que la evidencia aparezca.
nDCG@50,80\ge 0,80Que aparezca arriba.
Groundedness0,90\ge 0,90Que no añada afirmaciones sin soporte.
Citation precision0,95\ge 0,95Que las citas sean revisables.
Correct abstention0,85\ge 0,85Que no responda fuera del corpus.
Unsupported answer rate0,02\le 0,02Que no conteste sin evidencia.
Latencia p952,5s\le 2,5sQue sea usable.
Coste por respuestapresupuesto\le presupuestoQue sea sostenible.

Los umbrales no salen de una tabla universal. Salen del dominio. Un asistente de lectura puede aceptar más incertidumbre que un asistente que orienta trámites administrativos. Lo importante es que el equipo escriba el umbral antes de mirar si su cambio favorito pasa.

Evaluadores LLM y rúbricas

Los evaluadores LLM son útiles para escalar revisión, pero hay que usarlos con cuidado. Un evaluador no sustituye una rúbrica: ejecuta una rúbrica.

Una rúbrica mínima para groundedness:

Evalúa si la respuesta está sostenida por el contexto.

Entrada:
- Pregunta del usuario.
- Contexto recuperado con ids de chunk.
- Respuesta generada.

Devuelve JSON:
{
  "score": 0.0 a 1.0,
  "claims_no_sostenidos": ["..."],
  "citas_invalidas": ["..."],
  "decision": "pasa" | "revisar" | "falla"
}

Criterio:
- 1.0: todas las afirmaciones relevantes están sostenidas.
- 0.5: la respuesta mezcla evidencia con inferencias no citadas.
- 0.0: la respuesta contradice o inventa respecto al contexto.

Buenas prácticas:

PrácticaMotivo
Separar evaluación de retrieval y generación.Un evaluador de respuesta no descubre por sí solo si faltó un chunk.
Guardar entradas del evaluador.Sin prompt, contexto y respuesta no puedes auditar el score.
Usar muestras revisadas por personas.El evaluador debe calibrarse contra criterio humano.
Evaluar con varios tipos de pregunta.Una métrica media puede esconder fallos en tablas, fechas o multi-hop.
Repetir evaluaciones críticas.Algunos evaluadores tienen variabilidad; mide estabilidad.
No entrenar el sistema para complacer al evaluador.El objetivo es utilidad verificable, no ganar una métrica estrecha.

Evaluación offline, online y sombra

Hay tres modos de evaluación que se complementan.

ModoQué haceCuándo usarlo
OfflineEjecuta un dataset fijo contra una versión del RAG.Antes de publicar cambios.
SombraEjecuta una versión candidata con tráfico real sin mostrarla.Para medir deriva y casos reales sin afectar a usuarios.
OnlineMide interacción real, feedback, coste, latencia y errores.Cuando el sistema está en uso.

Offline te da repetibilidad. Sombra te da realidad sin exposición directa. Online te da señales de producto. Ninguna sustituye a las otras.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a construir un evaluador local de RAG. No necesita APIs. No pretende reemplazar Ragas, TruLens, LangSmith o Phoenix; pretende que entiendas qué está midiendo cada herramienta por dentro.

Guarda esto como evaluar_rag_minimo.py:

from collections import Counter
import json
import math


K = 3

CHUNKS = {
    "norm-2026#c1": (
        "La ampliación de matrícula se abre en septiembre. "
        "El estudiante puede solicitar ampliación si no mantiene "
        "pagos pendientes vencidos."
    ),
    "faq-campus#c1": (
        "Si no puedes entrar al campus virtual, revisa el doble "
        "factor y restablece la contraseña desde la página de acceso."
    ),
    "norm-2024#c1": (
        "En 2024 la ampliación de matrícula no revisaba pagos "
        "pendientes antes de enviar la solicitud."
    ),
    "becas-2026#c1": (
        "Las becas generales tienen calendario propio y no modifican "
        "la normativa de ampliación de matrícula."
    ),
}

RUNS = [
    {
        "id": "q1",
        "pregunta": "¿Puedo ampliar matrícula con pagos pendientes?",
        "answerable": True,
        "gold_chunks": {"norm-2026#c1"},
        "retrieved": [
            ("norm-2026#c1", 3),
            ("becas-2026#c1", 1),
            ("faq-campus#c1", 0),
        ],
        "answer": (
            "Puedes solicitar ampliación en septiembre si no mantienes "
            "pagos pendientes vencidos. [norm-2026#c1]"
        ),
        "citations": {"norm-2026#c1"},
        "claims": [
            "La ampliación se abre en septiembre",
            "No puede haber pagos pendientes vencidos",
        ],
        "abstained": False,
    },
    {
        "id": "q2",
        "pregunta": "¿Cuál es el horario de cafetería?",
        "answerable": False,
        "gold_chunks": set(),
        "retrieved": [
            ("faq-campus#c1", 0),
            ("becas-2026#c1", 0),
            ("norm-2026#c1", 0),
        ],
        "answer": "No tengo evidencia suficiente.",
        "citations": set(),
        "claims": [],
        "abstained": True,
    },
    {
        "id": "q3",
        "pregunta": "¿Cómo recupero acceso al campus virtual?",
        "answerable": True,
        "gold_chunks": {"faq-campus#c1"},
        "retrieved": [
            ("norm-2026#c1", 0),
            ("faq-campus#c1", 3),
            ("becas-2026#c1", 0),
        ],
        "answer": (
            "Revisa el doble factor y restablece la contraseña desde "
            "la página de acceso. [faq-campus#c1]"
        ),
        "citations": {"faq-campus#c1"},
        "claims": [
            "Revisa el doble factor",
            "La contraseña se restablece desde la página de acceso",
        ],
        "abstained": False,
    },
    {
        "id": "q4",
        "pregunta": "¿La ampliación se aprueba automáticamente?",
        "answerable": False,
        "gold_chunks": set(),
        "retrieved": [
            ("norm-2026#c1", 1),
            ("norm-2024#c1", 0),
            ("becas-2026#c1", 0),
        ],
        "answer": (
            "Sí, la ampliación se aprueba automáticamente. "
            "[norm-2026#c1]"
        ),
        "citations": {"norm-2026#c1"},
        "claims": ["La ampliación se aprueba automáticamente"],
        "abstained": False,
    },
]

STOPWORDS = {
    "a",
    "al",
    "con",
    "de",
    "del",
    "desde",
    "el",
    "en",
    "es",
    "la",
    "las",
    "los",
    "no",
    "por",
    "se",
    "si",
    "y",
}


def tokens(texto):
    limpio = "".join(
        c.lower() if c.isalnum() else " "
        for c in texto
    )
    return {
        token
        for token in limpio.split()
        if token not in STOPWORDS and len(token) > 2
    }


def precision_at_k(run, k):
    top = run["retrieved"][:k]
    if not top:
        return 0.0
    relevantes = sum(1 for _chunk_id, rel in top if rel > 0)
    return relevantes / len(top)


def recall_at_k(run, k):
    gold = run["gold_chunks"]
    if not gold:
        return None
    top_ids = {chunk_id for chunk_id, _rel in run["retrieved"][:k]}
    return len(top_ids & gold) / len(gold)


def hit_at_k(run, k):
    recall = recall_at_k(run, k)
    if recall is None:
        return None
    return 1.0 if recall > 0 else 0.0


def reciprocal_rank(run):
    gold = run["gold_chunks"]
    if not gold:
        return None
    for pos, (chunk_id, _rel) in enumerate(run["retrieved"], start=1):
        if chunk_id in gold:
            return 1 / pos
    return 0.0


def dcg(relevancias, k):
    total = 0.0
    for pos, rel in enumerate(relevancias[:k], start=1):
        total += (2**rel - 1) / math.log2(pos + 1)
    return total


def ndcg_at_k(run, k):
    relevancias = [rel for _chunk_id, rel in run["retrieved"]]
    ideal = sorted(relevancias, reverse=True)
    ideal_dcg = dcg(ideal, k)
    if ideal_dcg == 0:
        return None
    return dcg(relevancias, k) / ideal_dcg


def citation_precision(run):
    if not run["citations"]:
        return None
    validas = run["citations"] & run["gold_chunks"]
    return len(validas) / len(run["citations"])


def citation_recall(run):
    if not run["gold_chunks"]:
        return None
    validas = run["citations"] & run["gold_chunks"]
    return len(validas) / len(run["gold_chunks"])


def claim_supported(claim, cited_chunks):
    claim_tokens = tokens(claim)
    if not claim_tokens:
        return True
    evidence = " ".join(CHUNKS[c] for c in cited_chunks if c in CHUNKS)
    evidence_tokens = tokens(evidence)
    overlap = len(claim_tokens & evidence_tokens)
    return overlap / len(claim_tokens) >= 0.5


def groundedness(run):
    if not run["claims"]:
        return None
    supported = sum(
        1
        for claim in run["claims"]
        if claim_supported(claim, run["citations"])
    )
    return supported / len(run["claims"])


def decision(run):
    if not run["answerable"] and run["abstained"]:
        return "abstencion_correcta"
    if not run["answerable"] and not run["abstained"]:
        return "respuesta_sin_soporte"
    if run["answerable"] and run["abstained"]:
        return "abstencion_innecesaria"

    grounded = groundedness(run) or 0.0
    cit_rec = citation_recall(run) or 0.0
    if grounded >= 0.8 and cit_rec >= 1.0:
        return "respuesta_sostenida"
    return "respuesta_debil"


def media(valores):
    limpios = [v for v in valores if v is not None]
    if not limpios:
        return None
    return sum(limpios) / len(limpios)


def resumen(runs):
    decisiones = Counter(decision(run) for run in runs)
    respondidas = sum(1 for run in runs if not run["abstained"])
    no_respondibles = sum(1 for run in runs if not run["answerable"])
    sin_soporte = decisiones["respuesta_sin_soporte"]

    return {
        "precision@3": media(precision_at_k(run, K) for run in runs),
        "recall@3": media(recall_at_k(run, K) for run in runs),
        "hit@3": media(hit_at_k(run, K) for run in runs),
        "mrr": media(reciprocal_rank(run) for run in runs),
        "ndcg@3": media(ndcg_at_k(run, K) for run in runs),
        "citation_precision": media(citation_precision(r) for r in runs),
        "citation_recall": media(citation_recall(r) for r in runs),
        "groundedness": media(groundedness(run) for run in runs),
        "coverage": respondidas / len(runs),
        "unsupported_answer_rate": sin_soporte / max(no_respondibles, 1),
        "decisiones": dict(decisiones),
    }


if __name__ == "__main__":
    for run in RUNS:
        print(run["id"], decision(run))
    print(json.dumps(resumen(RUNS), indent=2, ensure_ascii=False))

Salida esperada aproximada:

q1 respuesta_sostenida
q2 abstencion_correcta
q3 respuesta_sostenida
q4 respuesta_sin_soporte
{
  "precision@3": 0.3333,
  "recall@3": 1.0,
  "hit@3": 1.0,
  "mrr": 0.75,
  "ndcg@3": 0.877,
  "citation_precision": 0.6667,
  "citation_recall": 1.0,
  "groundedness": 0.6667,
  "coverage": 0.75,
  "unsupported_answer_rate": 0.5,
  "decisiones": {
    "respuesta_sostenida": 2,
    "abstencion_correcta": 1,
    "respuesta_sin_soporte": 1
  }
}

Este ejemplo enseña algo importante: recall@3 puede salir perfecto y, aun así, el sistema puede responder sin soporte en una pregunta no respondible. Por eso evaluar RAG exige retrieval, groundedness, citas y abstención a la vez.

Cómo encaja todo

graph TD
    subgraph "Capítulo 10: evaluación RAG"
        DATASET["Dataset evaluable"]
        QRELS["Qrels y gold chunks"]
        TRACE["Trazas"]
        RETMET["Retrieval metrics"]
        CTXMET["Context metrics"]
        GROUNDED["Groundedness"]
        CITEMET["Citas"]
        ABSTMET["Abstención"]
        GATE["Gate de publicación"]
    end
    subgraph "Viene de antes"
        EMB["Embeddings (F4C7)"]
        VECTORDB["Base vectorial (F4C8)"]
        HYBRID["Búsqueda híbrida (F4C8)"]
        RAG["Primer RAG (F4C9)"]
    end
    subgraph "Sigue después"
        AGENTIC["Agentic RAG (F4C11)"]
        SQL["Text-to-SQL (F4C12)"]
        LAB["Laboratorio mínimo (F4C13)"]
        OPS["Operación y producto (F6)"]
    end

    DATASET -->|"define verdad de prueba"| QRELS
    QRELS -->|"evalúa"| RETMET
    TRACE -->|"permite calcular"| RETMET
    TRACE -->|"permite calcular"| CTXMET
    TRACE -->|"permite revisar"| GROUNDED
    TRACE -->|"permite validar"| CITEMET
    TRACE -->|"permite medir"| ABSTMET
    RETMET -->|"entra en"| GATE
    CTXMET -->|"entra en"| GATE
    GROUNDED -->|"entra en"| GATE
    CITEMET -->|"entra en"| GATE
    ABSTMET -->|"entra en"| GATE
    EMB -->|"afecta a"| RETMET
    VECTORDB -->|"afecta a"| RETMET
    HYBRID -->|"afecta a"| RETMET
    RAG -->|"produce"| TRACE
    GATE -->|"prepara"| AGENTIC
    GATE -->|"prepara"| SQL
    GATE -->|"se practica en"| LAB
    TRACE -->|"alimenta"| OPS

    style DATASET fill:#F5F5F5,stroke:#000000,stroke-width:2
    style QRELS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TRACE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RETMET fill:#F5F5F5,stroke:#000000,stroke-width:2
    style CTXMET fill:#F5F5F5,stroke:#000000,stroke-width:2
    style GROUNDED fill:#F5F5F5,stroke:#000000,stroke-width:2
    style CITEMET fill:#F5F5F5,stroke:#000000,stroke-width:2
    style ABSTMET fill:#F5F5F5,stroke:#000000,stroke-width:2
    style GATE fill:#111111,stroke:#000000,stroke-width:2,color:#FFFFFF
    style EMB stroke-dasharray: 5 5
    style VECTORDB stroke-dasharray: 5 5
    style HYBRID stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style AGENTIC stroke-dasharray: 5 5
    style SQL stroke-dasharray: 5 5
    style LAB stroke-dasharray: 5 5
    style OPS stroke-dasharray: 5 5

Vocabulario aprendido

TérminoResponde aDefinición útil
Evaluación offline¿Mejora antes de publicar?Prueba repetible sobre dataset fijo.
Evaluación online¿Qué ocurre en uso real?Medición con tráfico, feedback, coste y latencia.
Evaluación sombra¿Cómo pruebo sin mostrar al usuario?Ejecutar versión candidata en paralelo y registrar resultados.
Qrel¿Qué chunk debería recuperar?Juicio de relevancia pregunta-documento.
Precision@k¿Cuánto ruido hay en top-k?Proporción de resultados relevantes entre los k primeros.
Recall@k¿Apareció la evidencia esperada?Proporción de evidencias recuperadas.
Hit@k¿Aparece al menos una evidencia?Indicador binario de recuperación suficiente mínima.
MRR¿Cuán pronto aparece la primera evidencia?Media del inverso de la primera posición relevante.
nDCG¿Lo más útil aparece arriba?Métrica con relevancia graduada y descuento por posición.
Context precision¿El contexto final está limpio?Proporción de chunks útiles dentro del contexto usado.
Context recall¿El contexto contiene lo necesario?Proporción de evidencia esperada incluida en el prompt.
Groundedness¿La respuesta se apoya en contexto?Claims sostenidos por evidencia recuperada.
Citation precision¿Las citas usadas son válidas?Citas que apuntan a evidencia real entre citas usadas.
Citation recall¿Cité toda la evidencia necesaria?Evidencias necesarias citadas entre evidencias esperadas.
Coverage¿Cuánto responde el sistema?Porcentaje de preguntas no abstendidas.
Gate¿Publicamos esta versión?Regla de aceptación con umbrales técnicos y de producto.
Evaluador LLM¿Quién puntúa respuestas abiertas?Modelo evaluador guiado por una rúbrica auditable.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Evaluar solo la respuesta finalNo sabes si falló retrieval, contexto o generación.Guardar trazas y medir por capas.
Usar tres preguntas elegidas a manoLa demo se adapta a tus expectativas.Crear dataset con casos reales y no respondibles.
Optimizar recall subiendo top-k sin límiteTraes más evidencia, pero también más ruido y coste.Medir recall, precision, nDCG y tokens de contexto.
Confiar ciegamente en un evaluador LLMEl evaluador también se equivoca y depende de la rúbrica.Calibrarlo con revisión humana y guardar entradas.
No medir abstenciónEl sistema aprende a contestarlo todo.Incluir preguntas sin evidencia y umbrales.
No versionar corpus e índiceNo puedes reproducir por qué respondió algo.Guardar versión de corpus, embeddings, chunks y prompt.
Mezclar cambiosSi mejoras o empeoras, no sabes por qué.Cambiar una variable por experimento.
Mirar solo mediasUn promedio alto oculta fallos graves por tipo de pregunta.Reportar métricas por segmento: tabla, multi-hop, fecha, rol.

Antes de pasar página

  • ¿Sé explicar por qué evaluar una respuesta final no basta?
  • ¿Sé construir una fila mínima de dataset de evaluación?
  • ¿Sé qué es un qrel y cuándo usar relevancia graduada?
  • ¿Sé calcular precision@k, recall@k, hit@k, MRR y nDCG?
  • ¿Sé separar retrieval metrics de context metrics?
  • ¿Sé evaluar groundedness separando claims y evidencia?
  • ¿Sé medir precisión y recall de citas?
  • ¿Sé construir una matriz de abstención?
  • ¿Sé definir gates de publicación con umbrales explícitos?
  • ¿Sé usar un evaluador LLM sin tratarlo como oráculo?
  • ¿Sé guardar trazas suficientes para comparar versiones?

En resumen

Idea fuerzaDetalle
Evaluar RAG es evaluar una cadena.Corpus, parsing, retrieval, contexto, generación, citas y abstención.
Retrieval se mide antes del LLM.Si no recuperas la evidencia, la generación no puede arreglarlo.
Groundedness exige claims.No basta con una sensación global de respuesta correcta.
Citar también se evalúa.Una cita debe sostener la frase que acompaña.
Abstenerse puede ser correcto.Coverage alto con respuestas sin soporte es mala señal.
Un evaluador LLM necesita rúbrica.La herramienta puntúa; el criterio lo diseña el equipo.
Un gate protege producto.Publicas si la versión candidata supera umbrales críticos.
Sin trazas no hay aprendizaje.Cada consulta debe dejar ranking, contexto, respuesta, citas y costes.

Para saber más

Arize Phoenix. (2026). Evaluate RAG. https://arize.com/docs/phoenix/cookbook/evaluation/evaluate-rag

Arize Phoenix. (2026). Evaluation concepts. https://arize.com/docs/phoenix/evaluation/concepts-evals/evaluation

Es, S., James, J., Espinosa-Anke, L. y Schockaert, S. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. https://arxiv.org/abs/2309.15217

LangChain. (2026). Evaluate a RAG application. https://docs.langchain.com/langsmith/evaluate-rag-tutorial

LlamaIndex. (2026). Evaluation modules. https://developers.llamaindex.ai/python/framework/module_guides/evaluating/modules/

OpenAI. (2026). Graders. https://developers.openai.com/api/docs/guides/graders

Ragas. (2026). List of available metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/

Thakur, N. et al. (2021). BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. https://arxiv.org/abs/2104.08663

TruLens. (2026). RAG Triad. https://www.trulens.org/getting_started/core_concepts/rag_triad/

Notas

  1. Es, S., James, J., Espinosa-Anke, L. y Schockaert, S. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. https://arxiv.org/abs/2309.15217.

  2. Ragas. (2026). List of available metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/. Consultado el 25 de mayo de 2026.

  3. TruLens. (2026). RAG Triad. https://www.trulens.org/getting_started/core_concepts/rag_triad/. Consultado el 25 de mayo de 2026.

  4. LangChain. (2026). Evaluate a RAG application. https://docs.langchain.com/langsmith/evaluate-rag-tutorial. LlamaIndex. (2026). Evaluation modules. https://developers.llamaindex.ai/python/framework/module_guides/evaluating/modules/. Arize Phoenix. (2026). Evaluate RAG. https://arize.com/docs/phoenix/cookbook/evaluation/evaluate-rag. Consultado el 25 de mayo de 2026.

  5. OpenAI. (2026). Graders. https://developers.openai.com/api/docs/guides/graders. Consultado el 25 de mayo de 2026.

  6. Thakur, N. et al. (2021). BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. https://arxiv.org/abs/2104.08663. Muennighoff, N. et al. (2023). MTEB: Massive Text Embedding Benchmark. https://arxiv.org/abs/2210.07316.

  7. Ragas, 2026.

  8. TruLens, 2026.

  9. OpenAI, 2026.

Capítulo 11

Facsímil 4 · La caja de herramientas

Capítulo 11: Agentic RAG y GraphRAG: cuándo complicar

Cuando un RAG fijo se queda corto

En el capítulo 09 construimos el RAG básico: partir una colección, buscar fragmentos, pasarlos al modelo y responder con citas. En el capítulo 10 aprendimos a medir si la evidencia aparece, si el contexto sostiene la respuesta y si el sistema sabe abstenerse.

Ese patrón funciona muy bien para preguntas directas: “¿qué dice esta normativa sobre X?”, “¿cuál es el plazo?”, “¿dónde se configura este parámetro?”. Pero en proyectos reales aparecen preguntas menos limpias:

  • “Compara los requisitos de matrícula con los requisitos de beca y dime dónde se contradicen.”
  • “¿Qué temas se repiten en todas las quejas del alumnado este curso?”
  • “¿Qué documentos explican por qué cambió esta política?”
  • “Busca primero en la normativa; si no basta, mira FAQ, calendario y expediente.”
  • “Esta respuesta cita una fuente floja; revisa si hay otra fuente mejor.”

Aquí ya no basta con una única búsqueda top-k. El sistema necesita planificar, reescribir consultas, dividir la pregunta, escoger fuentes, comprobar si lo recuperado basta, volver a buscar si no basta y quizá consultar un grafo de relaciones. Eso es lo que solemos llamar Agentic RAG y GraphRAG. La pregunta importante no es “¿puedo hacerlo?”, sino “¿merece la pena pagar la complejidad?”.

Estado del arte con fecha de corte

Fecha de corte: 26 de mayo de 2026.
Fuentes consultadas ese día: documentación oficial de LangChain sobre arquitecturas RAG, documentación de LlamaIndex sobre estrategias agentic, documentación de Microsoft GraphRAG, papers de ReAct, Toolformer, HyDE, Self-RAG, Corrective RAG, RAPTOR y GraphRAG, y el capítulo anterior de evaluación del propio libro.

El patrón base sigue siendo RAG: recuperar información externa en tiempo de consulta para responder con contexto específico.1 Lo que cambia es quién decide los pasos. LangChain distingue entre RAG de dos pasos, Agentic RAG e híbridos con validación; en su documentación, el RAG de dos pasos es más predecible y el agentic gana flexibilidad a costa de latencia variable.2 LlamaIndex describe estrategias agentic como routing, query transformations, sub-question query engines y agentes de datos sobre motores RAG existentes.3

La parte “agentic” bebe de trabajos como ReAct, que intercalan razonamiento y acciones para consultar fuentes externas durante la resolución.4 Toolformer exploró cómo modelos de lenguaje pueden aprender a usar herramientas externas mediante ejemplos auto-supervisados.5 En retrieval avanzado aparecen técnicas como HyDE, que genera un documento hipotético para buscar textos reales cercanos; Self-RAG, que introduce recuperación y crítica/reflexión; y Corrective RAG, que evalúa la calidad de documentos recuperados y activa acciones correctivas si la evidencia es débil.6

GraphRAG se volvió relevante porque muchas preguntas no piden “el chunk más parecido”, sino entender relaciones o patrones globales del corpus. El paper de Microsoft GraphRAG plantea un índice basado en grafo de entidades y resúmenes de comunidades para responder preguntas de comprensión global sobre colecciones privadas.7 La documentación de GraphRAG separa local search, global search, DRIFT search, basic search y question generation.8

Qué no es complicar bien

Complicar un RAG no significa meter un agente delante de todo. Si el sistema siempre hace una pregunta directa sobre un documento vigente, un RAG de dos pasos puede ser mejor: menos latencia, menos coste, menos puntos de fallo y evaluación más sencilla.

Tampoco significa que el modelo “piense libremente” hasta encontrar la respuesta. En ingeniería, un Agentic RAG serio tiene herramientas permitidas, límites de pasos, trazas, umbrales de evidencia y reglas de salida. Si no puedes reconstruir qué buscó, qué encontró, qué descartó y por qué respondió, no has ganado inteligencia: has perdido depuración.

GraphRAG tampoco es “usar una base de grafos porque suena potente”. Un grafo merece la pena cuando las relaciones importan: entidades, dependencias, comunidades, jerarquías, trazabilidad entre documentos, patrones globales o preguntas que no se resuelven con un párrafo aislado. Si tu corpus son veinte FAQs cortas, GraphRAG puede ser una mudanza para cruzar la calle.

Qué sí es Agentic RAG

Agentic RAG es un RAG donde el sistema puede decidir pasos intermedios antes de responder. La palabra clave no es “autonomía”; la palabra clave es control del flujo.

Un RAG fijo hace esto:

pregunta -> retrieval -> contexto -> respuesta

Un Agentic RAG puede hacer esto:

pregunta -> diagnosticar
         -> elegir herramienta
         -> buscar
         -> evaluar evidencia
         -> responder o abstenerse

Ejemplo de fórmula. La forma técnica de verlo:

st=(x,  Ht,  Et,  Bt)s_t = (x,\; H_t,\; E_t,\; B_t) at=πθ(st)a_t = \pi_{\theta}(s_t) st+1=step(st,  at)s_{t+1} = \operatorname{step}(s_t,\; a_t)
SímboloSignificadoEjemplo
xxPregunta original.“Compara normativa y FAQ sobre pagos pendientes”.
HtH_tHistorial de pasos hasta el momento.Búsqueda en normativa, lectura de FAQ, validación.
EtE_tEvidencia acumulada.Chunks, citas, resultados SQL o relaciones de grafo.
BtB_tPresupuesto restante.Máximo 4 pasos, 2 búsquedas y 1 consulta externa.
sts_tEstado del flujo en el paso tt.Lo que el sistema sabe y puede hacer ahora.
ata_tAcción elegida.buscar_normativa, consultar_grafo, responder.
πθ\pi_{\theta}Política de decisión del modelo o del router.Decide el siguiente paso.
step\operatorname{step}Ejecución controlada de una acción.Llama a una herramienta y actualiza la traza.

La lista de acciones no debería ser infinita. En un sistema real se define algo así:

AcciónQué haceCuándo tiene sentidoQué registra
buscar_textoBusca chunks por consulta.Pregunta directa o evidencia textual.Query, filtros, top-k, scores y chunks.
buscar_hibridoCombina vector, BM25 y filtros.Hay términos exactos y significado aproximado.Rankings de cada señal y fusión.
descomponerDivide una pregunta en subpreguntas.La respuesta depende de varias fuentes.Subpreguntas y razón de cada una.
routerElige corpus, índice o herramienta.Hay normativa, FAQ, SQL, tickets o grafo.Opción elegida y alternativas descartadas.
evaluar_evidenciaMide si lo recuperado basta.Antes de redactar o cuando hay duda.Soporte, citas candidatas y huecos.
consultar_grafoBusca relaciones entre entidades.Importan dependencias, comunidades o vínculos.Nodos, aristas, fuente y camino usado.
consultar_tablaConsulta datos estructurados.Fechas, importes, estados o conteos exactos.Consulta, resultado y validación.
responderRedacta con citas.Evidencia suficiente.Claims, citas y decisión final.
abstenerseNo responde por falta de soporte.Evidencia insuficiente o permisos insuficientes.Qué faltó y qué fuente sería necesaria.

Ejemplo de fórmula. El presupuesto se puede escribir de forma simple:

Ctotal=t=1Tc(at)C_{\text{total}} = \sum_{t=1}^{T} c(a_t) TTmaxT \le T_{\max}
SímboloSignificadoEjemplo
CtotalC_{\text{total}}Coste total de la ejecución.Tokens, llamadas, latencia o euros.
c(at)c(a_t)Coste de la acción ata_t.Una búsqueda cuesta poco; una llamada LLM cuesta más.
TTNúmero de pasos ejecutados.3 pasos.
TmaxT_{\max}Límite máximo permitido.5 pasos por pregunta.

La frase que debería quedarse en la cabeza: Agentic RAG no es “hacer más cosas”; es decidir si hace falta hacerlas y dejar rastro de cada decisión.

Tipos de RAG avanzado y casos de uso

Antes de elegir una arquitectura, conviene ponerle nombre a cada patrón. Muchos problemas se arreglan con una pieza pequeña: reescritura, router o validación. No todos necesitan un bucle completo.

TipoQué significaCaso donde encajaNo sirve para
RAG de dos pasosSiempre recupera primero y genera después.FAQ, documentación técnica, normativa directa.Preguntas que requieren decidir varias rutas.
Multi-query retrievalGenera varias consultas para una misma pregunta y une resultados.“doble factor”, “2FA”, “MFA” y “autenticación” pueden aparecer en documentos distintos.Fuentes con permisos complejos si no filtras antes.
HyDEGenera un texto hipotético que parece responder y busca documentos reales similares.Consulta vaga sin etiquetas de relevancia ni ejemplos de entrenamiento.Dominios donde el texto hipotético puede desviar hacia detalles falsos.
Query decompositionDivide una pregunta compuesta en subpreguntas.Comparar beca, matrícula y pagos pendientes.Preguntas simples donde añade latencia sin necesidad.
Router RAGElige corpus, índice, herramienta o flujo.Normativa en PDFs, estado vivo en SQL y manuales en Markdown.Sistemas sin fuentes diferenciadas.
Hybrid RAG con validaciónRecupera, evalúa evidencia, corrige o regenera si hace falta.Dominios donde citar mal rompe confianza.Prototipos donde aún no hay dataset de evaluación.
Corrective RAGEvalúa la calidad de los documentos recuperados y activa otra búsqueda si no basta.Corpus incompleto, preguntas ambiguas o retrieval frágil.Sistemas sin fuente alternativa ni umbral definido.
Self-RAGDecide cuándo recuperar y critica pasajes/respuestas mediante señales internas o entrenadas.Respuestas largas donde no siempre hace falta recuperar.Integraciones donde necesitas flujo totalmente determinista.
RAPTORConstruye árboles de resúmenes para recuperar a varios niveles de abstracción.Manuales largos, informes extensos, libros internos.Corpus pequeño con respuestas puntuales.
GraphRAG localUsa grafo para preguntas sobre entidades y relaciones concretas.“¿Qué políticas dependen de este requisito?”Preguntas puramente textuales sin relaciones útiles.
GraphRAG globalUsa resúmenes de comunidades para preguntas sobre el corpus completo.“¿Qué patrones aparecen en todas las incidencias?”Preguntas que solo piden una fecha exacta.
RAG con herramientasEl modelo consulta búsqueda, SQL, APIs o grafo según necesidad.“Busca la política y comprueba si mi expediente cumple.”Saltarse permisos o validar por intuición.

La clave práctica: cada fila añade una nueva promesa y una nueva deuda. Multi-query promete más cobertura y paga más búsquedas. Router promete elegir mejor la fuente y paga clasificación. GraphRAG promete visión relacional y paga extracción, grafo, resúmenes y evaluación nueva.

Arquitecturas, una por una

Cuándo complicar un RAG Cada arquitectura compra una capacidad y paga coste, latencia, trazas y evaluación. 1. RAG de dos pasos pregunta retrieve contexto respuesta top-k 2. Multi-query y HyDE pregunta consulta A consulta B doc hipotético más recall, más ruido posible 3. Router RAG pregunta router PDF SQL API 4. Corrective / Self-RAG retrieve evaluar responder o abstener si la evidencia no basta, corrige 5. Agentic RAG con herramientas estado decidir buscar SQL grafo bucle con límite de pasos 6. GraphRAG local global entidades, relaciones y comunidades La escalera correcta no empieza por el agente Primero mide RAG básico. Luego añade una sola pieza: reescritura, router, validación, bucle o grafo. Cada pieza nueva exige trazas, dataset propio, umbrales y presupuesto de pasos. IA para gente curiosa / Facsímil 04 / Capítulo 11 / 686f6c61

El diagrama enseña la escalera de complejidad. Si una pregunta se resuelve con un RAG de dos pasos, no necesitas un bucle. Si falla por vocabulario, quizá basta multi-query. Si falla por elegir mal la fuente, quizá basta router. Si falla porque el corpus completo tiene patrones, GraphRAG puede tener sentido.

GraphRAG: qué cambia cuando aparece un grafo

Un grafo representa entidades y relaciones. En vez de tratar el corpus solo como chunks sueltos, intentamos extraer una estructura:

G=(V,  E)G = (V,\; E)
SímboloSignificadoEjemplo
GGGrafo de conocimiento extraído o curado.Grafo de normativa, trámites y requisitos.
VVConjunto de nodos o entidades.“Ampliación de matrícula”, “pagos pendientes”, “beca general”.
EEConjunto de aristas o relaciones.“requiere”, “contradice”, “se aplica a”, “depende de”.

Ejemplo de fórmula. Cada arista debería guardar procedencia:

e=(vi,  r,  vj,  fuente,  confianza)e = (v_i,\; r,\; v_j,\; fuente,\; confianza)
SímboloSignificadoEjemplo
viv_iEntidad origen.“Ampliación de matrícula”.
rrRelación.“requiere”.
vjv_jEntidad destino.“no tener pagos pendientes vencidos”.
fuenteDocumento o chunk que sostiene la relación.norm-2026#art-14.
confianzaSeñal de extracción o validación.0,82.

La diferencia con RAG básico es que GraphRAG puede contestar usando caminos, vecindarios o comunidades:

ModoQué buscaPregunta típica
Local searchEntidades y relaciones cercanas a una entidad.“¿Qué requisitos dependen de pagos pendientes?”
Global searchResúmenes de comunidades del grafo.“¿Qué patrones aparecen en las incidencias del curso?”
DRIFT searchCombina señal comunitaria con seguimiento local más amplio.“Explora este tema y saca líneas de investigación.”
Question generationPropone preguntas siguientes para investigar el corpus.“¿Qué debería revisar ahora?”

La documentación de Microsoft GraphRAG lo expresa así: local search combina datos del grafo con chunks originales; global search busca sobre community reports en estilo map-reduce; DRIFT usa información de comunidades para ampliar el punto de partida local.9

GraphRAG encaja cuando la pregunta no vive en un solo fragmento:

Caso cercanoPor qué GraphRAG ayuda
Analizar miles de tickets de soporte.Las relaciones entre producto, síntoma, versión y solución revelan comunidades.
Revisar normativa dispersa.Las dependencias entre requisitos importan tanto como cada párrafo.
Explorar literatura académica.Autores, métodos, datasets y resultados forman un grafo natural.
Mapear incidencias de producto.Puede mostrar temas recurrentes y relaciones entre módulos.
Entender una organización documental.Las entidades conectan documentos que no comparten las mismas palabras.

Pero también tiene costes:

CosteQué implica
ExtracciónEl modelo debe detectar entidades y relaciones con calidad suficiente.
Normalización“doble factor”, “2FA” y “MFA” quizá son la misma entidad.
ActualizaciónSi cambian documentos, hay que actualizar grafo, resúmenes e índices.
EvaluaciónYa no basta medir top-k; hay que medir caminos, relaciones y resúmenes.
ExplicabilidadUna respuesta global debe enseñar qué comunidades o fuentes la sostienen.

Cómo elegir sin montar una catedral

La pregunta de arquitectura se puede formular como diagnóstico:

SíntomaPrimera mejora razonableSi no basta
No aparece el documento correcto por vocabulario.Multi-query o HyDE.Entrenar o cambiar embeddings; añadir BM25/fusión.
La pregunta mezcla varias cosas.Query decomposition.Agentic RAG con subpreguntas y validación.
Hay varias fuentes con reglas distintas.Router.Herramientas especializadas por fuente.
El sistema trae contexto flojo.Retrieval validation y reranker.Corrective RAG o búsqueda alternativa.
El corpus es largo y jerárquico.Resúmenes por sección.RAPTOR o índice jerárquico.
La respuesta depende de relaciones.Grafo de entidades.GraphRAG local.
La pregunta pide patrones del corpus entero.Resúmenes agregados.GraphRAG global.
Hace falta estado exacto.Tool o SQL.Capítulo 12: Text-to-SQL y herramientas de datos.

Regla práctica: añade una sola pieza por experimento. Si incorporas router, multi-query, GraphRAG y validación a la vez, cuando mejore o empeore no sabrás por qué.

Evaluar RAG avanzado

Todo lo que complicas debe medirse. Un Agentic RAG no se evalúa solo por respuesta final: se evalúa por ruta.

CapaMétrica o revisiónPregunta
RouterAccuracy de ruta o matriz de confusión.¿Eligió la fuente correcta?
Multi-queryRecall@k por consulta y unión final.¿Las variantes trajeron evidencia nueva o solo ruido?
DecompositionSubpreguntas necesarias y suficientes.¿Dividió bien el problema?
Corrective RAGTasa de corrección útil.¿Volvió a buscar cuando debía?
Agentic loopPasos, coste, latencia y salida.¿Gastó pasos con sentido?
Graph localNodos/aristas correctos y fuentes.¿El camino usado está sostenido?
Graph globalCobertura, diversidad y trazabilidad.¿El resumen global representa el corpus?
RespuestaGroundedness, citas y abstención.¿Lo dicho está sostenido?

Ejemplo de fórmula. Un gate mínimo:

GτgG \ge \tau_g CCmaxC \le C_{\max} TTmaxT \le T_{\max}
SímboloSignificadoEjemplo
GGGroundedness o soporte mínimo.0,92.
τg\tau_gUmbral de soporte.0,90.
CCCoste total de la ejecución.0,012 euros o 8.000 tokens.
CmaxC_{\max}Coste máximo aceptable.0,02 euros.
TTPasos realizados.4.
TmaxT_{\max}Máximo de pasos.5.

Para GraphRAG hay una evaluación adicional: no basta con que la respuesta suene bien. Hay que revisar si las entidades, relaciones y comunidades usadas son correctas. Un resumen global puede ser fluido y aun así esconder que una comunidad importante no entró en el mapa.

Casos cercanos

Secretaría académica. Preguntan: “¿Puedo ampliar matrícula si tengo una beca pendiente y pagos vencidos?”. Un RAG básico quizá encuentra solo la normativa de ampliación. Query decomposition separa “ampliación”, “beca pendiente” y “pagos vencidos”. El router decide si mirar normativa, becas y calendario. La respuesta final debe citar cada pieza.

Equipo de soporte técnico. Preguntan: “¿Qué problemas se repiten desde la última versión?”. No quieres una respuesta sobre un ticket concreto. Quieres agrupar incidencias por producto, síntoma, versión y solución. Aquí GraphRAG global o resúmenes jerárquicos pueden mostrar patrones que un top-k no ve.

Documentación de ingeniería. Preguntan: “¿Cómo configuro el conector si uso PostgreSQL y despliegue local?”. El sistema puede necesitar buscar en documentación, consultar ejemplos de configuración y revisar límites de versión. Agentic RAG con herramientas de búsqueda acotadas puede ser útil, siempre que cite y limite dominios.

Compliance documental. Preguntan: “¿Qué políticas dependen de este requisito?”. GraphRAG local encaja porque lo importante es el vecindario de una entidad: requisito, políticas, controles, evidencias y documentos fuente.

Producto con datos vivos. Preguntan: “¿Qué clientes están afectados y qué documento explica la política?”. Aquí no basta RAG. Necesitas tool o SQL para estado vivo y RAG para explicar la política. Este puente prepara el capítulo 12.

Para entenderlo sin perderse

Una forma sencilla de explicarlo: un RAG básico se parece a preguntar a alguien que tiene una estantería y te trae tres páginas parecidas a tu pregunta. Si esas páginas contienen la respuesta, perfecto. Si la pregunta exige comparar, comprobar vigencia, mirar otra fuente o seguir una relación entre documentos, la persona necesita una libreta de trabajo: primero mira dónde buscar, luego consulta, después comprueba si lo encontrado basta y solo entonces responde.

En esa metáfora:

PiezaTraducción mentalQué debería quedar claro
RAG básicoTraer páginas parecidas.Sirve si la pregunta vive en uno o varios fragmentos cercanos.
RouterDecidir en qué estantería mirar.No es una caja negra: clasifica la pregunta y elige fuente.
Multi-queryPreguntar lo mismo con varias palabras.Mejora cobertura cuando el vocabulario cambia.
DescomposiciónSeparar una pregunta grande en preguntas pequeñas.Ayuda si hay que comparar o juntar varias condiciones.
ValidadorComprobar si las páginas sostienen la respuesta.Reduce respuestas bonitas pero poco justificadas.
TrazaLibreta de lo que hizo el sistema.Permite depurar, auditar y evaluar.
GrafoMapa de cosas conectadas.Ayuda cuando la relación importa tanto como el texto.
ComunidadBarrio dentro del grafo.Grupo de entidades que aparecen conectadas muchas veces.

El error habitual es imaginar que Agentic RAG “razona más”. En producción me interesa una definición menos romántica: Agentic RAG toma decisiones explícitas entre pasos permitidos y deja evidencia de esas decisiones. Si no hay pasos permitidos, presupuesto y traza, no tienes una arquitectura: tienes una conversación difícil de depurar.

Cómo lo montaría en un sistema real

Si tuviera que construir esto en una empresa, no empezaría por GraphRAG ni por un agente completo. Empezaría por una pregunta incómoda: “¿qué fallo real quiero corregir?”. Si el fallo es vocabulario, multi-query. Si el fallo es escoger mal la fuente, router. Si el fallo es no saber si la evidencia basta, validador. Si el fallo es entender relaciones entre documentos, grafo.

La arquitectura mínima seria tendría estas capas:

Agentic RAG en producción No es un bloque único: es una cadena con contratos, permisos, trazas, evaluación y presupuesto. API usuario, sesión, permisos Normalizador limpia pregunta detecta idioma y tipo Router elige flujo y fuentes devuelve razón y confianza Planificador pasos permitidos presupuesto y parada Orquestador ejecuta herramientas guarda traza completa Índice vectorial embeddings, filtros top-k y metadatos Búsqueda léxica BM25, exact match nombres, códigos, fechas Grafo entidades, relaciones comunidades y fuentes Herramientas SQL, API, cálculo estado vivo Fusión y reranking RRF, reranker, filtros deduplicación Validador soporte, citas, vigencia decide seguir o parar Generador respuesta con citas formato contratado Salida respuesta citas traza resumida Observabilidad logs, latencia, coste, errores Evaluación offline datasets, ablation, regresiones Gobierno de datos versionado, permisos, caducidad Si una caja no tiene contrato, métrica y logs, no está lista para producción. IA para gente curiosa / Facsímil 04 / Capítulo 11 / 686f6c61

Cada caja de la figura debería poder probarse por separado. Un ingeniero no debería preguntar “¿funciona el agente?”, sino cosas más concretas:

  • ¿El router elige bien entre normativa, FAQ, SQL, tickets y grafo?
  • ¿El retriever trae evidencia suficiente con filtros de permisos?
  • ¿La fusión elimina duplicados y mantiene fuentes distintas?
  • ¿El validador detecta falta de soporte, citas flojas o documentos caducados?
  • ¿El generador respeta el formato de salida y no inventa campos?
  • ¿La traza permite reproducir la respuesta cinco días después?

Contratos de herramientas

Una herramienta en un sistema agentic no debería ser “una función que el modelo puede llamar”. Debe ser un contrato. El contrato dice qué recibe, qué devuelve, qué permisos exige, cuánto tarda, cómo falla y cómo se audita.

Ejemplo de contrato de una herramienta de búsqueda documental:

{
  "name": "buscar_normativa",
  "input": {
    "query": "string",
    "filters": {
      "curso": "2026",
      "estado": "vigente"
    },
    "top_k": 8
  },
  "output": {
    "results": [
      {
        "chunk_id": "norm-2026#art-14",
        "score": 0.84,
        "source": "Normativa 2026",
        "valid_from": "2026-01-01",
        "text": "fragmento recuperado"
      }
    ]
  },
  "errors": [
    "permission_denied",
    "source_unavailable",
    "low_recall"
  ],
  "timeout_ms": 1200,
  "audit": true
}

Lo importante no es el JSON, sino la disciplina:

CampoQué aportaQué se rompe si falta
nameIdentidad estable de la herramienta.No puedes comparar ejecuciones ni versionar cambios.
inputQué puede pedir el sistema.El modelo puede mandar consultas vagas o imposibles.
filtersPermisos, vigencia, idioma, cliente o corpus.Puedes mezclar documentos que no deberían mezclarse.
top_kLímite de resultados.Coste y contexto crecen sin control.
scoreSeñal de recuperación.No sabes por qué entró un fragmento.
sourceProcedencia legible.No puedes citar ni revisar.
valid_fromVigencia temporal.Respuestas correctas pueden quedar obsoletas.
errorsFallos esperados.El sistema trata un fallo como si fuera ausencia de información.
timeout_msLímite de latencia.Una herramienta lenta secuestra toda la respuesta.
auditObligación de guardar traza.No puedes reproducir ni explicar decisiones.

Un contrato de herramienta también debe declarar qué hacer cuando no basta:

ResultadoDecisión correcta
permission_deniedNo buscar atajos; responder que no hay permiso suficiente.
source_unavailableDegradar con aviso o pedir reintento, según criticidad.
low_recallReescribir consulta, usar búsqueda híbrida o abstenerse.
empty_resultDistinguir “no existe” de “no he podido encontrarlo”.
conflicting_sourcesPriorizar vigencia, autoridad y citar el conflicto.

GraphRAG por dentro

GraphRAG no empieza en la query; empieza en la ingesta. Antes de responder hay que convertir documentos en entidades, relaciones, comunidades y resúmenes. Ese proceso tiene mucha ingeniería escondida.

Una tubería razonable:

PasoQué haceRiesgo principalControl
IngestaLee PDFs, HTML, Markdown, tickets, tablas o transcripciones.Perder estructura del documento.Guardar fuente, página, sección y fecha.
ChunkingParte documentos en unidades recuperables.Cortar relaciones importantes.Chunks con solape y metadatos ricos.
ExtracciónDetecta entidades y relaciones.Extraer nombres distintos para lo mismo.Diccionario, revisión y normalización.
CanonicalizaciónUne variantes de una entidad.Mezclar entidades parecidas pero distintas.Alias, reglas y confianza.
AristasCrea relaciones con fuente.Relación sin prueba documental.Toda arista guarda chunk, frase y score.
ComunidadesAgrupa zonas densas del grafo.Comunidades demasiado grandes o pequeñas.Medir modularidad y revisar muestras.
ResúmenesResume comunidades.Perder excepciones importantes.Citar nodos y documentos representativos.
ÍndicesIndexa texto, nodos, aristas y resúmenes.Recuperar solo una vista parcial.Búsqueda híbrida y evaluación por tipo.
ActualizaciónReprocesa cambios del corpus.Grafo viejo con documentos nuevos.Versionado, diffs y caducidad.

Se puede escribir de forma compacta:

V=canon(entidades(D))V = \operatorname{canon}(\operatorname{entidades}(D)) E={(vi,  r,  vj,  fuente,  confianza)}E = \{(v_i,\; r,\; v_j,\; fuente,\; confianza)\} Rc=resumen(Cc,  fuentesc)R_c = \operatorname{resumen}(C_c,\; fuentes_c)
SímboloSignificadoEjemplo
DDDocumentos de entrada.Normativa, FAQ, tickets y manuales.
VVEntidades normalizadas.“2FA” y “doble factor” como la misma entidad.
EERelaciones con fuente.“ampliación requiere no tener pagos vencidos”.
CcC_cComunidad del grafo.Trámites de matrícula y pagos.
RcR_cResumen de comunidad.“Los bloqueos se concentran en pagos y plazos”.

La parte difícil no es dibujar nodos. La parte difícil es saber si el nodo es correcto, si dos nodos son la misma cosa, si la relación tiene fuente, si el resumen no borra excepciones y si el grafo se actualiza cuando cambia el corpus.

Coste, latencia y presupuesto

Un RAG avanzado puede mejorar respuestas y empeorar producto si duplica latencia sin medirlo. Por eso hay que convertir la arquitectura en números.

Si generas qq consultas, traes kk fragmentos por consulta y cada fragmento tiene LL tokens de media:

NctxqkLN_{\text{ctx}} \approx q \cdot k \cdot L

Si además usas reranker y un generador:

Ttotal=Trouter+maxi(Tretrieval,i)+Trerank+Tvalidacioˊn+TgeneracioˊnT_{\text{total}} = T_{\text{router}} + \max_i(T_{\text{retrieval},i}) + T_{\text{rerank}} + T_{\text{validación}} + T_{\text{generación}}

Y el coste de una respuesta puede aproximarse así:

Crespuesta=Cemb+Cretrieval+Crerank+CLLMC_{\text{respuesta}} = C_{\text{emb}} + C_{\text{retrieval}} + C_{\text{rerank}} + C_{\text{LLM}}
NúmeroQué significaQué mirar en producción
qqNúmero de consultas generadas.Si sube, suben recall, coste y ruido.
kkFragmentos recuperados por consulta.Un top-k alto puede tapar la evidencia buena.
LLTokens por fragmento.Chunks largos llenan contexto rápido.
TtotalT_{\text{total}}Latencia total.Medir P50, P95 y timeouts, no solo media.
CrespuestaC_{\text{respuesta}}Coste por respuesta.Dividir por respuesta útil, no por llamada.
CindexC_{\text{index}}Coste de indexar.En GraphRAG puede ser alto antes de la primera query.

Para GraphRAG hay otro coste:

Cgraph-index=Cextraccioˊn+Cnormalizacioˊn+Ccomunidades+CresuˊmenesC_{\text{graph-index}} = C_{\text{extracción}} + C_{\text{normalización}} + C_{\text{comunidades}} + C_{\text{resúmenes}}

Este coste se paga al construir o actualizar el índice, no solo al responder. Por eso GraphRAG puede ser brillante en colecciones estables y caro en corpus que cambian cada hora.

Evaluación para ingeniería

La evaluación de RAG avanzado tiene que separar piezas. Si solo miras la respuesta final, no sabes si mejoró el retriever, el router, el grafo o simplemente hubo suerte en una muestra.

Un plan de evaluación serio:

PruebaQué comparaPregunta que responde
BaselineRAG básico contra sistema nuevo.¿Complicar mejora algo medible?
AblationQuitar una pieza cada vez.¿Qué aporta router, multi-query, grafo o validador?
Router accuracyRuta esperada contra ruta elegida.¿Va a la fuente correcta?
Recall@kEvidencia esperada dentro del top-k.¿Recupera lo necesario?
MRR / nDCGOrden de documentos relevantes.¿Lo bueno aparece arriba?
Node precisionNodos correctos del grafo.¿Las entidades recuperadas son válidas?
Edge precisionRelaciones correctas del grafo.¿Las aristas están sostenidas por fuentes?
Citation supportClaims con cita suficiente.¿Cada afirmación importante está soportada?
Abstention rateCasos donde no responde.¿Se abstiene cuando falta evidencia?
Latencia P95Tiempo para el 95% de consultas.¿El producto aguanta en uso real?
Coste por respuesta útilCoste dividido por respuestas aceptables.¿La mejora compensa?

El ablation test es especialmente sano:

VarianteQué activaQué esperas aprender
ARAG básico.Línea base.
BA + búsqueda híbrida.Si el problema era vocabulario exacto.
CB + multi-query.Si faltaba cobertura semántica.
DC + router.Si elegir fuente aporta mejora.
ED + validador.Si reduce respuestas sin soporte.
FE + GraphRAG.Si las relaciones añaden valor real.

Si F gana solo un 1% pero dobla latencia y coste, quizá no merece producción. Si F gana mucho en preguntas relacionales y pierde en preguntas simples, el router debe activar GraphRAG solo cuando toque.

Caso completo de diseño

Supongamos un sistema con 50.000 documentos internos: normativa, manuales, tickets, FAQs, actas y algunas tablas con estado vivo. El objetivo es responder a equipos internos con citas, permisos y trazas.

Yo lo diseñaría por fases:

FaseQué haríaCriterio para avanzar
1. CorpusInventario de fuentes, permisos, vigencia y formatos.Saber qué puede ver cada usuario y qué fuente manda.
2. BaselineRAG básico con chunking, embeddings y búsqueda híbrida.Dataset de 100-300 preguntas reales con respuestas esperadas.
3. EvaluaciónMedir recall@k, soporte de citas, abstención y latencia.Detectar el fallo dominante.
4. RouterSeparar normativa, FAQ, tickets, manuales, SQL y grafo.Accuracy de ruta suficiente y trazas legibles.
5. ValidaciónAñadir groundedness, vigencia y conflicto entre fuentes.Menos respuestas sin soporte.
6. Grafo localEntidades y relaciones para normativa y dependencias.Mejora clara en preguntas relacionales.
7. GraphRAG globalComunidades para tickets, actas e incidencias.Mejora clara en preguntas de patrones.
8. ProducciónObservabilidad, costes, permisos, caché y regresiones.Alertas y evaluación continua antes de ampliar uso.

La decisión final no sería “usar GraphRAG sí o no”. Sería una política:

Tipo de preguntaFlujo recomendado
“¿Dónde dice X?”RAG básico con búsqueda híbrida y citas.
“¿Cuál es el estado actual de X?”Tool o SQL, después explicación con RAG.
“Compara X e Y”Descomposición, router y validación.
“¿Qué depende de este requisito?”GraphRAG local.
“¿Qué patrones aparecen en todo el corpus?”GraphRAG global.
“No hay evidencia suficiente”Abstención con fuente necesaria.

Este diseño también ayuda a una persona curiosa: no hay una herramienta universal. Hay una escalera. Cada peldaño se sube cuando el anterior falla de una forma concreta.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a simular un mini Agentic RAG sin APIs. El objetivo no es crear un agente real, sino entender la traza: router, búsqueda textual, consulta de grafo, evaluación de evidencia y decisión final.

from collections import Counter


CHUNKS = {
    "norm-2026#ampliacion": {
        "titulo": "Normativa 2026: ampliación",
        "texto": (
            "La ampliación de matrícula se solicita en septiembre. "
            "No se admite si existen pagos pendientes vencidos."
        ),
        "tipo": "normativa",
    },
    "becas-2026#calendario": {
        "titulo": "Becas 2026: calendario",
        "texto": (
            "La beca pendiente no bloquea por sí sola la ampliación. "
            "El pago vencido sí requiere revisión previa."
        ),
        "tipo": "becas",
    },
    "faq#doble-factor": {
        "titulo": "FAQ: doble factor",
        "texto": (
            "Si no puedes entrar al campus virtual, revisa el doble "
            "factor y restablece la contraseña."
        ),
        "tipo": "faq",
    },
    "incidencias#version": {
        "titulo": "Incidencias versión 4.2",
        "texto": (
            "Desde la versión 4.2 se repiten incidencias "
            "de doble factor, sesiones caducadas y "
            "permisos de matrícula."
        ),
        "tipo": "ticket",
    },
}

GRAPH = [
    (
        "ampliación de matrícula",
        "requiere",
        "no tener pagos pendientes vencidos",
        "norm-2026#ampliacion",
    ),
    (
        "beca pendiente",
        "no bloquea",
        "ampliación de matrícula",
        "becas-2026#calendario",
    ),
    (
        "versión 4.2",
        "se relaciona con",
        "doble factor",
        "incidencias#version",
    ),
    (
        "versión 4.2",
        "se relaciona con",
        "permisos de matrícula",
        "incidencias#version",
    ),
]

STOPWORDS = {
    "a", "al", "con", "de", "del", "el", "en", "es", "la",
    "las", "lo", "los", "me", "mi", "no", "por", "que",
    "se", "si", "un", "una", "y",
}


def tokens(texto):
    limpio = "".join(c.lower() if c.isalnum() else " " for c in texto)
    return [
        t for t in limpio.split()
        if t not in STOPWORDS and len(t) > 2
    ]


def buscar_texto(query, tipo=None, k=2):
    q = set(tokens(query))
    resultados = []
    for chunk_id, chunk in CHUNKS.items():
        if tipo and chunk["tipo"] != tipo:
            continue
        texto = chunk["texto"] + " " + chunk["titulo"]
        score = len(q & set(tokens(texto)))
        if score:
            resultados.append((score, chunk_id))
    ranking = sorted(resultados, reverse=True)[:k]
    return [chunk_id for score, chunk_id in ranking]


def buscar_grafo(query):
    q = set(tokens(query))
    hallazgos = []
    modo_global = bool({"patrones", "repiten", "temas"} & q)
    minimo = 1 if modo_global else 2
    for origen, relacion, destino, fuente in GRAPH:
        texto = f"{origen} {relacion} {destino}"
        score = len(q & set(tokens(texto)))
        if score >= minimo:
            hallazgos.append((score, origen, relacion, destino, fuente))
    return sorted(hallazgos, reverse=True)


def evaluar_evidencia(ids):
    texto = " ".join(CHUNKS[i]["texto"] for i in ids)
    cobertura = len(set(tokens(texto)))
    return min(cobertura / 18, 1.0)


def elegir_plan(pregunta):
    t = set(tokens(pregunta))
    if {"patrones", "repiten", "temas"} & t:
        return ["buscar_grafo", "buscar_texto", "responder"]
    if {"compara", "beca", "pagos"} & t:
        return [
            "descomponer",
            "buscar_texto",
            "buscar_grafo",
            "evaluar",
            "responder",
        ]
    if {"estado", "clientes", "afectados"} & t:
        return ["consultar_tabla", "buscar_texto", "responder"]
    return ["buscar_texto", "evaluar", "responder"]


def responder(pregunta):
    plan = elegir_plan(pregunta)
    traza = []
    evidencia = []
    relaciones = []

    for paso in plan:
        if paso == "descomponer":
            subpreguntas = [
                "ampliación de matrícula pagos pendientes",
                "beca pendiente ampliación matrícula",
            ]
            traza.append(("descomponer", subpreguntas))
        elif paso == "buscar_texto":
            consultas = [pregunta]
            if "descomponer" in [p for p, _ in traza]:
                consultas = traza[-1][1]
            for consulta in consultas:
                palabras = set(tokens(consulta))
                tipo = None
                if "beca" in palabras:
                    tipo = "becas"
                elif {"normativa", "pagos", "vencidos"} & palabras:
                    tipo = "normativa"
                nuevos = buscar_texto(consulta, tipo=tipo)
                if not nuevos:
                    nuevos = buscar_texto(consulta)
                for chunk_id in nuevos:
                    if chunk_id not in evidencia:
                        evidencia.append(chunk_id)
            traza.append(("buscar_texto", list(evidencia)))
        elif paso == "buscar_grafo":
            relaciones = buscar_grafo(pregunta)
            fuentes = [r[-1] for r in relaciones]
            evidencia.extend(i for i in fuentes if i not in evidencia)
            traza.append(("buscar_grafo", relaciones[:3]))
        elif paso == "consultar_tabla":
            traza.append((
                "consultar_tabla",
                "en este ejemplo no hay tabla viva",
            ))
        elif paso == "evaluar":
            soporte = evaluar_evidencia(evidencia)
            traza.append(("evaluar", round(soporte, 2)))
            if soporte < 0.55:
                extra = buscar_texto(
                    "normativa matrícula beca pagos vencidos",
                    k=3,
                )
                evidencia.extend(i for i in extra if i not in evidencia)
                traza.append(("corregir_busqueda", extra))
        elif paso == "responder":
            citas = sorted(set(evidencia))
            conteo = Counter(CHUNKS[i]["tipo"] for i in citas)
            traza.append((
                "responder",
                {"citas": citas, "tipos": dict(conteo)},
            ))

    return traza


preguntas = [
    "Compara beca pendiente y pagos vencidos para ampliar matrícula",
    "Qué problemas se repiten desde la versión 4.2",
    "Cómo recupero el acceso con doble factor",
]

for pregunta in preguntas:
    print("\\nPREGUNTA:", pregunta)
    for paso, detalle in responder(pregunta):
        print("-", paso, "=>", detalle)

Salida esperada aproximada:

PREGUNTA: Compara beca pendiente y pagos vencidos
- descomponer => 2 subpreguntas
- buscar_texto => norm-2026#ampliacion, becas-2026#calendario
- buscar_grafo => [...]
- evaluar => 1.0
- responder => citas y tipos de fuente

PREGUNTA: Qué problemas se repiten desde la versión 4.2
- buscar_grafo => [...]
- buscar_texto => incidencias#version
- responder => citas y tipos de fuente

PREGUNTA: Cómo recupero el acceso con doble factor
- buscar_texto => faq#doble-factor, incidencias#version
- evaluar => 1.0
- responder => citas y tipos de fuente

Prueba tres cambios:

  • Baja el umbral de evaluar_evidencia y observa cuándo se corrige menos.
  • Añade un chunk contradictorio y mira si tu plan debería incluir validación de vigencia.
  • Añade una fuente sql simulada y prepara el puente hacia el capítulo 12.

Cómo encaja todo

graph TD
    subgraph "Capítulo 11: RAG avanzado"
        BASIC["RAG de dos pasos"]
        MQ["Multi-query / HyDE"]
        DECOMP["Descomposición"]
        ROUTER["Router"]
        VALIDAR["Validar evidencia"]
        LOOP["Bucle agentic"]
        GRAPH["GraphRAG"]
        GLOCAL["Local search"]
        GGLOBAL["Global search"]
        TRACE["Trazas y presupuesto"]
        GATE["Gate de publicación"]
    end
    subgraph "Viene de antes"
        EMB["Embeddings (F4C7)"]
        VECTOR["Bases vectoriales (F4C8)"]
        RAGB["RAG básico (F4C9)"]
        EVAL["Evaluar RAG (F4C10)"]
        CLASICA["Búsqueda y grafos (F2)"]
    end
    subgraph "Sigue después"
        SQL["Text-to-SQL (F4C12)"]
        LAB["Laboratorio mínimo (F4C13)"]
        AGENTES["Agentes (F5)"]
        OPS["Operación (F6)"]
        EVALS["Evaluación avanzada (F7)"]
    end

    EMB -->|"hace posible"| BASIC
    VECTOR -->|"sirve a"| BASIC
    RAGB -->|"se amplía con"| MQ
    RAGB -->|"se amplía con"| DECOMP
    RAGB -->|"se amplía con"| ROUTER
    MQ -->|"aumenta recall"| VALIDAR
    DECOMP -->|"genera subpreguntas"| VALIDAR
    ROUTER -->|"elige fuente"| LOOP
    VALIDAR -->|"decide si repetir"| LOOP
    CLASICA -->|"prepara intuición de grafos"| GRAPH
    GRAPH -->|"usa"| GLOCAL
    GRAPH -->|"usa"| GGLOBAL
    LOOP -->|"debe registrar"| TRACE
    GRAPH -->|"debe registrar"| TRACE
    EVAL -->|"define métricas para"| GATE
    TRACE -->|"alimenta"| GATE
    LOOP -->|"prepara"| AGENTES
    ROUTER -->|"prepara"| SQL
    GATE -->|"se practica en"| LAB
    TRACE -->|"pasa a"| OPS
    GATE -->|"pasa a"| EVALS

    style BASIC fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MQ fill:#F5F5F5,stroke:#000000,stroke-width:2
    style DECOMP fill:#F5F5F5,stroke:#000000,stroke-width:2
    style ROUTER fill:#F5F5F5,stroke:#000000,stroke-width:2
    style VALIDAR fill:#F5F5F5,stroke:#000000,stroke-width:2
    style LOOP fill:#F5F5F5,stroke:#000000,stroke-width:2
    style GRAPH fill:#F5F5F5,stroke:#000000,stroke-width:2
    style GLOCAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style GGLOBAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TRACE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style GATE fill:#111111,stroke:#000000,stroke-width:2,color:#FFFFFF
    style EMB stroke-dasharray: 5 5
    style VECTOR stroke-dasharray: 5 5
    style RAGB stroke-dasharray: 5 5
    style EVAL stroke-dasharray: 5 5
    style CLASICA stroke-dasharray: 5 5
    style SQL stroke-dasharray: 5 5
    style LAB stroke-dasharray: 5 5
    style AGENTES stroke-dasharray: 5 5
    style OPS stroke-dasharray: 5 5
    style EVALS stroke-dasharray: 5 5

Vocabulario aprendido

TérminoDefinición
Agentic RAGRAG donde un modelo o router decide pasos de búsqueda, herramientas y validación antes de responder.
RAG de dos pasosFlujo fijo: recuperar contexto y generar respuesta.
Multi-query retrievalVarias consultas para cubrir vocabularios distintos de una misma necesidad.
HyDEGenerar un documento hipotético para buscar documentos reales parecidos.
Query decompositionDividir una pregunta grande en subpreguntas recuperables.
RouterPieza que decide qué corpus, índice, herramienta o flujo usar.
Corrective RAGRAG que evalúa si lo recuperado basta y corrige si no basta.
Self-RAGEnfoque donde la recuperación y la crítica forman parte del comportamiento del modelo.
RAPTORRecuperación con árbol de resúmenes a distintos niveles de abstracción.
GraphRAGRAG basado en grafo de entidades, relaciones y resúmenes.
Local searchBúsqueda centrada en entidades o relaciones concretas del grafo.
Global searchBúsqueda sobre resúmenes de comunidades para preguntas del corpus completo.
Community summaryResumen de un grupo de nodos relacionados dentro del grafo.
Presupuesto de pasosLímite de acciones, llamadas, coste o latencia permitido.
Contrato de herramientaEspecificación de entradas, salidas, errores, permisos, timeout y auditoría de una herramienta.
TrazaRegistro reproducible de ruta, consultas, resultados, decisiones, costes y citas usadas.
CanonicalizaciónUnión controlada de variantes que representan la misma entidad.
Ablation testPrueba donde se quita una pieza del sistema para medir qué aporta realmente.
Latencia P95Tiempo por debajo del cual responde el 95% de las consultas.
Edge precisionProporción de relaciones del grafo que son correctas y están sostenidas por fuentes.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Llamar agente a cualquier ifUn router determinista puede ser suficiente; no todo flujo condicional es un agente.Nombrar la pieza exacta: router, validador, descomposición o bucle.
Añadir bucles sin presupuestoLa latencia y el coste se vuelven impredecibles.Definir TmaxT_{\max}, coste máximo y condición de parada.
Usar GraphRAG sin relaciones útilesSi no hay entidades ni vínculos relevantes, el grafo añade mantenimiento sin mejorar respuestas.Probar primero si las preguntas fallan por relaciones, no por retrieval básico.
No guardar la ruta de decisiónSi falla, no sabes qué fuente eligió ni por qué.Guardar traza: ruta, consultas, resultados, scores, citas y validaciones.
Medir solo groundedness finalPuede responder bien por casualidad aunque haya elegido mal el camino.Evaluar router, subpreguntas, nodos, aristas, pasos y respuesta.
Mezclar todas las mejoras a la vezNo puedes atribuir la mejora ni depurar el fallo.Añadir una pieza por experimento y comparar contra baseline.
No definir contratos de herramientasCada integración acaba devolviendo lo que quiere y fallando de forma distinta.Declarar input, output, errores, timeout, permisos y trazas.
Ignorar el coste de indexar GraphRAGEl grafo también cuesta antes de contestar la primera pregunta.Separar coste de indexación y coste por query.
Mirar solo la media de latenciaLa media puede ocultar consultas lentas que rompen la experiencia.Medir P50, P95, timeouts y coste por respuesta útil.

Antes de pasar página

  • ¿Puedo explicar cuándo basta un RAG de dos pasos?
  • ¿Sé distinguir multi-query, HyDE y query decomposition?
  • ¿Puedo explicar qué hace un router y qué debe registrar?
  • ¿Sé por qué Corrective RAG y Self-RAG no son “más complejidad por presumir”, sino validación y decisión de recuperación?
  • ¿Puedo escribir qué es un grafo G=(V,E)G=(V,E) y qué significa una arista con fuente?
  • ¿Sé cuándo GraphRAG local encaja mejor que GraphRAG global?
  • ¿Puedo explicar cómo se construye GraphRAG: entidades, relaciones, comunidades y resúmenes?
  • ¿Sé diseñar un contrato mínimo para una herramienta de búsqueda?
  • ¿Puedo definir un presupuesto de pasos, coste y latencia para un Agentic RAG?
  • ¿Sé calcular por qué multi-query aumenta contexto, coste y ruido?
  • ¿Sé qué métricas miraría además de la respuesta final?
  • ¿Puedo plantear un ablation test para saber qué pieza aporta mejora?
  • ¿He ejecutado el ejemplo y mirado la traza de cada pregunta?

En resumen

Idea fuerzaDetalle
No empieces por Agentic RAG.Empieza por RAG básico evaluado; complica solo cuando sabes qué falla.
Agentic RAG decide pasos.Puede reescribir, dividir, enrutar, consultar herramientas, validar y volver a buscar.
GraphRAG usa relaciones.Sirve cuando importan entidades, dependencias, comunidades o preguntas globales del corpus.
Producción exige contratos.Cada herramienta necesita entradas, salidas, errores, permisos, timeout y trazas.
GraphRAG cuesta antes de responder.Extraer entidades, normalizar, crear comunidades y resumir también forman parte del presupuesto.
Cada pieza nueva exige evaluación propia.Router, descomposición, grafo, bucle y respuesta final se miden por separado.
El ablation test evita autoengaños.Compara RAG básico contra mejoras incrementales para saber qué aporta cada pieza.
El coste no es solo dinero.También pagas latencia, trazabilidad, mantenimiento, permisos y complejidad de depuración.

Para saber más

Asai, A., Wu, Z., Wang, Y., Sil, A. y Hajishirzi, H. (2023). Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. arXiv

Cormack, G. V., Clarke, C. L. A. y Buettcher, S. (2009). Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. Proceedings of SIGIR, 758-759. DOI

Edge, D. et al. (2024). From Local to Global: A Graph RAG Approach to Query-Focused Summarization. arXiv

Gao, L., Ma, X., Lin, J. y Callan, J. (2022). Precise Zero-Shot Dense Retrieval without Relevance Labels. arXiv

LangChain. (2026). Retrieval. Documentación oficial

Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474. NeurIPS

LlamaIndex. (2026). Agentic strategies. Documentación oficial

Microsoft. (2026). GraphRAG Query Engine overview. Documentación oficial

OpenAI. (2026). Graders. Documentación oficial

Sarthi, P. et al. (2024). RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval. arXiv

Schick, T. et al. (2023). Toolformer: Language Models Can Teach Themselves to Use Tools. DOI

Yan, S.-Q., Gu, J.-C., Zhu, Y. y Ling, Z.-H. (2024). Corrective Retrieval Augmented Generation. arXiv

Yao, S. et al. (2023). ReAct: Synergizing Reasoning and Acting in Language Models. International Conference on Learning Representations. arXiv

Notas

  1. Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474. NeurIPS.

  2. LangChain. (2026). Retrieval. Documentación oficial. Consultado el 26 de mayo de 2026. La página compara 2-step RAG, Agentic RAG e Hybrid RAG, y explica que un agente puede decidir cuándo y cómo recuperar mediante herramientas.

  3. LlamaIndex. (2026). Agentic strategies. Documentación oficial. Consultado el 26 de mayo de 2026.

  4. Yao, S. et al. (2023). ReAct: Synergizing Reasoning and Acting in Language Models. International Conference on Learning Representations. arXiv.

  5. Schick, T. et al. (2023). Toolformer: Language Models Can Teach Themselves to Use Tools. DOI.

  6. Gao, L., Ma, X., Lin, J. y Callan, J. (2022). Precise Zero-Shot Dense Retrieval without Relevance Labels. arXiv. Asai, A., Wu, Z., Wang, Y., Sil, A. y Hajishirzi, H. (2023). Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. arXiv. Yan, S.-Q., Gu, J.-C., Zhu, Y. y Ling, Z.-H. (2024). Corrective Retrieval Augmented Generation. arXiv.

  7. Edge, D. et al. (2024). From Local to Global: A Graph RAG Approach to Query-Focused Summarization. arXiv.

  8. Microsoft. (2026). GraphRAG Query Engine overview. Documentación oficial. Consultado el 26 de mayo de 2026.

  9. Microsoft, 2026.

Capítulo 12

Facsímil 4 · La caja de herramientas

Capítulo 12: Text-to-SQL y herramientas de datos

La pregunta que no está en un documento

En el capítulo 11 vimos que un sistema puede decidir consultar distintas fuentes antes de responder. Algunas fuentes son textos: normativas, manuales, tickets, actas. Otras no son textos en sentido estricto: tablas, bases de datos, métricas, historiales, estados de pedidos, matrículas, pagos, sensores o registros de producto.

Ahí aparece una frontera importante. Un RAG puede explicar “qué dice la política de matrícula”. Pero si preguntas “¿cuántos alumnos tienen pago pendiente y beca en revisión?”, no quieres que el modelo imagine un número a partir de párrafos. Quieres que consulte datos.

Text-to-SQL nace de esa necesidad: convertir una pregunta humana en una consulta SQL controlada. No es “hablar con la base de datos como si fuera una persona”. Es construir un puente auditado entre intención, esquema, permisos, consulta, ejecución, resultado y explicación.

Estado del arte con fecha de corte

Fecha de corte: 26 de mayo de 2026.
Fuentes consultadas ese día: papers de Spider, RAT-SQL, PICARD, BIRD y Spider 2.0; documentación oficial de LangChain SQL Agent, LlamaIndex NL SQL, OpenAI Function Calling y Structured Outputs; documentación de SQLGlot, DuckDB y repositorio público de Vanna.

Text-to-SQL se estudia desde hace años como una tarea de semantic parsing: traducir lenguaje natural a una representación formal ejecutable. Spider marcó un salto porque propuso preguntas y consultas complejas sobre 200 bases de datos, con esquemas distintos entre entrenamiento y prueba; su objetivo era medir generalización a bases nuevas, no memorizar una sola tabla.1

Después se vio que una parte crítica no era solo generar SQL, sino entender el esquema. RAT-SQL trabajó explícitamente el schema linking: conectar palabras de la pregunta con tablas, columnas y relaciones del esquema mediante atención consciente de relaciones.2 PICARD atacó otro problema: incluso un modelo capaz puede producir SQL inválido. Su propuesta restringe la decodificación con parsing incremental para rechazar tokens que romperían la sintaxis formal.3

Los benchmarks más recientes empujaron la tarea hacia condiciones más reales. BIRD introdujo bases de datos grandes, valores sucios, conocimiento externo y eficiencia de consulta; reportó 12.751 pares pregunta-SQL sobre 95 bases de datos y 33,4 GB.4 Spider 2.0 fue más allá: problemas de flujo empresarial, más de 1.000 columnas en algunas bases, varios dialectos como BigQuery y Snowflake, metadatos extensos y tareas que pueden requerir múltiples consultas.5

En herramientas prácticas, LangChain documenta un flujo de SQL agent que lista tablas, decide cuáles son relevantes, inspecciona esquemas, genera consulta, revisa errores comunes, ejecuta y formula respuesta.6 LlamaIndex ofrece componentes NL SQL y advierte que ejecutar consultas generadas requiere especial cuidado con permisos y entorno.7 OpenAI documenta function calling como forma de describir herramientas mediante esquemas y recibir argumentos estructurados; Structured Outputs permite exigir que una salida siga un esquema JSON compatible.8

Qué no es Text-to-SQL

Text-to-SQL no es “darle acceso libre a la base de datos al modelo”. El modelo no debería decidir por su cuenta qué puede leer, cuántas filas puede sacar, qué tablas puede cruzar o si una consulta es aceptable. Eso lo decide el sistema.

Tampoco es un reemplazo completo de un equipo de datos. Muchas preguntas parecen simples y esconden definiciones de negocio: “cliente activo”, “ingreso neto”, “alumno matriculado”, “churn”, “pedido completado”. Si esas definiciones no viven en una capa semántica o en documentación recuperable, el modelo puede generar una consulta válida que responde a otra pregunta.

Y no es solo una tarea de SQL. En producción intervienen permisos, catálogo, metadatos, documentación de columnas, dialecto, coste, timeouts, límites de filas, validación, trazas y evaluación. La consulta es una pieza. El sistema es todo lo que evita que esa pieza se use mal.

Qué sí es

Text-to-SQL es una tubería controlada:

pregunta -> intención -> esquema relevante -> SQL candidato
         -> validación -> ejecución limitada -> resultado
         -> explicación con trazas

La unidad de trabajo no es “una query”. La unidad de trabajo es una solicitud de datos con contexto:

PiezaQué contieneEjemplo
PreguntaLo que pide la persona.“Ingresos por campus en marzo”.
UsuarioIdentidad, rol y permisos.analista_matricula, campus permitido.
DominioÁrea de datos.Matrícula, becas, pagos, soporte.
EsquemaTablas, columnas, claves y tipos.pagos, alumnos, campus.
SemánticaDefiniciones de negocio.“ingreso neto = importe - devoluciones”.
SQLConsulta candidata.SELECT campus, SUM(...) ....
ValidaciónReglas antes de ejecutar.Solo SELECT, LIMIT, timeout.
ResultadoFilas devueltas.Tabla agregada.
ExplicaciónResumen humano.“Campus Norte concentra el 42%”.
TrazaRegistro completo.Tablas usadas, SQL, coste, tiempo.

Para entenderlo bien, pensemos en una pregunta sencilla:

“¿Cuáles son los tres campus con más pagos pendientes?”

El sistema no debería saltar directamente a escribir SQL. Primero debe saber qué significa “pagos pendientes”, dónde vive “campus”, si la persona puede ver todos los campus, si hay que excluir pagos anulados, si la moneda importa y si “tres” implica ordenar de mayor a menor.

SQL desde cero, pero sin rebajarlo

SQL es el lenguaje clásico para consultar datos relacionales. Una base relacional organiza información en tablas. Cada tabla tiene filas y columnas. Una fila representa una entidad o evento; una columna representa un atributo.

Una base de datos no es un Excel grande. En una hoja plana es habitual repetir datos para que todo quepa en una misma vista. En una base relacional se separan entidades y eventos para que cada cosa tenga un lugar claro: alumnos por un lado, pagos por otro, becas por otro. Después una consulta une lo que necesita mediante claves.

SQL pregunta sobre relaciones, no sobre una hoja gigante El modelo debe entender entidades, claves, filtros y métricas antes de escribir una consulta. Hoja plana todo repetido para leer rápido alumno campus pago estado 1 Norte 420 pendiente 1 Norte 380 pendiente 2 Sur 300 pagado 2 Sur 120 pendiente fácil de mirar, difícil de gobernar Base relacional entidades separadas y unidas por claves alumnos alumno_id PK campus pagos pago_id PK alumno_id FK becas alumno_id FK estado_beca cada unión debe declarar una clave correcta Consulta controlada la pregunta se convierte en contrato métrica: SUM(importe) filtro: estado = pendiente grupo: campus límite, permisos y traza El modelo no debería “saber el número”: debería construir una consulta que otro componente pueda revisar. IA para gente curiosa / Facsímil 04 / Capítulo 12 / 686f6c61

Ejemplo mínimo:

tabla pagossignificado
pago_ididentificador del pago.
alumno_idalumno asociado.
campuscampus administrativo.
estadopagado, pendiente, devuelto.
importecantidad.
fechafecha del movimiento.

Una consulta básica:

SELECT campus, SUM(importe) AS total_pendiente
FROM pagos
WHERE estado = 'pendiente'
GROUP BY campus
ORDER BY total_pendiente DESC
LIMIT 3;

Esto se lee así:

CláusulaQué haceEn castellano
SELECTElige columnas o cálculos.Quiero campus y suma de importe.
FROMIndica la tabla base.Usa la tabla de pagos.
WHEREFiltra filas antes de agrupar.Solo pagos pendientes.
GROUP BYAgrupa filas por una dimensión.Junta pagos por campus.
ORDER BYOrdena resultados.Primero los importes mayores.
LIMITLimita filas devueltas.Dame solo tres.

La parte que más se suele subestimar es el JOIN: cruzar tablas. Si pagos tiene alumno_id, pero la titulación vive en alumnos, necesitamos unir:

SELECT a.titulacion, SUM(p.importe) AS total_pendiente
FROM pagos AS p
JOIN alumnos AS a ON a.alumno_id = p.alumno_id
WHERE p.estado = 'pendiente'
GROUP BY a.titulacion
ORDER BY total_pendiente DESC
LIMIT 5;

Un JOIN no es decoración. Es una afirmación sobre relación entre tablas. Si la clave es incorrecta, el resultado puede ser válido en SQL y falso en negocio.

Errores SQL que cambian la historia

Para un ingeniero, el peligro no está solo en que el modelo genere SQL inválido. Ese fallo es fácil de detectar. El peligro serio es que genere SQL válido, rápido y aparentemente razonable, pero calcule otra cosa.

ErrorSQL que suele aparecerQué rompeCómo pensarlo
Duplicar filas con un JOINUnir alumnos con varias filas de pagos.COUNT(*) sube porque un alumno aparece varias veces.Cuenta entidades con COUNT(DISTINCT alumno_id).
Confundir evento y entidadContar pagos como si fueran alumnos.Mide movimientos, no personas.Pregunta si cada fila es “cosa” o “suceso”.
Ignorar NULLAVG(importe) sin revisar ausentes.Algunos cálculos excluyen nulos sin avisar.Decide si nulo significa desconocido, cero o no aplica.
Filtrar tardeUsar HAVING cuando tocaba WHERE.Agrupa datos que no deberían entrar.WHERE filtra filas; HAVING filtra grupos.
Fechas mal cerradasfecha <= '2026-03-31'.Puede perder horas del último día.Usa rangos semiabiertos: >= inicio y < fin.
Moneda mezcladaSUM(importe) sin moneda.Suma euros, dólares o créditos como si fueran iguales.Agrupa o convierte antes de sumar.
Estado de negocio incompletoestado != 'pagado'.Incluye anulados, devueltos o pruebas.Define catálogo permitido, no solo excluido.

Miremos el fallo de duplicación, que aparece mucho en sistemas Text-to-SQL. Si una tabla alumnos tiene una fila por alumno y pagos tiene varias filas por alumno, este SQL cuenta pagos, no alumnos:

SELECT COUNT(*) AS alumnos_con_pago
FROM alumnos AS a
JOIN pagos AS p ON p.alumno_id = a.alumno_id
WHERE p.estado = 'pendiente';

Si un alumno tiene tres pagos pendientes, aparece tres veces. La consulta puede ejecutarse sin quejarse y devolver un número bonito, pero el significado es otro. Para contar alumnos únicos:

SELECT COUNT(DISTINCT a.alumno_id) AS alumnos_con_pago_pendiente
FROM alumnos AS a
JOIN pagos AS p ON p.alumno_id = a.alumno_id
WHERE p.estado = 'pendiente';

La diferencia entre COUNT(*) y COUNT(DISTINCT ...) no es un detalle académico. Es la diferencia entre contar filas y contar entidades. Cuando una persona pregunta “cuántos alumnos”, normalmente quiere entidades. Cuando pregunta “cuántos pagos”, quiere eventos.

También importan las fechas. Si fecha incluye hora, esta condición parece correcta pero puede dejar fuera movimientos del 31 de marzo por la tarde:

WHERE fecha >= '2026-03-01'
  AND fecha <= '2026-03-31'

El patrón más robusto para intervalos suele ser:

WHERE fecha >= '2026-03-01'
  AND fecha < '2026-04-01'

Un buen sistema Text-to-SQL no solo valida sintaxis. También mira estas trampas: cardinalidad de las tablas, tipo de métrica, nulos, moneda, rango temporal, catálogo de estados y claves de unión.

El mecanismo paso a paso

Text-to-SQL funciona bien cuando se separan tareas. Un modelo que recibe “todas las tablas, toda la documentación y genera SQL” puede acertar en una demo pequeña, pero se vuelve frágil con esquemas grandes.

Text-to-SQL no es una query, es una cadena de control El modelo propone, pero el sistema selecciona esquema, valida, limita, ejecuta y registra. Pregunta intención humana Clasificador dominio y permisos Selector de esquema tablas, columnas, claves Capa semántica métricas y reglas Generador SQL consulta candidata Parser sintaxis y dialecto AST de la consulta Validador solo lectura, límites tablas permitidas Dry-run / explain coste, columnas, plan sin traer todo Ejecución limitada timeout, filas, coste read-only replica Tabla resultado filas, columnas, tipos muestra o agregado Explicación qué se calculó sin inventar datos Visualización opcional y declarada tipo de gráfico Traza SQL, tiempo usuario, versión Evaluación offline pregunta, SQL esperado, filas Observabilidad latencia, coste, fallos, drift Gobierno de datos permisos, retención, versiones La pregunta entra en lenguaje natural; la respuesta sale con datos, límites y trazabilidad. IA para gente curiosa / Facsímil 04 / Capítulo 12 / 686f6c61

Un sistema así no depende de una única llamada al modelo. Depende de contratos entre piezas. La persona pregunta; el clasificador decide si la pregunta es de datos o de documentos; el selector reduce el esquema; la capa semántica aporta definiciones; el generador propone SQL; el validador inspecciona; la base ejecuta con límites; la respuesta explica y registra.

El problema real: esquema, semántica y valores

Cuando un humano experto escribe SQL, no solo recuerda sintaxis. Recuerda el significado de cada tabla, qué columnas son fiables, qué claves se unen, qué estados se excluyen, qué fechas mandan y qué métricas no se calculan directamente.

Text-to-SQL falla por tres razones principales:

FocoQué fallaEjemplo
EsquemaEl modelo elige tabla o columna incorrecta.Usa created_at en vez de paid_at.
SemánticaLa consulta no respeta definición de negocio.Cuenta alumnos anulados como activos.
ValoresNo sabe cómo están escritos los datos reales.Busca pendiente pero la tabla usa PENDING.

El schema linking conecta pregunta y esquema. Si la pregunta dice “campus con pagos pendientes”, el sistema debe vincular:

Palabra de la preguntaCandidato en datosPor qué
campusalumnos.campus o pagos.campusDimensión de agrupación.
pagostabla pagosHecho económico.
pendientespagos.estado = 'pendiente'Filtro.
tresLIMIT 3Tamaño de salida.
másORDER BY total DESCOrden descendente.

En bases pequeñas se puede meter todo el esquema en el prompt. En bases grandes no. Hay que recuperar el esquema como se recuperan documentos: por dominio, por nombres, por descripciones, por consultas de ejemplo y por permisos.

La capa semántica como contrato

La capa semántica es el lugar donde una organización deja de discutir cada vez qué significa una métrica. No es una frase bonita en un prompt. Es un contrato versionado entre negocio, ingeniería y análisis.

Una definición mínima de métrica debería responder:

PreguntaEjemplo para ingreso_neto
¿Qué entidad mide?Pagos confirmados.
¿Qué columna numérica usa?pagos.importe.
¿Qué estados entran?Solo pagado.
¿Qué estados salen?pendiente, anulado, devuelto, prueba.
¿Qué fecha manda?fecha_pago, no fecha_creacion.
¿Qué dimensiones permite?campus, titulacion, mes.
¿Quién puede verla?Roles de análisis y dirección académica.
¿Cómo se prueba?Casos con resultado esperado.

Esa misma definición puede expresarse de muchas formas: YAML, dbt metrics, una tabla de metadatos, una API interna o una vista SQL. Lo importante es que el modelo no invente la definición cada vez.

metric: ingreso_neto
entity: pagos
expression: SUM(importe)
filters:
  - estado = 'pagado'
time_dimension: fecha_pago
allowed_dimensions:
  - campus
  - titulacion
  - mes
blocked_columns:
  - dni
  - email_personal
owner: equipo_datos_matricula

Con esa capa, una pregunta como “ingresos por campus en marzo” no empieza desde cero. El sistema sabe que “ingresos” apunta a ingreso_neto, que marzo debe filtrarse con fecha_pago, que el grupo permitido es campus y que algunas columnas ni siquiera deben entrar en el contexto.

Para una persona curiosa, la idea es esta: una base de datos guarda datos, pero no siempre guarda significado. La capa semántica añade significado compartido. Para un ingeniero, añade algo igual de importante: reduce libertad donde la libertad produce errores.

Herramientas de datos: no todo es Text-to-SQL

Text-to-SQL es una herramienta, pero no la única. A veces conviene no dejar que el modelo genere SQL libre, sino exponer operaciones más estrechas.

Tipo de herramientaQué haceCuándo usarlaEjemplo
Text-to-SQL libreGenera consultas nuevas.Análisis exploratorio con validación fuerte.“Agrupa pagos por campus y mes”.
Plantilla parametrizadaRellena parámetros de una consulta fija.Métricas críticas y repetibles.campus, fecha_inicio, fecha_fin.
Stored procedureLlama a una función definida en la base.Regla compleja y estable.calcular_morosidad(campus, mes).
Semantic layerConsulta métricas y dimensiones declaradas.BI, reporting y definiciones de negocio.ingreso_neto por campus.
DataFrame toolOpera sobre tablas en memoria.Exploración local, CSV, notebooks.Pandas, Polars, DuckDB.
Chart toolConvierte resultados en gráfico.Cuando la salida natural es visual.Barras por campus.
Data quality toolComprueba nulos, duplicados o rangos.Antes de confiar en una respuesta.“¿Hay importes negativos?”.

La pregunta de arquitectura no es “¿puedo generar SQL?”. La pregunta buena es “¿qué superficie de datos quiero exponer?”. Cuanto más abierta sea la herramienta, más validación necesita.

Contrato de una herramienta SQL

Una herramienta SQL no debería aceptar una cadena cualquiera. Debería tener un contrato que obligue a declarar intención, dominio, límites y formato de respuesta.

{
  "name": "consultar_datos",
  "input": {
    "question": "Pagos pendientes por campus en marzo",
    "domain": "matricula",
    "sql": "SELECT campus, SUM(importe) AS total FROM pagos ...",
    "dialect": "sqlite",
    "max_rows": 50,
    "timeout_ms": 1500,
    "purpose": "analisis_agregado"
  },
  "output": {
    "columns": ["campus", "total"],
    "rows": [["Norte", 19320.0]],
    "row_count": 1,
    "elapsed_ms": 24,
    "trace_id": "f4c12-001"
  },
  "errors": [
    "tabla_no_permitida",
    "consulta_no_select",
    "demasiadas_filas",
    "timeout",
    "sql_invalido"
  ]
}

Cada campo tiene una razón:

CampoQué controlaPor qué importa
domainÁrea funcional.Evita mezclar tablas sin contexto.
sqlConsulta candidata.Debe poder validarse y registrarse.
dialectMotor SQL esperado.LIMIT, fechas y funciones cambian entre motores.
max_rowsFilas máximas.Protege coste y evita respuestas inmanejables.
timeout_msTiempo máximo.Una mala consulta no debe bloquear el sistema.
purposeUso declarado.No es igual explorar que cerrar un informe.
trace_idIdentificador de ejecución.Permite reproducir y auditar.

La herramienta puede estar detrás de OpenAI Function Calling, de un agente de LangChain, de LlamaIndex, de una API propia o de un servicio interno. El principio no cambia: el modelo propone argumentos estructurados, pero el servidor valida y ejecuta.

Permisos, datos sensibles y trazas

Una herramienta de datos tiene tres identidades distintas y conviene no mezclarlas:

IdentidadQué representaError típico
Persona usuariaQuien hace la pregunta.Darle acceso por el rol técnico del servidor.
ModeloQuien propone la consulta.Tratar su SQL como decisión autorizada.
Servicio de datosQuien ejecuta.Conectar con permisos demasiado amplios.

El modelo no debería tener permisos. Quien tiene permisos es el servicio que recibe una petición, revisa el rol de la persona, aplica reglas y ejecuta una consulta limitada. Esa separación evita que una respuesta dependa de una frase del prompt.

En una arquitectura seria, pondría estos controles:

ControlQué protegeEjemplo concreto
Conexión read-onlyEvita modificar datos.Usuario SQL sin INSERT, UPDATE, DELETE ni DDL.
Réplica de lecturaAísla producción.Consultar una réplica o almacén analítico.
Row-level securityLimita filas por rol.Un campus solo ve sus alumnos.
Column allowlistLimita columnas visibles.Exponer campus, no dni ni email_personal.
Query timeoutEvita consultas largas.Cortar a 1.500 ms en exploración.
Row limitControla volumen de salida.Máximo 100 filas por respuesta.
Redacción de logsEvita guardar datos innecesarios.Registrar SQL y hash de usuario, no tabla completa.
TrazabilidadPermite reproducir.trace_id, versión de esquema, modelo y validador.

La traza no es vigilancia decorativa. Es el expediente técnico de la respuesta. Si alguien pregunta “¿de dónde salió este número?”, necesitas poder reconstruir pregunta, usuario, rol, tablas candidatas, SQL generado, reglas aplicadas, resultado, latencia y versión del sistema.

Validación antes de ejecutar

La validación no es un adorno. Es el corazón del sistema. Un validador mínimo debería revisar:

ControlQué compruebaEjemplo de regla
Tipo de consultaSolo lectura.Aceptar únicamente SELECT o WITH ... SELECT.
Tablas permitidasSuperficie acotada.pagos, alumnos, campus.
Columnas permitidasEvitar columnas fuera de contrato.No exponer dni si no hace falta.
Límite de filasSalida manejable.Añadir o exigir LIMIT 100.
TimeoutCoste temporal.Cortar a 1,5 segundos.
DialectoSintaxis correcta.SQLite no es BigQuery.
AgregaciónPreguntas agregadas devuelven agregados.SUM, COUNT, AVG.
Filtros de usuarioPermisos por rol.Campus permitido por sesión.
ExplicaciónRespuesta basada en resultado.No resumir columnas que no salieron.

Herramientas como SQLGlot ayudan a parsear, inspeccionar y transpilar SQL entre dialectos.9 Motores embebidos como DuckDB son útiles para análisis local, CSV, Parquet o notebooks, porque permiten ejecutar SQL desde Python sin levantar un servidor externo.10

Query plan: cuando una consulta correcta no cabe

Una consulta puede ser correcta y aun así no ser aceptable. Si tarda treinta segundos, bloquea recursos o escanea una tabla enorme para devolver tres filas, el problema no es solo del modelo: es del sistema que no miró el plan.

El query plan es la explicación interna que calcula el motor antes de ejecutar. Según el motor puede verse con EXPLAIN, EXPLAIN ANALYZE, perfiles de consulta o dry-run. Los nombres cambian, pero la pregunta es la misma: ¿qué tendrá que hacer la base para responder?

EXPLAIN
SELECT campus, SUM(importe) AS total_pendiente
FROM pagos
WHERE estado = 'pendiente'
GROUP BY campus
ORDER BY total_pendiente DESC
LIMIT 3;

Al leer un plan, no hace falta entender todo desde el primer día. Empieza por estas señales:

SeñalQué significaPor qué importa
Full scanRecorrer muchas filas de una tabla.Puede ser normal en tablas pequeñas, caro en tablas grandes.
Index scanLocalizar filas con un índice.Suele ayudar si el filtro es selectivo.
Cardinalidad estimadaFilas que el motor cree que pasarán.Si estima mal, elige planes pobres.
Join strategyForma de unir tablas.Nested loop, hash join o merge join tienen costes distintos.
SortOrdenación intermedia.ORDER BY sobre muchas filas puede ser caro.
Temporary spillDatos intermedios fuera de memoria.Señal de presión de memoria o consulta pesada.

Un índice no resuelve todo. Acelera ciertas búsquedas a cambio de ocupar espacio y complicar escrituras. Si filtras mucho por estado y fecha, un índice puede ayudar:

CREATE INDEX idx_pagos_estado_fecha
ON pagos (estado, fecha);

Pero si casi todos los pagos están en estado = 'pendiente', ese índice quizá no aporta mucho, porque el filtro no reduce bastante. Esta idea se llama selectividad: un filtro útil descarta muchas filas.

Para Text-to-SQL, el plan sirve como control previo:

Pregunta técnicaDecisión del sistema
¿Escanea más filas de las permitidas?Pedir aclaración, añadir filtro o bloquear.
¿Usa tablas fuera del dominio?Rechazar y pedir reformulación.
¿Ordena millones de filas sin agregación previa?Proponer una consulta agregada.
¿No hay índice para el filtro principal?Avisar de latencia o derivar a informe offline.

Dialecto SQL: el mismo pedido cambia por motor

SQL tiene una gramática común, pero cada motor añade funciones, tipos y límites. Un sistema que genera SQL debe saber para qué motor escribe. SQLite no es PostgreSQL, PostgreSQL no es BigQuery, BigQuery no es Snowflake.

NecesidadSQLitePostgreSQLBigQuerySnowflake
Limitar filasLIMIT 10LIMIT 10LIMIT 10LIMIT 10
Mes de una fechastrftime('%Y-%m', fecha)date_trunc('month', fecha)DATE_TRUNC(fecha, MONTH)DATE_TRUNC('MONTH', fecha)
Concatenar texto`ab``a
Fecha actualdate('now')CURRENT_DATECURRENT_DATE()CURRENT_DATE()
Muestra aproximadaLimitadoTABLESAMPLETABLESAMPLE SYSTEMSAMPLE

No hace falta memorizar todos los dialectos. Lo importante es no mezclar. Si el contrato dice dialect: "sqlite", el generador, el parser, los ejemplos y el validador deben hablar SQLite. Si el almacén real es BigQuery, conviene validar con BigQuery o con un parser que entienda sus particularidades.

Esta es una razón más para no pegar ejemplos al azar en el prompt. Un ejemplo de PostgreSQL puede enseñar al modelo una función que luego falla en BigQuery. Los ejemplos son datos de entrenamiento local para la consulta que estás a punto de generar; si están mal elegidos, orientan mal.

Cómo elegir arquitectura

La solución correcta depende del riesgo, del tamaño del esquema y de lo repetible que sea la pregunta.

SituaciónArquitectura recomendadaPor qué
Métrica crítica y estable.Plantilla parametrizada.Menos libertad, más confianza.
Exploración interna con datos agregados.Text-to-SQL con validación y trazas.Permite flexibilidad controlada.
BI con definiciones compartidas.Semantic layer + modelo.Las métricas viven fuera del prompt.
CSV local o notebook.DuckDB/DataFrame tool.Iteración rápida y entorno cerrado.
Esquema enorme.Retrieval de esquema + examples RAG.No cabe todo el catálogo.
Varias bases y documentos.Router + herramientas especializadas.No todo debe ir por SQL.
Preguntas repetidas de negocio.Stored procedures o vistas.La lógica queda versionada.

Mi regla práctica: si la pregunta puede romper un informe importante, no empieces con SQL libre. Empieza con métrica declarada, plantilla o vista. Usa Text-to-SQL libre para exploración, no para convertir cada pregunta de negocio en una consulta nueva sin revisión.

Coste, latencia y contexto

Text-to-SQL parece barato porque la salida es corta. Pero el contexto puede crecer muchísimo: documentación de tablas, columnas, ejemplos, métricas, permisos, dialecto y trazas anteriores.

Ejemplo de fórmula. Podemos aproximar el contexto así:

Tctx=Tpregunta+Tschema+Tdocs+Tejemplos+TpoliticasT_{\text{ctx}} = T_{\text{pregunta}} + T_{\text{schema}} + T_{\text{docs}} + T_{\text{ejemplos}} + T_{\text{politicas}}
SímboloSignificadoEjemplo
TctxT_{\text{ctx}}Tokens totales de contexto.6.400 tokens.
TpreguntaT_{\text{pregunta}}Pregunta del usuario.30 tokens.
TschemaT_{\text{schema}}Tablas, columnas y claves incluidas.2.500 tokens.
TdocsT_{\text{docs}}Documentación de negocio.1.200 tokens.
TejemplosT_{\text{ejemplos}}Consultas parecidas.1.800 tokens.
TpoliticasT_{\text{politicas}}Permisos y reglas.870 tokens.

Ejemplo de fórmula. La latencia total no es solo generación:

Ltotal=Lrouter+Lschema+LLLM+Lvalidacion+Ldb+LresumenL_{\text{total}} = L_{\text{router}} + L_{\text{schema}} + L_{\text{LLM}} + L_{\text{validacion}} + L_{\text{db}} + L_{\text{resumen}}
SímboloSignificadoEjemplo
LschemaL_{\text{schema}}Tiempo de recuperar esquema relevante.80 ms.
LLLML_{\text{LLM}}Tiempo de generar SQL o plan.1.200 ms.
LvalidacionL_{\text{validacion}}Parser, reglas y dry-run.90 ms.
LdbL_{\text{db}}Ejecución en base de datos.300 ms.
LresumenL_{\text{resumen}}Redacción final.700 ms.

Ejemplo de fórmula. Y el coste esperado por pregunta:

C=Cmodelo+Cdb+Cobservabilidad+CmantenimientoC = C_{\text{modelo}} + C_{\text{db}} + C_{\text{observabilidad}} + C_{\text{mantenimiento}}

La parte invisible suele ser CmantenimientoC_{\text{mantenimiento}}: documentar columnas, versionar métricas, revisar consultas fallidas, actualizar ejemplos y controlar cambios de esquema.

Evaluar Text-to-SQL

La evaluación clásica compara SQL generado contra SQL esperado. Eso ayuda, pero no basta. Dos consultas distintas pueden devolver el mismo resultado; dos consultas parecidas pueden divergir en casos frontera.

MétricaQué mideCuidado
Exact matchSi el SQL coincide con el esperado.Penaliza consultas equivalentes escritas distinto.
Execution accuracySi produce el resultado correcto.Puede acertar por casualidad en datos pequeños.
Result-set matchSi filas y columnas coinciden.Hay que controlar orden, tipos y redondeo.
Component matchSi SELECT, WHERE, JOIN, GROUP BY están bien.Útil para depurar.
Schema-link accuracySi eligió tablas y columnas correctas.Necesita anotación o revisión.
Permission pass rateSi respeta permisos y límites.No basta medir precisión.
Query costCoste estimado o real de ejecución.Una query correcta puede ser inviable.
Latencia P95Tiempo para el 95% de consultas.La media oculta colas lentas.
Clarification rateCuándo pide aclaración.Preguntar puede ser mejor que inventar.

Un dataset propio debería tener:

CampoEjemplo
question“Pagos pendientes por campus en marzo”.
user_roleanalista_matricula.
allowed_tablespagos, alumnos, campus.
gold_sqlConsulta esperada o plantilla.
expected_resultFilas esperadas.
must_not_useColumnas que no proceden.
notesDefinición de negocio relevante.

En producción mediría, como mínimo:

CapaPregunta de evaluación
Clasificador¿Detectó que era una pregunta de datos?
Selector de esquema¿Incluyó las tablas necesarias y excluyó ruido?
Generador¿Produjo SQL válido para el dialecto?
Validador¿Bloqueó lo que debía bloquear?
Ejecución¿Devolvió resultado correcto dentro de límite?
Resumen¿Explicó solo lo que aparece en la tabla?
Trazas¿Puedo reproducir la respuesta?

Una forma práctica de empezar es construir un pequeño harness de evaluación. No tiene que ser perfecto. Tiene que hacer visible cuándo el sistema mejora o empeora.

{
  "id": "matricula-001",
  "question": "Alumnos con pago pendiente por campus en marzo",
  "role": "analista_matricula",
  "dialect": "sqlite",
  "allowed_tables": ["pagos", "alumnos"],
  "expected_sql_patterns": ["GROUP BY campus", "estado = 'pendiente'"],
  "expected_result": [
    {"campus": "Norte", "alumnos": 2},
    {"campus": "Centro", "alumnos": 1}
  ],
  "max_latency_ms": 1500,
  "max_rows": 20,
  "must_not_use": ["dni", "email_personal"],
  "review_note": "Debe contar alumnos únicos, no pagos."
}

En ese ejemplo no basta con que aparezca GROUP BY campus. El caso dice explícitamente que hay que contar alumnos únicos. Esa nota evita que una consulta con COUNT(*) pase por casualidad cuando los datos de prueba son pequeños.

Un harness útil debería guardar cuatro salidas:

SalidaPara qué sirve
SQL generadoRevisar estructura y dialecto.
Resultado devueltoComparar con la tabla esperada.
Razón de validaciónSaber si el sistema aceptó o bloqueó con criterio.
TrazaReproducir el caso con misma versión de esquema y modelo.

Y debería separar tipos de error. No es igual fallar por sintaxis que fallar por semántica:

Tipo de falloEjemploQué arreglar
SintaxisFunción inexistente para el dialecto.Ejemplos y parser.
Schema linkingUsa created_at en lugar de paid_at.Descripciones y selector de esquema.
SemánticaCuenta pagos cuando debía contar alumnos.Capa semántica y casos de prueba.
PermisosIncluye columna no permitida.Validador y allowlist.
CosteQuery correcta pero pesada.Plan, índices, límites o vista agregada.
ExplicaciónResume algo que no está en el resultado.Contrato de respuesta y groundedness.

Cuando conviene pedir aclaración

Una buena herramienta de datos no responde siempre. A veces pregunta.

Pregunta originalQué faltaMejor respuesta del sistema
“Ventas del mes”.Mes, definición de venta, región.“¿Qué mes y qué métrica de ventas quieres usar?”
“Alumnos activos”.Definición de activo.“Puedo usar matrícula vigente o acceso reciente. ¿Cuál prefieres?”
“Top clientes”.Top por ingresos, pedidos o margen.“¿Quieres ordenar por ingresos, margen o número de pedidos?”
“Comparar campus”.Periodo y métrica.“Dime periodo y métrica principal.”

Pedir aclaración no es fallar. En datos, muchas respuestas incorrectas nacen de contestar demasiado rápido.

Caso completo: de pregunta a respuesta trazable

Tomemos una pregunta realista:

“Dame los tres campus con más alumnos con pagos pendientes en marzo.”

Parece una pregunta simple. No lo es. Hay una entidad, alumnos; un evento, pagos; una condición, pendientes; una ventana temporal, marzo; una agrupación, campus; un ranking, los tres primeros.

El sistema debería recorrer algo parecido a esto:

PasoDecisiónResultado intermedio
1. IntenciónEs una pregunta de datos agregados.Ruta a herramienta SQL.
2. DominioMatrícula y pagos.No consulta documentos generales.
3. UsuarioRol analista_matricula.Puede ver agregados por campus.
4. EsquemaNecesita pagos y quizá alumnos.No carga todo el catálogo.
5. Semántica“Alumnos con pagos pendientes” cuenta alumnos únicos.Métrica: COUNT(DISTINCT alumno_id).
6. FechaMarzo se interpreta como rango semiabierto.>= '2026-03-01' y < '2026-04-01'.
7. SQL candidatoGenera consulta agregada.GROUP BY campus, ORDER BY, LIMIT 3.
8. ValidaciónSolo lectura, tablas permitidas, límite.Acepta o pide corrección.
9. PlanEstima filas y coste.Bloquea si escanea demasiado.
10. EjecuciónEjecuta en réplica de lectura.Devuelve tabla pequeña.
11. RespuestaExplica solo la tabla.No inventa causas.
12. TrazaRegistra expediente.Permite reproducir.

Un SQL razonable podría ser:

SELECT
  p.campus,
  COUNT(DISTINCT p.alumno_id) AS alumnos_con_pago_pendiente
FROM pagos AS p
WHERE p.estado = 'pendiente'
  AND p.fecha >= '2026-03-01'
  AND p.fecha < '2026-04-01'
GROUP BY p.campus
ORDER BY alumnos_con_pago_pendiente DESC
LIMIT 3;

Fíjate en lo que no hace:

No hacePor qué
No usa COUNT(*).Contaría pagos, no alumnos.
No usa estado != 'pagado'.Metería estados no definidos.
No pide columnas personales.La pregunta solo necesita agregados.
No devuelve filas individuales.La salida pedida es un ranking.
No explica causas.La consulta no investigó causas, solo conteos.

La respuesta humana debería sonar así:

“Con la definición de pagos pendientes como estado = 'pendiente' y tomando marzo como [2026-03-01, 2026-04-01), los tres campus con más alumnos únicos con pagos pendientes son Norte, Centro y Sur. La consulta usa datos agregados y no incluye información personal.”

Y la traza técnica podría guardar:

{
  "trace_id": "f4c12-demo-023",
  "question": "Dame los tres campus con más alumnos con pagos pendientes en marzo",
  "route": "sql_tool",
  "role": "analista_matricula",
  "dialect": "sqlite",
  "tables": ["pagos"],
  "metric": "count_distinct_alumno_id",
  "validated": true,
  "checks": ["read_only", "allowed_tables", "row_limit", "date_range"],
  "row_count": 3,
  "elapsed_ms": 31
}

Esta traza no es para enseñarla entera a la persona que pregunta. Es para que el equipo pueda auditar, depurar y mejorar el sistema.

Soluciones de terceros y piezas habituales

Hay herramientas ya hechas, pero conviene saber qué problema resuelve cada una. No todas sustituyen una arquitectura propia.

PiezaQué aportaQué revisaría antes de usarla
LangChain SQL AgentFlujo agentic para inspeccionar esquema, generar, revisar y ejecutar SQL.Permisos de conexión, trazas, límites y revisión humana.
LlamaIndex NL SQLQuery engines para lenguaje natural sobre tablas SQL.Qué esquema entra, cómo controla ejecución y cómo registra fuentes.
VannaEnfoque natural language -> SQL -> respuestas con permisos y componentes de UI.El repositorio público aparece archivado desde marzo de 2026; revisaría mantenimiento y versión usada.11
SQLGlotParser, AST y transpiler SQL.Cobertura del dialecto y reglas propias de validación.
DuckDBMotor local para análisis, CSV y Parquet desde Python.Memoria, tamaño de datos y diferencias frente al motor de producción.
dbt / capa semánticaMétricas y transformaciones versionadas.Quién mantiene definiciones y cómo se exponen al modelo.
BI tradicionalDashboards y métricas curadas.Qué preguntas quedan fuera del dashboard.

Una buena arquitectura puede mezclar varias: LangChain o LlamaIndex para orquestar, SQLGlot para validar, DuckDB para prototipos locales, una capa semántica para métricas y una API propia para permisos.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a construir una mini herramienta Text-to-SQL sin llamar a ningún modelo. Simularemos la parte que haría el modelo con un diccionario de preguntas, pero construiremos lo importante: base de datos, SQL candidato, validación, ejecución limitada y traza.

import re
import sqlite3
from pprint import pprint


SCHEMA = {
    "pagos": {
        "columns": {
            "pago_id",
            "alumno_id",
            "campus",
            "estado",
            "importe",
            "fecha",
        }
    },
    "alumnos": {
        "columns": {
            "alumno_id",
            "titulacion",
            "campus",
            "estado_matricula",
        }
    },
}

ALLOWED_TABLES = set(SCHEMA)
MAX_ROWS = 20


def crear_bd():
    con = sqlite3.connect(":memory:")
    con.execute("""
        CREATE TABLE alumnos (
            alumno_id INTEGER PRIMARY KEY,
            titulacion TEXT,
            campus TEXT,
            estado_matricula TEXT
        )
    """)
    con.execute("""
        CREATE TABLE pagos (
            pago_id INTEGER PRIMARY KEY,
            alumno_id INTEGER,
            campus TEXT,
            estado TEXT,
            importe REAL,
            fecha TEXT
        )
    """)
    con.executemany(
        "INSERT INTO alumnos VALUES (?, ?, ?, ?)",
        [
            (1, "Informática", "Norte", "vigente"),
            (2, "Matemáticas", "Sur", "vigente"),
            (3, "Informática", "Norte", "vigente"),
            (4, "Diseño", "Centro", "pausada"),
        ],
    )
    con.executemany(
        "INSERT INTO pagos VALUES (?, ?, ?, ?, ?, ?)",
        [
            (101, 1, "Norte", "pendiente", 420.0, "2026-03-10"),
            (102, 2, "Sur", "pagado", 300.0, "2026-03-11"),
            (103, 3, "Norte", "pendiente", 380.0, "2026-03-14"),
            (104, 4, "Centro", "pendiente", 250.0, "2026-03-18"),
            (105, 2, "Sur", "pendiente", 120.0, "2026-04-02"),
        ],
    )
    return con


def simular_modelo(pregunta):
    texto = pregunta.lower()
    if "campus" in texto and "pendiente" in texto:
        return """
            SELECT campus, SUM(importe) AS total_pendiente
            FROM pagos
            WHERE estado = 'pendiente'
            GROUP BY campus
            ORDER BY total_pendiente DESC
            LIMIT 3
        """
    if "titulaci" in texto and "pendiente" in texto:
        return """
            SELECT a.titulacion, SUM(p.importe) AS total_pendiente
            FROM pagos AS p
            JOIN alumnos AS a ON a.alumno_id = p.alumno_id
            WHERE p.estado = 'pendiente'
            GROUP BY a.titulacion
            ORDER BY total_pendiente DESC
            LIMIT 5
        """
    return """
        SELECT estado, COUNT(*) AS pagos
        FROM pagos
        GROUP BY estado
        LIMIT 10
    """


def tablas_usadas(sql):
    patron = r"\b(?:FROM|JOIN)\s+([a-zA-Z_][a-zA-Z0-9_]*)"
    return {m.group(1).lower() for m in re.finditer(patron, sql, re.I)}


def validar_sql(sql):
    limpio = " ".join(sql.strip().split())
    errores = []

    if not re.match(r"^(SELECT|WITH)\b", limpio, re.I):
        errores.append("solo se permiten consultas de lectura")

    palabras_bloqueadas = {
        "insert", "update", "delete", "create", "alter", "drop",
        "truncate", "attach", "pragma",
    }
    usadas = set(re.findall(r"\b[a-zA-Z_]+\b", limpio.lower()))
    if usadas & palabras_bloqueadas:
        errores.append("aparece una operación fuera de contrato")

    desconocidas = tablas_usadas(limpio) - ALLOWED_TABLES
    if desconocidas:
        errores.append(f"tablas no permitidas: {sorted(desconocidas)}")

    if " limit " not in f" {limpio.lower()} ":
        limpio += f" LIMIT {MAX_ROWS}"

    return limpio, errores


def ejecutar(con, sql):
    sql, errores = validar_sql(sql)
    traza = {"sql": sql, "errores": errores}
    if errores:
        return {"ok": False, "traza": traza, "rows": []}

    plan = [
        fila[3]
        for fila in con.execute("EXPLAIN QUERY PLAN " + sql).fetchall()
    ]
    cur = con.execute(sql)
    columnas = [c[0] for c in cur.description]
    filas = cur.fetchmany(MAX_ROWS + 1)
    if len(filas) > MAX_ROWS:
        return {
            "ok": False,
            "traza": {**traza, "errores": ["demasiadas filas"]},
            "rows": [],
        }
    return {
        "ok": True,
        "traza": {
            **traza,
            "columnas": columnas,
            "row_count": len(filas),
            "query_plan": plan,
        },
        "rows": [dict(zip(columnas, fila)) for fila in filas],
    }


def responder(pregunta):
    con = crear_bd()
    sql = simular_modelo(pregunta)
    resultado = ejecutar(con, sql)
    return {
        "pregunta": pregunta,
        "sql_candidato": " ".join(sql.split()),
        "resultado": resultado,
    }


def evaluar_caso(caso):
    respuesta = responder(caso["question"])
    filas = respuesta["resultado"]["rows"]
    errores = []
    if filas != caso["expected_result"]:
        errores.append("resultado distinto al esperado")
    sql = respuesta["resultado"]["traza"]["sql"].lower()
    for patron in caso["expected_sql_contains"]:
        if patron.lower() not in sql:
            errores.append(f"falta patrón SQL: {patron}")
    return {
        "case_id": caso["id"],
        "pass": not errores,
        "errores": errores,
        "trace": respuesta["resultado"]["traza"],
    }


preguntas = [
    "Tres campus con más pagos pendientes",
    "Importe pendiente por titulación",
    "Cuántos pagos hay por estado",
]

casos_eval = [
    {
        "id": "matricula-001",
        "question": "Tres campus con más pagos pendientes",
        "expected_sql_contains": ["GROUP BY campus", "LIMIT 3"],
        "expected_result": [
            {"campus": "Norte", "total_pendiente": 800.0},
            {"campus": "Centro", "total_pendiente": 250.0},
            {"campus": "Sur", "total_pendiente": 120.0},
        ],
    }
]

for pregunta in preguntas:
    print("\\n---")
    pprint(responder(pregunta), width=88)

print("\\n--- evaluación")
pprint(evaluar_caso(casos_eval[0]), width=88)

Salida esperada aproximada:

---
pregunta: Tres campus con más pagos pendientes
rows:
  Norte  -> 800.0
  Centro -> 250.0
  Sur    -> 120.0

---
pregunta: Importe pendiente por titulación
rows:
  Informática -> 800.0
  Diseño      -> 250.0
  Matemáticas -> 120.0

---
evaluación:
  pass: True
  trace:
    row_count: 3
    query_plan:
      - SCAN pagos
      - USE TEMP B-TREE FOR GROUP BY

Prueba tres cambios:

  • Quita el LIMIT de una consulta y observa cómo el validador lo añade.
  • Cambia pagos por una tabla inexistente y comprueba el error.
  • Añade una columna sensible inventada y decide si debería estar en SCHEMA.
  • Cambia el resultado esperado del caso de evaluación y mira cómo falla el harness.

Cómo encaja todo

graph TD
    subgraph "Capítulo 12: datos y SQL"
        TSQL["Text-to-SQL"]
        INTENT["Intención de datos"]
        SCHEMA["Schema linking"]
        SEM["Capa semántica"]
        PERMS["Permisos y roles"]
        DIALECT["Dialecto SQL"]
        SQL["SQL candidato"]
        VALID["Validación"]
        PLAN["Query plan"]
        EXEC["Ejecución limitada"]
        RESULT["Resultado tabular"]
        TRACE["Traza"]
        EVAL["Evaluación SQL"]
    end
    subgraph "Viene de antes"
        API["Function calling (F4C2)"]
        STRUCT["Structured outputs (F4C2)"]
        VECTOR["Búsqueda híbrida (F4C8)"]
        RAG["RAG básico (F4C9)"]
        RAGEVAL["Evaluar RAG (F4C10)"]
        AGENTIC["Agentic RAG (F4C11)"]
    end
    subgraph "Sigue después"
        LAB["Laboratorio mínimo (F4C13)"]
        RECAP["Recapitulación (F4C14)"]
        AGENTES["Agentes con herramientas (F5)"]
        OPERAR["Operación y observabilidad (F6)"]
        DATOS["Ciencia de datos (F8)"]
    end

    API -->|"declara herramienta"| TSQL
    STRUCT -->|"estructura argumentos"| TSQL
    VECTOR -->|"recupera esquema"| SCHEMA
    RAG -->|"recupera documentación"| SEM
    AGENTIC -->|"decide usar"| TSQL
    TSQL -->|"parte de"| INTENT
    INTENT -->|"necesita"| SCHEMA
    SCHEMA -->|"alimenta"| SQL
    SEM -->|"corrige significado"| SQL
    PERMS -->|"acota superficie"| VALID
    DIALECT -->|"marca sintaxis"| VALID
    SQL -->|"pasa por"| VALID
    VALID -->|"revisa coste"| PLAN
    PLAN -->|"autoriza"| EXEC
    EXEC -->|"devuelve"| RESULT
    RESULT -->|"se explica con"| TRACE
    TRACE -->|"alimenta"| EVAL
    RAGEVAL -->|"inspira métricas"| EVAL
    EVAL -->|"se practica en"| LAB
    TSQL -->|"se resume en"| RECAP
    TSQL -->|"será herramienta de"| AGENTES
    TRACE -->|"pasa a"| OPERAR
    RESULT -->|"conecta con"| DATOS

    style TSQL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style INTENT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SCHEMA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SEM fill:#F5F5F5,stroke:#000000,stroke-width:2
    style PERMS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style DIALECT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SQL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style VALID fill:#111111,stroke:#000000,stroke-width:2,color:#FFFFFF
    style PLAN fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EXEC fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RESULT fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TRACE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EVAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style API stroke-dasharray: 5 5
    style STRUCT stroke-dasharray: 5 5
    style VECTOR stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style RAGEVAL stroke-dasharray: 5 5
    style AGENTIC stroke-dasharray: 5 5
    style LAB stroke-dasharray: 5 5
    style RECAP stroke-dasharray: 5 5
    style AGENTES stroke-dasharray: 5 5
    style OPERAR stroke-dasharray: 5 5
    style DATOS stroke-dasharray: 5 5

Vocabulario aprendido

TérminoDefinición
Text-to-SQLTraducción controlada de una pregunta humana a una consulta SQL ejecutable.
SQLLenguaje declarativo para consultar y transformar datos relacionales.
TablaConjunto de filas y columnas.
FilaRegistro individual dentro de una tabla.
ColumnaAtributo de una tabla, como fecha, estado o importe.
Clave primariaColumna o conjunto de columnas que identifica una fila única.
Clave foráneaColumna que conecta una tabla con otra.
JOINUnión entre tablas mediante una relación, normalmente una clave.
CardinalidadNúmero aproximado de filas que participan en una operación.
ÍndiceEstructura que ayuda a encontrar filas sin recorrer toda la tabla.
Schema linkingVincular palabras de la pregunta con tablas, columnas, claves y valores.
Dialecto SQLVariante de SQL de un motor concreto.
Semantic layerCapa de métricas y reglas de negocio compartidas.
Dry-runComprobación previa de una consulta antes de ejecutarla plenamente.
Query planPlan interno de ejecución calculado por la base de datos.
Read-onlyConexión que solo permite lectura.
Row-level securityRegla que limita qué filas puede ver cada rol o persona.
Execution accuracyMétrica que evalúa si el resultado producido por la consulta es correcto.
Result-set matchComparación entre resultado esperado y resultado devuelto.
Row limitLímite máximo de filas que puede devolver una consulta.
Traza SQLRegistro de pregunta, SQL, usuario, tablas, tiempo, resultado y errores.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Confundir SQL válido con respuesta correctaUna consulta puede ejecutar y aun así calcular otra cosa.Evaluar resultado, tablas usadas y definición de negocio.
Meter todo el esquema en el promptCon esquemas grandes sube el coste y entra ruido.Recuperar solo tablas, columnas y ejemplos relevantes.
Olvidar la capa semántica“Ingresos”, “activo” o “pendiente” no significan lo mismo en cada empresa.Declarar métricas y definiciones fuera del prompt.
Contar filas cuando quería entidadesCOUNT(*) puede contar pagos, no alumnos.Revisar cardinalidad y usar COUNT(DISTINCT ...) cuando proceda.
Ignorar el plan de consultaUna query correcta puede ser demasiado cara.Revisar EXPLAIN, cardinalidad, índices y límites.
Mezclar dialectosUna función válida en un motor puede fallar en otro.Pasar dialect en el contrato y validar contra ese motor.
Ejecutar antes de validarLa base de datos no sabe qué pretendía el usuario.Parsear, limitar, revisar permisos y hacer dry-run.
Medir solo exact matchSQL distinto puede producir el mismo resultado correcto.Combinar execution accuracy, result-set match y revisión por componentes.
No registrar trazasNo puedes explicar por qué salió un número.Guardar pregunta, esquema, SQL, resultados, latencia y versión.

Antes de pasar página

  • ¿Puedo explicar por qué Text-to-SQL no es acceso libre a una base de datos?
  • ¿Sé leer una consulta con SELECT, FROM, WHERE, JOIN, GROUP BY, ORDER BY y LIMIT?
  • ¿Puedo explicar qué es schema linking con un ejemplo?
  • ¿Sé distinguir entidad, evento, clave primaria y clave foránea?
  • ¿Puedo detectar cuándo un JOIN duplica filas?
  • ¿Sé por qué COUNT(*) y COUNT(DISTINCT ...) no responden a lo mismo?
  • ¿Sé distinguir SQL libre, plantilla parametrizada, stored procedure y semantic layer?
  • ¿Puedo diseñar un contrato mínimo para una herramienta SQL?
  • ¿Sé qué validaciones haría antes de ejecutar una consulta?
  • ¿Sé leer las señales básicas de un query plan?
  • ¿Sé por qué el dialecto SQL debe viajar en el contrato?
  • ¿Puedo calcular por qué el contexto crece con esquema, documentación y ejemplos?
  • ¿Sé diferenciar exact match, execution accuracy y result-set match?
  • ¿He ejecutado el ejemplo y revisado la traza?

En resumen

Idea fuerzaDetalle
Text-to-SQL no es una query suelta.Es una cadena con intención, esquema, SQL, validación, ejecución, resultado y traza.
El esquema manda.Si el sistema no entiende tablas, columnas, claves y valores, generará consultas frágiles.
La semántica evita números falsos.Las métricas de negocio deben estar definidas fuera del prompt.
La cardinalidad cambia respuestas.Contar filas, eventos o entidades no es lo mismo.
El plan importa.Una consulta correcta puede ser demasiado cara para ejecutarse en vivo.
La validación es obligatoria.Solo lectura, límites, permisos, dialecto, timeout y trazas antes de ejecutar.
La evaluación mira resultados.Exact match ayuda, pero execution accuracy y result-set match son centrales.

Para saber más

DuckDB. (2026). Python API. Documentación oficial

LangChain. (2026). Build a SQL agent. Documentación oficial

Lei, F. et al. (2024). Spider 2.0: Evaluating Language Models on Real-World Enterprise Text-to-SQL Workflows. arXiv

Li, J. et al. (2023). Can LLM Already Serve as A Database Interface? A BIg Bench for Large-Scale Database Grounded Text-to-SQLs. arXiv

LlamaIndex. (2026). NL SQL table query engine. Documentación oficial

OpenAI. (2026). Function calling. Documentación oficial

OpenAI. (2026). Structured model outputs. Documentación oficial

Scholak, T., Schucher, N. y Bahdanau, D. (2021). PICARD: Parsing Incrementally for Constrained Auto-Regressive Decoding from Language Models. Proceedings of EMNLP, 9895-9901. DOI

SQLGlot. (2026). Python SQL parser and transpiler. Documentación

Vanna AI. (2026). Vanna 2.0: Turn Questions into Data Insights. GitHub

Wang, B., Shin, R., Liu, X., Polozov, O. y Richardson, M. (2020). RAT-SQL: Relation-Aware Schema Encoding and Linking for Text-to-SQL Parsers. Proceedings of ACL, 7567-7578. DOI

Yu, T. et al. (2018). Spider: A Large-Scale Human-Labeled Dataset for Complex and Cross-Domain Semantic Parsing and Text-to-SQL Task. Proceedings of EMNLP, 3911-3921. ACL Anthology

Notas

  1. Yu, T. et al. (2018). Spider: A Large-Scale Human-Labeled Dataset for Complex and Cross-Domain Semantic Parsing and Text-to-SQL Task. Proceedings of EMNLP, 3911-3921. ACL Anthology.

  2. Wang, B., Shin, R., Liu, X., Polozov, O. y Richardson, M. (2020). RAT-SQL: Relation-Aware Schema Encoding and Linking for Text-to-SQL Parsers. Proceedings of ACL, 7567-7578. DOI.

  3. Scholak, T., Schucher, N. y Bahdanau, D. (2021). PICARD: Parsing Incrementally for Constrained Auto-Regressive Decoding from Language Models. Proceedings of EMNLP, 9895-9901. DOI.

  4. Li, J. et al. (2023). Can LLM Already Serve as A Database Interface? A BIg Bench for Large-Scale Database Grounded Text-to-SQLs. arXiv.

  5. Lei, F. et al. (2024). Spider 2.0: Evaluating Language Models on Real-World Enterprise Text-to-SQL Workflows. arXiv.

  6. LangChain. (2026). Build a SQL agent. Documentación oficial. Consultado el 26 de mayo de 2026.

  7. LlamaIndex. (2026). NL SQL table query engine. Documentación oficial. Consultado el 26 de mayo de 2026.

  8. OpenAI. (2026). Function calling. Documentación oficial. OpenAI. (2026). Structured model outputs. Documentación oficial. Consultado el 26 de mayo de 2026.

  9. SQLGlot. (2026). Python SQL parser and transpiler. Documentación. Consultado el 26 de mayo de 2026.

  10. DuckDB. (2026). Python API. Documentación oficial. Consultado el 26 de mayo de 2026.

  11. Vanna AI. (2026). Vanna 2.0: Turn Questions into Data Insights. GitHub. Consultado el 26 de mayo de 2026. El repositorio público consultado aparece como archivado el 29 de marzo de 2026.

Capítulo 13

Facsímil 4 · La caja de herramientas

Capítulo 13: Laboratorio mínimo: notebooks, evals y trazas

El lugar donde una demo se vuelve discutible

Una demo sirve para ver una posibilidad. Un laboratorio sirve para decidir si esa posibilidad aguanta un poco de realidad.

En este facsímil hemos hablado de APIs, modelos locales, tokens, costes, embeddings, RAG, evaluación, GraphRAG y Text-to-SQL. Todo eso puede quedarse en palabras si no lo llevamos a una mesa de trabajo mínima: datos pequeños, código ejecutable, métricas claras, trazas legibles y una decisión final.

Este capítulo es ese puente. No vamos a montar una plataforma industrial. Vamos a construir lo justo para que otra persona pueda ejecutar, revisar, criticar y mejorar lo que hicimos. Ese es el gesto profesional: no pedir confianza; dejar evidencia.

Qué no es un laboratorio

Un laboratorio no es un notebook que funciona una vez en tu máquina y queda abandonado. Tampoco es una captura bonita de una respuesta acertada. Y no es una colección de librerías instaladas sin una pregunta clara.

Un laboratorio tampoco sustituye a producción. En producción aparecen permisos reales, usuarios, colas, coste variable, cambios de datos y mantenimiento. El laboratorio no pretende resolver todo eso. Pretende descubrir antes qué merece pasar a una fase más seria.

Si no sabes qué pregunta estás probando, qué métrica mirarás y qué harás si sale mal, todavía no tienes laboratorio. Tienes una exploración. Puede ser útil, pero no permite decidir.

Qué sí debería dejar

Un laboratorio mínimo debe dejar cinco artefactos:

ArtefactoQué contienePor qué importa
DatasetCasos de prueba con respuesta o fuente esperada.Permite repetir la evaluación.
RunnerCódigo que ejecuta el sistema sobre esos casos.Evita evaluar a mano caso por caso.
MétricasNúmeros que resumen el comportamiento.Permite comparar versiones.
TrazasPasos internos de cada ejecución.Permite depurar por qué falló.
DecisiónPasar, parar, cambiar o medir más.Evita terminar con “parece que va bien”.

Los notebooks son útiles porque mezclan explicación, código y resultados. El formato Jupyter se basa en documentos JSON con celdas, salidas y metadatos, lo que permite guardar no solo código, sino también el contexto de ejecución.1 Esa flexibilidad es estupenda para aprender, pero también exige disciplina: fijar datos, ordenar celdas, limpiar salidas innecesarias y convertir lo aprendido en scripts o tests cuando el experimento empieza a importar.

En observabilidad, OpenTelemetry describe una traza como una operación formada por spans, donde cada span representa una unidad de trabajo con contexto y atributos.2 Nosotros haremos una versión casera: una lista de pasos con nombre, entrada, salida y metadatos. No será una herramienta de producción, pero enseñará la forma mental correcta.

Estado de herramientas con fecha de corte

Fecha de corte: 26 de mayo de 2026.
Fuentes consultadas ese día: documentación de Jupyter nbformat, OpenTelemetry Tracing API, OpenAI Graders, Ragas metrics, LangSmith RAG evaluation y Arize Phoenix evaluation.

OpenAI documenta graders como evaluadores usados en evals y fine-tuning, incluyendo validación de graders y ejemplos de evaluadores basados en modelos.3 Ragas organiza métricas para aplicaciones RAG, entre ellas context precision y faithfulness.4 LangSmith estructura la evaluación de RAG alrededor de corrección, relevancia, groundedness y relevancia de documentos.5

Phoenix separa evaluación de retrieval y evaluación de respuesta, y permite trabajar con trazas para analizar qué documentos se recuperaron y cómo se generó la respuesta.6 Su documentación de evaluación distingue evaluadores deterministas y evaluadores con modelo, y los presenta como forma de detectar regresiones y comparar cambios.7

La lección estable no depende de una marca concreta: una evaluación útil separa dataset, ejecución, métrica, trazas y decisión.

Anatomía de un laboratorio mínimo

Laboratorio mínimo: del caso a la decisión No basta con ejecutar: hay que dejar datos, métricas, trazas y una conclusión revisable. Dataset casos esperados fuentes y límites Runner ejecuta variantes sin tocar los casos Sistema RAG, tool, SQL o regla clásica Métricas hit, groundedness coste, latencia Decisión pasar, parar o cambiar Manifest versión, datos, entorno configuración usada Trazas route, retrieve, generate eval, tool, error Gate umbrales mínimos para avanzar Un laboratorio termina cuando permite tomar una decisión, no cuando imprime una respuesta. IA para gente curiosa / Facsímil 04 / Capítulo 13 / 686f6c61

La figura puede leerse de izquierda a derecha: parto de casos, ejecuto variantes, observo resultados y decido. Debajo aparecen las tres piezas que suelen faltar en las demos: manifest, trazas y gate.

Laboratorio

Un laboratorio, dentro de este libro, es una práctica guiada para poner en juego los conceptos del facsímil. Aquí no buscamos impresionar con una respuesta aislada. Buscamos construir algo pequeño que se pueda ejecutar, medir, explicar y corregir.

En este laboratorio vamos a tocar cuatro zonas del facsímil:

  • Del capítulo 2: contratos, tools y salidas estructuradas.
  • Del capítulo 3: coste, contexto y disciplina de ejecución.
  • De los capítulos 7, 8, 9 y 10: embeddings, búsqueda, RAG y evaluación.
  • Del capítulo 12: herramientas de datos, SQL, permisos, trazas y validación.

Los dos retos dejan solución completa. La idea no es esconder la respuesta, sino enseñar cómo piensa alguien que quiere llevar una idea desde “parece que funciona” hasta “puedo defender esta decisión”.

El kit real está en:

kit/

El capítulo muestra la lógica paso a paso. El kit deja los artefactos ejecutables: datos, contratos, scripts, trazas, gates y checker.

Reto 1: evaluar un mini RAG con trazas reproducibles

Contexto

Imagina que una escuela quiere un asistente interno para responder dudas administrativas. Hay documentos vigentes, documentos antiguos y preguntas donde el sistema debería decir “no tengo evidencia suficiente”.

Un RAG básico podría responder muy bien en una demo. Pero antes de confiar en él necesitamos saber tres cosas: si recupera el documento correcto, si evita documentos no vigentes y si deja una traza para depurar los fallos.

Objetivo

Construir un harness mínimo de evaluación RAG con Python puro. Debe ejecutar un conjunto de casos, recuperar documentos, decidir una respuesta sencilla, calcular métricas y guardar trazas.

Esto sale del capítulo 09, donde construimos RAG como recuperación más generación; del capítulo 10, donde medimos retrieval y groundedness; y del capítulo 14, donde decimos que una solución sin trazas sigue siendo una demo.

En el kit se ejecuta así:

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/evaluate_mini_rag.py --write
python3 -m json.tool output/ci_rag_gate.json
cat output/rag_decision.md

Material base

Tendremos cinco documentos:

IDEstadoTexto
matricula_vigentevigenteLa matrícula ordinaria se puede modificar hasta el 15 de septiembre.
matricula_antiguasustituidoLa matrícula ordinaria se podía modificar hasta el 1 de septiembre.
becas_vigentevigenteLas becas internas se revisan en dos fases: documentación y entrevista.
pagos_vigentevigenteLos pagos pendientes se consultan en el panel económico por campus.
soporte_vigentevigenteLas incidencias de acceso se atienden desde el portal de soporte.

Y cuatro casos de evaluación:

CasoPreguntaDocumento esperadoDebe responder
c1¿Hasta cuándo puedo modificar la matrícula?matricula_vigente
c2¿Cómo se revisan las becas internas?becas_vigente
c3¿Cuántos pagos pendientes hay por campus?pagos_vigenteNo con RAG textual
c4¿Cuál es el horario de cafetería?NingunoNo

El tercer caso es importante. El documento habla de dónde consultar pagos, pero no contiene el número. Una respuesta honesta debe reconocer que necesita una herramienta de datos.

Enunciado

  1. Representa documentos y casos como estructuras de datos.
  2. Implementa recuperación léxica con filtro de documentos vigentes.
  3. Guarda una traza con spans para cada caso.
  4. Calcula Hit@1, MRR, tasa de abstención correcta y cobertura de trazas.
  5. Decide si el RAG puede avanzar o qué habría que mejorar.

Resolución paso a paso

Primero hacemos explícito qué medimos.

Hit@1=casos donde el primer documento es el esperadocasos con documento esperado\operatorname{Hit@1} = \frac{\text{casos donde el primer documento es el esperado}} {\text{casos con documento esperado}}
SímboloSignificadoEn este reto
Hit@1Acierto en primera posiciónSi matricula_vigente sale primero en c1.
Casos con documento esperadoPreguntas que sí tienen fuente textualc1, c2, c3.
Documento esperadoFuente que debería aparecerCampo expected_doc.

MRR mide en qué posición aparece el primer resultado correcto:

MRR=1Ni=1N1ranki\operatorname{MRR} = \frac{1}{N}\sum_{i=1}^{N}\frac{1}{\operatorname{rank}_i}
SímboloSignificadoEn este reto
NNNúmero de casos con fuente esperadaTres casos.
ranki\operatorname{rank}_iPosición del documento esperado1 si sale primero, 2 si sale segundo.
1ranki\frac{1}{\operatorname{rank}_i}Premio por encontrar pronto1.0 si sale primero, 0.5 si sale segundo.

Ahora programamos el laboratorio.

from collections import Counter
import json
import math
import re
import time
import uuid


DOCUMENTS = [
    {
        "id": "matricula_vigente",
        "status": "vigente",
        "text": "La matrícula ordinaria se puede modificar hasta el 15 de septiembre.",
    },
    {
        "id": "matricula_antigua",
        "status": "sustituido",
        "text": "La matrícula ordinaria se podía modificar hasta el 1 de septiembre.",
    },
    {
        "id": "becas_vigente",
        "status": "vigente",
        "text": "Las becas internas se revisan en dos fases: documentación y entrevista.",
    },
    {
        "id": "pagos_vigente",
        "status": "vigente",
        "text": "Los pagos pendientes se consultan en el panel económico por campus.",
    },
    {
        "id": "soporte_vigente",
        "status": "vigente",
        "text": "Las incidencias de acceso se atienden desde el portal de soporte.",
    },
]

CASES = [
    {
        "id": "c1",
        "question": "¿Hasta cuándo puedo modificar la matrícula?",
        "expected_doc": "matricula_vigente",
        "should_answer": True,
    },
    {
        "id": "c2",
        "question": "¿Cómo se revisan las becas internas?",
        "expected_doc": "becas_vigente",
        "should_answer": True,
    },
    {
        "id": "c3",
        "question": "¿Cuántos pagos pendientes hay por campus?",
        "expected_doc": "pagos_vigente",
        "should_answer": False,
        "reason": "La pregunta pide un número vivo, no una explicación documental.",
    },
    {
        "id": "c4",
        "question": "¿Cuál es el horario de cafetería?",
        "expected_doc": None,
        "should_answer": False,
        "reason": "No hay documento sobre cafetería.",
    },
]

STOPWORDS = {
    "a",
    "al",
    "como",
    "con",
    "cual",
    "cuando",
    "cuantos",
    "de",
    "del",
    "desde",
    "donde",
    "el",
    "en",
    "es",
    "hasta",
    "la",
    "las",
    "lo",
    "los",
    "me",
    "mi",
    "no",
    "por",
    "puedo",
    "que",
    "se",
    "un",
    "una",
    "y",
}


def tokens(text):
    return [
        token
        for token in re.findall(r"[a-záéíóúñ0-9]+", text.lower())
        if token not in STOPWORDS
    ]


def vector(text):
    return Counter(tokens(text))


def score(query, document):
    q = vector(query)
    d = vector(document["text"])
    overlap = sum(min(q[t], d[t]) for t in q)
    if overlap == 0:
        return 0.0
    return overlap / math.sqrt(sum(v * v for v in d.values()))


def trace_span(trace, name, **attrs):
    trace["spans"].append(
        {
            "name": name,
            "timestamp_ms": int(time.time() * 1000),
            "attrs": attrs,
        }
    )


def retrieve(case, top_k=2):
    trace = {"trace_id": str(uuid.uuid4()), "case_id": case["id"], "spans": []}
    trace_span(trace, "input", question=case["question"])

    candidates = [doc for doc in DOCUMENTS if doc["status"] == "vigente"]
    trace_span(trace, "filter_documents", candidates=[doc["id"] for doc in candidates])

    ranked = sorted(
        ((score(case["question"], doc), doc["id"], doc["text"]) for doc in candidates),
        reverse=True,
    )
    ranked = [item for item in ranked if item[0] > 0][:top_k]
    trace_span(trace, "retrieve", results=[doc_id for _, doc_id, _ in ranked])

    answerable = case["should_answer"] and bool(ranked)
    if not answerable:
        answer = "No tengo evidencia suficiente para responder con este RAG."
    else:
        answer = ranked[0][2]
    trace_span(trace, "generate", answer=answer, answerable=answerable)

    return {
        "case_id": case["id"],
        "expected_doc": case["expected_doc"],
        "should_answer": case["should_answer"],
        "ranked_docs": [doc_id for _, doc_id, _ in ranked],
        "answer": answer,
        "trace": trace,
    }


def reciprocal_rank(expected_doc, ranked_docs):
    if expected_doc is None:
        return None
    if expected_doc not in ranked_docs:
        return 0.0
    return 1.0 / (ranked_docs.index(expected_doc) + 1)


def evaluate(results):
    with_expected = [r for r in results if r["expected_doc"] is not None]
    hit_at_1 = sum(
        r["ranked_docs"][:1] == [r["expected_doc"]] for r in with_expected
    ) / len(with_expected)

    rr_values = [reciprocal_rank(r["expected_doc"], r["ranked_docs"]) for r in with_expected]
    mrr = sum(rr_values) / len(rr_values)

    abstention_ok = sum(
        (not r["should_answer"]) == r["answer"].startswith("No tengo evidencia")
        for r in results
    ) / len(results)

    trace_ok = sum(len(r["trace"]["spans"]) >= 4 for r in results) / len(results)

    return {
        "hit@1": round(hit_at_1, 2),
        "mrr": round(mrr, 2),
        "abstention_ok": round(abstention_ok, 2),
        "trace_ok": round(trace_ok, 2),
    }


results = [retrieve(case) for case in CASES]
metrics = evaluate(results)

print("Métricas")
print(json.dumps(metrics, ensure_ascii=False, indent=2))

print("\nTrazas resumidas")
for result in results:
    span_names = [span["name"] for span in result["trace"]["spans"]]
    print(result["case_id"], result["ranked_docs"], span_names)

Salida esperada

Métricas
{
  "hit@1": 1.0,
  "mrr": 1.0,
  "abstention_ok": 1.0,
  "trace_ok": 1.0
}

Trazas resumidas
c1 ['matricula_vigente'] ['input', 'filter_documents', 'retrieve', 'generate']
c2 ['becas_vigente'] ['input', 'filter_documents', 'retrieve', 'generate']
c3 ['pagos_vigente'] ['input', 'filter_documents', 'retrieve', 'generate']
c4 [] ['input', 'filter_documents', 'retrieve', 'generate']

Solución

El resultado pasa el gate mínimo:

MétricaValor esperadoLectura
Hit@11.0Cuando hay documento esperado, aparece primero.
MRR1.0El documento esperado aparece en la primera posición.
Abstención correcta1.0El sistema no responde cuando no debe.
Trazas completas1.0Cada caso deja pasos mínimos para depurar.

El caso c3 enseña la parte más importante: recuperar pagos_vigente no autoriza inventar un número. El documento sirve para saber dónde consultar pagos. La pregunta pide una cifra viva, así que la respuesta correcta para este RAG textual es abstenerse y derivar a una herramienta de datos.

Por qué funciona

Este reto junta varias piezas del facsímil:

  • Capítulo 08: usamos filtro de metadatos para evitar documentos sustituidos.
  • Capítulo 09: separamos recuperación y generación.
  • Capítulo 10: medimos retrieval y abstención.
  • Capítulo 12: reconocemos cuándo una pregunta documental pasa a ser una pregunta de datos.

La clave es que el laboratorio no solo mide respuestas. Mide piezas: si recuperó, si debía responder, si dejó traza y si el resultado permite decidir.

Cómo explicarlo a otra persona

"No hemos preguntado si el asistente suena bien. Hemos preparado cuatro casos, sabemos qué fuente debería recuperar, comprobamos si responde solo cuando toca y guardamos los pasos. Si falla, sabremos si falló buscando, filtrando o decidiendo responder."

Variaciones

  • Cambia el estado de matricula_antigua a vigente y observa si aparece ruido.
  • Añade una métrica precision@2.
  • Añade un campo source_status en la traza de retrieval.
  • Añade un caso donde el documento correcto sale segundo y revisa cómo baja MRR.

Reto 2: decidir ruta entre RAG, SQL, clasificador y cálculo

Contexto

Ahora queremos probar una idea más cercana a producto. Una misma interfaz recibe preguntas distintas: algunas son documentales, otras piden datos, otras clasifican tickets y otras son cálculos exactos.

No queremos un agente complejo todavía. Queremos un laboratorio mínimo que enrute cada caso a la herramienta adecuada y mida si la decisión fue correcta. Esto prepara el paso al facsímil 5, donde sí hablaremos de orquestación con más profundidad.

Objetivo

Construir un router pequeño con cuatro rutas:

RutaUso
ragPreguntas sobre documentos.
sqlPreguntas sobre datos tabulares.
classifierClasificación estructurada de tickets.
codeCálculo determinista sin modelo.

El sistema debe devolver una salida estructurada y una traza. La evaluación debe medir si eligió la ruta correcta, si produjo el resultado esperado y si dejó evidencia suficiente.

En el kit se ejecuta así:

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/evaluate_router.py --write
python3 -m json.tool output/ci_router_gate.json
cat output/router_decision.md

Material base

Casos:

CasoEntradaRuta esperada
r1¿Hasta cuándo puedo modificar la matrícula?rag
r2¿Cuántos pagos pendientes hay por campus?sql
r3Clasifica: no puedo entrar en mi cuentaclassifier
r4Suma 230 y 515code

Este reto fuerza una idea del capítulo 01: no todo se arregla con la misma herramienta.

Enunciado

  1. Define casos con ruta esperada y resultado esperado.
  2. Implementa cuatro herramientas mínimas.
  3. Implementa un router explicable.
  4. Registra spans route, tool y evaluate.
  5. Calcula route_accuracy, task_pass_rate y trace_complete_rate.
  6. Decide si el router puede avanzar al laboratorio del capítulo 14 o debe corregirse.

Resolución paso a paso

Primero definimos qué significa pasar:

route_accuracy=casos con ruta correctacasos totales\operatorname{route\_accuracy} = \frac{\text{casos con ruta correcta}}{\text{casos totales}} task_pass_rate=casos con resultado correctocasos totales\operatorname{task\_pass\_rate} = \frac{\text{casos con resultado correcto}}{\text{casos totales}}
MétricaPregunta que respondePor qué importa
Route accuracy¿Elegimos la herramienta correcta?Un buen resultado por casualidad no basta.
Task pass rate¿El caso terminó bien?Mide utilidad visible.
Trace complete rate¿Podemos depurar cada caso?Sin traza, no hay aprendizaje reproducible.

Ahora el código.

import json
import re
import sqlite3
import time
import uuid


DOCS = {
    "matricula": "La matrícula ordinaria se puede modificar hasta el 15 de septiembre.",
    "becas": "Las becas internas se revisan en dos fases.",
}

CASES = [
    {
        "id": "r1",
        "input": "¿Hasta cuándo puedo modificar la matrícula?",
        "expected_route": "rag",
        "expected_contains": "15 de septiembre",
    },
    {
        "id": "r2",
        "input": "¿Cuántos pagos pendientes hay por campus?",
        "expected_route": "sql",
        "expected_contains": "Norte: 2",
    },
    {
        "id": "r3",
        "input": "Clasifica: no puedo entrar en mi cuenta",
        "expected_route": "classifier",
        "expected_contains": "acceso",
    },
    {
        "id": "r4",
        "input": "Suma 230 y 515",
        "expected_route": "code",
        "expected_contains": "745",
    },
]


def new_trace(case_id):
    return {"trace_id": str(uuid.uuid4()), "case_id": case_id, "spans": []}


def span(trace, name, **attrs):
    trace["spans"].append(
        {"name": name, "timestamp_ms": int(time.time() * 1000), "attrs": attrs}
    )


def route(text):
    lower = text.lower()
    if "suma" in lower or re.search(r"\d+\s+y\s+\d+", lower):
        return "code", "la entrada pide cálculo exacto"
    if "cuántos" in lower or "pagos pendientes" in lower:
        return "sql", "la entrada pide datos agregados"
    if "clasifica" in lower:
        return "classifier", "la entrada pide etiqueta estructurada"
    return "rag", "la entrada pregunta por documentación"


def tool_rag(text):
    if "matrícula" in text.lower() or "matricula" in text.lower():
        return {"answer": DOCS["matricula"], "evidence": ["matricula"]}
    return {"answer": "No tengo evidencia suficiente.", "evidence": []}


def tool_sql(_text):
    con = sqlite3.connect(":memory:")
    con.execute("CREATE TABLE pagos (campus TEXT, estado TEXT)")
    con.executemany(
        "INSERT INTO pagos VALUES (?, ?)",
        [
            ("Norte", "pendiente"),
            ("Norte", "pendiente"),
            ("Sur", "pagado"),
            ("Centro", "pendiente"),
        ],
    )
    rows = con.execute("""
        SELECT campus, COUNT(*) AS pagos
        FROM pagos
        WHERE estado = 'pendiente'
        GROUP BY campus
        ORDER BY pagos DESC, campus ASC
    """).fetchall()
    answer = "; ".join(f"{campus}: {count}" for campus, count in rows)
    return {"answer": answer, "evidence": ["sql:pagos"]}


def tool_classifier(text):
    lower = text.lower()
    if "entrar" in lower or "cuenta" in lower:
        category = "acceso"
    elif "factura" in lower:
        category = "facturacion"
    else:
        category = "general"
    return {"answer": json.dumps({"categoria": category}, ensure_ascii=False), "evidence": ["rules:ticket"]}


def tool_code(text):
    numbers = [int(n) for n in re.findall(r"\d+", text)]
    return {"answer": str(sum(numbers)), "evidence": ["python:sum"]}


TOOLS = {
    "rag": tool_rag,
    "sql": tool_sql,
    "classifier": tool_classifier,
    "code": tool_code,
}


def run_case(case):
    trace = new_trace(case["id"])
    selected_route, reason = route(case["input"])
    span(trace, "route", selected_route=selected_route, reason=reason)

    result = TOOLS[selected_route](case["input"])
    span(trace, "tool", route=selected_route, evidence=result["evidence"])

    route_ok = selected_route == case["expected_route"]
    task_ok = case["expected_contains"] in result["answer"]
    span(trace, "evaluate", route_ok=route_ok, task_ok=task_ok)

    return {
        "case_id": case["id"],
        "route": selected_route,
        "answer": result["answer"],
        "route_ok": route_ok,
        "task_ok": task_ok,
        "trace": trace,
    }


def summarize(results):
    total = len(results)
    return {
        "route_accuracy": sum(r["route_ok"] for r in results) / total,
        "task_pass_rate": sum(r["task_ok"] for r in results) / total,
        "trace_complete_rate": sum(len(r["trace"]["spans"]) == 3 for r in results) / total,
    }


results = [run_case(case) for case in CASES]
metrics = summarize(results)

for result in results:
    print(result["case_id"], result["route"], result["answer"])

print(json.dumps(metrics, indent=2))

Salida esperada

r1 rag La matrícula ordinaria se puede modificar hasta el 15 de septiembre.
r2 sql Norte: 2; Centro: 1
r3 classifier {"categoria": "acceso"}
r4 code 745
{
  "route_accuracy": 1.0,
  "task_pass_rate": 1.0,
  "trace_complete_rate": 1.0
}

Solución

El router pasa el gate mínimo:

CasoRuta elegidaPor qué es correcta
r1ragLa respuesta vive en un documento.
r2sqlLa pregunta pide una agregación sobre datos.
r3classifierLa salida esperada es una categoría estructurada.
r4codeEs un cálculo exacto; no necesita modelo.

El resultado más valioso quizá sea r4. Nos recuerda que la caja de herramientas incluye no usar IA generativa cuando una operación determinista resuelve mejor.

Por qué funciona

Este reto junta casi todo el facsímil:

  • Capítulo 01: elegimos intervención según el cuello real.
  • Capítulo 02: devolvemos salidas estructuradas en el clasificador.
  • Capítulo 09: usamos RAG solo para documentación.
  • Capítulo 12: usamos SQL cuando la respuesta está en datos.
  • Capítulo 14: dejamos trazas y métricas antes de confiar.

La frontera con agentes está muy cerca, pero todavía no la cruzamos del todo. Aquí el router es simple y explicable. Eso es una virtud: si falla, sabemos dónde mirar.

Cómo explicarlo a otra persona

"Hemos construido una ventanilla que no responde todo igual. Si la pregunta va de documentos, busca documentos. Si pide datos, consulta una tabla. Si pide clasificación, devuelve una categoría. Si pide una suma, usa código. Además, deja una traza para saber qué ruta eligió y si acertó."

Variaciones

  • Añade una ruta human_review para preguntas ambiguas.
  • Añade un presupuesto máximo de latencia por ruta.
  • Añade un caso que parezca documental pero necesite SQL.
  • Cambia el router para devolver también una confianza y decide cuándo pedir aclaración.

Validar la entrega

La solución de referencia se valida con:

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/check_student_submission.py --submission-dir solutions/reference --write

Para una entrega propia:

python3 ops/check_student_submission.py --submission-dir solutions/mi-equipo --write --fail-on-missing

La referencia obtiene 70/70. La carpeta esperada es:

tools-evals-release/
  rag_eval_report.json
  ci_rag_gate.json
  rag_traces.jsonl
  rag_decision.md
  router_eval_report.json
  ci_router_gate.json
  router_traces.jsonl
  router_decision.md

Cierre del laboratorio

Si has trabajado los dos retos, ya has hecho lo esencial de un laboratorio de IA: preparar casos, ejecutar una variante, medir resultados, guardar trazas y convertir los números en una decisión.

El primer reto te obligó a mirar un RAG por dentro. El segundo te obligó a decidir qué herramienta usar para cada tipo de petición. Esa es la idea central del facsímil 4: la caja de herramientas no vale por la cantidad de piezas, sino por saber cuándo usar cada una y cómo comprobar que funcionó.

Lo que viene después, en agentes y orquestación, no cambia esta base. La amplifica. Cuantas más herramientas coordine un sistema, más importante será medir rutas, permisos, coste, evidencia y trazas.

Cómo encaja todo

graph TD
    subgraph "Capítulo 13: laboratorio mínimo"
        LAB["Laboratorio"]
        DATASET["Dataset de evaluación"]
        RUNNER["Runner"]
        METRICS["Métricas"]
        TRACE["Trazas"]
        GATE["Gate de decisión"]
        RAGLAB["Reto RAG"]
        ROUTERLAB["Reto router"]
    end
    subgraph "Viene del facsímil 04"
        API["APIs y contratos (F4C2)"]
        TOKENS["Coste y contexto (F4C3)"]
        VECTOR["Búsqueda híbrida (F4C8)"]
        RAG["RAG básico (F4C9)"]
        RAGEVAL["Evaluar RAG (F4C10)"]
        SQL["Text-to-SQL (F4C12)"]
    end
    subgraph "Sigue después"
        RECAP["Recapitulación (F4C14)"]
        AGENTES["Agentes y orquestación (F5)"]
        OPERAR["Construir y operar (F6)"]
        EVALUAR["Evaluar e interpretar (F7)"]
    end

    LAB -->|"se apoya en"| DATASET
    DATASET -->|"alimenta"| RUNNER
    RUNNER -->|"produce"| METRICS
    RUNNER -->|"registra"| TRACE
    METRICS -->|"activan"| GATE
    TRACE -->|"explica"| GATE
    RAG -->|"se practica en"| RAGLAB
    RAGEVAL -->|"mide"| RAGLAB
    SQL -->|"se practica en"| ROUTERLAB
    API -->|"estructura"| ROUTERLAB
    VECTOR -->|"recupera en"| RAGLAB
    TOKENS -->|"limita"| GATE
    RAGLAB -->|"prepara"| RECAP
    ROUTERLAB -->|"prepara"| AGENTES
    TRACE -->|"prepara"| OPERAR
    METRICS -->|"prepara"| EVALUAR

    style LAB fill:#F5F5F5,stroke:#000000,stroke-width:2
    style DATASET fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RUNNER fill:#F5F5F5,stroke:#000000,stroke-width:2
    style METRICS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TRACE fill:#111111,stroke:#000000,stroke-width:2,color:#FFFFFF
    style GATE fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RAGLAB fill:#F5F5F5,stroke:#000000,stroke-width:2
    style ROUTERLAB fill:#F5F5F5,stroke:#000000,stroke-width:2
    style API stroke-dasharray: 5 5
    style TOKENS stroke-dasharray: 5 5
    style VECTOR stroke-dasharray: 5 5
    style RAG stroke-dasharray: 5 5
    style RAGEVAL stroke-dasharray: 5 5
    style SQL stroke-dasharray: 5 5
    style RECAP stroke-dasharray: 5 5
    style AGENTES stroke-dasharray: 5 5
    style OPERAR stroke-dasharray: 5 5
    style EVALUAR stroke-dasharray: 5 5

Vocabulario aprendido

TérminoDefinición
LaboratorioEspacio reproducible para probar una idea con casos, métricas y trazas.
NotebookDocumento ejecutable que mezcla explicación, código, salidas y metadatos.
EvalPrueba sistemática para medir si un sistema cumple un comportamiento esperado.
Dataset de evaluaciónConjunto de casos con entradas, resultados esperados y criterios de aceptación.
TrazaRegistro estructurado de los pasos que produjeron una respuesta.
SpanPaso concreto dentro de una traza.
Hit@kIndica si el resultado esperado aparece entre los k primeros.
MRRMedia del inverso de la posición del primer resultado correcto.
GateUmbral o regla que decide si una variante puede avanzar.
ManifestRegistro de versión, datos, entorno y configuración del experimento.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Confundir notebook con laboratorioUn notebook puede ejecutar sin dejar decisión ni reproducibilidad.Añadir dataset, métricas, manifest y conclusión.
Medir solo la respuesta finalNo sabes si falló retrieval, routing, herramienta o generación.Medir por capas y guardar spans.
No incluir casos donde debe abstenerseEl sistema aprende a contestar siempre.Añadir casos sin evidencia suficiente.
Cambiar prompt y dataset a la vezSi mejora, no sabes qué lo causó.Fijar dataset y cambiar una variable por experimento.
No poner gateLa evaluación queda como un informe decorativo.Definir umbrales antes de mirar resultados.
Olvidar que una regla puede ser mejorAlgunas rutas no necesitan modelo generativo.Comparar contra SQL, código o reglas simples.

Antes de pasar página

  • ¿Puedo explicar la diferencia entre demo, notebook y laboratorio?
  • ¿Sé qué debe contener un dataset de evaluación mínimo?
  • ¿Puedo calcular Hit@1 y MRR con un ejemplo pequeño?
  • ¿Sé por qué una traza se divide en spans?
  • ¿Puedo distinguir evaluación de retrieval, respuesta, ruta y herramienta?
  • ¿Sé diseñar un caso donde el sistema debe abstenerse?
  • ¿Puedo explicar por qué un documento recuperado no siempre autoriza responder?
  • ¿Sé cuándo usar RAG, SQL, clasificador o cálculo determinista?
  • ¿Puedo definir un gate antes de ejecutar el experimento?
  • ¿He ejecutado los dos retos y revisado al menos una traza?

En resumen

Idea fuerzaDetalle
Un laboratorio no es una demo.Debe dejar casos, métricas, trazas y decisión.
El dataset manda.Sin casos fijos, no puedes comparar variantes con rigor.
Las trazas explican los fallos.Permiten saber si falló recuperar, enrutar, ejecutar o evaluar.
La abstención también se evalúa.Un sistema útil debe saber cuándo no tiene evidencia suficiente.
El router prepara agentes.Antes de orquestar muchas herramientas, hay que medir rutas simples.
El gate cierra el ciclo.Una métrica solo sirve si cambia una decisión.

Para saber más

Arize Phoenix. (2026). Evaluate RAG. Documentación oficial

Arize Phoenix. (2026). Evaluation concepts. Documentación oficial

Cormack, G. V., Clarke, C. L. A. y Büttcher, S. (2009). Reciprocal rank fusion outperforms Condorcet and individual rank learning methods. Proceedings of SIGIR, 758-759. DOI

Johnson, J., Douze, M. y Jégou, H. (2019). Billion-scale similarity search with GPUs. IEEE Transactions on Big Data, 7(3), 535-547. DOI

Jupyter. (2026). The Notebook file format. Documentación oficial

LangChain. (2026). Evaluate a RAG application. Documentación oficial

Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474. NeurIPS

OpenAI. (2026). Graders. Documentación oficial

OpenTelemetry. (2026). Tracing API. Documentación oficial

Ragas. (2026). List of available metrics. Documentación oficial

Robertson, S. y Zaragoza, H. (2009). The probabilistic relevance framework: BM25 and beyond. Foundations and Trends in Information Retrieval, 3(4), 333-389. DOI

Notas

  1. Jupyter. (2026). The Notebook file format. Documentación oficial. Consultado el 26 de mayo de 2026.

  2. OpenTelemetry. (2026). Tracing API. Documentación oficial. Consultado el 26 de mayo de 2026.

  3. OpenAI. (2026). Graders. Documentación oficial. Consultado el 26 de mayo de 2026.

  4. Ragas. (2026). List of available metrics. Documentación oficial. Consultado el 26 de mayo de 2026.

  5. LangChain. (2026). Evaluate a RAG application. Documentación oficial. Consultado el 26 de mayo de 2026.

  6. Arize Phoenix. (2026). Evaluate RAG. Documentación oficial. Consultado el 26 de mayo de 2026.

  7. Arize Phoenix. (2026). Evaluation concepts. Documentación oficial. Consultado el 26 de mayo de 2026.

Capítulo 14

Facsímil 4 · La caja de herramientas

Capítulo 14: Lo que deberías saber: la caja de herramientas

Entrando en el cierre

Este facsímil empezó con una pregunta muy práctica: cuando un sistema de IA no hace lo que necesitas, ¿qué cambias exactamente?

Podrías cambiar el prompt. Podrías exigir una salida estructurada. Podrías añadir documentos con RAG. Podrías conectar una herramienta. Podrías elegir otro modelo. Podrías servirlo en local. Podrías usar embeddings, bases vectoriales, búsqueda híbrida, evaluación, GraphRAG o Text-to-SQL. La caja ya no está vacía.

Pero tener herramientas no significa tener criterio. Un equipo puede complicar una solución hasta que nadie la entiende. También puede quedarse corto y llamar “IA” a una demo que no soporta coste, permisos, datos reales ni evaluación. Este capítulo es una revisión activa: no busca que recuerdes nombres, sino que puedas justificar decisiones.

Si has entendido el facsímil, deberías poder mirar un caso y decir con calma: qué problema hay, qué intervención toca, qué contrato hace falta, qué métrica lo prueba, qué coste introduce y qué pasaría si mañana cambia el dato, el modelo o el usuario.

El capítulo 13 es el lugar donde esto se practica con las manos: notebooks, evaluaciones y trazas. Este capítulo 14 hace otra cosa: te da la brújula para revisar lo construido. Si el laboratorio es el banco de pruebas, esta recapitulación es la conversación técnica posterior: qué hemos aprendido, qué sigue flojo y qué ya podríamos defender delante de otra persona.

Fecha de corte y alcance

Fecha de corte: 26 de mayo de 2026.
Alcance: este cierre resume mecanismos estables del facsímil y referencias ya trabajadas: function calling, salidas estructuradas, prompt caching, model cards, LoRA, QLoRA, RAG, búsqueda vectorial, búsqueda híbrida, evaluación RAG, GraphRAG y Text-to-SQL.

Las herramientas concretas cambiarán. Los nombres de APIs, modelos, precios, runtimes y proveedores se moverán. El criterio que queremos conservar es más estable: separar entrada, contexto, herramienta, pesos, despliegue, evaluación y trazas.

Qué debería llevarse cada perfil

Este libro está escrito para gente curiosa con o sin perfil técnico. Eso no significa que todo el mundo tenga que llevarse lo mismo. Significa que cada persona debería poder salir con una forma más precisa de mirar un sistema de IA.

PerfilDebería poder hacer al terminar este facsímilPregunta que ya no debería aceptar sin más
IngenieríaDiseñar una arquitectura mínima con contratos, permisos, trazas y evaluación.“¿Y si le ponemos un agente?”
DatosSeparar pregunta documental, búsqueda semántica, métrica, SQL y validación de resultados.“¿El modelo ya sabe los datos?”
ProductoDecidir si una función merece prompt, RAG, tool, ajuste, modelo local o nada de IA.“¿Podemos meter IA aquí?”
DocenciaExplicar cada herramienta con un ejemplo pequeño, una limitación y una prueba.“¿Esto funciona porque lo he visto en una demo?”
Persona curiosaPreguntar qué entra, qué sale, qué se mide, qué cuesta y qué pasa cuando no hay evidencia.“¿La IA lo ha dicho, entonces será verdad?”

El punto común es el criterio. La persona técnica lo aplicará escribiendo código, definiendo contratos o levantando infraestructura. La persona no técnica lo aplicará haciendo mejores preguntas, detectando promesas débiles y pidiendo pruebas antes de confiar.

La brújula: qué quieres cambiar

La pregunta más importante del facsímil no es “¿qué herramienta está de moda?”. Es esta:

¿Dónde está el cuello del problema?

Cada intervención cambia una parte distinta del sistema. Si confundes la parte, puedes trabajar mucho y mejorar poco.

Si el problema está en...Intervención naturalQué cambiaQué no arregla
Instrucción confusaPrompt, ejemplos, formatoLa entradaDatos que el modelo no tiene.
Salida frágilJSON schema, parser, validadorEl contrato de salidaLa calidad del contenido.
Conocimiento cambianteRAG o herramienta de consultaEl contextoEl razonamiento sobre datos mal definidos.
Estado realTool/API/base de datosLa capacidad de actuar o consultarPermisos, auditoría o validación.
Estilo repetidoFine-tuning o LoRAParte del comportamientoInformación viva.
Coste o latenciaModelo menor, caché, local, cuantizaciónInfraestructura y presupuestoPreguntas mal formuladas.
Recuperación pobreEmbeddings, BM25, híbrida, rerankingEvidencia encontradaRespuestas sin verificación.
Preguntas relacionalesGraphRAG o Text-to-SQLEstructura consultableAmbigüedad de negocio.

OpenAI documenta function calling como forma de describir herramientas con esquemas y recibir argumentos estructurados.1 Las salidas estructuradas permiten exigir una forma de respuesta compatible con un esquema.2 Esos mecanismos no hacen que el modelo “sepa más”; hacen que el sistema sea más gobernable.

La primera habilidad del facsímil es elegir la intervención pequeña que toca. La segunda es saber cuándo esa intervención ya no basta.

El mapa completo de la caja

Facsímil 04: la caja de herramientas No elegimos herramientas por nombre: elegimos qué superficie del sistema vamos a controlar. Problema qué falla y por qué Intervención entrada, contexto o pesos Contrato schema, límites, errores Evidencia documento, vector, SQL Respuesta con límites Modelo y API mensajes · streaming structured outputs · tools Presupuesto tokens · caché · latencia cloud · local · cuantización Recuperación embeddings · BM25 híbrida · reranking Estructura RAG · GraphRAG Text-to-SQL · datos Evaluación offline casos, métricas, regresiones antes de enseñar a usuarios Trazabilidad versiones, prompts, fuentes modelo, coste y resultado Operación observabilidad, cambios coste, permisos, mantenimiento La herramienta correcta es la que puedes explicar, medir, limitar y mantener. IA para gente curiosa / Facsímil 04 / Capítulo 14 / 686f6c61

El mapa tiene una idea central: las herramientas no están alineadas por glamour, sino por responsabilidad. Si una pieza no tiene contrato, métrica ni traza, todavía no es una pieza de ingeniería; es una promesa.

1. Elegir la intervención correcta

En el capítulo 01 aprendimos a no empezar por la herramienta. Antes de decir RAG, fine-tuning, modelo local o agente, hay que diagnosticar el tipo de fallo.

Para recordar. Prompt y ejemplos cambian la entrada. Structured outputs cambian el contrato. RAG cambia el contexto. Una tool cambia la capacidad de consultar o actuar. Fine-tuning y LoRA cambian parte del comportamiento aprendido.3

Caso cercano. Una universidad quiere un asistente que responda dudas de matrícula. Si falla el tono, quizá basta con ejemplos. Si falla porque desconoce normas nuevas, RAG. Si debe consultar expediente, tool. Si debe devolver JSON para abrir un ticket, structured outputs. Si siempre corrige con una rúbrica propia, puede aparecer un ajuste.

Vuelve al capítulo 01 si: no puedes rellenar una tabla de “cambia / sirve cuando / no sirve para” sin mirar apuntes.

2. APIs: mensajes, eventos y contratos

En el capítulo 02 dejamos de tratar la API como una caja negra. Una llamada moderna tiene mensajes, roles, instrucciones, entradas multimodales, parámetros, streaming, tools, esquemas de salida y manejo de errores.

Para recordar. Una API buena no es solo “envío prompt y recibo texto”. Es un contrato entre aplicación y modelo. La aplicación decide qué datos manda, qué formato exige, qué herramientas declara, cómo valida y qué hace si la respuesta no cumple.

PiezaPregunta que respondeError típico
Mensajes¿Quién dice qué y con qué prioridad?Mezclar instrucciones de sistema con texto de usuario.
Parámetros¿Cuánta variación permito?Tocar temperatura sin evaluación.
Streaming¿Cómo llega la respuesta?No diseñar cancelación ni estados parciales.
Tools¿Qué puede consultar el sistema?Dejar que el modelo decida permisos.
Structured outputs¿Qué forma debe tener la salida?Parsear texto libre con expresiones frágiles.

Caso cercano. Un asistente administrativo clasifica correos y crea tareas. No debería “redactar algo parecido a JSON”. Debería devolver campos obligatorios, categorías permitidas, confianza y razón breve. La aplicación valida antes de crear nada.

Vuelve al capítulo 02 si: no puedes diseñar una llamada completa con entrada, salida estructurada, streaming opcional, tool declarada y errores previstos.

3. Tokens, contexto y caché

En el capítulo 03 aprendimos que el contexto no es gratis. Cada tabla, documento, ejemplo, historial y política consume tokens. El coste no vive solo en la salida.

Para recordar. El contexto es memoria temporal, no memoria perfecta. La caché puede reducir coste si reutilizas prefijos estables, pero no arregla un prompt lleno de ruido. La documentación de prompt caching trata precisamente esta idea: reutilizar segmentos repetidos para optimizar coste y latencia cuando el patrón lo permite.4

Ejemplo de fórmula. Una cuenta de ejemplo para razonar:

Cturno=Cinput+Coutput+Cherramientas+CobservabilidadC_{\text{turno}} = C_{\text{input}} + C_{\text{output}} + C_{\text{herramientas}} + C_{\text{observabilidad}}
SímboloQué significaQué revisar
CinputC_{\text{input}}Coste de contexto enviado.Esquema, docs, ejemplos, historial.
CoutputC_{\text{output}}Coste de tokens generados.Respuestas largas, razonamiento, formato.
CherramientasC_{\text{herramientas}}Consultas externas.Bases, vector stores, APIs.
CobservabilidadC_{\text{observabilidad}}Trazas y evaluación.Logs, métricas, almacenamiento.

Caso cercano. Meter todo el manual de 300 páginas en cada llamada puede funcionar en una demo y fracasar en coste. Un sistema serio recupera lo relevante, cachea instrucciones estables y mide si el contexto añadido mejora la respuesta.

Vuelve al capítulo 03 si: no puedes explicar por qué más contexto puede empeorar una respuesta.

4. Model cards y elección de modelos

En el capítulo 04 aprendimos a leer una ficha de modelo como un expediente técnico. Nombre, licencia, parámetros, contexto, precisión, benchmarks, proveedor, plantilla de chat y formato de pesos no son adornos.

Para recordar. Una model card sirve para comparar promesas con restricciones. El trabajo original sobre model cards propuso documentar uso previsto, factores relevantes, métricas, datos de evaluación y consideraciones éticas de los modelos.5

TérminoQué aporta realmentePregunta sana
Parámetros totalesCapacidad almacenada aproximada.¿Cuántos se activan por token?
Context lengthVentana máxima declarada.¿Con qué coste y calidad real?
Tensor typePrecisión y formatos presentes.¿Qué usa inferencia de verdad?
LicenseCondiciones de uso.¿Permite mi caso concreto?
BenchmarksSeñales comparativas.¿Se parecen a mi tarea?
Chat templateContrato de entrada.¿Estoy enviando mensajes como toca?

Caso cercano. Un modelo con ventana de contexto enorme puede parecer perfecto para contratos. Pero si recupera mal detalles de la mitad del documento, tarda demasiado o cuesta diez veces más, el número de contexto no resuelve el producto.

Vuelve al capítulo 04 si: no puedes explicar una model card sin convertirla en un catálogo de siglas.

5. Local, cuantización y dependencia operativa

En los capítulos 05 y 06 bajamos del modelo abstracto a la máquina real: memoria, VRAM, cuantización, runtimes, cloud, local, alquiler de GPUs, latencia, privacidad, coste y mantenimiento.

Para recordar. Local no significa gratis. Cloud no significa simple. Un modelo local necesita formato, runtime, memoria, CPU/GPU, plantilla, servidor, límites, observabilidad y actualización. Un modelo cloud necesita contrato de datos, precios, latencia, región, cuotas, cambios de versión y fallback.

DecisiónGanasPagas
API cloudFacilidad, escalado, modelos potentes.Coste variable, dependencia, políticas de datos.
Local en portátilControl y aprendizaje.Menos potencia, setup, rendimiento limitado.
Servidor localControl operativo.GPU, mantenimiento, scheduling, monitorización.
GPU alquiladaCapacidad temporal.Gestión de imágenes, datos, apagado y coste/hora.
CuantizaciónMenos memoria y coste.Posible pérdida de calidad y estabilidad.

Caso cercano. Si una herramienta interna procesa expedientes sensibles y tiene pocos usuarios, local puede ser razonable. Si atiende miles de solicitudes variables y necesita el mejor modelo disponible, cloud puede ganar. Si la carga es estable y grande, quizá compensa un servidor propio. No hay respuesta universal: hay presupuesto y restricciones.

Vuelve a los capítulos 05 y 06 si: no puedes estimar memoria, latencia y coste antes de instalar nada.

6. Embeddings y búsqueda semántica

En el capítulo 07 convertimos texto en vectores. Un embedding no es una etiqueta. Es una posición en un espacio de dimensiones donde podemos medir cercanía.

Para recordar. La dimensión de un embedding no es “una idea humana”, sino un eje numérico aprendido. El significado aparece por la combinación de muchos ejes. Comparar embeddings permite buscar por parecido semántico, no solo por palabras exactas.

La similitud coseno sigue siendo la fórmula mental básica:

sim(q,d)=qdqd\operatorname{sim}(q,d)= \frac{q\cdot d}{\|q\|\|d\|}
SímboloQué significaLectura
qqVector de la consulta.Lo que pregunta la persona en números.
ddVector del documento.Un fragmento convertido en números.
qdq\cdot dProducto escalar.Cuánto apuntan en dirección parecida.
q\|q\|, d\|d\|Normas.Tamaño de cada vector.

FAISS mostró cómo buscar de forma eficiente entre miles de millones de vectores en GPU.6 HNSW es una estructura de grafos aproximados muy usada para búsqueda vectorial eficiente.7

Caso cercano. “Cómo pedir vacaciones” puede recuperar “procedimiento de ausencia laboral” aunque no comparta palabras. Eso es buenísimo. Pero “parecido” no significa “suficiente”: todavía hay que validar fecha, versión, permisos y respuesta.

Vuelve al capítulo 07 si: no puedes explicar qué es una dimensión y por qué dos textos cercanos pueden no responder la misma pregunta.

7. Bases vectoriales y búsqueda híbrida

En el capítulo 08 vimos que buscar no es solo guardar embeddings. Hay índices, filtros, metadatos, fragmentos, reranking y búsqueda híbrida.

Para recordar. BM25 sigue siendo fuerte para coincidencia léxica y términos exactos.8 Reciprocal Rank Fusion permite combinar rankings de distintas búsquedas sin convertir todo a una misma escala.9

PiezaQué aportaQué puede fallar
ChunkingDivide documentos recuperables.Cortar contexto necesario.
EmbeddingsBusca por significado.Perder números, códigos o nombres exactos.
BM25Busca por términos.No captar paráfrasis.
FiltrosAcotan por permisos, fecha, tipo.Filtrar demasiado o tarde.
RerankingReordena candidatos.Añadir latencia sin mejorar.
MetadatosDan contexto operativo.Estar incompletos o desactualizados.

Caso cercano. Si buscas “artículo 17.3” necesitas texto exacto. Si buscas “cómo se solicita una revisión”, necesitas semántica. Si necesitas ambas cosas, híbrida.

Vuelve al capítulo 08 si: no puedes diseñar un índice con texto, metadatos, filtros y estrategia de recuperación.

8. RAG básico: evidencia antes que respuesta

En el capítulo 09 construimos RAG: recuperar información externa, pasarla al modelo y responder con evidencia. RAG no es “hacer que el modelo sepa más”. Es darle contexto verificable en el momento de responder.

Para recordar. Un RAG mínimo necesita colección, limpieza, chunking, embeddings o búsqueda léxica, recuperación, contexto, generación, citas y abstención. El trabajo original de RAG combinó recuperación con generación para tareas intensivas en conocimiento.10

Caso cercano. Un asistente de normativa responde mejor si cita el documento vigente. Pero si recupera un párrafo antiguo, la redacción puede ser impecable y la respuesta falsa. RAG no elimina el problema: lo hace medible.

Vuelve al capítulo 09 si: no puedes explicar la diferencia entre entrenar conocimiento y recuperar evidencia.

9. Evaluar RAG y no fiarse de la demo

En el capítulo 10 dejamos claro que una demo que responde bien tres veces no es una evaluación. Hay que medir retrieval, contexto, groundedness, abstención, latencia, coste y regresiones.

Para recordar. Evaluar RAG tiene varias capas:

CapaPregunta
Recuperación¿Aparece la fuente correcta entre los candidatos?
Contexto¿Lo recuperado contiene evidencia suficiente?
Generación¿La respuesta se apoya en esa evidencia?
Abstención¿Sabe parar cuando no hay base?
Operación¿Cuánto cuesta y tarda en casos reales?

RAGAS popularizó métricas orientadas a RAG como faithfulness, answer relevancy, context precision y context recall.11

Caso cercano. Si tu sistema responde “no lo sé” ante una pregunta que sí tiene respuesta, falta recall. Si responde con seguridad sin evidencia, falta abstención o groundedness. Si acierta pero tarda doce segundos, falta operación.

Vuelve al capítulo 10 si: no puedes proponer un dataset de evaluación con preguntas, fuentes esperadas, respuestas aceptables y casos donde debe abstenerse.

10. Agentic RAG y GraphRAG: complicar con permiso

En el capítulo 11 vimos cuándo un RAG fijo se queda corto: preguntas compuestas, rutas de búsqueda, fuentes múltiples, relaciones globales o necesidad de comprobar si la evidencia basta.

Para recordar. Agentic RAG añade decisiones de flujo: descomponer, elegir fuente, recuperar de nuevo, revisar evidencia, usar herramientas. GraphRAG añade estructura de entidades y relaciones para responder preguntas locales o globales sobre corpus complejos. Microsoft GraphRAG propuso construir grafos de entidades y resúmenes de comunidades para preguntas de comprensión global.12

Si necesitas...Puede tener sentido
Comparar varias fuentesQuery decomposition o multi-query.
Buscar con distintas estrategiasRouter controlado.
Entender relaciones entre entidadesGraphRAG o grafo de conocimiento.
Revisar evidencia insuficienteCorrective RAG o validación previa.
Limitar costePresupuesto de pasos y trazas.

Caso cercano. “Qué departamentos aparecen conectados a quejas sobre becas y retrasos de pago” no se resuelve igual que “cuál es el plazo de becas”. La primera pide patrones y relaciones. La segunda pide recuperar una fuente concreta.

Vuelve al capítulo 11 si: no puedes explicar qué complejidad pagas cuando añades pasos agentic.

11. Text-to-SQL y herramientas de datos

En el capítulo 12 cruzamos otra frontera: preguntas que no se contestan con documentos, sino con datos. Ahí no basta con recuperar párrafos. Hay que consultar tablas, esquemas, permisos y métricas.

Para recordar. Text-to-SQL traduce una pregunta humana a SQL controlado. No es acceso libre a la base de datos. Es una cadena: intención, esquema, semántica, SQL candidato, validación, plan, ejecución limitada, resultado y traza. Spider ayudó a medir Text-to-SQL con bases nuevas y consultas complejas.13

PreguntaRiesgo si lo simplificas
“Alumnos con pagos pendientes”Contar pagos en vez de alumnos.
“Ingresos de marzo”Usar fecha de creación en vez de fecha de pago.
“Top campus”No definir si top es por alumnos, importe o incidencias.
“Datos por titulación”Exponer columnas innecesarias.

Caso cercano. Un JOIN mal hecho puede duplicar filas y devolver una cifra convincente pero falsa. En Text-to-SQL, la sintaxis correcta es solo el comienzo. La semántica y la cardinalidad mandan.

Vuelve al capítulo 12 si: no puedes explicar por qué COUNT(*) y COUNT(DISTINCT alumno_id) responden preguntas distintas.

La matriz de decisión final

Esta matriz es una forma de hacer arquitectura sin postureo. Primero identifica el tipo de necesidad; después elige la herramienta más pequeña que cubre el caso.

Necesidad realPrimer intento razonableEscalaría a...Señal de que te estás pasando
Mejor tono o formatoPrompt + ejemplosStructured outputsCambiar pesos sin evaluar prompts.
JSON fiableSchema + validadorTool contractParsear texto libre.
Responder sobre documentosRAG básicoAgentic RAGUsar agente para una FAQ simple.
Encontrar documentosHíbrida + filtrosRerankingEmbeddings sin metadatos.
Consultar datos vivosTool o Text-to-SQLSemantic layerDejar SQL libre sin permisos.
Reducir costeModelo menor + cachéCuantización/localRecortar contexto sin medir calidad.
Controlar despliegueAPI cloud con contratosServidor propioMontar GPUs sin carga estable.
Dominio repetidoPlantillas + evalsLoRA/fine-tuningAjustar pesos para conocimiento cambiante.
Relaciones globalesRAG + metadatosGraphRAGConstruir grafo sin preguntas de competencia.

La matriz no decide por ti. Te obliga a decir qué estás comprando con cada capa nueva. Esa frase, “qué estoy comprando”, es una de las mejores defensas contra arquitecturas hinchadas.

Mini casos con solución

Una buena recapitulación no solo pregunta “¿lo entendiste?”. Te pone delante situaciones pequeñas y obliga a decidir. Estas son decisiones de bolsillo, pero condensan casi todo el facsímil.

CasoDecisión razonablePor qué
Una FAQ interna cambia cada mes y el asistente responde con información antigua.RAG básico con fuentes versionadas, citas y evaluación.El problema es conocimiento cambiante, no estilo del modelo.
Un clasificador de tickets devuelve texto libre y rompe el flujo de soporte.Structured outputs con catálogo cerrado y validador.El fallo está en el contrato de salida, no en saber más contenido.
Una herramienta debe decir cuántos pagos pendientes hay por campus.Tool de datos o Text-to-SQL con permisos y métricas definidas.La respuesta vive en tablas, no en documentos parecidos.
Un RAG recupera fragmentos relacionados pero no la norma exacta.Búsqueda híbrida, filtros por fecha/tipo y quizá reranking.El problema está en retrieval, no en generación.
El sistema acierta pero tarda demasiado y cuesta demasiado por consulta.Reducir contexto, cachear prefijos, probar modelo menor o cuantización.El cuello está en presupuesto operativo.
El equipo quiere fine-tuning para añadir una política que cambia cada semana.No ajustar pesos; usar RAG o tool.El conocimiento vivo debe vivir fuera del modelo.
La pregunta exige comparar documentos, entidades y patrones de varias fuentes.Agentic RAG controlado o GraphRAG si las relaciones importan.La complejidad se justifica si la pregunta necesita pasos o estructura.

Ahora lo mismo, pero con tres decisiones desarrolladas.

Caso 1: “Tenemos una FAQ interna que cambia cada mes”.
No empezaría por fine-tuning. Cambiar pesos para información viva suele crear deuda: cada cambio exige nuevo ajuste, evaluación y despliegue. Empezaría por RAG básico: documentos versionados, chunking razonable, búsqueda híbrida si hay códigos o artículos, citas visibles y casos donde el sistema debe abstenerse. La métrica mínima sería: ¿recupera la fuente vigente?, ¿cita bien?, ¿responde solo con esa evidencia?

Caso 2: “Queremos consultar pagos pendientes”.
No usaría RAG como primera opción. Un documento puede explicar qué es un pago pendiente, pero el número está en una base de datos. Haría una tool o Text-to-SQL controlado: rol de usuario, tablas permitidas, métrica definida, SQL validado, límite de filas, query plan y traza. La pregunta clave sería: ¿estoy contando pagos, alumnos o expedientes?

Caso 3: “El RAG responde bonito pero cita mal”.
No tocaría temperatura ni modelo todavía. Mediría recuperación antes: recall de fuentes esperadas, precisión de contexto, frescura de documentos, metadatos, filtros y reranking. Si la evidencia correcta no entra en el contexto, el generador no puede arreglarlo de forma fiable. Si la evidencia entra pero la respuesta no se apoya en ella, entonces revisaría prompt, formato de citas y evaluación de groundedness.

La idea de estos casos no es memorizar respuestas. Es aprender el gesto mental: diagnosticar la pieza que falla antes de cambiar la herramienta.

Cuándo no usar IA

Una caja de herramientas seria también incluye la opción de no sacar ninguna herramienta de IA. Hay problemas que se resuelven mejor con una regla, una consulta, un formulario, una validación clásica o una interfaz más clara.

SituaciónMejor primera opciónPor qué
Categorías cerradas y reglas simples.if, reglas o tabla de decisión.Más barato, explicable y determinista.
Cálculo exacto.Código o SQL.Un modelo generativo no debería inventar aritmética.
Flujo con permisos estrictos.API con roles y validación.El permiso no debe depender de texto.
Información ya estructurada.Consulta directa o dashboard.No hace falta traducir a lenguaje natural si el informe basta.
Tarea muy sensible y sin evaluación.Esperar, medir o rediseñar.No se despliega lo que no se puede comprobar.
Problema mal definido.Taller de requisitos.La IA no arregla una pregunta que nadie entiende.

Esto no va contra la IA. Va a favor de usarla bien. A veces la decisión más profesional es decir: aquí no hace falta un modelo; hace falta una regla clara, una tabla limpia o una conversación de producto.

El cálculo que deberías hacer antes de construir

Antes de escribir código, puedes estimar si una solución tiene sentido.

Ejemplo de fórmula. Una cuenta cualitativa para discusión técnica es:

U=Q(Ctokens+Clatencia+Cmantenimiento+Criesgo)U = Q - (C_{\text{tokens}} + C_{\text{latencia}} + C_{\text{mantenimiento}} + C_{\text{riesgo}})
SímboloQué significaCómo se observa
UUUtilidad neta de la solución.Valor práctico después de costes.
QQCalidad útil para la tarea.Aciertos, groundedness, satisfacción, ahorro real.
CtokensC_{\text{tokens}}Coste de entrada y salida.Tokens, caché, modelo elegido.
ClatenciaC_{\text{latencia}}Tiempo que tarda.TTFT, P95, tiempo total.
CmantenimientoC_{\text{mantenimiento}}Trabajo de sostenerlo.Datos, índices, evals, versiones.
CriesgoC_{\text{riesgo}}Impacto de fallos.Permisos, datos sensibles, decisiones críticas.

No es una fórmula para sacar decimales. Es una disciplina mental. Si una herramienta sube QQ un poco pero dispara mantenimiento y latencia, quizá no compensa. Si baja coste sin destruir calidad, merece atención. Si reduce riesgo aunque cueste algo más, puede ser la decisión correcta.

Manos a la obra

Kit ejecutable y descargable: kit descargable. Ejecuta python3 ops/run_f4_practices.py --all --write --fail-on-invalid para correr todas las prácticas del facsímil, o python3 ops/run_f4_practices.py --chapter c01 --write --fail-on-invalid cambiando c01 por el capítulo que quieras aislar.

Vamos a construir una pequeña rúbrica ejecutable. La idea es puntuar una propuesta de sistema antes de enamorarnos de ella. No sustituye a una evaluación real, pero obliga a mirar las piezas que este facsímil nos ha enseñado.

from dataclasses import dataclass


@dataclass
class PropuestaIA:
    nombre: str
    problema_definido: bool
    contrato_salida: bool
    evidencia_verificable: bool
    permisos_explicitos: bool
    coste_estimado: bool
    latencia_estimable: bool
    evaluacion_offline: bool
    trazas: bool
    mantenimiento_asignado: bool
    complejidad_justificada: bool


PESOS = {
    "problema_definido": 2,
    "contrato_salida": 2,
    "evidencia_verificable": 2,
    "permisos_explicitos": 2,
    "coste_estimado": 1,
    "latencia_estimable": 1,
    "evaluacion_offline": 2,
    "trazas": 2,
    "mantenimiento_asignado": 1,
    "complejidad_justificada": 2,
}


def revisar(propuesta):
    total = sum(PESOS.values())
    puntos = 0
    pendientes = []

    for campo, peso in PESOS.items():
        if getattr(propuesta, campo):
            puntos += peso
        else:
            pendientes.append(campo)

    ratio = puntos / total
    if ratio >= 0.85:
        decision = "puede pasar a prototipo controlado"
    elif ratio >= 0.65:
        decision = "necesita cerrar huecos antes de prototipar"
    else:
        decision = "todavía es una demo, no una arquitectura"

    return {
        "nombre": propuesta.nombre,
        "puntos": puntos,
        "total": total,
        "ratio": round(ratio, 2),
        "decision": decision,
        "pendientes": pendientes,
    }


casos = [
    PropuestaIA(
        nombre="FAQ con RAG básico",
        problema_definido=True,
        contrato_salida=True,
        evidencia_verificable=True,
        permisos_explicitos=True,
        coste_estimado=True,
        latencia_estimable=True,
        evaluacion_offline=True,
        trazas=True,
        mantenimiento_asignado=True,
        complejidad_justificada=True,
    ),
    PropuestaIA(
        nombre="Agente que consulta todo",
        problema_definido=False,
        contrato_salida=False,
        evidencia_verificable=True,
        permisos_explicitos=False,
        coste_estimado=False,
        latencia_estimable=False,
        evaluacion_offline=False,
        trazas=True,
        mantenimiento_asignado=False,
        complejidad_justificada=False,
    ),
]


for caso in casos:
    print(revisar(caso))

Salida esperada:

{
  'nombre': 'FAQ con RAG básico',
  'puntos': 17,
  'total': 17,
  'ratio': 1.0,
  'decision': 'puede pasar a prototipo controlado',
  'pendientes': []
}
{
  'nombre': 'Agente que consulta todo',
  'puntos': 4,
  'total': 17,
  'ratio': 0.24,
  'decision': 'todavía es una demo, no una arquitectura',
  'pendientes': [...]
}

Prueba tres variaciones:

  • Añade una propuesta de Text-to-SQL y decide qué campos deberían ser obligatorios.
  • Cambia pesos: haz que permisos y trazas valgan más en sistemas con datos sensibles.
  • Añade un campo plan_de_retroceso: qué hace el sistema cuando no hay evidencia suficiente.

Del laboratorio al siguiente facsímil

El cierre natural de este facsímil no es “ya sabemos usar herramientas”. Es más preciso: ya sabemos poner herramientas bajo control.

El laboratorio del capítulo 13 debería servir para demostrarlo con algo concreto: una evaluación reproducible, una traza que explique qué ocurrió, una decisión de arquitectura defendible y una salida que no dependa de buena suerte. Si una solución no deja rastro, no tiene contrato y no se puede evaluar, todavía pertenece al terreno de la demo.

El facsímil 05 dará el siguiente paso: agentes y orquestación. Ahí las herramientas dejan de ser piezas aisladas y empiezan a coordinarse. Por eso este cierre insiste tanto en límites, permisos, trazas, costes y abstención. Un agente sin esas piezas no es más capaz: solo es más difícil de depurar.

Laboratorio

El laboratorio operativo de este facsímil está en el capítulo 13 y se descarga desde kit descargable. Esta recapitulación no lo duplica: lo cierra. Si has hecho los retos, deberías tener artefactos concretos: rag_eval_report.json, rag_traces.jsonl, router_eval_report.json, router_traces.jsonl, decisiones Markdown y gates de CI.

Además, las prácticas cortas de capítulo están agrupadas en kit descargable. Úsalas como banco de comprobación rápido antes de volver al laboratorio largo: intervención correcta, payload de API, presupuesto de tokens, model card, modelo local, cloud/local, embeddings, índice híbrido, mini RAG, eval RAG, Agentic RAG, Text-to-SQL y rúbrica de arquitectura.

La regla editorial queda así: el laboratorio final prueba el sistema de extremo a extremo; las prácticas de capítulo prueban una pieza aislada. Las dos cosas hacen falta. Sin pieza aislada no sabes depurar; sin laboratorio no sabes integrar.

Cómo encaja todo

graph TD
    subgraph "Facsímil 04: caja de herramientas"
        PROBLEMA["Problema definido"]
        INTERVENCION["Intervención correcta"]
        API["API y contrato"]
        TOKENS["Tokens y presupuesto"]
        MODELO["Modelo y model card"]
        LOCAL["Cloud, local y cuantización"]
        EMB["Embeddings"]
        VECTOR["Índices y búsqueda híbrida"]
        RAG["RAG básico"]
        EVAL["Evaluación"]
        AGENTIC["Agentic RAG y GraphRAG"]
        SQL["Text-to-SQL"]
        TRAZA["Trazabilidad"]
        NOIA["No usar IA"]
        LAB["Laboratorio (F4C13)"]
        CRITERIO["Criterio de arquitectura"]
    end
    subgraph "Viene de antes"
        FUND["Tokens y embeddings (F1)"]
        TRANS["Transformer y sampling (F3)"]
        CSP["Restricciones y planificación (F2)"]
        ONTO["Grafos y ontologías (F2)"]
    end
    subgraph "Sigue después"
        AGENTES["Agentes y orquestación (F5)"]
        OPERAR["Construir y operar (F6)"]
        EVALUAR["Evaluar e interpretar (F7)"]
        DATOS["Ciencia de datos (F8)"]
    end

    FUND -->|"preparar"| EMB
    TRANS -->|"explicar coste de"| TOKENS
    CSP -->|"inspirar límites"| API
    ONTO -->|"anticipar relaciones"| AGENTIC
    PROBLEMA -->|"determinar"| INTERVENCION
    INTERVENCION -->|"declarar"| API
    API -->|"consumir"| TOKENS
    TOKENS -->|"condicionar"| MODELO
    MODELO -->|"desplegar en"| LOCAL
    EMB -->|"alimentar"| VECTOR
    VECTOR -->|"sostener"| RAG
    RAG -->|"necesitar"| EVAL
    EVAL -->|"decidir si compensa"| AGENTIC
    AGENTIC -->|"consultar relaciones"| SQL
    SQL -->|"producir datos con"| TRAZA
    PROBLEMA -->|"puede concluir en"| NOIA
    TRAZA -->|"cerrar"| CRITERIO
    CRITERIO -->|"se practica en"| LAB
    CRITERIO -->|"preparar"| AGENTES
    TRAZA -->|"preparar"| OPERAR
    EVAL -->|"preparar"| EVALUAR
    SQL -->|"conectar con"| DATOS

    style PROBLEMA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style INTERVENCION fill:#F5F5F5,stroke:#000000,stroke-width:2
    style API fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TOKENS fill:#F5F5F5,stroke:#000000,stroke-width:2
    style MODELO fill:#F5F5F5,stroke:#000000,stroke-width:2
    style LOCAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EMB fill:#F5F5F5,stroke:#000000,stroke-width:2
    style VECTOR fill:#F5F5F5,stroke:#000000,stroke-width:2
    style RAG fill:#F5F5F5,stroke:#000000,stroke-width:2
    style EVAL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style AGENTIC fill:#F5F5F5,stroke:#000000,stroke-width:2
    style SQL fill:#F5F5F5,stroke:#000000,stroke-width:2
    style TRAZA fill:#111111,stroke:#000000,stroke-width:2,color:#FFFFFF
    style NOIA fill:#F5F5F5,stroke:#000000,stroke-width:2
    style LAB fill:#F5F5F5,stroke:#000000,stroke-width:2
    style CRITERIO fill:#F5F5F5,stroke:#000000,stroke-width:2
    style FUND stroke-dasharray: 5 5
    style TRANS stroke-dasharray: 5 5
    style CSP stroke-dasharray: 5 5
    style ONTO stroke-dasharray: 5 5
    style AGENTES stroke-dasharray: 5 5
    style OPERAR stroke-dasharray: 5 5
    style EVALUAR stroke-dasharray: 5 5
    style DATOS stroke-dasharray: 5 5

Vocabulario aprendido

TérminoDefinición
IntervenciónCambio concreto sobre entrada, contexto, herramienta, pesos o infraestructura.
ContratoEspecificación de entrada, salida, límites, errores y trazas.
Superficie de controlLugar del sistema donde puedes limitar, validar o medir.
Presupuesto operativoLímite de coste, tokens, latencia, memoria, pasos o consultas.
EvidenciaInformación recuperada o calculada que sostiene una respuesta.
AbstenciónDecisión de no responder cuando no hay base suficiente.
TrazabilidadCapacidad de reconstruir cómo se produjo una respuesta.
Evaluación offlinePruebas con casos preparados antes de uso real.
Evaluación onlineMedición con uso real, tráfico y cambios.
Arquitectura mínima suficienteDiseño más sencillo que resuelve el problema medido.
Decisión sin IAElección de resolver con reglas, SQL, interfaz o proceso cuando un modelo no aporta valor suficiente.

Dónde solía tropezar yo

ErrorPor qué es un errorAntídoto
Empezar por la herramienta“Metamos RAG” no dice qué fallo estás resolviendo.Escribir primero qué cambia: entrada, contexto, herramienta, pesos o despliegue.
Confundir demo con sistemaUna respuesta buena no prueba permisos, coste, latencia ni regresiones.Exigir dataset mínimo, trazas y casos donde debe abstenerse.
Meter más contexto sin medirPuede subir coste y ruido sin mejorar groundedness.Medir recall, precisión de contexto y calidad de respuesta por separado.
Complicar antes de cerrar lo básicoAgentic RAG o GraphRAG pueden esconder problemas de chunking, filtros o definición.Probar primero el flujo más simple que pueda evaluarse.
Olvidar mantenimientoÍndices, prompts, modelos, esquemas y datos cambian.Nombrar propietario, versión y prueba de regresión de cada pieza.
No diseñar salida de falloEl sistema acaba respondiendo incluso cuando no sabe.Definir abstención, aclaración y escalado antes de producción.
No contemplar la opción sin IAAlgunas tareas se resuelven mejor con reglas, SQL o una interfaz más clara.Preguntar siempre qué gana el modelo frente a una solución clásica.

Antes de pasar página

Responde sin consultar los capítulos. Si fallas una pregunta, el número te dice dónde volver.

  • 1. ¿Puedo elegir entre prompt, RAG, tool y ajuste explicando qué cambia cada uno?
  • 2. ¿Sé diseñar una llamada API con mensajes, parámetros, streaming, tool y salida estructurada?
  • 3. ¿Puedo estimar coste de tokens, contexto y caché antes de construir?
  • 4. ¿Sé leer una model card y separar marketing, métrica y restricción operativa?
  • 5. ¿Puedo explicar cuándo usar local, cloud, GPU alquilada o cuantización?
  • 6. ¿Sé qué es una dimensión de embedding y qué mide la similitud coseno?
  • 7. ¿Puedo diseñar búsqueda híbrida con filtros y metadatos?
  • 8. ¿Sé explicar por qué RAG no es memoria ni entrenamiento?
  • 9. ¿Puedo proponer métricas de evaluación RAG y casos donde el sistema debe abstenerse?
  • 10. ¿Sé cuándo Agentic RAG o GraphRAG compensan su complejidad?
  • 11. ¿Puedo diseñar una herramienta Text-to-SQL con permisos, validación y traza?
  • 12. ¿Puedo revisar una arquitectura y decir qué falta para llevarla a prototipo controlado?
  • 13. ¿Puedo defender cuándo no usar IA?
  • 14. ¿Puedo explicar cómo el laboratorio del capítulo 13 prueba este criterio?

En resumen

Idea fuerzaDetalle
La caja de herramientas empieza con diagnóstico.Antes de elegir técnica, hay que saber qué parte del sistema falla.
Un modelo útil necesita contratos alrededor.APIs, schemas, herramientas, validadores y trazas hacen gobernable la respuesta.
La evidencia se recupera, se mide y se cita.RAG, búsqueda híbrida, GraphRAG y Text-to-SQL son formas distintas de traer base verificable.
El coste también es arquitectura.Tokens, contexto, caché, latencia, GPU, local y cloud cambian el diseño.
La evaluación separa demo de sistema.Sin casos, métricas, abstención y regresiones, no sabes si mejoraste.
La complejidad tiene que ganarse el sitio.Agentic RAG, GraphRAG o fine-tuning solo compensan cuando resuelven un fallo medido.
No usar IA también es una decisión técnica.Si una regla, SQL o una interfaz clara resuelve mejor, esa es la herramienta correcta.
El siguiente paso es coordinar herramientas.El facsímil 05 parte de esta base para hablar de agentes y orquestación.

Para saber más

Cormack, G. V., Clarke, C. L. A. y Büttcher, S. (2009). Reciprocal rank fusion outperforms Condorcet and individual rank learning methods. Proceedings of SIGIR, 758-759. DOI

Edge, D. et al. (2024). From Local to Global: A Graph RAG Approach to Query-Focused Summarization. arXiv

Es, S. et al. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. arXiv

Hu, E. J. et al. (2022). LoRA: Low-Rank Adaptation of Large Language Models. arXiv

Johnson, J., Douze, M. y Jégou, H. (2019). Billion-scale similarity search with GPUs. IEEE Transactions on Big Data, 7(3), 535-547. DOI

Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474. NeurIPS

Malkov, Y. A. y Yashunin, D. A. (2020). Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs. IEEE Transactions on Pattern Analysis and Machine Intelligence, 42(4), 824-836. DOI

Mitchell, M. et al. (2019). Model Cards for Model Reporting. Proceedings of FAT 2019, 220-229. DOI

OpenAI. (2026). Function calling. Documentación oficial

OpenAI. (2026). Prompt caching. Documentación oficial

OpenAI. (2026). Structured model outputs. Documentación oficial

Robertson, S. y Zaragoza, H. (2009). The probabilistic relevance framework: BM25 and beyond. Foundations and Trends in Information Retrieval, 3(4), 333-389. DOI

Yu, T. et al. (2018). Spider: A Large-Scale Human-Labeled Dataset for Complex and Cross-Domain Semantic Parsing and Text-to-SQL Task. Proceedings of EMNLP, 3911-3921. ACL Anthology

Notas

  1. OpenAI. (2026). Function calling. Documentación oficial. Consultado el 26 de mayo de 2026.

  2. OpenAI. (2026). Structured model outputs. Documentación oficial. Consultado el 26 de mayo de 2026.

  3. Hu, E. J. et al. (2022). LoRA: Low-Rank Adaptation of Large Language Models. International Conference on Learning Representations. arXiv. Dettmers, T. et al. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. Advances in Neural Information Processing Systems 36. arXiv.

  4. OpenAI. (2026). Prompt caching. Documentación oficial. Consultado el 26 de mayo de 2026.

  5. Mitchell, M. et al. (2019). Model Cards for Model Reporting. Proceedings of FAT 2019, 220-229. DOI.

  6. Johnson, J., Douze, M. y Jégou, H. (2019). Billion-scale similarity search with GPUs. IEEE Transactions on Big Data, 7(3), 535-547. DOI.

  7. Malkov, Y. A. y Yashunin, D. A. (2020). Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs. IEEE Transactions on Pattern Analysis and Machine Intelligence, 42(4), 824-836. DOI.

  8. Robertson, S. y Zaragoza, H. (2009). The probabilistic relevance framework: BM25 and beyond. Foundations and Trends in Information Retrieval, 3(4), 333-389. DOI.

  9. Cormack, G. V., Clarke, C. L. A. y Büttcher, S. (2009). Reciprocal rank fusion outperforms Condorcet and individual rank learning methods. Proceedings of SIGIR, 758-759. DOI.

  10. Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474. NeurIPS.

  11. Es, S. et al. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. arXiv.

  12. Edge, D. et al. (2024). From Local to Global: A Graph RAG Approach to Query-Focused Summarization. arXiv.

  13. Yu, T. et al. (2018). Spider: A Large-Scale Human-Labeled Dataset for Complex and Cross-Domain Semantic Parsing and Text-to-SQL Task. Proceedings of EMNLP, 3911-3921. ACL Anthology.