Genkit LLM Judge Prompts

AI Evaluation & Safety advanced Building with AI ~22 min read

Overview

Genkit LLM Judge Prompts are code-defined evaluation prompts within the Firebase Genkit framework that use LLM providers as automated judges. Rather than writing hand-crafted heuristics to evaluate LLM output quality, this pattern delegates judgment to another LLM call with a carefully structured prompt. The judge receives the original input, the system’s output, and evaluation criteria, then returns a structured JSON response containing a pass/fail decision, a numeric score, a reasoning explanation, and extracted keywords.

In this pattern, judge prompts are defined as Go structs with typed EvalInput and EvalOutput schemas. The Genkit framework handles serialization, provider routing, and retry logic. Each judge prompt is registered as a reusable evaluator that can be composed into evaluation suites run by the evaluation runner.

The key advantage of this approach over rule-based evaluation is flexibility: judge prompts can assess subjective qualities like “helpfulness,” “factual accuracy,” or “tone appropriateness” that resist simple pattern matching. The tradeoff is cost (each evaluation requires an additional LLM call) and the meta-problem of evaluating the evaluator itself. This pattern addresses both challenges through confidence metadata that flags low-confidence judgments for human review.

Core Concepts

Typed EvalInput/EvalOutput Structs

The judge prompt’s input and output are defined as Go structs with JSON tags. EvalInput contains the original user query, the system’s response, any reference answer, and evaluation criteria. EvalOutput contains Pass (bool), Score (float64, 0–1), Reason (string explanation), Keywords (extracted quality signals), and Confidence (float64, how certain the judge is). Type safety ensures that malformed judge responses are caught at deserialization time rather than propagating through the evaluation pipeline.

Code-Defined Prompt Templates

Judge prompts are defined in Go code rather than external files. The prompt template is a Go string constant that uses Genkit’s template syntax to interpolate EvalInput fields. This keeps the prompt version-controlled alongside the evaluator logic and enables programmatic prompt composition—for example, wrapping a base judge prompt with additional context for domain-specific evaluation.

Provider Model Routing

Each judge prompt specifies which LLM provider and model to use for the judgment call. A well-designed evaluation system routes expensive, high-stakes evaluations (factual accuracy) to more capable models and cheaper evaluations (format compliance) to faster, cheaper models. Provider routing is configured per-evaluator, not globally, allowing fine-grained cost optimization across an evaluation suite.

Confidence Metadata

The judge’s structured output includes a Confidence field (0.0–1.0) indicating how certain the LLM is about its judgment. Low-confidence judgments (below a configurable threshold, typically 0.7) are flagged for human review rather than being treated as authoritative pass/fail decisions. This addresses the meta-reliability problem: the judge itself can be uncertain, and the system should surface that uncertainty rather than hiding it.

Structured JSON Output Enforcement

The judge prompt instructs the LLM to respond in strict JSON format matching the EvalOutput schema. Genkit’s output parsing validates the response against the schema and retries with a corrective prompt if the response is malformed. This is more reliable than regex-based parsing and handles edge cases like the LLM wrapping JSON in markdown code fences or adding explanatory text outside the JSON block.

How It Works

The judge prompt lifecycle is: define the prompt template and IO types, register it as a Genkit prompt, wrap it in an evaluator adapter, and plug it into the evaluation runner. Each evaluation call serializes the input, invokes the LLM, parses the structured output, and returns the result.

EvalInput/EvalOutput Type Definitions

package eval

type EvalInput struct {
    Query     string `json:"query"`
    Response  string `json:"response"`
    Reference string `json:"reference,omitempty"`
    Criteria  string `json:"criteria"`
}

type EvalOutput struct {
    Pass       bool     `json:"pass"`
    Score      float64  `json:"score"`
    Reason     string   `json:"reason"`
    Keywords   []string `json:"keywords"`
    Confidence float64  `json:"confidence"`
}

Judge Prompt Definition

package eval

import (
    "context"
    "encoding/json"
    "fmt"

    "github.com/firebase/genkit/go/ai"
    "github.com/firebase/genkit/go/genkit"
)

