← blog

Laya en el vigía: un modelo de decisión de 322M reemplaza al LLM, y lo que aprendí probándolo

Mi agente que vigila la infra decidía todo con un LLM, entre 10 y 40 segundos por decisión. Ahora decide Laya, un modelo de decisión tipada open de 322M en CPU, en menos de un segundo. Nueve experimentos pre-registrados, cuatro negativos, y una conclusión: el modelo chico llegó al nivel del LLM que lo entrenó y ahí se quedó.

·
  • ai
  • llm
  • devops
  • alerting
  • fine-tuning
  • open-source
  • homelab
  • agents

Dos robots en una sala de control: el chico elige el canal en un panel de tres salidas, el grande escribe el mensaje.

TL;DR

Mi agente que vigila la infra (clau-vigía, lo conté acá) decidía todo con un solo LLM: si avisarme, por qué canal y qué decir. Entre 10 y 40 segundos por decisión. Desde esta semana la decisión la toma Laya, un modelo de decisión tipada de 322 millones de parámetros, open, corriendo en CPU, en menos de un segundo por señal. El LLM solo escribe el mensaje.

Lo que vale de este post no es el modelo: es el método para saber si servía. Nueve experimentos pre-registrados, cuatro con resultado negativo, y una conclusión que no esperaba: el modelo chico llegó exactamente al nivel del LLM que lo entrenó, y ahí se quedó. Para pasar de ahí, las etiquetas tienen que ser mías.

El problema: un LLM tomando una decisión de tres opciones

clau-vigía corre cada 15 minutos y cuando hay una señal nueva (un servicio caído, un pod en CrashLoop, una alerta de Grafana, un meeting inminente) le pregunta al LLM: ¿le aviso a Carlos? ¿Por llamada, por mensaje o por mensaje con botones “tomo / descarto”? ¿Qué le digo?

Las dos primeras preguntas son una clasificación. La tercera es una redacción. Usar un LLM de frontera para clasificar en tres opciones es caro y lento, y la latencia se nota: entre que salta la alerta y suena el teléfono pasaban 30 segundos.

En septiembre salieron dos modelos pensados para exactamente esto, con una semana de diferencia:

Los dos responden preguntas tipadas sobre un estado: noul (sí/no con probabilidad), choice (una opción entre varias) y score (un nivel ordinal). No generan texto, así que no hay nada que parsear ni que alucinar.

Quería probar Jev. Cerró las suscripciones nuevas por demanda. Buscando alternativa encontré Laya, y que sea abierta cambió todo lo que sigue.

Lo que Laya sabe hacer sin entrenar: nada útil

El model card de Laya es honesto y conviene leerlo antes que la prensa: los checkpoints base dan 0,36 en su propio benchmark de decisiones tipadas, con random en 0,32. “Laya es una base rápida para especializar, no un motor de decisión zero-shot”.

Lo confirmé en el primer experimento. Le di la señal “servicio caído: qwen, crit, nuevo” y las tres preguntas. Respondió “avisar” con probabilidad 1,00. Y también con 1,00 a “scrub del NAS terminado sin errores”, a “3 mails importantes sin leer” y a todo lo demás. El base dice que sí a todo.

Eso no es un problema, es el punto de partida. Pero define el piso: un modelo que siempre dice “avisar” acierta exactamente la proporción de positivos del test. Cualquier versión tiene que superar eso, no el 50%.

El método, antes que el modelo

Vengo de un hackathon donde lo que mejor funcionó no fue el agente sino la disciplina para medirlo. Apliqué lo mismo:

  1. Ground truth propio. Escribí 128 casos a mano, con las formas reales de las señales de vigía y la respuesta correcta según mi criterio: servicios caídos, nodos NotReady, pools ZFS degradados, backups fallidos, demos en vivo, mails urgentes, meetings a distintos minutos, y sus versiones repetidas y recuperadas. 70 para entrenar, 58 congelados como test. El test no se tocó más.
  2. Control. Laya sin entrenar, con las mismas preguntas. Se re-corre cada vez que cambia algo del instrumento.
  3. Pre-registro. Cada versión con su hipótesis y su resultado esperado escritos antes de correrla. Los negativos se guardan igual que los positivos.
  4. Métricas por clase. Nunca accuracy sola: recall de positivos, recall de negativos, falsos negativos en severidad crítica, calibración. Con clases desbalanceadas, accuracy miente.

