Free lesson · GenAI Solutions Architecture

Measure eval gate effectiveness with precision-recall tracking

You will build an EvalEffectivenessTracker that measures the precision and recall of each eval gate by comparing automated gate decisions against human ground-truth labels, enabling data-driven threshold tuning. Implement HumanLabel as a Pydantic model with fields: label_id: str, request_id: str, gate_id: str, human_judgment: HumanJudgment (GOOD, BAD, BORDERLINE), confidence: float (labeler's confidence 0-1), labeler: str, labeled_at: datetime, notes: str, time_spent_seconds: int. Store labels in PostgreSQL human_eval_labels table with columns: label_id VARCHAR(64) PRIMARY KEY, request_id VARCHAR(64), gate_id VARCHAR(64), human_judgment VARCHAR(16), confidence FLOAT, labeler VARCHAR(64), labeled_at TIMESTAMPTZ, notes TEXT, UNIQUE(request_id, gate_id, labeler). Build LabelCollectionAPI with POST /api/v1/eval-gates/{gate_id}/labels for submitting human labels with validation ensuring the request_id exists in eval_results, and GET /api/v1/eval-gates/{gate_id}/labels/pending returning unlabeled samples using SELECT er.request_id, er.overall_score FROM eval_results er LEFT JOIN human_eval_labels hel ON er.request_id = hel.request_id AND er.gate_id = hel.gate_id WHERE hel.label_id IS NULL ORDER BY ABS(er.overall_score - 0.5) ASC LIMIT 50 to prioritize borderline cases. Implement PrecisionRecallComputer with compute_metrics() that joins eval_results with human_eval_labels on request_id and gate_id, mapping gate passed + human GOOD to true positive (TP), gate passed + human BAD to false positive (FP), gate blocked + human BAD to true negative (TN), gate blocked + human GOOD to false negative (FN). Calculate precision = TP / (TP + FP), recall = TP / (TP + FN), f1 = 2 * precision * recall / (precision + recall), specificity = TN / (TN + FP), accuracy = (TP + TN) / (TP + TN + FP + FN). Return EffectivenessMetrics Pydantic model with all computed metrics plus sample_size: int, label_agreement_rate: float (inter-annotator agreement). Build CostAnalyzer with analyze_error_costs() computing false_positive_cost (bad content reaching users, estimated from downstream user complaint rate tracked in user_complaints table) and false_negative_cost (good content blocked, estimated from retry rate and user abandonment tracked in user_sessions table), returning ErrorCostAnalysis with fp_cost_per_incident: float, fn_cost_per_incident: float, total_fp_cost_per_day: float, total_fn_cost_per_day: float. Implement ROCCurveGenerator with generate_curve() that varies the eval gate threshold from 0.0 to 1.0 in 0.05 increments and computes true positive rate and false positive rate at each point, storing curve data in eval_roc_curves table with curve_id, gate_id, threshold, tpr, fpr, precision, recall, generated_at. Build OperatingPointSelector with select_optimal_threshold() that finds the threshold maximizing F1 score or minimizing a custom cost function total_cost = fp_cost_weight * FP_count + fn_cost_weight * FN_count, returning OptimalThreshold with threshold: float, expected_f1: float, expected_precision: float, expected_recall: float. Create Grafana dashboard with panels: precision-recall trend line over 30 days, ROC curve with current operating point marker and AUC value, confusion matrix heatmap, and error cost breakdown bar chart. Integrate DSPy 3.0's GEPA (Reflective Prompt Evolution) for systematic prompt optimization as part of eval pipelines: use dspy.Evaluate() to define metric functions that measure gate precision and recall, then run dspy.MIPROv2() or dspy.BootstrapFinetune() to automatically optimize the prompts used in LLM-as-judge evaluators, iteratively improving gate accuracy without manual prompt engineering. DSPy's programmatic prompt optimization treats eval gate thresholds and judge prompts as optimizable parameters, using the human-labeled ground-truth dataset as the optimization target to systematically reduce false positive and false negative rates. Emit eval_gate_precision{gate_id}, eval_gate_recall{gate_id}, eval_gate_f1{gate_id}, eval_gate_error_cost_dollars{gate_id,error_type}, eval_gate_dspy_optimization_improvement{gate_id} Prometheus metrics.

Course: GenAI Architecture & Design Patterns · Chapter 4 · Eval-First Architecture Engine

Free to read — no subscription required.

Introduction

Every eval gate in your architecture is a binary classifier masquerading as an infrastructure component. When a quality gate decides that a generation is "good enough" to pass through, it is making a classification decision — and that decision has a measurable false positive rate and false negative rate. Without rigorous precision-recall tracking, you are flying blind: your eval gates might be rejecting perfectly valid outputs (destroying latency with unnecessary retries) or passing garbage downstream (destroying user trust). This section equips you with the instrumentation, mathematics, and implementation patterns to measure every eval gate's effectiveness and select operating points that align with your system's actual cost trade-offs.

Key Terminology

  • Operating Point: A specific threshold configuration for an eval gate that yields a particular precision-recall pair; selected based on system cost trade-offs rather than abstract optimization
  • Ground-Truth Label: A human-annotated binary judgment (pass/fail) for a specific input-output pair, used as the reference against which automated gate decisions are compared
  • Confusion Matrix: A 2×2 matrix recording true positives, false positives, true negatives, and false negatives for a gate at a given operating point
  • ROC Curve: A plot of true positive rate versus false positive rate across all possible thresholds, enabling visual and quantitative comparison of gate configurations
  • AUC-ROC: The area under the ROC curve, summarizing a gate's discriminative ability independent of any specific threshold; values above 0.85 indicate a gate worth keeping in your evaluator registry
  • Precision-Recall Curve: A threshold-sweep plot more informative than ROC under class imbalance, showing how precision degrades as recall increases
  • Eval Budget: The total compute or latency allocation for evaluation within a request lifecycle; operating point selection must respect this constraint

Concepts

Why Precision-Recall Over Accuracy

In eval-first architectures, the class distribution at each gate is almost always skewed. A well-tuned generation pipeline produces acceptable outputs 85–95% of the time, which means your eval gate sees far more positives (pass) than negatives (reject). Raw accuracy is useless here — a gate that blindly passes everything achieves 90% accuracy on a pipeline with a 90% pass rate. Precision and recall decompose the gate's performance into two questions that map directly to system costs:

  • Precision answers: "Of all outputs the gate passed, what fraction were truly acceptable?" Low precision means bad outputs leak downstream, triggering user complaints or cascading failures in agent pipelines.
  • Recall answers: "Of all truly acceptable outputs, what fraction did the gate pass?" Low recall means the gate is over-rejecting, forcing expensive retries through your eval cascade and inflating latency.

The tension between precision and recall is not academic — it maps to a concrete dollar-and-latency trade-off in every eval cascade. A gate tuned for high precision (aggressive rejection) burns compute on retries and fallback evaluators. A gate tuned for high recall (permissive passing) burns trust and downstream error-handling budgets. ROC curve analysis gives you the tool to navigate this trade-off systematically rather than by guesswork.

Closing the Loop: From Metrics to Registry Updates

Code Walkthrough

The Measurement Pipeline

Before you can compute precision or recall, you need a continuous flow of ground-truth comparisons. The following diagram illustrates how production eval gate decisions are captured alongside periodic human annotations, fed into an effectiveness tracker, and used to produce the ROC analysis that drives threshold tuning.

Code snippet mermaid
Loading diagram...
  • Line 1: Declares a top-down flowchart representing the measurement pipeline from production traffic to threshold updates.
  • Lines 2-3: Define the primary data capture path — each Production Request passes through the Eval Gate Decision, which emits a pass/fail verdict plus continuous score to the Decision Log.
  • Lines 4-6: Define the ground-truth annotation path — the same request is routed through an adaptive Sampling Router to the Human Annotation Queue, whose labels flow into the Label Store.
  • Lines 7-8: Connect both data streams into the EvalEffectivenessTracker, which joins gate decisions with human labels to compute the confusion matrix.
  • Lines 9-12: Define the analysis pipeline — the Confusion Matrix feeds both Precision-Recall Curves and ROC Curves + AUC, which converge into the Operating Point Selector.
  • Lines 13-14: Close the feedback loop — the selected operating point updates the threshold in the Evaluator Registry, which changes the behavior of the Eval Gate Decision node for subsequent requests.

The critical insight in this pipeline is the adaptive sampling step. You cannot afford to send every production output to human annotators, but uniform random sampling wastes annotation budget on easy cases. Adaptive sampling over-samples outputs near the gate's decision boundary (where the eval score is close to the threshold), giving you denser ground-truth coverage exactly where measurement precision matters most.

Building the Effectiveness Tracker

The EvalEffectivenessTracker class is the core instrumentation component that collects gate decisions, joins them with ground-truth labels, and computes precision-recall metrics across threshold sweeps. The implementation below defines this tracker with methods record_decision for logging each gate's score and binary outcome, record_ground_truth for ingesting human labels, and compute_metrics for producing the full confusion matrix and derived metrics at any given threshold. The tracker uses a dictionary keyed by gate name, allowing a single instance to monitor every gate in your evaluator registry simultaneously.

Code snippet python
1from dataclasses import dataclass, field 2from typing import Optional 3 4@dataclass 5class GateDecision: 6 request_id: str 7 score: float 8 gate_passed: bool 9 ground_truth: Optional[bool] = None 10 11class EvalEffectivenessTracker: 12 def __init__(self): 13 self._decisions: dict[str, list[GateDecision]] = {} 14 15 def record_decision( 16 self, gate_name: str, request_id: str, score: float, passed: bool 17 ) -> None: 18 if gate_name not in self._decisions: 19 self._decisions[gate_name] = [] 20 self._decisions[gate_name].append( 21 GateDecision(request_id=request_id, score=score, gate_passed=passed) 22 ) 23 24 def record_ground_truth( 25 self, gate_name: str, request_id: str, label: bool 26 ) -> None: 27 for decision in self._decisions.get(gate_name, []): 28 if decision.request_id == request_id: 29 decision.ground_truth = label 30 return 31 32 def compute_metrics(self, gate_name: str, threshold: float) -> dict: 33 labeled = [ 34 d for d in self._decisions.get(gate_name, []) 35 if d.ground_truth is not None 36 ] 37 tp = sum(1 for d in labeled if d.score >= threshold and d.ground_truth) 38 fp = sum(1 for d in labeled if d.score >= threshold and not d.ground_truth) 39 fn = sum(1 for d in labeled if d.score < threshold and d.ground_truth) 40 tn = sum(1 for d in labeled if d.score < threshold and not d.ground_truth) 41 42 precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 43 recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0 44 fpr = fp / (fp + tn) if (fp + tn) > 0 else 0.0 45 46 return { 47 "threshold": threshold, 48 "tp": tp, "fp": fp, "fn": fn, "tn": tn, 49 "precision": precision, 50 "recall": recall, 51 "fpr": fpr, 52 "f1": 2 * precision * recall / (precision + recall) 53 if (precision + recall) > 0 else 0.0, 54 }
  • Lines 1–2: Imports dataclass and field for structured decision records, plus Optional for the nullable ground-truth field.
  • Lines 5–9: Defines GateDecision as a lightweight data class capturing the request identifier, the eval gate's continuous score, the binary pass/fail decision the gate made at its current threshold, and an optional ground-truth label that arrives asynchronously from human annotators.
  • Lines 12–13: Initializes the tracker with _decisions, a dictionary mapping each gate name in the evaluator registry to its list of recorded decisions.
  • Lines 15–21: The record_decision method appends a new GateDecision for the specified gate. This is called inline during request processing, capturing the raw score before the threshold is applied so that later threshold sweeps can re-evaluate the decision.
  • Lines 23–28: The record_ground_truth method performs a join between the human annotation and the stored decision by matching on request_id. When no matching decision exists, the label is silently dropped — in production you would log this as a coverage gap.
  • Lines 30–33: The compute_metrics method begins by filtering to only those decisions that have received a ground-truth label, since unlabeled decisions cannot contribute to precision-recall calculations.
  • Lines 34–37: Computes the four cells of the confusion matrix by sweeping the provided threshold against each decision's continuous score, independent of what threshold the gate was actually using in production.
  • Lines 39–41: Derives precision (fraction of gate-passed items that were truly good), recall (fraction of truly good items the gate passed), and false positive rate (fraction of truly bad items the gate incorrectly passed).
  • Lines 43–47: Returns a dictionary with all raw counts and derived metrics, including F1 as the harmonic mean of precision and recall — useful as a single-number summary but never sufficient alone for operating point selection.

From Curves to Operating Point Selection

With the tracker collecting labeled decisions, you sweep across thresholds to generate the ROC and precision-recall curves, then apply asymmetric costs to pick the threshold to deploy. generate_roc_curve iterates through evenly spaced thresholds calling compute_metrics at each, returning both curves plus an AUC score. select_operating_point then scans the precision-recall curve and applies cost weights — fp_cost for the downstream damage of passing a bad output (user-facing error, agent failure, compliance violation), fn_cost for the cost of rejecting a good output (retry latency, additional LLM-as-judge invocations in the eval cascade, wasted eval budget) — to find the threshold that minimizes total expected cost.

Code snippetpython
1def generate_roc_curve( 2 tracker: EvalEffectivenessTracker, gate_name: str, steps: int = 100 3) -> tuple[list[dict], list[dict], float]: 4 roc_points = [] 5 pr_points = [] 6 7 for i in range(steps + 1): 8 threshold = i / steps 9 metrics = tracker.compute_metrics(gate_name, threshold) 10 11 roc_points.append({ 12 "fpr": metrics["fpr"], 13 "tpr": metrics["recall"], 14 "threshold": threshold, 15 }) 16 pr_points.append({ 17 "precision": metrics["precision"], 18 "recall": metrics["recall"], 19 "threshold": threshold, 20 }) 21 22 # Compute AUC-ROC using trapezoidal rule 23 sorted_roc = sorted(roc_points, key=lambda p: p["fpr"]) 24 auc = 0.0 25 for j in range(1, len(sorted_roc)): 26 dx = sorted_roc[j]["fpr"] - sorted_roc[j - 1]["fpr"] 27 avg_y = (sorted_roc[j]["tpr"] + sorted_roc[j - 1]["tpr"]) / 2 28 auc += dx * avg_y 29 30 return roc_points, pr_points, auc 31 32def select_operating_point( 33 pr_points: list[dict], 34 total_labeled: int, 35 fp_cost: float = 10.0, 36 fn_cost: float = 1.0, 37 min_precision: float = 0.9, 38) -> dict: 39 best_point = None 40 best_cost = float("inf") 41 42 for point in pr_points: 43 if point["precision"] < min_precision and point["recall"] > 0: 44 continue 45 46 estimated_fp_rate = 1.0 - point["precision"] if point["recall"] > 0 else 0.0 47 estimated_fn_rate = 1.0 - point["recall"] if point["precision"] > 0 else 1.0 48 49 expected_cost = ( 50 estimated_fp_rate * fp_cost + estimated_fn_rate * fn_cost 51 ) 52 53 if expected_cost < best_cost: 54 best_cost = expected_cost 55 best_point = { 56 "threshold": point["threshold"], 57 "precision": point["precision"], 58 "recall": point["recall"], 59 "expected_cost": expected_cost, 60 } 61 62 if best_point is None: 63 return {"threshold": 0.5, "precision": 0.0, "recall": 0.0, 64 "expected_cost": float("inf"), "fallback": True} 65 66 return best_point

Curve generation walkthrough:

  • Threshold sweep: Iterates from 0.0 to 1.0 in even increments — 100 steps gives 0.01 granularity, sufficient for most eval gates. At threshold 0.0 everything passes (max recall, min precision); at 1.0 nothing passes.
  • ROC vs PR points: Each ROC point records FPR/TPR; each PR point records precision/recall. Under the skewed class distributions typical of eval gates, the precision-recall curve is often more informative because it exposes precision collapse at high recall.
  • AUC-ROC: Trapezoidal approximation after sorting by FPR. An AUC below 0.85 signals the gate lacks discriminative power and should be retrained, replaced in the evaluator registry, or removed from the eval cascade entirely.

Operating point selection walkthrough:

  • Cost weights: The default fp_cost=10.0 versus fn_cost=1.0 encodes the common pattern where passing a bad output is ten times more expensive than rejecting a good one.
  • Hard precision floor: Points below min_precision are skipped — this prevents the optimizer from picking a permissive threshold that would flood downstream components with low-quality outputs, regardless of cost weighting.
  • Expected cost: Weighted sum of (1 − precision) × fp_cost and (1 − recall) × fn_cost. Intentionally linear; in practice you may extend it with nonlinear penalties or tiered cost functions reflecting your eval cascade's retry pricing.
  • Fallback path: If no point meets the precision floor, returns threshold 0.5 with fallback=True, signaling that this gate needs retraining or replacement before it can be meaningfully deployed.

Done when select_operating_point returns a threshold without the fallback flag and the achieved precision clears min_precision by ≥0.05 — push that threshold to the evaluator registry to close the feedback loop.

Do's and Don'ts

Do's

  1. Do record the raw continuous score in GateDecision before applying the thresholdcompute_metrics re-evaluates every historical decision at any threshold by comparing d.score >= threshold, so if you only persist the binary gate_passed outcome you lose the ability to sweep thresholds post-hoc and must re-run the evaluator on stale production traffic.
  2. Do over-sample outputs near the gate's decision boundary for human annotation — uniform random sampling wastes annotation budget on easy cases far from the threshold where the gate's classification is unambiguous; adaptive sampling concentrates ground-truth labels exactly where confusion-matrix precision matters most and drives meaningful ROC curve resolution.
  3. Do key EvalEffectivenessTracker._decisions by gate name — a single tracker instance can monitor every gate in the evaluator registry simultaneously, so the Operating Point Selector sees precision-recall curves across all gates and can propagate threshold updates back to the registry in one feedback cycle.

Don'ts

  1. Don't treat false positives and false negatives as symmetric costs when selecting an operating point — a false positive (gate rejects a valid output) destroys latency through unnecessary retries, while a false negative (gate passes garbage downstream) destroys user trust; the ROC curve analysis only yields a useful operating point when you weight FPR vs. FNR according to which cost actually dominates your system.
  2. Don't attach ground-truth labels to GateDecision synchronously at request timerecord_ground_truth deliberately arrives asynchronously from the Human Annotation Queue and joins records by request_id; coupling annotation to the request hot path introduces annotator latency into gate decisions and defeats the purpose of the decoupled Label Store.
  3. Don't use a single global threshold across all gates in the evaluator registry — each gate's compute_metrics call returns its own confusion matrix, so a threshold calibrated for a retrieval-quality gate will misclassify outputs under a generation-coherence gate; the Operating Point Selector must push per-gate threshold updates back to the registry individually.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Solutions Architecture subscription.

From · cancel anytime · Already a subscriber? Sign in →

More free lessons in GenAI Architecture & Design Patterns

All free lessons in GenAI Solutions Architecture