What Youโll Build
A progression of custom judges: a lambda check, a reusable deterministic judge, a composed AI judge, an LLM judge with full control, and a RAG-specific judge. Youโll wire them into a jury alongside built-in judges. This tutorial builds on Build an Evaluation Pipeline. You should be comfortable withJudgmentContext, Judgment, and SimpleJury before starting.
Step 1: Lambda Judge
The simplest custom judge is a lambda โ three lines:Judge is a @FunctionalInterface with a single method: judge(JudgmentContext) -> Judgment.
Any lambda or method reference that matches this signature is a judge.
Lambda judges work well for one-off checks.
For production judges, extend a base class to get metadata and structured checks.
Step 2: Extend DeterministicJudge
For a judge youโll reuse, extendDeterministicJudge.
It implements JudgeWithMetadata so infrastructure code (logging, metrics, verdict reporting) can discover the judgeโs name and type.
- Constructor calls
super(name, description)โ this becomesmetadata()for logging and verdicts - Checks provide granular sub-assertions โ on failure, you can distinguish โfile not foundโ from โfile found but annotation missingโ
Judgment.error()handles unexpected failures without crashing the jury
Step 3: Add Metadata to Lambda Judges
If you prefer lambdas but still want metadata, useJudges.named():
| Approach | Metadata | Checks | Best for |
|---|---|---|---|
| Lambda | No (unless wrapped) | No | One-off, inline checks |
Judges.named(lambda) | Yes | No | Named lambdas in juries |
DeterministicJudge subclass | Yes | Yes | Reusable, production judges |
Step 4: Build an AI Judge with ModelBackedJudge
For semantic criteria that canโt be checked from files or commands, you need an AI-backed judge.ModelBackedJudge composes three parts โ a prompt template, a model backend, and a response classifier โ into a judge without subclassing.
| Component | Role | Examples |
|---|---|---|
JudgePromptTemplate | Renders {{variable}} placeholders from JudgmentContext | fromString(id, text), fromClasspath(path) |
JudgeModel | Invokes the AI backend | SpringAiJudgeModel (agent-judge-llm), AgentClientJudgeModel (agent-judge-agent-client) |
JudgmentClassifier | Maps the modelโs text response to a Judgment | JudgmentClassifiers.passFail(...), custom lambda |
{{goal}}, {{output}}, {{workspace}}, {{status}}, {{metadata.*}}.
ModelBackedJudge lives in the agent-judge-ai-core module, which has zero external dependencies.
The actual AI backend arrives through a JudgeModel implementation from a bridge module (agent-judge-llm or agent-judge-agent-client).| Approach | Best for | Trade-off |
|---|---|---|
ModelBackedJudge | Most AI judges โ composable, testable, framework-neutral | Classifier must handle parsing |
LLMJudge subclass | Custom parsing logic that doesnโt fit a classifier pattern | Requires subclassing, coupled to Spring AI |
ModelBackedJudge as the default. Reach for LLMJudge when you need full control over prompt construction or response parsing.
Step 5: Write a Custom LLM Judge
For full control over prompt construction and response parsing, extendLLMJudge.
It uses the template method pattern โ you implement two methods:
buildPrompt()โ construct the evaluation prompt from the contextparseResponse()โ parse the LLMโs response into aJudgment
LLM judges require the
agent-judge-llm module and Spring AI.
Always handle parse failures gracefully โ Judgment.error() prevents one broken response from crashing the jury.Prefer deterministic judges when the criterion can be checked from files, commands, or structured metadata. Use LLM judges when the criterion is semantic and cannot be expressed reliably as code.
Step 6: Wire Custom Judges into a Jury
Combine your custom judges with built-in ones:WeightedAverageStrategy normalizes all scores to [0, 1] and computes a weighted average.
Build success (weight 2.0) matters more than file existence (weight 1.0), and quality (weight 1.5) falls in between.
Step 7: Write a Custom RAG Judge
RAG judges follow the sameLLMJudge pattern but use the RagContext helper to extract the (question, context, answer) triple from metadata.
Hereโs a custom judge that evaluates answer completeness โ did the answer address all parts of the question?
- Use
RagContext.question(),RagContext.context(),RagContext.answer()to extract the triple - Return
ABSTAINwhen required inputs are missing โ this prevents misleading verdicts - Use the
(?mi)^\s*Answer:\s*(YES|NO)regex pattern for reliable LLM response parsing
RagContext.question() falls back to context.goal() and RagContext.answer() falls back to context.agentOutput(), but the explicit metadata keys are the recommended convention. LangChain4jโs sources are also available as a fallback for RagContext.context().
What You Built
| Judge | Approach | Score | What it checks |
|---|---|---|---|
timestampCheck | Lambda | Boolean | Agent log exists |
AnnotationJudge | DeterministicJudge | Boolean | File contains @RestController |
| Goal completion | ModelBackedJudge | Boolean | Agent accomplished its goal |
CodeQualityJudge | LLMJudge | Numerical (0-10) | Code quality assessment |
CompletenessJudge | LLMJudge (RAG) | Boolean | Answer addresses all parts of the question |
RagContext metadata convention).
Whatโs Next
Built-in Judges
Catalog of built-in judges โ see whatโs already available before writing custom judges
Jury System
CascadedJury for tiered evaluation, voting strategy configuration