Lesson 22 / 22
Bias-Reducing Prompts
Measuring how much of a subgroup gap prompt balancing closes and what it leaves: on 200 fictional tickets the central hit rate is 0.9016, the rural hit rate 0.5513 and the gap 0.3504. A bias-reducing notice in the prompt moves the gap to 0.3586 and adds 3800 tokens; a balanced example brings it down to 0.2888 and adds 14400 tokens. The hit rate of the sampled local spelling rises from 0.4848 to 0.8788, while the unsampled one stays fixed at 0.3023. The prompt closes only a 0.1758 share of the gap. The course wrap-up gathers the previous prompt, changed prompt and cost rows of twenty-two lessons.
Published Updated
In this lesson
- Traces the source of a subgroup gap down to the cue table of the model and shows it with numbers.
- Measures the bias-reducing notice and the balanced example on the same gap and separates what each closes.
- Shows with numbers, through the unsampled spelling, why a prompt does not solve data bias.
- Gathers the course's twenty-two lessons in one table and names the lessons whose difference stays below the two-run difference.
Contents
The previous lesson split the errors into two sources. What went unasked was who the errors were concentrated on: the overall hit rate can be acceptable while all the remaining wrong answers accumulate in one set of subscribers; for that set the system does not work, and the average hides it.
Subgroup, gap and fairness measures were established in the Model Evaluation, Interpretability and Ethics course and are not repeated. This lesson’s question is narrow: how much of a subgroup gap can a prompt close, and what part remains.
- OC41. The code is a simulator; no real endpoint is called. Subscriber groups, ticket spellings and the cue table are fictional, seed 20260218.
- OC42. There are two fictional subscriber groups, central and rural; the group name never enters the decision and serves only measurement. Each topic has one standard and two local spellings; central subscribers mostly write the standard one, rural subscribers mostly the local ones.
- OC43. The source of the bias is in the data: the cue table was built only from the standard spelling, and when no cue is found the decision falls to the most frequent class.
- OC44. The first balancer is a notice written into the prompt (the spelling does not affect the decision), which in the simulator turns the fall into an equal draw. The second balancer is the balanced example: each topic’s local A spelling goes into the prompt with its label, and the simulator extracts a temporary cue table from the examples. Local B is not sampled.
- OC45. The measures are the topic hit rate per group and the gap (central minus rural, small is good). Cost is written as input tokens; the second run has seed 20260219.
The Source of the Gap
The gap does not come from the model knowing something about the groups; the group name is never in the prompt. Its source is which texts built the cue table.
# SIMULATOR -- FICTIONAL. No real model endpoint is called. Subscriber groups, # ticket spellings and the cue table are FICTIONAL. SEED, M32 = 20260218, 0xFFFFFFFF def rng(t): x = ((t ^ (t >> 16)) * 2246822507) & M32 x = ((x ^ (x >> 13)) * 3266489909) & M32 s = [(x ^ (x >> 16)) & M32] def next_value(): s[0] = (s[0] * 1664525 + 1013904223) & M32 return s[0] / 4294967296 return next_value def pick(u, w): t = 0.0 for i, x in enumerate(w): t += x if u < t: return i return len(w) - 1 def tokenize(text): # FICTIONAL tokenizer, a piece has at most five characters out = [] for piece in text.lower().replace(",", " , ").split(): while len(piece) > 5: out.append(piece[:5]) piece = piece[5:] out.append(piece) return out def trace_id(text): h = 2166136261 for c in text: h = ((h ^ ord(c)) * 16777619) & M32 return h # each topic: one standard and two local spellings; the local ones are FICTIONAL SPELLINGS = { "fatura": ["bu ayki fatura tutari gecen aya gore cok yuksek geldi", "bu ayki hesap pusulasi baya kabarik cikmis", "gecen ayki odemeye gore rakam iki misli olmus"], "ariza": ["evde su akmiyor bir ariza var galiba", "muslugu actim damla bile gelmiyor", "avludaki boru patlamis her yer golet"], "sayac": ["sayac okumasi yanlis girilmis endeks tutmuyor", "saatin rakamlari bizimkiyle uyusmuyor", "kutunun icindeki numara yerinde saymis"], "tarife": ["hangi tarife basamagindayim ogrenmek istiyorum", "bizden hangi ucretlendirmeyle para aliniyor", "kademeli hesaplama nasil yapiliyor anlamadim"], "kesinti": ["mahallede kesinti var ne zaman gelecek", "iki gundur su yok kimse haber vermedi", "koyun ustunde bir gundur akmiyor diyorlar"], } TOPICS = list(SPELLINGS) # central mostly write the standard spelling, rural mostly the local ones SPELLING_SHARE = {"merkez": [0.86, 0.08, 0.06], "kirsal": [0.24, 0.40, 0.36]} TICKETS = [] for i in range(200): r = rng(SEED + i) group = "merkez" if r() < 0.62 else "kirsal" topic = TOPICS[pick(r(), [0.24, 0.22, 0.18, 0.20, 0.16])] j = pick(r(), SPELLING_SHARE[group]) TICKETS.append({"id": 4001 + i, "topic": topic, "group": group, "spelling": j, "text": SPELLINGS[topic][j]}) # the cue table was built ONLY from the standard spelling: this is the data bias CUES = {} for k in TOPICS: for t in tokenize(SPELLINGS[k][0]): CUES.setdefault(t, k) MOST_COMMON = "fatura" # with no evidence it falls to the most frequent class COUNTER = [0] def example_table(toks): """Temporary cue table from the 'example: <text> -> <topic>' pieces.""" table, buffer = {}, [] for i, t in enumerate(toks): if t == "->" and i + 1 < len(toks) and toks[i + 1] in TOPICS: for s in buffer: table.setdefault(s, toks[i + 1]) buffer = [] elif t == "ornek": buffer = [] else: buffer.append(t) return table def call(prompt, ticket, seed=SEED): """Topic assignment. Without evidence it falls to the most frequent class; the bias-reducing notice turns this fall into an equal draw.""" COUNTER[0] += 1 toks = tokenize(prompt) table = dict(CUES) table.update(example_table(toks)) # examples in the prompt extend the table p = {k: 0.0 for k in TOPICS} for t in tokenize(ticket["text"]): if table.get(t) in p: p[table[t]] += 1.0 if max(p.values()) == 0.0: # no cue at all if "esit" in toks: # bias-reducing notice return TOPICS[pick(rng((seed + 5) ^ trace_id(prompt))(), [0.2] * 5)] return MOST_COMMON return max(sorted(p), key=lambda a: p[a]) NOTICE = ("Talebin yazim bicimi karari etkilemez ; her yazim esit olcutle " "degerlendirilir . ") EXAMPLES = " ".join(f"ornek: {SPELLINGS[k][1]} -> {k} ." for k in TOPICS) + " " print(f"FICTIONAL tickets {len(TICKETS)}: central {sum(x['group'] == 'merkez' for x in TICKETS)}, " f"rural {sum(x['group'] == 'kirsal' for x in TICKETS)}") local_count = {g: sum(x["group"] == g and x["spelling"] > 0 for x in TICKETS) for g in ("merkez", "kirsal")} print(f"tickets with local spelling: central {local_count['merkez']}, " f"rural {local_count['kirsal']}") print(f"cue table {len(CUES)} entries, all from the standard spelling") for j, name in enumerate(("standard", "local A", "local B")): x = next(t for t in TICKETS if t["topic"] == "sayac" and t["spelling"] == j) print(f" {name:<9} {x['text'][:42]:<43} -> {call('Talep: ' + x['text'], x)}")
FICTIONAL tickets 200: central 122, rural 78 tickets with local spelling: central 18, rural 58 cue table 49 entries, all from the standard spelling standard sayac okumasi yanlis girilmis endeks tutmu -> sayac local A saatin rakamlari bizimkiyle uyusmuyor -> fatura local B kutunun icindeki numara yerinde saymis -> fatura
Three spellings of one topic give three results. The standard spelling is read correctly;
both local spellings come out as fatura (invoice), because no cue is found in the table and the decision
falls to the most frequent class. 58 of the rural tickets are local, only 18 of the central ones; the
error accumulates on rural subscribers. The model does not distinguish them — it only
fails to recognize a text with no counterpart in its own table, and who wrote that text is a fact
from the data.
What the Prompt Closes and What It Does Not
SETUPS = [("baseline", ""), ("+ notice", NOTICE), ("+ balanced example", EXAMPLES), ("+ both", NOTICE + EXAMPLES)] def measure(prefix, seed=SEED): COUNTER[0] = 0 by_group = {"merkez": [0, 0], "kirsal": [0, 0]} by_spelling = [[0, 0], [0, 0], [0, 0]] g = 0 for x in TICKETS: prompt = prefix + f"Talep {x['id']} : {x['text']}" topic = call(prompt, x, seed) by_group[x["group"]][0] += topic == x["topic"] by_group[x["group"]][1] += 1 by_spelling[x["spelling"]][0] += topic == x["topic"] by_spelling[x["spelling"]][1] += 1 g += len(tokenize(prompt)) m, ru = by_group["merkez"][0] / by_group["merkez"][1], by_group["kirsal"][0] / by_group["kirsal"][1] return m, ru, m - ru, [a / b for a, b in by_spelling], g print("setup central rural gap gap B localA localB input") for name, prefix in SETUPS: m, ru, a, y, g = measure(prefix) ab = measure(prefix, SEED + 1)[2] print(f"{name:<18} {m:>7.4f} {ru:>7.4f} {a:>8.4f} {ab:>10.4f} {y[1]:>7.4f} " f"{y[2]:>7.4f} {g:>6}") t, e = measure("")[2], measure(NOTICE + EXAMPLES)[2] print(f"\nbaseline gap {t:.4f}, closed by the prompt {t - e:.4f} " f"({(t - e) / t:.4f} share), remaining {e:.4f}")
setup central rural gap gap B localA localB input baseline 0.9016 0.5513 0.3504 0.3504 0.4848 0.3023 2624 + notice 0.9098 0.5513 0.3586 0.3457 0.4848 0.3256 6424 + balanced example 0.9426 0.6538 0.2888 0.2888 0.8788 0.3023 17024 + both 0.9426 0.6538 0.2888 0.2760 0.8788 0.3023 20824 baseline gap 0.3504, closed by the prompt 0.0616 (0.1758 share), remaining 0.2888
The second row best fits intuition and sits emptiest in the table. Writing “her yazim esit olcutle degerlendirilir” (every spelling is judged by an equal standard) into the prompt leaves the rural hit rate at 0.5513 and raises the gap to 0.3586; with a two-run difference of 0.0129 for the row, even this worsening of 0.0082 counts as unmeasured. The only thing measured is the cost: input tokens from 2624 to 6424. A declaration of intent adds no cue to the model.
The third row carries a real gain: the balanced example brings the gap down to 0.2888, a gain of 0.0616 against a two-run difference of 0.0000. Its cost is 14400 tokens, 6.5 times the baseline. That the examples also raise the central hit rate (from 0.9016 to 0.9426) is why the gap closes by less than the rural gain.
The last two columns explain the remainder. The hit rate of the sampled local A spelling rises from 0.4848 to 0.8788; the unsampled local B spelling does not move from 0.3023. A prompt does not solve data bias. Only a 0.1758 share of the baseline gap closed; the rest closes not with a better sentence but by building the cue table from local spellings too.
Summary
- A subgroup gap comes not from the model recognizing the group but from which texts the cue table was built from; at baseline central is 0.9016, rural is 0.5513 and the gap is 0.3504.
- The bias-reducing notice moves the gap to 0.3586 and adds 3800 tokens; with a two-run difference of 0.0129, this change counts as unmeasured.
- The balanced example brings the gap down to 0.2888 and adds 14400 tokens; the 0.0616 gain is measured, but the examples also raise the central hit rate, so the gap closes by less than the rural gain.
- The hit rate of the sampled local spelling rises from 0.4848 to 0.8788 and the unsampled spelling stays fixed at 0.3023: the prompt closes a 0.1758 share of the gap, and the remaining 0.2888 is data bias.
Course Wrap-Up
The accounting of twenty-two lessons is in a single table, and every number was read from its own lesson. The rows share no unit; the second and third columns state by which measure each row is read. The difference uses a single sign convention: the changed prompt’s number minus the previous prompt’s number, in parentheses in the third column.
| Lesson | Previous prompt | Changed prompt | Cost |
|---|---|---|---|
| Design 01 What Is a Prompt | context-free prompt 0.7083 | full prompt 0.7833 (+0.0750) | +1797 tokens; two-run difference 0.2000 → 0.0583 |
| Design 02 System Instruction | prompt without instruction 0.4583 | medium instruction 0.7500 (+0.2917) | +2871 tokens; long instruction +3487 tokens (−0.0083) |
| Design 03 Specifying Role and Behavior | baseline prompt 0.7500 | role prefix 0.6833 (−0.0667) | +1080 to +3240 tokens; seed band 0.1500, all four prefixes unmeasurable |
| Design 04 Clear and Measurable Requests | ambiguous request, 4.81 distinct responses | closed set 3.89 (−0.92) | +15 tokens per request; unlabeled share 0.4333 → 0.2889 |
| Design 05 Format and Length Control | free response, parseable 0.0000 | format notice 0.9750 (+0.9750) | input 6140 → 8780, output 1392 → 2026 |
| Design 06 Using Examples | zero examples 0.8571 | three examples 0.9643 (+0.1071) | +4928 tokens, +44 per request |
| Design 07 Stating Constraints and Prohibitions | prompt without notice, violation 0.2583 | positive notice 0.0167 (−0.2416) | +3360 tokens, 115.9 per prevented violation; a negative constraint makes the violation 0.5333 |
| Design 08 Prompt Iteration | starting prompt, test 0.3778 | fifth iteration 0.9333 (+0.5555) | 2640 calls; falls to 0.8111 at the eighth |
| Reasoning 01 Chain of Thought | direct answer, calculation 0.3000 | intermediate step prompt 0.8833 (+0.5833) | tokens 1.82 times; unit cost 243.2 → 150.1 |
| Reasoning 02 Self-Consistency | single sample 0.7667 | nine samples 0.8833 (+0.1167) | calls 60 → 540, tokens 7954 → 71211, per correct answer 172.9 → 1343.6 |
| Reasoning 03 Tree of Thoughts | solution in a single prompt 0.4792 | branch 3 tree 0.6250 (+0.1458) | calls 48 → 795, tokens 43182; branch 4 (−0.0208) |
| Reasoning 04 Step-Back Prompting | single-stage prompt 0.5417 | two-stage prompt 0.7500 (+0.2083) | calls 48 → 96, tokens 1.66 times, per correct code 198 → 237 |
| Reasoning 05 Reason and Act | action space prompt, valid field 0.1000 | prompt with shortened thought 0.8000 (+0.7000) | input 5300 → 8060; per valid field 645.8 → 102.1 tokens |
| Reasoning 06 Reasoning Models | single-pass class, two-field task | reasoning class (+0.1000 and +0.1667) | tokens 1.51 times, per correct answer 83.9 → 110.1 |
| Reasoning 07 Prompt Ensemble | best single prompt 0.7833 | majority vote of three prompts 0.8000 (+0.0167) | calls 120 → 360, per correct label 61.1 → 180.7 tokens |
| Reasoning 08 Automatic Prompt Optimization | single candidate, test 0.6750 | selected from ten candidates 0.6500 (−0.0250) | calls 40 → 400; validation +0.1250, optimism share 0.2500 |
| Output 01 Sampling Parameters | baseline setting 0.6417 | all three narrowed together 0.8667 (+0.2250) | input 5300 fixed, output 191 → 204, calls 120 → 120; run difference 0.5667 → 0.0083 |
| Output 02 Penalties and Stop Sequences | plain prompt, repetition 0.3431 | penalty and stop sequence 0.0578 (−0.2853) | output 4073 → 2653, input fixed; usability 0.8833 → 0.8333 |
| Output 03 Structured Output | format description, conformance 0.8000 | imposed schema 1.0000 (+0.2000) | input 5614 → 2974 (−2640 tokens), output 1789 → 1810 |
| Output 04 Output Validation | schema and business rule, acceptance 0.6000 | one retry 0.8167 (+0.2167) | +48 calls, +2947 tokens, 1.85 calls per recovery |
| Output 05 Prompt and Context Engineering Difference | plain prompt, both correct 0.4083 | rule and code table 0.8667 (+0.4584) | input 1534 → 17734; instruction fixes 10 errors with 1680 tokens, information fixes 66 with 14520 |
| Output 06 Bias-Reducing Prompts | plain prompt, gap 0.3504 | notice and balanced example 0.2888 (−0.0616) | input 2624 → 20824; unsampled spelling fixed at 0.3023 |
This is the rule of the course: a prompt is a design object; a prompt changed without measurement is not an improvement but a guess. In no row did the model change: the same simulator, seed and set of fictional tickets. Only the text changed, and the fourth column states twenty-two times that every change of it has a cost.
The second claim of the course also reads from the same table, by name: reasoning techniques are expensive and do not pay off on every task. In Reasoning 01 chain of thought raised tokens 1.81 times in classification and left the difference inside the band; in Reasoning 04 the step-back prompt raised them 2.69 times on the grade task without moving the hit rate; in Reasoning 06 the reasoning class raised them 1.75 times on the single-field task and left the difference inside the band of 0.1167; in Reasoning 07 the prompt ensemble tripled calls and gained +0.0167; in Reasoning 08 the search budget grew tenfold and the test fell by 0.0250. The same reading holds outside reasoning: in Design 02 the long instruction added 3487 tokens and lost 0.0083, in Design 06 going from the third to the eighth example gained 0.0089 for 8736 tokens, in Output 03 the example-bearing description produced a difference equal to the two-run difference for 2040 tokens, and in Output 04 the second retry stays below that difference at 1366 tokens.
The third number stands alone: in three of the twenty-two lessons, the difference in the table stayed below the two-run difference of the same lesson — Design 03 (role specification), Reasoning 07 (prompt ensemble) and Reasoning 08 (automatic prompt optimization). In all three the cost column is full and the gain column empty.
Over twenty-two lessons the prompt became a design object and the cost of every change was written down. But one assumption was never questioned: that every call happens alone, immediately and without error. The number of calls was counted as a cost item; what happens to these prompts where calls travel over a network, queue up, are cut off midway, hit a rate limit and spend tokens instead of money was not asked. A prompt ensemble of three hundred sixty calls and a prompt search of two thousand six hundred forty calls were measured in a sequential loop on a single machine. The next course, Building Applications with Model APIs, opens this assumption.
To keep your progress and take notes, Log in
My notes
Log in to take notes.