El turno de noche: un Gemma local escribe mi informe de las 7 de la mañana, y medí dónde inventa
¿Qué pasó anoche en el laboratorio? Casi todas las mañanas la respuesta es nada, y saberlo igual obliga a abrir registros. Algunas mañanas es algo, y los registros son una forma lenta de enterarse.
Así que armé un turno de noche. Dos equipos pequeños mandan un latido toda la noche, unas reglas fijas revisan la noche a las 6:30 de la mañana y, antes de las 7, cada equipo deja un informe corto escrito por Gemma 3, corriendo en el propio equipo, sin internet y sin llave de ninguna API. Lo cómodo: nadie tiene que leer registros en la mañana. Lo cuidadoso: no le di ese trabajo al modelo hasta medir dónde se equivoca.
Qué construí
El laboratorio es una mini PC Intel N100 y una Raspberry Pi 5 en la misma red.
- Un latido. Cada 10 segundos cada equipo manda un registro pequeño: número de secuencia, hora de envío, hora de arranque de su proceso, carga de CPU, memoria disponible, temperatura y cuántos latidos no pudo enviar. Cada equipo guarda también su copia en disco, por si lo que falla es el enlace.
- Un colector en el N100 que guarda lo que llega, con la hora de llegada. Los dos equipos le mandan; el N100, a sí mismo.
- Un detector que corre una vez, a las 6:30 de la mañana, sobre toda la noche (de las 10 de la noche a las 6:30 de la mañana) con reglas fijas. Escribe un JSON con cada incidente: inicio, fin, duración, cuándo se detectó, si se recuperó solo y la evidencia.
- Un redactor. Gemma 3 4B en Ollama lee sólo ese JSON y escribe el informe de la mañana en Markdown. No decide si algo falló. Eso se decidió antes de que despertara.
Cada equipo escribe su propio informe con el mismo JSON: el N100 a las 6:30 y la Pi a las 6:40. Así puedo comparar una mini PC x86 y una placa ARM haciendo el mismo trabajo con el mismo modelo. Un hueco conocido: todo pasa por el N100, así que si el N100 está caído, no hay informe.
Demo
Esta es una parte del informe de la Pi después de una noche con ocho fallas provocadas, tal como lo escribió Gemma (con las líneas cortadas y dos de los ocho incidentes):
# Turno de noche: 2026-10-21 22:00
**Salud general:** mala. El 99.9% de los latidos recibidos provienen del
equipo N100, mientras que el 87.2% provienen del Raspberry Pi 5.
## Incidentes
* **N100:** servicio reiniciado, 22:39:51 - 22:40:30, 39 s, detectado
22:40:21, no confirmado
Hechos observados: Ningún latido recibido de 22:39:51 a 22:40:30 (39 s;
se esperaba uno cada 10 s). El latido siguiente viene de un proceso
nuevo, arrancado a las 22:40:30. El vigilante del equipo registró la
caída del proceso a las 22:40:00 (código -15). / Posibles explicaciones:
Fallo en el sistema operativo. / Causa confirmada: no confirmada
* **Raspberry Pi 5:** el equipo no envió, 23:29:51 - 23:35:01, 310 s,
detectado 23:30:21, no confirmado
Hechos observados: Ningún latido recibido de 23:29:51 a 23:35:01 (310 s;
se esperaba uno cada 10 s). El mismo proceso antes y después; faltan 30
latidos en la secuencia. El propio equipo registró 30 latidos como no
enviados en ese lapso. / Posibles explicaciones: Problema de red. /
Causa confirmada: no confirmada
Los hechos observados están bien. Varias cosas alrededor de ellos están mal, y encontrarlas es de lo que trata esta nota.
Código
Todo lo que corrió está en github.com/israel0negrete-crtdrops/night-shift, con licencia MIT: latido, vigilante, intermediario, colector, detector, redactor, inyector de fallas, planes de fallas y evaluador, todo en Python de la biblioteca estándar más Ollama. Es el código tal como corrió estas noches, no una versión arreglada después: los archivos de los dos equipos son idénticos, byte por byte, a los del repositorio. Trae también los latidos crudos de las dos noches con fallas; al volver a correr el detector con ellos sale exactamente el mismo JSON, y el redactor se puede volver a correr con cualquiera de las noches.
Lo construí con Claude Code en pareja: yo decidí qué medir, a qué se podía someter el laboratorio y a qué no, y qué tenía que decir el informe; Claude escribió casi todo el código y lo corrió conmigo en los equipos.
Cómo lo construí
Por qué el modelo no lee los registros
La primera versión fue la obvia: darle a Gemma las últimas 40 advertencias del registro del N100 y pedirle dos líneas. Les di las mismas 40 a los dos equipos.
Los dos tardaron unos 2 minutos, escribieron un párrafo en vez de dos líneas y describieron el ruido de fondo del escritorio (GNOME, PipeWire, un llavero) como “inestabilidad del sistema”; uno recomendó investigarlo. En un equipo de escritorio la mayoría de las advertencias no son fallas, y el modelo no tiene forma de saber cuáles sí.
Así que el trabajo se partió. Las reglas deciden qué pasó; el modelo sólo lo pone en palabras. El detector tiene menos de 200 líneas. Y para saber si las reglas acertaban, necesitaba noches en las que supiera exactamente qué había fallado y cuándo.
Tres datos que hacen legible un hueco
El número de secuencia, la hora de arranque del proceso y la cuenta que lleva el propio equipo de lo que no pudo enviar. Una hora de arranque nueva después de un hueco quiere decir que el proceso se reinició. El mismo proceso con números de secuencia faltantes quiere decir que se perdieron latidos. Y si el propio equipo los contó como no enviados, nunca salieron; si no, salieron y no llegaron. Sin esos tres datos, todo esto se ve como el mismo hueco en la gráfica.
Noches con fallas conocidas
Primero dos noches tranquilas, sin fallas, como base. Luego dos noches con ocho fallas controladas cada una, provocadas por un programa a horas fijas y registradas con su inicio y fin reales; la segunda noche cambió cada falla al otro equipo y a otra hora:
| Falla | Qué hace | Qué debe decir el detector |
|---|---|---|
| Interrupción | mata el proceso del latido; un vigilante lo reinicia 30 s después | servicio reiniciado |
| Silencio | el proceso sigue vivo pero deja de enviar | el equipo no envió |
| Corte | el intermediario tira los latidos de ese equipo | latidos perdidos en el camino |
| Latencia | el intermediario los retrasa 3–5 s | latencia |
| CPU | cuatro procesos ocupados en cuatro núcleos | saturación de CPU |
| Memoria | ocupa memoria hasta dejar 15 % | memoria baja |
| Pausa | detiene el proceso hasta después de que cierra la ventana | sin latido, sin resolver |
Casi todas las fallas tocan sólo el flujo experimental. CPU y memoria cargan al propio equipo, con límite: la de CPU se corta a cierta temperatura y la de memoria nunca deja menos de 600 MB. Nada de reinicios, ningún cambio a la red real de los equipos, ningún servicio productivo detenido.
Un evaluador para el redactor
Un programa compara cada informe con el JSON que recibió. Busca incidentes omitidos, horas y cifras que no están en el JSON, causas dadas por confirmadas, calificativos como “temperatura elevada” sin un incidente que los respalde, incidentes inventados en una noche tranquila, y una comparación con la noche anterior que falta o que es inventada. El registro real de las fallas califica al detector, no al informe.
El evaluador corre en mi laptop, no en el laboratorio, y se le escapan cosas. Casi todo lo que está mal en la demo de arriba lo encontré leyendo los informes.
Resultados: la detección
| Noche | Fallas provocadas | Detectadas | Falsas alarmas | Retraso de detección |
|---|---|---|---|---|
| Tranquila 1 | 0 | 0 | 0 | — |
| Tranquila 2 | 0 | 0 | 0 | — |
| Con fallas, noche 1 | 8 | 8 | 0 | 21–30 s; CPU 111 s |
| Con fallas, noche 2 | 8 | 8 | 0 | 20–30 s; CPU 112 s |
En las noches tranquilas llegaron 3,060 de 3,060 latidos de cada equipo. Con fallas, las reglas encontraron las dieciséis, cada una con el tipo correcto. La última falla de cada noche, una pausa que sigue después de las 6:30, salió como sin resolver, que es lo correcto.
La segunda noche además tuvo un incidente que nadie provocó. La falla de CPU llevó al N100 a 84 °C, arriba de la regla de 80 °C, durante 50 latidos, y el detector reportó temperatura alta. Mi evaluador lo contó como falsa alarma porque no estaba en el plan, pero la temperatura de verdad pasó el límite: es un efecto de la falla, y la regla hizo bien en atraparlo.
El detector corre una sola vez, a las 6:30, así que “detectado” es cuándo se habría disparado la regla, reconstruido con los datos, no una alerta en vivo. La falla de CPU aparece tarde porque la carga promedio de un minuto necesita como minuto y medio para pasar de 3.0 (75 % de cuatro núcleos), más 20 segundos de los tres latidos seguidos que pide la regla.
La detección es aburrida, y así debe ser.
Resultados: lo que escribió Gemma
En la primera noche tranquila, los dos equipos inventaron incidentes. El JSON decía cero incidentes. Cada informe listó uno por equipo de todos modos, armado con la temperatura máxima de ese equipo (73 °C en el N100, 50.1 °C en la Pi, sin regla disparada), les puso una duración de “00:30” que no aparece en ningún dato y recomendó revisar la temperatura o la ventilación. Una noche tranquila es justo cuando un resumidor siente la tentación de encontrar algo que decir.
En la segunda noche tranquila, la Pi escribió bien que no hubo incidentes, pero dejó fuera la comparación con la noche anterior, que venía en el JSON. El N100 sí incluyó la comparación, pero puso lecturas normales bajo “Incidentes”. Y los dos pusieron en el título el inicio de la noche (2026-10-20 22:00) en vez de la fecha de la mañana que pide la plantilla.
En la primera noche con fallas, acertó la estructura y casi todas las cifras. Ningún equipo omitió un incidente ni dio una causa por confirmada, y el evaluador no encontró horas ni cifras que no estén en el JSON. Pero el evaluador sólo busca números que no aparezcan en ninguna parte del JSON, y así se coló el peor error:
- La Pi inventó un hecho con una cifra real. En el último incidente, la pausa que seguía abierta a las 6:30, escribió “El propio equipo registró 30 latidos como no enviados en ese lapso”. Esa frase es del incidente de silencio, seis horas antes. El JSON le daba a este incidente dos hechos, y la Pi tiró uno: que al cierre de la ventana seguía sin llegar ningún latido. Ni siquiera podía ser cierto, porque la pausa mata el proceso del latido, así que nada estaba contando. El 30 pasó el evaluador porque el 30 aparece en otra parte del JSON.
- La línea de salud lee mal las cifras. 99.9 % y 87.2 % son la parte de los latidos de cada equipo que llegó, no de dónde vienen los latidos.
- Las posibles explicaciones contradicen la evidencia. La Pi registró sus 30 latidos como no enviados, así que nunca llegaron a la red; la causa real fue la falla de silencio, y “problema de red” lo descartan los hechos dos líneas arriba. El proceso del N100 se detuvo con una señal de terminación normal (código -15), que no es un fallo del sistema operativo.
- “No confirmado” donde la plantilla pregunta si se recuperó solo. El JSON decía que sí.
El evaluador sí atrapó una: la Pi volvió a saltarse la comparación, y esta vez también el N100; escribieron que no había noche anterior para comparar, y el JSON traía sus cifras. Lo demás es menor: el N100 redujo cada incidente a una línea genérica (“El equipo N100 reinició el servicio.”) y tiró la evidencia, la Pi escribió “null” en el fin del incidente que seguía abierto a las 6:30, y los dos títulos volvieron a llevar el inicio de la noche.
En la segunda noche con fallas, los dos equipos cambiaron de equipo dos incidentes. La interrupción de las 22:29 fue de la Pi, y los dos informes la pusieron en el N100; la latencia de las 23:20 fue del N100, y los dos la pusieron en la Pi. El mismo error en x86 y en ARM, y el evaluador no lo puede ver, porque los nombres de los dos equipos están en el JSON. La línea de salud del N100 además le dio a la Pi 87.2 %, que era la cifra de la noche anterior, copiada del bloque de comparación; esa noche la Pi tuvo 98.5 %. Escribió 23:40:22 como hora de detección de un incidente detectado a las 00:40:22, y eso sí lo atrapó el evaluador. Y los dos se saltaron la comparación otra vez: tres noches seguidas en la Pi.
Mismo modelo, mismo JSON, mismas instrucciones, temperatura 0 y semilla 42 en los dos equipos, y aun así el texto salió distinto en cada uno. Con temperatura 0 la semilla casi no influye; lo que cambia es la aritmética. Las versiones para x86 y para ARM redondean un poco distinto, y un solo token distinto cambia todo lo que sigue. No probé repetir en el mismo equipo, así que no puedo decir que fijar el equipo baste; sólo que fijar el modelo no basta.
¿N100 o Pi 5?
| N100 | Raspberry Pi 5 | |
|---|---|---|
| Informe con 8 incidentes | 294.8 s | 468.8 s |
| Lectura de las instrucciones (2,565 tokens) | 20.7 tokens/s | 26.3 tokens/s |
| Escritura del informe | 5.7 tokens/s (934 tokens) | 3.3 tokens/s (1,199 tokens) |
| Informe en una noche tranquila | 86–128 s | 76–101 s |
En la segunda noche con fallas fueron 303.4 s en el N100 y 520.9 s en la Pi. La Pi lee más rápido y escribe más lento, y con fallas además escribe más, porque copia la evidencia que el N100 tira. Tres tokens por segundo suena lento, y a las 6:40 de la mañana no importa: nadie está esperando, y los dos informes están listos mucho antes de las 7. Para un turno de noche, despacio y constante alcanza.
¿Por qué importa que sea abierto?
- Los registros nunca salen del laboratorio. Ollama escucha en 127.0.0.1. Nada de mis equipos va al servidor de nadie más, y el turno de noche sigue trabajando aunque no haya internet.
- No cuesta nada cada noche. No hay tokens que pagar ni llave que rotar. Corre en equipos que ya estaban prendidos.
- Lo puedo fijar. El modelo es
gemma3:4b, id68ceab7e468a, Q4_K_M, con temperatura 0, semilla 42 y contexto de 8,192 tokens, en Ollama 0.40.6. Un modelo hospedado puede cambiar entre una noche y la siguiente; éste sólo cambia cuando yo lo cambio. Eso importa cuando lo que se quiere es medirlo. - Puedo correr el mismo modelo en x86 y en ARM y ver la diferencia, que es como encontré que la misma configuración no da el mismo texto en dos procesadores.
Para ser precisos con “abierto”: los pesos de Gemma son abiertos bajo los Términos de Uso de Gemma, no con una licencia OSI; Ollama es MIT. Lo que importa aquí es que los dos corren completos en mis equipos.
Lo que me ahorra
Nadie tiene que abrir registros en la mañana: se lee la línea de salud y, si no dice “buena”, los incidentes de abajo. La palabra de salud viene del JSON del detector; Gemma la copió bien todas las noches, pero eso es Gemma copiando, no una garantía, así que el siguiente cambio es que esa línea la escriba el detector.
Lo que no hago es confiar sólo en el informe. Las reglas deciden, yo corro el evaluador sobre lo que escribió el modelo, y sus explicaciones se toman como suposiciones, porque la noche con fallas enseñó que pueden contradecir los hechos que tienen justo arriba. Lleva cinco noches, no meses, y esto es lo que enseñaron cinco noches.
Israel Negrete Lepe es ingeniero en electrónica. Veinticinco años haciendo que sistemas distintos funcionen juntos en condiciones reales: del sensor, el vehículo o la transacción al backend, los datos, la seguridad y la operación. Hoy, cada vez más, IA en el borde.