Grab usa AI Agent para aumentar la productividad: ejemplos prácticos
1. Descripción General
Muchas grandes empresas tecnológicas como Grab están transitando de “usar chatbot para hacer preguntas” a “usar AI Agent para que actúe automáticamente” y aumentar la productividad técnica: revisar código automáticamente, resumir pull requests, hacer triage de incidentes. Este artículo explica qué es realmente un AI Agent y cómo es diferente de una simple llamada a un LLM, la arquitectura Planner – Tool – Executor común, y cómo puedes construir uno similar usando Java 21 + Spring AI. Después de leerlo, serás capaz de escribir un pequeño agent que llame herramientas reales (leer archivos, llamar APIs internas) y sabrás cuándo no deberías llevar un agent a producción.
2. AI Agent vs. una llamada LLM típica
Una simple llamada a un LLM recibe un prompt y retorna texto — sin estado, sin acciones. Un AI Agent tiene tres características:
- Planificación automática: divide una solicitud grande en pasos pequeños.
- Llamadas a herramientas (tool calling / function calling): el agent puede llamar APIs, leer archivos, ejecutar queries — no solo genera texto.
- Ciclo de observación-acción: el agent observa el resultado que retorna la herramienta, decide el siguiente paso, repite hasta completar la tarea.
Ejemplo real en grandes equipos de ingeniería: el agent lee el diff de un pull request, llama la herramienta “obtener convenciones de código del equipo”, llama la herramienta “ejecutar análisis estático”, luego sintetiza todo en un comentario de review — todo sin que nadie tenga que esperar cada paso.
Nota: si tu tarea es solo “resumir texto” o “traducir idiomas”, una simple llamada LLM es suficiente. No le pongas la etiqueta “agent” a todo lo que llama la API de OpenAI — la complejidad innecesaria hará que el sistema sea difícil de depurar.
3. Arquitectura básica del agent: Planner – Tool – Executor
El modelo más común consta de tres partes:
Solicitud del usuario
│
▼
[Planner] -- genera lista de pasos a hacer, o elige la herramienta siguiente
│
▼
[Tool Executor] -- llama herramientas reales: API, BD, sistema de archivos
│
▼
[Observación] -- el resultado de la herramienta se devuelve al Planner
│
└── repite hasta que Planner diga "hecho"
El Planner suele ser un LLM al que se le proporciona un “menú” de herramientas (nombre, descripción, schema de parámetros). El LLM no ejecuta código por sí mismo — solo decide qué herramienta llamar con qué parámetros; tu código es quien ejecuta realmente.
3.1. Dependencias necesarias
Spring AI proporciona una abstracción bastante limpia para este patrón:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini
4. Ejemplo práctico: Code Review Agent
Digamos que quieres un agent que reciba el diff de un servicio Order, que automáticamente compruebe convenciones y sugiera correcciones. Primero, declara las herramientas que el agent puede usar — aquí, una herramienta que lee convenciones de código de un archivo interno.
@Component
public class ConventionTool {
@Tool(description = "Obtiene la convención de código actual del equipo para un lenguaje")
public String getConvention(String language) {
return switch (language.toLowerCase()) {
case "java" -> "Usa record para DTO; no uses @Autowired en campos; usa SLF4J para logging.";
default -> "No hay convención específica para este lenguaje.";
};
}
}
@Tooles una anotación de Spring AI que marca este método como una función que el LLM puede llamar.descriptiones lo que el LLM lee para decidir “¿debería llamar esta herramienta?” — escribe clara y específicamente para que el LLM elija correctamente.
A continuación, el servicio de orquestación — que juega el rol de Planner + Executor:
@Service
public class CodeReviewAgent {
private final ChatClient chatClient;
public CodeReviewAgent(ChatClient.Builder builder, ConventionTool conventionTool) {
this.chatClient = builder
.defaultTools(conventionTool)
.build();
}
public String review(String diff) {
return chatClient.prompt()
.system("Eres un reviewer. Usa la herramienta para obtener convenciones si es necesario antes de hacer comentarios.")
.user("Revisa el siguiente diff:\n" + diff)
.call()
.content();
}
}
defaultTools(conventionTool)registra la herramienta para todas las solicitudes delchatClient.- Cuando llamas
review(diff), Spring AI automáticamente envía el “menú de herramientas” junto con el prompt al modelo. Si el modelo decide llamargetConvention("java"), el framework ejecuta automáticamente el método Java correspondiente, obtiene el resultado, lo devuelve al modelo para que genere la respuesta final — todo este ciclo ocurre en un único comandocall().
Intenta llamarlo:
@RestController
@RequestMapping("/api/review")
public class ReviewController {
private final CodeReviewAgent agent;
public ReviewController(CodeReviewAgent agent) {
this.agent = agent;
}
@PostMapping
public String review(@RequestBody String diff) {
return agent.review(diff);
}
}
Salida típica:
La convención del equipo requiere usar record para DTO y SLF4J para logging.
En este diff, la clase OrderDto es una clase normal con getters/setters —
deberías cambiarla a record. La línea 42 usa System.out.println en lugar de logger,
necesita corrección según la convención.
El punto clave: no escribes la lógica “si ves una clase normal sugiere record” — el modelo deduce automáticamente basándose en la convención obtenida de la herramienta. Por eso el agent “aumenta productividad”: la revisión ya no depende de que alguien recuerde manualmente las convenciones.
5. Agent llamando múltiples herramientas: PR Summary Agent
En realidad, los grandes equipos generalmente necesitan agents que llamen múltiples herramientas seguidas — por ejemplo: obtener lista de archivos cambiados, obtener cobertura de tests, luego resumir el PR para el reviewer.
@Component
public class PullRequestTools {
private final GitService gitService;
private final CoverageService coverageService;
public PullRequestTools(GitService gitService, CoverageService coverageService) {
this.gitService = gitService;
this.coverageService = coverageService;
}
@Tool(description = "Obtiene la lista de archivos cambiados en un PR por id")
public List<String> listChangedFiles(long prId) {
return gitService.changedFiles(prId);
}
@Tool(description = "Obtiene el porcentaje de cobertura de tests de un archivo")
public double getCoverage(String filePath) {
return coverageService.coverageOf(filePath);
}
}
- Dos herramientas independientes, cada una hace una cosa pequeña, fácil de probar por separado.
- El modelo decidirá automáticamente el orden de llamadas: típicamente llama
listChangedFilesprimero, luego llamagetCoveragepara cada archivo retornado — este es el ciclo “observe-act” mencionado en la sección 3.
@Service
public class PullRequestSummaryAgent {
private final ChatClient chatClient;
public PullRequestSummaryAgent(ChatClient.Builder builder, PullRequestTools tools) {
this.chatClient = builder.defaultTools(tools).build();
}
public String summarize(long prId) {
return chatClient.prompt()
.system("Resume el PR incluyendo: número de archivos cambiados, archivos con cobertura bajo 70% que necesitan atención.")
.user("Resume el PR id " + prId)
.call()
.content();
}
}
Un agent así resuelve correctamente el problema de productividad: el reviewer no necesita abrir cada archivo manualmente para revisar la cobertura — el agent ya ha sintetizado todo en un resumen.
6. Probando el agent
La parte más difícil cuando llevas un agent a producción no es lograr llamar al LLM, sino asegurar que la herramienta se llame correctamente cuando cambian los inputs. Por eso conviene separar pruebas unitarias para herramientas (determinísticas) de pruebas de integración para el agent (puede ser necesario mockear ChatClient).
class ConventionToolTest {
private final ConventionTool tool = new ConventionTool();
@Test
void shouldReturnJavaConvention() {
String result = tool.getConvention("Java");
assertThat(result)
.contains("record")
.contains("SLF4J");
}
@Test
void shouldReturnDefaultMessageForUnknownLanguage() {
String result = tool.getConvention("cobol");
assertThat(result).isEqualTo("No hay convención específica para este lenguaje.");
}
}
- Probar la herramienta es como probar un método normal, sin necesidad de mockear el LLM — esta es la parte que controlas 100%.
- Para la parte donde el agent llama al LLM real, deberías escribir pruebas a nivel de “contrato”: aserta que la respuesta contiene palabras clave obligatorias (por ejemplo “record”, “coverage”), evita assertions de strings exactos porque la salida del LLM no es absolutamente estable entre llamadas.
@Test
void reviewShouldMentionConventionKeyword() {
String diff = "class OrderDto { private String id; }";
String result = codeReviewAgent.review(diff);
assertThat(result.toLowerCase()).containsAnyOf("record", "convention");
}
Nota: las pruebas que llaman al LLM real son costosas y lentas — conviene separarlas en una suite de pruebas independiente (ej. con tag
@Tag("llm-integration")) y ejecutarlas solo en un pipeline separado, no en cada commit.
7. Errores comunes al implementar agents
- Descripción de herramienta vaga: si la descripción no es clara, el modelo elige la herramienta equivocada o no la llama cuando debería. Escribe descripción como si escribieras Javadoc para alguien que lee por primera vez.
- Sin límite de iteraciones: el agent puede quedar atrapado en un ciclo de llamadas continuas si la lógica del punto final no es clara. Siempre establece
maxIterationso timeout en la capa de orquestación. - Dar al agent permisos para escribir datos sensibles: no hagas que el agent tenga una herramienta “desplegar a producción” o “eliminar datos” sin un paso de confirmación humana. El agent debe tener permisos amplios para leer, pero limitados para escribir.
- No registrar el proceso de llamadas a herramientas: cuando el agent responde incorrectamente, necesitas saber qué herramientas llamó, con qué parámetros, para poder depurar. Sin este registro, rastrear errores es prácticamente imposible.
- Usar agent para algo que se puede resolver con reglas simples: si la lógica “si X entonces Y” es fija, escribir código normal es más rápido, más barato y más fácil de probar que un agent.
8. Cuándo no usar AI Agent
Un agent es adecuado para trabajo que tiene muchos pasos no predefinidos, necesita sintetizar información de múltiples fuentes, y acepta pequeños márgenes de error (sugerencias de review, resúmenes). Un agent no es apropiado para:
- Tareas que requieren precisión absoluta (procesamiento de pagos, transacciones financieras).
- Tareas con proceso fijo y poco cambiante — un motor de reglas o workflow engine (Camunda, Temporal) es más estable y más fácil de auditar.
- Sistemas que necesitan latencia baja, porque cada ciclo de tool-calling tiene un round-trip adicional hacia el LLM.
9. Conclusión
AI Agent aporta valor claro de productividad cuando se aplica correctamente: tareas repetitivas de ingeniería, múltiples pasos, que necesitan sintetizar información dispersa — revisión de código, resúmenes de PR, triage de errores. En lugar de construir un agent que “sepa hacer de todo”, deberías empezar con pocas herramientas pequeñas y claras, con pruebas propias, y dejar que el Planner (LLM) decida cómo orquestarlas. Lo más importante es separar permisos: da al agent permiso amplio para leer, permiso limitado para escribir, y siempre mantén la revisión humana en el paso final para acciones con grandes consecuencias.