SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Evaluation and Monitoring

Databricks GenAI Engineer: Evaluating LLMs and Agents with MLflow Scorers and Judges

20 min readDatabricks GenAI · Evaluation and MonitoringUpdated

Evaluating LLMs and agents on Databricks means measuring an application's quality with MLflow: running mlflow.genai.evaluate over an evaluation dataset, scoring every record with built-in judges, custom code scorers and custom LLM judges, reading the traces to find where failures start, and bringing subject-matter experts in through the Review App to supply ground truth and align the judges. This lesson covers choosing a model size and architecture from quantitative results, the evaluation harness and its data modes, which judges need ground truth, writing custom scorers and judges, and turning SME feedback into better evaluation.

What you’ll learn
  • Select an LLM by size and architecture from quantitative evaluation results, applying hard thresholds before the objective and weighing dense versus mixture-of-experts throughput and memory
  • Run mlflow.genai.evaluate with the right data shape, predict_fn signature or to_predict_fn endpoint URI, and know when to score stored outputs or traces instead of re-running the app
  • Use evaluation traces and run comparison to locate failing steps and per-request regressions
  • Identify which built-in judges require ground truth in expectations, and choose between Guidelines and ExpectationsGuidelines
  • Write @scorer code scorers, including trace-based checks, and custom LLM judges with make_judge
  • Collect SME feedback and expectations through labeling sessions and the Review App chat, sync labels to evaluation datasets and align judges with expert feedback

How do you choose an LLM size and architecture from evaluation metrics?

You choose an LLM by measuring every candidate on the same representative evaluation set with the same scorers, recording operational metrics such as latency and cost alongside the quality scores, and then applying the application's requirements in order: drop every model that misses a hard threshold, and only then let the stated objective (lowest cost, highest quality, highest throughput) pick among the survivors.

The evidence has to come from this application. Public leaderboards, general benchmarks and a model card's parameter count describe general capability, not how a model answers your users' questions against your quality bar. Building a different evaluation dataset for each candidate breaks the comparison too: the dataset, the scorers and the judge configuration must be identical so the only variable is the model.

Thresholds first, objective second

  1. List the hard requirements (for example a minimum Correctness mean, a minimum RetrievalGroundedness mean, a latency ceiling such as p95 under a set number of milliseconds, a memory budget).
  2. Eliminate every candidate that fails any one of them, even by a hundredth. A model that is cheap, fast and nearly good enough is still out.
  3. Rank the survivors on the objective the stakeholders named. If only one model survives, the objective never gets a say.

The common mistakes are treating a gate as the objective (choosing the most grounded model when groundedness was only a floor and correctness was the goal), and ignoring a gate because a model excels on the objective.

Metric familyExamplesTypical role
Quality (judge or scorer means)Correctness, RetrievalGroundedness, Guidelines pass rate, custom scorer resultsFloors that every candidate must meet; sometimes the objective
LatencyMedian and p95 end-to-end latency, time to first tokenCeilings for interactive use
ThroughputTokens or requests per second at a given hardware sizeObjective for high-volume or batch work
CostPrice per thousand requests or tokens, GPU cost for self-hostingUsually the tie-breaker objective once quality passes

Which offline metric fits which task (for example ROUGE for summarisation) is taught in the evaluation-metrics lesson of the application development domain; here the focus is using the numbers to decide.

Dense versus mixture-of-experts: throughput and memory

A dense model uses all of its parameters for every token, while a mixture-of-experts (MoE) model has a router that sends each token through only a few expert sub-networks, so it is described by two numbers: total parameters and active parameters per token.

Dense modelMixture-of-experts model
Parameters used per tokenAll of themOnly the active experts (the "active" count)
Compute per token, and so generation speedScales with total sizeScales with active parameters
GPU memory needed for weightsTotal sizeTotal size: every expert must be resident, even though few run per token
QualityTends to grow with sizeOften close to a dense model of much larger size

