RAG and Agent Evaluation
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Written on 2026-10-01 against RAGAS 0.4.3, DeepEval 4.2.7, agentevals 0.0.9 with LangSmith SDK 0.14.2, LangChain 1.4.3 on
Python 3.12, and Spring AI 2.0.1 on Spring Boot 4.1.x with Java 21 (see the
version baseline). Every snippet on this page was written from the official
documentation of the tool it uses and was not run: each metric calls a judge model, and no judge endpoint was available
when this page was written. Where the documentation did not state a detail (for example the exact placement of metrics in
DeepEval’s tracing loop), the prose says so instead of guessing. No benchmark scores and no vendor prices are quoted; cost is
expressed as a formula over tokens.
A RAG pipeline and an agent have the same evaluation problem: the answer is the output of several steps, and a bad answer tells you almost nothing about which step broke. This page is about decomposing the system so that a failing score points at a component. The vocabulary of retrieval metrics (recall at , MRR, nDCG) and the first RAGAS example are on Evaluating RAG Systems; synthetic test sets, UMBRELA and nugget metrics, CI gates and champion/challenger tests for RAG are on RAG Systems in Production: Evaluation at Scale; and the first agent harness, trajectory modes and rubric judges are on Agent Evaluation. This page does not repeat them. It adds the reference-free view (scoring without a written ideal answer, which is what makes sampled online evaluation possible), a failure-attribution method, the three levels of agent evaluation, reliability across repeated runs, and cost per task. The metric theory underneath is in Evaluation Methodology and the judge mechanics in LLM-as-a-Judge. Chapters on evaluating retrieval and generation separately in Mendelevitch and Bao’s Hands-On RAG for Production, and on agent evaluation in Albada’s Building Applications with AI Agents, inform the decomposition below; the text, formulas and code are original paraphrase (see the bibliography).
Why decompose
Consider a DocsAssistant answer that is wrong. At least four different faults produce it:
-
the retriever did not return the right page (a recall fault);
-
it returned the page among noise, and the model followed the noise (a precision or robustness fault);
-
the page was there and the model ignored or contradicted it (a faithfulness fault);
-
the model answered a different question than the one asked (a relevancy fault).
A single end-to-end "correct / incorrect" score cannot tell these apart, and each has a different remedy: re-chunk, rerank, change the prompt, or change the model. So an evaluation suite for RAG scores each stage and the answer separately, and an evaluation suite for an agent scores the outcome, each step and the route. The cost is a larger suite; the benefit is that a regression caught by the CI gate in Evaluation in CI names its own cause.
Reference-free RAG metrics
A reference-based metric needs a written ideal answer for every question, which is expensive and goes stale as the documentation changes (see Building Eval Sets). A reference-free metric needs only the three things any production trace already contains: the question, the retrieved passages and the generated answer. That is why reference-free metrics are the ones used on sampled live traffic in Online Evaluation and Feedback. Four of them cover the whole pipeline:
| Metric | Inputs it uses | What a low score means |
|---|---|---|
Faithfulness (groundedness) |
answer, retrieved passages |
Claims in the answer are not supported by what was retrieved: the model added or contradicted content. |
Answer relevancy |
question, answer |
The answer is off topic, evasive or incomplete, whether or not it is grounded. |
Context relevancy (or context precision without reference) |
question, retrieved passages (and, for rank-aware variants, the answer) |
The retriever returned passages that do not help: noise, or the right page ranked too low. |
Task-specific rubric |
whatever the rubric names |
The product-specific property you care about, for example "cites a page path that exists". |
The first three together are often called the RAG triad: relevance of the context to the question, groundedness of the answer in the context, and relevance of the answer to the question. If all three are high the answer is on topic, supported, and built on useful evidence; a low value points at one edge of the triangle.
Faithfulness as a formula
Faithfulness-style metrics decompose the answer into atomic claims and ask a judge whether each is supported by the retrieved context. With the set of claims extracted from the answer and the supported ones:
This is the definition given in the RAGAS paper (Es et al.) and the RAGAS documentation. Two properties matter in practice. Faithfulness says nothing about whether the answer is correct: an answer faithfully repeating an outdated page scores 1. And it is only as good as the judge’s claim extraction, so calibrate the judge against human labels on a sample before trusting a threshold (the calibration method, Cohen’s , is on the judge page).
Context precision without a reference
Rank-aware context precision averages precision at each position that holds a useful chunk. With the judged usefulness of the chunk at rank among retrieved chunks:
The reference-free variant in RAGAS (LLMContextPrecisionWithoutReference) lets the judge decide usefulness by comparing each
chunk with the generated response, instead of with a reference answer. That makes it cheap to run on live traffic, with one
caveat: if the response is wrong, a chunk that would have helped can be marked useless, so read this metric together with
faithfulness rather than alone.
Scoring a DocsAssistant answer with RAGAS
The snippet scores one logged interaction with two reference-free RAGAS metrics. It follows the metric classes and the
SingleTurnSample fields of the RAGAS documentation; as noted in the example on
Evaluating RAG Systems, RAGAS 0.4 is moving metrics to
ragas.metrics.collections, so check the deprecation warnings of your installed version.
import asyncio
import os
from openai import AsyncOpenAI
from ragas import SingleTurnSample
from ragas.llms import llm_factory
from ragas.metrics import Faithfulness, LLMContextPrecisionWithoutReference
judge = llm_factory(
os.environ["JUDGE_MODEL"],
provider="openai",
client=AsyncOpenAI(base_url=os.environ["JUDGE_BASE_URL"], api_key=os.environ["JUDGE_API_KEY"]),
)
faithfulness = Faithfulness(llm=judge)
context_precision = LLMContextPrecisionWithoutReference(llm=judge)
async def score_trace(question: str, answer: str, passages: list[str]) -> dict[str, float]:
sample = SingleTurnSample(user_input=question, response=answer, retrieved_contexts=passages)
f, p = await asyncio.gather(
faithfulness.single_turn_ascore(sample),
context_precision.single_turn_ascore(sample),
)
return {"faithfulness": f, "context_precision": p}
Source: RAGAS — Context Precision and
RAGAS — Faithfulness. The documentation
shows single_turn_ascore on metric instances built with an evaluator LLM; whether Faithfulness imported from
ragas.metrics and the one from ragas.metrics.collections share that call signature in 0.4.3 was not verified here, which is
why the earlier page uses the collections form with ascore. Pick one form and pin the RAGAS version in the lock file.
The same triad in DeepEval
DeepEval states that ContextualRelevancyMetric does not need expected_output: it grades the retrieval context against the
input alone. FaithfulnessMetric compares the actual output with the retrieval context, and AnswerRelevancyMetric compares
the actual output with the input. All three therefore work on a trace with no reference.
from deepeval import evaluate
from deepeval.metrics import AnswerRelevancyMetric, ContextualRelevancyMetric, FaithfulnessMetric
from deepeval.test_case import LLMTestCase
def triad_metrics() -> list:
return [
ContextualRelevancyMetric(threshold=0.7, include_reason=True),
FaithfulnessMetric(threshold=0.8, include_reason=True),
AnswerRelevancyMetric(threshold=0.7, include_reason=True),
]
def score_interactions(interactions: list[dict]):
cases = [
LLMTestCase(
input=i["question"],
actual_output=i["answer"],
retrieval_context=i["passages"],
)
for i in interactions
]
return evaluate(test_cases=cases, metrics=triad_metrics())
Source: DeepEval — Contextual Relevancy and
DeepEval — Faithfulness. The thresholds are placeholders: derive real ones
from the score distribution of a known-good build, as described on
Evaluation in CI. include_reason=True makes the judge write why a score was low, which
is what you read when triaging a failure.
Attributing a failure to a stage
With the triad scores in hand, a short decision table turns a failing trace into an owner:
| Context relevancy | Faithfulness | Likely cause and first action |
|---|---|---|
low |
any |
Retrieval fault. Inspect the query rewrite, the chunking and the reranker; check whether the page exists in the index at all (see Ingestion Pipelines and Freshness). |
high |
low |
Generation fault. The right evidence was present and the model did not stay inside it; tighten the prompt, lower the temperature, or add the grounding checks from Hallucination Detection and Correction. |
high |
high, but answer relevancy low |
The model answered a different question; check the query handling and the conversation context (see Conversational RAG and Memory). |
high |
high, answer relevancy high, human says wrong |
The corpus is wrong or outdated, or the judge is miscalibrated. Check the source page before touching the pipeline. |
The last row is the one teams skip: reference-free metrics measure consistency between question, evidence and answer, not truth. Keep a small reference-based golden set (see Building Eval Sets) alongside the reference-free scores so that "consistently wrong" remains detectable.
Agents: three levels of evaluation
An agent adds a loop of decisions on top of a pipeline. LangSmith’s agent-evaluation tutorial names three complementary levels, which are a useful common vocabulary:
| Level | What it scores | Strength and weakness |
|---|---|---|
Final response |
Whether the last message is correct and complete, judged against a reference or a rubric. |
Maximum signal about what the user sees; cannot say where a multi-step run went wrong. |
Single step |
One component in isolation: the first model decision given a fixed state, a classifier, a tool-selection prompt. |
Cheap, deterministic enough to debug and iterate on; misses interaction effects between steps. |
Trajectory |
The sequence of tool calls (and optionally of graph nodes) against a reference route or a rubric. |
Gives partial credit and catches right-answer-wrong-route runs; needs a reference route and can penalise valid alternatives. |
Source: LangSmith — Evaluate a complex agent. Use all three: the
final-response score is the product metric, single-step tests are the fast development loop, and trajectory scores explain
regressions. The DocsAssistant agent (the one from AI Agents) has three tools, docs_search,
fetch_page and github_open_issue, and the cases below use them.
Final response
The final answer of a RAG-backed agent can be scored exactly as in the previous section, using the retrieved passages the
agent saw across all its search calls as the context. Concatenate the tool outputs of the run, deduplicate them, and pass them
as retrieval_context. For answers that must match a reference, use a rubric judge from
LLM-as-a-Judge.
Single step
A single-step test freezes the state and checks one decision. It is an ordinary pytest test, needs no judge, and runs in seconds:
import pytest
from langchain_core.messages import HumanMessage
from docsassistant.agent import build_model_with_tools # returns a chat model bound to the agent's tools
CASES = [
("What does ef_search control in HNSW?", "docs_search"),
("The memory page is wrong, please report it.", "github_open_issue"),
]
@pytest.mark.parametrize("question,expected_tool", CASES)
def test_first_tool_choice(question, expected_tool):
reply = build_model_with_tools().invoke([HumanMessage(question)])
names = [call["name"] for call in reply.tool_calls]
assert expected_tool in names
def test_no_tool_for_small_talk():
reply = build_model_with_tools().invoke([HumanMessage("Thanks, that was helpful.")])
assert reply.tool_calls == []
Source: LangChain — Models (tool calling on chat models). A model is not deterministic even at temperature 0 on every provider, so run each case several times and assert on a rate, as in the reliability section below.
Trajectory
Trajectory scoring compares the route with a reference. The match modes (strict, unordered, subset, superset) and a worked
superset example are on Agent Evaluation; here is the other half, an LLM judge for
trajectories that have no single correct route, and a per-tool argument override for the match evaluator. The agentevals README
documents both calls.
from agentevals.trajectory.llm import TRAJECTORY_ACCURACY_PROMPT, create_trajectory_llm_as_judge
from agentevals.trajectory.match import create_trajectory_match_evaluator
# Match the tools in order, but compare the search query case-insensitively.
match = create_trajectory_match_evaluator(
trajectory_match_mode="strict",
tool_args_match_mode="exact",
tool_args_match_overrides={
"docs_search": lambda got, want: got["query"].lower() == want["query"].lower(),
},
)
# For open-ended runs, a judge grades the route against a generic accuracy rubric.
judge = create_trajectory_llm_as_judge(
prompt=TRAJECTORY_ACCURACY_PROMPT,
model="openai:o3-mini",
)
def grade(outputs: list[dict], reference: list[dict] | None = None) -> dict:
result = {"judge": judge(outputs=outputs)}
if reference is not None:
result["match"] = match(outputs=outputs, reference_outputs=reference)
return result
Source: agentevals. outputs is the message list of the run in the OpenAI message
format (role, content, tool_calls); LangChain message objects are also accepted according to the README’s earlier
examples, but only the dictionary form is shown above because it is the one the README documents for these two functions. The
model string openai:o3-mini is the README’s example; substitute your own judge using the same provider:model convention.
For LangGraph agents the package also has graph-trajectory evaluators (create_graph_trajectory_llm_as_judge,
graph_trajectory_strict_match) that compare node steps rather than tool calls.
Tool-call accuracy
The set-based tool metrics (recall, precision, forbidden-call rate) are defined on Agent Evaluation. They ignore arguments and order, which is often what matters: a call to the right tool with the wrong page path is a failure. RAGAS and DeepEval both define a more complete score. In RAGAS the agent metrics are Tool Call Accuracy, Tool Call F1, Agent Goal Accuracy and Topic Adherence; the F1 variant combines precision and recall of the calls:
where a call matches when the tool name and the (possibly rule-compared) arguments agree. DeepEval’s ToolCorrectnessMetric
compares tools_called with expected_tools and, according to its documentation, can additionally require matching input
parameters or outputs (evaluation_params), a specific order (should_consider_ordering), or an exact list
(should_exact_match).
from deepeval import evaluate
from deepeval.metrics import ToolCorrectnessMetric
from deepeval.test_case import LLMTestCase, ToolCall
case = LLMTestCase(
input="The memory page is wrong, please report it.",
actual_output="I opened an issue for the memory page.",
tools_called=[ToolCall(name="docs_search"), ToolCall(name="github_open_issue")],
expected_tools=[ToolCall(name="docs_search"), ToolCall(name="github_open_issue")],
)
metric = ToolCorrectnessMetric(threshold=0.9, should_consider_ordering=True)
evaluate(test_cases=[case], metrics=[metric])
Source: DeepEval — Tool Correctness. Build tools_called from the run’s
messages (the tool_calls of each assistant message). Tool descriptions are prompts, so the same suite is the regression test
for any edit to a tool’s name, description or schema; see Tool Design. For a safety-relevant
tool such as github_open_issue, additionally assert that it is never called when the question did not ask for it; that is a
hard zero-tolerance check, not a score.
Task completion without a reference
For goal-level success there is often no reference route. DeepEval’s TaskCompletionMetric is described as referenceless: it
needs neither expected_output nor tools_called, infers the task and the outcome from the agent’s trace, and therefore
requires the agent function to be traced with DeepEval’s @observe decorator and the metric to be attached to the evaluation
loop. A standalone LLMTestCase does not work with it.
from deepeval.dataset import EvaluationDataset, Golden
from deepeval.metrics import TaskCompletionMetric
from deepeval.tracing import observe
from docsassistant.agent import run_agent # your agent entry point
@observe()
def docs_assistant(question: str) -> str:
return run_agent(question)
dataset = EvaluationDataset(goldens=[
Golden(input="Which page explains HNSW's ef_search parameter?"),
Golden(input="The memory page is wrong, please report it."),
])
metric = TaskCompletionMetric(threshold=0.7)
for golden in dataset.evals_iterator(metrics=[metric]):
docs_assistant(golden.input)
Source: DeepEval — Task Completion. The documentation names @observe()
and evals_iterator() as the integration points; the exact keyword for passing metrics to the iterator can differ between
DeepEval releases, so confirm it against the page for your pinned version. A referenceless judge on a trace is a useful smoke
signal on live traffic, but it inherits every judge bias described on the judge page, so keep the reference-based trajectory
and tool checks for the CI gate.
Reliability: the same task, repeated
Agents are stochastic, and one passing run is weak evidence. Two numbers describe repeated runs of a task. With independent runs of which succeed, an unbiased estimate of the chance that at least one of attempts succeeds is the same pass@k estimator used for code (Chen et al.):
which is the right metric when a retry is cheap and a human or verifier picks the winner. For an agent that must be right every time a user asks, the relevant quantity is the opposite, the chance that all attempts succeed. With a per-run success probability estimated as and treating runs as independent:
(the exact unbiased estimator replaces the power with ). An agent that succeeds 90% of the time per run succeeds on five consecutive runs only about 59% of the time, which is why a single green run misleads. The distinction between "any of k" and "all of k" follows the reliability argument of the tau-bench paper; use pass@k for tasks with a verifier and pass^k for tasks that act on the user’s behalf.
from math import comb
def pass_at_k(n: int, c: int, k: int) -> float:
"""Probability at least one of k draws (without replacement) from n runs is a success."""
if n - c < k:
return 1.0
return 1.0 - comb(n - c, k) / comb(n, k)
def pass_pow_k(n: int, c: int, k: int) -> float:
"""Probability all k draws (without replacement) from n runs are successes."""
return comb(c, k) / comb(n, k) if c >= k else 0.0
def reliability(results: dict[str, list[bool]], k: int = 3) -> dict[str, tuple[float, float]]:
"""results maps a case id to the pass/fail of each of its n repeated runs."""
return {
case_id: (pass_at_k(len(runs), sum(runs), k), pass_pow_k(len(runs), sum(runs), k))
for case_id, runs in results.items()
}
Source: Chen et al., Evaluating Large Language Models Trained on Code for the pass@k estimator;
the combinatorial pass_pow_k is the direct counterpart written for this page and is not taken from a library. In CI, gate on
the lower of the two for actions with side effects, and on repeated-run means for the rest (see
Evaluation in CI for repeat runs and flakiness budgets).
Cost per task
An agent loop multiplies the token bill: every step re-sends the growing conversation. A task that needs model calls, where call has input and output tokens, costs
where and are the per-token prices of the model you read from your own contract or the provider’s pricing page (this page quotes none), and are the costs of non-model tool calls such as paid search APIs. Because input tokens accumulate across steps, grows faster than linearly with unless the context is trimmed or prompt caching applies; the full cost model, caching and routing are on Cost Management.
Evaluating cost means reporting it next to quality, per case, so that a change that improves accuracy by buying five extra steps is visible as such. Two derived numbers are worth a column each in the eval report: tokens per task, which is price-independent and survives a vendor price change, and cost per successful task, which divides total spend by the number of successes and so charges failures to the successes:
The model calls and their token counts are in the trace (see Observability for Agents and, for the attribute names, Observability with OpenTelemetry), so the eval harness can sum them without a second instrumentation path.
A cost and step budget as a JUnit test
Spring AI emits a token-usage meter from its chat model observations. Its documentation lists the base name
gen_ai.client.token.usage (exported to Prometheus as gen_ai_client_token_usage_total), a counter with a
gen_ai.token.type tag whose values are input, output and total. The test below runs the agent on one case with a
SimpleMeterRegistry bound to the observation registry and fails if the task used more tokens than its budget. The wiring of the
registry to the observations follows Spring Boot’s test support; the snippet shows the assertion, and the exact auto-configuration
annotations were not verified against Spring AI 2.0.1.
import static org.assertj.core.api.Assertions.assertThat;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Statistic;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DocsAssistantBudgetTest {
private static final double MAX_TOTAL_TOKENS_PER_TASK = 20_000;
@Autowired DocsAssistantAgent agent; // the application's agent bean
@Autowired MeterRegistry registry; // the registry that Spring AI observations report to
@Test
void simpleLookupStaysInsideTheTokenBudget() {
double before = totalTokens();
agent.answer("Which page explains HNSW's ef_search parameter?");
double used = totalTokens() - before;
assertThat(used).isLessThanOrEqualTo(MAX_TOTAL_TOKENS_PER_TASK);
}
private double totalTokens() {
return registry.find("gen_ai.client.token.usage")
.tag("gen_ai.token.type", "total")
.meters().stream()
.mapToDouble(DocsAssistantBudgetTest::value)
.sum();
}
private static double value(Meter meter) {
for (var measurement : meter.measure()) {
if (measurement.getStatistic() == Statistic.COUNT || measurement.getStatistic() == Statistic.TOTAL) {
return measurement.getValue();
}
}
return 0.0;
}
}
Source: Spring AI — Observability for the meter name and
tag, and JUnit User Guide. Set the budget from the 95th percentile of a known-good
build plus a margin, not from a guess, and keep one such test per task class (lookup, multi-page synthesis, issue filing). The
Spring AI evaluators RelevancyEvaluator and FactCheckingEvaluator for the quality side of the same test are on
Spring AI Evaluation.
Putting the suite together for the DocsAssistant
A practical layout, from fastest and cheapest to slowest:
-
Unit and single-step tests on every commit: tool selection, argument construction, refusal cases. No judge.
-
Trajectory and tool-call checks on every pull request: reference routes for a small set of canonical tasks, the forbidden-call assertions, and step and token budgets.
-
Reference-free RAG triad and task completion on the golden set on every pull request that touches a prompt, a model, the chunking or the index, with repeated runs for the stochastic cases (this is the gate built in Evaluation in CI).
-
Sampled reference-free scoring of live traffic, feeding dashboards and the flywheel into the golden set (see Online Evaluation and Feedback).
-
Periodic full runs against the pinned and the latest provider model to catch silent vendor changes (see Drift and Regression Monitoring).
Each layer reports quality, cost per task and latency in one table, so a change is accepted only when it does not trade one of them away silently. Security-focused testing of agents (prompt-injection and tool-abuse red teaming) belongs to AI Red Teaming in the AI Security & Responsible AI sub-section and is not covered here. The evaluation sections of the framework pages, such as LangChain Evaluation, show the same ideas inside one framework.