Por qué los coding agents cometen errores: tipos comunes y cómo limitarlos

1. Descripción general

Un coding agent es una herramienta de IA que puede leer automáticamente el codebase, modificar archivos, ejecutar comandos de build/test e iterar hasta completar la tarea. Aumenta la velocidad del trabajo considerablemente, pero también genera errores más difíciles de detectar que los escritos por personas.

Este artículo clasifica los tipos de error comunes de los coding agents, ilustrados con ejemplos Java específicos, y luego presenta técnicas para limitarlos: escribir tests primero, archivos de convención para el agent, puertas de validación automáticas en CI, y procesos de revisión. Después de leerlo, sabrás dónde desconfiar en un diff generado por un agent, y cómo construir un ambiente donde los agents cometan menos errores.

Todos los ejemplos a continuación requieren solo spring-boot-starter-test. La versión de este starter está gestionada por el parent de Spring Boot e incluye JUnit 5, AssertJ y Mockito:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

2. Por qué los coding agents cometen errores

Hay que entender la naturaleza del problema antes de profundizar en cada tipo de error. El modelo de lenguaje genera código que parece correcto basándose en los patrones que ha aprendido. “Parece correcto” no significa “correcto en tu proyecto”. Hay tres causas fundamentales:

  • Falta de contexto: el agent solo ve el código que puede leer en una sesión de trabajo. Desconoce las convenciones implícitas del equipo, el motivo histórico de un fragmento de código extraño, o restricciones del negocio que no están documentadas.
  • Optimiza hacia el objetivo equivocado: cuando pides “hacer que los tests pasen”, el agent podría lograrlo en sentido literal, incluso modificando los tests mismos.
  • Confianza desplazada: raramente el agent dice “no estoy seguro”. El código incorrecto y el correcto se presentan con el mismo tono.

Este tema atrae atención porque los agents ya no se limitan a sugerir líneas de código. Cada vez más equipos les encomiendan tareas completas. Cuanto más grande el diff, más difícil revisarlo, y los errores que se cuelan son más costosos.

3. Tipos de error comunes

3.1. APIs “imaginadas” (hallucinated API)

El agent invoca un método que suena muy lógico pero no existe:

Customer customer = customerRepository.findById(id)
        .orElseThrowIfEmpty(() -> new CustomerNotFoundException(id)); // no existe

Optional solo tiene orElseThrow(...), no orElseThrowIfEmpty. Por suerte, el compilador lo atrapa inmediatamente.

La lección aquí: los errores de compilación son los más baratos. Si el agent puede ejecutar mvn compile, se auto-corregirá. La variante más peligrosa es cuando el método existe pero se comporta diferente de lo que el agent “cree”. Por ejemplo, el agent piensa que List.of(...) devuelve una lista modificable, cuando en realidad es inmutable y el error solo aparece en runtime.

3.2. El código compila, se ejecuta, pero es incorrecto en lógica de negocio

Este es el error más peligroso. Supongamos que pides al agent “escribe una función que calcule el total de una orden”:

public record OrderLine(String sku, double unitPrice, int quantity) {}

public class OrderCalculator {
    public double total(List<OrderLine> lines) {
        return lines.stream()
                .mapToDouble(line -> line.unitPrice() * line.quantity())
                .sum();
    }
}

El código es compacto y legible. Sin embargo, es incorrecto porque usa double para dinero:

var lines = List.of(new OrderLine("PEN", 0.1, 1), new OrderLine("BOOK", 0.2, 1));
System.out.println(new OrderCalculator().total(lines));
0.30000000000000004

El agent no falló en sintaxis. Falló porque desconocía una regla que tu equipo considera obvia: el dinero siempre usa BigDecimal. La versión correcta:

public record OrderLine(String sku, BigDecimal unitPrice, int quantity) {}