So an MoE with a large total but modest active count can generate faster than a dense model whose total is smaller than the MoE's total but larger than its active count, provided the hardware can hold all of the MoE's weights. When memory is not a constraint and throughput is the objective, compare dense size with MoE active parameters. When a memory budget is stated, compare it with MoE total parameters first: a model whose total weights do not fit is eliminated, however small its active count.

How does mlflow.genai.evaluate run an evaluation?

mlflow.genai.evaluate() is the MLflow 3 evaluation harness: you pass data (the evaluation records), optionally a predict_fn (the app to run) and a list of scorers, and it creates an evaluation run in the experiment in which every record gets a trace with the scorers' assessments attached.

Each record can carry three fields:

  • inputs: a dictionary with the request. Its keys are the parameter names of predict_fn, because MLflow calls predict_fn(**inputs). A record {"inputs": {"question": ..., "region": ...}} therefore needs def predict_fn(question, region); a function that takes one dictionary, omits a key or uses different parameter names fails or receives the wrong values.
  • outputs (optional): an answer the app already produced.
  • expectations (optional): ground truth, such as expected_facts, expected_response or per-record guidelines.
import mlflow
from mlflow.genai.scorers import Correctness, Safety

results = mlflow.genai.evaluate(
    data=eval_records,            # list of dicts, DataFrame or evaluation dataset
    predict_fn=my_agent,          # called as my_agent(**record["inputs"])
    scorers=[Correctness(), Safety()],
)

Data can be a list of dictionaries, a pandas or Spark DataFrame, or a Unity Catalog evaluation dataset. To evaluate an agent that is already deployed, without loading it locally or writing a REST wrapper, use mlflow.genai.to_predict_fn("endpoints:/<endpoint-name>"), which turns a Model Serving endpoint into a predict function (an apps:/<app-name> URI does the same for a Databricks App). The URI names a running endpoint or app, so the evaluation calls the deployed version exactly as clients do; loading a registered model into the notebook evaluates a local copy instead.

When should you score stored outputs or traces instead of re-running the app?

Score what already exists when you want to judge answers that were already produced; re-run the app with a predict_fn when you want to judge a version's own answers to a set of requests. The data you pass decides which mode you are in.

Data passedpredict_fnWhat gets scored
inputs (+ optional expectations)RequiredFresh outputs from the app you pass, each with a new trace
inputs + outputs (+ optional expectations)Must not be providedThe stored outputs, as they are
A trace column (for example from mlflow.search_traces)Must not be providedThe finished executions in those traces

Two consequences follow. First, stored answers from a model that can no longer be called are scored by passing inputs, outputs and any expectations with no predict_fn; leaving outputs out leaves nothing to judge, and adding a predict_fn would replace the stored answers. Second, to compare a new version on requests captured in production traces, rebuild the records from each trace's request as inputs, drop the trace column, and keep the predict_fn: scoring the trace column would only evaluate the old version again.

Also remember what stored data lacks. A table or CSV of questions and final replies has no execution trace, so a scorer that inspects tool calls or retrieval has nothing to read; if the app is still available, re-running it with a predict_fn produces traces to inspect.

How do traces help you diagnose and compare evaluation results?

Traces turn a failing score into a located defect: because mlflow.genai.evaluate traces the app on every record and attaches each scorer's assessment to that trace, opening the trace of a failed record shows every span, such as the retriever's query and returned chunks, the tool chosen with its arguments and output, and the final LLM generation.

Aggregate means and a judge's rationale tell you that a record failed and roughly why; the spans tell you where. For a RAG agent that fails Correctness, check whether the required fact is in the retriever span's outputs:

  • Fact missing from the retrieved chunks, answer consistent with them: the defect is in retrieval (chunking, query, index contents). Generation-side fixes cannot supply context that was never retrieved.
  • Fact present in the chunks, answer ignores or contradicts it: the defect is in generation (prompt, model, parameters).
  • Wrong tool called, or a tool returned an error or bad data: the defect is in tool selection or the tool itself.

The rule is to fix the stage the spans point at: more measurement repeats the diagnosis, and the evaluation run's own traces are where its records live.

