← Volver a artículos

Un juez chico antes del commit

En el trabajo pusimos un modelo System One a contestar una pregunta por archivo antes de cada commit del agente. Lo que más costó fue escribir bien la pregunta.

iaagentesworkflowingeniería

¿Se puede poner un modelo a revisar lo que hace un agente antes de cada commit, sin que tarde como una review ni devuelva tres párrafos de opinión? La respuesta corta es que sí, si el modelo contesta una sola pregunta con un número y el mensaje de arreglo lo escribe un script.

Hace poco hicimos ese experimento en el trabajo, sobre una aplicación Rails grande. Usamos un modelo de la clase System One, la que conté en Un modelo que no escribe: no genera texto, recibe una afirmación y devuelve la probabilidad de que sea cierta. Lo probamos en dos lugares. Uno fue el commit del agente, que es la parte que más me gusta. El otro fue estimar cuánto riesgo tiene una PR antes de mergearla.

Dos clases de chequeo

Lo primero fue separar los chequeos que tienen respuesta conocida de los que piden juicio.

Los primeros ya estaban resueltos. RuboCop, el linter de Ruby, dice si el código cumple las reglas de estilo. Packwerk dice si un módulo usa algo que no le corresponde. Una regla de rutas dice si un cambio toca sólo pruebas. Ninguno necesita un modelo, y ponerles uno encima es gastar para acertar menos.

Los segundos son los que hasta ahora caían en la review de una persona. ¿Esta prueba afirma lo que el usuario ve en pantalla? ¿Este comentario repite lo que el código ya dice? ¿Esta PR es segura para mergear sin que nadie la mire?

Ahí entra el modelo, con una restricción: contesta una pregunta angosta y no escribe una review.

De dónde salió el primer chequeo

No lo inventamos. Fuimos a buscar en qué se equivocan los agentes de verdad.

Revisamos sesiones viejas de implementación. De 110 chats candidatos, 24 eran implementaciones terminadas, y 22 de esas 24 habían necesitado una corrección manual. Es el mismo síntoma que describí en Loops que se mejoran solos, ahora con un conteo.

El problema de juicio que más se repetía eran las pruebas de interfaz flacas. El agente escribía un spec que visitaba una página, hacía clic y verificaba que la URL fuera la correcta. La prueba pasaba y no decía nada sobre lo que la persona veía en la pantalla.

Ese fue el primer chequeo.

El commit que rebota

El mecanismo es un hook de pre-commit. Cuando el agente intenta commitear, corre un comando que hace tres cosas: pasa RuboCop sobre los archivos Ruby del commit, le hace al modelo las preguntas que apliquen, y termina con error si alguna falla. Git rechaza el commit.

Pre-commit
El agente commitea Git corre el hook antes de aceptar.
Chequeos con respuesta conocida RuboCop sobre los archivos Ruby del commit.
Una pregunta al modelo Sólo si el archivo es del tipo que la pregunta cubre. Vuelve una probabilidad.
El script decide Compara el número con el umbral. Si falla, sale con error.
Commit rechazado El agente lee un mensaje fijo, corrige y vuelve a intentar.
Commit aceptado La persona que revisa ya no tiene que marcar eso.
El modelo pone el juicio y el script pone la instrucción. El agente nunca lee texto escrito por el modelo.

La decisión de diseño que más rindió fue esa división. El modelo hace el juicio, y nuestro script pone la instrucción de arreglo. Cuando un chequeo falla, el agente no recibe una opinión redactada en el momento. Recibe un mensaje fijo, escrito por nosotros una vez, que dice qué falta y qué hacer.

Eso mantiene la salida corta y accionable. Un agente que lee tres párrafos de review los discute o arregla la mitad, y uno que lee una instrucción la ejecuta y vuelve a intentar.

Así se ve desde el lado del agente. La prueba que escribe pasa: entra a la página y verifica que exista un elemento con cierta clase CSS. Esa clase es un detalle de implementación. La página puede estar vacía, o mostrar un error dentro de ese elemento, y la prueba sigue en verde.

sesión del agente

1. El agente escribe la prueba

spec/features/employees_spec.rb
it "shows the employees page" do
visit employees_path
expect(page).to have_css(".employees")
end

2. Intenta commitear y el commit rebota

