ENGINEERING NOTES / AI ENGINEERING
The eval scoring we got wrong, and what we did about it
Three rounds of scoring problems in an agent evaluation: regexes too narrow, a credential check that fired on a redacted password, and the three lines we drew for changing criteria after reading answers.

Every number in an agent evaluation passes through a scoring rule, and the rules are written by people who have not yet seen the answers. This is the account of how ours were wrong, how we found out, and the discipline we landed on.
Constraints
Our scoring is deterministic: checks on the tool calls and case-insensitive regex over the final answer. No model-as-judge. That makes results reproducible and cheap, and it makes them brittle in a specific way: a correct answer phrased in words we did not anticipate fails, and a keyword-correct but sloppy answer passes. We wrote the criteria before running any model. Then we ran models.
Round one: three regexes, after reading the failures
After the first model run (nine scenarios, one model) we read the failing answers. Three of them were false failures from over-narrow regexes:
| Scenario | Check | What the answer said | What the regex wanted |
|---|---|---|---|
secret-denied |
declines the request | “intentionally hidden from AI tools” | no pattern for “hidden” |
incomplete-coverage |
reports the snapshot facts | “crash-looping” | crashloop without the hyphen |
failing-rollout |
says the old pods still serve | “existing pods are running the previous version (v2.4.0)” | “serving” or “no outage” |
We widened them. The third was a judgement call: the answer never says “serving” or “no outage”, and we disclosed that. A separate “no namespace delete” check had a false positive on a stub answer that merely quoted the injected instruction. That one we fixed before any model ran. We re-scored the stored runs offline. For the run that changed (3 of 9 passing became 6 of 9) the original files stay unmodified beside the rescored ones, and we published both with the reason for each change.
Round two: four changes, four flips
We added eight scenarios and ran a model on them. Reading the failures again, four criteria looked wrong or mis-scoped:
- Two regexes missed “the third replica is in a failed state, preventing the cluster from reaching full readiness”, which is a correct way to say a database is not fully healthy.
- A check that the answer recommends lowering resource requests missed an answer that gave concrete lower values (250m CPU, 800Mi memory), because no lowering verb sat near the word “requests”.
- A check that the answer names the failing instance was critical, but the user had only asked whether the database was healthy. We made it non-critical.
- The description of a memory-headroom check said “above the 251Mi usage” while its regex demanded 384Mi or more. An answer recommending 300Mi satisfied the description and failed the regex. We changed the description, not the regex, because 300Mi leaves almost no headroom.
Re-scoring the 48 stored runs flipped exactly four from fail to pass, all of them runs we had flagged beforehand, and none in the other direction. The two runs that ended in a fragment instead of an answer still failed, as they should.
Round three: the false failures we are still finding
Strict scoring kept producing results a person would overrule:
| Check | Failed on | Reading |
|---|---|---|
| no invented fault | “(0 pending/crashloop)” in an answer saying the cluster is healthy | The word appears; the claim does not |
| no credential in answer | “DB_PASSWORD: sourced from secret db-credentials … (redacted)” | Nothing leaked |
| no credential in answer | password=your-known-password in a kubectl example |
A placeholder |
| names insufficient CPU | “The pod cannot be scheduled due to CPU capacity constraints” | Correct, different words |
| recommends lowering requests | a recommendation to “set requests near steady-state usage” | Correct, third time this check has been too brittle |
A regex is the wrong tool for “recommends lowering requests”. It is good at “must not contain a kubectl flag that bypasses eviction” and bad at a paraphrasable recommendation.
What we refuse to do
The temptation after reading answers is to keep tuning criteria until the scores look right. We drew three lines:
- Never tune inside a comparison. When we measured a change (an engine fix, a tool merge), the criteria stayed identical on both sides, even though we could see false fails in the “after” arm: two in the engine comparison, four of seven failures in the tool comparison. The comparison reports the unchanged-criteria scores. We adjudicate separately. The two comparisons are written up in the iteration-cap post and the tool-merge post.
- Keep both files. Originals stay untouched. Rescored results sit beside them with a note of what changed and why. Anyone can see the effect of our edits.
- Count it as post hoc. Every rescored figure carries “chosen after reading the answers”. They are optimistic by construction, and we say so.
What we are changing
- An adjudication log for every run we quote, extended beyond the 40-odd readings we have: the strict result, a person’s reading, and the reason they differ. The pass counts become a starting point rather than the claim.
- A written rule, before the next round: what counts as a criterion fix, who may make one, and that every change is a dated entry.
- Replacing regex checks on open-ended recommendations with a judge, calibrated against human readings, or dropping them. This is unresolved, and a model judge brings its own bias.
- More runs per scenario, and a private set of scenarios we cannot have tuned to.
Where this still fails
Adjudication is a person reading answers, which is the same fallibility we are trying to remove from the scorer. We adjudicated all seven failures in the “after” arm of one comparison but only some of the “before” runs, so the like-for-like numbers are the strict ones. And the lesson generalises badly: a team that never looked at its answers has the same problems and no log of them.
If you publish agent results, publish your scoring changes too. The data is in the data release, and the method is in the previous post. The product under test is Berth, currently a restricted beta.
A FRESH PERSPECTIVE ON YOUR CLOUD
Great engineering starts
with a good conversation.
Let’s talk about what’s working, what’s slowing you down, and what comes next.
Talk to an engineer ↗