Custom Scorers and mlflow.genai.evaluate(): Evaluating an Agent from Its Traces
Databricks GenAI Engineer · Deep dive

Custom Scorers and mlflow.genai.evaluate(): Evaluating an Agent from Its Traces

A custom scorer in MLflow is a Python function decorated with @scorer that grades one evaluated example, reading its inputs, outputs, expectations or full trace and returning a bool, a number, a string or a Feedback with a rationale. mlflow.genai.evaluate() runs the app over an evaluation dataset, records a trace per row and applies custom scorers alongside built-in LLM judges.

Last updated November 2026.

What this deep dive adds

Chapter 49 of the course introduces the @scorer decorator, the four arguments a scorer can read and its return types; Chapter 50 explains why agents are evaluated from their traces. This post joins them in one working example: a traced app, an evaluation dataset, four scorers including one that grades the retrieval step from the trace, the results, and re-scoring stored traces without running the app again.

Both are Section 6 (Evaluation and Monitoring) objectives in the 18 March 2026 exam guide. Which built-in judges need ground truth is Chapter 48 of the course, summarised in the table below.

The complete example

A small support agent retrieves a policy and answers in JSON. Each function carries @mlflow.trace, so every call produces a trace with an agent span and a retriever span inside it:

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

mlflow.set_experiment("/Shared/support-agent-eval")

POLICIES = {
    "returns": "Items can be returned within 30 days with a receipt.",
    "shipping": "Standard shipping takes 3 to 5 business days.",
}

@mlflow.trace(span_type=SpanType.RETRIEVER)
def retrieve(question: str) -> list[dict]:
    return [{"doc_id": k, "text": v}
            for k, v in POLICIES.items() if k in question.lower()]

@mlflow.trace(span_type=SpanType.AGENT)
def support_agent(question: str) -> str:
    docs = retrieve(question)
    answer = docs[0]["text"] if docs else "I don't know."
    return json.dumps({"answer": answer,
                       "sources": [d["doc_id"] for d in docs]})

eval_data = [
    {"inputs": {"question": "What is your returns window?"},
     "expectations": {"expected_source": "returns"}},
    {"inputs": {"question": "How long does shipping take?"},
     "expectations": {"expected_source": "shipping"}},
    {"inputs": {"question": "Do you price match?"},
     "expectations": {"expected_source": "pricing"}},
]

@scorer
def valid_json(outputs) -> bool:
    try:
        json.loads(outputs)
        return True
    except (TypeError, ValueError):
        return False

@scorer
def concise(outputs) -> Feedback:
    words = len(json.loads(outputs)["answer"].split())
    return Feedback(value=words <= 40,
                    rationale=f"Answer has {words} words; the limit is 40.")

@scorer
def cites_expected_source(outputs, expectations) -> bool:
    return expectations["expected_source"] in json.loads(outputs)["sources"]

@scorer
def retrieval_hit(trace, expectations) -> Feedback:
    spans = trace.search_spans(span_type=SpanType.RETRIEVER)
    retrieved = [d["doc_id"] for s in spans for d in (s.outputs or [])]
    hit = expectations["expected_source"] in retrieved
    return Feedback(value=hit, rationale=f"Retrieved {retrieved or 'nothing'}.")

results = mlflow.genai.evaluate(
    data=eval_data,
    predict_fn=support_agent,
    scorers=[valid_json, concise, cites_expected_source, retrieval_hit],
)
print(results.metrics)

Run against MLflow 3.18 with a local tracking store. It printed valid_json/mean 1.0, concise/mean 1.0, and cites_expected_source/mean and retrieval_hit/mean of 0.67 each: the price-match question has no policy, so both source checks fail on that row. API from the Databricks "Code-based scorers" pages and the MLflow 3.18 package.

Reading the results

Each scorer becomes a metric named <scorer>/mean, the average across rows with a bool counted as 1 or 0. The evaluation run also stores one trace per row with every scorer's feedback attached, which is where the rationales appear. In the example, the low retrieval_hit and cites_expected_source means point to the same row, and its rationale ("Retrieved nothing") says the fix belongs in retrieval, not the prompt.

