1. Introducción
Los advisors estándar de Spring AI en la guía de Spring AI siguen un modelo de pasada única. Una solicitud atraviesa la cadena de advisors, llega al LLM, y la respuesta regresa. Esto funciona bien para interacciones simples, pero resulta insuficiente cuando necesitamos un comportamiento iterativo —por ejemplo, reintentar una salida estructurada fallida o ejecutar múltiples llamadas a herramientas en secuencia.
Spring AI 1.1 introduce Recursive Advisors para resolver este problema. Un advisor recursivo puede recorrer la cadena de advisors descendente varias veces, llamando repetidamente al LLM hasta que se cumpla una condición específica.
En este tutorial exploraremos cómo funcionan los advisors recursivos, examinaremos las dos implementaciones integradas y construiremos un advisor recursivo personalizado.
2. Dependencias Maven
Utilizamos Spring Boot 3.5 con Spring AI 1.1.2. Spring AI 1.1 requiere Spring Boot 3.4 o 3.5. Para soporte de Spring Boot 4, Spring AI 2.0 está en desarrollo pero actualmente sólo disponible como lanzamiento de milestone (2.0.0-M2).
Nuestro pom.xml incluye el Spring AI BOM y el OpenAI Starter:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
Podemos intercambiar el starter de OpenAI por cualquier proveedor compatible sin cambiar el código del advisor.
3. Cómo Funcionan los Advisors Recursivos
Un advisor regular procesa una solicitud exactamente una vez. Entrega la solicitud a la cadena, espera la respuesta y la devuelve. El flujo es lineal.
Un advisor recursivo rompe este modelo de pasada única. Puede evaluar la respuesta y, si el resultado no cumple una condición, volver a recorrer la parte descendente de la cadena para otro intento. Cada iteración dispara una nueva pasada a través de todos los advisors debajo del recursivo —incluyendo otra llamada al LLM.
Dos propiedades hacen que este patrón funcione. Primero, solo los advisors descendentes del advisor recursivo se vuelven a ejecutar en cada bucle. Los advisors que se encuentran ascendentes en la cadena se ejecutan solo una vez, durante la solicitud inicial. Segundo, los advisors descendentes siguen observando cada iteración. Un advisor de logging colocado después del recursivo, por ejemplo, captura cada reintento —preservando la observabilidad completa.
Cada advisor recursivo debe definir una condición de terminación. Sin un límite máximo de intentos o un criterio de salida explícito, el bucle podría ejecutarse indefinidamente, consumiendo tokens y generando costos con cada pasada.
La imagen siguiente muestra cómo varios advisors recursivos trabajan juntos en la cadena:

