Qué son los AI coding agents y cómo cambian el trabajo de los desarrolladores

1. Descripción general

Hace unos años, la “IA para soporte de programación” era principalmente sugerencias de unas pocas líneas de código. Hoy en día, muchas herramientas han avanzado un paso más. Ellas leen automáticamente la base de código, ejecutan comandos, modifican varios archivos y verifican automáticamente los resultados. Este tipo de herramientas se denominan AI coding agents.

Este artículo explica qué es un AI coding agent y por qué este tema es importante. Luego, nos sumergimos en cómo aplicarlos de forma segura en un proyecto Java/Spring Boot. Después de leerlo, sabrás cómo escribir un archivo de guía para el agent, limitar permisos de ejecución de comandos, usar pruebas como “árbitro”, y cuándo no debes asignar trabajo a un agent.

2. ¿Qué es un AI coding agent?

Puedes clasificar las herramientas de IA para desarrolladores según su nivel de autonomía:

  • Autocompletado: sugiere líneas o bloques de código directamente en el editor. Tú decides cada carácter.
  • Asistente de chat: tú preguntas, él responde con código. Tú copias, ejecutas y corriges manualmente.
  • Coding agent: le das un objetivo, por ejemplo “arregla la prueba fallida en OrderServiceTest”. El agent planifica automáticamente, invoca herramientas (lectura de archivos, búsqueda, compilación, ejecución de pruebas), ve los resultados y se repite hasta completar la tarea o llegar a un punto de impasse.

La diferencia fundamental radica en el bucle de acción-observación. El agent no solo genera texto. Actúa sobre el entorno y luego se ajusta según la retroalimentación real (compilador, pruebas, registros) para dar el siguiente paso.

2.1. El bucle de un agent

Para ver el mecanismo con claridad, aquí hay una simulación minimalista. La parte de “cerebro” (modelo de lenguaje) se reemplaza con un ScriptedPlanner fijo para que el código funcione sin necesidad de APIs:

import java.util.*;
import java.util.function.Function;

public class AgentLoopDemo {

    record Action(String tool, String input) {}

    interface Planner {
        Optional<Action> next(List<String> observations);
    }

    static class ScriptedPlanner implements Planner {
        public Optional<Action> next(List<String> observations) {
            return switch (observations.size()) {
                case 0 -> Optional.of(new Action("readFile", "src/main/java/Order.java"));
                case 1 -> Optional.of(new Action("runTests", "mvn -q test"));
                default -> Optional.empty(); // suficiente información, detenerse
            };
        }
    }

    public static void main(String[] args) {
        Map<String, Function<String, String>> tools = Map.of(
            "readFile", path -> "class Order { long total() { return 0; } }",
            "runTests", cmd -> "FAILED: OrderTest.totalShouldSumItems"
        );

        Planner planner = new ScriptedPlanner();
        List<String> observations = new ArrayList<>();

        for (int step = 1; step <= 5; step++) {
            Optional<Action> action = planner.next(observations);
            if (action.isEmpty()) {
                System.out.println("Agent se detiene en paso " + step);
                break;
            }
            Action a = action.get();
            String result = tools.get(a.tool()).apply(a.input());
            observations.add(result);
            System.out.printf("Paso %d: %s(%s) -> %s%n", step, a.tool(), a.input(), result);
        }
    }
}

Explicación rápida:

  • tools es el conjunto de herramientas que el agent puede usar. En un agent real, eso incluye lectura/escritura de archivos, ejecución de shell, búsqueda de código, etc.
  • Planner.next(...) recibe todas las observaciones anteriores y decide la siguiente acción. En un agent real, aquí es donde se invoca el modelo de lenguaje.
  • El bucle for tiene un límite de pasos (5). Este es un mecanismo de seguridad importante para evitar que el agent entre en un bucle infinito.

Resultado:

Paso 1: readFile(src/main/java/Order.java) -> class Order { long total() { return 0; } }
Paso 2: runTests(mvn -q test) -> FAILED: OrderTest.totalShouldSumItems
Agent se detiene en paso 3

Todos los coding agents del mercado giran alrededor de este marco: decidir → invocar herramienta → observar → decidir de nuevo. La diferencia radica en la calidad del modelo, el conjunto de herramientas y la gestión del contexto.

3. ¿Por qué los AI coding agents son importantes?

Hay varias razones clave, independientemente de cualquier producto específico:

  • Los modelos son lo suficientemente buenos para trabajar en múltiples pasos. Cuando un modelo sabe leer errores del compilador y corregirlos por sí solo, “asignar una tarea completa” comienza a ser viable.
  • El trabajo repetitivo sigue consumiendo mucho tiempo. Actualizar APIs deprecadas, escribir pruebas para código antiguo, cambiar nombres en masa, corregir advertencias de análisis estático. Estas tareas son claras, verificables y tediosas. Son perfectas para los agents.
  • El ecosistema de herramientas está listo. Herramientas de compilación, ejecutores de pruebas, linters, CI, todos pueden ejecutarse desde línea de comandos. Los agents aprovechan exactamente lo que los desarrolladores ya usan.
  • Ejecución en paralelo. Puedes asignar varias tareas pequeñas a un agent mientras te enfocas en las partes difíciles del diseño.