$git commit -m "Add employees page"
pre-commit-checks · spec/features/employees_spec.rb
✓ rubocop
Lo decide el modelo
✗ ui-spec-asserts-visible-behavior0.027 FAIL
Lo escribe nuestro script, siempre igual

This spec does not assert anything the user sees. Add at least one expectation on visible content: a heading, a table row, a message. A path or a CSS selector does not count. Fix the spec and commit again.

commit rejected (exit 1)

3. Corrige y vuelve a intentar

spec/features/employees_spec.rb
visit employees_path
- expect(page).to have_css(".employees")
+ expect(page).to have_content("Employees")
$git commit -m "Add employees page"
✓ rubocop
✓ ui-spec-asserts-visible-behavior0.998 PASS
[add-employees-page 3f2c1a9] Add employees page
Reconstruido: el nombre del chequeo y la redacción del mensaje son aproximados, los puntajes son los de casos equivalentes que medimos.

En el segundo paso hay dos bloques, y son de dueños distintos. El número y el FAIL vienen del modelo. El párrafo de abajo es nuestro: está escrito a mano una vez por chequeo, y el script lo imprime igual cada vez que ese chequeo falla.

Ese párrafo es el que dirige al agente. No discute el caso puntual ni explica por qué dio 0,027. Dice qué cuenta como arreglo y qué no cuenta, que es justo lo que el agente habría intentado después. El commit entra al segundo intento y nadie tuvo que escribir un comentario de review.

Después sacamos el comando a la consola, como bin/pre-commit-checks. Sin argumentos revisa los archivos del commit, que es lo que usa el hook. Con archivos como argumento los lee del disco. Eso último parecía un detalle y terminó siendo lo que nos dejó probar una redacción nueva de la pregunta contra archivos sueltos, sin armar un commit.

Lo que salió mal: una prueba buena, rechazada

La primera versión de la pregunta sobre las pruebas de interfaz parecía andar. Probamos tres casos armados:

  • Visitar, hacer clic y verificar la URL: 0,004. Falla, como tenía que fallar.
  • Verificar una tabla y el texto visible: 0,964. Pasa.
  • Verificar sólo un título: 0,982. Pasa.

Después la corrimos contra una prueba real del proyecto. Usaba have_section "Employees", un helper propio que verifica que en la página exista una sección con ese título. Dio 0,415 y falló.

Estaba mal rechazada. Esa prueba seguía la convención del proyecto de afirmar texto que el usuario ve. El modelo no conocía el helper y nuestra pregunta no le decía que eso contaba.

Cambiamos la pregunta, no el umbral. Con la nueva redacción:

  • have_section "Employees": 0,998. Pasa.
  • Tabla y texto visible: 0,995. Pasa.
  • Verificar un test ID y una clase CSS: 0,027. Falla.
  • Visitar y verificar la URL: 0,007. Falla.

Para mí fue la parte más útil del experimento. La rúbrica cambió por un rechazo falso sobre código real, y los casos armados por nosotros nunca lo habrían mostrado.

También simplificamos cuándo corre. La primera versión pedía que el commit tuviera una plantilla de interfaz y además su prueba. Pero la plantilla sólo hacía de interruptor: el modelo nunca la leía. La sacamos. Ahora la pregunta corre para las pruebas de pantalla completa y las de componentes, y el resto de las pruebas sólo pasa por RuboCop.

El segundo chequeo: comentarios que repiten el código

La segunda pregunta era si un comentario se limita a repetir lo que el código ya dice.

Un método con el comentario # Increments the visit count by one dio 0,999 y falló. El mismo método sin el comentario dio 0,256 y pasó. Las directivas del lenguaje y los comentarios que explican una restricción que no se ve en el código quedaron permitidos por la rúbrica.

Es un chequeo chico, y es exactamente el tipo de corrección que una persona hace una y otra vez y nunca llega a escribir como regla.

El riesgo de una PR

La idea original del experimento era otra: un puntaje en la integración continua que marcara una PR como mergeable sola o como necesitada de review humana. Las señales candidatas eran varias, entre ellas si el cambio está detrás de un feature flag, si toca contratos compartidos, cuánta cobertura le falta y qué tan reversible es.

Lo primero que vimos es que un único puntaje ponderado puede esconder una señal peligrosa. Varias señales tranquilas pueden tapar una grave. El modelo más seguro era el de compuertas duras más juicios blandos.

