Evaluating RAG Applications
Score a RAG dataset for context relevance, completeness, and factual accuracy with fi.evals, then find the weakest row.
Load a RAG dataset, score each row for context relevance, completeness, and factual accuracy with fi.evals, then pull out the row with the lowest score to see exactly where the pipeline failed.
| Time | Difficulty | Package |
|---|---|---|
| 15 min | Intermediate | ai-evaluation |
- Future AGI account → app.futureagi.com
- API keys:
FI_API_KEYandFI_SECRET_KEY(see Get your API keys) - Python 3.11
Install
pip install ai-evaluation datasets
export FI_API_KEY="your-api-key"
export FI_SECRET_KEY="your-secret-key"
Tutorial
Import the evaluator
fi.evals ships in the ai-evaluation package, not futureagi.
from fi.evals import evaluate
from datasets import load_dataset Load a RAG dataset sample
ragas-wikiqa pairs each question with retrieved context and an answer generated from that context, which is what a RAG eval needs.
dataset = load_dataset("explodinggradients/ragas-wikiqa")
sample_data = dataset["train"]
df = sample_data.to_pandas().head(10)
print(df[["question", "context", "generated_with_rag"]])You should see 10 rows with question, context, and generated_with_rag columns populated.
Score the first row to see what a real result looks like before running the whole sample.
first = df.iloc[0]
result = evaluate(
"context_relevance",
input=first["question"],
context=first["context"],
model="turing_small",
)
print(result.score, result.reason)You should see a score between 0 and 1 and a short text explanation.
Pick the RAG metrics to run
Each metric needs a different combination of context, input, and output. Passing all three to every call, as Step 4 does, covers every metric here.
metrics = ["context_relevance", "completeness", "factual_accuracy"]context_relevance(needscontext,input): are the retrieved chunks relevant to the questioncompleteness(needsinput,output): does the answer address every part of the questionfactual_accuracy(needsinput,output,context): are the claims in the answer correct
Score every row
Scale the single evaluate() call from Step 2 to every metric and every row.
for metric in metrics:
df[metric] = None
for index, row in df.iterrows():
for metric in metrics:
result = evaluate(
metric,
input=row["question"],
output=row["generated_with_rag"],
context=row["context"],
model="turing_small",
)
df.at[index, metric] = result.scoreYou should see each metric column filled with a score between 0 and 1 for all 10 rows.
Aggregate the scores
for metric in metrics:
print(f"Average {metric}: {df[metric].mean():.2f}")Expected output (illustrative, your numbers depend on the sample and model version):
Average context_relevance: 0.81
Average completeness: 0.88
Average factual_accuracy: 0.76factual_accuracy is the weakest of the three here, so that’s the metric worth digging into next.
Find the weakest row
An average hides which row is actually broken. Sort on the metric you care about and inspect the lowest scorer.
worst = df.sort_values("factual_accuracy").iloc[0]
print(f"Question: {worst['question']}")
print(f"Answer: {worst['generated_with_rag']}")
print(f"Score: {worst['factual_accuracy']}")Read the answer against its context column. A low factual_accuracy score with a context that does contain the fact points at the generation step, not retrieval; a low score with missing context points at the retriever.
Rerun the weak rows with a larger model
Change one thing and rerun: re-score the rows below the sample average with turing_large instead of turing_small, and compare the means.
before = df["factual_accuracy"].mean()
weak = df[df["factual_accuracy"] < before]
for index, row in weak.iterrows():
result = evaluate(
"factual_accuracy",
input=row["question"],
output=row["generated_with_rag"],
context=row["context"],
model="turing_large",
)
df.at[index, "factual_accuracy"] = result.score
after = df["factual_accuracy"].mean()
print(f"factual_accuracy: {before:.2f} -> {after:.2f}")Expected output (illustrative, your numbers depend on the sample and model version):
factual_accuracy: 0.76 -> 0.84turing_large catching claims turing_small missed is the usual driver of a delta like this; a rerun that doesn’t move the mean points back at retrieval instead.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
ImportError: cannot import name 'evaluate' from 'fi.evals' | ai-evaluation is not installed; futureagi alone does not ship fi.evals | pip install ai-evaluation |
401 Unauthorized from evaluate(..., model="turing_small") | FI_API_KEY / FI_SECRET_KEY not exported, or exported with a stray space | Re-copy both keys from app.futureagi.com settings and re-export |
KeyError: 'context' when reading a row | The loaded dataset uses a different column name for context | Run df.columns first and match the exact column name |
result.score is None | A cloud metric was called without a model | Pass a valid model name, for example model="turing_small" |
| Looping over the dataframe takes minutes on larger samples | Each evaluate() call is a separate network request to a Turing model | Score a smaller sample while iterating, or parallelize the loop with a thread pool |
context_relevance scores are uniformly low | context holds the full source article instead of the retrieved chunk | Pass only the chunk your retriever actually returned, not the whole document |
For a metric-by-metric breakdown of retrieval failures versus generation failures on a single test case, see RAG Evaluation: Retrieval vs Generation.
Questions & Discussion