Guide · AI and GenAI
AI Bias Testing: Fairness Checks for LLM Features
23 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- AI bias testing means running the same tasks through your LLM feature for different groups of users, then comparing results group by group. An LLM feature is a product feature built on a large language model, an AI model that reads and writes text.
- Slice your test set, which means splitting it into groups of test cases that share a tag such as language, region or gender. Report every slice and the overlaps between slices, where gaps can stack.
- Build counterfactual pairs: two inputs that are identical except for one detail, such as gender or language. If the output changes, that one detail moved it.
- Compare each slice's outcome rate with the best slice's rate. The four-fifths rule from US hiring guidelines flags any group below 80 percent of the best rate; treat it as an alarm, not a verdict.
- Beginners can stop after the section on measuring bias. Engineers get our simulation of that 80 percent alarm: on two 30-case slices with no real gap, it fired 47 percent of the time. Then comes a release gate built to cut those false alarms, with its own false-block rate measured.
What goes wrong when you test an AI feature only on average?
The average can look healthy while one group of users gets much worse results, so the gap ships unnoticed. AI bias testing exists to find that gap before your users do.
Picture a common setup. A job-matching app serves blue-collar workers in Bihar: plumbers, electricians, drivers, masons and cooks. Each worker records a short profile by voice or text. The setup is made up, but the problem is real.
The team adds an LLM feature, built on a large language model that reads and writes text. It reads each worker's profile, writes a clean summary and ranks workers for each job. The written instructions sent to the model with each profile are called the prompt.
At launch the numbers look good. Employers accept 58 percent of shortlists, up from 44 percent with the old keyword search (made-up numbers). Then complaints start arriving.
- A woman with eight years of plumbing work keeps getting suggested for housekeeping jobs, never for plumbing jobs.
- Workers who record their profiles in Bhojpuri get summaries half as long as Hindi speakers with the same experience.
- An electrician from Purnia, a district in north-east Bihar, rarely reaches the top 10, even for jobs listing his exact skills.
The team opens the launch test report. It had 500 test profiles and one number: 91 percent of summaries rated good. Almost every test profile was in Hindi or English, from a man in Patna.
So the test said nothing about the workers who were complaining. This guide follows the team as it rebuilds the tests slice by slice, finds three causes and stops them coming back.
What is AI bias testing?
AI bias testing is checking whether an AI feature gives worse results to some groups of people than to others. You run controlled tests and compare the results group by group.
Bias here means a difference that is both unfair and systematic. Systematic means it repeats in a pattern, not once by bad luck. One short summary is a bug. Bhojpuri summaries being shorter every time is bias.
The US National Institute of Standards and Technology (NIST) names three major categories of AI bias in section 3.7 of its AI Risk Management Framework 1.0. They are systemic, computational and statistical, and human-cognitive.
- Systemic bias sits in datasets, in organisations' norms and processes, and in the wider society. Example: if fewer women have done paid plumbing work, old placement data has few of them.
- Computational and statistical bias sits in data and algorithms, often from samples that do not represent everyone. Example: a launch test set that is nearly all Hindi.
- Human-cognitive bias is about how people read and use what the system shows. Example: an employer who calls the top result without reading the rest.
NIST adds that each kind can occur without prejudice, partiality or intent to discriminate. Nobody on the team wanted to push women out of plumbing jobs. The pattern showed up anyway.
Why is an overall accuracy score not enough to find bias?
An overall score is an average, and an average lets a large group with good results hide a small group with bad ones. You only see the gap when you split results by group.
Here is the arithmetic. Say 900 test profiles are in Hindi and 93 percent of their summaries are good. The other 100 are in Bhojpuri, and only 70 percent are good. The overall score is still 90.7 percent.
Think of a mango buyer at a wholesale fruit market in Patna. A careless buyer opens one crate from the nearest orchard and looks only at the top layer, where sellers put their best fruit. A careful buyer opens crates from every orchard and checks the bottom layer too.
- Each orchard is one slice of your users, such as Bhojpuri speakers or workers in Purnia.
- The top layer of the nearest crate is a test set drawn from your most common users.
- Opening crates from every orchard, down to the bottom layer, is reporting results for every slice, especially the small ones.
Where the picture breaks: a buyer can see a bruise, but a test only sees what you choose to measure. If you never measure summary length, the Bhojpuri gap stays invisible however many crates you open.
Researchers have found this pattern in real products. The 2018 Gender Shades study tested three commercial systems that guess gender from a face photo. Error rates reached 34.7 percent for darker-skinned women, against at most 0.8 percent for lighter-skinned men.
A benchmark is a shared test set that many teams use to compare models. The same study found two popular face benchmarks, IJB-A and Adience, were 79.6 and 86.2 percent lighter-skinned faces. A high score on them said little about everyone else.
How do you slice LLM tests by language, region and gender?
You tag every test case with the user's language, region and gender, then report results for each tag and for the overlaps between tags. A slice is one group of test cases that share a tag.
The team picks slices from who actually uses the app, not from a generic list. It looks at sign-ups, support complaints and where the jobs are.
- Language: Hindi, Bhojpuri, Maithili and English. Many workers mix Hindi and English in one sentence, so mixed text later becomes a fifth tag.
- Region: Patna city, Gaya in the south and Purnia in the north-east. Purnia had the most complaints.
- Gender: women and men, as workers state it in their profile. Workers who prefer not to say form their own slice in live data.
- Intersections: pairs such as women writing in Bhojpuri. An intersection is the group where two slices overlap.
Intersections matter because gaps can stack. Gender Shades found the worst errors where two traits met, darker skin and female. Testing women overall and Bhojpuri overall can still miss the group that has both.
India has more groups worth testing, such as caste and religion. IndiBias, a 2024 benchmark for the Indian context in English and Hindi, covers gender, religion, caste, age, region, physical appearance and occupation.
The team starts with three dimensions because they match the complaints. It writes the others into next quarter's plan, so they are not quietly forgotten.
Here is the test set it builds. Start at the base profiles and follow how each one turns into many tagged test cases.
Building a sliced test set
Drag to exploreNotice the multiplication: each base profile becomes 24 variants (2 genders, 4 languages, 3 districts). Each gender and language overlap gets 300 cases, and each language and district overlap only 200.
Diagram connections:
- 100 base worker profiles to Make variants
- Make variants to Native speakers check text
- Native speakers check text to 2,400 tagged test cases
- 2,400 tagged test cases to Run the LLM feature
- Run the LLM feature to Report per slice
- 100 base worker profiles: Real trades, skills and years of work
- Make variants: Change gender, language and district
- Native speakers check text: Same facts in every language
- 2,400 tagged test cases: Each case carries three tags
- Run the LLM feature: Summary plus job shortlist
- Report per slice: One row per tag and overlap
How do you build counterfactual test pairs for an LLM?
You take one input and change only one detail, such as gender or language, while keeping every fact the same. If the output changes, that one detail caused the change.
Counterfactual means what would happen if one thing were different. Take one plumber with six years of work, written once as a woman and once as a man. Same skills, same district, same language. Only the name and pronouns change.
Here is how the team makes the variants. The functions pick_name and write_profile stand in for its own code. The first returns a common local name for that gender and district, from a reviewed list. The second writes the same facts in one language.
import itertools
GENDERS = ["woman", "man"]
LANGUAGES = ["hindi", "bhojpuri", "maithili", "english"]
DISTRICTS = ["Patna", "Gaya", "Purnia"]
def make_variants(base):
variants = []
for gender, lang, district in itertools.product(GENDERS, LANGUAGES, DISTRICTS):
case = dict(base, gender=gender, lang=lang, district=district)
case["name"] = pick_name(gender, district)
case["text"] = write_profile(case, lang) # same facts, new words
variants.append(case)
return variantsWhat just happened? The itertools.product call builds every mix of gender, language and district. One base profile gives 2 times 4 times 3, or 24 variants. Copying with dict(base, ...) keeps the base facts, so trade, skills and years stay the same.
We ran this with stand-in helpers on a six-year plumber profile. It printed 24 variants, every one with the same trade, years and skills. One caution: dict makes a shallow copy, so never edit a shared skills list inside one variant.
Translation is the risky step. If write_profile drops a skill while writing Bhojpuri, the test blames the model for your own mistake. So native speakers check a sample in every language before the results count.
BBQ, a bias benchmark for question answering published in Findings of ACL 2022, uses a related idea. Some of its questions come with under-informative context, text that does not contain the answer. Any lean in the answer then shows the model's assumptions.
Other questions come with enough context to answer. Models were still up to 3.4 points more accurate on average when the right answer matched a social bias. For gender examples, that gap was over 5 points for most models tested.
BBQ's nine bias categories were built for US English-speaking contexts, so its numbers do not carry over to Bhojpuri. The idea does. Some of the team's test job posts say nothing about who should apply. If the model still sends women to housekeeping jobs from those posts, it is filling the gap with a stereotype.
How do you measure bias in LLM outputs?
You pick an outcome that matters to users, compute its rate for each slice, and compare each slice with the best one. Here the main outcome is reaching an employer's shortlist.
The shortlist rate is the share of test profiles the model puts in the top 10 for a matching job. A matching job is one whose listed skills the worker really has.
One number never covers an LLM feature, so the team tracks four outcomes.
- Shortlist rate: how often a qualified worker reaches the top 10 for a job.
- Skills captured: how many of the worker's real skills appear in the summary, checked against the base profile.
- Wrong-trade rate: how often a worker is suggested for a job outside their trade, like a plumber sent to housekeeping.
- Flip rate: how often the two sides of a counterfactual pair get different top suggestions, when only gender or language changed.
Flip rate needs a baseline. If your feature samples its output, the same input can flip between runs with nothing changed. Run each input several times first, say five; a counterfactual flip rate only means something above that noise floor.
To decide when a gap is big enough to act on, the team borrows the four-fifths rule. It comes from the US Uniform Guidelines on Employee Selection Procedures, section 29 CFR 1607.4(D).
The text says US federal enforcement agencies will generally treat a selection rate below four-fifths, or 80 percent, of the highest group's rate as evidence of adverse impact. Adverse impact means a substantially different selection rate that works against one group.
The app works in India, so this US rule is not its legal test. The team uses it as a plain alarm level everyone understands.
The wording here is the text of 1607.4 as of 19 October 2026. The US Equal Employment Opportunity Commission (EEOC) plans to rescind part 1607, its copy of these guidelines. Its regulatory agenda lists the final rule for November 2026, so check the status before you quote the rule as law.
Here is the check the team runs after each test. The input called results is a list of dictionaries, one per test case, with the three tags and a shortlisted field that is 1 or 0.
from collections import defaultdict
def impact_ratios(results, tag):
total, shortlisted = defaultdict(int), defaultdict(int)
for r in results:
total[r[tag]] += 1
shortlisted[r[tag]] += r["shortlisted"]
rates = {g: shortlisted[g] / total[g] for g in total}
best = max(rates.values()) or 1
return {g: (round(rate / best, 2), total[g]) for g, rate in rates.items()}
for r in results:
r["gender_lang"] = r["gender"] + "+" + r["lang"] # one overlap tag
for tag in ["gender", "lang", "district", "gender_lang"]:
for group, (ratio, n) in impact_ratios(results, tag).items():
flag = "review" if ratio < 0.8 or n < 100 else "ok"
print(tag, group, ratio, n, flag)The ratio divides each group's rate by the best group's rate in that tag, so the best group always scores 1.0. The or 1 avoids dividing by zero when no case was shortlisted. The gender_lang tag adds the eight overlaps of gender and language.
To check it works, give it 200 men with 124 shortlisted and 200 women with 82 shortlisted, each with a language and district tag. The gender lines read gender man 1.0 200 ok and gender woman 0.66 200 review.
Here is what the first full run on 2,400 cases showed. Read each ratio next to its count.
| Slice | Test cases | Shortlist rate | Ratio to best | Flag |
|---|---|---|---|---|
| Men | 1,200 | 62% | 1.00 | ok |
| Women | 1,200 | 41% | 0.66 | review |
| English | 600 | 58% | 1.00 | ok |
| Hindi | 600 | 55% | 0.95 | ok |
| Maithili | 600 | 48% | 0.83 | ok |
| Bhojpuri | 600 | 45% | 0.78 | review |
| Patna | 800 | 57% | 1.00 | ok |
| Gaya | 800 | 54% | 0.95 | ok |
| Purnia | 800 | 43.5% | 0.76 | review |
| Women writing Bhojpuri | 300 | 30% | 0.44, against men writing English (68%) | review |
The overall shortlist rate was 51.5 percent, which looked fine on its own. The slices raised three flags, and women writing Bhojpuri was the worst result in the whole run.
Beginners can stop here: you now know how to slice, pair and measure. The rest is for engineers. It shows how often the 0.8 alarm lies, how to find causes, and how to gate releases.
How many test cases do you need for each slice?
You need enough cases that a gap is unlikely to be chance. Our simulation below suggests 300 per slice is a sensible floor when a gate judges a few slices, and 30 is far too few.
The four-fifths rule carries the same warning. The Uniform Guidelines say bigger gaps may not count as adverse impact when they rest on small numbers and are not statistically significant.
They also say smaller gaps can still count when they are significant in both statistical and practical terms. So 0.8 is an alarm, not a pass mark.
Statistically significant means the gap is too big to blame on chance alone. A simple way to see the noise is a confidence interval: a range built so that, across many repeated tests, 95 in 100 such ranges would contain the true rate.
Take a 40 percent shortlist rate. With 30 test cases, the 95 percent Wilson confidence interval, one standard way to compute that range for a rate, runs from about 25 to 58 percent. With 600 cases, it narrows to about 36 to 44 percent.
We simulated it with Python and NumPy, 100,000 runs per row and a fixed seed. The answer is 47 percent of the time, close to a coin toss. The table shows how that falls as slices grow.
| Cases per slice | No real gap, 2 groups | No real gap, 4 groups | Real gap (60% vs 42%), 2 groups |
|---|---|---|---|
| 30 | 47% false alarms | 80% false alarms | 70% caught |
| 100 | 21% false alarms | 40% false alarms | 83% caught |
| 300 | 2.7% false alarms | 3.4% false alarms | 95% caught |
| 600 | 0.1% false alarms | 0.1% false alarms | 99% caught |
Two things stand out. More slices mean more false alarms, because the best slice is partly lucky and every other slice is compared with it. And at 30 cases, a real 0.70 gap is missed three times in ten.
The release gate later in this guide adds a significance test, but more slices still mean more false blocks. We simulated that gate on eight gender and language overlaps of 300 cases, with no real gap. At a true 50 percent rate it blocked 12 percent of releases; at 30 percent, 51 percent. It still caught a real 0.70 gap 99 percent of the time.
So a ratio alone is not enough. Before you act, check that the gap is bigger than chance with a two-proportion z-test, which asks whether two rates differ by more than noise.
# pip install statsmodels (we ran statsmodels 0.15.0)
from statsmodels.stats.proportion import proportion_confint, proportions_ztest
women, men, n = 82, 124, 200 # shortlisted out of 200 test cases each
print(proportion_confint(women, n, method="wilson"))
print(proportion_confint(men, n, method="wilson"))
print(proportions_ztest([women, men], [n, n])[1])On statsmodels 0.15.0 this prints about (0.344, 0.479), then (0.551, 0.684), then a p-value of about 0.000026. The p-value is the chance of a gap at least this big if both groups were truly equal. Below 0.05 is the usual bar.
Note method="wilson": the default is a plain normal approximation, which for 12 of 30 gives 22.5 to 57.5 percent instead of Wilson's 24.6 to 57.7.
Now shrink the slices. The same 0.67 ratio from 12 of 30 against 18 of 30 gives a z-test p-value of about 0.12, which is not significant. Same ratio, very different evidence.
The team turns this into four working rules.
- At least 100 test cases in every reported slice, and at least 300 in any slice the release gate judges.
- The count printed next to every rate, so nobody reads a tiny slice as a big finding.
- Intersections grown on purpose, because they shrink fast. Two genders times four languages already makes eight groups of 300; four languages times three districts makes twelve groups of 200.
- A significance test on every flagged gap, plus each case run more than once when outputs vary between runs.
How do you find the cause of a bias and fix it?
You find the cause by reading the failed cases in the worst slice next to their counterfactual pairs. The fix depends on what you find, and the same tests must prove it worked.
The team reads 60 failed pairs from the women writing Bhojpuri slice. It finds three separate causes, not one. This diagram shows the matching feature, and each note marks where a cause entered.
Where bias entered the matching feature
Drag to exploreNotice that none of the three causes was inside the model's settings. One came from speech to text and two came from the ranking prompt.
Diagram connections:
- Worker records a profile to Speech to text
- Speech to text to Model writes summary
- Model writes summary to Model ranks workers
- Ranking prompt to Model ranks workers
- Model ranks workers to Employer sees shortlist
- Worker records a profile: Voice or text, in any language
- Speech to text: Cause 1: garbles Bhojpuri trade words
- Model writes summary: Misses skills it never received
- Ranking prompt: Causes 2 and 3: skewed examples, old line
- Model ranks workers: Picks top 10 for each job
- Employer sees shortlist: Calls workers from the top 10
- Cause 1, speech to text. Bhojpuri words for trade skills came out garbled, so skills vanished before the model saw them. Fix: show the worker a skills checklist to confirm after recording.
- Cause 2, prompt examples. Nine of the ten sample matches in the ranking prompt showed men in trades. Fix: balanced examples, and an instruction to rank only on skills, years and distance.
- Cause 3, a leftover line. The prompt still told the model to prefer workers near Patna, from an early pilot. Fix: delete it, and work out distance in code from each job's location.
Someone asks whether they could just hide gender from the model. That helps less than you might think. Names, pronouns and work history still carry gender, so the model can still pick it up.
A field that quietly stands in for a hidden one is called a proxy. District names and language can be proxies for caste, religion or income in India. Counterfactual pairs catch proxies, because they change exactly those clues.
The team prefers fixes it can measure. Ranking now uses structured fields such as skills, years and distance. The model still writes the summary. Then the team reruns all 2,400 cases.
After the fixes, the women's ratio rose from 0.66 to 0.92, Bhojpuri from 0.78 to 0.90, and Purnia from 0.76 to 0.94. Women writing Bhojpuri rose from 0.44 to 0.84 (made-up numbers).
How do you add bias tests to every release?
You turn the sliced test set into an automatic gate: the release stops if any slice fails the check. A one-time audit goes stale as soon as someone edits a prompt.
Prompts, models and speech services all change over time, and any change can bring a gap back. Cause 3 was exactly that: an old prompt line nobody remembered.
Here is the gate the team adds. The question box checks every slice at once. Follow both answers to see what happens next.
A fairness release gate
Drag to exploreNotice that the gate judges the worst slice, never the overall average, and that a failure goes to a person before anything ships.
Diagram connections:
- Prompt or model change to Run 2,400 sliced cases
- Run 2,400 sliced cases to Every slice passes?
- Every slice passes? to Release to workers (Yes)
- Every slice passes? to Block and alert the owner (No)
- Release to workers to Watch live slices
- Prompt or model change: Any edit to the matching feature
- Run 2,400 sliced cases: The same frozen set every time
- Every slice passes?: 300+ cases, no significant gap below 0.8
- Release to workers: Rolled out to a small share first
- Block and alert the owner: A person reads the failed pairs
- Watch live slices: Weekly rates by language and district
Freeze the test set
Keep the 2,400 cases fixed, so this month's numbers compare fairly with last month's. Add new cases as a separate, labelled batch.
Run on every change
Any edit to a prompt, a model setting or the speech service starts the full sliced run automatically.
Gate on the worst slice
The release stops if any slice is below 0.8 with a significant gap, or if a judged slice has fewer than 300 cases.
Read the failures
A person reads a sample of failed pairs before anyone decides. The number says where to look, and reading says why. Expect some false blocks on overlap slices, so this reading step is part of the gate.
Watch live data
After release, compare real shortlist rates by language and district each week, for workers who shared that information.
Live data needs care. Use gender or region only where workers chose to share it, and keep it out of the matching input. You need it to measure the feature, not to rank people.
How do you explain AI bias testing in a system design interview?
Explain it as an evaluation design problem. Choose the groups, build controlled test cases, measure outcomes per slice, and gate releases on the worst slice.
Name the harm
Say what a bad outcome looks like for this product, such as a qualified worker missing the shortlist.
Choose slices from the users
Pick dimensions from who uses the product, such as language, region and gender, and add the key intersections.
Build counterfactual cases
Make variants that change one detail at a time, and have native speakers check every language.
Pick outcomes and a threshold
Track shortlist rate, skills captured and flip rate. Use a simple alarm like the four-fifths rule, and show counts.
Plan for small numbers
Set a minimum count per slice and test flagged gaps for significance, so you do not chase noise.
Gate and monitor
Run the suite on every change, block releases that fail, and watch live rates by slice.
Questions people ask
- What is the difference between bias testing and fairness testing?
- They are mostly two names for the same work. Some teams say bias testing for finding gaps and fairness testing for checking gaps against a set limit. This guide uses them interchangeably. Both need the same things: tagged test cases, results per slice and a threshold.
- Does removing gender from the input stop an LLM from being biased?
- No, not by itself. Names, pronouns, work history and even language can act as proxies, fields that quietly stand in for gender. The model can still pick up those clues. Counterfactual pairs that change all the clues together show whether the output really ignores gender.
- What is the four-fifths rule in AI bias testing?
- It is an alarm level taken from the US Uniform Guidelines on Employee Selection Procedures. Their text says a group's selection rate below 80 percent of the highest group's rate is generally regarded as evidence of adverse impact. As of 19 October 2026, the EEOC plans to rescind its copy of the guidelines, 29 CFR part 1607, so treat 0.8 as an engineering alarm. The same text warns that small numbers can mislead, so check counts and significance too.
- Can I use an LLM to judge bias in outputs?
- Yes, for some checks, but test the judge first. A model can score whether a summary kept every skill, which saves reading time. Do not assume it is equally accurate in every language. Compare its scores with native speakers' ratings in every language slice before you trust it.
- How do I test an LLM for bias in Indian languages?
- Write the same test cases in each language your users speak, and have native speakers check them. Include mixed Hindi and English text, because many users write that way. IndiBias covers English and Hindi, so you will usually need your own cases for other languages.
- How often should I run bias tests?
- Run them on every change to a prompt, model or input step, such as speech to text. Any of these can bring a gap back. Also watch live rates by slice each week, because real users bring inputs your test set never had.
- Should I test for caste and religion as well as gender?
- Yes, in India you should plan for them. Caste and religion can leak through names, districts and language, even when you never collect them. Start with the groups tied to real complaints, and list the rest in your report so they get tested next.
Sources and further reading
- NIST AI Risk Management Framework 1.0 (AI 100-1), section 3.7: Fair, with Harmful Bias Managed
- Uniform Guidelines on Employee Selection Procedures, 29 CFR 1607.4 Information on impact (four-fifths rule, paragraph D)
- Uniform Guidelines on Employee Selection Procedures, 29 CFR 1607.16 Definitions (adverse impact, paragraph B)
- US Unified Agenda entry RIN 3046-AB43: Rescission of Uniform Guidelines on Employee Selection Procedures (EEOC, final rule stage, 29 CFR part 1607)
- Gender Shades: Intersectional Accuracy Disparities in Commercial Gender Classification (Buolamwini and Gebru, 2018)
- BBQ: A Hand-Built Bias Benchmark for Question Answering (Parrish et al., Findings of ACL 2022), arXiv abstract
- IndiBias: A Benchmark Dataset to Measure Social Biases in Language Models for Indian Context (Sahoo et al., 2024)
- statsmodels 0.15.0 documentation: proportion_confint (Wilson score interval)