Online sampling, drift detection, golden sets, regression CI, sampled human review, feedback loops — keeping eval signal fresh after your model ships.
You cannot score every production request — inference cost, latency budget, and annotation throughput all prevent exhaustive eval. The goal is a statistically representative sample that gives you signal on tail failures without dominating your budget.
Score every k-th request. Simple, cheap, unbiased. Fails to catch rare tail events at reasonable sample rates. At 1 % sample rate on 100 000 req/day = 1 000 scored/day. Works for monitoring average quality.
Maintain sampling buckets per topic, user tier, or language. Oversample rare categories to ensure minimum representation. Prevents dominant categories from drowning out long-tail failures.
Score requests where a cheap proxy signal suggests high uncertainty: low log-prob on output, high perplexity, long response with many hedge words, or classifier confidence < 0.7. Focus expensive eval resources on the requests most likely to be wrong.
Always score requests matching policy-relevant patterns: contains PII, exceeds 2 000 output tokens, customer tier = enterprise, topic = medical / legal / financial. These are the requests where mistakes cost most.
Route sampled requests to a scoring queue (e.g. SQS, Pub/Sub, or Redis Stream) immediately after the response is served. Judge workers consume from the queue asynchronously. This decouples scoring latency from serving latency. Budget for a scoring queue depth that can absorb 2× peak traffic without blocking.
Distribution drift is the silent quality killer. No model change happened, no deploy occurred — yet user query distribution shifted and your quality metrics degraded. You won't see it without monitoring both the input and output distributions.
| Signal | Measurement | Alert threshold |
|---|---|---|
| Topic distribution | Embed queries weekly; KL divergence vs baseline embedding histogram | KL > 0.3 vs 30-day rolling baseline |
| Query length | P50, P95 token count; two-sample KS test vs baseline | KS p-value < 0.01 on 7-day window |
| Language mix | langdetect classifier; daily histogram | Any language >2× baseline share for 3 days running |
| Novel entities / jargon | OOV rate against a rolling vocabulary; spike detection | OOV > 5 % of tokens in any 1-hour window |
| Signal | Measurement | Alert threshold |
|---|---|---|
| Refusal rate | Fast classifier; daily rolling mean | ±2 pp vs 14-day baseline (absolute) |
| Hallucination rate | LLM judge on sampled RAG responses | >5 % absolute increase vs baseline |
| Output length | P50 tokens; anomaly detection (3σ) | 3σ spike for 2+ consecutive hours |
| Toxicity rate | Detoxify or Perspective API on sampled outputs | Any increase above floor → immediate alert |
Embed 1 000 input queries per day into a fixed-dimensional space (e.g. text-embedding-3-small at 1 536 dims). Reduce to 2D with UMAP for visualisation; use Maximum Mean Discrepancy (MMD) or KL divergence on the binned histogram for alerting. Tools: evidently library for automated drift reports, or custom MMD via scipy.
A golden set is your ground truth: a fixed, annotated corpus of (prompt, expected_quality) pairs against which every model version is regression-tested. It is the most valuable artefact your eval programme produces.
# Each line in golden_set_v4.2.jsonl
{
"id": "gs-00a3f9", # stable, never reused
"version_added": "v4.0", # golden-set release that added this item
"category": "rag_summarisation",
"split": "test", # "dev" or "test"
"prompt": "...", # full prompt including system prompt
"reference": "...", # gold answer or rubric
"human_score": 4, # 1-5 from annotator
"annotator": "a12",
"tags": ["long-context", "en"],
"deprecated": false,
"deprecation_reason": null
}
deprecated: true with a reason. Deprecated items stay in the file and can be reactivated. Deletion loses the failure history.The golden-set file must never appear in training data. Enforce this by storing it in a separate repo with no training pipeline access, and by hashing every item and running a dedup check before any fine-tuning data preparation job. If contamination occurs, the golden-set scores will artificially inflate and your regression gate becomes worthless.
Regression CI runs your golden-set eval on every candidate model before it reaches production. The gate is: all category scores must be within tolerance of the current production baseline.
# .github/workflows/eval_gate.yml (excerpt)
jobs:
eval-gate:
runs-on: ubuntu-latest
steps:
- name: Run golden-set eval
run: |
python ci/run_eval.py \
--golden-set golden_set_v4.2.jsonl \
--model "${{ env.CANDIDATE_MODEL }}" \
--baseline-scores baselines/prod_v4.1.json \
--tolerance 0.02 \
--fail-on-regression
def check_regression(candidate: dict, baseline: dict, tol: float) -> list[str]:
failures = []
for category, baseline_score in baseline.items():
cand_score = candidate.get(category, 0.0)
if cand_score < baseline_score - tol:
failures.append(
f"{category}: {cand_score:.3f} < {baseline_score:.3f} - {tol}"
)
return failures # empty = pass
Human review closes the loop that automated evals cannot close: it catches systematic judge biases, surfaces qualitatively novel failure modes, and provides the ground truth used to calibrate and retrain judge models.
| Failure tag | Definition | Routing action |
|---|---|---|
hallucination | Model asserts false factual claim with confidence | Add to golden set; escalate if rate > 3 % |
over_refusal | Safe benign request refused | Add to golden set; tune refusal threshold |
instruction_ignore | System prompt constraint violated | Escalate to model team immediately |
format_error | Correct content, wrong structure (markdown, JSON, etc.) | Add test case to CI eval |
tone_mismatch | Correct content, wrong register for context | Add to style rubric; refine judge prompt |
Cohen's κ ≥ 0.7 is the minimum bar before you trust the annotation. If κ falls below 0.6 on a category, run a calibration session: annotators jointly review 20 items, discuss disagreements, and update the rubric. Low IAA almost always indicates a vague rubric, not inherently subjective content.
Implicit user feedback is the highest-volume signal you have. Converting it into structured eval data requires careful denoising — raw thumbs-down data is noisy and biased toward edge cases.
Well-designed feedback pipelines convert roughly 15–30 % of thumbs-down events into confirmed eval items. The rest are noise (latency frustration, preference disagreements, ambiguous prompts). If your conversion rate is >50 %, your triage criteria are too permissive. If <10 %, your denoising is too aggressive and you are missing real failures.
Eval data is only useful if it is visible. A well-designed dashboard gives on-call engineers a 30-second read on system health and a 5-minute drill path to root cause.
Daily rolling average of LLM judge scores per category. Colour-coded: green (>baseline), yellow (−1–2 pp), red (−>2 pp). Single-number headline + sparkline per category.
Input embedding KL divergence vs 30-day baseline. Query length P50/P95. Language mix histogram. Output length P95. Refusal rate 7-day rolling. All on a single timeline.
Daily thumbs-down events → denoised negatives → human-reviewed items → golden-set additions. Conversion rate trend. Annotator queue depth (backlog health).
| Alert | Trigger | Owner | SLA |
|---|---|---|---|
| Quality regression | Any category >2 pp drop vs baseline for 2 consecutive hours | On-call ML engineer | Investigate within 1 hour |
| Safety metric spike | Toxicity or refusal rate crosses floor | Safety team | Page immediately |
| Input drift | KL > 0.3 vs baseline | ML ops | Investigate within 4 hours |
| Scoring queue backlog | Queue depth > 2× 1-hour throughput | Infra on-call | Scale workers within 30 min |
Production eval has four cost levers: sample rate, judge model tier, human review headcount, and scoring frequency. Model these explicitly before committing to a continuous eval cadence.
# Inputs
daily_requests = 100_000
sample_rate = 0.01 # 1%
judge_input_tokens = 800 # system + prompt + response
judge_output_tokens = 200 # score + rationale
judge_cost_per_1k_in = 0.003 # Claude 3.5 Sonnet
judge_cost_per_1k_out= 0.015
human_items_per_week = 200
human_cost_per_item = 0.25 # trained annotator (USD)
# Monthly judge cost
scored_per_day = daily_requests * sample_rate # 1000
judge_in_cost = (scored_per_day * 30 * judge_input_tokens / 1000) * judge_cost_per_1k_in
judge_out_cost = (scored_per_day * 30 * judge_output_tokens / 1000) * judge_cost_per_1k_out
human_cost_month = human_items_per_week * 4.3 * human_cost_per_item
# Results: ~$72 judge + ~$215 human = $287/month
print(f"Judge: ${judge_in_cost+judge_out_cost:.0f}/month")
print(f"Human: ${human_cost_month:.0f}/month")
print(f"Total: ${judge_in_cost+judge_out_cost+human_cost_month:.0f}/month")
At 1 M req/day: same 1 % sample = 10 000 scored/day = ~$720/month judge + same human cost. If this is too high: drop to 0.3 % sample on normal traffic + 10 % on trigger-flagged traffic. Use the batch API for 50 % discount on Anthropic models. Cache identical prompts in the scoring queue to avoid re-scoring duplicates (common in chatbot UX where users retry the same question).
Deck 05 covers red-teaming: threat modelling, manual and automated red teams, HarmBench, AdvBench, PAIR, JailbreakBench, and building an internal red-team practice that feeds back into production eval.