Solving tips
- The judge is a model, so treat its output as untrusted: clamp the score into [0, 1] and never let a malformed or crashing judge take down the run. A bad grade is still a data point.
- Separate the two numbers reviewers care about: the mean score tells you overall quality, the flagged list tells you which specific cases to go look at. One without the other is not actionable.
- Be ready to explain why you'd use an LLM judge at all (open-ended outputs a substring check can't grade) and its failure modes (position/verbosity/self bias, and that it needs an explicit rubric to be reproducible).
A substring check can tell you whether an agent said “Paris,” but it cannot tell you whether a summary is faithful, whether an explanation is helpful, or whether a rewrite kept the original meaning. Those are open-ended judgments, and the practical way to score them at scale is to ask another model to grade the output against a rubric. That grader is the LLM judge, and this exercise is the harness that runs it over a batch and turns its opinions into numbers you can track.
Why an LLM judge
Rule-based checks are cheap, deterministic, and perfect when correctness is a predicate. They fall apart on free-form text where many different strings are all “good.” An LLM judge fills that gap: given the prompt and the output, it returns a graded score and a short reason. The cost is that the judge is itself a model, with all the fallibility that implies, so the harness has to defend against it rather than trust it.
How a case works
A case is a dict with a prompt (what the agent was asked) and an output (what it produced). The harness packs those into a pair dict and hands it to judge, which returns {"score": <0..1>, "reason": <str>}. You record the clamped score and its reason, decide whether the score falls below threshold, and move on. Because judge is passed in, swapping in a deterministic stub makes the whole harness testable without a network.
Task
Complete run_judge_eval(judge, cases, threshold=0.5):
- Iterate the cases in order. For each, resolve its
name(default"case_<i>"using the zero-based index). - Build
pair = {"prompt": case["prompt"], "output": case["output"]}and calljudge(pair). - Read the score and clamp it into
[0.0, 1.0]as a float; keep thereason(default""). Ifjudgeraises or the score is not numeric, the case scores0.0,errorholds the exception text, andreasonrecords it too. - A case is
flaggedwhen its (clamped) score is strictly less thanthreshold. - Collect one result dict per case:
{"name", "score", "reason", "flagged", "error"}(errorisNoneon success). - Return the summary with
results,mean_score(mean of scores, or0.0when empty),flagged(the names below threshold, in order), andtotal.
Example
def judge(pair):
# toy judge: full marks when the gold answer appears in the output
gold = {"2+2": "4", "cap": "Paris"}
want = gold.get(pair["prompt"], "")
if want and want in pair["output"]:
return {"score": 1.0, "reason": "contains the expected answer"}
return {"score": 0.0, "reason": "missing the expected answer"}
cases = [
{"name": "math", "prompt": "2+2", "output": "the answer is 4"},
{"name": "geo", "prompt": "cap", "output": "the capital is Paris"},
{"name": "bad", "prompt": "2+2", "output": "i don't know"},
]
summary = run_judge_eval(judge, cases, threshold=0.5)
round(summary["mean_score"], 3) # -> 0.667
summary["flagged"] # -> ["bad"]
[r["score"] for r in summary["results"]] # -> [1.0, 1.0, 0.0]
summary["results"][2]["flagged"] # -> True
Constraints
- One case whose judge crashes or returns garbage must not stop the run; it scores
0.0and every case still gets a result. - Clamp every score into
[0.0, 1.0]; a judge that returns1.4or-0.2must not skew the mean. - Preserve case order in both
resultsandflagged. - Do not call any real LLM, network, or clock —
judgeis provided and is the only grader.