Libro 15 · Artefacto 6

Artefacto 6 · Monitorización · Fases 0–2

El servicio a oscuras

El agente llevaba semanas desplegado, con su token, su contenedor y su URL. Funcionaba. Y sin embargo, a dos preguntas de negocio elementales —¿cuánto cuesta cada petición? ¿cuántas fallan?— no había forma de responder. Este libro es el acto de encender la luz.

22 de julio de 2026 · agente de búsqueda de empleo · 2 ejecuciones reales · medio céntimo

01Qué falta cuando todo funciona

El último artefacto del roadmap se llama «despliegue en producción real», y su primer paso —Docker, API, nube— ya estaba hecho dos veces antes de empezar: el RAG y el agente viven en Cloud Run desde hace semanas. Lo que en junio parecía la montaña del artefacto resultó ser un sello ya estampado.

Lo que quedaba es más sutil y vale más. La definición de «hecho» de la bitácora no pide que el servicio corra: pide poder decir cuánto cuesta cada petición y cuántas fallan. Eso tiene nombre propio: observabilidad — la diferencia entre un sistema que funciona y un sistema del que puedes afirmar cosas.

Un servicio sin métricas no está roto. Está a oscuras, que es peor: no sabrías decir si está roto.

Y hay una segunda razón, específica de esta disciplina: latencia y tasa de errores son la observabilidad clásica de cualquier API, pero tokens y coste por petición son la métrica propia de los sistemas LLM. En una empresa real, la primera pregunta sobre un producto con modelo dentro es siempre la misma: ¿cuánto nos cuesta esto por usuario? Saber responderla con un número es lo que distingue «desplegué un LLM» de «operé un LLM».

02Los datos que se tiraban

La sorpresa de la primera inspección: no había que pedir ningún dato nuevo.

Cada respuesta de la API de OpenAI trae un objeto usage con los tokens exactos que costó esa llamada — cuántos leyó el modelo (prompt_tokens) y cuántos escribió (completion_tokens). Llega en todas las respuestas, desde el primer día del agente. Nunca lo habíamos mirado.

La idea que se lleva uno La monitorización no empieza instalando nada. Empieza dejando de ignorar los datos que ya pasan por delante. El contador de la luz ya estaba girando; solo faltaba leerlo.

Y el precio de esos tokens es público: tanto por millón de entrada, tanto por millón de salida. Generar cuesta unas cuatro veces más que leer — para gpt-4o-mini, $0,15 el millón leído y $0,60 el millón escrito. Con la tabla de precios y el usage, el coste de una llamada es una multiplicación.

03Una petición no es una llamada

Aquí estaba la trampa que habría hecho mentir al contador desde el primer día.

El bucle del agente (libro 01) da hasta seis vueltas, y cada vuelta es una llamada al modelo. Pero las herramientas que el bucle ejecuta —analizar la oferta, calcular el encaje, redactar la carta— también llaman al modelo por dentro, a través del embudo de _parsear() (libro 11). Una candidatura completa resultó ser:

Quién gastaLlamadasQué hace
El bucle4orquestar: decidir qué herramienta toca
Las herramientas3trabajar: extraer, encajar, redactar
Total7una petición del usuario

Si el medidor hubiera vivido solo en el bucle, habría contado 4 llamadas de 7 y declarado la mitad del gasto real. Un contador que existe y está mal es peor que ninguno: da confianza sin dar verdad.

04El carril de cada petición

Si el gasto ocurre en dos ficheros distintos, hace falta un punto donde se sume. La tentación obvia es una variable global: total_tokens = 0, y que todos sumen ahí. En el portátil funcionaría siempre. En producción, no.

Cloud Run atiende varias peticiones a la vez dentro del mismo contenedor. Con un contador global, los tokens de la petición de un visitante se sumarían a los de otro que llegó medio segundo después: contaminación cruzada, y ningún error que te avise. La respuesta de Python a este problema es ContextVar: se usa como una global, pero cada petición ve su propio carril.

Un eco de los namespaces Es el mismo principio que el multitenancy de Pinecone del Artefacto 2: cuando dos inquilinos comparten un edificio, el aislamiento tiene que ser estructural —cada uno su partición— y no una convención que confía en que nadie se cruce.

De ahí nace medidor.py, un módulo de tres verbos: iniciar() abre el contador a cero al arrancar el bucle; registrar(usage) lo llama cada punto de gasto —una línea en el bucle, una línea en el embudo de las herramientas—; resumen(modelo) cierra la cuenta y le pone precio. Dos detalles de criterio:

05La cuenta de la primera ejecución

Con el medidor cableado, la primera candidatura real medida de la historia del agente:

completado : True · 4 turnos · analizar → match → redactar
latencia   : 24,7 s
llamadas   : 7 · entrada 8.500 tok · salida 1.874 tok
coste      : $0.0024

Tres lecturas, de menor a mayor:

Un cuarto de céntimo por candidatura. Podrías analizar cuatrocientas ofertas por un dólar. Tener este número concreto, medido y defendible es media definición de «hecho» del artefacto.

