Ayyoub Amjahed

El estado vive fuera del modelo: cómo una mascota de escritorio sabe que llevas tres días sin hablarle

Un LLM con memoria da un chatbot amable. Una máquina de estados con reloj inyectado da una criatura. La frase «I know» solo tiene gracia si es verdad.

5 min de lectura Proyecto: NOVA arquitecturaJavaNOVA

NOVA es una criatura de escritorio con ojos, memoria y carácter. Todo el proyecto cuelga de un guion de cuatro líneas:

Tú: «I built a ChatGPT pet.» La criatura: «You haven’t talked to me in 3 days.» Tú: «What?» La criatura: «I know.»

Lo obvio sería enchufar un modelo de lenguaje con memoria y dejar que improvise. El resultado de eso es un chatbot amable, no una criatura. Y la línea de los tres días, si la genera el modelo, es una frase plausible. Si la genera un temporizador, es verdad. La diferencia se nota en el único momento que importa.

La decisión

El estado vive fuera del modelo. Son cuatro valores: afecto, energía, humor y última interacción. Decaen con el tiempo según reglas propias, con sus tests. Las entradas (hoy tres botones, mañana una clasificación de sentimiento sobre lo que dices) inyectan valores. Y hay disparadores proactivos: el dispositivo habla sin que le hables porque un temporizador se cumplió.

El modelo recibe ese estado en el prompt y escribe la línea. A veces ni eso: hay frases escritas a mano. «Un LLM con memoria» frente a «un Tamagotchi que sabe hablar». Lo segundo es lo que quiero construir.

El dominio, sin framework

El módulo de personalidad es Java puro: cero dependencias, ni una anotación de Spring. Spring lo envuelve desde fuera y lo expone por WebSocket; la lógica no sabe que existe un framework, y los tests corren en milisegundos. Cuatro clases: Creature (la máquina de estados), Tuning (todas las constantes de personalidad en un solo sitio), Snapshot (lo que se entrega al dispositivo) y los enumerados.

Cinco decisiones que aguantan:

  1. Decaimiento exponencial continuo, no por pasos. Tres días en un salto dan exactamente lo mismo que 72 saltos de una hora, y hay un test que lo comprueba. Sin esa propiedad, un dispositivo que se desconecta una tarde vuelve con el estado mal, y ese bug es infernal de encontrar en una placa.
  2. El humor revierte hacia una línea base marcada por el afecto. El vínculo largo fija el estado de reposo; los eventos lo perturban. Es una línea de código y es lo que hace que la criatura sea coherente consigo misma.
  3. Rendimientos decrecientes en los botones. Una variable de saturación que decae. Sin esto, machacar el corazón sube el afecto al máximo y la mecánica se nota falsa en diez segundos.
  4. La energía no se persiste: se deriva de la hora local. Función pura del reloj, mínimo a las 4:00, máximo a las 16:00. No hay nada que guardar.
  5. El Snapshot no devuelve geometría. Ni un ángulo de párpado ni una curva: devuelve qué siente y a qué ritmo. La forma vive en el firmware, y eso permite rediseñar la cara entera sin tocar la personalidad.

El tiempo se inyecta

Toda la lógica depende del paso del tiempo, y depender del reloj de pared haría imposible probarla. Creature recibe un java.time.Clock en el constructor. En producción es el reloj del sistema; en los tests lo controlo yo, y simulo tres días, una semana o seis meses de silencio en milisegundos, sin la placa encima de la mesa.

Esta es, de paso, la razón de elegir Java sobre Python o Node para el backend: Clock es literalmente la fuente de tiempo inyectable que pide el diseño. La clase existe para esto. En otros lenguajes habría acabado con monkeypatching o una abstracción a mano.

Al añadir el histórico de eventos apareció un fallo que merece la pena contar. El reloj de repetición nacía por defecto en 1970 hasta que alguien lo colocaba en una fecha. Lo arreglé quitándole al constructor la posibilidad de nacer sin saber cuándo: el instante inicial pasa a ser obligatorio. Los objetos que pueden nacer en un estado inválido acaban naciendo en un estado inválido.

Cliente de pruebas HTML de NOVA con el snapshot: afecto 99,8, humor 19,3, energía 90,8, expresión NEUTRAL y personalidad CALM
El cliente de pruebas, sin placa: tres botones, presencia y el snapshot que devuelve el dominio. Cinco valores; ni un ángulo de párpado.

La ética, como constantes con test

La mecánica de «se pone triste si la ignoras» es el gancho del producto y a la vez el riesgo: encantador o coercitivo. Decidí que eso no fuera una intención sino tres números en Tuning, cada uno con su test:

ConstanteValorQué garantiza
affectionRecoveryFactor2,5El afecto sube 2,5 veces más rápido de lo que baja. Volver siempre compensa más que irse.
affectionFloor15Nunca llega a cero. No hay criatura muerta que dé culpa mirar.
neglectCooldown24 h, sin escaladoLo menciona como mucho una vez al día y nunca sube el tono.

No es «lo dice una vez y nunca más», que sería peor: si te vas dos meses, lo mencionaría el día tres y callaría para siempre. Es como mucho una vez al día, sin escalar. Si algún día alguien afloja esos tres números, que sea sabiendo lo que afloja.

Una línea temporal

arranque     afecto= 55.0 humor=  +0.0  NEUTRAL CALM
❤️           afecto= 75.0 humor= +30.0  HAPPY   CALM
+2 h         afecto= 74.5 humor= +19.9  NEUTRAL CALM
😡           afecto= 72.5 humor= -21.3  HURT    CALM
+8 h         afecto= 70.7 humor=  +6.4  NEUTRAL TIRED   <- el enfado se pasó solo
+3 días      afecto= 56.4 humor=  +2.5  NEUTRAL TIRED   -> dispara NEGLECTED
+2 semanas   afecto= 28.9 humor=  -8.4  BORED   TIRED
❤️ vuelve    afecto= 48.9 humor= +21.6  HAPPY   TIRED   <- recuperación asimétrica

Queda por ajustar conviviendo con ella, no teorizando: ¿dos semanas de silencio deberían dejarla en 28? ¿Un solo corazón debería subirla de 28 a 48? Pero la pregunta ya es sobre dos números concretos, no sobre una sensación, y esa es la ventaja de haber sacado el estado del modelo.

Lo que me llevo

Cuando algo tiene que ser verdad, no se lo pidas a un generador de texto. El modelo es bueno verbalizando; es malo sabiendo. El estado, el tiempo y las reglas viven en código con tests, y el modelo recibe el resultado y lo pone en palabras. Es una arquitectura menos vistosa que «una IA con memoria», y es la única en la que «I know» significa algo.