Comparing two versions

Each evaluate call is its own evaluation run. For a visual, request-by-request comparison, select two runs in the experiment's Evaluation runs view and choose Compare: each request is listed across the runs with its traces and judge assessments, which a non-coding stakeholder can read. To find regressions programmatically, fetch each run's traces with mlflow.search_traces(run_id=...), pair them by their inputs and compare each judge's value per request. Inputs are the stable key because every run creates new traces with new IDs. Run-level means can hide regressions because improvements on some requests offset losses on others.

Which evaluation judges require ground truth?

A judge requires ground truth when it needs to know what the right answer contains, which it reads from the record's expectations; judges that only relate the request, the response and the retrieved context can run on a dataset that holds nothing but questions, because the trace already has everything they read.

Built-in judgeWhat it checksNeeds expectations?
CorrectnessResponse agrees with the expected facts or expected responseYes (expected_facts or expected_response)
RetrievalSufficiencyRetrieved context contains what is needed for the expected answerYes
ExpectationsGuidelinesResponse follows the guidelines stored in that recordNo ground truth, but needs per-record guidelines in expectations
ToolCallCorrectnessThe agent called the right tools with the right argumentsYes in Databricks' judges table (expected tool calls); open-source MLflow also offers a mode without expectations
RelevanceToQueryResponse addresses the requestNo
SafetyResponse is free of harmful contentNo
GuidelinesResponse follows rules given to the judge itselfNo
RetrievalGroundednessResponse is supported by the retrieved contextNo (reads the retriever span)
RetrievalRelevanceRetrieved chunks are relevant to the requestNo (reads the retriever span)

The near-twins are worth fixing in memory: Sufficiency needs to know the answer to say whether the context covers it, while Groundedness and Relevance only compare things the trace already holds. The tool-call judges read the tool spans. ToolCallCorrectness is listed by Databricks among the judges that require ground truth, because it compares the calls made with the expected tool calls in the record (open-source MLflow can also run it without them, judging the calls against the request). ToolCallEfficiency needs no ground truth: it looks for redundant or wasteful calls, not for a required order. How the retrieval judges score individual chunks is covered in the retrieval-evaluation lesson.

Practical planning follows from the table: when labeling budget is limited, spend it on the records you will score with Correctness or RetrievalSufficiency, and start immediately with the judges that need no labels. ExpectationsGuidelines sits between the two groups: it needs no reference answer, but someone must still write rules into each record.

Guidelines or ExpectationsGuidelines: global or per-record rules?

Use Guidelines when the same rules apply to every response, and ExpectationsGuidelines when the rules differ from record to record.

GuidelinesExpectationsGuidelines
Where the rules livePassed once to the judge, in natural languageIn each record's expectations as guidelines
Applied toEvery response the judge seesOnly that record's response
Ground truth neededNo: works on unlabeled dataNo reference answer, but someone writes the rules into each record's expectations
FitsBrand, tone, legal and policy rules (never name a competitor, never promise a date)Question-specific requirements (a leave answer must name the approval form)

Putting per-question rules into one global Guidelines judge, or into one judge per category, makes the judge apply every rule to every response it sees, which produces false failures wherever a rule does not apply. Both are pass/fail judges; neither produces a custom numeric scale. Behavioural rules belong in a guidelines judge because the other built-in judges have fixed criteria: Safety checks harmful content and Correctness matches facts.

How do you write custom code-based scorers?

A code-based scorer is a Python function decorated with @scorer (from mlflow.genai.scorers) that MLflow calls once per record; it is the right choice for checks that are exact, deterministic and free of judge-model cost, such as valid JSON, a required field, a length limit, a regular expression or a tool-call order.

Parameters are filled by name