const factualAccuracyPrompt = `You are an expert evaluator assessing the factual accuracy of an AI response.

## Input
User Query: {{.Query}}
AI Response: {{.Response}}
{{if .Reference}}Reference Answer: {{.Reference}}{{end}}

## Evaluation Criteria
{{.Criteria}}

## Instructions
Evaluate the AI response for factual accuracy. Consider:
1. Are all stated facts correct?
2. Are there any hallucinated claims?
3. Is the response consistent with the reference answer (if provided)?

Respond with a JSON object matching this exact schema:
{
  "pass": true/false,
  "score": 0.0 to 1.0,
  "reason": "explanation of your judgment",
  "keywords": ["key", "quality", "signals"],
  "confidence": 0.0 to 1.0
}`

type FactualAccuracyJudge struct {
    model  ai.Model
    prompt *genkit.Prompt[EvalInput, EvalOutput]
}

func NewFactualAccuracyJudge(g *genkit.Genkit, model ai.Model) (*FactualAccuracyJudge, error) {
    prompt, err := genkit.DefinePrompt(g, "factual-accuracy-judge",
        ai.WithPromptText(factualAccuracyPrompt),
        ai.WithInputType[EvalInput](),
        ai.WithOutputType[EvalOutput](),
        ai.WithModel(model),
        ai.WithConfig(&ai.GenerationCommonConfig{
            Temperature: 0.0, // deterministic judging
        }),
    )
    if err != nil {
        return nil, fmt.Errorf("define prompt: %w", err)
    }

    return &FactualAccuracyJudge{model: model, prompt: prompt}, nil
}

Evaluator Adapter

package eval

import (
    "context"
    "fmt"
)

func (j *FactualAccuracyJudge) Name() string {
    return "factual-accuracy"
}

func (j *FactualAccuracyJudge) Evaluate(
    ctx context.Context, ex Example, out Output,
) (EvalResult, error) {
    input := EvalInput{
        Query:    fmt.Sprintf("%v", ex.Input["query"]),
        Response: out.Response,
        Criteria: "Assess factual accuracy. Flag any hallucinated or unsupported claims.",
    }

    // Check for reference answer
    if ref, ok := ex.Input["reference"]; ok {
        input.Reference = fmt.Sprintf("%v", ref)
    }

    // Invoke the judge prompt
    resp, err := j.prompt.Execute(ctx, input)
    if err != nil {
        return EvalResult{}, fmt.Errorf("judge prompt execution: %w", err)
    }

    evalOut := resp.Output()

    return EvalResult{
        Pass:     evalOut.Pass,
        Score:    evalOut.Score,
        Reason:   evalOut.Reason,
        Keywords: evalOut.Keywords,
    }, nil
}

func (j *FactualAccuracyJudge) MinConfidence() float64 {
    return 0.7
}

Composing Multiple Judges into a Suite

package eval

import (
    "github.com/firebase/genkit/go/ai"
    "github.com/firebase/genkit/go/genkit"
)

func BuildEvalSuite(g *genkit.Genkit, capableModel, cheapModel ai.Model) ([]Evaluator, error) {
    factual, err := NewFactualAccuracyJudge(g, capableModel)
    if err != nil {
        return nil, err
    }

    // Tone and helpfulness use a cheaper model
    tone, err := NewToneJudge(g, cheapModel)
    if err != nil {
        return nil, err
    }

    format, err := NewFormatComplianceJudge(g, cheapModel)
    if err != nil {
        return nil, err
    }

    return []Evaluator{factual, tone, format}, nil
}

Practical Applications

LLM judge prompts are the primary mechanism for evaluating RAG (Retrieval-Augmented Generation) pipeline quality. The factual accuracy judge checks whether retrieved context was faithfully used in the response. A separate “groundedness” judge verifies that every claim in the response can be traced back to a retrieved document, catching hallucinations where the model invents plausible-sounding facts.

The confidence metadata powers a human-in-the-loop review workflow. When a judge reports confidence below 0.7, the example is routed to a review queue where a human annotator makes the final pass/fail decision. This is especially common for ambiguous cases where the evaluation criteria are subjective (e.g., “is the tone appropriate for a technical audience?”). Over time, the human annotations are fed back as few-shot examples in the judge prompt, improving its calibration.

Provider model routing enables cost-efficient evaluation suites. Teams typically run factual accuracy evaluations (which require strong reasoning) on a capable model, while format compliance checks (which are mechanical) run on a fast, cheap model. A typical suite of 5 evaluators across 500 examples costs roughly $2–5 when mixing model tiers, compared to $15–25 if all evaluators used the most capable model.

Common Pitfalls

Further Reading