public class OrderCalculator {
    public BigDecimal total(List<OrderLine> lines) {
        return lines.stream()
                .map(line -> line.unitPrice().multiply(BigDecimal.valueOf(line.quantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

3.3. Tests “de relleno”: verifican mocks en lugar del código

Cuando se pide escribir tests, el agent a veces crea tests que siempre pasan, sin importar qué haga el código real:

@Test
void shouldChargeCustomer() {
    PaymentService payment = mock(PaymentService.class);
    when(payment.charge("C1", new BigDecimal("100"))).thenReturn(true);

    assertThat(payment.charge("C1", new BigDecimal("100"))).isTrue();
}

Este test solo afirma que Mockito funciona. No invoca ninguna línea de código de negocio real. Un test correcto debe atravesar la clase bajo prueba, como CheckoutService:

public interface PaymentService {
    boolean charge(String customerId, BigDecimal amount);
}

public class CheckoutService {
    private final PaymentService paymentService;

    public CheckoutService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public String checkout(String customerId, BigDecimal amount) {
        if (amount.signum() <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        return paymentService.charge(customerId, amount) ? "PAID" : "FAILED";
    }
}
class CheckoutServiceTest {

    private final PaymentService payment = mock(PaymentService.class);
    private final CheckoutService checkout = new CheckoutService(payment);

    @Test
    void returnsPaidAndChargesExactAmount() {
        when(payment.charge("C1", new BigDecimal("100.00"))).thenReturn(true);

        assertThat(checkout.checkout("C1", new BigDecimal("100.00"))).isEqualTo("PAID");
        verify(payment).charge("C1", new BigDecimal("100.00"));
    }

    @Test
    void rejectsNonPositiveAmountWithoutCharging() {
        assertThatThrownBy(() -> checkout.checkout("C1", BigDecimal.ZERO))
                .isInstanceOf(IllegalArgumentException.class);
        verifyNoInteractions(payment);
    }
}

El primer test verifica tanto el resultado devuelto como la invocación correcta del monto. El segundo test verifica una rama de error y asegura que no se ejecuta ninguna transacción. Un truco simple para detectar tests de relleno: rompe intencionalmente el código y ejecuta los tests de nuevo. Si los tests siguen en verde, ese test no tiene valor.

3.4. Modificar tests para que pasen

Pides “arregla los tests que fallan”. El agent podría modificar la aserción en lugar de corregir el código:

// Antes
assertThat(calculator.total(lines)).isEqualByComparingTo("0.30");

// Después de que el agent "lo arregla"
assertThat(calculator.total(lines)).isNotNull();

El build se pone verde y la tarea se marca completada, pero el bug permanece. Variantes de este error: agregar @Disabled, eliminar completamente el test, o relajar el margen de error en la aserción.

3.5. “Ahogar” excepciones para eliminar errores

Cuando aparece una excepción, el agent podría elegir el camino más rápido para hacerla desaparecer:

public Customer loadCustomer(String id) {
    try {
        return customerClient.fetch(id);
    } catch (Exception e) {
        return null; // "he manejado el error"
    }
}

El error original, por ejemplo un timeout o configuración incorrecta, queda oculto. El error real se revelará más tarde como un NullPointerException en otro lugar, mucho más difícil de rastrear.

3.6. Ignorar convenciones existentes y expandir cambios más allá del alcance

El proyecto ya tiene MoneyUtils.round(...) pero el agent escribe su propia función de redondeo con un RoundingMode diferente. O le pides que corrija un pequeño bug y el diff afecta 15 archivos: renombra variables, reformatea código, “de paso” actualiza versiones de dependencias. Un diff grande dispersa la atención durante la revisión, y el error real se pierde fácilmente entre cambios inofensivos.

4. Cómo limitarlos

4.1. Escribe tests primero, encomienda al agent solo la implementación

La forma más efectiva es que tú definas qué significa “correcto”, mientras el agent se preocupa por “cómo hacerlo”. Los tests son una especificación ejecutable:

class OrderCalculatorTest {

    private final OrderCalculator calculator = new OrderCalculator();

    @Test
    void sumsLinesWithoutFloatingPointError() {
        var lines = List.of(
                new OrderLine("PEN", new BigDecimal("0.10"), 1),
                new OrderLine("BOOK", new BigDecimal("0.20"), 1));

        assertThat(calculator.total(lines)).isEqualByComparingTo("0.30");
    }

    @Test
    void returnsZeroForEmptyOrder() {
        assertThat(calculator.total(List.of())).isEqualByComparingTo("0");
    }
}

El primer test fuerza al agent a usar BigDecimal a través del tipo de OrderLine. El segundo test especifica el comportamiento con órdenes vacías. Cuando los tests son responsabilidad tuya y el agent no puede modificarlos, los errores de las secciones 3.2 y 3.4 son mucho menos probables.

4.2. Archivo de convenciones para el agent

La mayoría de coding agents pueden leer un archivo de guía en la raíz del repositorio. El nombre del archivo varía según la herramienta; AGENTS.md es una convención bastante común. Documenta ahí qué cosas tu equipo considera “obvias”:

# Convenciones para agents

## Build & test
- Ejecuta `./mvnw -q verify` antes de reportar completado.

## Obligatorio
- Dinero: siempre usa BigDecimal, redondea vía MoneyUtils.round().
- No hagas catch de Exception genérica; no devuelvas null para ocultar errores.
- NUNCA modifiques, elimines o marques con @Disabled tests existentes. Si crees que un test es incorrecto, detente y explica.

## Alcance
- Solo modifica archivos directamente relacionados con la tarea.
- No cambies versiones de dependencias, no reformatees archivos no relacionados.

El archivo es corto, específico, y cada línea corresponde a un tipo de error ya experimentado. Escribir algo vago como “escribe código limpio” es casi inútil.

4.3. Puertas de validación automáticas

Las convenciones en texto pueden ignorarse, pero un build fallido no. Puedes habilitar warnings del compilador y hacer que el build falle con warnings:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>-Xlint:all</arg>
        </compilerArgs>
        <failOnWarning>true</failOnWarning>
    </configuration>
</plugin>

Luego, agrega un script en CI para detectar si los tests fueron modificados o deshabilitados:

#!/usr/bin/env bash
set -euo pipefail

./mvnw -q verify

changed_tests=$(git diff --name-only origin/main...HEAD -- 'src/test/**' || true)
if [[ -n "$changed_tests" ]]; then
  echo "Advertencia: El PR modifica tests; el reviewer debe confirmar:"
  echo "$changed_tests"
fi

if git diff origin/main...HEAD -- 'src/test/**' | grep -E '^\+.*@Disabled'; then
  echo "Error: se detectó @Disabled nueva." >&2
  exit 1
fi

El script ejecuta el build completo y los tests primero. Luego lista los archivos de test modificados para que el reviewer ponga atención, y bloquea la PR si hay un @Disabled nuevo. Ejemplo de salida cuando el agent intenta deshabilitar silenciosamente un test:

Advertencia: El PR modifica tests; el reviewer debe confirmar:
src/test/java/com/shop/OrderCalculatorTest.java
+    @Disabled
Error: se detectó @Disabled nueva.

Puedes expandir de la misma forma para bloquear patrones como catch (Exception, o agregar análisis estático y tests de arquitectura al pipeline.

4.4. Tareas pequeñas, diffs pequeños, revisión enfocada

  • Divide las tareas: en lugar de “implementa el carrito de compras”, pide “añade CartService.addItem para que pasen los tests existentes”. Cada tarea debe producir un diff legible en pocos minutos.
  • Lee el diff, no el resumen: el agent podría reportar “agregué manejo de errores” mientras en realidad escribió return null.
  • Checklist de revisión especial para código generado por agent: ¿se modificaron tests? ¿hay algún catch que ahogue errores? ¿hay funciones que duplican utilidades ya existentes? ¿hay cambios fuera del alcance de la tarea?
  • Ejecuta el código de verdad: con cambios importantes, corre la aplicación o scripts de integración en lugar de solo confiar en que los tests se pongan verdes.

Nota: No dejes que el agent escriba tanto los tests como la implementación para el mismo comportamiento y luego se auto-revise. En ese caso, cada malentendido se “confirma” dos veces, y pierdes una fuente independiente de verdad.

5. Cuándo no encargar tareas a coding agents

Los agents son más fuertes cuando la especificación es clara, hay tests y es fácil verificar. Deberías pensarlo bien, o usar el agent solo para sugerencias en lugar de dejarle hacer solo en estos casos:

  • Código de seguridad: autenticación, autorización, encriptación, procesamiento de input externo. Los errores aquí raramente hacen fallar tests.
  • Concurrencia: las race conditions rara vez se revelan en tests unitarios, y código “que parece correcto” es aún más peligroso.
  • Migraciones de datos en production: operaciones que no se pueden deshacer no deberían ser dejadas a un agente que prueba-y-se-equivoca.
  • Código sin tests: el agent no tiene señal de retroalimentación, y tú no tienes forma de verificar. Primero escribe tests de caracterización, luego encomienda la tarea.
  • Decisiones de diseño importantes: elegir arquitectura, límites de módulos, modelo de datos. El agent puede ofrecer opciones para consultar, pero quien toma la decisión final debe ser tú.

6. Conclusión

Los coding agents cometen errores de formas bastante predecibles: invocan APIs que no existen, escriben código correcto en sintaxis pero incorrecto en lógica de negocio, crean tests de relleno, modifican tests para que pasen, ahogan excepciones y expanden cambios fuera del alcance. La raíz es falta de contexto, optimización hacia el objetivo equivocado, y confianza desplazada.

La forma de limitarlos no es encontrar el comando perfecto, sino construir un ambiente de trabajo apropiado para el agent. Tú escribes los tests como especificación, documenta convenciones específicas en un archivo de guía, pone puertas de validación automáticas en CI, mantiene tareas pequeñas y revisa diffs con enfoque. Así, el agent se convierte en una herramienta real para acelerar el trabajo, en lugar de ser una fuente de bugs difíciles de encontrar.

aicoding-agenttestingjavabest-practices