Declare only the parameters you need: inputs (the record's request), outputs (the app's response), expectations (the record's ground-truth dictionary) and trace (the full execution trace). A scorer that reads expectations["expected_sku"] therefore needs that key in every record's expectations. Nothing is passed per record when you list the scorer in scorers.

import json
from mlflow.entities import Feedback
from mlflow.genai.scorers import scorer

@scorer
def format_checks(outputs):
    data = json.loads(outputs)          # an uncaught error becomes Feedback(value=None, error=...)
    return [
        Feedback(name="has_order_id", value="order_id" in data),
        Feedback(name="under_150_words", value=len(str(data).split()) < 150),
    ]

Return values

  • A primitive (bool, int, float, str) or a single Feedback becomes one metric, named after the Feedback's name or, if none, the function.
  • A list of Feedback becomes several metrics, and each Feedback in the list needs its own distinct name.
  • The number of metrics is decided by the number of Feedback objects returned, not by what a value contains.

Errors

If the scorer raises an exception for a record, MLflow catches it, records a Feedback whose value is None with the error details, and continues with the other records. One bad record therefore shows up as an error on that record, never as a failed check or a stopped evaluation. You may also catch the error yourself and return a Feedback carrying the error.

How do scorers inspect the trace instead of the final answer?

When the requirement concerns how the app reached its answer (which tools ran, in what order, whether retrieval returned anything), only the trace holds the evidence, so the scorer declares the trace parameter and searches its spans.

from mlflow.entities import SpanType
from mlflow.genai.scorers import scorer

@scorer
def identity_before_refund(trace):
    tools = [s.name for s in trace.search_spans(span_type=SpanType.TOOL)]
    if "issue_refund" not in tools:
        return True
    return "verify_identity" in tools and tools.index("verify_identity") < tools.index("issue_refund")

trace.search_spans(span_type=SpanType.RETRIEVER) returns the retriever spans; a span's inputs hold what it was given (the query) and its outputs hold what it returned (the documents), so an empty-retrieval check reads the outputs. Watch edge cases: all() over zero matching spans is True, so decide explicitly what a trace without the span should score.

A Guidelines judge or a scorer that reads only outputs cannot see tool calls when the final reply looks the same either way. A trace-aware LLM judge can see them, but it is an LLM call: it costs tokens and is not deterministic, so for exact order or presence checks the code scorer wins.

How do you build a custom LLM judge with make_judge?

make_judge creates a custom LLM judge from natural-language instructions that reference template variables, plus a feedback value type; use it when the assessment is subjective (a rubric, tone, appropriate use of evidence) and none of the built-in judges' fixed criteria fit.

from typing import Literal
from mlflow.genai.judges import make_judge

compliance = make_judge(
    name="compliance",
    instructions=(
        "Read the customer request in {{ inputs }} and the reply in {{ outputs }}. "
        "Classify the reply against the bank's disclosure policy."
    ),
    feedback_value_type=Literal["compliant", "non_compliant", "needs_review"],
)
Template variableWhat the judge sees
{{ inputs }}The request
{{ outputs }}The response
{{ expectations }}The record's ground truth
{{ trace }}The whole execution trace, which the judge explores (tool calls, intermediate results); a judge model must be specified for trace-based judges

feedback_value_type controls the shape of the result: bool for pass/fail, int or float for a numeric scale (for example a 1 to 5 rubric), Literal[...] for a fixed set of categories enforced through structured output, simple dicts and lists, and str for free text. If you leave it unset, the judge decides the type itself, so MLflow recommends setting it explicitly. Only Literal constrains the value to named categories: free text relies on the instructions alone, so labels can drift ("Compliant."), and numeric or boolean types carry no category names. Every judgement comes back as Feedback with a rationale.

Choosing between scorer types

NeedBest fit
Exact, deterministic, no model cost@scorer function
Global plain-language rules, pass/failGuidelines
Custom scale or categories over request and responsemake_judge with {{ inputs }}/{{ outputs }}
Semantic judgement about intermediate stepsmake_judge with {{ trace }} and a model

How do you incorporate SME feedback into evaluation?

Subject-matter experts improve an agent by recording their judgements on traces without writing code, through the Review App; those judgements then become ground truth for evaluation datasets or human labels for aligning LLM judges.

Expectations versus feedback

ExpectationFeedback
MeaningWhat the app should produce (ground truth)An assessment of what it did produce
ExamplesExpected facts, expected response, per-record guidelinesAcceptable or not, a rating, a comment
Used byCorrectness, RetrievalSufficiency, ExpectationsGuidelines; becomes the record's expectations in a datasetQuality tracking; judge alignment
Recorded withAdding an expectation from the trace view, mlflow.log_expectation, EXPECTED_FACTS label schemaTrace UI feedback, mlflow.log_feedback, feedback label schemas

Labeling sessions: reviewing existing traces

A labeling session is a run in the experiment that holds the traces to review, the label schemas that define the questions (built-in schemas such as expected facts and guidelines, or custom feedback and expectation schemas) and the assigned reviewers. Experts work in the Review App; their answers are saved as assessments on each trace. When the goal is ground truth for Correctness, use the expected-facts schema, then call session.sync(to_dataset=...): sync matches records on inputs, overwrites the same-named expectations of matching records, adds unmatched traces as new records and leaves untouched records as they were. Syncing is a merge keyed on inputs.

Chat testing: experts talk to the agent

Before any traces or dataset exist, experts can converse with a deployed agent. Attach a Model Serving endpoint with get_review_app() and review_app.add_agent(agent_name=..., model_serving_endpoint=...), then share the Review App URL ending in /chat. Every conversation and rating is captured as a trace. Chat reviewers need three things: an identity provisioned in the Databricks account, the workspace access entitlement for the workspace hosting the experiment (provisioning alone is not enough) and CAN_QUERY on the serving endpoint. Grant the least permission that lets them query the endpoint.

How do you align an LLM judge with expert feedback?

Alignment makes an LLM judge apply the experts' standard by calling the judge's align() method on traces that carry both the judge's assessment and human feedback; it returns a new judge, which you then use (and can register) in place of the original, with no hand-editing of the instructions.

Alignment works on built-in judges and on judges created with make_judge. It needs:

  • Matching names. The human feedback's assessment name must exactly match the judge's name. Feedback logged under a different name (for example a labeling schema called "quality" for a judge named "answer_quality") is ignored, and the aligned judge behaves like the original.
  • Feedback, not expectations. Alignment reads human feedback on the judged traces; expected facts are a different label type.
  • Enough examples. At least 10 labeled traces is the minimum, and 50 to 100 work better.
  • Balance. Include poor and fair answers so that a meaningful share of labels, around 30%, are negative; a set where every label is "good" teaches the judge nothing about what experts reject. The sample must include traces the judge passes as well as fails, or its false passes stay hidden.
  • Real reasoning. Experts' own, specific rationales carry the standard the judge learns from.

The principle: a judge learns a team's standard only from that team's labeled examples, which is what alignment supplies.

Tip. Expect scenario questions in which a Generative AI Engineer has a concrete requirement (no judge-model cost, deterministic results, a model that can no longer be called, non-coding reviewers, a memory budget, thresholds followed by an objective) and must pick a model, a scorer, a data setup or an SME workflow. Model-selection items give a small table of evaluation results with architectures, latency or cost and reward applying every threshold before the objective. Harness items show record dictionaries, predict_fn signatures, endpoint URIs or short scorer code and test what MLflow passes and returns. Judge items contrast near-twins such as RetrievalSufficiency and RetrievalGroundedness, Guidelines and ExpectationsGuidelines, expectations and feedback, and code scorers versus trace-aware LLM judges. SME items cover labeling schemas, dataset sync behaviour, Review App chat access and the conditions for judge alignment. Some items ask you to select two answers.

Key takeaways
  • Compare candidate LLMs on one shared, representative evaluation set with identical scorers, plus latency and cost; eliminate on every hard threshold first, then optimise the stated objective.
  • MoE generation speed tracks active parameters, but GPU memory must hold the total parameters.
  • mlflow.genai.evaluate calls predict_fn(**inputs); records with outputs or a trace column are scored as they are and must not come with a predict_fn.
  • To test a new version on captured requests, turn the requests into inputs, drop traces and outputs, and keep predict_fn; to_predict_fn("endpoints:/<name>") evaluates a deployed endpoint.
  • Correctness, RetrievalSufficiency and (per Databricks) ToolCallCorrectness need ground truth in expectations; ExpectationsGuidelines needs per-record guidelines instead; RelevanceToQuery, Safety, Guidelines, RetrievalGroundedness and RetrievalRelevance need neither.
  • @scorer functions get inputs, outputs, expectations and trace by name; return primitives, one Feedback or a list of distinctly named Feedback; uncaught errors become Feedback with value None.
  • Tool order and retrieval results live only in the trace: use trace.search_spans in a code scorer for deterministic checks, or make_judge with {{ trace }} and a model for semantic ones.
  • Expectations are ground truth, feedback rates the output; labeling sessions sync expectations to datasets by inputs, and judge.align() needs feedback named exactly like the judge, ideally 50 to 100 traces with about 30% negative.

Frequently asked questions

Which MLflow judges require ground truth?

In MLflow 3 on Databricks, Correctness (expected facts or an expected response), RetrievalSufficiency (expected facts or response, to check the retrieved context covers them) and ToolCallCorrectness (expected tool calls) require ground truth in each record's expectations. ExpectationsGuidelines needs no reference answer but reads per-record guidelines from expectations. RelevanceToQuery, Safety, Guidelines, RetrievalGroundedness and RetrievalRelevance work from the request, the response and the trace alone, so they can run on a dataset that contains only questions.

Can mlflow.genai.evaluate score answers that were produced earlier?

Yes. Pass records that contain inputs, outputs and any expectations, or a trace column from mlflow.search_traces, and do not provide a predict_fn; the scorers judge those stored outputs as they are. Provide a predict_fn only when you want MLflow to run an app on the inputs and score its fresh outputs.

What is the difference between Guidelines and ExpectationsGuidelines?

The Guidelines judge receives one set of natural-language rules and applies them to every response, so it needs no labels. ExpectationsGuidelines reads rules stored in each record's expectations and judges that record's response only against them, which suits requirements that differ from question to question.

When should I write a code-based scorer instead of an LLM judge?

Write an @scorer function when the check is exact and objective, such as valid JSON, a required field, a length limit or the order of tool calls in the trace; it is deterministic and has no judge-model cost. Use an LLM judge, built-in or from make_judge, when the assessment needs semantic judgement such as tone, a rubric or whether evidence was used appropriately.

What does make_judge's feedback_value_type do?

It sets the type of value the custom judge returns in its Feedback: bool for pass/fail, int or float for a numeric scale, Literal[...] for a fixed set of categories enforced through structured output, or str for free text. Leaving it unset lets the judge choose the type, so set it explicitly. Choosing Literal guarantees every value is exactly one of the allowed labels.

How do SME labels from the Review App reach an evaluation dataset?

Create a labeling session with the traces and a label schema such as the built-in expected facts schema, assign the experts, and let them label in the Review App. Calling session.sync(to_dataset=...) then upserts the labeled traces into the Unity Catalog evaluation dataset, matching on inputs: matching records get their expectations overwritten and unmatched traces become new records.

Why might judge.align() leave an LLM judge unchanged?

Alignment only uses human feedback whose assessment name exactly matches the judge's name, so feedback recorded under a different name is ignored. Results also suffer when there are too few labeled traces (10 is the minimum, 50 to 100 work better) or when nearly all labels are positive, because the judge never sees what the experts reject.

Does a mixture-of-experts model need less GPU memory than a dense model of the same total size?

No. A mixture-of-experts model computes each token with only its active experts, so its generation speed tracks active parameters, but all experts' weights must be loaded, so its memory requirement tracks total parameters, the same as a dense model of that total size.

Source

This lesson covers the "Evaluation and Monitoring" domain of the official Databricks GenAI exam guide. Vendors revise their guides — check the source for the current version.

Test yourself on this topic
Practice now

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?