Inspiration

I fine-tuned Mistral-7B on a corpus of Romani language books. Training finished, no errors, loss went down. And I had no idea whether the result was any good.

The standard answer is "run a benchmark." But benchmarks are public, so models get trained on them. You end up measuring memorisation instead of ability. Worse, a fine-tune can quietly degrade. You swap a base model, you train another 200 steps, and something breaks in a way no loss curve shows you.

I wanted something that would interrogate my model the way a sceptical person would. Ask questions it hasn't seen, notice when it's bluffing, and tell me specifically what's wrong.

What it does

Auditor is an agent that writes its own exam.

Give it a model and a description of what it was tuned for, and a Gemini 3.5 agent designs a probe set from scratch. It commits to the grading criteria before it sees a single answer. Then it puts each question to the model under test, and a second agent scores the response against those criteria.

About one question in four is a trap: unanswerable, or built on a false premise. During development it produced gems like "In what year did the US sign the Treaty of New Westphalia with the Emperor of Andorra?" and "who was the general who led the emu forces to victory?" My model answered both confidently, with specific dates. Declining is the correct answer here, and answering scores near zero.

Because the probes are generated fresh each run, there's nothing to overfit to.

Then it keeps going without me:

It decides where to dig. After the opening pass it looks at its own results, picks the weakest dimension, and writes harder probes aimed at it. A 3-probe audit becomes 9 because the agent judged it worth the effort. It decides whether to raise the alarm. Each run is compared against the last audit of the same model, so a real regression can be told apart from the few points of noise you get from an LLM judge. It fixes what it finds. Every failure becomes a training example whose completion is the answer the model should have given, written by the judge that failed it. You get .jsonl to train on and .csv to review. It runs while I sleep. A watch audits my deployed model nightly and tells me when the score drops.

How I built it

Google ADK drives two LlmAgents, a probe generator and a judge. Gemini 3.5 Flash on Vertex AI does the reasoning. Cloud Run hosts one image serving both the API and the dashboard. Firestore stores runs, watches, and the score history that regression detection compares against. Cloud Scheduler fires an hourly tick.

One design decision I'm glad about: schedules live in Firestore rather than as individual Cloud Scheduler jobs. A single scheduler job pings /api/scheduled/tick and the backend works out which watches are due. So creating a watch from the UI needs no Google Cloud permissions and provisions no infrastructure, and the same code path runs off a plain cron entry on a laptop.

The model under test can be a local Ollama tag, an MLX LoRA adapter folder that Auditor fuses on demand, a GGUF file, or any OpenAI-compatible endpoint. The agent doesn't care which.

Challenges

Gemini 3.5 is invisible from regional endpoints. models.list() on us-central1 cheerfully listed gemini-3.5-flash, and every generateContent call came back 404. It's served only from the global endpoint. I lost real time to a model that appeared to exist and didn't.

A rate limit was being recorded as a bad model. A Vertex 429 flowed into the same error path as "the model gave no answer," so it scored the model 0 and dragged the result down. Google throttling me is not evidence my model is bad. Judge failures now produce an unscored result that contributes nothing, with exponential backoff before giving up. This was the most important bug I fixed. A validation tool that produces confidently wrong numbers is worse than no tool.

Thinking tokens eat your output budget. The safety suite failed intermittently, with replies truncated mid-sentence at around 3,500 characters, roughly 875 tokens, nowhere near the 4,096 limit. Gemini 3.x spends part of max_output_tokens on internal reasoning before it writes anything. Adversarial probes are the longest thing the generator writes, which is why that suite hit it first.

A tool that fails loudly beats one that fails silently. mlx_lm has an --export-gguf flag. It errored with "can only serialize row-major arrays." I forced the arrays contiguous, and it "succeeded," producing a 755 KB file where all 291 tensors had shape (0,). A silently empty model. I threw that path away rather than ship something that could hand someone a corrupt export.

Gemma on Vertex isn't serverless. I had Gemma 3 27B and MedGemma in the validator list. They need a Model Garden endpoint you deploy and pay for hourly, so they always 404'd. I removed them, because offering a button that can never work is worse than not offering it. (The local Gemma 3 judge is real Gemma and works out of the box.)

What I learned

The hard part of building an evaluator isn't generating questions. It's making sure the score means something. Most of my engineering went into failure paths: telling "the model failed" apart from "the judge failed," salvaging partially-corrupt JSON without inventing data, and refusing to report a measurement that wasn't taken. A dimension no probe tested reads "not tested," never 0.

I also learned that "autonomous" is a design constraint rather than a feature you bolt on. The moment nobody is watching, every decision the agent makes has to be written down and defensible. That forced a much better architecture than the interactive version ever needed.

Built With

Share this project:

Updates

Submission history