Giskard: Testing and Evaluating ML Models plus LLM/RAG Apps So Nothing Leaks in Production
Hey friends, it is Ruby Abdullah again. This time I want to introduce you to a tool that I honestly think belongs in every serious ML or LLM builder's toolbox. It is called Giskard. It is an open source library for testing and evaluating ML models as well as LLM/RAG applications. So it is not just about checking accuracy, it is about hunting down the bugs that usually stay invisible in standard metrics. Bugs like bias, hallucination, and robustness issues.
Why am I so excited about this? Because honestly, a lot of us look at a model through a single number: accuracy. The model hits 92 percent, great, ship it. But behind that 92 percent, the model could quietly be discriminating against a specific group, or it could be trivially fooled just by changing one word in the input. Giskard is here to surface exactly those hidden problems automatically. Let us go through it slowly from zero until you can use it yourself.
Introduction
Giskard is essentially a quality assurance platform for AI models. I like to describe it as "unit tests but for models". Just like in regular software engineering where we write tests to make sure functions behave as expected, with Giskard we write (or auto generate) tests to make sure our model does not carry hidden weaknesses.
There are a few main problems Giskard helps you detect. The first is performance bias, which is when the model performs badly on a specific subset of data even though it looks great overall. The second is robustness issues, which is when the model easily flips its prediction just because of a small perturbation in the input, like a typo or a change in capitalization. The third is unrobustness to features that should not matter. The fourth, specific to LLMs, covers hallucination, prompt injection, leakage of sensitive information, and toxic or biased output.
What makes Giskard different from just writing plain asserts is its automatic scan. You simply hand over a model and a dataset, and Giskard runs hundreds of heuristic and machine learning based tests to hunt for vulnerabilities. The result is an interactive report that is easy to read. From there you can immediately generate a test suite that you can rerun every time the model gets updated. This is hugely important for a machine learning CI/CD pipeline.
For context, Giskard supports many types of models. From tabular (classification and regression), NLP, all the way to LLMs. It is framework agnostic, so whether you use scikit-learn, PyTorch, TensorFlow, HuggingFace, or even a custom model wrapped in a plain Python function, it all works. For LLMs and RAG, Giskard has a dedicated module called RAGET which we will cover in the advanced section later.
Okay, before we jump into code, I want to stress one thing. Giskard is not a replacement for your manual evaluation or your domain expertise. It is a tool to speed up and broaden the coverage of your testing. Think of it as a very diligent QA assistant who tries all sorts of scenarios you might have forgotten to check. The final decision still rests with you. Deal? Let us continue.
Installation
Installing Giskard is super easy, just use pip. I recommend creating a virtual environment first so things stay tidy and do not collide with your other project dependencies.
# Create a virtual environment first (optional but strongly recommended)
python -m venv venv
source venv/bin/activate (Linux/Mac)
venv\Scripts\activate (Windows)
Install the base Giskard package
pip install giskard
If you want to use the LLM and RAG evaluation features (RAGET), you need to install with the extra dependency. This is because the LLM module needs some additional libraries.
# pip install "giskard[llm]"
Once installed, you can check the version to make sure everything is in order.
import giskard
print(giskard.version)
For LLM features, Giskard uses LiteLLM by default to call language models. So you need to set the API key for the provider you use. If you use OpenAI for example, you set the environment variable first.
import os
Set the API key for the LLM provider you use
os.environ["OPENAIAPIKEY"] = "sk-..." # replace with your key
Configure the default model Giskard uses for LLM evaluation
import giskard
from giskard.llm.client.openai import OpenAIClient
giskard.llm.setllmmodel("gpt-4o-mini")
giskard.llm.setembeddingmodel("text-embedding-3-small")
Now that your environment is ready, we can start the fun part, which is using Giskard on a real model. I will start with a classic tabular case first so the concept sinks in, then we go up to LLMs.
Basic Usage
In this section we will practice using the legendary Titanic dataset. The goal is simple: predict whether a passenger survived. I deliberately picked this dataset because you are probably already familiar with it, which makes it easy to focus on the Giskard concepts rather than the data domain.
Preparing and Training a Model
First, we prepare the data and train a simple model with scikit-learn. Nothing weird here, this is the usual machine learning flow you already know.
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linearmodel import LogisticRegression
from sklearn.modelselection import traintestsplit
Get the Titanic dataset
url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
df = pd.readcsv(url)
Pick the columns we want to use
featurecols = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked"]
targetcol = "Survived"
df = df[featurecols + [targetcol]].copy()
Split the data
X = df[featurecols]
y = df[targetcol]
Xtrain, Xtest, ytrain, ytest = traintestsplit(
X, y, testsize=0.2, randomstate=42, stratify=y
)
Build preprocessing + model in one pipeline
numericfeatures = ["Age", "SibSp", "Parch", "Fare"]
categoricalfeatures = ["Pclass", "Sex", "Embarked"]
numerictransformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
])
categoricaltransformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="mostfrequent")),
("onehot", OneHotEncoder(handleunknown="ignore")),
])
preprocessor = ColumnTransformer(transformers=[
("num", numerictransformer, numericfeatures),
("cat", categoricaltransformer, categoricalfeatures),
])
model = Pipeline(steps=[
("preprocessor", preprocessor),
("classifier", LogisticRegression(maxiter=1000)),
])
model.fit(Xtrain, ytrain)
print("Test accuracy:", model.score(Xtest, ytest))
At this point you have a working model. If we stopped here, we would only know its accuracy. But we want more than that. We want to know where this model is fragile. This is where Giskard comes in.
Wrapping the Dataset with Giskard
The first step in Giskard is wrapping your dataset with a giskard.Dataset object. This matters because Giskard needs to know which column is the target and which columns are categorical so it can run relevant tests.
import giskard
Combine features and target for the Giskard dataset
rawtest = Xtest.copy()
rawtest[targetcol] = ytest
giskarddataset = giskard.Dataset(
df=rawtest,
target=targetcol, # the column we want to predict
name="Titanic test set",
catcolumns=categoricalfeatures, # categorical columns
)
print(giskarddataset)
Notice that I put the target column into the dataframe, then I tell Giskard the column name through the target parameter. This lets Giskard compare model predictions against ground truth while running the scan.
Wrapping the Model with Giskard
After the dataset, now it is the model's turn. We wrap the model with giskard.Model. The important thing here is that we provide a prediction function, the model type (classification or regression), the feature names, and the list of class labels.
def predictfn(dfinput):
# This function receives a raw dataframe and returns probabilities
return model.predict
proba(dfinput)
giskard
model = giskard.Model(
model=predictfn,
modeltype="classification",
name="Titanic survival classifier",
classificationlabels=[0, 1], # order must match predictproba output
featurenames=featurecols,
)
Quick validation: make sure the wrapper works correctly
print(giskardmodel.predict(giskarddataset).raw[:5])
There are two approaches to wrapping a model. The first uses a prediction function like above, which is the most flexible and the one I use most often since it works for any model. The second lets you pass a scikit-learn model object directly and Giskard auto detects it, but the function approach is more universal.
One important tip from me: make sure the order of classificationlabels matches exactly the column order of your predictproba output. If it is flipped, the scan results will be nonsense and confuse the heck out of you.
Running the Automatic Scan
Now this is my favorite part. With the wrapped model and dataset, we just call one function to run hundreds of automatic tests.
# Run the automatic scan to hunt for vulnerabilities
scanresults = giskard.scan(giskardmodel, giskarddataset)
Show the report (renders interactively in a notebook)
display(scanresults)
Outside a notebook, save it to HTML
scanresults.tohtml("titanicscanreport.html")
This scan checks many things at once. It checks performance bias across various data slices, for example whether the model performs worse for female passengers or third class passengers. It checks robustness by adding perturbations to the input. It checks overconfidence and underconfidence, meaning cases where the model is too sure while being wrong, or hesitant while the answer should be obvious. And much more.
The resulting report is interactive and tells you exactly where the problems are, how severe they are, plus sample data. Honestly the first time I ran this on an old production model, I was shocked because there was a data slice whose performance dropped drastically while the global metric looked perfectly healthy.
Generating a Test Suite from Scan Results
After the scan finds problems, Giskard can immediately turn those findings into a test suite that you can rerun. This is amazing for regression testing.
# Turn the scan results into an automatic test suite
testsuite = scanresults.generatetestsuite("Titanic test suite")
Run the test suite
suiteresults = testsuite.run()
print(suiteresults)
This test suite contains a collection of concrete tests with certain thresholds. For example "accuracy for the female passenger slice must be above 0.7" or "prediction must stay stable against capitalization changes in the Sex column". Every time you retrain the model, you just run this suite again to make sure there is no regression. This is what I meant earlier about integrating into CI/CD.
Advanced Usage
Okay now we level up. I split this section into two. First is about writing custom tests and tuning the scan for tabular data, second is about evaluating LLMs and RAG with RAGET. The LLM part is the one my friends have been asking me about the most lately, so I will go into a bit more detail.
Adding Custom Tests to a Test Suite
Sometimes Giskard's built in tests are not enough for your specific business needs. Luckily you can add your own custom tests. Giskard has plenty of ready to use tests in the giskard.testing module, and you can also write your own test functions.
from giskard import testing, Suite
Build a manual suite with Giskard's built in tests
customsuite = Suite(name="Custom Titanic Suite")
Test 1: minimum overall accuracy
customsuite.addtest(
testing.testaccuracy(
model=giskardmodel,
dataset=giskarddataset,
threshold=0.75,
)
)
Test 2: minimum F1 score
customsuite.addtest(
testing.testf1(
model=giskardmodel,
dataset=giskarddataset,
threshold=0.70,
)
)
results = customsuite.run()
print("All tests passed:", results.passed)
If you need truly custom test logic, you can use the @test decorator.
from giskard import test, TestResult
@test(name="Predictions must not all be identical")
def testpredictionvariety(model, dataset):
preds = model.predict(dataset).prediction
uniquepreds = set(preds)
passed = len(uniquepreds) > 1
return TestResult(
passed=passed,
metric=len(uniquepreds),
)
Add it to the suite
customsuite.addtest(
testpredictionvariety(model=giskardmodel, dataset=giskarddataset)
)
print(customsuite.run().passed)
Custom tests like this are super useful for encoding business rules that only you know. For example in a fraud detection case, you might want to make sure the model never assigns a very low risk score to a transaction above a certain amount. A rule like that will never be caught by a generic scan, so you have to write it yourself.
Evaluating LLMs and RAG with RAGET
Now we get into the LLM part. Say you build a RAG (Retrieval Augmented Generation) application, for example a chatbot that answers questions based on internal company documents. How do you know this chatbot is actually good? Testing it manually one by one is exhausting and does not scale. This is where Giskard's RAGET (RAG Evaluation Toolkit) shines.
RAGET has two main capabilities. First, it can automatically generate a test set from your knowledge base. It reads your documents, then builds questions along with their reference answers. Second, it can evaluate the answers of your RAG application and give scores per component, so you know which part of the RAG pipeline is failing.
First, we generate a test set from the knowledge base.
import pandas as pd
from giskard.rag import KnowledgeBase, generatetestset
Assume we have documents in the form of a dataframe
Each row is one chunk of text
documents = pd.DataFrame({
"text": [
"Giskard is an open source library for testing ML and LLM models.",
"RAGET is Giskard's module for evaluating RAG applications.",
"Giskard's automatic scan detects bias, robustness, and hallucination.",
"Giskard supports models from scikit-learn, PyTorch, and HuggingFace.",
# ... ideally hundreds to thousands of chunks from your real documents
]
})
Build the knowledge base
knowledgebase = KnowledgeBase(documents)
Generate a test set automatically
testset = generatetestset(
knowledgebase,
numquestions=30, # number of questions to generate
agentdescription="A chatbot that answers questions about Giskard",
)
Save the test set so it can be reused
testset.save("giskardragettestset.jsonl")
Look at sample generated questions
for sample in testset.topandas().head(3).itertuples():
print(sample)
The cool thing about RAGET is that it generates various question types. There are simple questions, complex questions that need reasoning, questions whose answers are spread across multiple documents, situational questions, and more. This matters because your RAG application must withstand testing from various angles, not just easy questions.
Once you have the test set, we evaluate our RAG application. We need to give RAGET a function that takes a question and returns an answer from our RAG application.
from giskard.rag import evaluate
def raganswerfn(question, history=None):
# Here you call your actual RAG application.
# Simple example: retrieval + generation.
# Replace this part with your RAG pipeline.
retrieved = retrieverelevantdocs(question) # your retrieval function
answer = generateanswer(question, retrieved) # your generation function
return answer
Run the evaluation using the test set from earlier
report = evaluate(
raganswerfn,
testset=testset,
knowledgebase=knowledgebase,
)
Show the report (interactive in a notebook)
display(report)
Save to HTML
report.tohtml("ragetreport.html")
Look at the overall score
print(report.correctness)
The RAGET report is truly informative. It gives scores per RAG component, namely the Generator (the LLM that produces answers), the Retriever (which fetches relevant documents), the Rewriter (if any), the Router, and the Knowledge Base itself. So if your score is bad, you can tell whether the problem is in retrieval fetching the wrong documents, or in the generator hallucinating even though the documents were correct. This is far more useful diagnostics than just a single accuracy number.
RAGET also classifies questions by topic and type, so you can see which topics your RAG application is weak at. For example it turns out your chatbot is great at product questions but weak at refund policy questions. Insight like this is pure gold for prioritizing improvements.
Running an LLM Scan Directly
Besides RAGET which focuses on RAG, Giskard also has a scan for LLMs in general. It is conceptually similar to the tabular scan earlier, but the tests are specific to LLM problems like prompt injection, hallucination, harmful output, and leakage of sensitive information.
import giskard
import pandas as pd
def llmpredict(dfinput):
# Function that calls your LLM for each input row
outputs = []
for question in dfinput["question"]:
answer = callyourllm(question) # replace with your LLM call
outputs.append(answer)
return outputs
Wrap as a Giskard model of type textgeneration
llmmodel = giskard.Model(
model=llmpredict,
modeltype="textgeneration",
name="Customer support assistant",
description="An assistant that answers customer questions about our product",
featurenames=["question"],
)
Sample dataset with a few questions
llmdataset = giskard.Dataset(pd.DataFrame({
"question": [
"How do I reset my password?",
"What is your refund policy?",
]
}))
Run the LLM scan
llmscan = giskard.scan(llmmodel, llmdataset)
llmscan.tohtml("llmscanreport.html")
Something important to remember in the LLM scan: the description parameter is not just documentation. Giskard uses that description to understand the intent and constraints of your application, then generates relevant tests. So write a clear and specific description. If the description is sloppy, the generated tests become less sharp too.
Best Practices
After using Giskard for quite a while across various projects, I have a few principles I want to share with you so you do not fall into the same holes I did.
First, do not rely on global metrics alone. This is the core message of this whole article. A 92 percent accuracy means nothing if that 8 percent of errors is concentrated in one group that matters, whether for business or ethical reasons. Always run the scan to see performance across various data slices. Giskard makes this easy, so there is no excuse not to do it.
Second, make the test suite part of your pipeline. Do not just run the scan once during development and forget it. Generate a test suite from the scan results, save it, and rerun it every time the model is retrained or the data changes. I usually put this in a CI step, so if a previously fixed weakness regresses, the build fails immediately and I get notified. This has saved me from many production incidents.
Third, write clear model descriptions especially for LLMs. Like I said, the quality of generated tests depends heavily on how well you describe what your model does and what its constraints are. Take the time to write a proper description. Imagine you are explaining it to a new QA engineer who knows nothing about your product yet.
Fourth, for RAG, evaluate per component, not just end to end. RAGET's strength is that it can pinpoint which component is failing. Take advantage of that. If the final score is bad, do not rush to swap the LLM. Check first, the problem might be in the retriever fetching the wrong documents. Often a fix in retrieval is far cheaper and more effective than swapping LLMs back and forth.
Fifth, start with a small test set then scale up. For RAGET, generating a test set from a knowledge base requires costly LLM calls. Start with a small number of questions, say 20 to 30, to validate that your setup works. Once you are confident, scale up to hundreds for a more thorough evaluation. This saves cost and time.
Sixth, review generated tests manually. Giskard is smart, but that does not mean it is perfect. Occasionally the generated test set can have odd questions or slightly incorrect reference answers. Take the time to read a sample of the test set, especially early on. Your domain expertise is still irreplaceable. Drop tests that do not make sense and fix the ones that need fixing.
Seventh, watch your API costs when using LLM features. LLM scans and RAGET test set generation send a lot of requests to your LLM provider. If you use an expensive model like GPT-4 for all of this, the bill can balloon. I usually use a cheaper model like gpt-4o-mini for routine evaluation, and reserve big models for cases that truly need high precision.
Eighth, document your findings. Every time a scan finds a vulnerability, note what the finding is and what your decision was. Sometimes you decide a "weakness" is actually acceptable for your business context. Document the reasoning, so your team does not get confused later and so you yourself do not forget why you made that decision.
Conclusion
Alright friends, we have gone pretty far from installation to a fairly deep RAG evaluation. I hope you now have a full picture of why Giskard is so valuable for anyone serious about bringing AI models to production.
The big point I want you to take home: a model that looks good on paper is not necessarily good in the real world. Global metrics can deceive. Giskard gives you a systematic way to uncover hidden weaknesses, whether that is bias, robustness issues, or hallucination in LLM applications. And more importantly, it makes this testing process repeatable and automatable, so you do not just test once and forget.
For your next step, I recommend you practice right away. Take one model you already have, wrap it with Giskard, and run the scan. I am almost certain you will find at least one thing that makes you think "wow, good thing I caught that now". Once you are comfortable with tabular, try moving up to LLM evaluation and RAGET if you are working on an LLM based application.
Remember, our goal is not to build a model that looks perfect in a demo, but to build a model that can truly be trusted when real people use it. Serious testing is an inseparable part of that. Giskard is just a tool, but the right tool can change the way you work into something far calmer and more professional.
That is it from me for now. If you try it and find an interesting insight, I would really love to hear the story. Happy hacking, and see you in the next tutorial. Keep up the learning, friends!