1. Visión general
Anthropic recientemente publicó un documento sobre cómo construir Agentes Efectivos de IA. En este documento, presentan algunos patrones de agentes que los desarrolladores de software pueden seguir como buenas prácticas. También afirman que podemos usarlos como alternativa a los frameworks complejos. Sin embargo, la realidad es que, en el ecosistema Java, usamos frameworks grandes en todas partes, como Spring.
Aunque Spring es un framework grande y complejo, ofrece herramientas que simplifican la creación de agentes efectivos usando Spring AI.
En este artículo, revisaremos los patrones presentados en la publicación, junto con algunas definiciones clave usadas, para garantizar claridad. Luego, implementaremos estos patrones usando Spring AI. Dado que el enfoque es la implementación de los patrones, no nos centraremos en la integración con hosts de modelos reales.
2. Construyendo Agentes Efectivos con Spring AI
Después de trabajar con docenas de equipos construyendo agentes LLM en diversas industrias, Anthropic ha propuesto algunos patrones simples y componibles. Pero primero, aclaremos dos conceptos que usan en la publicación:
- Los agentes son sistemas donde los LLMs dirigen dinámicamente sus propios procesos y el uso de herramientas, manteniendo el control sobre cómo llevan a cabo las tareas.
- Los flujos de trabajo son sistemas donde los LLMs y las herramientas se orquestan mediante caminos de código predefinidos.
Teniendo esto en cuenta, los patrones presentados son:
- El flujo de trabajo de encadenamiento de prompts descompone tareas complejas en una secuencia de pasos, con la salida de cada prompt LLM sirviendo como entrada al prompt LLM siguiente.
- El flujo de trabajo de paralelización permite el procesamiento concurrente de múltiples operaciones LLM, con sus salidas agregadas programáticamente.
- El flujo de trabajo de enrutamiento dirige el enrutamiento inteligente de entradas a manejadores especializados según la clasificación del contenido.
- El flujo de trabajo Orquestador-Trabajadores, donde un LLM central descompone tareas, las delega a LLMs trabajadores y combina sus resultados en una respuesta final.
- El flujo de trabajo Evaluador-Optimizador, donde un LLM genera un resultado mientras otro proporciona evaluación y retroalimentación, en un bucle.
3. Dependencias
Mantendremos las cosas simples usando solo las dependencias mínimas que necesitamos. Esto significa que no incluiremos ninguna implementación como spring-ai-ollama-spring-boot-starter. Nuestra solución se basará en interfaces en lugar de usar cualquier implementación Model:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-model</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
<version>1.0.2</version>
</dependency>
Estas dos son suficientes para construir agentes con Spring AI, ya que podemos utilizar la interfaz Model y su extensión, ChatModel. Además, el ChatClient se usa ampliamente hoy en día, como veremos en los ejemplos más adelante en este tutorial.
4. Agentes de flujo de trabajo de encadenamiento con Spring AI
El flujo de trabajo de encadenamiento es una buena opción en casos donde una tarea se puede descomponer en subtareas secuenciales. El resultado de cada subtarea se pasará a la siguiente. También tenemos la oportunidad de añadir algo de código intermedio, para decisiones o cambios.
Un buen ejemplo para usar en nuestro CI/CD es una canalización de compilación. Podemos descomponer la canalización de compilación en pasos concretos y secuenciales:
- Checkout desde VCS
- Compilar el código y empaquetar
- Contenerizar y subir al repositorio Docker
- Desplegar la imagen Docker en el entorno de pruebas
- Ejecutar pruebas de integración
Comencemos asumiendo que tenemos una interfaz OpsClient que extiende ChatClient y también una implementación para interactuar con un modelo DevOps. El agente efectivo con Spring AI se verá así:
public String opsPipeline(String userInput) {
String response = userInput;
for (String prompt : OpsClientPrompts.DEV_PIPELINE_STEPS) {
String request = String.format("{%s}\n {%s}", prompt, response);
ChatClient.ChatClientRequestSpec requestSpec = opsClient.prompt(request);
ChatClient.CallResponseSpec responseSpec = requestSpec.call();
response = responseSpec.content();
if (response.startsWith("ERROR:")) {
break;
}
}
return response;
}
Primero, usamos OpsClientPrompts.DEV_PIPELINE_STEPS en un bucle for. Estos pasos serán más descriptivos que los pasos mencionados anteriormente. Por ejemplo, para hacer checkout desde VCS, sería algo como "Checkout the code from the given URL. Return the path of the checked‑out code or else the error occurred".
En el patrón de encadenamiento, cada respuesta alimenta la entrada del paso siguiente. Por lo tanto, el campo request hace exactamente eso. Dado el siguiente prompt y la respuesta anterior, crea el request que será el parámetro del método prompt(), que se ejecutará usando el método call(). El resultado se almacena en response para ser usado en el siguiente paso. El valor inicial de response será la entrada del usuario. Finalmente, entre pasos, añadimos una cláusula if que interrumpe el bucle en caso de errores. El flujo de trabajo de encadenamiento debería verse así:

