Module 3: The Eval as a Fitness Function
6. The eval in CI: a gate on the deploy
Overview
By the end of this lesson you'll see the quality gate take its final step: stop depending on someone remembering to run it and become an automatic pipeline step that blocks the deploy when the score drops. In lessons 4 and 5 you ran the eval by hand and decided by hand. That works until the first Friday in a hurry, when someone deploys without running the eval and a regression reaches production. The solution is the same one classic software found decades ago for tests: put them in continuous integration (CI) so they run on their own on every change, and make a failure stop the delivery. You're going to execute a simulated CI step that runs the eval, produces an exit code —0 if it passes, 1 if it fails— and lets the deploy continue or blocks it according to that code. Two pull requests pass through the pipeline: one improves the prompt (exit 0 → MERGE) and another drops the model and regresses (exit 1 → BLOCK). The eval stops being an optional best practice and becomes a barrier the pipeline enforces, just as a red test blocks a merge.
This matters because a gate that depends on human discipline isn't a gate —it's a recommendation—. You can have the best eval-set in the world, but if running it is a manual step someone can skip, it will be skipped the day it matters most: under pressure, with a deadline looming, "just this once." The only way for a quality rule to be reliable is for the system to enforce it without asking permission or depending on anyone's memory. Putting the eval into CI does exactly that: it turns "we should run the eval before deploying" into "you can't deploy without the eval passing." It's the same jump that made unit tests reliable —from "run the tests before pushing" to "CI doesn't merge if the tests fail"—, applied to probabilistic quality. And it's what closes the module's arc: the eval started as a score (lesson 3), became a gate (lesson 4), learned to catch regressions (lesson 5), and here it's installed as a permanent guardian of the deploy.
Connection with the module: this lesson automates what the previous ones built by hand. Lesson 4's gate and lesson 5's comparison against a baseline are exactly what runs inside the CI step —there's no new mechanism, there's a new position: the gate goes from "something you run" to "something the pipeline runs for you, always"—. Here a boundary is also marked: the mechanics of assembling a serious CI/CD pipeline (runners, stages, environments, rollback) are from the delivery guides; in this lesson the pipeline is the minimum needed to show the eval as the step that governs the deploy. Lesson 7, the last topical one, opens the box of the success criterion. In one sentence: here the gate becomes inevitable.
The analogy: the subway turnstile
Think of the subway entry turnstile. To get to the platform, you have to tap your card; if it has valid balance, the barrier opens; if not, it stays closed and you don't pass. Notice three things. First: there's no human deciding case by case who passes —the turnstile applies the rule on its own, with the same yardstick for everyone, without getting tired or making exceptions for a hurry—. Second: it's on the path, not to the side —to reach the platform you have to pass through it, you can't go around it—. Third: its verdict is binary and executes a physical action —it opens or it doesn't—, it's not a suggestion you can ignore. A turnstile doesn't tell you "it would be good if you had balance"; it simply doesn't let you through without it.
The eval in CI is the quality turnstile on the path to the deploy. The pipeline is the corridor to the platform: every change has to travel it to reach production. The eval step is the turnstile: it runs the eval-set, and if the score passes the threshold, the barrier opens (the deploy continues); if not, it stays closed (the deploy is blocked). Like the turnstile, there's no human deciding at the moment —the rule is written in the threshold and the pipeline applies it on its own—; it's on the path —you can't deploy going around the eval—; and its verdict executes an action —the deploy proceeds or stops—, it's not an optional notice. Developers already know this turnstile in another form: the unit test that goes red and doesn't let the pull request merge. The eval in CI is that same turnstile, with a probabilistic yardstick —a score against a threshold instead of an exact assert— but the same role: an automatic barrier on the path, that can't be skipped.
Worked example: two PRs against the turnstile
We're going to simulate the eval step inside a CI pipeline. The step runs the eval-set, prints what a CI log would print, and returns an exit code —CI's universal convention: 0 means success (the pipeline continues) and anything other than 0 means failure (the pipeline stops)—. We pass two pull requests. PR #841 improves the agent's prompt (score 0.90). PR #842 drops it to a cheaper model to save and, unintentionally, regresses (score 0.60). The threshold is 0.80.
First, where the turnstile lives in the pipeline:
commit --> build --> unit tests --> [ EVAL GATE ] --> deploy
|
score < threshold?
/ \
no (0) yes (1)
| |
continues BLOCKED
The eval gate is another step in the pipeline, between the tests and the deploy. If the score passes the threshold, the pipeline continues toward the deploy; if not, it stops there, and the deploy never happens.
# Lesson 06 — the eval in CI: a gate on the deploy
# Everything SIMULATED. Zero network, zero API, zero keys. Deterministic output.
# (Reuses EVAL_SET, GOLD, POOR_ANSWER, make_agent, run_eval from lessons 3-5.)
EVAL_SET = [
{"id": "q1", "question": "where is my order", "must_contain": "tracking"},
{"id": "q2", "question": "how do i return a product", "must_contain": "return"},
{"id": "q3", "question": "how long does shipping take", "must_contain": "3 to 5 days"},
{"id": "q4", "question": "can i pay in installments", "must_contain": "installments"},
{"id": "q5", "question": "the product arrived broken", "must_contain": "refund"},
{"id": "q6", "question": "how do i change my address", "must_contain": "profile"},
{"id": "q7", "question": "i didn't get my invoice", "must_contain": "email"},
{"id": "q8", "question": "i want to cancel my order", "must_contain": "cancel"},
{"id": "q9", "question": "the coupon doesn't work", "must_contain": "expiration"},
{"id": "q10", "question": "how do i contact a seller", "must_contain": "messages"},
]
GOLD = {
"q1": "You can see the tracking of your order in your profile.",
"q2": "For a return, go to your order and click Return.",
"q3": "Standard shipping takes 3 to 5 days.",
"q4": "Yes, you can pay in installments with no interest by card.",
"q5": "We're sorry about that; you can request a refund from the order.",
"q6": "Change your address in the Profile, Addresses section.",
"q7": "We'll resend the invoice to your account email.",
"q8": "You can cancel the order if it hasn't shipped yet.",
"q9": "Check the coupon's expiration; it may have already expired.",
"q10": "Message the seller from the Messages section.",
}
POOR_ANSWER = "Sorry, I don't have information about that."
def make_agent(competent_ids):
def agent(question, case_id):
return GOLD[case_id] if case_id in competent_ids else POOR_ANSWER
return agent
def run_eval(agent):
passed = sum(
case["must_contain"] in agent(case["question"], case["id"]).lower()
for case in EVAL_SET
)
return passed / len(EVAL_SET)
# --- THE EVAL STEP IN CI: runs the eval and returns an exit code (0 ok, 1 fail) ---
def ci_eval_step(agent, threshold):
total = len(EVAL_SET)
score = run_eval(agent)
passed = score >= threshold
print(f" [ci] running eval-set ({total} cases)...")
print(f" [ci] score = {score:.2f} threshold = {threshold:.2f}")
if passed:
print(f" [ci] EVAL PASSED -> deploy continues")
return 0 # exit 0: the pipeline continues
else:
print(f" [ci] EVAL FAILED -> deploy BLOCKED (like a red test)")
return 1 # exit 1: the pipeline stops
THRESHOLD = 0.80
ALL_IDS = {c["id"] for c in EVAL_SET}
print("PR #841: 'improve the support agent prompt'")
code_a = ci_eval_step(make_agent(ALL_IDS - {"q9"}), THRESHOLD) # 0.90
print(f" exit code = {code_a}\n")
print("PR #842: 'drop the model to save cost'")
code_b = ci_eval_step(make_agent({"q1","q2","q3","q4","q6","q8"}), THRESHOLD) # 0.60
print(f" exit code = {code_b}")
print(f"\nCI summary: PR #841 {'MERGE' if code_a==0 else 'BLOCK'} | "
f"PR #842 {'MERGE' if code_b==0 else 'BLOCK'}")
What to expect. When you run it:
PR #841: 'improve the support agent prompt'
[ci] running eval-set (10 cases)...
[ci] score = 0.90 threshold = 0.80
[ci] EVAL PASSED -> deploy continues
exit code = 0
PR #842: 'drop the model to save cost'
[ci] running eval-set (10 cases)...
[ci] score = 0.60 threshold = 0.80
[ci] EVAL FAILED -> deploy BLOCKED (like a red test)
exit code = 1
CI summary: PR #841 MERGE | PR #842 BLOCK
Here's the turnstile on the path to the deploy. Read it by PR.
PR #841 passes: exit code 0, MERGE. Someone improved the prompt. The eval step runs on its own inside the pipeline —the developer didn't invoke it by hand, CI triggered it when the PR was opened—, measures a score of 0.90, sees that it passes the threshold, and returns exit code 0. In CI, exit 0 means "this step succeeded," so the pipeline continues toward the deploy: the PR is merged. The barrier opened because the card had balance.
PR #842 fails: exit code 1, BLOCK. Someone dropped the agent to a cheaper model to save. The eval step runs —again, on its own—, measures a score of 0.60, sees that it does not pass the threshold, and returns exit code 1. In CI, any exit other than 0 means "this step failed," and a failed step stops the pipeline: the deploy is blocked, the PR isn't merged. And here's the force of putting it in CI: nobody decided to block it. There was no meeting, no reviewer who noticed, no luck of someone remembering to run the eval. The pipeline applied the rule on its own, with the same yardstick with which it let #841 through, and stopped the regression before it touched a single customer. The turnstile stayed closed because the card had no balance, no matter how much of a hurry whoever tried to pass was in.
The summary says it all: PR #841 MERGE | PR #842 BLOCK. Two changes, same pipeline, same threshold, opposite verdicts, zero human intervention at the moment of decision. This is what makes the gate reliable: it doesn't depend on anyone's discipline. PR #842's regression —exactly the cheap-model change from lesson 5— would have reached production in a manual process the day someone was in a hurry; in CI, it's impossible for it to reach production, because the deploy doesn't happen without the eval passing. AI quality stopped being a good intention and became a property the system guarantees.
Going deeper: the exit code, the parallel with tests, and the boundary
Why the exit code is the interface. Every CI in the world works with the same primitive convention: each pipeline step is a command that ends with an exit code, and 0 means success while any other number means failure. A step that returns something other than 0 stops the pipeline. This convention is what makes the eval fit without ceremony into any CI: you don't need a special plugin or an exotic integration —you wrap the eval in a script that returns 0 if the score passes the threshold and 1 if not, and CI already knows what to do with that—. It's exactly how unit tests are integrated (the runner returns 0 if all pass, 1 if any fails). The eval isn't a second-class citizen in the pipeline: it speaks the same language as the tests, the exit code, and that's why the pipeline treats it the same —a step that can stop the deploy—.
The exact parallel with the red test (and the only difference). A developer already has the right mental model for this, just applied to tests. When you write code and break a unit test, CI goes red and doesn't let you merge until you fix it. Nobody argues whether "the red test is just a suggestion"; it's a hard barrier. The eval in CI is identical in role: it goes red (exit 1) when the quality falls below the threshold and doesn't let you deploy. The only difference is the nature of the test. A unit test is deterministic and exact: it verifies a condition that's true or false without ambiguity (assert sum(2,2) == 4). The eval is statistical and probabilistic: it verifies that an aggregate score is above a threshold (0.90 >= 0.80). But to the pipeline, the two are the same —a step that returns 0 or 1—. This equivalence is the heart of the module: the eval gives a probabilistic component the same kind of automated safety net a test gives to deterministic code.
An honest nuance: the eval is slower and "noisier" than a test. It's worth naming two practical differences with a unit test, because they affect how the eval is put into CI. First, it's slower: running an eval-set can take seconds or minutes (in a real system, many calls to the model), versus the milliseconds of a unit test —that's why the eval sometimes runs in a separate stage, not on every commit—. Second, with a real model it can be noisy: since the component is non-deterministic, the score can vary a bit between runs of the same code, so the threshold must have some margin so as not to block over statistical noise. (In our simulations the stub is deterministic, so there's no noise; in production, this is handled with sufficiently large eval-sets and thresholds with room to spare —and how to do it well is eval design, AI Engineering—.) These differences don't change the eval's role as a gate; they only nuance how it's operated in a real pipeline.
The boundary: here the eval as a step, not the pipeline in depth. You'll have noticed that this lesson's "pipeline" is minimal: a five-box diagram and a function that returns an exit code. It's deliberate. Assembling a production CI/CD pipeline —choosing the tool, configuring runners, handling staging and production environments, orchestrating the rollback if something goes wrong, managing secrets, parallelizing stages— is a whole topic, and it's from the delivery and infrastructure guides. What this lesson teaches is where the quality gate fits into that pipeline —a step between the tests and the deploy, that can stop it— and why its interface (the exit code) makes it fit into any CI. The pipeline's mechanics are from another guide; the eval as the step that governs the deploy is from this one.
Common mistakes
Leaving the eval as a manual step (a process mistake). What happens: the team has a good eval-set and the habit of running it "before deploying," but it's a manual step. It works for a while, until the day in a hurry when someone deploys without running it, a regression reaches production, and the investigation reveals that "we forgot to run the eval." Why it happens: a manual step depends on memory and discipline, which fail exactly under pressure —when it matters most—. How to spot it: if running the eval before a deploy depends on someone remembering, it's not a gate, it's a recommendation. How to fix it: put it into CI as a step that returns an exit code and that the pipeline enforces —just like unit tests—; make it impossible to deploy without it passing.
Putting the eval in CI but as a notice, not a block (a configuration mistake). What happens: the team adds the eval to the pipeline, but configured to notify without stopping the deploy —it prints a yellow warning and continues—. Over time, the warnings become background noise nobody reads, and regressions pass anyway, now with a log that announced them and that nobody looked at. Why it happens: configuring the eval as blocking generates friction (it sometimes stops deploys), and it's tempting to leave it as "informational" to avoid complaints. How to spot it: if your eval step has never stopped a deploy because it's in notice mode, it's decorative. How to fix it: make the exit code stop the pipeline when the score falls below the threshold —a turnstile that notifies but always opens isn't a turnstile—.
A threshold so strict (or an eval so noisy) that it blocks everything (a calibration mistake). What happens: the team sets the threshold too high, or the eval-set is so small that the score varies a lot between runs, and the gate goes red constantly over good changes or over pure statistical noise. Frustrated by the unfair blocks, they end up disabling the gate —and are left with no protection—. Why it happens: a gate that gives false alarms is worse than annoying; it erodes trust until someone turns it off. How to spot it: if your eval gate blocks deploys that were actually fine, or its verdict changes without the code changing, it's badly calibrated. How to fix it: calibrate the threshold with some margin and use a sufficiently large eval-set so the score is stable (this is eval design, AI Engineering); a reliable gate blocks the real regressions and lets the good changes through, with no false alarms that condemn it to being turned off.
Exercises
Exercise 1 — Read the exit code. A pipeline has the eval as a step before the deploy, with a 0.85 threshold. For each run, say the exit code the eval step returns and whether the deploy proceeds or is blocked. (a) score 0.91. (b) score 0.85. (c) score 0.79.
See solution
With the rule passed = score >= 0.85, and exit 0 if it passes, 1 if it fails:
- (a) 0.91: 0.91 ≥ 0.85 → passed → exit 0, the deploy proceeds. The barrier opens.
- (b) 0.85: 0.85 ≥ 0.85 → passed (by the
>=convention) → exit 0, the deploy proceeds. Right at the threshold, it passes. - (c) 0.79: 0.79 < 0.85 → not passed → exit 1, the deploy is blocked. The pipeline stops at the eval step; the deploy never happens.
The moral: the exit code is the interface between the eval and the pipeline. 0 opens the turnstile, any other number closes it. The pipeline doesn't need to understand scores or thresholds —it just looks at the exit code and acts—, which is exactly why the eval fits into any CI without ceremony.
Exercise 2 — Notice versus block. Two teams put the eval in CI. Team A configures it to block the deploy if the score falls below the threshold (exit 1 stops the pipeline). Team B configures it to notify (it prints a warning but the deploy always continues). Both have the same eval-set and the same threshold. A month later, which team is more likely to have deployed a regression, and why does the configuration matter as much as having the eval?
See solution
Team B is much more likely to have deployed a regression, despite having exactly the same eval-set and threshold as A. The difference isn't in the eval's quality, but in what the pipeline does with its result. In team A, a score below the threshold stops the deploy: the regression is impossible to deploy, the pipeline doesn't allow it. In team B, a score below the threshold only prints a warning and the deploy continues anyway: the regression is deployed, with a yellow log that announced it and that —like all warnings that don't block— nobody read.
Why the configuration matters as much as having the eval: an eval that measures but doesn't act is a thermometer, not a gate (lesson 4). Team B has the measurement but not the barrier; it's like having a turnstile that records whether you have balance but always lets you through —the record is useless if it doesn't change what happens—. The lesson: putting the eval in CI isn't enough; it has to be configured to block, not just notify. A warning that stops nothing becomes background noise. The only useful version of the eval in CI is the one that can say "no" and enforce it.
Exercise 3 — From the eval as a gate or the CI/CD mechanics? For each task, say whether it's from this module (the eval as the step that governs the deploy) or from the boundary (CI/CD mechanics, another guide), and why. (a) Wrapping the eval in a script that returns exit 1 if the score falls below the threshold. (b) Configuring the pipeline's runners and staging and production environments. (c) Placing the eval step between the tests and the deploy so it can stop it. (d) Designing the automatic rollback strategy if the deploy fails in production.
See solution
- (a) Wrap the eval in a script with an exit code → this module. It's the interface between the eval and the pipeline: how the score becomes a deploy decision (0 or 1). The essence of this lesson.
- (b) Configure runners and environments → boundary (CI/CD, another guide). It's pipeline mechanics: the infrastructure the steps run on. It's not about the eval, it's about the CI. Outside this module.
- (c) Place the eval between tests and deploy so it can stop it → this module. It's the architectural position of the gate: where the gate lives to govern the deploy. From this lesson.
- (d) Design the automatic rollback → boundary (CI/CD, another guide). It's a delivery technique (what to do if a deploy goes wrong) independent of the eval. Outside this module.
The rule that separates: if the task is about the eval as the step that decides the deploy —its exit code, its position in the flow— (a, c), it's from here; if it's about the pipeline's infrastructure —runners, environments, rollback— (b, d), it's from the delivery guides. This module puts the gate into the pipeline; it doesn't build the pipeline.
Summary and next step
In this lesson the quality gate took its final step: it stopped depending on human discipline and became an automatic pipeline step that blocks the deploy when the score drops. With the subway-turnstile analogy you saw the three properties that make it reliable: it applies the rule on its own (with no human deciding at the moment), it's on the path (it can't be gone around), and its verdict executes an action (it opens or blocks, it's not a notice). You executed a CI eval step that returns an exit code —CI's universal interface— and saw two PRs get opposite verdicts with no human intervention: #841 improved the prompt (exit 0 → MERGE) and #842 dropped the model and regressed (exit 1 → BLOCK), the same regression from lesson 5, now impossible to deploy. You understood the exact parallel with the red test that doesn't let you merge —same barrier, probabilistic yardstick instead of exact— and the boundary: here the eval as the step that governs the deploy, not the pipeline's mechanics (another guide).
Before moving on you should be able to: explain why a manual gate isn't reliable and what it gains from being in CI; describe how the exit code connects the eval with any pipeline; distinguish configuring the eval to block from leaving it as a notice; and separate the eval as a gate from the CI/CD mechanics.
What follows is opening the box we took for granted until now: the success criterion. Throughout the module the criterion was contains —does the response contain the key phrase?—, the simplest one. But there's more than one type, and the choice has architectural consequences. In lesson 7 you're going to execute four criteria over the same cases —exact-match (too strict), contains (binary), LLM-as-judge (with partial credit), and the statistical threshold— and you're going to run into the module's most important warning: the LLM-as-judge is another AI component, with its own latency, cost, and non-determinism, so judging with it recurses all the properties of this guide. It's the step from "I know how to use the eval as a gate" to "I know which type of criterion feeds that gate and what each one governs."
Resources
- martinfowler.com — "Emerging Patterns in Building GenAI Applications", Bharani Subramaniam and Martin Fowler — the pattern of evals integrated into the delivery pipeline as an automatic quality gate, in parallel with the tests; this lesson's architecture frame.
- Anthropic — Claude docs, evaluations in the development flow (conceptual) — the guide to running evaluations in an automated, repeatable way as part of the development cycle, not by hand; the backing for taking the eval to CI, without fixing a version.
- Chip Huyen — AI Engineering (O'Reilly), evaluation and operation chapters — the treatment of evaluation as a continuous, automated practice, with the practical differences (latency, noise) versus classic tests; the reference for operating evals in a real pipeline.