Programación con agentes de IA: encargo, pruebas y revisión
Cómo escribirle al agente un encargo con criterios de aceptación, por qué trabajar en pasos pequeños, un experimento con pruebas de mutación (cuántos errores detectan las pruebas del camino feliz, los casos límite y las pruebas basadas en propiedades), quién debe escribir las pruebas y a qué prestar atención.
Serie Fundamentos de IA Parte 51 de 52
- Mapa de la inteligencia artificial: qué es cada cosa y cómo encaja todo
- Breve historia de la inteligencia artificial: de Turing a los modelos de lenguaje
- Datos, modelo, entrenamiento, inferencia: los conceptos básicos en un solo ejemplo
- Cómo leer la notación matemática: símbolos, subíndices, sumatorios y cuantificadores
- Conjuntos y relaciones: las piezas con las que se construyen las definiciones en IA
- Funciones y aplicaciones: por qué todo modelo de IA es una función
- Lógica y demostraciones: las reglas con las que se razona en IA
- Grafos y árboles: la estructura sobre la que se apoyan la búsqueda y las redes neuronales
- Álgebra lineal para aprendizaje automático: vectores, matrices y por qué la IA sobre todo multiplica
- Derivadas, gradiente y regla de la cadena: cómo sabe un modelo en qué dirección aprender
- Descenso de gradiente: cómo aprende un modelo
- Qué es la probabilidad: del lanzamiento de un dado a los axiomas de Kolmogórov
- Probabilidad condicionada y teorema de Bayes: cómo cambiar una estimación cuando te enteras de algo nuevo
- Variable aleatoria y función de distribución: una sola función que lo dice todo sobre la distribución
- Función de densidad y función de probabilidad: por qué una variable continua tiene probabilidad cero en cada punto
- Esperanza, varianza y covarianza: dónde está una distribución, qué anchura tiene y qué se relaciona con qué
- Resumen de las distribuciones de probabilidad: Bernoulli, binomial, Poisson, uniforme, normal y exponencial
- Ley de los grandes números y teorema central del límite: por qué las medias se comportan de forma tan predecible
- Estadística y estimación de parámetros: máxima verosimilitud, MAP e intervalos de confianza
- Teoría de la información: entropía, entropía cruzada y divergencia KL, el origen de las funciones de pérdida
- Búsqueda en el espacio de estados: BFS, DFS, UCS y A* con animaciones
- Juegos y adversarios: minimax, poda alfa-beta y Monte Carlo tree search
- Satisfacción de restricciones: backtracking, propagación de restricciones y AC-3 con un sudoku y un mapa de regiones checas
- Lógica y sistemas basados en conocimiento: encadenamiento hacia delante y hacia atrás, resolución y sistemas expertos
- Planificación: STRIPS, el mundo de bloques, heurísticas a partir de la relajación y la planificación en los agentes de IA
- Búsqueda local y recocido simulado: un viajante por las capitales regionales checas
- Algoritmos genéticos: población, cruce, mutación y selección en una mochila para la excursión
- Estrategias evolutivas y programación genética: de la regla de un quinto a la ley de Kepler
- Optimización por colonia de hormigas: cómo un rastro de feromona encuentra un camino corto
- Optimización por enjambre de partículas: inercia, experiencia propia y experiencia del enjambre
- Neuroevolución: evolución de los pesos y la forma de las redes neuronales, y el algoritmo NEAT
- Conjuntos difusos y funciones de pertenencia: cómo escribir «algo cálido»
- Lógica difusa y probabilidad: por qué «algo cálido» no significa «probablemente cálido»
- Lógica difusa: t-normas, implicaciones y reglas SI–ENTONCES
- Control difuso: Mamdani y Sugeno, del ventilador al péndulo invertido sobre un carro
- Agrupamiento difuso: el algoritmo fuzzy c-means y los puntos entre clusters
- Sistemas neuro-difusos (ANFIS): reglas difusas que aprenden de los datos
- Cadenas de Markov: matriz de transición, distribución estacionaria y PageRank
- Modelos ocultos de Márkov: el algoritmo de Viterbi y el casino deshonesto
- Redes bayesianas: por qué está mojada la hierba y qué significa eso para el aspersor
- El filtro de Kalman: cómo estimar a partir de mediciones imprecisas dónde está un coche y a qué velocidad va
- Tipos de aprendizaje automático: supervisado, no supervisado, semisupervisado y más
- Prompts, contexto y RAG: cómo darle a un modelo la información correcta
- Autocodificadores variacionales (VAE): imágenes nuevas a partir de un espacio latente continuo
- Redes generativas adversarias (GAN): el falsificador contra el detective
- Modelos de difusión: cómo el ruido puro se convierte en una imagen
- Procesos de decisión de Markov: estados, acciones, recompensas y ecuaciones de Bellman
- Q-learning y DQN: aprender el valor de las acciones, de la tabla a la red neuronal
- Gradiente de la política y PPO: aprender directamente la política
- Cómo funciona un agente de IA: un modelo de lenguaje en un bucle con herramientas
- Programación con agentes de IA: encargo, pruebas y revisión
- Evaluación y seguridad de los sistemas de IA: cómo medir y cómo vigilar
Temario completo de la serie 52 de 83 temas publicados
- Qué debe incluir el encargo para el agente para que sepa cuándo ha terminado.
- Cuántos errores detectan pruebas de distinta minuciosidad y por qué importan los casos límite.
- Qué errores cometen los agentes y cómo encontrarlos al revisar.
Conviene conocer antes: Cómo funciona un agente de IA: un modelo de lenguaje en un bucle con herramientas, Cómo abordar el desarrollo con agentes de IA
En la entrega anterior vimos cómo funciona un agente por dentro. Esta entrega es práctica: cómo encargarle trabajo, cómo revisarlo y cómo no perder de vista lo que ocurre en el proyecto. La mayoría de los consejos se resumen en una frase: tienes que darle al agente una forma de saber por sí mismo que ha terminado, y tú necesitas una forma de comprobarlo. Con un experimento veremos cuánto importa.
Continúa las entregas Cómo funciona un agente de IA y Cómo abordar el desarrollo con agentes de IA, donde se habla más del papel de la persona y de por qué conviene dejar que el agente proponga primero una solución.
1. El encargo: qué, dónde y cómo saber que está terminado
Definición — criterios de aceptación
Los criterios de aceptación son condiciones verificables que el resultado debe cumplir para que la tarea esté terminada: un comportamiento concreto («para un carrito vacío devuelve 0 CZK»), pruebas que deben pasar, un comando que debe terminar sin errores. A diferencia de la descripción del objetivo, de ellos se puede decidir sin ambigüedad si se cumplen.
«Ordena tu cuarto» es un objetivo. «No hay nada en el suelo, la cama está hecha y la papelera está vacía» son criterios de aceptación. Solo con ellos se puede decir si el cuarto está ordenado, y el niño (o el agente) sabe cuándo puede parar.
Definición — la especificación como supuesto y compromiso
Los criterios de aceptación se pueden escribir con precisión mediante una terna de Hoare (C. A. R. Hoare, 1969; la notación actual usa llaves): {P} C {Q} dice que si antes de ejecutar el programa C se cumple la condición P y C termina, después se cumple la condición Q. P es el supuesto (lo que el programa puede esperar de la entrada), Q el compromiso (lo que debe cumplirse sobre el resultado). Un programa es correcto respecto a la especificación si la terna se cumple para todo estado que satisface P.
{ castka ≥ 0 } s := sazba(castka) { s = 0 para castka < 1 000; s = 0,05 para 1 000 ≤ castka < 5 000; s = 0,1 en otro caso }
- castka ≥ 0
- supuesto: de un importe negativo la función no responde; está aquí para dejar claro qué no hace falta probar y qué debe vigilar quien la llama
- s := sazba(castka)
- el programa del que hablamos; la especificación no dice nada sobre cómo está escrito, así que el agente puede cumplirla como quiera
- s = 0 / 0,05 / 0,1
- compromiso: exactamente tres tramos y sus límites (≤ o <); justo aquí es donde más se falla, por eso los valores 999, 1 000, 4 999 y 5 000 forman parte de los criterios de aceptación
Un buen encargo para un agente tiene cuatro partes. No porque sea un ritual, sino porque cada una corresponde a algo que, de otro modo, el agente tendría que adivinar:
Objetivo: En la página del carrito muestra un descuento según el importe (desde 1 000 CZK 5 %, desde 5 000 CZK 10 %).
Contexto: El cálculo del precio está en src/kosik.js, la plantilla en pohledy/kosik.ejs.
El envío se calcula de forma parecida en la función doprava(); mantén el mismo estilo.
Restricciones: No cambies el formato de los datos en la base de datos. Nada de bibliotecas nuevas.
Terminado cuando:
- la función sazba(castka) devuelve 0 / 0,05 / 0,1 y las pruebas de test/kosik.test.js pasan,
- añades pruebas para los límites 999, 1 000, 4 999 y 5 000 CZK,
- npm test termina sin errores y me enseñas las diferencias (git diff).
El contexto le ahorra al agente la búsqueda (y a ti tokens; véase el coste del contexto). Las restricciones impiden que «resuelva» el problema de otra forma de la que quieres. Y los criterios de aceptación le dan la retroalimentación sin la cual las tareas largas se desmoronan.
2. Pasos pequeños, revisiones frecuentes
En la entrega sobre agentes calculamos que una tarea de veinte pasos, cada uno de los cuales sale bien en el 90 % de los casos, sin revisión solo sale bien en el 12 % de los casos. La misma regla vale para ti: cuanto mayor es el trozo de trabajo que el agente entrega de una vez, más difícil es revisarlo y más cara sale la corrección. Consecuencias prácticas:
- Un cambio = una tarea. Divide lo grande en pasos que se puedan revisar por separado (primero el cálculo, luego la visualización, luego el cambio en los datos).
- El control de versiones como red de seguridad. Antes de cada tarea, un estado limpio en git; después, un commit. Si algo se estropea, vuelves atrás con un solo comando.
- Lee las diferencias, no el resumen. El «hecho, todo funciona» del agente es una afirmación. Las diferencias en el código (git diff) y la salida de las pruebas son evidencias.
Péxito = pn
- p
- probabilidad de que un paso salga bien; está aquí porque cada paso es una nueva oportunidad de equivocarse
- n
- número de pasos entre dos revisiones; está en el exponente porque las probabilidades de pasos independientes se multiplican, y con cada paso más sin revisión la probabilidad cae de forma geométrica, no lineal
- Péxito
- probabilidad de que todo el tramo entre revisiones salga bien y no haya que rehacer nada
Ejemplo — cuánto trabajo ahorra revisar cada cinco pasos
Sin revisión: 0,920 = 0,1216, es decir, el 12 %. Con una revisión cada cinco pasos: 0,95 = 0,5905 para cada uno de los cuatro tramos.
Supongamos que la revisión siempre detecta el error y que el tramo estropeado se rehace. El número de intentos hasta el primer éxito sigue una distribución geométrica con media 1 / Péxito. Así, la tarea completa sin revisión cuesta de media 20 / 0,1216 ≈ 165 pasos; con una revisión cada cinco, 4 · 5 / 0,5905 ≈ 34 pasos, y con una revisión tras cada paso, 20 / 0,9 ≈ 22 pasos. Los pasos pequeños no aumentan la probabilidad de que un paso salga bien: reducen cuánto trabajo se tira cuando hay un error.
3. Las pruebas como seguro: un experimento
¿Cuánto ayudan las pruebas? Tomamos cinco funciones pequeñas como las que un agente escribe a diario (media, año bisiesto, recorte de texto, búsqueda binaria y descuento según el importe) y creamos de ellas versiones estropeadas a propósito.
Definición — pruebas de mutación
Un mutante es una copia del programa con un único cambio pequeño y automático: < en lugar de <=, - 1 en lugar de + 1, || en lugar de &&, otra constante. Un conjunto de pruebas detecta al mutante si al menos una prueba falla con él. La proporción de mutantes detectados mide lo bien que las pruebas vigilan los errores. Los mutantes que se comportan igual que el original (equivalentes) no se cuentan.
Es como probar un detector de humo: enciendes a propósito una vela pequeña y miras si pita. Aquí metes a propósito en el código un pequeño error (justo del tipo que cometen las personas y los agentes) y miras si las pruebas lo notan.
A partir del código fuente de las cinco funciones se generaron automáticamente 32 mutantes. 3 de ellos
nunca se diferenciaron del original en 2 000 entradas aleatorias (por ejemplo, a[s] <= x en lugar de a[s] < x
en la búsqueda binaria, donde la igualdad ya la trataba la línea anterior), así que quedan 29 errores reales. Frente a ellos,
cuatro niveles de pruebas, cada uno de los cuales incluye también los anteriores:
- Camino feliz: uno o dos ejemplos típicos por función. Son las pruebas que salen cuando solo se dice «añade pruebas».
- Casos límite: lista vacía, límites de los tramos (999, 1 000, 4 999, 5 000 CZK), los años 1900, 2000 y 2100, un texto justo en el límite de longitud.
- Propiedades: 200 entradas aleatorias y reglas que deben cumplirse siempre: el resultado del recorte tiene como mucho max caracteres y es el comienzo del texto, el índice encontrado señala de verdad el número buscado, la media coincide con un cálculo escrito de otra forma.
Las pruebas del camino feliz solo atrapan 15 de los 29 errores (el 52 %). Pasan incluso con una función que para una lista vacía devuelve 1, con un descuento que no empieza hasta 1 001 CZK o con una búsqueda binaria que en una lista de un solo elemento no encuentra nada. Añadir los casos límite sube la proporción al 90 %, y las pruebas de propiedades, al 97 %.
| función | mutantes | camino feliz | + casos límite | + propiedades |
|---|---|---|---|---|
prumer | 5 | 4 | 5 | 5 |
jePrestupny | 8 | 5 | 5 | 7 |
zkrat | 3 | 2 | 3 | 3 |
najdi | 6 | 3 | 6 | 6 |
sazba | 7 | 1 | 7 | 7 |
MS = |O| / ( |M| − |E| )
- MS
- puntuación de mutación (mutation score): la proporción de errores reales que atrapa el conjunto de pruebas; es la altura de la barra en el gráfico
- |O|
- número de mutantes detectados (al menos una prueba falló con ellos, o se colgaron o entraron en un bucle infinito); lo que las pruebas saben hacer
- |M|
- todos los mutantes generados: la reserva de errores con la que medimos las pruebas
- |E|
- mutantes equivalentes; se restan porque no se diferencian del original y, por tanto, ninguna prueba puede detectarlos; de lo contrario, ni unas pruebas perfectas llegarían al 100 %
En nuestro caso |M| = 32 y |E| = 3, así que el denominador es 29. Camino feliz: 15 / 29 = 0,517; con casos límite, 26 / 29 = 0,897; con propiedades, 28 / 29 = 0,966. El procedimiento con el que se obtuvieron las cifras:
para cada función f:
original = compila(código de f)
para cada operador (de → a) y cada aparición de «de» en el código:
mutante = compila(código con esta aparición de «de» sustituida por «a»)
si el mutante nunca se diferencia del original en 2 000 entradas aleatorias:
equivalente: no se cuenta, siguiente
para cada nivel de pruebas u:
detectado[u] = alguna prueba del nivel u falla con el mutante,
este se cuelga o entra en bucle (tope tras 10 000 iteraciones)
MS(u) = número de detectados en el nivel u / número de no equivalentes
Lo más interesante es el año bisiesto. Los casos límite dejan pasar a los mutantes que consideran bisiesto cualquier año par
o que descartan los años divisibles entre diez, es decir, también el año 2020. Las pruebas con los años 1900, 2000 y 2100 no lo notan, porque
para esos años casualmente responden bien. La prueba de propiedades con años aleatorios encuentra ambos errores. El único mutante que sobrevivió
a todo es (rok % 4 === 0 && rok % 100 !== 0) || rok % 200 === 0:
solo se diferencia en los años divisibles entre 200 pero no entre 400 (200, 600, 1000…), y entre los 200 años aleatorios no hubo ninguno así.
Ni siquiera las pruebas aleatorias son omnipotentes.
Psobrevive = ( 1 − q )n
- q
- proporción de entradas en las que el mutante se diferencia del original; 1 − q es la probabilidad de que una sola entrada aleatoria no lo detecte
- n
- número de entradas aleatorias; las entradas se sortean de forma independiente, así que las probabilidades de «no lo detectó» se multiplican
- Psobrevive
- probabilidad de que las n entradas no den con el mutante: el error pasa aunque la propiedad esté bien escrita
Ejemplo — por qué sobrevivió el mutante del año divisible entre 200
La prueba de propiedades sortea años entre 0 y 2 399. El mutante rok % 200 solo se diferencia del original en 6 de ellos (200, 600, 1000, 1400, 1800, 2200), así que q = 6 / 2400 = 0,0025 y con n = 200 sobrevive con probabilidad (1 − 0,0025)200 = 0,606, más o menos en seis de cada diez intentos. Que sobreviviera no fue mala suerte, sino el resultado esperado.
El mutante rok % 2 se diferencia en 600 años (q = 0,25): sobrevive con probabilidad 0,75200 ≈ 10-25, es decir, prácticamente nunca. El mutante rok % 10 se diferencia en 96 años (q = 0,04) y sobrevive con probabilidad 0,96200 = 0,00028.
¿Cuántas entradas harían falta para atrapar al mutante del año 200 con una probabilidad del 95 %? De (1 − q)n ≤ 0,05 se sigue n ≥ ln 0,05 / ln(1 − q) = −2,996 / −0,00250 ≈ 1 196,8, es decir, 1 197 entradas. Sale más barato añadir el caso límite del año 200 o 600: quien conoce la regla del calendario sabe dónde buscar.
4. Quién debe escribir las pruebas
En el experimento escribimos las pruebas a partir del encargo (lo que debe hacer la función), no a partir del código. Eso es esencial. Cuando el agente escribe el código y enseguida sus pruebas, existe el riesgo de que las pruebas solo repitan lo que hace el código, errores incluidos. Pasan y no demuestran nada. Ayuda lo siguiente:
- Las pruebas (o al menos su lista) primero. Deja que el agente escriba primero las pruebas según el encargo, léelas, y solo después el código. Los criterios de aceptación del encargo son exactamente esa lista.
- Enumerar los casos límite. Entrada vacía, un solo elemento, límites de los tramos, números negativos, tildes y diacríticos. El agente a menudo los añade por su cuenta, pero solo si los pides.
- Propiedades donde se pueda. «Una lista ordenada sigue ordenada», «codificar y decodificar = texto original», «el nuevo cálculo da lo mismo que el antiguo». Bibliotecas como fast-check (JavaScript) o Hypothesis (Python) generan solas las entradas aleatorias.
Definición — pruebas basadas en propiedades (property-based testing)
En lugar de pares concretos de entrada y salida se escribe una propiedad que debe cumplirse para todas las entradas, y la herramienta la comprueba con muchas entradas generadas al azar. Cuando encuentra un contraejemplo, normalmente lo reduce al caso fallido más sencillo. La idea la popularizó la biblioteca QuickCheck (Koen Claessen y John Hughes, 2000).
No preguntas «¿cuánto es 7 + 5?», sino «¿se cumple que a + b siempre es igual que b + a?», y dejas que el ordenador pruebe mil pares aleatorios. Encontrará casos en los que no habrías pensado.
Una propiedad es un enunciado con el cuantificador «para todo». Para la función zkrat del experimento, la prueba la escribe así:
∀ t, m ≥ 1: |v| ≤ m ∧ ( |t| ≤ m ⇒ v = t ) ∧ ( |t| > m ⇒ v = t[0, |v| − 1) + «…» ), donde v = zkrat(t, m)
- ∀ t, m ≥ 1
- para todo texto y toda longitud; por eso se puede comprobar con entradas aleatorias sin necesidad de conocer de antemano el resultado correcto
- |v| ≤ m
- la promesa principal de la función: el resultado cabe; atrapa al mutante que olvida contar los «…» (
slice(0, max)) - |t| ≤ m ⇒ v = t
- un texto corto no cambia; sin esto pasaría una función que recorta siempre
- v = t[0, |v| − 1) + «…»
- el resultado recortado es el comienzo del texto original y termina en puntos suspensivos; sin esto pasaría una función que devuelve, por ejemplo, un texto vacío
Una herramienta de pruebas basadas en propiedades hace entonces más o menos esto:
para i = 1 … N: # N suele ir de 100 a 1 000
x = genera una entrada aleatoria
si propiedad(x, f(x)) no se cumple:
# reducción: busca una entrada más simple con la que siga fallando
mientras exista y = simplifica(x) con la que la propiedad no se cumple:
x = y # texto más corto, número menor, lista más corta
informa del contraejemplo x
informa de «pasaron N entradas» # no es una demostración, solo una buena noticia
La última línea es esencial. Las pruebas, ni siquiera las aleatorias, demuestran la corrección: solo buscan contraejemplos. Edsger Dijkstra lo expresó en 1969 diciendo que las pruebas pueden mostrar la presencia de errores, nunca su ausencia. La probabilidad de que un error raro se cuele la hemos calculado más arriba.
5. A qué prestar atención
Los agentes cometen errores distintos de los de las personas. Los más frecuentes, que vale la pena buscar al revisar:
- «Hecho» sin comprobación. El agente anuncia el éxito sin haber ejecutado las pruebas, o tras ejecutarlas solo en parte. Pide la salida de las pruebas.
- Modificar las pruebas en lugar del código. Cuando una prueba falla, el «arreglo» más fácil es cambiar el valor esperado o borrar la prueba. En las diferencias, busca cambios en las pruebas.
- Errores silenciados. Un
catchvacío, un valor por defecto en lugar de un error, un resultado escrito a mano para la entrada de prueba. - Trabajo de más. Un archivo reescrito que no tiene que ver con la tarea, una biblioteca nueva, un formato de datos «mejorado». Las restricciones del encargo y los pasos pequeños lo mantienen a raya.
- Secretos y pasos irreversibles. Contraseñas en el código, borrado de archivos, escritura en la base de datos de producción, envío al repositorio remoto. Haz que estas acciones requieran tu aprobación.
6. No perder la visión de conjunto
El agente no recuerda nada entre sesiones. Lo que debe saber siempre (cómo se ejecuta y se prueba el proyecto, qué convenciones tiene, qué evitar) va en un archivo de instrucciones en la raíz del proyecto que el agente lee al empezar. Escribe ahí lo que has tenido que advertirle al agente más de una vez. Y lo que solo sabes tú (por qué algo está hecho así, qué se probó y no funcionó) va en los commits y en la documentación, para el agente y para ti mismo dentro de medio año.
El último consejo es el más sencillo: no te desentiendas. No necesariamente delante del ordenador: el agente puede trabajar solo y tú echas un vistazo cuando necesite una decisión. Para eso nació coden: el encargo, la aprobación de los pasos y la revisión del resultado los tienes en el móvil, mientras el agente trabaja en tu ordenador.
7. Resumen
- Un buen encargo: objetivo, contexto (archivos, modelos a seguir), restricciones y criterios de aceptación verificables.
- Pasos pequeños, un commit tras cada uno, y revisar las diferencias en el código y la salida de las pruebas, no el resumen.
- En el experimento, las pruebas del camino feliz detectaron el 52 % de los errores; con casos límite, el 90 %; con propiedades, el 97 %.
- Escribe (o aprueba) las pruebas según el encargo y, a ser posible, antes del código; si no, solo repetirán los errores de la implementación.
- Vigila las modificaciones de las pruebas, los errores silenciados, el trabajo de más y los pasos irreversibles.
¿Quieres probarlo tú mismo? Pídele a un agente de IA que ejecute en tu proyecto una herramienta de pruebas de mutación (Stryker para JavaScript, mutmut para Python) y que te enseñe los mutantes que han sobrevivido. Cada uno de ellos es una prueba que falta.
¿Lo ves de otra manera? Escribe a support@coden-app.com.