Durante cosa de un día este mes, una de nuestras bases de datos en producción no pudo aceptar una entrada nueva en su tabla de vocabulario. Cada inserción fallaba con el mismo error: una columna que ya no existía. No saltó ninguna alerta. Ninguna prueba se puso roja. El sistema que gobierna con qué palabras se pueden construir nuestras herramientas había dejado de aprender, y la única forma en que alguien se enteró fue intentando enseñarle algo.
La causa fue de lo más común. Una migración de esquema partió una columna en dos y eliminó la original. Añadió una versión nueva de una función auxiliar para la forma nueva, pero dejó la versión vieja en su lugar, todavía leyendo la columna eliminada, y un disparador seguía llamando a la vieja. La pregunta interesante no es cómo entró el error. Es por qué nada lo detectó.
La respuesta es que tres migraciones se habían aplicado a mano a la base de datos viva y no estaban registradas en ninguna parte: ni en la lista de migraciones del código, ni en el libro mayor. El fixture de pruebas automáticas se construye a partir de esa lista. Así que el fixture nunca llegó a la forma en la que de verdad estaba la base de datos viva, y cada prueba que pasaba estaba pasando contra una base de datos que no existía. Un fixture puede demostrar lógica. No puede demostrar estado. Era la tercera vez que registrábamos exactamente esa clase de falla.
El patrón que hay debajo
De la misma sesión salieron otros cinco casi-accidentes, y riman entre sí.
Un agente de IA que trabajaba en el código corrió una consulta que contaba capacidades registradas y obtuvo cero. Veía renglones relacionados que claramente existían, así que concluyó que la consulta estaba mal. La consulta estaba bien; los renglones estaban en el fixture de pruebas, no en la base viva. La respuesta correcta nunca fue un solo número. Era «0 en la viva, 2 en el fixture», y la diferencia era el hallazgo.
El mismo agente escribió a mano un reporte de error y lo numeró B-124. B-124 ya existía, en la base de datos que reparte números de error. El archivo chocó con un identificador vivo.
Cada uno de estos se corrigió igual: no con alguien que tuviera mejor opinión, sino volviendo a derivar una medición con su contexto pegado. Qué consulta. Qué base de datos. Qué valor. Cuando la medición y el juicio compiten, ese día ganó la medición todas las veces.
La falla característica de un modelo capaz, en nuestra experiencia, no es la ignorancia. El agente sabía qué era un fixture. Sabía que los identificadores salen del sistema de registro. La falla es la generalización confiada a partir de una sola medición, y eso no se arregla pidiéndole al modelo que tenga menos confianza. Se arregla haciendo que la medición cargue su propio contexto se acuerde alguien de preguntarlo o no, y haciendo que el identificador venga de un escritor que no acepta un número de quien lo llama.
Reglas que son mecanismos
Corremos un conjunto de programas guardia que inspeccionan cada llamada a herramienta que hace el agente antes de que se ejecute. Ese mismo día, uno de ellos rechazó una llamada python3 -c en línea con un mensaje que nombraba la regla y la alternativa: lo que sea repetible se convierte en un programa de verdad; usa el editor de archivos para algo de una sola vez; si no, construye la herramienta. El agente cambió de enfoque en un solo turno.
Luego una segunda capa, más burda, rechazó la misma llamada después de que al guardia se le había enseñado a permitir una excepción atribuida. La segunda capa corre primero, no sabe expresar «permitido con atribución», y no devuelve ningún mensaje. El guardia que sí sabía de la excepción nunca llegó a hablar.
De ese par salieron dos lecciones. Primera: un rechazo que nombra el reemplazo cuesta un turno; una negativa callada cuesta varios y no enseña nada, porque solo le dice al agente que una ruta está cerrada. Segunda: dos capas de decisión sin un orden de evaluación compartido acabarán por discrepar, y ganará la burda.
La cadena que inspecciona una llamada a herramienta ya tiene forma de filtro de paquetes: una lista gruesa, luego guardias más finos, luego la herramienta, luego una bitácora. Así que la estamos configurando como tal, sobre el modelo del pf de OpenBSD: un conjunto de reglas ordenado donde decide la última que coincide, una palabra quick que deja a las reglas peligrosas salirse de ese orden para que ninguna flexibilización posterior las alcance, y etiquetas para que una categoría se clasifique una vez y después se le haga referencia. La gramática está escrita y tiene 49 pruebas en verde. Todavía no está conectada a nada; hoy las dos capas siguen siendo dos capas.
Lo que sacamos de ahí
Una regla escrita en prosa que se le pide a un modelo recordar no es una regla. Un mecanismo fuera del cual no puede actuar, sí. La memoria no puede bloquear una fusión; una comprobación sí.
Una cuenta sin el nombre de su base de datos no es una cuenta.
Los identificadores salen del sistema de registro. Un autor que acuña uno ha bifurcado el registro.
Los guardias deben enseñar. Nombra la regla, nombra el reemplazo, cita la línea.
Y toda regla debería poder nombrar el incidente que se la ganó. La que no puede es una convención vestida de regla.
Ninguno de los arreglos de ese día vino de pedirle al agente que se esforzara más. Todos vinieron de construir algo fuera de lo cual no podía actuar, y de asegurarse de que cuando se le detuviera, se le dijera por qué.