5 Patrones de Agentes IA - explicación detallada con ejemplos Java
1. Descripción general
Cuando combinas un LLM con algunas líneas de código para que “se ejecute solo”, ya tienes un Agente IA básico — pero ese tipo de agente suele preguntar y responder sin más, no sabe autocorregirse, no sabe llamar herramientas, no sabe dividir el trabajo. Este artículo explica 5 patrones de diseño que hacen los agentes “más inteligentes”: Reflection, Tool Use, Planning, Multi-Agent y ReAct. Cada patrón incluye un ejemplo de código Java corto (Spring AI) que funciona, con explicación de cuándo usarlo y cuándo no. Después de leerlo, podrás elegir el patrón correcto para tu problema en lugar de meter todo en un prompt gigante.
Antes de entrar en detalles, necesitas esta dependencia de Spring AI para los ejemplos:
<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
Nota: El código en este artículo ilustra la estructura del patrón, no es un producto production — necesitarás añadir reintentos, timeouts, logging al desplegar en producción.
2. Patrón Reflection: autocrítica y corrección de respuestas
Reflection es el patrón más simple: el agente genera una respuesta, luego usa el mismo LLM (u otro) para evaluarla/criticarla, y después la corrige. Este ciclo es como escribir un borrador y releerlo antes de enviarlo.
2.1. Flujo de trabajo
- Generate: el agente produce la respuesta inicial.
- Critique: el agente (u otro agente) actúa como “revisor”, señalando errores.
- Revise: el agente corrige basándose en la crítica, puede repetirse 2–3 veces hasta alcanzar calidad.
@Service
public class ReflectionAgent {
private final ChatClient chatClient;
public ReflectionAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String answerWithReflection(String question, int maxRounds) {
String draft = chatClient.prompt()
.user("Responde brevemente la pregunta: " + question)
.call()
.content();
for (int round = 0; round < maxRounds; round++) {
String critique = chatClient.prompt()
.user("""
Eres un revisor exigente. Señala errores y omisiones
en la siguiente respuesta. Si es correcta, devuelve "OK".
Pregunta: %s
Respuesta: %s
""".formatted(question, draft))
.call()
.content();
if (critique.strip().equalsIgnoreCase("OK")) {
break;
}
draft = chatClient.prompt()
.user("Corrige la respuesta según esta retroalimentación: " + critique
+ "\nRespuesta anterior: " + draft)
.call()
.content();
}
return draft;
}
}
answerWithReflection genera un borrador inicial, luego itera hasta maxRounds veces: llama al LLM en rol de revisor, si devuelve “OK” se detiene, sino genera una versión corregida. El resultado es una respuesta que tiende a ser más precisa que llamar al LLM una sola vez.
Nota: Reflection cuesta 2–3 veces más llamadas al LLM que lo normal. Úsalo solo para tareas que requieren alta precisión (revisión de código, cálculos, redacción de contratos), no para chat común porque es lento y costoso.
3. Patrón Tool Use: permitir que el agente llame herramientas externas
El LLM no sabe la hora actual, no puede acceder a bases de datos, no calcula con precisión números grandes. El patrón Tool Use permite al agente “llamar funciones” cuando lo necesita — el LLM decide qué herramienta usar, el código ejecuta esa herramienta y devuelve el resultado al LLM para que continúe razonando.
@Component
public class OrderTools {
private final OrderRepository orderRepository;
public OrderTools(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Tool(description = "Consulta el estado de un pedido por su ID")
public String getOrderStatus(String orderId) {
return orderRepository.findById(orderId)
.map(Order::getStatus)
.orElse("Pedido no encontrado: " + orderId);
}
@Tool(description = "Calcula el total incluyendo impuesto del 10%")
public double calculateTotal(double subtotal) {
return subtotal * 1.1;
}
}
@Service
public class CustomerSupportAgent {
private final ChatClient chatClient;
public CustomerSupportAgent(ChatClient.Builder builder, OrderTools orderTools) {
this.chatClient = builder
.defaultTools(orderTools)
.build();
}
public String handle(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}
@Tool marca el método como una función que el LLM puede llamar, description ayuda al modelo a entender cuándo usarla. Cuando defaultTools(orderTools) se registra, Spring AI automáticamente: envía el esquema de las herramientas al LLM, recibe una solicitud de llamada (en JSON), ejecuta el método Java correspondiente, devuelve el resultado al LLM para completar la respuesta.
Por ejemplo, si el usuario pregunta “¿Dónde está el pedido ORD-001?”, el agente decide automáticamente llamar a getOrderStatus("ORD-001") sin que escribas lógica manual para parsear intenciones.
Usuario: ¿Dónde está el pedido ORD-001?
Agente: El pedido ORD-001 está actualmente en estado "En envío".
Test simple con JUnit 5 + Mockito para verificar que la herramienta funciona, sin llamar al LLM real:
class OrderToolsTest {
private final OrderRepository repository = mock(OrderRepository.class);
private final OrderTools tools = new OrderTools(repository);
@Test
void shouldReturnStatusWhenOrderExists() {
when(repository.findById("ORD-001"))
.thenReturn(Optional.of(new Order("ORD-001", "En envío")));
String result = tools.getOrderStatus("ORD-001");
assertThat(result).isEqualTo("En envío");
}
}
Nota: Siempre valida los parámetros antes de que la herramienta acceda a bases de datos o APIs reales — el LLM podría pasar parámetros incorrectos (ej: orderId vacío) si el prompt no es claro.
4. Patrón Planning: dividir tareas grandes en pasos pequeños
Cuando la solicitud es compleja (“escribe un informe comparando 3 proveedores, con tabla de precios y recomendación”), pedir al LLM hacerlo de una vez tiende a omitir pasos. El patrón Planning separa dos roles: Planner crea una lista de pasos, Executor ejecuta cada paso secuencialmente.
public record PlanStep(int order, String description) {}
@Service
public class PlannerAgent {
private final ChatClient chatClient;
public PlannerAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public List<PlanStep> createPlan(String goal) {
String json = chatClient.prompt()
.user("""
Divide el siguiente objetivo en máximo 5 pasos ejecutables,
devuelve JSON array en forma [{"order":1,"description":"..."}]:
%s
""".formatted(goal))
.call()
.content();
return parseSteps(json);
}
}
@Service
public class ExecutorAgent {
private final ChatClient chatClient;
public ExecutorAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String runPlan(String goal, List<PlanStep> steps) {
StringBuilder context = new StringBuilder();
for (PlanStep step : steps) {
String result = chatClient.prompt()
.user("Objetivo: " + goal
+ "\nContexto de lo ya hecho: " + context
+ "\nEjecuta este paso: " + step.description())
.call()
.content();
context.append("Paso ").append(step.order())
.append(": ").append(result).append("\n");
}
return context.toString();
}
}
PlannerAgent solo genera la lista de pasos en JSON, ExecutorAgent ejecuta cada paso y acumula el context para que el siguiente paso sepa qué se hizo antes. Separar planificador/ejecutor facilita el debug: si el resultado es incorrecto, verifica el plan primero (¿es sensato?), luego cada paso de ejecución.
Plan para "escribir informe comparativo de 3 proveedores":
1. Recopilar información de precios y características de cada proveedor
2. Crear tabla comparativa
3. Analizar ventajas y desventajas
4. Proporcionar recomendación
5. Escribir resumen ejecutivo
Nota: El patrón Planning es efectivo cuando la tarea tiene múltiples pasos interdependientes. Para preguntas de un paso (ej: “resume este párrafo”), añadir planificación solo ralentiza y gasta tokens innecesariamente.
5. Patrón Multi-Agent: múltiples agentes especializados colaborando
En lugar de un agente que “sabe de todo”, Multi-Agent divide el trabajo entre agentes especializados: agente investigador, agente redactor, agente revisor… y un Orchestrator coordina el flujo entre ellos. Es como tener un pequeño equipo donde cada persona es experta en algo.
@Service
public class ResearchAgent {
private final ChatClient chatClient;
public ResearchAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String research(String topic) {
return chatClient.prompt()
.user("Lista los 5 datos más importantes sobre: " + topic)
.call()
.content();
}
}
@Service
public class WriterAgent {
private final ChatClient chatClient;
public WriterAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String write(String topic, String researchNotes) {
return chatClient.prompt()
.user("Escribe un artículo de 200 palabras sobre '" + topic
+ "' basándote en estos datos: " + researchNotes)
.call()
.content();
}
}
@Service
public class OrchestratorAgent {
private final ResearchAgent researchAgent;
private final WriterAgent writerAgent;
public OrchestratorAgent(ResearchAgent researchAgent, WriterAgent writerAgent) {
this.researchAgent = researchAgent;
this.writerAgent = writerAgent;
}
public String produceArticle(String topic) {
String notes = researchAgent.research(topic);
return writerAgent.write(topic, notes);
}
}
OrchestratorAgent no llama al LLM directamente sino que coordina: llama a ResearchAgent para obtener datos crudos, los pasa a WriterAgent para redactar el artículo final. Cada agente tiene su propio prompt, optimizado para una tarea específica, así que típicamente la calidad de cada parte es mejor que si metieras todo en un prompt largo.
Test con mock para verificar que la coordinación es correcta, sin depender del LLM:
class OrchestratorAgentTest {
@Test
void shouldPassResearchNotesToWriter() {
ResearchAgent research = mock(ResearchAgent.class);
WriterAgent writer = mock(WriterAgent.class);
when(research.research("Spring AI")).thenReturn("nota-A, nota-B");
when(writer.write("Spring AI", "nota-A, nota-B")).thenReturn("Artículo completo");
OrchestratorAgent orchestrator = new OrchestratorAgent(research, writer);
String article = orchestrator.produceArticle("Spring AI");
assertThat(article).isEqualTo("Artículo completo");
verify(writer).write("Spring AI", "nota-A, nota-B");
}
}
Nota: Multi-Agent aumenta latencia (múltiples llamadas al LLM en serie) y complejidad del sistema. Separa agentes solo cuando cada uno realmente tiene un prompt/contexto distinto y bien definido — dividir demasiado (un agente por línea) solo añade overhead innecesario.
6. Patrón ReAct: razonamiento entrelazado con acción
ReAct (Reasoning + Acting) es un patrón que combina Tool Use con un ciclo de razonamiento: el agente piensa (Thought) → actúa (Action, ej: llama una herramienta) → observa el resultado (Observation) → repite hasta tener suficiente información para responder. A diferencia de Tool Use simple (llama 1 herramienta y listo), ReAct permite al agente llamar múltiples herramientas en secuencia flexible, decidiendo el siguiente paso según las observaciones previas.
@Service
public class ReActAgent {
private final ChatClient chatClient;
private final List<Object> tools;
public ReActAgent(ChatClient.Builder builder, WeatherTool weatherTool, SearchTool searchTool) {
this.tools = List.of(weatherTool, searchTool);
this.chatClient = builder.defaultTools(tools.toArray()).build();
}
public String solve(String question) {
return chatClient.prompt()
.system("""
Cuando necesites datos reales, llama a la herramienta apropiada antes de responder.
Razona paso a paso: Thought -> Action -> Observation -> Thought...
Responde solo al final cuando tengas suficiente información.
""")
.user(question)
.call()
.content();
}
}
Con la pregunta “¿Debería llevar un impermeable si voy a Đà Lạt el fin de semana?”, el agente puede razonar automáticamente: llama a weatherTool.forecast("Đà Lạt") para obtener el pronóstico, observa el resultado “lluvia 70%”, luego responde — toda esta secuencia Thought/Action/Observation ocurre dentro del framework sin que escribas manualmente el ciclo, porque Spring AI maneja automáticamente las llamadas múltiples a herramientas hasta que el modelo decide parar.
Thought: Necesito conocer el tiempo en Đà Lạt el fin de semana.
Action: forecast("Đà Lạt")
Observation: Lluvia 70%, 18-22 grados C
Thought: Debería recomendar un impermeable.
Answer: Sí, deberías llevar un impermeable, el pronóstico muestra lluvia del 70% en Đà Lạt el fin de semana.
Nota: ReAct puede entrar en “bucles infinitos” si el agente sigue llamando herramientas sin converger a una respuesta. Siempre establece un límite máximo de llamadas a herramientas (
maxToolCallso timeout) para evitar gastar crédito de API sin control.
7. Comparación rápida de los 5 patrones
| Patrón | Resuelve qué problema | Costo (número de llamadas LLM) | Cuándo usarlo |
|---|---|---|---|
| Reflection | Baja calidad de respuestas | Alto (2–3x) | Tareas que requieren precisión, pueden autoevaluarse |
| Tool Use | El LLM no accede datos/sistemas reales | Bajo–medio | Necesita consultar, calcular, llamar APIs |
| Planning | Tareas con múltiples pasos, fácil olvidar pasos | Medio | Tareas complejas con estructura clara |
| Multi-Agent | Un agente no es suficientemente experto para todo | Alto | Pipeline con roles claros (investigar/escribir/revisar) |
| ReAct | Necesita herramientas pero pasos desconocidos | Variable | Tareas que requieren búsqueda flexible de información |
En la práctica, estos patrones no son excluyentes — un agente de soporte al cliente podría usar Tool Use para consultar pedidos, Planning para procesar solicitudes complejas, y Reflection para verificar respuestas sensibles antes de enviar.
8. Conclusión
Los 5 patrones — Reflection, Tool Use, Planning, Multi-Agent, ReAct — son los bloques de construcción fundamentales para crear Agentes IA capaces de autocorregirse, interactuar con sistemas reales, procesar tareas complejas y explorar información de forma flexible. No necesitas usar los 5 en cada problema; empieza con Tool Use (prácticamente todo agente lo necesita), luego añade Planning o ReAct cuando la tarea es más compleja, y usa Reflection/Multi-Agent solo cuando el costo adicional justifica la mejora de calidad. Elegir el patrón correcto según el problema es más importante que intentar meter todos en un único agente.