La prioridad nunca estuvo en el texto

Iba a construir una función que dedujera la prioridad de mis pendientes con un modelo on-device. Antes de escribirla la medí contra 67 items que yo mismo había etiquetado a mano. El acuerdo con mi propio criterio fue cero.

Llevo años escribiendo un registro de trabajo. Un archivo Markdown por proyecto donde anoto lo que pasó: terminé esto, subí el PR, estoy esperando la revisión, esta decisión la tomamos por tal razón. No es un gestor de tareas. Es memoria.

Este año empecé a construir una app para leerlo, y el plan del siguiente hito decía algo que sonaba razonable: usar un modelo on-device para deducir la prioridad y el estado de cada pendiente a partir del texto. Apple regala FoundationModels desde macOS 26, corre local, no manda nada a ningún servidor. Encaja perfecto con un producto cuya promesa es que tus archivos no salen de tu Mac.

Antes de escribir esa función la medí. El resultado fue cero, y es el resultado más útil que he tenido en meses.

Un dataset etiquetado, gratis, en mi propio historial

Hasta hace poco mi registro tenía una sección ## Pendientes en cada proyecto, y cada item de primer nivel llevaba un emoji de prioridad puesto a mano: 🔴, 🟡, 🟢. Cuando migré el formato a un log de eventos, esos emojis desaparecieron.

Y ahí está el detalle que hace posible el experimento: el commit anterior a la migración es un dataset etiquetado. Sesenta y siete pendientes cuya prioridad había asignado yo mismo, con criterio propio, en el momento en que los escribí. No hay anotadores que contratar ni consenso que negociar. La verdad de referencia es mi propio juicio, que es exactamente lo que la función tendría que reproducir.

El registro completo, en el momento de la prueba: 438 eventos repartidos en 27 proyectos.

El método

SystemLanguageModel de Apple, con generación guiada restringida a las tres etiquetas, temperatura 0, una sesión nueva por item para que ninguna respuesta contaminara la siguiente.

Probé dos prompts. El primero simplemente describe los tres niveles. El segundo está calibrado: le doy las frecuencias reales de mis etiquetas y dos ejemplos por clase, sacados de items que no entran en la evaluación.

Y lo medí con tres números: exactitud, la línea base de la clase mayoritaria, y la kappa de Cohen.

Ese tercer número es el que importa, y ahora se ve por qué.

Los resultados

exactitudlínea basekappa
prompt simple (n=67)32%46%0.04
prompt calibrado (n=61)49%47%0.03

Kappa mide el acuerdo por encima del azar. Cero es azar. Uno es acuerdo perfecto. Un 0.03 no es “flojo”: es ninguno.

Mira la trampa de la exactitud. El prompt calibrado sube de 32% a 49% y parece que mejoró la mitad. No mejoró nada. Lo que hizo fue aprender la distribución: respondió estaSemana en 56 de 61 casos. Acertó mucho porque esa es la etiqueta más común, no porque entendiera un solo item. De los tres pendientes urgentes que había, detectó cero.

El prompt simple fallaba al revés: predijo urgente 28 veces cuando solo 5 lo eran. Uno grita y el otro se encoge de hombros, y los dos tienen exactamente el mismo conocimiento del problema, que es ninguno.

Por eso se reporta kappa y no exactitud. Un modelo que responde siempre lo mismo puede sacar 49% y sacará kappa 0.

Por qué era imposible desde el principio

La explicación no es que el modelo sea pequeño, ni que el prompt fuera mejorable. Es más simple y más incómoda:

La prioridad nunca estuvo en el texto.

Ese 🔴 no describía el item. Describía una decisión mía, un martes, sabiendo cosas que no escribí: que el cliente llamaba el jueves, que ese PR bloqueaba a otra persona, que yo tenía tres días de viaje encima. El texto dice “arreglar el error de validación en el endpoint de pagos”. La urgencia estaba en mi cabeza y en el calendario, no en esa frase.

Ningún modelo recupera lo que nadie escribió. Da igual si corre local o en un datacenter.

Esto me parece el error más fácil de cometer ahora mismo con los LLM, y lo cometí yo: tenía datos de aspecto rico, en lenguaje natural, con mucha señal aparente. Pedirle a un modelo que extraiga un campo de ahí se siente como extracción. Es generación. El modelo va a responder siempre, con seguridad y con formato correcto, y si no mides no tienes forma de distinguir entre recuperar un dato y fabricarlo.

El segundo hallazgo, que dolió más

De los mismos datos salió algo que no estaba buscando.

Las secciones ## Pendientes de todo mi historial tenían 69 items abiertos y cero completados.

Cero. Nunca, en ningún formato, registré que algo se terminara. Cuando acababa un pendiente lo borraba de la lista. Es lo natural con una lista: tachas, limpias, sigues. Pero significa que mi registro de trabajo, el archivo que existe precisamente para acordarse de lo que hice, no contenía ni un solo cierre.

Y eso tiene una consecuencia que no se arregla con mejor software: “qué me quedó pendiente” no se puede reconstruir hacia atrás. La información no está incompleta, no existe. Solo se puede saber de ahora en adelante, y solo si cerrar algo pasa a ser un evento que se escribe, en lugar de una línea que se borra.

Qué cambió en el producto

Tres cosas, todas en la dirección de construir menos.

La prioridad quedó fuera. No como “para más adelante”, sino medida y descartada. Si la quiero algún día tendré que escribirla, y si vale la pena escribirla es una pregunta de producto que todavía nadie ha respondido que sí.

Cerrar se volvió un evento. En lugar de una casilla que se marca, cerrar algo es anotar el hecho de que se cerró. El trabajo abierto se deriva de ahí. Es la única forma de que la pregunta tenga respuesta dentro de un año.

“Qué estoy esperando” no lo construí. Detectar espera en texto libre es exactamente el mismo error con otro disfraz. Buscar “esperando” o “pendiente” parece determinista y es una heurística disfrazada, de las que fallan en silencio justo cuando confías en ellas. Mi propio registro dice “queda pendiente crear el PR” sobre trabajo que no está esperando a nadie más que a mí.

Lo que me llevo

La función más barata es la que mides antes de construirla. Este experimento fueron unas horas, un script de extracción y un clasificador de cien líneas. La función habría sido semanas, y peor: habría funcionado. Habría mostrado prioridades, con su color y todo, y yo habría confiado en ellas durante meses antes de notar que eran ruido bien presentado.

Y la versión general, que aplica a cualquier cosa con IA encima ahora mismo: un modelo sobre datos que nadie escribió está adivinando. La pregunta que hay que hacerse antes de conectarlo no es si el modelo es lo bastante bueno. Es si el dato llegó a existir alguna vez.

En mi caso no existía. Mejor saberlo con un script de un día que con una función en producción.


El experimento completo — el método, los números y el código para reproducirlo — vive en el repositorio de la app. La app se llama Noonly, es para macOS, guarda todo en Markdown que es tuyo y no manda nada a ningún lado. Está publicada en infante.io/noonly.

Comentarios