El state que ve el modelo es la señal más un contexto en palabras:

{
  "contexto": "SEÑAL REPETIDA: ya avisada 2 veces, cambió el detalle.",
  "severidad": "crit",
  "titulo": "Servicio caído: whisper-tts",
  "detalle": "whisper-tts (http://ai-server:8000/health) no responde OK — timeout.",
  "fuente": "collector.infra",
  "key": "service-down/whisper-tts",
  "cambio": "modificada",
  "veces_avisada": 2,
  "otras_senales_activas": 1
}

Y las preguntas, en castellano, con los criterios de cada opción:

{
  "avisar":   {"type": "noul",   "instructions": "¿Esta señal amerita avisarle a Carlos ahora por Telegram? ...",
               "criteria": {"false": "no avisar: ruido, informativo, ya avisado, o sin acción posible",
                            "true":  "avisar ahora: caída, incidente, demo en vivo, recuperación de algo ya avisado, meeting inminente"}},
  "canal":    {"type": "choice", "instructions": "Si se avisa, ¿por qué canal?",
               "criteria": {"voice": "llamada telefónica: grave y urgente, servicio core caído, pod en CrashLoop, demo crit",
                            "text": "mensaje de Telegram: el default, dato accionable, recuperaciones, meetings, avisos informativos",
                            "actionable": "mensaje con botones tomo/descarto: incidente operativo que alguien debe tomar"}},
  "urgencia": {"type": "score",  "instructions": "¿Qué tan urgente es esta señal para Carlos?",
               "criteria": ["low: puede esperar horas", "med: conviene mirarlo en la próxima hora", "high: ahora mismo, interrumpir"]}
}

Laya corre en un servidor FastAPI en mi AI Server, en CPU, con torch sin CUDA a propósito: la GPU la tiene Qwen y no hay VRAM para los dos. Unos 600 ms por señal con las tres preguntas.

Siete versiones, cuatro negativos

Un cubo encendido sobre una balanza, un cronómetro y un tablero de tildes y cruces.

Todo sobre el mismo test de 58 casos: 37 positivos, 21 negativos, 19 críticos. Mayoría de clase 0,638.

versióntrainavisar accrecall+recall-FN critcanalqué cambió
base00,6381,000,0000,34control: avisa todo
v1220,420,540,1730,23receta del notebook, 18 pasos
v2220,710,880,2200,72260 pasos
v3390,6550,840,3330,54más casos a mano
v4390,6030,650,5230,30contexto adelante del state
v5700,7410,810,6210,57casos apuntados a las fallas
v64930,8100,920,6200,65+ 423 generados, LLM etiqueta
v78890,8100,870,7100,60+ 396 generados apuntados

Cada fila enseñó algo distinto.

v1 salió peor que el control. Porté el notebook de fine-tuning de Convai, pensado para 2 GPUs y 1200 casos, y copié sus hiperparámetros: 16 ejemplos por actualización. Con 50 ejemplos y 6 vueltas eso son 18 correcciones en total. El modelo no se equivocaba, estaba indeciso: casi todas las probabilidades pegadas a 0,5. Lección: cuando portás una receta, portá la cantidad de actualizaciones, no los números sueltos.

v3 memorizó. Con la receta corregida y 39 casos, train 1,00 en todo y test exactamente en la mayoría de clase. El alumno que estudia las respuestas del examen viejo y en el nuevo no sabe qué hacer. Un modelo de 322M con 39 ejemplos no necesita entender nada.