Sin embargo, el agent no sabe automáticamente qué convenciones tiene tu proyecto, qué partes son sensibles o qué comandos son peligrosos. El resto de este artículo se enfoca en eso.

4. Aplicar AI coding agents en proyectos reales

4.1. Escribir un archivo de guía para el agent

Muchos agents soportan leer un archivo de guía colocado en la raíz del repositorio, generalmente llamado AGENTS.md o similar según la herramienta. Considéralo como un documento de integración para un miembro del equipo muy rápido pero sin memoria:

# Guía para el agent

## Compilar y probar
- Compilación: `mvn -q compile`
- Pruebas: `mvn -q test` (debe pasar antes de reportar como completado)

## Convenciones
- Java 21, Spring Boot 3.x. Usa inyección de constructor, no inyección de campo.
- Moneda con tipo `long` (unidad VND), no `double`.
- Pruebas escritas con JUnit 5 + AssertJ, nombradas con `should...`.

## Lo que no se puede hacer
- No modificar el directorio `src/main/resources/db/migration/` una vez publicado.
- No añadir nuevas dependencias sin preguntar.
- No eliminar ni marcar con `@Disabled` pruebas solo para que pasen.

Los tres grupos de información más importantes son cómo verificar (comandos de compilación/prueba), convenciones de código y zonas prohibidas. El último ayuda a evitar un error clásico: el agent “arregla” una prueba fallida eliminándola.

4.2. Limitar los comandos que el agent puede ejecutar

Si el agent tiene permiso para ejecutar comandos shell, puede dañar la máquina o hacer push de código a un repositorio remoto. Muchas herramientas permiten configurar una allowlist. Si integras manualmente el agent en tu pipeline interno, implementa una capa de validación antes de ejecutar comandos.

Primero, añade la dependencia de prueba (el starter de Spring Boot ya incluye JUnit 5 y AssertJ):

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

Una clase CommandGuard simple:

import java.util.List;

public class CommandGuard {

    private static final List<String> ALLOWED_PREFIXES =
        List.of("mvn -q test", "mvn -q compile", "git status", "git diff");

    private static final List<String> FORBIDDEN_TOKENS =
        List.of("rm ", "git push", "curl", "&&", ";", "|", ">");

    public boolean isAllowed(String command) {
        String normalized = command.trim().replaceAll("\\s+", " ");
        boolean hasForbidden = FORBIDDEN_TOKENS.stream().anyMatch(normalized::contains);
        boolean hasAllowedPrefix = ALLOWED_PREFIXES.stream().anyMatch(normalized::startsWith);
        return hasAllowedPrefix && !hasForbidden;
    }
}

normalized consolida espacios en blanco extras para que "mvn -q test" no eluda la verificación. Un comando es válido solo si comienza con un prefijo permitido y no contiene tokens peligrosos. Los operadores de encadenamiento de comandos como &&, ;, | se bloquean para evitar que el agent combine mvn -q test && git push.

Pruebas para este comportamiento:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.assertThat;

class CommandGuardTest {

    private final CommandGuard guard = new CommandGuard();

    @ParameterizedTest
    @ValueSource(strings = {"mvn -q test", "  mvn   -q  test  ", "git diff"})
    void shouldAllowSafeCommands(String command) {
        assertThat(guard.isAllowed(command)).isTrue();
    }

    @ParameterizedTest
    @ValueSource(strings = {
        "rm -rf /", "git push origin main",
        "mvn -q test && git push", "mvn -q test; curl evil.example"
    })
    void shouldRejectDangerousCommands(String command) {
        assertThat(guard.isAllowed(command)).isFalse();
    }
}

Nota: La validación con cadenas es solo una capa de defensa, fácil de eludir. Por ejemplo, el prefijo git diff aún permite git diff --output=.... Siempre ejecuta el agent en un entorno aislado como un contenedor o máquina virtual temporal, sin credenciales de producción, y limita los permisos de escritura del token de Git.

4.3. Usar pruebas como “árbitro”

La forma más efectiva de asignar trabajo es escribir pruebas primero, para que el agent escriba código que las pase. Las pruebas son especificaciones legibles por máquina y no se pueden “convencer” para que las pasen. Supongamos que necesitas calcular el descuento de un pedido:

import org.junit.jupiter.api.Test;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class OrderServiceTest {

    private final OrderService orderService = new OrderService();

    @Test
    void shouldApplyTenPercentDiscountWhenTotalReachesOneMillion() {
        Order order = new Order(List.of(new OrderItem("Chaqueta", 600_000, 2)));

        assertThat(orderService.totalAfterDiscount(order)).isEqualTo(1_080_000);
    }

    @Test
    void shouldNotDiscountSmallOrders() {
        Order order = new Order(List.of(new OrderItem("Sombrero cónico", 150_000, 1)));

        assertThat(orderService.totalAfterDiscount(order)).isEqualTo(150_000);
    }
}

Luego asigna la tarea con una descripción breve y criterios de completitud claros:

