Building a Continuous Evaluation Pipeline
Integrate LLM-as-judge evaluation into your CI/CD pipeline so every prompt or model change is automatically evaluated against a regression test suite before deployment.
Building a Continuous Evaluation Pipeline is a free AI Engineering Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Evaluation Must Be Continuous
One-time evaluation at deployment is not enough. LLM quality degrades silently: model providers update their models, system prompt changes slip in, retrieval quality shifts as the document corpus grows, and user query distribution changes over time. A continuous evaluation pipeline runs the same suite of evaluations on every change and on a schedule so quality regressions are caught within hours, not weeks.
Core Components of the Pipeline
A continuous evaluation pipeline has five components: a test dataset (curated questions with expected outputs), a system runner (calls your LLM pipeline for each test question), a judge (scores each response), a results store (database or time-series store for historical metrics), and a reporting layer (dashboards and alerts). Each component is independently upgradeable.
# Pipeline architecture:
#
# test_dataset.json
# |
# v
# system_runner.py --> calls your LLM pipeline
# |
# v
# judge.py --> scores each (question, answer) pair
# |
# v
# results_db --> stores timestamped metric history
# |
# v
# dashboard + alert --> Grafana / Slack notificationStructuring the Test Dataset
Store your evaluation test set as a versioned JSON or YAML file in your repository. Each entry contains a question, the category (factual, procedural, out-of-scope), and optionally a reference answer. Version the test set separately from the code — adding new test cases is a backward-compatible change, while removing cases may hide regressions. Aim for 200-500 cases across all relevant categories.
# eval/test_set_v3.json
# {
# 'version': '3.0',
# 'created': '2026-06-01',
# 'cases': [
# {
# 'id': 'faq_001',
# 'category': 'factual',
# 'question': 'What is the cancellation policy?',
# 'reference': 'Cancellations must be made 24 hours in advance.',
# 'min_correctness': 4
# },
# ...
# ]
# }Running the Evaluation Suite
The evaluation runner calls your production system (or a staging version) for each test case and records the response and metadata. Tag each evaluation run with a unique run ID, the commit SHA that triggered it, the timestamp, and the test set version. This makes it possible to compare runs exactly and diagnose which code change caused a regression.
import asyncio
import uuid
from datetime import datetime
async def run_eval_suite(system, test_set: list, commit_sha: str) -> dict:
run_id = str(uuid.uuid4())
results = []
for case in test_set:
response = await system.answer(case['question'])
score = await judge(case['question'], response, case.get('reference'))
results.append({
'run_id': run_id,
'commit_sha': commit_sha,
'case_id': case['id'],
'category': case['category'],
'response': response,
'score': score.model_dump(),
'evaluated_at': datetime.utcnow().isoformat()
})
return {'run_id': run_id, 'results': results}Storing and Querying Historical Metrics
Persist every eval run result to a database. A simple eval_results table with run_id, commit_sha, case_id, and score fields is sufficient. Query aggregate scores by run_id to compute per-run metrics. Compare the current run against the last successful run on main branch to detect regressions. A time-series database like InfluxDB works well for continuous monitoring.
-- PostgreSQL schema
CREATE TABLE eval_runs (
run_id UUID PRIMARY KEY,
commit_sha TEXT NOT NULL,
test_set_version TEXT NOT NULL,
triggered_by TEXT, -- 'ci', 'scheduled', 'manual'
started_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE eval_results (
id SERIAL PRIMARY KEY,
run_id UUID REFERENCES eval_runs(run_id),
case_id TEXT NOT NULL,
category TEXT,
correctness INT,
overall INT,
response TEXT
);
CREATE INDEX idx_run_id ON eval_results(run_id);Detecting Regressions Automatically
After each evaluation run, compare aggregate scores to the baseline (the last run from the main branch that was manually approved). A regression is defined as: any score dimension dropping more than 5% below baseline, or any category's mean score dropping below a hard minimum. Regressions should block deployment and alert the team. Improvements can be auto-approved.
def detect_regression(current: dict, baseline: dict, threshold_pct: float = 5.0) -> dict:
regressions = []
for metric in ['correctness', 'helpfulness', 'clarity']:
delta_pct = (current[metric] - baseline[metric]) / baseline[metric] * 100
if delta_pct < -threshold_pct:
regressions.append({
'metric': metric,
'baseline': baseline[metric],
'current': current[metric],
'delta_pct': round(delta_pct, 1)
})
return {'has_regression': len(regressions) > 0, 'regressions': regressions}Integrating with CI/CD
Add the evaluation suite as a CI step that runs on every pull request. The CI pipeline calls your staging system, runs the judge, stores results, and checks for regressions. If a regression is detected, the CI step fails and blocks the PR merge. Add this as a required status check in GitHub or GitLab so no one can bypass it. Keep the eval run time under 10 minutes by using a subset of 50-100 cases for PR checks.
# .github/workflows/eval.yml
# name: LLM Quality Evaluation
# on: [pull_request]
# jobs:
# eval:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Run eval suite
# run: python eval/run_suite.py --commit $GITHUB_SHA --mode pr
# env:
# OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# - name: Check for regressions
# run: python eval/check_regression.py --run-id $EVAL_RUN_IDScheduled Full Evaluation Runs
Beyond PR checks, run the full evaluation suite (all 300+ cases) on a daily schedule against production. This catches gradual quality drift that no single change introduces — for example, if the vector database's recall degrades as more documents are added, or if the LLM provider silently updates their model. Daily full runs give you a quality time series that makes trends visible.
# Separate eval modes:
EVAL_CONFIGS = {
'pr_check': {
'test_cases': 'eval/test_set_core_100.json',
'target': 'staging',
'max_runtime_min': 8
},
'nightly': {
'test_cases': 'eval/test_set_full_350.json',
'target': 'production',
'max_runtime_min': 30
},
'weekly_deep': {
'test_cases': 'eval/test_set_full_350.json',
'target': 'production',
'include_pairwise': True,
'max_runtime_min': 90
}
}Alerting on Quality Degradation
Configure alerts when metric trends cross warning and critical thresholds. A 3% drop in mean correctness over the past 7 days triggers a warning. A 10% drop in a single run triggers an immediate alert. Route warnings to a team Slack channel and critical alerts to PagerDuty. Include the regression analysis, a link to the eval dashboard, and the git blame for recent changes in every alert message.
import httpx
def send_regression_alert(regression_report: dict, webhook_url: str):
regressions = regression_report['regressions']
blocks = [{
'type': 'section',
'text': {'type': 'mrkdwn', 'text': '*LLM Quality Regression Detected*'}
}]
for r in regressions:
blocks.append({
'type': 'section',
'text': {'type': 'mrkdwn',
'text': f'*{r["metric"]}*: {r["baseline"]} -> {r["current"]} ({r["delta_pct"]}%)'}
})
httpx.post(webhook_url, json={'blocks': blocks})Managing Test Set Expansion
Continuously grow your test set based on real production failures. When a user reports a bad response, add that question (anonymized if needed) to the test set with a human-verified expected answer. This ensures your evaluation suite reflects actual user needs rather than hypothetical cases. Treat the test set as a living document and review it quarterly to remove stale cases.
def add_to_test_set(question: str, reference_answer: str, category: str,
source: str, test_set_path: str):
import json, uuid
with open(test_set_path, 'r') as f:
test_set = json.load(f)
test_set['cases'].append({
'id': f'user_report_{uuid.uuid4().hex[:8]}',
'category': category,
'question': question,
'reference': reference_answer,
'source': source, # 'user_report', 'regression', 'manual'
'added': '2026-06-21'
})
with open(test_set_path, 'w') as f:
json.dump(test_set, f, indent=2)Visualizing Quality Trends Over Time
Build a simple quality dashboard that plots your key metrics over time: mean correctness score, cache hit rate, p95 latency, and cost per query. Use a weekly moving average to smooth out noise from small sample sizes. A trend chart makes quality drift immediately visible — a gradual 3% decline over six weeks is invisible in individual run reports but obvious on a time-series chart. Grafana or even a simple Python matplotlib script works well for this.
import matplotlib.pyplot as plt
import pandas as pd
def plot_quality_trend(eval_history: list):
df = pd.DataFrame(eval_history)
df['date'] = pd.to_datetime(df['evaluated_at'])
df = df.sort_values('date')
# 7-day rolling average
df['score_ma7'] = df['mean_correctness'].rolling(window=7).mean()
plt.figure(figsize=(12, 4))
plt.plot(df['date'], df['mean_correctness'], alpha=0.3, label='Daily')
plt.plot(df['date'], df['score_ma7'], label='7-day avg', linewidth=2)
plt.axhline(y=4.0, color='r', linestyle='--', label='Min threshold')
plt.legend()
plt.title('LLM Answer Quality Over Time')
plt.savefig('quality_trend.png')Quick Check
Test your understanding of continuous evaluation pipelines for LLM applications.
Lesson Recap
In this lesson you learned: continuous evaluation pipelines run automated quality checks on every PR and on a daily schedule to catch regressions early, regression detection compares current scores to a baseline and blocks deployment if quality drops, and test set expansion from real failures keeps your evaluation suite grounded in actual user needs. Next up we classify agent failure modes and design recovery strategies.
Frequently asked questions
Is the “Building a Continuous Evaluation Pipeline” lesson free?
Yes — the full text of “Building a Continuous Evaluation Pipeline” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.
What will I learn in “Building a Continuous Evaluation Pipeline”?
Integrate LLM-as-judge evaluation into your CI/CD pipeline so every prompt or model change is automatically evaluated against a regression test suite before deployment. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Engineering Academy?
No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Building a Continuous Evaluation Pipeline” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Engineering Academy lesson?
Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- The LLM-as-Judge Pattern
- Pointwise and Pairwise Evaluation
- Calibrating Judge Models Against Humans
- Building a Continuous Evaluation Pipeline