eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Course – Summer Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

Course – Summer Sale 2026 – NPI (cat=Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

1. Overview

With Spring AI, we can integrate LLMs into our Spring applications. This is helpful, for example, for processing unstructured data like wiki pages, emails, or chat messages by extracting structured information to store it in a database. However, LLM responses are not deterministic and so neither testable nor handable by our application code. The best way to implement a kind of quality gate for LLM responses is to use an LLM again for evaluation. This pattern is called LLM-as-a-judge.

In this tutorial, we’ll learn how to implement the LLM-as-a-Judge pattern in Spring AI.

2. Maven Dependencies

We add the Spring AI OpenAI starter to our pom.xml:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

The latest version of spring-ai-starter-model-openai is available on Maven Central.

3. The LLM-as-a-Judge Pattern

The LLM-as-a-Judge pattern is like a code review. A developer (the generator) writes code, and a senior engineer (the judge) reviews it and gives structured feedback. If the judge decides that the code doesn’t meet the requirements, the developer needs to improve it, and the judge then needs to review it again.

Applied to LLMs, the process is:

  • The generator produces an initial answer to the user’s question
  • The judge evaluates that answer against a rubric and returns a numeric score and written feedback
  • If the score is too low, the generator receives the original question plus the judge’s critique and produces a refined answer.
  • This will repeat until the answer meets the requirements. To avoid endless repetition in case of never reaching the needed quality, it’s a best practice to limit the number of loops.

This pattern improves output quality without model fine-tuning, without a human in the loop, and without changing a single line of calling code.

It’s most valuable wherever a weak answer has real consequences, e.g., customer support bots, where a vague answer to a billing question damages trust directly. In such cases, the pattern acts as an invisible quality gate between the model and the user.

3.1. Why Recursive Advisors Are the Right Fit

Advisors in Spring AI are interceptors that wrap the request/response cycle of a ChatClient. A CallAroundAdvisor can inspect a request before it reaches the model and modify the response before it reaches the caller, but it only ever makes one model call per invocation.

Recursive advisors go further: they hold an internal ChatClient reference and can make additional model calls from within the advisor itself.

We can apply that capability to a specific problem: using the second model call as a judge, not just an extension. The generate-judge-refine loop becomes a single, self-contained component that’s completely transparent to the caller.

The caller just invokes chatClient.prompt().user(“…”).call().content() as usual. The judge’s logic is invisible.

4. Implementing the LLM-as-a-Judge Advisor

Let’s build the feature in three parts: a value object for the verdict, a prompt template for the judge, and the advisor itself.

4.1. The Verdict Record

The judge returns a structured verdict. We model it as a simple Java record:

public record Verdict(double score, String feedback) {}

In our sample, the score is a value between 0.0 (poor) and 1.0 (excellent). The feedback explains what’s missing or weak, and it’s what we’ll feed back to the generator during refinement.

4.2. The Judge Prompt Template

The judge needs a clear, structured system prompt. For this, let’s define it as a constant inside the advisor:

private static final String JUDGE_SYSTEM_PROMPT = """
    You are a strict quality evaluator for AI-generated answers.
    
    Given a user question and an AI-generated answer, rate the answer quality.
    
    Use this rubric:
    - 1.0: Complete, accurate, and clearly explained
    - 0.7: Mostly correct but missing details or clarity
    - 0.4: Partially correct or overly vague
    - 0.0: Incorrect, irrelevant, or harmful
    
    Respond ONLY with a valid JSON object. Do not add any explanation outside the JSON.
    Format: {"score": <0.0 to 1.0>, "feedback": "<one concise sentence>"}
    """;

It’s important to declare an explicit rubric and a strict JSON output to get a parseable and consistent answer from the judge model.

4.3. The Advisor Class and Its Dependencies

The key design decision of a recursive advisor is that it holds its own ChatClient instance to make additional model calls independently of the advisor chain. We also inject the configurable quality thresholds:

public class LlmJudgeAdvisor implements CallAdvisor {

    private final ChatClient judgeClient;
    private final double scoreThreshold;
    private final int maxRefinements;

    public LlmJudgeAdvisor(
      ChatClient judgeClient,
      double scoreThreshold,
      int maxRefinements
    ) {
        this.judgeClient = judgeClient;
        this.scoreThreshold = scoreThreshold;
        this.maxRefinements = maxRefinements;
    }

    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public String getName() {
        return "LlmJudgeAdvisor";
    }

    // ...
}

The ChatClient.Builder receives the same auto-configured model settings as the rest of the application. We could wire a different, specialized judge model here. The official Spring AI docs recommend this specifically to avoid the model judging its own output too leniently.

4.4. Evaluating and Refining the Response

The adviseCall() method is the only entry point that the advisor chain calls. Rather than forwarding the request once, we call chain.copy(this).nextCall(request), which creates a sub-chain that includes our advisor again. That’s what enables the loop to re-evaluate and re-generate across multiple attempts:

@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
    for (int attempt = 1; attempt <= maxRefinements + 1; attempt++) { 
        ChatClientResponse response = chain.copy(this).nextCall(request);
        if (attempt > maxRefinements) {
          return response;
        }
        Verdict verdict = evaluate(request, response); 
        if (verdict.score() >= scoreThreshold) {
            return response;
        }

        request = addFeedback(request, verdict.feedback());
    }
    return chain.copy(this).nextCall(request);
}