La entrada pesa 4,5 veces más que la salida. No es casualidad: es la consecuencia directa de algo que el libro 01 ya contaba — la API es sin estado, y el bucle reenvía toda la conversación acumulada en cada vuelta. El turno cuatro relee todo lo que pasó en los turnos uno, dos y tres.

La frase para la entrevista En sistemas agénticos, el coste dominante no es generar: es releer contexto. Por eso los proveedores inventaron el prompt caching —cobrar barato lo ya leído— y por eso entrada y salida se cobran a precio distinto.

El número se mueve con la carga real. La segunda ejecución, con una oferta más corta, dio $0.0021 con 7.628 tokens de entrada. El medidor distingue peticiones distintas — no repite una cifra fija, mide.

06Un print bien formado es telemetría

Medir dentro del motor resuelve la mitad del problema. La otra mitad: ¿dónde se guarda la cuenta? Y aquí vuelve un viejo conocido — Cloud Run escala a cero (libro 04). El contenedor muere entre visitas, y con él muere cualquier variable, lista o fichero local. Las métricas no pueden vivir en la aplicación: tienen que salir.

La doctrina estándar de los servicios en la nube (los «doce factores») lo resuelve con una elegancia que desarma: la aplicación no escribe ficheros de log ni se conecta a ningún sistema de métricas. Escupe una línea por evento a la salida estándar, y la plataforma se encarga del resto. El stdout —el sitio donde siempre imprimiste tus print() de andar por casa— es el canal de telemetría.

Con un matiz que lo cambia todo: si la línea es JSON válido, Cloud Logging no la guarda como texto — la indexa campo a campo. La diferencia se ve comparando:

# log de prosa: para leerlo a ojo, grep y llorar
Petición procesada en 24.7s, costó $0.0024

# log estructurado: una fila de una tabla consultable
{"estado": 200, "latencia_ms": 24700, "coste_usd": 0.0024}

El primero se lee; el segundo se consulta: «media de coste_usd donde estado=200», «cuántas líneas con estado≥500 esta semana». Las dos preguntas del artefacto son literalmente dos consultas sobre esta bitácora. ¿Y el patrón te suena? Es el historial.json de la báscula (libro 07): registro append-only, una fila por evento, campos fijos — la misma disciplina, pero por petición y gestionada por la plataforma.

El detalle del búfer Cada línea se emite con flush=True. Sin él, el print puede quedarse en el búfer del proceso — y si el contenedor muere ahí (escala a cero, otra vez), la línea se pierde. Normalmente justo la línea que querías ver, porque los contenedores mueren después de las peticiones raras, no de las normales.

07Los cuatro caminos de una petición

La función bitacora() vive en la API y escribe una línea por petición, pase lo que pase. El endpoint tiene cuatro salidas —token malo, oferta vacía, reventón, éxito— y las cuatro dejan constancia. Las líneas reales de la verificación en local:

{"severity": "WARNING", "estado": 401, "latencia_ms": 0, ...}
{"severity": "WARNING", "estado": 400, "latencia_ms": 0, ...}
{"severity": "INFO", "estado": 200, "latencia_ms": 25481,
 "turnos": 4, "completado": true, "llamadas_llm": 7,
 "tokens_entrada": 7628, "tokens_salida": 1596,
 "coste_usd": 0.002102}

La clave severity es un idioma que Cloud Logging entiende: INFO para el éxito, WARNING para los rechazos, ERROR para el reventón. «¿Cuántas fallan?» se responde filtrando por ella.

Pero lo más elocuente de esas líneas es lo que no está. Las dos fallidas: cero milisegundos y ningún campo de coste. No es un hueco — es la prueba de que el portero del token (libro 03) hace exactamente lo que prometía: rechaza antes de que el agente gire. Un desconocido probando tokens contra tu URL te cuesta $0,000, y ahora hay una bitácora que lo demuestra.

La ausencia es información En un log estructurado, que un campo no esté cuenta tanto como su valor. La línea del 401 sin coste_usd dice: «aquí la cartera ni se abrió».

El fallo que el código HTTP no cuenta

Queda un caso traicionero: el guardarraíl de max_turnos devuelve HTTP 200 —la petición se atendió— pero con completado: false, porque el agente agotó sus vueltas sin cerrar. Es un fallo blando: un monitor que solo mirase códigos HTTP diría «todo bien» mientras el agente da vueltas sin llegar a nada. Por eso completado viaja en cada línea de éxito: la bitácora registra la verdad del agente, no solo la del protocolo.

08Las palabras que se lleva el CV

TérminoLo que significa aquí
observabilitypoder afirmar cosas del sistema: coste, fallos, latencia — no solo «funciona»
token usage accountingleer el usage de cada llamada y sumarlo por petición, herramientas incluidas
cost per requestla métrica de negocio de un sistema LLM: $0.002/candidatura, medido, no estimado
request-scoped stateContextVar: un carril por petición, sin contaminación cruzada bajo concurrencia
structured logginguna línea JSON por evento a stdout; la plataforma la indexa campo a campo
twelve-factor logsla app no gestiona sus logs: los emite como flujo y la plataforma los captura
soft failureHTTP 200 con completado: false — el fallo que el código de estado no ve

09Lo que queda abierto

· · ·