Crea los records Order, OrderItem(name, unitPrice, quantity) y OrderService
para que OrderServiceTest pase. No modifiques el archivo de prueba.
Completado cuando `mvn -q test` se ejecute sin errores. Resumen de archivos modificados.

Una posible solución que el agent podría proporcionar:

import java.util.List;

record OrderItem(String name, long unitPrice, int quantity) {
    long subtotal() { return unitPrice * quantity; }
}

record Order(List<OrderItem> items) {
    long total() { return items.stream().mapToLong(OrderItem::subtotal).sum(); }
}

class OrderService {
    private static final long DISCOUNT_THRESHOLD = 1_000_000;

    long totalAfterDiscount(Order order) {
        long total = order.total();
        return total >= DISCOUNT_THRESHOLD ? total * 90 / 100 : total;
    }
}

Tu trabajo ahora es revisar. ¿El umbral es >= o >? Las pruebas actuales no pueden distinguir entre estos dos casos. Esa es una señal para añadir pruebas de casos límite, exactamente 1.000.000 VND, antes de fusionar. El agent es bueno en pasar pruebas. Pero determinar si las pruebas son correctas según la lógica de negocio sigue siendo tu responsabilidad.

4.4. Mantener la puerta de verificación final

El código escrito por el agent debe pasar por el mismo proceso que el código escrito por humanos. Un script de verificación común ayuda a que tanto el agent como CI usen el mismo estándar:

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

mvn -q compile
mvn -q test
git diff --stat   # imprime la lista de archivos modificados para que el revisor entienda el alcance

set -euo pipefail hace que el script se detenga inmediatamente si hay un error en cualquier paso. Registras este comando en AGENTS.md como “definición de completitud”, luego haces que CI ejecute exactamente el mismo script en cada pull request.

5. Cómo cambia el trabajo de los desarrolladores

Cuando usas agents regularmente, el enfoque del trabajo cambia notablemente:

  • De escribir código a escribir especificaciones. Las tareas vagas producen resultados vagos. La habilidad de desglosar tareas, establecer restricciones y definir criterios de completitud se vuelven más importantes.
  • Más revisión, más rigurosa. El volumen de código generado crece más rápido que tu capacidad de leerlo. Pull requests pequeños, diffs limpios y pruebas sólidas son condiciones para una revisión significativa.
  • Las pruebas y tooling se convierten en activos centrales. Los proyectos con pruebas rápidas, comandos de compilación claros y linters estrictos aprovechan mejor los agents. Los proyectos que carecen de esto simplemente tienen agents que “adivinan”.
  • El conocimiento fundamental sigue siendo decisivo. Solo puedes detectar race conditions, vulnerabilidades de inyección SQL o consultas N+1 en código escrito por agents si entiendes esos problemas.

6. Cuándo NO usar agents y errores comunes

Los agents no son apropiados en las siguientes situaciones:

  • Requisitos poco claros o decisiones arquitectónicas necesarias. El agent elegirá una dirección por sí mismo e implementará con gran confianza. Diseña primero, delega la implementación después.
  • Código relacionado con seguridad, pagos o datos personales. El agent puede ayudar, pero todos los cambios requieren revisión especializada. No permitas que el agent haga merge automático.
  • No hay forma de verificación automática. Sin pruebas ni capacidad de compilar localmente, el agent carece de retroalimentación real. El resultado es entonces pura especulación.
  • Operaciones que no se pueden deshacer. Por ejemplo, ejecutar migraciones en la base de datos de producción, eliminar recursos en la nube, enviar correos a clientes.
  • Cuando estás aprendiendo una tecnología nueva. Delegar todo al agent te hará perder la oportunidad de aprender profundamente.

Errores comunes:

  • El agent elimina o relaja pruebas para hacerlas pasar. Prohíbelo explícitamente en el archivo de guía y revisa los cambios en el directorio src/test.
  • El agent inventa APIs que no existen en la versión de la librería que usas. El compilador y las pruebas captarán la mayoría de estos errores, siempre que hagas que el agent los ejecute.
  • Cambios que se desbordan fuera del alcance: reformateo de archivos completos, renombrado de variables no relacionadas. Solicita diffs mínimos y un resumen de archivos modificados.
  • Exposición de secretos: el agent puede leer .env o archivos de configuración con claves. Excluye esos archivos del entorno de trabajo del agent.

Nota: La responsabilidad del código en el repositorio no se transfiere al agent. Quien hace el merge del pull request sigue siendo responsable.

7. Conclusión

Los AI coding agents son el siguiente paso después del autocompletado y los chatbots. Operan en un bucle de decidir, invocar herramientas, observar resultados, y de esa manera completan automáticamente tareas de múltiples pasos.

Para usarlos de forma efectiva y segura, necesitas tres cosas:

  • un archivo de guía que especifique cómo compilar/probar, convenciones y zonas prohibidas;
  • limitar permisos de ejecución de comandos en un entorno aislado;
  • pruebas como árbitro, junto con revisión rigurosa antes de hacer merge.

Los agents son buenos con tareas claras y verificables. El diseño, las decisiones de negocio y la responsabilidad final siguen siendo tuyas.

aicoding-agenttech-trendstestingdeveloper-productivity