08/2026
agent-evaluator
A grader that never trusts the score the model reports
Solo — evaluation design, implementation, adversarial testing
Overview
This is the runnable implementation of an analysis I published at quocdungtlu.github.io/work/agent-evaluation/ — the blind-calibration run, injection test, oracle-poisoning test and repeatability finding, now executing end to end against the real Claude API instead of fixtures alone.
The essay's finding was uncomfortable: an LLM asked to grade the same criteria vector three times produced three different self-reported totals, once dropping a score without noticing. A grader that reports its own arithmetic is therefore not a grader. So src/normalizer.js parses the C1–C5 vector out of the response and recomputes the sum and verdict itself; a malformed or missing vector yields verdict: FAIL with schemaViolation: true rather than defaulting to a pass.
Security findings are kept in a separate flag set from the quality score instead of being blended into it, so an output that is technically correct can still be flagged and blocked — prompt injection, forged authority and downstream instructions do not get to average themselves away against good prose.
Key Features
- Deterministic normalizer: recomputes score and verdict from the criteria vector, never reads the model's self-reported total
- Fails closed — a malformed or missing vector is a FAIL with schemaViolation, not a silent pass
- Security flags are a separate channel from quality, so a well-written injected output is still blocked
- Corpus hashed with SHA-256 at load time, asserting the injection control is byte-identical to its base case — a provable control, not a visually similar one
- Five suites: calibration, injection, oracle poisoning, repeatability, and a trigger suite where a live target agent generates fresh output for grading
- Wilson confidence intervals printed next to every accuracy figure, because each suite runs a small n
Tech Stack
Folder Structure
agent-evaluator/ ├── src/ │ ├── normalizer.js # recomputes score + verdict; fails closed on a bad vector │ ├── grader.js # rubric prompt + Claude API call │ ├── corpus.js # SHA-256 hashing; asserts INJ_C is byte-identical to T1 │ ├── target.js # live target agent for the trigger suite │ ├── stats.js # Wilson confidence intervals │ └── suites/ # calibration · injection · oracle · repeatability · trigger └── fixtures/ # fixed text with known oracles, not live model output
Challenges & Solutions
Problem · The grader is an LLM, so its self-reported total is exactly as unreliable as the output it is judging — measured at three different totals across three identical runs.
Solution · Never read the reported number. The normalizer extracts the C1–C5 vector and recomputes the sum and verdict deterministically, treating a malformed or absent vector as a schema violation that fails closed.
Problem · An injection test is worthless if the control and treatment texts differ in any way other than the injection itself.
Solution · The corpus hashes every fixture with SHA-256 and asserts at load time that the control is byte-identical to its base case, so the control is provable rather than assumed.
Problem · A grader that cannot be made to fail has not been tested — it has only been used.
Solution · Two attacks aimed at the grader itself: a fake SYSTEM NOTE instructing it to return PASS, and an oracle-poisoning suite comparing blind, oracle-neutral and oracle-adversarial framings of the same case.
Lessons Learned
- An evaluator that cannot be failed is not an evaluator — build the attack on your own grader before trusting a single one of its numbers.
- Grading a corpus requires knowing the answer in advance, so fixtures must be fixed text; a freshly generated response gives you nothing to grade against.
- Separate the security verdict from the quality score, or a fluent injected output will average its way to a pass.
- Report confidence intervals with small-n results — a clean run is encouraging, not proof.
Future Improvements
- Grounding is closed-book: C1 is checked against the model's parametric knowledge with no retrieval step, so an obscure but real subject could be misjudged as fabricated
- The only positive control in the corpus is a very well-known book — the easy case, not the risky one
- The trigger suite has no oracle; it proves the pipeline closes end to end and its results should not be pooled with the fixed-corpus suites