Exact-match scoring breaks the moment your model outputs free text, reordered JSON, or answers that are right but phrased differently. The choice of LLM-as-a-judge vs exact-match scoring is fundamentally about whether you can enumerate correctness, not about which tool is newer. Pick wrong and you either ship a brittle test suite or burn tokens judging trivial parses.
1. Map correctness to an output contract
Before writing any eval, write down the contract. If the task is “extract the invoice total from a PDF”, the correct answer is a float. If the task is “summarize this thread”, correctness is a spectrum.
Closed-form tasks have a finite, verifiable output space. Open-form tasks have infinite acceptable surfaces. This mapping decides your scoring method. Don’t skip this step—most eval failures trace back to an undefined contract, not a bad model.
2. Use exact-match when the space is closed
Exact-match (or its cousins: normalized string equality, JSON schema validation, set overlap, numeric tolerance) is cheap, deterministic, and debuggable. Use it whenever you can write a parser that confirms the answer without reading meaning.
Variants that still count as exact
- Strict string equality after lowercasing and whitespace collapse for classification labels.
- JSON schema validation with type checks and required keys.
- Set equality for tag lists or extracted entity sets.
- Numeric tolerance (
abs(a-b) < 1e-6) for computed values. - Regex capture when the structure is fixed but formatting varies.
Code: a strict gate
import json
def exact_match(expected: dict, actual: str) -> bool:
try:
parsed = json.loads(actual)
except json.JSONDecodeError:
return False
if parsed.get("total") != expected["total"]:
return False
if set(parsed.get("line_items", [])) != set(expected["line_items"]):
return False
return True
This runs in microseconds and needs no model. If it passes, you have structural correctness. It does not tell you if the summary is good, but it tells you the pipeline didn’t emit garbage.
The trap: exact-match is too strict for semantically equivalent outputs. “The cat sat on the mat.” vs “On the mat sat the cat.” both correct, but a string diff fails. That mismatch is your signal to reach for a judge.
3. Deploy LLM-as-a-judge when semantics are the product
The moment “correct” includes phrasing, tone, completeness, or reasoning, exact-match is blind. LLM-as-a-judge vs exact-match scoring becomes a question of whether you can tolerate a probabilistic scorer that approximates human preference.
A judge is just another LLM call with a tight prompt and structured output. Treat it like a microservice: define input, output schema, and failure modes.
Designing the judge prompt
Keep the judge prompt isolated from the generation prompt. Give it the rubric, the reference (if any), and the candidate. Ask for a score and a one-line reason. Avoid vague instructions like “rate quality”. Use anchored scales.
from openai import OpenAI
import json
client = OpenAI(base_url="https://api.n4n.ai/v1", api_key="YOUR_KEY")
def judge_summary(article: str, summary: str) -> dict:
resp = client.chat.completions.create(
model="anthropic/claude-3.5-sonnet",
messages=[
{"role": "system", "content": "You are a strict eval judge. Score summary fidelity 1-5 where 5 means all key facts present and no hallucinations. Reply JSON: {score:int, reason:str}"},
{"role": "user", "content": f"ARTICLE:\n{article}\n\nSUMMARY:\n{summary}"}
],
response_format={"type": "json_object"}
)
return json.loads(resp.choices[0].message.content)
Pairwise judging
When you compare two candidates, rotate order to defeat position bias.
def judge_pair(a: str, b: str, prompt: str) -> str:
resp = client.chat.completions.create(
model="openai/gpt-4o-mini",
messages=[
{"role": "system", "content": "Pick the better response A or B. Reply JSON: {winner:str, reason:str}"},
{"role": "user", "content": f"PROMPT:\n{prompt}\n\nA:\n{a}\n\nB:\n{b}"}
],
response_format={"type": "json_object"}
)
return json.loads(resp.choices[0].message.content)["winner"]
4. Hybrid: exact-match as a gate, judge as a sampler
Don’t choose one globally. In a real pipeline, run exact-match first. If it fails, you already know the output is structurally wrong—no need to pay for a judge. If it passes, sample a subset for semantic judgment to catch “technically valid but garbage” outputs.
import random
def evaluate(expected, actual, article=None):
if not exact_match(expected, actual):
return {"pass": False, "stage": "exact"}
if article and random.random() < 0.1: # judge 10% of passes
j = judge_summary(article, actual)
return {"pass": j["score"] >= 4, "stage": "judge", "score": j["score"]}
return {"pass": True, "stage": "exact"}
This cuts judge cost by 90% on well-formed pipelines while preserving semantic signal. The LLM-as-a-judge vs exact-match scoring debate resolves to layering: exact-match is the filter, judge is the spotlight.
5. Common pitfalls and tradeoffs
Judge bias is real
LLMs favor verbose, confident text. A longer summary often scores higher even if less accurate. Mitigate by anchoring the rubric to the reference and penalizing extraneous detail. In pairwise mode, always randomize A/B placement.
Self-enhancement bias
A judge model tends to rate outputs from its own family higher. If you generate with Claude and judge with Claude, expect inflated scores. Cross-model judging (generate with one, judge with another) is cheaper insurance than human calibration studies.
Cost and latency
A judge call is a full inference. At 10M generations/day, even a 1% judge sample is 100K extra calls. Exact-match is essentially free CPU. Measure your token burn before committing, and put judge calls behind a sampling flag from day one.
Calibration drift
Model judges shift with version upgrades. Pin the judge model or re-baseline your rubric quarterly. Log judge scores alongside model IDs and prompt hashes so you can attribute a score drop to a judge change vs a generation regression.
Absence of ground truth
Exact-match has a ground truth by construction. A judge produces a proxy. Never report judge scores as “accuracy” to stakeholders; call them “preference estimates”. If you need hard numbers, keep a human-labeled gold set and report judge agreement rate against it.
6. Routing and reliability
If you run judges in production, the judge model itself can be rate-limited. A gateway that honors client routing directives and forwards provider cache-control hints avoids silent eval gaps. For example, set x-routing: prefer:anthropic/claude-3.5-sonnet, fallback:openai/gpt-4o and the gateway handles degradation. n4n.ai exposes one OpenAI-compatible endpoint addressing 240+ models with automatic fallback when a provider is rate-limited or degraded, which keeps eval jobs from stalling.
Per-token usage metering lets you attribute judge cost separately from generation cost—critical for debugging budget overruns when a prompt change accidentally doubles judge calls.
7. Actionable checklist
- Write the output contract. If you can parse it, use exact-match.
- For open-ended tasks, prototype a judge with a JSON schema and a narrow rubric.
- Gate with exact-match; sample-judge the passes at 5–10%.
- Log judge model version, score distribution, and token cost per stage.
- Rotate judge models behind a routing directive to avoid vendor lock and self-bias.
- Keep a 200-example human-labeled set to measure judge agreement monthly.
LLM-as-a-judge vs exact-match scoring isn’t a religion. It’s a layered defense: deterministic gates where you can, probabilistic eyes where you must.