Single-Turn, E2E Evals
Learn how to run end-to-end testing for single-turn use cases
Overview
End-to-end testing treats your LLM application as a black box — you provide an input and evaluate the final output. This is ideal for simple pipelines, RAG systems, or when you only care about the end result rather than intermediate steps.
Requirements:
- A dataset of goldens containing golden inputs
- A list of metrics to evaluate with
- Construction of an
LLMTestCaseat runtime (mappinginput→actual_output)
How It Works
- Pull your dataset from Confident AI
- Loop through goldens in your dataset, for each golden:
- Invoke your LLM app using golden inputs to generate test case parameters such as actual output, tools called, and
- Map golden fields to test case parameters
- Add test case back to your dataset
- Run evaluation on test cases, which can happen locally or on Confident AI
Here's a visual representation of where the data-flows throughout the process:
sequenceDiagram
participant Your Code
participant Confident AI
participant LLM App
participant Metrics
Your Code->>Confident AI: Pull dataset
Confident AI-->>Your Code: List of goldens
loop For each golden
Your Code->>LLM App: Invoke with golden.input
LLM App-->>Your Code: actual_output
Your Code->>Your Code: Create test case (input + actual_output)
end
Your Code->>Metrics: Run evaluation locally
Metrics-->>Confident AI: Upload results & generate testing reportsequenceDiagram
participant Your Code
participant Confident AI
participant LLM App
Your Code->>Confident AI: Pull dataset
Confident AI-->>Your Code: List of goldens
loop For each golden
Your Code->>LLM App: Invoke with golden.input
LLM App-->>Your Code: actual_output
Your Code->>Your Code: Create test case (input + actual_output)
end
Your Code->>Confident AI: Send test cases to Evals API
Confident AI->>Confident AI: Run metrics remotely
Confident AI-->>Your Code: Return testing report linkRun E2E Tests Locally
Running evals locally is only possible if you are using the Python deepeval library. If you're working with Typescript or any other language, skip to the remote end-to-end evals section instead.
For this section, we'll be using this mock LLM app, that is a simple RAG pipeline:
See Mock LLM App
from openai import OpenAI
def llm_app(query: str) -> str:
# Retriever for your vector db
def retriever(query: str) -> list[str]:
return ["List", "of", "text", "chunks"]
# Generator that combines retrieved context with user query
def generator(query: str, text_chunks: list[str]) -> str:
return OpenAI().chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": query}
]
).choices[0].message.content
# Calls retriever then generator
return generator(query, retriever(query))Pull dataset
Pull your dataset (and create one if you haven't already):
main.py from deepeval.dataset import EvaluationDataset dataset = EvaluationDataset() dataset.pull(alias="YOUR-DATASET-ALIAS")Loop through goldens to create test cases
A native for-loop calling your LLM app would do for this step:
main.py from deepeval.test_case import LLMTestCase from deepeval.dataset import EvaluationDataset dataset = EvaluationDataset() dataset.pull(alias="YOUR-DATASET-ALIAS") for golden in dataset.goldens: test_case = LLMTestCase( input=golden.input, actual_output=llm_app(input) ) dataset.add_test_case(test_case)Run evaluation using `evaluate()`
The
evaluate()function allows you to create test runs and uploads the data to Confident AI once evaluations have completed locally.main.py from deepeval.metrics import AnswerRelevancyMetric from deepeval import evaluate # Replace with your metrics evaluate(test_cases=dataset.test_cases, metrics=[AnswerRelevancyMetric()])Done ✅. You should see a link to your newly created sharable testing report.
- The
evaluate()function runs your test suite across all test cases and metrics - Each metric is applied to every test case (e.g., 10 test cases × 2 metrics = 20 evaluations)
- A test case passes only if all metrics for it pass
- The test run’s pass rate is the proportion of test cases that pass
Single-Turn Testing Reports - The
The evaluate() function is extremely unopinionated and non-instrusive, which means it is great for teams looking for a lightweight approach for running LLM evaluations. However, it also means that:
- You have to handle a lot of the ETL yourself to map test case fields, even rewriting your LLM app at times to return the correct data
- No visibility - you will still want to be able to debug your LLM app even if it is an end-to-end evaluation
In the next section, we'll show how you can avoid this ETL hellhole and bring LLM traces to end-to-end testing.
LLM Tracing for E2E Evals
LLM tracing solves all problems associated with constructing test cases.
Setup LLM tracing
All you need is to add a few lines of code to your existing LLM app (we'll be using the example from above):
main.py from openai import OpenAI from deepeval.tracing import observe, update_current_trace @observe() def llm_app(query: str) -> str: @observe() def retriever(query: str) -> list[str]: chunks = ["List", "of", "text", "chunks"] update_current_trace(retrieval_context=chunks) return chunks @observe() def generator(query: str, text_chunks: list[str]) -> str: res = OpenAI().chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": query}] ).choices[0].message.content update_current_trace(input=query, output=res) return res return generator(query, retriever(query))The example above shows how we are tracing our LLM app by simply adding a few
@observedecorators:- Each
@observedecorator creates a span, which represents components - A trace on the other hand is created by the top-level
@observedecorator, and is made up of many spans/components - When you run end-to-end testing, you can call the
update_current_tracefunction inside anywhere in your traced app to set test case parameters
Don't worry too much about learning everything you can about LLM tracing for now. We'll go through it in in a dedicated LLM tracing section.
- Each
Pull dataset, and loop through goldens
Pull your dataset in the same way as before, and use the
.evals_iterator()to loop through your goldens. You will use data in your goldens (most likely theinput) to call your LLM app:main.py from deepeval.metrics import AnswerRelevancyMetric from deepeval.dataset import EvaluationDataset dataset = EvaluationDataset() dataset.pull(alias="YOUR-DATASET-ALIAS") for golden in dataset.evals_iterator(metrics=[AnswerRelevancyMetric()]): llm_app(golden.input) # Replace with your LLM appDone ✅. You should see a link to your newly created sharable testing report. This is literally all it takes to run end-to-end evaluations, with the added benefit of a full testing report with tracing included on Confident AI.
Single-Turn Testing Reports (with Tracing)
Run E2E Tests Remotely
Remote end-to-end evals offer no tracibility for debugging but is great because:
- Team members can build metrics without going through code
- Supported through Evals API, for any language
This is possible via Confident AI's Evals API.
Create metric collection
Go to Project > Metric > Collections:
Metric Collection for Remote Evals Pull dataset and construct test cases
Using your language of choice, you would call your LLM app to construct a list of valid
LLMTestCasedata models.main.py from deepeval.dataset import EvaluationDataset from deepeval.test_case import LLMTestCase dataset = EvaluationDataset() dataset.pull(alias="YOUR-DATASET-ALIAS") for golden in dataset.goldens: test_case = LLMTestCase(input=golden.input, actual_output=llm_app(golden.input)) dataset.add_test_case(test_case)index.ts import { EvaluationDataset, LLMTestCase, Golden } from "deepeval"; const dataset = new EvaluationDataset(); await dataset.pull({ alias: "YOUR-DATASET-ALIAS" }); for (const golden of dataset.goldens as Golden[]) { const testCase = new LLMTestCase({ input: golden.input, actualOutput: llmApp(golden.input), }); dataset.addTestCase(testCase); }Call `/v1/evaluate` endpoint
main.py from deepeval import evaluate evaluate(test_case=dataset.test_cases, metric_collection="YOUR-COLLECTION-NAME")index.ts import { evaluate, EvaluationDataset, LLMTestCase } from "deepeval"; const dataset = new EvaluationDataset(); evaluate({ llmTestCases: dataset.testCases as LLMTestCase[], metricCollection: "YOUR-COLLECTION-NAME", });POST/v1/evaluate curl -X POST "https://api.confident-ai.com/v1/evaluate" \ -H "CONFIDENT_API_KEY: <PROJECT-API-KEY>" \ -H "Content-Type: application/json" \ -d '{ "metricCollection": "string", "llmTestCases": [ { "input": "string", "actualOutput": "string", "name": "string", "expectedOutput": "string", "retrievalContext": [ "string" ], "context": [ "string" ], "toolsCalled": [ { "name": "string", "description": "string", "inputParameters": {}, "output": "string", "reasoning": "string" } ], "expectedTools": [ { "name": "string", "description": "string", "inputParameters": {}, "output": "string", "reasoning": "string" } ] } ], "conversationalTestCases": [ { "turns": [ { "role": "user", "content": "string", "userId": "string", "retrievalContext": [ "string" ], "toolsCalled": [ { "name": null, "description": null, "inputParameters": null, "output": null, "reasoning": null } ] } ], "scenario": "string", "name": "string", "expectedOutcome": "string", "userDescription": "string", "chatbotRole": "string" } ], "hyperparameters": {}, "identifier": "string" }'
Advanced Usage
Now you've learnt how to run a single-turn, end-to-end evaluation, here are a few things you should also do.
Log prompts and models
Tell Confident AI the configurations used in your LLM app during the evaluation.
Simply add a free-form key-value pair to the hyperparameters argument in the evaluate() function:
from deepeval.prompt import Prompt
prompt = Prompt(alias="YOUR-PROMPT-ALIAS")
prompt.pull()
evaluate(
hyperparameters={
"Model": "YOUR-MODEL",
"Prompt Version": prompt # An instance of your Prompt
},
test_cases=[...],
metrics=[...]
)Simply add a free-form key-value pair to the hyperparameters argument in the evaluate() function:
evaluate({
hyperparameters: {
Model: "YOUR-MODEL",
"Prompt": prompt,
},
llmTestCases: [...],
metricCollection: "YOUR-COLLECTION-NAME",
});curl -X POST "https://api.confident-ai.com/v1/evaluate" \
-H "CONFIDENT_API_KEY: <PROJECT-API-KEY>" \
-H "Content-Type: application/json" \
-d '{
"metricCollection": "string",
"llmTestCases": [
{
"input": "string",
"actualOutput": "string",
"name": "string",
"expectedOutput": "string",
"retrievalContext": [
"string"
],
"context": [
"string"
],
"toolsCalled": [
{
"name": "string",
"description": "string",
"inputParameters": {},
"output": "string",
"reasoning": "string"
}
],
"expectedTools": [
{
"name": "string",
"description": "string",
"inputParameters": {},
"output": "string",
"reasoning": "string"
}
]
}
],
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "string",
"userId": "string",
"retrievalContext": [
"string"
],
"toolsCalled": [
{
"name": null,
"description": null,
"inputParameters": null,
"output": null,
"reasoning": null
}
]
}
],
"scenario": "string",
"name": "string",
"expectedOutcome": "string",
"userDescription": "string",
"chatbotRole": "string"
}
],
"hyperparameters": {},
"identifier": "string"
}'Add identifer to test runs
The identifer argument allows you to name test runs, which will come in extremely handy when you're trying to run regression tests on them on the platform.
evaluate(
identifer="Any custom string",
test_cases=[...],
metrics=[...]
)evaluate({
identifer: "Any custom string",
llmTestCases: [...],
metricCollection: "YOUR-COLLECTION-NAME",
});curl -X POST "https://api.confident-ai.com/v1/evaluate" \
-H "CONFIDENT_API_KEY: <PROJECT-API-KEY>" \
-H "Content-Type: application/json" \
-d '{
"metricCollection": "string",
"llmTestCases": [
{
"input": "string",
"actualOutput": "string",
"name": "string",
"expectedOutput": "string",
"retrievalContext": [
"string"
],
"context": [
"string"
],
"toolsCalled": [
{
"name": "string",
"description": "string",
"inputParameters": {},
"output": "string",
"reasoning": "string"
}
],
"expectedTools": [
{
"name": "string",
"description": "string",
"inputParameters": {},
"output": "string",
"reasoning": "string"
}
]
}
],
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "string",
"userId": "string",
"retrievalContext": [
"string"
],
"toolsCalled": [
{
"name": null,
"description": null,
"inputParameters": null,
"output": null,
"reasoning": null
}
]
}
],
"scenario": "string",
"name": "string",
"expectedOutcome": "string",
"userDescription": "string",
"chatbotRole": "string"
}
],
"hyperparameters": {},
"identifier": "string"
}'Name test cases
Similar to the identifer, naming test cases allows you to search and match test cases across different test runs during regression testing.
evaluate(
test_cases=[LLMTestCase(name="Any custom string", ...)],
metric_collection="..."
)evaluate({
llmTestCases: [new LLMTestCase({ name: "Any custom string", ... })],
metricCollection: "..."
});curl -X POST "https://api.confident-ai.com/v1/evaluate" \
-H "CONFIDENT_API_KEY: <PROJECT-API-KEY>" \
-H "Content-Type: application/json" \
-d '{
"metricCollection": "string",
"llmTestCases": [
{
"input": "string",
"actualOutput": "string",
"name": "string",
"expectedOutput": "string",
"retrievalContext": [
"string"
],
"context": [
"string"
],
"toolsCalled": [
{
"name": "string",
"description": "string",
"inputParameters": {},
"output": "string",
"reasoning": "string"
}
],
"expectedTools": [
{
"name": "string",
"description": "string",
"inputParameters": {},
"output": "string",
"reasoning": "string"
}
]
}
],
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "string",
"userId": "string",
"retrievalContext": [
"string"
],
"toolsCalled": [
{
"name": null,
"description": null,
"inputParameters": null,
"output": null,
"reasoning": null
}
]
}
],
"scenario": "string",
"name": "string",
"expectedOutcome": "string",
"userDescription": "string",
"chatbotRole": "string"
}
],
"hyperparameters": {},
"identifier": "string"
}'By default, Confident AI will match test cases based on matching inputs, so naming test cases is not strictly required for regression testing.
Scaling beyond prototype?For teams evaluating Confident AI in productionTalk to us