JevSelfRefineAdvisor¶
A self-refine CallAdvisor: it judges every model response with JevJudge
and, when the response falls short, feeds the defect back into the prompt and tries again.
How it works¶
flowchart LR
A[Request] --> B[Model]
B --> C{JevJudge}
C -- passed --> D[Return response]
C -- failed --> E[Append defect to prompt]
E --> B
C -- attempts exhausted --> F[Return last response<br/>or throw]
The retry prompt is rebuilt from the original request each time rather than from the previous attempt, so feedback does not compound across attempts.
Quick Start¶
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new WeatherTools())
.defaultAdvisors(JevSelfRefineAdvisor.builder()
.judge(judge)
.maxRepeatAttempts(3)
.build())
.build();
String answer = chatClient.prompt("What is the weather in Paris?").call().content();
Builder Configuration¶
| Builder method | Type | Default | Description |
|---|---|---|---|
judge(JevJudge) |
JevJudge |
— (required) | What to evaluate each response against. |
maxRepeatAttempts(int) |
int |
3 |
Retries after the first attempt. Capped at MAX_REPEAT_ATTEMPTS_LIMIT (100). |
failOnExhaustedAttempts(boolean) |
boolean |
false |
Throw JevSelfRefineFailedException instead of returning the best effort. |
skipEvaluationPredicate(BiPredicate<ChatClientRequest, ChatClientResponse>) |
— | skips when the response has tool calls | A tool call is not an answer yet, so there is nothing to judge. |
order(int) |
int |
LOWEST_PRECEDENCE - 2000 |
Where in the advisor chain this runs. |
Retry until it passes is not supported
maxRepeatAttempts is capped deliberately. Each attempt is a model call plus a
judging call, and a criterion the model cannot satisfy would otherwise loop forever.
What the judge can see¶
The judged state is the prompt on one side and the final answer on the other. Tool results
are included when they are present in the prompt — a ToolResponseMessage is unpacked
rather than dropped, since it carries no text of its own.
Internal tool execution hides tool results
With Spring AI's default internal tool execution the model loop runs inside the
ChatModel, and the intermediate ToolResponseMessages never reach an advisor at all.
This matters when writing criteria. A groundedness question phrased as "every claim must trace back to the question" will fail a correct tool-using answer, because the value the tool returned legitimately appears nowhere in the question. Either phrase the criterion against what is actually visible, or disable internal tool execution so the tool messages land in the prompt.
Failing hard¶
By default the advisor returns the best effort once attempts run out, which matches self-refine convention. Turn that around where shipping a rejected answer is worse than failing:
JevSelfRefineAdvisor.builder()
.judge(judge)
.maxRepeatAttempts(2)
.failOnExhaustedAttempts(true) // throws JevSelfRefineFailedException
.build();
catch (JevSelfRefineFailedException ex) {
log.error("gave up: {}", ex.verdict().summary());
ex.verdict().failures().forEach(f -> log.error(" {}", f.detail()));
}
Ordering with the guardrail advisor¶
The two compose, and they do different jobs.
JevGuardrailAdvisor defaults to
LOWEST_PRECEDENCE - 1000, which runs it later — nearer the model — so it screens the
answer self-refinement settled on rather than an intermediate draft.
.defaultAdvisors(
JevSelfRefineAdvisor.builder().judge(judge).build(), // quality, retries
JevGuardrailAdvisor.builder(typeSafeClient).build()) // safety, last word
Retrying does not help a guardrail: an unsafe answer is not a draft.
Streaming¶
adviseStream is unsupported and returns Flux.error(UnsupportedOperationException). A
verdict needs the whole answer, so there is nothing useful to emit incrementally.
See Also¶
- JevJudge — the criteria this advisor evaluates
- JevGuardrailAdvisor
- Demos —
LlmJudgeDemoApplication