v4 encontró un atajo. Los fallos de v3 eran pares gemelos: mismo título, mismo detalle, etiqueta opuesta, y lo único distinto era veces_avisada al final de un JSON de 500 tokens. Puse el contexto adelante y en palabras para ayudarlo. Salió peor: aprendió “nueva y crit, sí; warn, no; repetida, no” y dejó de leer el detalle. Hacerle fácil una cosa al modelo le da permiso para ignorar el resto, cuando los datos son pocos. Con 70 casos (v5) el formato empató contra el viejo; lo dejé porque las reglas que acierta son las que menos toleran error.

v6 es donde cambió la escala. Dejé de escribir casos a mano. Mi Qwen local generó 423 señales variadas, apuntadas a los 12 patrones que fallaban, y el LLM de producción de vigía las etiquetó con su prompt real, en lotes de 6, como si fueran alertas de verdad. Antes de entrenar revisé el 10% y encontré un sesgo del teacher: callaba las recuperaciones (3 de 37 con aviso previo) y filtraba demos repetidas. Eso lo corregí por criterio antes de entrenar, con la etiqueta original guardada. Resultado: 0,81, cero críticas calladas, calibración a la mitad, y todas las reglas aprendidas (DEMO, repetida, recuperada) sin una línea de código.

v7 movió el umbral, no el criterio. 396 más, desbalanceadas hacia lo que fallaba. Recall de negativos subió de 0,62 a 0,71 y el de positivos bajó lo mismo. Accuracy clavada en 0,81 por segunda vez.

Medir al maestro

Cuando dos versiones con 1,8x de datos dan el mismo número, el problema no son los datos. Hice lo que tendría que haber hecho antes: le mandé los 58 casos de test al LLM de producción dos veces, en distinto orden.

avisarcanal
LLM contra sí mismo (dos corridas)0,890,83
LLM contra mi criterio0,80 a 0,840,67 a 0,70
Laya v7 contra mi criterio0,810,73

Laya es un aprendiz de ese maestro: puede ser tan buena como él en lo que le mostró, no mejor, y si el maestro se equivoca de forma sistemática aprende el error como verdad. Llegó exactamente al mismo lugar. Más rondas con este teacher no van a subir el número, van a mover errores de un lado al otro.

La prueba más clara está en el canal. De mis 13 casos que piden llamada, el LLM llama en 4. Laya llama en 3, y en los mismos. No es que Laya no aprenda a llamar: es que el prompt que tenía en producción casi nunca llamaba, y nadie se lo había enseñado. Un fallo del modelo chico que resultó ser un fallo del sistema grande, y que llevaba semanas sin que lo viera.

Los negativos que fallan en todas las versiones son también los que falla el LLM: un meeting a 40 minutos, mails mundanos, Qwen apagado por el propio switchboard con restauración automática. Casos donde hay que leer el detalle y entender que el estado es esperado.

Producción: Laya decide, el LLM redacta

Con 0,81 y cero críticas calladas, el riesgo de ponerlo a decidir era el mismo que el del LLM, y la latencia bajaba veinte veces. Lo puse en producción con tres redes:

Así quedó el corazón del cambio en el daemon:

decision = None
if config.laya_url and config.laya_mode == "decide":
    if config.laya_shadow_llm:
        asyncio.create_task(laya.sombra_llm(config, prompt))
    decision = await laya.decidir_y_redactar(config, signals, new, changed, resolved, state)
if decision is None:
    decision = await decide(config, prompt)

Y el primer tick real con una alerta warn por webhook:

laya: decide key=nas/zfs/snapshot-nocturno cambio=nueva veces=0 -> avisar=True (p=0.90) canal=text urgencia=med (598 ms)
dispatcher: delegado por canal=text
laya: sombra del LLM registrada (1 aviso(s), 13009 ms)

Laya decidió en 598 ms. El LLM, en sombra, llegó 13 segundos después a la misma conclusión.

Lo que sigue

Lo honesto: 58 casos escritos por mí son direccionales, no significativos. Las señales reales pueden tener formas que ni Qwen ni yo imaginamos. Por eso la sombra.

Referencias: model card de Laya · Jev, TypeSafe AI · comparativa Laya vs Jev de Artifilog · benchmark pareado de Anthus · el post corto en LinkedIn