How mlflow.genai.evaluate turns rows into scored traces Each evaluation row passes its inputs to the app's predict function. The app runs and records a trace containing an agent span and a retriever span. The scorers then read the trace: valid_json and concise read the outputs, cites_expected_source reads outputs and expectations, and retrieval_hit reads the retriever span inside the trace. Each scorer attaches feedback to the trace, and the run reports one mean per scorer. Eval row inputs predict_fn runs the app Trace agent, retriever SCORERS READ THE TRACE valid_json: outputs concise: outputs cites_source: expectations retrieval_hit: span Feedback on each trace, one mean per scorer
The trace is the unit of evaluation: scorers can grade the answer or any step inside it.

Trace once, score again

Re-running an agent for every scorer change is slow and costs tokens. Stored traces can be scored directly: pass them as data and leave out predict_fn.

traces = mlflow.search_traces(run_id=results.run_id)
rescored = mlflow.genai.evaluate(data=traces, scorers=[retrieval_hit])

The expectations travel with the traces, so retrieval_hit scored them again without the original dataset. The same pattern applies to production traces: search them, add the useful ones to an evaluation dataset (a Unity Catalog table created with mlflow.genai.datasets.create_dataset, which needs CREATE TABLE on the schema), and evaluate against it after each change.

Built-in judges next to custom scorers

Custom scorers sit in the same scorers list as the built-in LLM judges, so one run can mix deterministic checks with judgment calls. The column that matters for the exam is whether a judge needs ground truth in expectations:

Built-in judgeNeeds ground truth
RelevanceToQuery, RetrievalRelevance, RetrievalGroundedness, Safety, Guidelines, ToolCallEfficiencyNo: they judge the response or context against the request or a stated rule.
Correctness, RetrievalSufficiency, ToolCallCorrectnessYes: they compare against expected facts, responses or tool calls.
ExpectationsGuidelinesNo ground-truth answer, but each row's guidelines go in expectations.

Choose a code-based scorer for exact rules (valid JSON, a schema, a required disclaimer, a length limit, an expected tool) and a judge when quality needs reading the way a person would. By default each judge uses a Databricks-hosted LLM; the model argument swaps it, written as <provider>:/<model-name>, for example databricks:/databricks-gpt-5-mini.

Judge table from the Databricks page "Built-in LLM judges"; mixing judges and scorers from "Scorers and LLM judges".

From evaluation to monitoring

The same scorer can grade live traffic. Two constraints from the docs decide how to write it: production monitoring accepts built-in judges and @scorer-decorated functions, but not class-based Scorer subclasses, and a decorated scorer used in monitoring must be defined and registered from a Databricks notebook, because the monitoring service serializes the function's code to run it remotely. State a monitored scorer needs belongs inside the function body.

What this means for the exam

Exam tip: a deterministic business rule (valid JSON, a banned phrase, a required citation) points to a custom @scorer, not a new LLM judge. "Did retrieval fail or did the model ignore good context?" points to the trace and a scorer that reads the retriever span. Iterating on scorers points to re-evaluating stored traces rather than re-running the agent.

Distractors offer a single score on the final answer, endpoint logs, or a judge that needs ground truth on a dataset that has none.

FAQ

How do I write a custom scorer for mlflow.genai.evaluate?

Write a Python function, decorate it with @scorer from mlflow.genai.scorers, and give it keyword arguments for what it needs: inputs, outputs, expectations or trace. Return a bool, a number, a string or a Feedback with a rationale, and pass the function in the scorers list of mlflow.genai.evaluate.

Can a custom scorer evaluate an agent's intermediate steps?

Yes. A scorer that takes the trace argument can search its spans, for example the retriever span, and grade what that step returned. That shows whether a wrong answer came from bad retrieval or from the model ignoring good context.

Can I re-run evaluation without calling my agent again?

Yes. Pass stored traces, for example from mlflow.search_traces, as the data argument and omit predict_fn. The scorers grade the recorded traces, and expectations stored with them are available to scorers that need ground truth.

Which MLflow built-in judges need ground truth?

Correctness, RetrievalSufficiency and ToolCallCorrectness need ground truth in expectations. RelevanceToQuery, RetrievalRelevance, RetrievalGroundedness, Safety, Guidelines and ToolCallEfficiency do not.

Back to the chapters

Custom scorers and evaluating agents with tracing are Chapters 49 and 50 of the Databricks Generative AI Engineer Associate course, in Unit 6, Evaluation and Monitoring, with exam-style checks on both.

Open Chapter 49 →