A higher-education institution in Mexico runs conversational agents on Google Cloud for a student population of more than 700,000: admissions questions, enrollment, appointment scheduling, routing to the right human team, and everyday student support. I work on that system as a contractor — as the dominant author of the production agent, and as the engineer who built the machinery that decides whether a change to it is allowed to ship.
This write-up describes the second part, because it is the part that generalizes. Client-identifying detail — names, architecture, business rules — stays out; what follows is the method, which is mine to keep.
The problem with shipping a probabilistic system
An agent is a language model wrapped in prompts, tools, and business rules. Change any of them and behavior moves somewhere you did not intend. In ordinary software, a regression suite answers "did I break something?" — deterministic assertions over deterministic code. In an agent, the thing under test is a conversation: the same input can produce many acceptable phrasings and many subtly wrong ones, and the difference between those two sets is the product.
Most teams respond by demo-testing: run a few conversations, read them, feel good, ship. That works until the first change that silently breaks appointment logic three turns into a conversation nobody re-read. The alternative is to make evaluation a deploy gate — something that runs in the pipeline and can say no. But a gate needs a verdict, and a verdict needs to be stable: if the same change passes on Tuesday and fails on Wednesday without anything changing, engineers learn to ignore it, and then you have a slower demo test.
Getting to a stable verdict took four pieces of engineering.
Golden cases, written as code
The base unit is a golden case: a scripted scenario — a prospective student asking about a program, an enrolled student needing to reschedule, a conversation that must end in a handoff — with the expectations for each turn written down. Fifty-five of them, versioned in the repository next to the code they test, so a change to behavior and the change to its expectations arrive in the same pull request and get reviewed together.
Two properties matter more than the count. First, cases carry mocked state: the date, the student's situation, the catalog the agent should consult all come from the fixture, not from the world. Second, expectations distinguish what is checkable deterministically (did the agent call the scheduling tool? did it refuse to invent a price?) from what needs judgment (was the answer actually responsive, in the right register, correct about the program?). Conflating those two is the root of most flaky agent suites.
A judge you can trust to say no
The judgment half is scored by an LLM judge — a second model that reads the transcript and the case's expectations and scores the turn. Everyone builds this. Almost nobody calibrates it, and an uncalibrated judge is worse than none: it fails good changes, engineers learn to re-run until green, and the gate is now theater.
Calibration was case-by-case labor against hand-labeled turns: run the judge, compare with the human label, find the disagreement, and fix the rubric — not the model — until they agree. The failures that surfaced were specific and instructive:
- The judge lived in the wrong time. Cases mock their date, and the judge was reasoning against the real clock — failing correct answers about deadlines that were "in the past." The fix anchors the judge's temporal reasoning to the case's mocked today, not the wall clock.
- The judge hallucinated a defect. In one calibration round it reported markdown formatting that was not in the transcript. That class of check — formatting, exact figures, tool invocations — moved to a deterministic-only mode: a judge is never asked to score what a string comparison can decide.
- One verdict is an anecdote. Judged turns are scored with majority voting across runs, so a single unlucky sample cannot flip a release decision either way.
The result is a judge whose no means something — trusted enough that it blocks merges, which is the whole point.
Hermetic cases, honest pass rates
The suite's early pass rates were quietly wrong: cases shared user state through the agent's memory, so case B started with residue from case A. A suite like that reports whatever the accident of execution order produces — and it forbids parallelism, because order is load-bearing.
The fix was hermetic per-case users: every case runs as its own synthetic user with its own seeded state, visible to the judge so it scores against what the case actually contains. Once cases were genuinely independent, parallelism came free — first at the set level, then per case, opt-in — and the full regression run stopped being the slowest step of the pipeline.
Gates that differ by environment
The final piece is where evaluation runs. One depth everywhere is wrong in both directions — too slow for every integration merge, too shallow for a production promotion. So the gate depth follows the environment, under trunk-based promotion: a smoke set on integration, the full golden regression before QA promotes. Each environment answers the question appropriate to it, and the expensive answer is bought exactly where it pays.
Around the agent itself, the same discipline extends outward: a costs-and-promotions service whose answers are pinned by recorded parity fixtures — pricing responses recorded and replayed so a refactor cannot silently change a number a student sees — and a bug-tracking app for the human QA team, with per-test matrices, so machine evaluation and human testing are two views over the same behavior instead of two parallel worlds.
The stack underneath
Agents on Google ADK, deployed through Agent Engine, with Vertex AI / Gemini as the model layer and its native online evaluators watching production alongside the offline harness. Services on Cloud Run, the estate managed with Terraform, pipelines on Azure DevOps, access under IAM with Secret Manager, telemetry to Cloud Monitoring with PII boundaries enforced before anything is exported. The harness itself is plain Python — 80+ test modules — because the machinery that judges a system should be more boring than the system it judges.
What this buys
The practical result is that a change to a system serving 700,000 students ships the way ordinary software ships: open a pull request, the gate runs, a stable verdict comes back, and no actually stops the line. Evaluation stopped being a phase after development and became the definition of done.
The general lesson I keep from it: a probabilistic system becomes shippable when the things around it are deterministic. The model may sample; the gate may not. Every piece of this — calibrated judging, deterministic-only checks, hermetic state, environment-scaled depth — is one instance of moving a decision from the model's discretion to the harness's.