The loop needs an explicit upper bound to avoid unbounded recursion. On the last allowed attempt, the method returns the response immediately, skipping the evaluation. There’s no point scoring an answer we can’t act on. Otherwise, if the score meets scoreThreshold, it returns early. If not, it augments the request with the judge’s feedback and continues to the next iteration.

4.5. Evaluating the Answer

The evaluate() method sends the original question and the generated answer to the judge model. We can use  .entity(…) to render the response JSON directly into our record, without any manual JSON parsing:

private Verdict evaluate(ChatClientRequest request, ChatClientResponse response) {
    String question = request.prompt().getUserMessage().getText();
    String answer = response.chatResponse().getResult().getOutput().getText();

    return judgeClient.prompt()
      .system(JUDGE_SYSTEM_PROMPT)
      .user("Question: " + question + "\n\nAnswer: " + answer)
      .call()
      .entity(Verdict.class);
}

Spring AI injects a JSON schema derived from the Verdict record into the model call and handles deserialization. This removes the need for any try-catch around JSON parsing and keeps the evaluation code focused on the prompt, not on format handling.

4.6. Augmenting the Request With Feedback

When the rating falls short, we don’t start over with the original prompt. Instead, let’s augment it, i.e., we append the judge’s critique to the user message so the model has full context for its next attempt. The addFeedback() helper does this using ChatClientRequest.mutate():

private ChatClientRequest addFeedback(ChatClientRequest original, String feedback) {
    Prompt augmented = original.prompt()
      .augmentUserMessage(msg -> msg.mutate()
          .text(msg.getText()
              + "\n\nYour previous answer was insufficient. Feedback: " + feedback
              + "\nPlease provide an improved answer.")
          .build());
    return original.mutate().prompt(augmented).build();
}

augmentUserMessage() gives us a mutable copy of the user message without touching the rest of the request. System prompt, tool definitions, and conversation history all stay intact. The updated ChatClientRequest is passed into the next loop iteration.

5. Configuring the Application

Let’s declare both beans in a single @Configuration class. The LlmJudgeAdvisor bean receives its own ChatClient instance, built from the same auto-configured ChatClient.Builder, along with the quality thresholds from application.properties:

@Configuration
public class ChatConfig {

    @Bean
    public ChatClient chatClient(
      ChatClient.Builder builder, 
      LlmJudgeAdvisor judgeAdvisor
    ) {
        return builder
          .defaultAdvisors(judgeAdvisor)
          .build();
    }

    @Bean
    public LlmJudgeAdvisor llmJudgeAdvisor(
      ChatClient.Builder builder,
      @Value("${judge.score-threshold:0.7}") double scoreThreshold,
      @Value("${judge.max-refinements:2}") int maxRefinements
    ) {
        return new LlmJudgeAdvisor(
          builder.build(),
          scoreThreshold,
          maxRefinements
        );
    }

}

We can then set the thresholds in application.properties:

spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini

judge.score-threshold=0.75
judge.max-refinements=2

A score threshold of 0.75 means the judge must rate the answer at least 75% quality before we accept it. Setting the number of maximum refinements to 2 caps the recursive loop at two improvement attempts, preventing runaway API usage.

6. Testing the Advisor

Let’s implement a @SpringBootTest with a single mocked ChatModel. Both the generator and the judge use the same underlying instance, so one mock covers the entire call sequence. No API key is needed.

The test stubs four sequential chatModel responses matching the expected flow:

@SpringBootTest
class LlmJudgeAdvisorTest {

    @MockitoBean
    ChatModel chatModel;

    @Autowired
    ChatClient chatClient;

    @Test
    void givenLowQualityAnswer_whenAdvisorRuns_thenAnswerIsRefined() {
        when(chatModel.call(any(Prompt.class)))
          .thenReturn(buildChatResponse("It runs Java."))
          .thenReturn(buildChatResponse("{\"score\": 0.3, \"feedback\": \"Too vague.\"}"))
          .thenReturn(buildChatResponse(
            "The JVM executes Java bytecode, manages memory, and enables platform independence."))
          .thenReturn(buildChatResponse("{\"score\": 0.9, \"feedback\": \"Complete and accurate.\"}"));

        String result = chatClient.prompt()
          .user("Explain what a JVM is.")
          .call()
          .content();

        assertThat(result).contains("bytecode");
    }

    private ChatResponse buildChatResponse(String content) {
        return new ChatResponse(List.of(new Generation(new AssistantMessage(content))));
    }
}

The four stubs map directly to the generate-evaluate-refine-evaluate cycle: call one is the weak generator response, call two is the judge’s low verdict, call three is the refined generator response, and call four is the judge’s passing verdict. The chatClient with the advisor is real. Only the model underneath is mocked. This means the full advisor logic runs as it would in production.

7. Conclusion

In this article, we implemented the LLM-as-a-Judge pattern in Spring AI using a recursive CallAdvisor. We saw how the generate-evaluate-refine loop maps naturally onto the advisor model: the first model call produces an answer, Spring AI’s structured output maps the judge’s response into a typed Verdict, and the loop augments the original request with feedback until the score is sufficient or the attempt limit is reached.

The result is a reusable component that improves output quality automatically and remains completely transparent to any caller using the ChatClient.

The code for this tutorial is available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – Summer Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

Course – Summer Sale 2026 – NPI (All)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

eBook Jackson – NPI EA – 3 (cat = Jackson)
guest
0 Comments
Oldest
Newest
Inline Feedbacks
View all comments