Igual probamos la versión blanda. Armamos bin/pr-risk, que recibe una PR y devuelve un número. La primera versión contestaba riesgo bajo, medio o alto. La cambiamos por una sola cifra, safe 0.527, donde más alto es más seguro para mergear sin review. Con el número en esa dirección, cualquier compuerta posterior se lee sin traducir.

Y acá apareció el segundo tropiezo. Una PR que cambiaba cuándo se dispara una regla central del negocio, con 36 archivos y más de 3.000 líneas agregadas, dio 0,730. Un cambio así no se mergea sin que alguien lo lea.

Probamos tres causas posibles:

  • El modelo: repetimos el mismo pedido tres veces y las tres dio 0,730. No era ruido.
  • El recorte del diff: le sacamos casi todo el diff y dio 0,713. No era que le faltara contexto.
  • La pregunta: era esto.

La rúbrica decía que lo inseguro era “un cambio de comportamiento sin pruebas”. Esa PR tenía pruebas, así que para el modelo podía ser segura. Le sacamos dos palabras y el criterio quedó en “un cambio de comportamiento”. El resultado bajó a 0,016, y después de más ajustes a 0,003.

El modelo estaba siguiendo la pregunta al pie de la letra, y la pregunta estaba mal.

Después afinamos la rúbrica contra las últimas 100 PRs mergeadas, con ejemplos del proyecto para cada lado. Del lado seguro quedaron los cambios que sólo tocan pruebas, los arreglos de pruebas inestables, las actualizaciones de dependencias, los cambios de integración continua y las herramientas internas. Del lado inseguro, las funcionalidades nuevas, los cambios de comportamiento en producción, pagos, autenticación y migraciones.

Algunos resultados después del ajuste:

  • Arreglo de pruebas inestables: 0,969.
  • Actualización de una dependencia: 0,990.
  • Cambio de permisos con efecto en producción: 0,005.
  • Cambio en una regla central del negocio: 0,003.

Esto sigue siendo un experimento. Nadie mergea nada solo por ese número, y la conclusión sobre las compuertas duras sigue en pie.

La historia del repositorio es el material de entrenamiento

Mirando para atrás, las tres veces que un chequeo mejoró fue porque lo enfrentamos con algo que ya existía en el repositorio.

  • Las sesiones viejas de los agentes dijeron qué preguntar. Sin ese conteo habríamos escrito el chequeo que se nos ocurriera, no el que hacía falta.
  • Las pruebas que ya estaban escritas mostraron el rechazo falso. have_section es una convención nuestra, y sólo aparece si corrés la pregunta contra código del proyecto.
  • Las últimas 100 PRs mergeadas definieron qué es seguro y qué no para este equipo. Cada una ya tenía un veredicto: alguien la había revisado, o la había dejado pasar.

Digo “entrenar” entre comillas porque el modelo no cambia. Lo que se ajusta es la pregunta, y la historia hace de conjunto de ejemplos con la respuesta ya puesta. Un repositorio con años de commits, reviews y pruebas trae resuelto lo más caro de cualquier clasificador, que es conseguir casos reales etiquetados por gente que conoce el proyecto.

También explica por qué estos chequeos no se copian de un equipo a otro. Una rúbrica genérica sobre pruebas de interfaz habría rechazado nuestras mejores pruebas. La que quedó sólo sirve para este repositorio, y por eso mismo funciona.

Lo que me llevo

Lo interesante del experimento no fue meter un modelo en el pre-commit sino convertir correcciones que una persona repetía en juicios chicos y ejecutables. La receta que quedó:

  • Una pregunta angosta. Una afirmación por chequeo, con criterios enumerados para verdadero y para falso.
  • Sólo cuando aplica. El tipo de archivo decide si la pregunta corre, y eso lo resuelve código.
  • Una probabilidad de vuelta. El umbral es nuestro y se puede mover.
  • El mensaje de arreglo, fijo. El modelo juzga y el script instruye.
  • El commit rebota. El agente se corrige solo, antes de que una persona lo vea.
  • La rúbrica se ajusta contra la historia del repositorio. Los casos armados la hacen parecer mejor de lo que es.
  • Un resultado malo es primero una sospecha sobre la pregunta. Las dos veces que el número nos sorprendió, la falla estaba en nuestra redacción.

Un chequeo probabilístico pide la misma precisión que cualquier otro código. Un modelo rápido no arregla una regla ambigua: la contesta rápido.