|
This version is still in development and is not considered stable yet. For the latest stable version, please use Spring AI 2.0.0! |
Recursive Advisors
What is a Recursive Advisor?
Recursive advisors are a special type of advisor that can loop through the downstream advisor chain multiple times.
This pattern is useful when you need to repeatedly call the LLM until a certain condition is met, such as:
-
Executing tool calls in a loop until no more tools need to be called
-
Validating structured output and retrying if validation fails
-
Implementing evaluation logic with modifications to the request
-
Implementing retry logic with modifications to the request
The CallAdvisorChain.copy(CallAdvisor after) method is the key utility that enables recursive advisor patterns.
It creates a new advisor chain that contains only the advisors that come after the specified advisor in the original chain
and allows the recursive advisor to call this sub-chain as needed.
This approach ensures that:
-
The recursive advisor can loop through the remaining advisors in the chain
-
Other advisors in the chain can observe and intercept each iteration
-
The advisor chain maintains proper ordering and observability
-
The recursive advisor doesn’t re-execute advisors that came before it
Built-in Recursive Advisors
Spring AI ships two built-in recursive advisors that demonstrate this pattern.
ToolCallingAdvisor
ToolCallingAdvisor implements the tool calling loop as part of the advisor chain rather than relying on per-ChatModel internal execution. It is auto-registered by DefaultChatClient whenever tools are present and is the default mechanism through which ChatClient drives tool-augmented conversations.
Highlights:
-
Loops through the advisor chain until the
ToolExecutionEligibilityCheckerreports no more tool calls. -
Uses
callAdvisorChain.copy(this)to create a sub-chain for recursive calls — other advisors can observe and intercept every iteration. -
Supports return-direct: when a tool’s result has
returnDirect = true, the advisor breaks the loop and returns the tool result to the caller without sending it back to the LLM. -
Implements the
ToolAdvisormarker interface, whichDefaultChatClientuses to enforce the single-tool-advisor invariant — custom subclasses register transparently as replacements.
Short example:
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager)
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(toolCallingAdvisor)
.build();
For the full builder API, hook methods, configuration options, memory-advisor ordering interactions, user-controlled execution patterns, and the custom-subclass extension pattern, see ToolCallingAdvisor.
For the conceptual overview of how the tool loop fits into the broader tool calling architecture, see Tool Calling: The Tool Calling Loop.
For a concrete subclass that uses `ToolCallingAdvisor’s extension hooks to implement progressive tool disclosure, see Tool Search Tool.
StructuredOutputValidationAdvisor
StructuredOutputValidationAdvisor validates the structured JSON output against a JSON schema and retries the call if validation fails, up to a configurable number of attempts.
Key features:
-
Derives a JSON schema from the expected output type, or accepts a pre-supplied schema string.
-
Validates the LLM response against the schema.
-
Retries the call when validation fails (default: up to 3 attempts).
-
Augments the prompt with the validation error message on retry attempts to help the model self-correct.
-
Uses
callAdvisorChain.copy(this)to create a sub-chain for recursive calls. -
Optionally supports a custom
JsonMapper.
The advisor can be configured via outputType (schema derived automatically) or outputJsonSchema (pre-supplied schema string); the two options are mutually exclusive.
Example with outputType:
var validationAdvisor = StructuredOutputValidationAdvisor.builder()
.outputType(MyResponseType.class)
.maxRepeatAttempts(3)
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(validationAdvisor)
.build();
Example with a pre-supplied JSON schema:
var validationAdvisor = StructuredOutputValidationAdvisor.builder()
.outputJsonSchema(myConverter.getJsonSchema())
.build();
Alternatively, enable schema validation directly on an entity() call without configuring the advisor manually, via EntityParamSpec:
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class, spec -> spec.schemaValidation());