5. Agentes de flujo de trabajo de paralelización con Spring AI
El flujo de trabajo de paralelización es una buena opción en casos donde una tarea se puede dividir en subtareas independientes que pueden trabajarse en paralelo. Los resultados se agregan programáticamente en un solo resultado.
Un ejemplo práctico, siguiendo el mismo concepto de modelo DevOps, es la tarea de desplegar una nueva versión de nuestro código en múltiples entornos, como test, dev, integración, etc. Dado que implica un agente de IA, esperamos que haya más cosas detrás de escena, como verificar las directrices del departamento para validar que esta imagen pueda desplegarse en cada entorno, etc. Los agentes efectivos con Spring AI utilizarán el ExecutorService y el CompletableFuture:
public List<String> opsDeployments(String containerLink, List<String> environments, int maxConcurentWorkers) {
try (ExecutorService executor = Executors.newFixedThreadPool(maxConcurentWorkers)) {
List<CompletableFuture<String>> futures = environments.stream()
.map(env -> CompletableFuture.supplyAsync(() -> {
try {
String request = OpsClientPrompts.NON_PROD_DEPLOYMENT_PROMPT + "\n Image:" + containerLink + " to environment: " + env;
ChatClient.ChatClientRequestSpec requestSpec = opsClient.prompt(request);
ChatClient.CallResponseSpec responseSpec = requestSpec.call();
return responseSpec.content();
} catch (Exception e) { /* manejar error */ }
}, executor))
.toList();
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
allFutures.join();
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
}
El método opsDeployments() acepta el enlace al contenedor con la imagen a desplegar, los entornos que apuntamos y el número máximo de trabajadores concurrentes a usar. Luego, usamos streams para descomponer la tarea en una tarea de despliegue por cada entorno dado.
Cada campo request es el prompt para el despliegue, en nuestro caso, OpsClientPrompts.NON_PROD_DEPLOYMENT_PROMPT, el containerLink, y el environment específico. El resultado es simplemente un array de cada resultado. El flujo de trabajo de paralelización debería verse así:

6. Agentes de flujo de trabajo de enrutamiento con Spring AI
El flujo de trabajo de enrutamiento se adapta bien a casos en los que queremos distribuir una tarea a un modelo IA más especializado. Un buen ejemplo es un LLM que actúa como soporte de servicio al cliente, recibe la entrada del cliente y luego redirige la tarea al modelo de soporte técnico, modelo de servicio de cuentas, etc. El cliente no necesita saber que hay más de un modelo. En su lugar, usa el modelo de propósito general, que actúa como un flujo de trabajo de enrutamiento.
En continuación a nuestro ejemplo de modelo DevOps, podemos tener un Agente DevOps de propósito general que acepta cualquier solicitud para construir una canalización de pull request (PR) o desplegar un PR en algún entorno. Luego, el flujo de trabajo de enrutamiento se utilizará para dirigir la solicitud a los agentes efectivos con Spring AI, de los ejemplos anteriores.
Empecemos usando la interfaz opsRoutingClient que extiende ChatClient. Proporcionamos las opciones de enrutamiento y la entrada del usuario al cliente. Luego, le pedimos que enrute la solicitud a la ChainWorkflow o ParallelizationWorkflow, como se implementó anteriormente en este artículo:
public class RoutingWorkflow {
private final OpsRouterClient opsRouterClient;
private final ChainWorkflow chainWorkflow;
private final ParallelizationWorkflow parallelizationWorkflow;
// constructor omitido
public String route(String input) {
String[] route = determineRoute(input, OPS_ROUTING_OPTIONS);
String opsOperation = route[0];
List<String> requestValues = route[1].lines()
.toList();
return switch (opsOperation) {
case "pipeline" -> chainWorkflow.opsPipeline(requestValues.getFirst());
case "deployment" -> executeDeployment(requestValues);
default -> throw new IllegalStateException("Unexpected value: " + opsOperation);
};
}
private String[] determineRoute(String input, Map<String, String> availableRoutes) {
String request = String.format("""
Given this map that provides the ops operation as key and the description for you to build the operation value, as value: %s.
Analyze the input and select the most appropriate operation.
Return an array of two strings. First string is the operations decided and second is the value you built based on the operation.
Input: %s""", availableRoutes, input);
ChatClient.ChatClientRequestSpec requestSpec = opsRouterClient.prompt(request);
ChatClient.CallResponseSpec responseSpec = requestSpec.call();
String[] routingResponse = responseSpec.entity(String[].class);
System.out.printf("Routing Decision: Operation is: %s\n, Operation value: %s%n", routingResponse[0], routingResponse[1]);
return routingResponse;
}
private String executeDeployment(List<String> requestValues) {
String containerLink = requestValues.getFirst();
List<String> environments = Arrays.asList(requestValues.get(1)
.split(","));
int maxWorkers = Integer.parseInt(requestValues.getLast());
List<String> results = parallelizationWorkflow.opsDeployments(containerLink, environments, maxWorkers);
return String.join(", ", results);
}
}
El método route() primero determina la tarea especializada a usar, ya sea pipeline o deployment. El array route contendrá la acción y el prompt que el opsRouterClient devolvió. Luego, envía la solicitud directamente a chainWorkflow si la acción es "pipeline". En el caso de "deployment", el método executeDeployment() prepara la solicitud y la envía al agente parallelizationWorkflow.
Una representación gráfica de agentes efectivos con Spring AI, para el patrón de flujo de trabajo de enrutamiento, se verá así:

7. Agentes de flujo de trabajo Orquestador-Trabajadores con Spring AI
El flujo de trabajo Orquestador-Trabajadores es el más adecuado cuando tenemos una tarea compleja que se puede descomponer en subtareas más simples, pero no podemos predecir de antemano cuáles serán. El Agente Orquestador luego delega las subtareas a Agentes Trabajadores. Finalmente, recopila los resultados y compone un resultado para la tarea inicial.
En nuestro ejemplo de modelo DevOps, podemos tener un Orquestador que acepta una URL de PR, analiza los cambios y determina en qué entornos se necesita probar. Por ejemplo, una actualización menor de validación de entrada puede probarse solo en nuestro entorno de prueba. En contraste, un cambio que afecta la integración con un sistema externo también requeriría pruebas en los entornos de integración.
Suponiendo que tenemos una clase OpsOrchestratorClient que extiende ChatClient. Podemos proporcionar un prompt que describa este caso y el enlace PR, y pedirle que devuelva los entornos en los que necesitamos ejecutar pruebas. Luego alimentar eso a nuestro OpsClient para ejecutar el despliegue y ejecutar pruebas:
public String remoteTestingExecution(String userInput) {
String orchestratorRequest = REMOTE_TESTING_ORCHESTRATION_PROMPT + userInput;
ChatClient.ChatClientRequestSpec orchestratorRequestSpec = opsOrchestratorClient.prompt(orchestratorRequest);
ChatClient.CallResponseSpec orchestratorResponseSpec = orchestratorRequestSpec.call();
String[] orchestratorResponse = orchestratorResponseSpec.entity(String[].class);
String prLink = orchestratorResponse[0];
StringBuilder response = new StringBuilder();
for (int i = 1; i < orchestratorResponse.length; i++) {
String testExecutionChainInput = prLink + " on " + orchestratorResponse[i];
for (String prompt : OpsClientPrompts.EXECUTE_TEST_ON_DEPLOYED_ENV_STEPS) {
String testExecutionChainRequest =
String.format("%s\n PR: [%s] environment", prompt, testExecutionChainInput);
ChatClient.ChatClientRequestSpec requestSpec = opsClient.prompt(testExecutionChainRequest);
ChatClient.CallResponseSpec responseSpec = requestSpec.call();
testExecutionChainInput = responseSpec.content();
System.out.printf("OUTCOME: %s\n", testExecutionChainInput);
}
response.append(testExecutionChainInput).append("\n");
}
return response.toString();
}
El String orchestratorRequest contendrá el prompt para analizar los cambios PR y devolver una lista de entornos. Obtenemos la lista de entornos en el array orchestratorResponse. Para cada entorno, realizamos la subtarea en dos pasos: primero, el despliegue, y segundo, la ejecución de pruebas.
Esperamos que hayas notado que usamos el flujo de trabajo de encadenamiento en el segundo bucle for. Empezamos con el prompt para desplegar, y como entrada, una frase con la URL PR y el entorno actual. La respuesta contiene una frase de entrada. Esta se alimenta al segundo prompt, que ejecuta las pruebas relevantes para el entorno actual basado en la información del enlace PR.
La salida contiene una frase con el resultado de la ejecución de pruebas para cada entorno.
Ten en cuenta que hemos mantenido el ejemplo simple evitando ejecuciones paralelas o asíncronas de las subtareas. Sin embargo, usar tales técnicas sería la forma más eficiente de implementar el patrón Orquestador-Trabajadores. La secuencia visual del patrón debería ser algo así:

8. Agentes Evaluador-Optimizador con Spring AI
El patrón final propuesto por la publicación de Anthropic es el Evaluador-Optimizador. El patrón de flujo de trabajo Evaluador-Optimizador, como su nombre indica, es adecuado para tareas que implican generar sugerencias. El Optimizador propone una solución inicial o mejora. El Evaluador luego brinda retroalimentación sobre la sugerencia y puede volver a llamar al Optimizador para refinar los resultados o producir resultados más precisos.
Ampliamos nuestro ejemplo DevOps, y ofrecemos una función más sobre PRs: proporcionaremos comentarios de revisión de PR usando el flujo de trabajo Evaluador-Optimizador. Suponiendo una interfaz CodeReviewClient que extiende ChatClient, creamos el Agente con Spring AI, con un método simple evaluate():
public class EvaluatorOptimizerWorkflow {
private final CodeReviewClient codeReviewClient;
static final ParameterizedTypeReference<Map<String, String>> mapClass = new ParameterizedTypeReference<>() {};
// constructor omitido
public Map<String, String> evaluate(String task) {
return loop(task, new HashMap<>(), "");
}
private Map<String, String> loop(String task, Map<String, String> latestSuggestions, String evaluation) {
latestSuggestions = generate(task, latestSuggestions, evaluation);
Map<String, String> evaluationResponse = evaluate(latestSuggestions, task);
String outcome = evaluationResponse.keySet().iterator().next();
evaluation = evaluationResponse.values().iterator().next();
if ("PASS".equals(outcome)) {
return latestSuggestions;
}
return loop(task, latestSuggestions, evaluation);
}
// veremos los métodos generate() y evaluate() después
}
El patrón Evaluador-Optimizador comienza con el método evaluate() que acepta una tarea. En nuestro ejemplo, la entrada es simplemente el enlace PR. Enviamos esta tarea al método loop() . Este método acepta tres parámetros: la tarea, las sugerencias previas y una evaluación. En el primer bucle, proporcionamos solo la tarea.
El método loop() llama a generate(), que veremos más adelante. El resultado es las últimas sugerencias, que pasamos al método evaluate(), junto con la tarea. Luego leemos el resultado de evaluationResponse, y si es "PASS", devolvemos las últimas sugerencias. Si no, entonces llamamos al método loop() de nuevo con las nuevas latestSuggestions y evaluation.
Usamos el agente CodeReviewClient para los métodos generate() y evaluate():
private Map<String, String> generate(String task, Map<String, String> previousSuggestions, String evaluation) {
String request = CODE_REVIEW_PROMPT +
"\n PR: " + task +
"\n previous suggestions: " + previousSuggestions +
"\n evaluation on previous suggestions: " + evaluation;
ChatClient.ChatClientRequestSpec requestSpec = codeReviewClient.prompt(request);
ChatClient.CallResponseSpec responseSpec = requestSpec.call();
Map<String, String> response = responseSpec.entity(mapClass);
return response;
}
private Map<String, String> evaluate(Map<String, String> latestSuggestions, String task) {
String request = EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT +
"\n PR: " + task +
"\n proposed suggestions: " + latestSuggestions;
ChatClient.ChatClientRequestSpec requestSpec = codeReviewClient.prompt(request);
ChatClient.CallResponseSpec responseSpec = requestSpec.call();
Map<String, String> response = responseSpec.entity(mapClass);
return response;
}
En generate(), proporcionamos el prompt, la tarea, las previousSuggestions y la evaluation al agente codeReviewClient. El prompt es proporcionar una revisión de código del PR, basado en las últimas sugerencias y la última evaluación. Las sugerencias deben seguir las reglas dadas y un formato específico.
En evaluate(), el prompt es "Evaluate the suggested code improvements for correctness, time complexity, and best practices", proporcionando la tarea y las últimas sugerencias. Luego devuelve la evaluación, "PASS", "NEEDS_IMPROVEMENT", "FAIL", junto con la retroalimentación.
El diagrama de secuencia del patrón Optimizer-Evaluator Workflow es:

9. Conclusión
En este tutorial, revisamos la publicación de Anthropic sobre Agentes Efectivos de IA. Presentamos todos los patrones, proporcionamos definiciones breves y damos ejemplos de casos de uso reales. Luego, demostramos una implementación de cada patrón usando agentes con Spring AI. Finalmente, incluyimos un diagrama de secuencia para cada uno para ayudar a visualizar cómo funciona el patrón en acción.
Newsletter Semanal de Java
Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.
Sin spam. Cancela cuando quieras.