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?

Advisors Recursive 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 ToolExecutionEligibilityChecker reports 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 ToolAdvisor marker interface, which DefaultChatClient uses 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());