Some questions have no fact to assert. “Is this answer actually supported by our overdraft policy?” is not a tracker fact. For free text, the framework sends the answer to a second LLM — the judge — and compares a returned score to a threshold.
The judge holds two jobs in a scenario. It scores your criteria — the
natural-language goals like “the agent asks exactly one clarifying question”
— against the whole transcript, with a written rationale per criterion. And
it powers the two generative assertions, which score a specific answer
against a specific standard. The criteria are where you express judged
intent; the generative assertions are precision tools, and here is the
sentence this chapter exists for: they are not variations of one idea.
They are computed by completely different machinery, they fail differently,
and confusing them is the most common way to get a meaningless eval.
(The machinery, for the record, is the same code the classic e2e tests used —
the scenario engine imports rasa.e2e_test.assertions directly
(rasa/builder/copilot/mcp_server/tools/assertion_engine.py:9), so every
citation below reads from the engine you have installed.)
Groundedness: a ratio of statements
From eval/scenarios/faq_grounded_answer.yml:
assertions:
- generative_response_is_grounded:
threshold: 0.8
ground_truth: >-
A replacement debit card is issued free of charge once per
calendar year. Additional replacements cost $12 each. Standard
delivery takes five to seven business days.
Mechanism: the judge splits the answer into atomic statements and marks each
one supported or unsupported against your ground truth
(rasa/e2e_test/llm_judge_prompts/groundedness_prompt_template.jinja2:7-12).
Then the score is plain arithmetic — no embeddings involved
(rasa/e2e_test/utils/generative_assertions.py:156-166):
score = supported statements / total statements
This is the metric for “did the agent make something up?” — and the
companion’s most valuable scenario turns it inside out:
faq_unknown_topic_refused.yml asks about a policy the source does not
contain, with a ground_truth that describes the refusal. An agent that
fills the gap from general banking knowledge produces fluent, helpful,
unsupported statements — and scores badly against a truth that says “I don’t
have that.”
The denominator insight — the detail that changes how you read every
threshold: the judge chooses how many statements the answer splits into, not
you. A terse two-statement answer can only score 0, 0.5, or 1.0. Against
that answer, threshold: 0.8 does not mean “80% good” — it means “both
statements must be supported”. Read your thresholds as fractions of a small
integer, not as percentages.
Relevance: cosine similarity of invented questions
assertions:
- generative_response_is_relevant:
threshold: 0.7
Completely different mechanism. The judge is shown only the answer — not
the ground truth, not your policy — and invents three questions that answer
would address (answer_relevance_prompt_template.jinja2:1-9; the count is
num_variations = 3 at assertions.py:1595). Those invented questions and
the user’s real question are embedded, and the score is their mean cosine
similarity (generative_assertions.py:123-153).
Sit with the consequence, because it is the most important sentence in this chapter:
Relevance never sees your ground truth. A confidently wrong answer to exactly the right question scores high.
Relevance detects evasion and topic drift — an agent that answers the
question it wished you’d asked. It cannot detect fabrication. Which is why
faq_grounded_answer.yml asserts both metrics on the same turn:
groundedness catches the confident lie, relevance catches the accurate
dodge. Neither is sufficient alone.
(Side effect of the mechanism: relevance needs an embedding model as well
as a judge — default OpenAI text-embedding-3-small,
generative_assertions.py:28-31. Groundedness never embeds anything.)
Pin your judge
The judge — and, separately, the simulator — is configured in
eval/conftest.yml:
simulation:
llm: # drives the user simulator
provider: openai
model: gpt-5.1
evaluation:
llm: # the judge: criteria scores + quality metrics
provider: openai
model: gpt-5.1
The two are independent on purpose, and the asymmetry matters: you can cheapen the simulator without touching what your results mean, but the judge model is an input to your scores. Swap it and yesterday’s 0.85 and today’s 0.79 were produced by different judges — nothing will tell you that unless the model name is pinned in a reviewed file. This file is that file.
The trap: a judge with nothing to score
This one trips everyone, and the error message doesn’t help. Generative
assertions do not look at every bot message. With no utter_source: given,
they consider only messages whose source metadata is one of:
EnterpriseSearchPolicy · ContextualResponseRephraser · IntentlessPolicy
(rasa/e2e_test/utils/generative_assertions.py:34-38.) A plain templated
response is invisible to them. Responses become eligible when they are
generated or passed through the rephraser — and the rephraser only touches
responses that explicitly set rephrase: true
(rasa/core/nlg/contextual_response_rephraser.py:122-124). Otherwise a
response’s source is simply its action name — which you can target, by
naming it in utter_source:.
So: if a generative assertion behaves as though it never ran, check the source of the message you think it is judging. Before the threshold, before the prompt, before blaming the judge.
Thresholds are not portable
A last habit before the honesty chapter: the thresholds in the companion are
illustrative, not engine defaults, and any threshold you tune is tuned
against three things at once — a specific judge model, a specific embedding
model, and a specific set of ground-truth strings. Change any of the three
and the old threshold is a number from a different experiment. (The engine’s
own fallback, for the record, is DEFAULT_THRESHOLD = 0.5,
assertions.py:70 — treat that as a placeholder, not a recommendation.)
Check your understanding
- Your FAQ agent starts answering overdraft questions with confident, fluent, wrong numbers. Which metric catches it, and why does the other one score it high?
- Why does the refusal scenario’s
ground_truthdescribe what the agent should say it cannot do, rather than containing the right answer? - Why does changing your embedding model invalidate relevance thresholds but leave groundedness scores untouched?
You now command the whole framework. The last chapter is about the hardest part: knowing what your green checkmarks are actually worth.
