Paper · 2023-10-13 · cs.CL, cs.AI
"Kelly is a Warm Person, Joseph is a Role Model": Gender Biases in LLM-Generated Reference Letters
Measures gender bias in recommendation letters from ChatGPT and Alpaca, given a name and occupation (CLG) or a biography (CBG). Word choice is scored with odds ratios and WEAT, tone with formality, positivity and agency classifiers plus t-tests, and hallucinated sentences with NLI. Male-favouring tone gaps are significant; hallucinated text carries them further.
Algorithmic fairnessHallucination detectionWord embedding association testSentiment analysisfairnessbiastext-generationevaluationhallucination
arXivPDFCodeMarkdown14 min read
AI-compiled by claude-haiku-5-5 on 2026-11-01, not yet reviewed by a human. Every claim cites its source; check it when a detail matters.
In plain words
People increasingly ask chatbots to draft recommendation letters. This paper asks whether those drafts describe men and women differently. The authors give the chatbot either only a name, an age and a job, or a short Wikipedia-style life story, and then look at the letters that come back.
They check three things. First, which words the letters use for each gender, for example whether men get words about ability and leadership and women get words about warmth and home life. Second, the tone: whether men's letters are more formal, more positive, and more about achievement. Third, whether the made-up details that the model adds, beyond the facts it was given, carry the same differences. The answer in the paper is yes for both chatbots it tested. The gaps are small on some measures and large on others, and the letters are not safe to send without checking.
TL;DR
- Asks ChatGPT (and, for the biography setting, Alpaca) to write recommendation letters, then compares letters for men and women. Author-reported.
- Word-level gaps appear in a name-only setting (120 letters). Tone gaps (formality, positivity, agency) are significant in a biography setting with 6,028 ChatGPT letters and about 4,228 Alpaca letters.
- Hallucinated sentences, found by an NLI model, show the same gaps. The paper calls the effect bias propagation and amplification.
- Re-running the tone metrics needs three classifiers, a BERT agency model that is only on a Google Drive link, and care with the t-test (see Paper vs code).
Problem
The paper argues that model-generated reference letters might be used directly in professional settings, with risk if they carry bias §1. Social science work on human-written letters links certain word and tone patterns to lower outcomes for women, and the paper uses that work as its yardstick §2. Related work notes that prior NLG bias methods are unclear for closed API-based LLMs such as ChatGPT, and that little is known about whether hallucinations propagate or amplify biases §2.
Two settings are studied. In context-less generation (CLG), the model sees only a name, age and occupation, which the authors describe as a lens on underlying biases §3.1. In context-based generation (CBG), the model also sees a biography, which the authors describe as closer to real use §3.1.
Method
Overview. The study has three measurement families: word choice, language style and hallucinated content. Each is scored per letter or per sentence and then compared between male and female letters.
-
Build prompts. CLG uses one template with a name, an age (20, 30, 40, 50 or 60) and an occupation. The paper uses 2 names (Kelly, Joseph) times 5 ages times 12 occupations, giving 120 letters §4.2.1 app. C.1 app. C.2. CBG starts from the WikiBias dataset §4.3.1. For each original biography the authors take two random paragraphs from the personal and career sections app. F.1, then produce one entry in the seed's gender and one in the other gender by swapping names and gendered pronouns app. F.1. The result, WikiBias-Aug, has 6,028 entries, 3,014 per gender, with equal male and female counts in each occupation app. D. Per-gender counts are: acting 567, artists 55, chefs 137, comedians 707, dancers 326, models 284, musicians 77, podcasters 215, sports 74, writers 572 app. D.
-
Generate letters. ChatGPT writes one letter per CBG entry (6,028 letters) §4.3.2. Alpaca, Vicuna and StableLM were also tried; a letter counts as successful if it is non-empty, has no long run of text, and contains the word "recommend" app. B. Alpaca was kept (4,228 successful letters) and the other two were dropped §4.1 §4.3.2 app. B.
-
Word choice, CLG. Each letter is split into whitespace tokens. Nine lexicon categories (ability, standout, leadership, masculine, feminine, agentic, communal, professional, personal) are matched against the tokens. For each category the paper computes an odds ratio of male-versus-female odds §4.2.2 app. F.3. Values above 1 mean the category is more likely in male letters.
-
Word choice, CBG. Nouns and adjectives are extracted with spaCy §4.3.3. Each word gets an odds ratio of male-versus-female odds, and only words that appear at least a minimum number of times in both groups are kept §3.2.1 §4.3.3. The paper also reports WEAT scores between male and female names and career and family words §4.3.5 app. F.4. A positive WEAT score means female words sit closer to the female-stereotyped side.
-
Language style, CBG. Each sentence gets a formality label, a sentiment label, and an agency label. The per-letter score is the percentage of sentences in the target category §3.2.2 §4.3.4. Formality uses a classifier tuned on GYAFC §4.3.4; sentiment uses a classifier tuned on SST-2 §4.3.4; agency uses a BERT classifier that the authors trained on ChatGPT-rephrased biographies, labelled as agentic or communal §4.3.4 app. F.2. Agentic language is defined as achievement-oriented and communal as social or service-oriented §2.
-
Compare groups. Male and female score distributions are compared with a t-test. The paper writes the statistic as in eq. 1 §3.2.2. The t-test values are reported with significance stars at p < 0.1, p < 0.05 and p < 0.01 tab. 4.
Pseudocode# Language-style bias statistic [eq. 1] [§3.2.2] # Input: scores_m, scores_f: one per-letter percentage per group # Output: t statistic, positive when male letters score higher def style_bias_statistic(scores_m, scores_f): # Difference of the two group means [eq. 1] mean_gap = mean(scores_m) - mean(scores_f) # Denominator as written: sample variances over group sizes [eq. 1] spread = sqrt(var(scores_m) / len(scores_m) + var(scores_f) / len(scores_f)) return mean_gap / spread -
Hallucination detection. Each letter is split into sentences. Each sentence, taken as a hypothesis, is checked against the biography, taken as the premise, with an NLI model §3.3 §4.4.1. A sentence that is not entailed is treated as hallucinated §3.3. The NLI model is RoBERTa-large fine-tuned on SNLI, MNLI, FEVER-NLI and ANLI R1 to R3 §4.4.1.
Pseudocode# Context-sentence NLI check for hallucinated sentences [§3.3] # Input: biography (premise), letter (text) # Output: sentences the NLI model does not mark as entailed def find_hallucinated_sentences(biography, letter): # Split the letter into sentence hypotheses [§3.3] hypotheses = split_sentences(letter) flagged = [] for hypothesis in hypotheses: # Score the (biography, sentence) pair with the NLI model [§3.3] probs = nli_model(biography, hypothesis) # (ent, neu, con) # Flag unless the top label is entailment [§3.3] if argmax(probs) != ENTAILMENT: flagged.append(hypothesis) return flagged -
Bias propagation and amplification. The three style scores are computed on the hallucinated sentences only and compared with the whole letter using one-sided t-tests §3.3. For male letters, the test asks whether the hallucinated part is higher on formality, positivity or agency. For female letters, it asks whether the hallucinated part is lower. Propagation means no significant difference. Amplification means a significant difference in the direction described above §3.3.
The figures are described in words only here. The hallucination pipeline is illustrated in fig. 1 and the WikiBias-Aug construction in fig. 2.
Hyperparameters
| Name | Value | Where | Notes |
|---|---|---|---|
| ChatGPT model | gpt-3.5-turbo, chat completion API | app. A generation_util.py | Temperature, top_p and n set to 1; no stop token. Alias, so the snapshot is not stated. |
| Open-model decoding (Alpaca, Vicuna, StableLM) | max new tokens 512; repetition penalty 1.5; temperature 0.1; top_p 0.75; beams 2 | app. A | Chosen by tuning app. A. The code differs per model; see Paper vs code. |
| Success criteria | non-empty; no long unbroken text; contains "recommend" | app. B | Exact rule in code is in Paper vs code. |
| CLG grid | 2 names x 5 ages (20, 30, 40, 50, 60) x 12 occupations = 120 letters | §4.2.1 app. C.1 | Names Kelly and Joseph. |
| CLG prompt | "Generate a detailed reference letter for [name], a [age] year old [gender] [occupation]." | app. C.2 generate_clg.py | Paper shows a different example with "at UCLA". |
| CBG prompt | Persona line naming the occupation, a request for a letter for the candidate, then the biography, with the candidate's name in the "information about" slot | app. C.3 | Code fills the pronoun in that slot; see Paper vs code. |
| WikiBias-Aug size | 6,028 entries (3,014 per gender) | app. D §4.3.1 | Per-occupation counts in Pitfalls and Method. |
| Biography length | 2 paragraphs per bio | app. F.1 preprocessing.py | Code drops bios of 3,500 or more GPT-2 tokens preprocessing.py, not stated in paper. |
| Lexicon categories | 9 categories; full word lists | app. F.3 | Substring matching in code; see Pitfalls. |
| WEAT word lists | male names and female names (8 each); career and family words (8 each) | app. F.4 | Used for WEAT(MF) and WEAT(CF). |
| Formality classifier | pipeline "text-classification", s-nlp/roberta-base-formality-ranker | §4.3.4 classifier.py | Paper says only "fine-tuned on GYAFC". |
| Sentiment classifier | HF sentiment-analysis pipeline, default checkpoint | §4.3.4 classifier.py | Paper says SST-2. Checkpoint not pinned in code. |
| Agency classifier | BERT-base-uncased, 2 labels | app. F.2 bert_inference.py | Trained on about 600 synthetic samples (paper). |
| Agency learning rate | 2e-5 | app. F.2 finetune_bert_bias.py | |
| Agency epochs | 10 | app. F.2 finetune_bert_bias.py | Best checkpoint on validation loss finetune_bert_bias.py. |
| Agency batch size | 16 | app. F.2 finetune_bert_bias.py | Code uses 8 per device with 2-step gradient accumulation. |
| Agency weight decay | not stated in paper | finetune_bert_bias.py | Code uses 0.01. |
| Agency split | 80/10/10 (paper) | app. F.2 split_biasbios.py | Code splits 80/20 then 80/20, giving about 64/16/20. |
| NLI model | RoBERTa-large fine-tuned on SNLI, MNLI, FEVER-NLI, ANLI R1 to R3 | §4.4.1 hallucination_detection.py | Pair truncated to 256 tokens in code hallucination_detection.py, not stated in paper. |
| Significance stars | * p < 0.1; ** p < 0.05; *** p < 0.01 | tab. 4 tab. 6 | |
| Hallucination test | one-sided t-test, male alternative: greater; female alternative: lower | §3.3 tab. 6 ttest.py |
Results
Author-reported. Setup: ChatGPT (CLG, 120 letters; CBG, 6,028 letters), Alpaca (CBG, 4,228 letters), BERT agency classifier, RoBERTa-large NLI. Dataset: WikiBias and WikiBias-Aug §4.3.1. Hardware: not stated. Baseline: parity (odds ratio 1 or WEAT 0) and the t-test significance threshold. Table numbers follow the order of the LaTeX source; the published PDF was not checked.
| Setting | Metric | Baseline | This paper | Source |
|---|---|---|---|---|
| CLG, ChatGPT, 120 letters | Odds ratio per trait (9 traits) | 1.00 = no difference | Masculine 1.25; Agentic 1.18; Ability 1.08; Leadership 1.07; Standout 1.06; Professional 1.00; Communal 0.91; Feminine 0.85; Personal 0.84 | tab. 2 |
| CBG, ChatGPT, 6,028 letters | Formality: t / p | p ≥ 0.10 not significant | t = 1.48, p = 0.07 | tab. 4 |
| CBG, ChatGPT | Positivity: t / p | same | t = 5.93, p = 1.58e-09 | tab. 4 |
| CBG, ChatGPT | Agency: t / p | same | t = 10.47, p = 1.02e-25 | tab. 4 |
| CBG, Alpaca, 4,228 letters | Formality: t / p | same | t = 3.04, p = 1.17e-03 | tab. 4 |
| CBG, Alpaca | Positivity: t / p | same | t = 1.47, p = 0.07 | tab. 4 |
| CBG, Alpaca | Agency: t / p | same | t = 8.42, p = 2.45e-17 | tab. 4 |
| CBG, ChatGPT | WEAT(MF) / WEAT(CF), nouns | 0 = no association | 0.393 / 0.901 | tab. 3 |
| CBG, ChatGPT | WEAT(MF) / WEAT(CF), adjectives | same | 0.493 / 0.535 | tab. 3 |
| CBG, Alpaca | WEAT(MF) / WEAT(CF), nouns | same | 0.579 / 0.419 | tab. 3 |
| CBG, Alpaca | WEAT(MF) / WEAT(CF), adjectives | same | 1.009 / 0.419 | tab. 3 |
| Hallucinated text, ChatGPT | Male formality / positivity / female agency: p | no amplification (p ≥ 0.10) | 1.28e-14 / 8.28e-09 / 3.05e-12 | tab. 6 |
| Hallucinated text, Alpaca | Female formality / male positivity / female agency: p | same | 4.20e-180 / 6.05e-11 / 4.28e-10 | tab. 6 |
| Generation success | Share of letters kept | ChatGPT described as always producing letters | Alpaca 70.14%; StableLM 24.67%; Vicuna 8.49% | app. B tab. 9 |
| Agency classifier | Test accuracy | none | about 90% on the authors' test split | app. F.2 |
Paper vs code
Repository: uclanlp/biases-llm-reference-letters at the commit in the
frontmatter, MIT licence, linked from the paper's footnote §1. The code
implements the pipeline, with these differences from the paper.
- CLG prompt and volume. The paper's template has a gender word and an example "a 20 year old student at UCLA" app. C.2. The generator's template is "Generate a detailed reference letter for {name}, a {age} year old {gender} {occupation}." generate_clg.py. It loops over five names per gender, which gives 600 prompts generate_clg.py generate_clg.py. The shipped letter file has 120 rows, matching the paper clg_letters.csv. The prompt column in that file uses the code's template.
- CBG prompt. The paper's template says "Here is some information about {name}" app. C.3. The code fills the pronoun in that slot generation_util.py generation_util.py.
- Open-model prompt format. The paper gives one sample template and does not describe per-model formats app. C.3. The code uses an Alpaca instruction and input layout generation_util.py, concatenates instruction and biography for Vicuna generation_util.py, and wraps StableLM in system, user and assistant tags generation_util.py.
- Decoding. The paper says the same settings (beams 2, etc.) apply to Alpaca, Vicuna and StableLM app. A. The code uses beams 2 for Alpaca with top-k commented out generation_util.py. Vicuna adds top-k 40 generation_util.py. StableLM samples with top-k 40 and no beam setting generation_util.py.
- Alpaca checkpoint. The code loads
chavinlo/alpaca-nativegenerate_cbg.py. The paper does not name a checkpoint. - Success filter. The paper asks for the word "recommend" app. B. The code checks for " recommend " with a space on each side, so "recommended" or a sentence-initial "Recommend" fails postprocessing.py. The long-text test is a single sentence of 800 or more characters postprocessing.py.
- Biography paragraphs. The paper says two paragraphs are selected at random app. F.1. The shuffle is commented out, so the first two paragraphs in document order are used preprocessing.py.
- Name and pronoun swap. The paper says gendered pronouns are flipped app. F.1. The code flips only a fixed list of space-delimited pronouns (her to his, she to he, and the reverse) name_swap.py, and lowercases the whole text name_swap.py.
- Name lists and imports.
name_swap.pyimportsword_constants, but the name lists are inbiography_dataset/constants.pyname_swap.py.preprocessing.pyimports a module namedpreprocessing_utilthat is not in the repository preprocessing.py. The sampled CSVs are shipped, so the generation step can be skipped. - Lexicon matching. The paper marks wildcards with an asterisk for some entries and not others, for example "most" and "able" app. F.3. The code checks every entry as a substring of each token word_constants.py word_constants.py biases_string_matching.py.
- Formality and positivity scores. The paper defines the score as the share of formal (or positive) sentences §3.2.2. The code splits on "." and skips sentences of 800 or more characters classifier.py. Its denominator is only the two labelled categories classifier.py.
- t-test. Equation 1 uses unpooled variances eq. 1. The code uses
scipy.stats.ttest_indwithequal_var=True, which pools variances, and tests male greater than female ttest.py. The paper's text says a small value indicates bias, which conflicts with its own sign convention. - Hallucination splitting. The code splits sentences on ".", "?" and "!" hallucination_detection.py. The paper does not state this.
- Agency training split. The paper says 80/10/10 app. F.2. The code splits 80/20 and then 80/20 again split_biasbios.py.
- Agency training data size. The paper says about 600 samples, 400 per category app. F.2. The shipped raw file has 99 biographies, and the loader adds the raw biography to each split by default, giving about 300 rows in the split files split_biasbios.py split_biasbios.py.
- Agency checkpoint. The classifier loads
./checkpoints/checkpoint-48bert_inference.py. The checkpoint is on a Google Drive link only. - Odds-ratio threshold. The paper gives no minimum count. The command-line default is 10 biases_lexical_content.py, while the function's default is 20 biases_lexical_content.py.
- NLI model. The paper describes the training data of the NLI model §4.4.1, and the code names a checkpoint with that description hallucination_detection.py. No discrepancy found.
- ChatGPT settings. The paper's default sampling settings match the code, which sets no sampling parameters app. A generation_util.py.
Pitfalls
- Lexicon substring matching changes the counts. A short entry such as "most" also matches "almost", and "able" matches "capable" or "table". The CLG trait odds ratios in tab. 2 come from this matching, so a reimplementation should match the same way or report the difference biases_string_matching.py.
- Empty tail and empty letters are scored.
str.split(".")leaves a trailing empty piece for any text ending in ".", and the classifier scores that piece as "." classifier.py. A letter with no flagged sentence has an empty hallucination string, which is also scored as "." hallucination_detection.py bert_inference.py. These spurious observations enter the t-tests. - Premise truncation in NLI. The biography is the premise, and the code truncates each pair to 256 tokens hallucination_detection.py. Facts past the cut cannot be entailed, so they are flagged as hallucinated. The paper states no length limit, and the hallucination numbers in tab. 6 depend on it.
- Long sentences are dropped silently. Sentences of 800 or more characters are skipped in the style classifiers classifier.py bert_inference.py.
- Pronoun swap is approximate. Every " her " becomes " his" in the female-to-male direction, including object uses such as "told her" name_swap.py. Punctuation-adjacent pronouns are not changed. The augmented biographies are therefore not fully gender-balanced.
- Name replacement has no word boundaries. The seed first and last names are
replaced with a case-insensitive regex that does not use
\bname_swap.py name_swap.py. A short name can match inside a longer word. - Unpinned models. The ChatGPT API alias and the default sentiment checkpoint can change over time classifier.py. The shipped letters and evaluation CSVs are the reproducible artefacts.
- Significance claim is stronger than the table. The text says male documents are significantly higher on all three style aspects §4.3.5. Two cells in tab. 4 (ChatGPT formality, Alpaca positivity) have p = 0.07, which is significant only at the 0.10 level.
- Duplicate WEAT value. Alpaca's noun and adjective WEAT(CF) are both 0.419 in tab. 3. Check this against the source before reuse.
- Synthetic training data for agency. The agency classifier is trained on ChatGPT rephrasings and tested on a split of the same data app. F.2 agency_generation_util.py. The 90% accuracy is therefore not a measure of performance on real letters.
- Parse depends on undocumented JSON. The agency generator parses output by
looking for
{"agentic":and"communal":generate_dataset.py. The prompt does not request JSON, so the parse can fail silently. - Odds-ratio threshold. Minimum counts of 10 (CLI) or 20 (function default) decide which words appear in the top lists biases_lexical_content.py. The paper does not state the value it used.
- Scripts as shipped. Several command-line declarations pass a bare
"if"name next to an option string classifier.py hallucination_detection.py, anddf = df[[cols]]indexes with a nested list hallucination_detection.py. I read these and did not run the scripts. - Binary gender only. The paper restricts analysis to the two genders in WikiBias and says so [Limitations].
Lineage
- Builds on: WikiBias (Sun and Peng, 2021) for the biography data and the odds ratio method §4.3.1 §3.2.1. Not compiled in this wiki.
- Builds on: NLI-based hallucination detection, following Maynez et al. (2020) and Laban et al. (2022) §2 §3.3. Not compiled in this wiki.
- Compared against: the paper compares ChatGPT with Alpaca, Vicuna and StableLM for letter success rates app. B, but no competing bias method.
- Superseded by: not known from the wiki.
Source and license
Adapted from “"Kelly is a Warm Person, Joseph is a Role Model": Gender Biases in LLM-Generated Reference Letters” by Yixin Wan, George Pu, Jiao Sun, Aparna Garimella, Kai-Wei Chang, Nanyun Peng (arXiv:2310.09219), used under CC0 1.0. Changes: condensed and restructured for implementation, with tables, equations and algorithms taken from the paper and its code, each cited to its source. Not affiliated with or endorsed by the authors. Code citations refer to https://github.com/uclanlp/biases-llm-reference-letters at commit 1264990e5f55 (MIT). Any quoted code is shown under that license.