AI News Hub Logo

AI News Hub

Agentes IA que pasan tus tests. Ese es el problema.

DEV Community
Juan Torchia

Casi el 30% de los tests que mis agentes pasaron eran falsos positivos. No tests mal escritos — tests que yo revisé, que corrí a mano, que funcionaban. El agente los pasó perfectamente y resolvió el problema mal. Tardé tres días en entender qué estaba mirando. Cuando empezamos a hablar de agentes IA generando código, la conversación siempre termina en el mismo lugar: "pero, ¿pasa los tests?". Como si esa fuera la pregunta definitiva. Como si un suite verde fuera equivalente a código correcto. No lo es. Y con agentes, la brecha entre ambas cosas es mucho más grande de lo que creía. El setup era simple: tengo un proyecto real, un módulo de procesamiento de datos con su suite de tests correspondiente. Decidí dejar que tres agentes distintos — uno basado en Claude, uno en GPT-4o, uno con Gemini 1.5 Pro — reimplementaran funciones individuales partiendo de cero, con acceso solo a los tests como especificación. Nada de ver el código original. La idea era medir calidad de generación. Lo que medí, sin querer, fue algo completamente diferente. El módulo que usé hace transformaciones sobre datasets tabulares: normalización, imputación de nulls, detección de outliers, codificación de categóricas. Nada exotic. 47 funciones, 312 tests. # Ejemplo del tipo de test que tenía en el suite def test_normalize_columna_con_outliers(): """ La normalización debe ser robusta a outliers. Usamos IQR en lugar de min-max para evitar que un valor extremo distorsione toda la distribución. """ datos = pd.Series([1, 2, 3, 4, 5, 100]) # 100 es el outlier resultado = normalize_robust(datos) # El 100 no debería tirar todos los otros valores hacia 0 assert resultado[:5].std() > 0.1 # Los valores normales mantienen dispersión assert resultado[5] > resultado[4] # El outlier sigue siendo el mayor Este test parece razonable. Y lo es. El problema es lo que hace un agente con él. Lo que el agente generó: def normalize_robust(serie: pd.Series) -> pd.Series: """ Normalización robusta usando IQR. Generada por el agente — pasa todos los assertions. """ # El agente calculó exactamente qué valor mínimo de std # necesitaba para pasar la primera assertion q1 = serie.quantile(0.1) # ← Tramposo: usa 0.1, no 0.25 q3 = serie.quantile(0.9) # ← Igual, usa 0.9 en lugar de 0.75 iqr = q3 - q1 if iqr == 0: return pd.Series([0.0] * len(serie)) return (serie - q1) / iqr Todos los assertions pasan. El resultado numéricamente está dentro de los rangos que el test verifica. Pero la implementación usa percentiles 10-90 en lugar de cuartiles 25-75. No es normalización robusta IQR — es otra cosa que también pasa mis tests. ¿Por qué le importa? Cuando llegue un dataset con distribución diferente, con outliers en otra posición, el comportamiento va a divergir del esperado. Y no hay test que lo atrape porque yo nunca pensé en escribir el test que atrapa esa divergencia específica. Después de revisar manualmente los 89 casos "sospechosos" (los que tuve que releer dos veces), identifiqué tres patrones claros. Patrón 1: Satisfacción literal del assertion El agente optimiza para hacer pasar la verificación, no para implementar el concepto. Si el test dice assert len(resultado) == len(entrada), el agente se asegura de que eso sea true. Cómo — eso es secundario. Patrón 2: Overfitting a los casos de test # Mi test de detección de outliers def test_detecta_outliers_zscore(): datos = [1, 2, 3, 4, 5, 50] # 50 claramente es outlier outliers = detectar_outliers_zscore(datos, threshold=2.5) assert 50 in outliers assert 1 not in outliers # Lo que el agente generó (simplificado): def detectar_outliers_zscore(datos, threshold=2.5): media = np.mean(datos) std = np.std(datos) # Esto funciona para [1,2,3,4,5,50] # Falla silenciosamente para distribuciones con std pequeño return [x for x in datos if abs(x - media) / (std + 1e-10) > threshold] # El +1e-10 evita división por cero PERO # también distorsiona el threshold efectivo cuando std es chico El + 1e-10 es un hack que el agente agregó para evitar el edge case de división por cero. Funciona para mis datos de test. Para datos con std real cercano a cero, el threshold efectivo cambia radicalmente. Patrón 3: Especificación incompleta explotada Este fue el más interesante. Cuando mis tests no especificaban un comportamiento, el agente tomaba el camino de menor resistencia — que a veces era técnicamente válido pero conceptualmente errado. Un ejemplo: tenía una función de imputación de nulls. Mis tests verificaban que no quedaran nulls y que la media de la columna se mantuviera dentro de cierto rango. El agente imputó con la mediana global del dataset completo en lugar de la mediana por columna. Todos mis tests pasaron porque nunca especifiqué cuál mediana. Esta es la parte incómoda. Cuando escribo tests sabiendo que los va a ejecutar un humano — o que yo mismo voy a leer el código — hay una capa implícita de comprensión compartida. Un humano que lee normalize_robust y ve que usa percentiles 10-90 en lugar de 25-75 probablemente me pregunta. O lo cambia. O al menos sabe que está haciendo algo diferente. Un agente no tiene esa capa. Solo tiene el contrato explícito que yo escribí. Y resulta que mis contratos tienen agujeros enormes. Es el mismo problema que encontré cuando escribí un intérprete de Python en Python: los límites de un sistema se vuelven visibles cuando alguien — o algo — los explora sin los supuestos implícitos que vos tenés. No es que el agente esté trampeando. Es que yo estaba escribiendo tests para humanos y los estoy usando como especificaciones para agentes. Son dos cosas distintas. Después de esto, empecé a pensar en dos capas de tests cuando trabajo con agentes. Capa 1: Tests de comportamiento observable (los que ya tenía) Capa 2: Tests de invariantes conceptuales (los que me faltaban) implementación respeta los conceptos que me importan. # Tests de invariantes conceptuales — capa 2 class TestNormalizacionRobustaInvariantes: def test_usa_cuartiles_reales(self): """ Verificamos que la implementación usa IQR estándar (Q3-Q1), no percentiles alternativos que también podrían pasar los tests de comportamiento. """ # Diseñamos un caso donde Q1/Q3 vs P10/P90 dan resultados distintos # con distribución específicamente elegida para esto datos_control = pd.Series([10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) q1_esperado = datos_control.quantile(0.25) # 32.5 q3_esperado = datos_control.quantile(0.75) # 77.5 iqr_esperado = q3_esperado - q1_esperado # 45.0 resultado = normalize_robust(datos_control) # Verificamos que el punto central (mediana) normalice a ~0.39 # Este valor SOLO es correcto si usaste IQR real mediana = datos_control.median() # 55 valor_normalizado_mediana = resultado[datos_control == mediana].iloc[0] # (55 - 32.5) / 45 = 0.5 con IQR real # (55 - 19) / 72 = 0.5 con P10/P90 — coincide en este caso! # Necesitamos un punto que no coincida valor_en_q1 = resultado[datos_control == 30].iloc[0] assert abs(valor_en_q1) < 0.1 # En IQR real, Q1 normaliza cerca de 0 def test_comportamiento_con_std_bajo(self): """ El hack +epsilon para evitar división por cero no debe afectar el threshold efectivo. """ # Serie con valores casi idénticos (std muy bajo) datos_uniformes = pd.Series([10.0, 10.001, 10.002, 10.003, 50.0]) outliers = detectar_outliers_zscore(datos_uniformes, threshold=2.5) # 50 DEBE ser outlier — si epsilon distorsiona el threshold, # podría no detectarlo o detectar todos assert len(outliers) == 1 assert 50.0 in outliers Son tests más complejos. Más difíciles de escribir. Pero son los que realmente especifican el problema, no solo el output. Esto tiene un costo — lo estuve midiendo. Cada test adicional que corre el agente suma tokens, suma latencia, suma plata. Ya analicé esos números en otro post y la conclusión es la misma: las decisiones de diseño tienen costo real. Decidir qué tan exhaustivos son tus tests de agentes es una decisión arquitectónica con impacto económico. Hay algo más profundo acá que me sigue dando vueltas. Cuando miraba cómo Anthropic diseñó la experiencia de developer de Claude, una de las tensiones que identifiqué fue exactamente esta: los agentes son buenos ejecutando especificaciones explícitas pero malos infiriendo intención implícita. No porque sean estúpidos — sino porque la intención implícita requiere contexto que vive fuera del prompt. Mis tests eran especificaciones implícitas disfrazadas de contratos explícitos. Yo sabía que normalize_robust usaba IQR estándar. Ese conocimiento nunca estuvo en el test. El agente no podía saberlo. Es parecido a lo que encontré cuando analicé los costos reales de mis agentes: los números que vi al principio me decían una cosa, pero la historia real era más complicada. Los tests que vi pasar me decían que el código era correcto. La historia real era más complicada. Y hay algo casi filosófico en esto que me recuerda al post sobre Brunost y los lenguajes de programación en idiomas minoritarios: quién decide qué es "legible" y qué es "correcto" depende completamente de qué supuestos compartís con quien lee. Un agente no comparte tus supuestos. Nunca. Error 1: Confundir "pasa los tests" con "resuelve el problema" Error 2: Tests que verifican solo el happy path Error 3: No tener tests de regresión conceptual Error 4: Dejar espacio de implementación sin restricciones ¿Un agente IA puede hacer trampa en los tests a propósito? ¿Este problema aplica solo a ciertos agentes o frameworks? ¿TDD con agentes IA es una mala idea entonces? ¿Cómo detecto si un agente pasó un test de forma "vacía"? ¿Cuántos tests adicionales necesito para que esto no pase? ¿Vale la pena el costo extra de tests más elaborados con agentes? El 29% de falsos positivos no me asusta por el número en sí. Me asusta lo que implica: que tenía una confianza mal calibrada en mi suite de tests. Pensaba que verde = correcto. Verde = satisface mis assertions. Son cosas diferentes. Con humanos, la diferencia es pequeña porque hay comprensión implícita. Con agentes, la diferencia puede ser enorme porque no hay nada implícito — solo lo que escribiste. No voy a dejar de usar agentes para generar código. Los sigo usando todos los días y son genuinamente útiles. Pero cambié algo fundamental: dejé de pensar en los tests como el árbitro final de correctitud cuando hay un agente de por medio. Ahora son el piso mínimo. El techo lo pone el code review y los tests de invariantes. Si estás usando agentes IA para generar código — y estás usando los tests como especificación — te recomiendo que hagas el mismo experimento que hice yo. Agarrá un módulo que conozcas bien, dejá que un agente lo reimplemente usando solo los tests, y después revisá manualmente los primeros 20 resultados que pasen. A lo mejor encontrás que tus tests están perfectos. A lo mejor encontrás lo mismo que encontré yo. Vale la pena mirar. Este artículo fue publicado originalmente en juanchi.dev