LLM Evaluations Series — Presentation 04

Production Evals

Online sampling, drift detection, golden sets, regression CI, sampled human review, feedback loops — keeping eval signal fresh after your model ships.

Online Eval Drift Detection Golden Sets Regression CI Human Review Feedback Loops Dashboards
Sample Traffic → Score & Store → Detect Drift → Human Review → Golden Set → Gate Deploy
00

Topics We'll Cover

01

Online Eval — Sampling Production Traffic

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.

Sampling strategies

Uniform random

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.

Stratified

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.

Uncertainty-based

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.

Trigger-based

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.

Async scoring pipeline

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.

02

Drift Detection — Input & Output Distributions

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.

Input drift signals

SignalMeasurementAlert 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

Output drift signals

SignalMeasurementAlert 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
Embedding-based drift detection

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.

03

Golden Sets — Curating, Versioning, Growing

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.

golden_set_schema.py — file format convention
# 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
}

Growth cadence

The contamination fence

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.

04

Regression CI — Gating Deploys on Eval Pass

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.

ci_eval_gate.py — GitHub Actions example
# .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
ci/run_eval.py — regression check logic
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

Gate configuration

05

Sampled Human Review — Triage and Agreement

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.

Weekly review workflow

Sample 200 items
stratified from weekly traffic
→
Auto-filter
remove duplicates, PII
→
Priority sort
uncertain judge scores first
→
Dual annotate
20% of items by 2 reviewers
→
Score + notes
1–5 Likert + failure tag
→
IAA check
κ ≥ 0.7 or re-calibrate

Annotation taxonomy

Failure tagDefinitionRouting action
hallucinationModel asserts false factual claim with confidenceAdd to golden set; escalate if rate > 3 %
over_refusalSafe benign request refusedAdd to golden set; tune refusal threshold
instruction_ignoreSystem prompt constraint violatedEscalate to model team immediately
format_errorCorrect content, wrong structure (markdown, JSON, etc.)Add test case to CI eval
tone_mismatchCorrect content, wrong register for contextAdd to style rubric; refine judge prompt
Inter-annotator agreement (IAA)

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.

06

Feedback Loops — Thumbs Up/Down to Eval Signal

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.

User feedback thumbs up / down Denoiser filter spam, bots Triage negatives sampled Human review confirm + tag confirmed failures → golden set Feedback loop: thumbs-down → golden-set item in ~3 days

Noise sources in thumbs-down data

Conversion rate target

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.

07

Eval Observability — Dashboards & Alerts

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.

Recommended dashboard panels

Quality scorecard

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.

Drift monitor

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.

Feedback funnel

Daily thumbs-down events → denoised negatives → human-reviewed items → golden-set additions. Conversion rate trend. Annotator queue depth (backlog health).

Alert runbook (minimum viable)

AlertTriggerOwnerSLA
Quality regressionAny category >2 pp drop vs baseline for 2 consecutive hoursOn-call ML engineerInvestigate within 1 hour
Safety metric spikeToxicity or refusal rate crosses floorSafety teamPage immediately
Input driftKL > 0.3 vs baselineML opsInvestigate within 4 hours
Scoring queue backlogQueue depth > 2× 1-hour throughputInfra on-callScale workers within 30 min
08

Cost Models for Continuous Eval

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.

eval_cost_model.py — monthly estimate
# 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")
Cost scaling chart

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).

09

What to Take Away

Where to next

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.