Contents
Fail quietly, test loudly: a four-layer stack, real gates, and a rollback you'll actually use
You tweaked one sentence in a prompt on Friday afternoon. By Monday, your support bot told a customer to "reboot the company." Nobody reviewed the change, and no test caught it.
I shipped almost the same blunder last year. A harmless-looking prompt edit broke our JSON output, and the downstream parser crashed for three hours. That day convinced me that CI/CD for LLM applications isn't optional.
This guide shows you what to test, how to wire those tests into your pipeline, and how to keep the whole thing cheap and stable. It's the CI-gates component of the LLMOps production guide — and the eval-before-promote rule the prompt versioning guide treats as the difference between "versioned" and "safe."
Why LLM Apps Break Differently
Traditional software fails loudly. A function returns the wrong value, a unit test goes red, and you fix it. LLM apps fail quietly, because the code runs fine while the answers get worse.
Four things cause most of the damage:
- Prompt edits: a single reworded instruction can change tone, format, or accuracy.
- Model updates: providers retire and refresh models, and behavior shifts with them.
- Retrieval changes: a new chunking strategy or index changes what context the model sees.
- Non-determinism: the same input can produce different outputs on different runs.
So how do you test something that never answers the same way twice? You stop testing for exact strings and start testing for properties and quality thresholds — the same cultural shift the LLMOps vs MLOps comparison calls the biggest one in the field.
What CI/CD Means for an LLM App
The core idea stays the same: every change triggers automated checks, and only changes that pass reach production. The twist is that "change" covers far more than code.
Version Everything That Shapes Output
Treat each of these like source code. Keep them in git, review them in pull requests, and run tests when they change:
- Prompts and system messages.
- Model name and parameters (temperature, max tokens, and so on).
- Retrieval settings (chunk size, top-k, embedding model).
- Tool and function schemas.
- Evaluation datasets and scoring rules.
IMO, prompts living in a database with no history is the single most common mistake I see. You can't roll back what you never versioned.
The Four-Layer Test Stack
Think of LLM testing as a pyramid. Cheap, fast checks sit at the bottom and run on every commit. Expensive, subjective checks sit at the top and run less often.
Layer 1: Deterministic Checks
These tests need no judgment, so they run fast and never flake. Start here — they catch an embarrassing share of real bugs:
- Format validation: parse the JSON, check the schema, confirm required fields exist.
- Constraint checks: verify length limits, language, and banned phrases.
- Tool-call checks: confirm the model calls the right function with valid arguments.
- Refusal checks: confirm the bot declines the requests it should decline.
import json
def test_cancellation_routes_to_the_right_tool():
out = call_app("I want to cancel my subscription")
call = json.loads(out) # Layer 1: it parses at all
assert call["name"] in {"cancel_plan", "downgrade_plan"}
assert set(call["args"]) <= {"account_id", "reason"} # no invented fields
Layer 2: Golden Dataset Evals
A golden dataset holds real inputs paired with the qualities a good answer must show. You run every prompt through your app and score the results. This layer tells you whether a change made things better or worse.
Layer 3: LLM-as-Judge
Some qualities resist simple rules, like helpfulness or tone. For those, a second model grades the output against a rubric. Use this layer for nuance, but never trust it blindly — the agent evaluation guide covers the validation discipline.
Layer 4: Safety and Adversarial Tests
This layer probes for prompt injection, data leaks, and harmful output. Run a fixed set of attack prompts every time, and add new ones whenever you discover a fresh exploit. Your users will test your guardrails anyway, so you might as well test them first.
Building a Golden Dataset That Works
Don't overthink it. Fifty good examples beat five hundred sloppy ones. Follow these steps:
- Pull real traffic. Sample actual user prompts from your logs, with personal data removed.
- Cover the edges. Include typos, vague questions, hostile prompts, and off-topic requests.
- Define "good" per example. Write a short checklist, like "mentions the refund window" or "cites the right policy."
- Label failures you've seen. Every production bug becomes a permanent test case.
- Review it quarterly. Your product changes, so your dataset should too.
On my last project, we started with 40 examples pulled from support tickets. Within two months, that set caught three regressions that would have reached customers.
Figure 1: The suite runs on every pull request — so the Friday-afternoon edit can't reach Monday
LLM-as-Judge: Useful, With Caveats
Judge models scale beautifully. They also carry biases, and you need to respect them. Ever notice how a model tends to love its own writing?
Protect yourself with these habits:
- Write a specific rubric. "Score 1-5 for helpfulness" invites noise, while "Does the answer state the refund window?" invites consistency.
- Use a different model as judge than the one you test, when you can.
- Spot-check by hand. Compare judge scores with human ratings on a sample each month.
- Prefer pass/fail questions over fine-grained scales. They flake less.
FYI: a judge that agrees with your humans 90% of the time already beats most manual review processes.
Wiring Tests Into Your Pipeline
You don't need exotic infrastructure. Your existing CI system handles this fine. A minimal GitHub Actions sketch:
name: llm-evals
on: [pull_request]
jobs:
evals:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest tests/deterministic
- run: python evals/run.py --suite smoke --min-pass-rate 0.95
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
Set Gates, Not Vibes
A test suite without a pass bar is just a report nobody reads. Define explicit gates so the pipeline blocks bad changes automatically:
- Hard gate: all deterministic checks must pass.
- Quality gate: the eval pass rate must stay at or above your baseline, minus a small tolerance.
- Safety gate: zero failures on your critical adversarial prompts.
- Cost gate: average tokens per request must stay inside your budget.
Taming Flakiness and Cost
Evals call real models, so they cost money and sometimes disagree with themselves. Both problems have practical fixes:
- Lower the temperature for test runs to reduce randomness.
- Run a smoke suite on every pull request and the full suite nightly or before release.
- Repeat borderline cases three times and use the majority result.
- Compare against a baseline score, not a perfect score, so small noise doesn't block you.
- Cache test calls when the prompt and settings haven't changed.
The goal isn't zero noise. The goal is a signal you trust enough to act on — the repeat-runs math in the cost guide shows exactly how much a single noisy run can lie.
Deploy Safely: Canary, Shadow, and Rollback
Pre-deploy tests catch most problems, but not all. Real users ask things your dataset never imagined. So ship carefully.
- Shadow mode: send live traffic to the new version, but show users only the old answers. Compare the two offline.
- Canary release: route 5% of traffic to the new version and watch your metrics before you widen the rollout.
- Instant rollback: keep the previous prompt and config one click away. You will use it.
Feed Production Back Into Your Tests
Your pipeline gets smarter when production teaches it. Log failures, thumbs-down ratings, and escalations. Then turn the best examples into new test cases every week.
This loop matters more than any tool you pick. Each bug you catch once becomes a bug you never ship twice.
Tools Worth a Look
You can build all of this with plain scripts, but several tools save time:
- Promptfoo: open-source, config-driven evals that fit neatly into CI.
- DeepEval and Ragas: Python libraries with ready-made metrics, including ones for retrieval quality.
- LangSmith and Braintrust: hosted platforms for datasets, experiments, and tracing.
- Plain pytest: perfectly fine for deterministic checks and small teams.
Start with whatever matches your stack, and check each project's current docs before you commit.
Mistakes I Made So You Don't Have To
I earned these lessons the hard way:
- Testing only the happy path. My first dataset held polite, well-formed questions. Real users typed in fragments and ALL CAPS.
- Ignoring model version pinning. An alias silently pointed to a new model, and our scores shifted overnight. Pin exact versions.
- Chasing a perfect score. I wasted a week tuning prompts to a dataset of 30 examples. Overfitting works on prompts too.
- Skipping cost tracking. A "better" prompt doubled our token usage. Nobody noticed for a month.
Common Pitfalls
- Shipping prompt edits with no suite behind them — the Friday-afternoon edit is a when, not an if.
- One layer of tests — deterministic checks alone miss quality drift; judges alone miss schema breaks.
- Gates defined as vibes — if the pipeline can't block it, it isn't a gate.
- A golden dataset frozen on day one — production failures are free test cases; feed them in weekly.
- Trusting a judge you never validated — check agreement with humans monthly.
- Perfect-score chasing — overfitting works on prompts exactly like it works on models.
- Full suite on every PR — cost and flakiness kill the pipeline's credibility; smoke on PR, full nightly.
- No rollback rehearsed — the one-click restore only works if someone has clicked it in a drill.
Best Book to Read
| Cover | Book | Description | Get it |
|---|---|---|---|
![]() |
Continuous Delivery | the discipline this article transplants to LLM apps: pipelines, gates, and deployments that are routine, fast, and boring. | View on Amazon |
![]() |
Designing Machine Learning Systems | evaluation-first delivery for ML: regression tests, staged rollouts, and the feedback loop from production. | View on Amazon |
![]() |
AI Engineering | where prompt evaluation, judges, and safety testing become an engineering practice. | View on Amazon |
![]() |
Building LLM Powered Applications | the hands-on companion: gates, guardrails, and shipping LLM features without Friday incidents. | View on Amazon |
As an Amazon Associate I earn from qualifying purchases. Links on this page are affiliate links, and I may earn a commission at no extra cost to you.
Unlock AI That Actually Works
Get lifetime access to GPT-6 Astra, Claude Fable 5.1, Gemini 3.5, Grok 4.5, and more — all in one platform. Build websites, apps, videos, content, and digital products from a single command. No monthly fees. No tool-hopping.
Click here to get GPTAstra Max now — one-time payment, lifetime access.
Frequently Asked Questions
Why do LLM apps need different tests than normal software?
Because they fail quietly. Traditional software fails loudly: a function returns the wrong value, a unit test goes red, you fix it. LLM apps run fine while the answers get worse. Four causes do most of the damage — prompt edits (one reworded instruction changes tone, format, or accuracy), model updates (providers retire and refresh models underneath you), retrieval changes (new chunking or index changes what context the model sees), and non-determinism (the same input can produce different outputs). The fix is a mindset shift: stop testing for exact strings, start testing for properties and quality thresholds.
What should an LLM CI pipeline test?
A four-layer stack, cheap to expensive. Layer 1, deterministic checks that never flake: JSON parses, schema matches, required fields exist, length limits, banned phrases, right tool called with valid arguments, refusals where required. Layer 2, golden-dataset evals: real inputs with defined qualities, scored on every prompt change, telling you better or worse. Layer 3, LLM-as-judge for qualities that resist rules (helpfulness, tone), validated against human ratings. Layer 4, safety and adversarial tests: a fixed set of injection, leak, and harmful-output probes run every time. Then gates: all deterministic checks pass, eval pass rate at or above baseline minus tolerance, zero failures on critical adversarial prompts, and tokens per request inside budget.
How do you build a golden dataset for LLM evals?
Fifty good examples beat five hundred sloppy ones. Pull real traffic from your logs with personal data removed; cover the edges (typos, vague questions, hostile prompts, off-topic requests); define "good" per example with a short checklist ("mentions the refund window"); turn every production bug into a permanent test case; and review the set quarterly as the product changes. Starting from 40 examples pulled from support tickets, one project's set caught three regressions within two months — each of which would have reached customers without it.
How do you deploy LLM changes safely?
Three patterns, layered. Shadow mode sends live traffic to the new version while users still see old answers, so you compare offline. A canary release routes ~5% of traffic to the new version while you watch metrics before widening. And instant rollback keeps the previous prompt and config one click away — you will use it. Tame the noise that would sabotage all three: lower temperature for test runs, run a smoke suite per pull request and the full suite nightly, repeat borderline cases three times and take the majority, compare against a baseline rather than a perfect score, and cache test calls when prompt and settings haven't changed.
Wrapping This Up
LLM apps fail quietly, so you need tests that catch quiet failures. Version your prompts and configs, layer cheap checks under expensive ones, set hard gates, and deploy gradually. Then feed every production bug back into your dataset.
Start small. One schema check and ten golden examples already beat "it looked fine to me." Your future self, the one who sleeps through Friday-night deploys, will thank you.