También podríamos implementar lógica iterativa con un bucle while explícito alrededor de las llamadas a ChatClient en el código de la aplicación. Los advisors recursivos mantienen esta preocupación dentro de la cadena, preservando la observabilidad y permitiendo que otros advisors intercepten cada iteración.
4. Advisors Recursivos Integrados
Spring AI incluye dos advisors recursivos que cubren los casos de uso más comunes.
4.1. ToolCallAdvisor
Por defecto, Spring AI maneja la llamada a herramientas dentro de la implementación de ChatModel. El ToolCallAdvisor mueve este bucle a la cadena de advisors, dando a otros advisors visibilidad completa de cada ronda de llamada a herramientas.
Supongamos que tenemos una herramienta de conversión de moneda que acepta un registro CurrencyRequest y devuelve la tasa de cambio actual, como se muestra en este ejemplo:
public record CurrencyRequest(String fromCurrency, String toCurrency) {}
var exchangeRateTool = FunctionToolCallback
.builder(
"getExchangeRate",
(CurrencyRequest req) -> "1 " + req.fromCurrency() + " = 0.91 " + req.toCurrency())
.description("Gets the current exchange rate between two currencies")
.inputType(CurrencyRequest.class)
.build();
Podemos conectar esta herramienta con el advisor en el ChatClient:
var toolCallAdvisor = ToolCallAdvisor
.builder()
.toolCallingManager(toolCallingManager)
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
.build();
var chatClient = ChatClient
.builder(chatModel)
.defaultAdvisors(toolCallAdvisor)
.defaultToolCallbacks(exchangeRateTool)
.build();
Cuando la respuesta del LLM contiene una llamada a herramienta, el advisor la ejecuta, alimenta el resultado de nuevo al prompt y vuelve a iterar. Esto continúa hasta que el LLM devuelve una respuesta de texto final sin más llamadas a herramientas.
Un prompt posterior puede activar la herramienta dos veces —una por par de divisas:
String answer = chatClient
.prompt()
.user("Convert 500 USD to EUR and then to GBP")
.call()
.content();
El advisor maneja ambas iteraciones de manera transparente. También soporta un modo "return direct" —cuando una herramienta establece returnDirect=true, el advisor omite la llamada final al LLM y devuelve la salida de la herramienta directamente al cliente.
4.2. StructuredOutputValidationAdvisor
Este advisor asegura que la salida JSON del LLM coincida con un esquema derivado de un registro Java. Si la validación falla, agrega los detalles del error al prompt y reintenta.
Supongamos que tenemos un registro BookSummary con title, author y una lista de themes:
public record BookSummary(String title, String author, List<String> themes) {}
Configuramos el advisor con un recuento máximo de reintentos de 5:
var validationAdvisor = StructuredOutputValidationAdvisor
.builder()
.outputType(BookSummary.class)
.maxRepeatAttempts(5)
.build();
Luego, creamos el ChatClient:
var chatClient = ChatClient
.builder(chatModel)
.defaultAdvisors(validationAdvisor)
.build();
El tipo objetivo se especifica cuando llamamos al ChatClient:
BookSummary result = chatClient
.prompt()
.user("Summarize '1984' by George Orwell with its main themes")
.call()
.entity(BookSummary.class);
La configuración de maxRepeatAttempts es necesaria para evitar bucles de reintento infinitos. El valor por defecto es 3.
5. Construyendo un Advisor Recursivo Personalizado
Construyamos un QualityCheckAdvisor que evalúe la longitud de la respuesta del LLM y reintente con retroalimentación si está por debajo de un umbral. Nuestro advisor implementa la interfaz CallAdvisor:
public class QualityCheckAdvisor implements CallAdvisor {
private static final int MAX_RETRIES = 3;
// ...
}
Definimos un límite duro de reintentos. Cada advisor recursivo debe incluir una condición de terminación para evitar bucles descontrolados.
La lógica principal reside en el método adviseCall. Comenzamos reenviando la solicitud a través de la cadena como de costumbre:
@Override public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
ChatClientResponse response = chain.nextCall(request);
int attempts = 0;
while (attempts < MAX_RETRIES && !isHighQuality(response)) {
// ...
attempts++;
}
return response;
}
Mientras la respuesta no cumpla nuestro umbral de calidad, aumentamos el prompt con retroalimentación y volvemos a ejecutar la cadena descendente. La llamada clave aquí es chain.copy(this).nextCall() —crea una subcadena fresca que comienza después de este advisor:
String feedback = "Your previous answer was incomplete. " + "Please provide a more thorough response.";
var augmentedPrompt = request
.prompt()
.augmentUserMessage(
userMessage -> userMessage.mutate().text(userMessage.getText() + System.lineSeparator() + feedback)
.build());
var augmentedRequest = request
.mutate()
.prompt(augmentedPrompt)
.build();
response = chain
.copy(this)
.nextCall(augmentedRequest);
La propia verificación de calidad es sencilla —verificamos que la respuesta supere 200 caracteres:
private boolean isHighQuality(ChatClientResponse response) {
String content = response
.chatResponse()
.getResult()
.getOutput()
.getText();
return content != null && content.length() > 200;
}
Registramos el advisor con el ChatClient:
var chatClient = ChatClient
.builder(chatModel)
.defaultAdvisors(new QualityCheckAdvisor())
.build();
Debemos notar que los advisors recursivos actualmente solo soportan modo no streaming. El advisor necesita la respuesta completa antes de decidir si reintenta.
6. Pruebas
Llamar a un LLM real en una prueba unitaria es no determinista y lento. Simulamos el ChatModel en su lugar, controlando exactamente lo que ve el advisor. Esto nos permite verificar el comportamiento de reintentos en aislamiento.
La estrategia es sencilla: configuramos el mock para que devuelva una respuesta corta en la primera llamada —demasiado corta para pasar la verificación de calidad— y una respuesta suficientemente larga en la segunda llamada. Si el advisor funciona correctamente, reintenta exactamente una vez.
Aquí está la prueba de ejemplo:
@SpringBootTest
class QualityCheckAdvisorTests {
@MockitoBean
ChatModel chatModel;
@Autowired
ChatClient.Builder chatClientBuilder;
@Test
void givenShortFirstResponse_whenAdvised_thenRetriesAndReturnsLongResponse() {
var shortResponse = "Too brief.";
var longResponse = "S".repeat(250);
when(chatModel.call(any(Prompt.class)))
.thenReturn(createChatResponse(shortResponse))
.thenReturn(createChatResponse(longResponse));
var chatClient = chatClientBuilder
.defaultAdvisors(new QualityCheckAdvisor())
.build();
String result = chatClient.prompt()
.user("Explain the SOLID principles.")
.call()
.content();
assertThat(result).hasSize(250);
verify(chatModel, times(2)).call(any(Prompt.class));
}
private ChatResponse createChatResponse(String content) {
return new ChatResponse(
List.of(new Generation(new AssistantMessage(content)))
);
}
}
La llamada a verify() es la aserción clave. Demuestra que el advisor disparó exactamente un reintento. La primera llamada devolvió una respuesta por debajo del umbral de 200 caracteres, por lo que el advisor iteró. La segunda llamada cumplió la condición, y el advisor devolvió.
7. Consideraciones de Rendimiento y Orden
Los advisors recursivos multiplican el número de llamadas al LLM. Cada iteración aumenta el costo, la latencia y el consumo de tokens. La tabla a continuación resume los compromisos clave:
| Preocupación | Impacto | Mitigación |
|---|---|---|
| Costo de API | Cada iteración de bucle incurre en una llamada LLM adicional | Establecer límites de reintentos estrictos |
| Latencia | Múltiples viajes de ida suman | Cachear resultados intermedios cuando sea posible |
| Consumo de tokens | Los prompts completos se vuelven a enviar cada iteración | Mantener los prompts aumentados concisos |
| Orden de advisors | Determina qué advisors observan cada iteración | Colocar advisors recursivos al principio o al final según necesidades de observabilidad |
Debemos colocar los advisors recursivos intencionalmente en la cadena. El ToolCallAdvisor normalmente se ordena cerca de HIGHEST_PRECEDENCE para que los advisors internos observen la ejecución de herramientas. El StructuredOutputValidationAdvisor se ubica cerca de LOWEST_PRECEDENCE, para que se ejecute justo antes de la llamada al modelo.
8. Conclusión
En este tutorial hemos aprendido cómo los advisors recursivos de Spring AI rompen el modelo de pasada única para permitir patrones de reintento iterativos. Soportan casos de uso como bucles de llamadas a herramientas, validación de salida estructurada y verificaciones de calidad personalizadas —todo dentro de la familiar abstracción de advisor.
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.