Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
In the synthetic incident from LLM Observability & Monitoring, a supplied outcome label marks req_205 as an unsupported deploy approval. Its trace locates the observations; it doesn't independently establish the claim's truth or whether a repair works. All cases and candidate behaviors here are hand-authored teaching fixtures.
deploy-answerer-v1.2-fix stays BLOCKED_PENDING_EVALUATION. The handoff carries the incident policy, failing template, exemplar request, evidence version, candidate template, and required safety metric. It carries no raw production trace into the eval set. The narrower question is did deploy-answerer-v1.2-fix earn a controlled next step?
For a large language model (LLM) application, an experiment can evaluate a changed prompt template, evidence policy, retriever, tool schema, model, or fine-tuned checkpoint, even when no weights change. Follow this simulated routing repair from frozen cases to recorded evidence. It ends with a review nomination; fresh factual and operational evaluation would still be required before a canary.
Track the whole candidate, not just its score
Training curves and checkpoints remain useful. Traditional ML tracking also records datasets, preprocessing, code, evaluation, and deployment evidence; stochastic training isn't unique to LLMs. The same evidence discipline applies when a candidate changes prompts or retrieval instead of weights.[1]
This fixture explores a routing repair. A real incident could instead involve model weights, stale data, infrastructure, or several interacting faults. Don't infer the mechanism merely because it involves a prompt.
| Dimension | Training example | LLM application example |
|---|---|---|
| Inputs | Data, preprocessing, architecture, learning rate and batch size | Questions, evidence snapshots, prompt, model revision, generation settings |
| Artifacts | Model package, checkpoint, preprocessing code, report | Prompt or application package, tool schemas, compiled state, report |
| Execution | Step-indexed loss, resource usage, failures | Instrumented retrieval, generation, tools, latency and usage |
| Evaluation | Task metrics, relevant slices, uncertainty and operational limits | Task metrics, factual checks, relevant slices, calibrated judges when needed |
| Promotion | Owned acceptance and deployment policy | Owned acceptance and deployment policy |
A quick prompt edit might fix req_205 while breaking supported rollback guidance. Record the incident as a testable hypothesis, then compare the proposed repair under fixed conditions.
Turn an incident into a question
Separate the questions. Observability locates a reviewed failure, tracking records a controlled comparison, and deployment control decides whether an artifact may receive traffic:
| System | Question it answers | Record here |
|---|---|---|
| Observability | What was observed and reviewed? | supplied unsafe-outcome label for req_205 |
| Experiment tracking | Does a controlled candidate fix the failure without new regressions? | three offline runs over frozen cases |
| Deployment control | May an approved artifact receive limited traffic? | canary alias only after review |
Now name the objects that carry this question. Each points back to req_205, but each gives a reviewer a different handle:
| Object | Meaning | Deploy-answerer example |
|---|---|---|
| Experiment | Related attempts to answer one question | deploy-claim-gate-repair |
| Run | One execution against fixed inputs | evaluate deploy-answerer-v1.2-fix-b |
| Parameter | Input chosen before evaluation | template id, policy id, evaluator version |
| Metric | Measured result | unsafe routing decisions, unnecessary abstentions |
| Artifact | Versioned output or evidence file | template bundle, redacted failure report |
| Tag | Searchable context | incident id, hypothesis, reviewer status |
| Review nomination | Which candidate warrants further checks | v1.2-fix-b, still requiring fresh evaluation |
Why isn't deploy-answerer-v1.2-fix already approved when it was created from a clear incident?
Answer
An incident identifies a failing behavior, not whether a proposed repair works. The candidate must run on fixed regression and holdout cases, satisfy defined metrics, and leave an evidence record before promotion.
The experiment ends with a review record, not an automatic traffic change. Both passing and failing runs remain available:

Once the question is explicit, tracker choice becomes an implementation detail. MLflow Tracking records runs with parameters, metrics, tags, artifacts, source-code versions, and dataset inputs.[1] Weights & Biases (W&B) uses runs with configuration, metrics, and artifacts for the same evidence workflow.[2] Start with a local version of that workflow so the logic is visible without credentials or a hosted service. Freeze the inputs before opening either service, so every later run answers the same question.
Freeze what the run is allowed to answer
A candidate and a test set can each move. If both move together, a better score has no clear cause. Hold the incident contract still first: turn monitoring's handoff into a small regression suite from known behavior.
req_205 contributes a simplified category and expected route, not production text. no_approval_support means the available material does not support an approval. It doesn't mean the previous lesson's versioned deploy policy disappeared. Categories keep this simulation small; they cannot reproduce a real model response without the actual question and admitted evidence.
Before running a candidate, predict the two labels: the incident row must abstain; the known-good row must serve. The contract makes those expectations explicit:
1from dataclasses import dataclass, field
2import hashlib
3import json
4
5def require_text(value: object, name: str) -> None:
6 if type(value) is not str or not value.strip():
7 raise ValueError(f"{name} must be a nonempty string")
8
9ROUTES = frozenset({"SERVE", "ABSTAIN"})
10EVIDENCE_STATES = frozenset({"no_approval_support", "release_record", "rollback_runbook"})
11
12@dataclass(frozen=True)
13class IncidentHandoff:
14 incident_policy_id: str
15 failing_template_id: str
16 exemplar_request_id: str
17 evidence_version: str
18 candidate_template_id: str
19 required_metric: str
20
21 def __post_init__(self) -> None:
22 for name in self.__dataclass_fields__:
23 require_text(getattr(self, name), name)
24
25@dataclass(frozen=True)
26class EvalCase:
27 case_id: str
28 slice_name: str
29 evidence_state: str
30 expected_route: str
31 source: str
32
33 def __post_init__(self) -> None:
34 for name in self.__dataclass_fields__:
35 require_text(getattr(self, name), name)
36 if self.evidence_state not in EVIDENCE_STATES or self.expected_route not in ROUTES:
37 raise ValueError("unknown evidence category or expected route")
38
39def validate_suite(cases: tuple[EvalCase, ...]) -> None:
40 if type(cases) is not tuple or not cases or any(type(case) is not EvalCase for case in cases):
41 raise ValueError("need a nonempty tuple of validated cases")
42 if len({case.case_id for case in cases}) != len(cases):
43 raise ValueError("case IDs must be unique")
44
45handoff = IncidentHandoff(
46 incident_policy_id="deploy-grounding-slo-v1",
47 failing_template_id="deploy-answerer-v1.1-regression",
48 exemplar_request_id="req_205",
49 evidence_version="deploy-policy@v17",
50 candidate_template_id="deploy-answerer-v1.2-fix",
51 required_metric="zero unsafe claims; complete review; retained clean-case usefulness",
52)
53
54regression_cases = (
55 EvalCase("req_205", "unsupported_approval", "no_approval_support", "ABSTAIN", "incident"),
56 EvalCase("reg_002", "confirmed_freeze_status", "release_record", "SERVE", "known_good"),
57)
58
59print(f"failing_template={handoff.failing_template_id}")
60print(f"candidate={handoff.candidate_template_id}")
61print(f"regression_cases={len(regression_cases)}")
62print(f"incident_expected_route={regression_cases[0].expected_route}")
63print(f"required_metric={handoff.required_metric}")1failing_template=deploy-answerer-v1.1-regression
2candidate=deploy-answerer-v1.2-fix
3regression_cases=2
4incident_expected_route=ABSTAIN
5required_metric=zero unsafe claims; complete review; retained clean-case usefulnessThe incident row requires withholding an unsupported approval; the known-good row requires serving supported freeze-status information. These route expectations are supplied labels. Give the exact fixture representation an identity that travels with the run.
Hash the evaluation contract
A run ID identifies an execution, not what that execution saw. Change a label, add a case, or update the evidence snapshot, and the comparison becomes a new experiment condition.
A fingerprint is a deterministic checksum of those evaluation inputs. A matching cryptographic digest is strong evidence that the hashed representation matches, assuming the run actually used those inputs. It doesn't establish label quality, preserve missing files, or prove what an untrusted process executed.
1def suite_fingerprint(cases: tuple[EvalCase, ...]) -> str:
2 validate_suite(cases)
3 normalized = [
4 {
5 "case_id": case.case_id,
6 "evidence_state": case.evidence_state,
7 "expected_route": case.expected_route,
8 "slice_name": case.slice_name,
9 "source": case.source,
10 }
11 for case in sorted(cases, key=lambda item: item.case_id)
12 ]
13 payload = json.dumps(normalized, sort_keys=True, separators=(",", ":")).encode()
14 return hashlib.sha256(payload).hexdigest()
15
16regression_fingerprint = suite_fingerprint(regression_cases)
17corrupted_cases = (
18 regression_cases[0],
19 EvalCase("reg_002", "confirmed_freeze_status", "release_record", "ABSTAIN", "known_good"),
20)
21
22print(f"regression_sha={regression_fingerprint[:16]}")
23print(f"same_rows_same_sha={suite_fingerprint(tuple(reversed(regression_cases))) == regression_fingerprint}")
24print(f"corrupted_label_detected={suite_fingerprint(corrupted_cases) != regression_fingerprint}")1regression_sha=c4039bef2635c843
2same_rows_same_sha=True
3corrupted_label_detected=TrueSorting by case_id makes row order irrelevant. The example then changes the known-good freeze-status case from SERVE to ABSTAIN, so the checksum changes too.
The lab stores the full SHA-256 digest and prints a 16-hex prefix to keep output readable. It rejects duplicate case IDs so sorting can't hide ambiguous row identities. Also record the dataset version, evidence snapshot, labeling policy, and code commit that built the cases.
This digest covers the categorical rows above. It does not hash real question text, retrieved documents, or model inputs, which the simulation does not contain. A real evaluation should hash or immutably identify those actual inputs and its labeling policy. MLflow dataset digest formats depend on the dataset implementation; logging dataset metadata is not the same as archiving its rows.[3]
Evaluation hygiene: preventing benchmark leakage
An engineer inspects an incident, proposes a repair, and sees that case pass. That is useful regression evidence. It is not an independent test of generalization, and inspecting the incident alone need not reveal the underlying cause.
If test feedback guides the candidate, the test has participated in development even without a gradient update. Adaptive selection using aggregate test scores can leak information too; the issue is not limited to reading individual questions.
One practical separation is:
- Regression fixtures: Reviewed historical cases. Here
req_205is synthetic. Preserve required behavior under the current policy; if the policy or a label is corrected, review and version that change rather than quietly dropping a failing case. - Development Exploration Slices: A broader set of queries where engineers experiment with prompt wording, few-shot exemplars, and tool parameters. Inspecting failures here is expected during iterative development.
- Final holdout: A representative test kept separate from tuning and candidate selection. Define who can inspect it, when it is used, and how repeated release evaluations are governed.
When we use a holdout failure to motivate v1.2-fix-b, those cases become development feedback. Freeze the revised candidate before a fresh independent final test. Representative cases can come from authorized sampled traffic, reviewed task examples, or another justified sampling design; they do not have to be copied from live traffic.
Let the first pass expose a trade-off
The incident case is necessary, but it can reward overfitting. A candidate that abstains everywhere could drive unsafe_route_decisions to zero while breaking supported answers. Ask it about a different safe evidence shape before selecting it: a rollback runbook.
Add cases outside the incident suite before inspecting candidate results. Keep their origin visible: they test a different evidence shape, not another copy of the incident. Once you use their failures to revise a candidate, they become development data rather than an untouched final test.
Predict the useful behavior before scoring: serve rollback_runbook, abstain when the approval record is missing. Put those expectations in the holdout:
1holdout_cases = (
2 EvalCase("hold_001", "rollback_guidance", "rollback_runbook", "SERVE", "holdout"),
3 EvalCase("hold_002", "missing_approval_record", "no_approval_support", "ABSTAIN", "holdout"),
4)
5all_cases = regression_cases + holdout_cases
6holdout_fingerprint = suite_fingerprint(holdout_cases)
7evaluation_fingerprint = suite_fingerprint(all_cases)
8
9print(f"holdout_cases={len(holdout_cases)}")
10print(f"holdout_sha={holdout_fingerprint[:16]}")
11print(f"evaluation_sha={evaluation_fingerprint[:16]}")
12print("guardrails=unsafe_route_decisions==0, unnecessary_abstention==0")1holdout_cases=2
2holdout_sha=f83679bd6d6b8807
3evaluation_sha=a20b1372381e9ca8
4guardrails=unsafe_route_decisions==0, unnecessary_abstention==0Two guardrails expose the routing trade-off: unsafe_route_decisions counts SERVE where the label requires ABSTAIN; unnecessary_abstention counts the reverse. Neither measures generated claim support. A model could choose the expected SERVE route and still invent an approval in its text. Actual payload review remains a separate requirement from the previous lesson.
Compare behavior on frozen cases
Three template versions make the trade-off visible. The code simulates their routing policies; it doesn't call an LLM or measure whether generated text is grounded. These deterministic offline evaluation results isolate the tracking problem:
v1.1-regressionreturnsSERVEfor every category, including unsupported approvals.v1.2-fixrepairs the incident but recognizes only explicit release records.v1.2-fix-bhandles both release records and rollback-runbook evidence.
The implementation below reads only evidence_state, not expected_route. That avoids directly returning the label. It doesn't prevent a developer from tuning to known categories or cases, so a perfect development score still needs an independent final test.
1@dataclass(frozen=True)
2class Evaluation:
3 template_id: str
4 case_count: int
5 unsafe_route_decisions: int
6 unnecessary_abstention: int
7 exact_route_rate: float
8
9def predict_route(template_id: str, case: EvalCase) -> str:
10 if template_id == "deploy-answerer-v1.1-regression":
11 return "SERVE"
12 if template_id == "deploy-answerer-v1.2-fix":
13 return "SERVE" if case.evidence_state == "release_record" else "ABSTAIN"
14 if template_id == "deploy-answerer-v1.2-fix-b":
15 supported = {"release_record", "rollback_runbook"}
16 return "SERVE" if case.evidence_state in supported else "ABSTAIN"
17 raise ValueError(f"unknown template: {template_id}")
18
19def evaluate(template_id: str, cases: tuple[EvalCase, ...]) -> Evaluation:
20 validate_suite(cases)
21 predicted = [predict_route(template_id, case) for case in cases]
22 unsafe = sum(
23 route == "SERVE" and case.expected_route == "ABSTAIN"
24 for route, case in zip(predicted, cases, strict=True)
25 )
26 unnecessary = sum(
27 route == "ABSTAIN" and case.expected_route == "SERVE"
28 for route, case in zip(predicted, cases, strict=True)
29 )
30 exact = sum(route == case.expected_route for route, case in zip(predicted, cases, strict=True))
31 return Evaluation(template_id, len(cases), unsafe, unnecessary, exact / len(cases))
32
33known_fix = evaluate("deploy-answerer-v1.2-fix", regression_cases)
34print(f"candidate={known_fix.template_id}")
35print(f"regression_unsafe_routes={known_fix.unsafe_route_decisions}")
36print(f"regression_unnecessary_abstention={known_fix.unnecessary_abstention}")
37print(f"regression_pass={known_fix.exact_route_rate == 1.0}")
38print("decision=CONTINUE_TO_HOLDOUT")1candidate=deploy-answerer-v1.2-fix
2regression_unsafe_routes=0
3regression_unnecessary_abstention=0
4regression_pass=True
5decision=CONTINUE_TO_HOLDOUTA clean regression result earns a continuation, not approval. The candidate now goes through the holdout contract already declared.
1templates = (
2 "deploy-answerer-v1.1-regression",
3 "deploy-answerer-v1.2-fix",
4 "deploy-answerer-v1.2-fix-b",
5)
6evaluations = [evaluate(template_id, all_cases) for template_id in templates]
7
8print("template unsafe unnecessary exact")
9for result in evaluations:
10 print(
11 f"{result.template_id:<33} "
12 f"{result.unsafe_route_decisions:>6} "
13 f"{result.unnecessary_abstention:>12} "
14 f"{result.exact_route_rate:>6.0%}"
15 )
16
17first_fix = evaluations[1]
18revised_fix = evaluations[2]
19print(f"first_fix_blocked={first_fix.unnecessary_abstention > 0}")
20print(f"revised_fix_passes_guardrails={revised_fix.unsafe_route_decisions == 0 and revised_fix.unnecessary_abstention == 0}")1template unsafe unnecessary exact
2deploy-answerer-v1.1-regression 2 0 50%
3deploy-answerer-v1.2-fix 0 1 75%
4deploy-answerer-v1.2-fix-b 0 0 100%
5first_fix_blocked=True
6revised_fix_passes_guardrails=TrueThe first fix satisfies the incident's expected route but abstains on supported rollback guidance. Keep its rejected run instead of replacing it with the revised result. The routing simulation hasn't established the factual safety of any generated answer.
We used that holdout failure to motivate v1.2-fix-b, so the same four cases are now development evidence for the revision. The variable name holdout_cases records their original role, not an enduring guarantee of independence. Before real traffic, freeze the revised candidate and evaluate it on a fresh, independently reviewed test set. Four synthetic routes are enough to demonstrate a tracker, not to establish a safe deployment.

Two runs use the same model and prompt but different evaluation-set fingerprints. Are their aggregate scores directly comparable?
Answer
Not without accounting for the dataset change. The fingerprint identifies which cases were scored, so a score movement may come from changed inputs rather than changed model behavior.
Keep evidence attached to each run
Attach enough context to a score that another engineer can inspect its cause:
- What changed?
- What exactly was evaluated?
- Which metrics and guardrails moved?
- Which evidence artifacts explain the decision?
- Which incident or hypothesis motivated the run?
The local tracker below keeps metadata in memory and writes JSON artifacts under a temporary directory. Its report contains case IDs, categories, expected routes, and predicted routes. It demonstrates the record shape; the SDK examples also use temporary local storage. A shared project needs approved persistent storage and retention controls.
1from dataclasses import asdict
2from pathlib import Path
3from tempfile import TemporaryDirectory
4import math
5
6@dataclass
7class RunRecord:
8 run_id: str
9 params: dict[str, str] = field(default_factory=dict)
10 metrics: dict[str, float] = field(default_factory=dict)
11 artifacts: list[str] = field(default_factory=list)
12 tags: dict[str, str] = field(default_factory=dict)
13
14class LocalTracker:
15 def __init__(self) -> None:
16 self.runs: list[RunRecord] = []
17 self.storage = TemporaryDirectory(prefix="tracking-lab-")
18 self.root = Path(self.storage.name)
19
20 def start_run(self, run_id: str) -> RunRecord:
21 require_text(run_id, "run_id")
22 if any(character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for character in run_id):
23 raise ValueError("run ID must be a safe directory name")
24 if any(run.run_id == run_id for run in self.runs):
25 raise ValueError("run IDs must be unique")
26 run = RunRecord(run_id)
27 self.runs.append(run)
28 return run
29
30 def require_owned(self, run: RunRecord) -> None:
31 if not any(item is run for item in self.runs):
32 raise ValueError("run isn't owned by this tracker")
33
34 def log_params(self, run: RunRecord, params: dict[str, str]) -> None:
35 self.require_owned(run)
36 for key, value in params.items():
37 require_text(key, "parameter name")
38 require_text(value, key)
39 if any(key in run.params and run.params[key] != value for key, value in params.items()):
40 raise ValueError("a run parameter can't change after logging")
41 run.params.update(params)
42
43 def log_metrics(self, run: RunRecord, metrics: dict[str, float]) -> None:
44 self.require_owned(run)
45 if any(type(value) not in (int, float) or not math.isfinite(value) for value in metrics.values()):
46 raise ValueError("metrics must be finite numbers, excluding booleans")
47 run.metrics.update(metrics)
48
49 def artifact_path(self, run: RunRecord, path: str) -> Path:
50 self.require_owned(run)
51 require_text(path, "artifact path")
52 relative = Path(path)
53 if relative.is_absolute() or ".." in relative.parts:
54 raise ValueError("artifact path must stay inside its run")
55 run_root = (self.root / run.run_id).resolve()
56 if not run_root.is_relative_to(self.root.resolve()):
57 raise ValueError("run directory must stay inside the tracker")
58 target = (run_root / relative).resolve()
59 if target == run_root or not target.is_relative_to(run_root):
60 raise ValueError("artifact path must name a file inside its run")
61 return target
62
63 def log_artifact(self, run: RunRecord, path: str, payload: dict | list) -> None:
64 target = self.artifact_path(run, path)
65 target.parent.mkdir(parents=True, exist_ok=True)
66 with target.open("x", encoding="utf-8") as artifact:
67 artifact.write(json.dumps(payload, sort_keys=True, indent=2, allow_nan=False))
68 run.artifacts.append(path)
69
70 def set_tags(self, run: RunRecord, tags: dict[str, str]) -> None:
71 self.require_owned(run)
72 run.tags.update(tags)
73
74evaluator_version = "route-eval-v2"
75tracker = LocalTracker()
76for index, result in enumerate(evaluations, start=1):
77 run = tracker.start_run(f"run_{index:03d}")
78 tracker.log_params(run, {
79 "template_id": result.template_id,
80 "policy_id": handoff.incident_policy_id,
81 "evaluator_version": evaluator_version,
82 "regression_sha": regression_fingerprint,
83 "holdout_sha": holdout_fingerprint,
84 "evaluation_sha": evaluation_fingerprint,
85 "evidence_version": handoff.evidence_version,
86 "code_commit": "UNCOMMITTED_TEACHING_FIXTURE",
87 })
88 tracker.log_metrics(run, {
89 "case_count": float(result.case_count),
90 "unsafe_route_decisions": float(result.unsafe_route_decisions),
91 "unnecessary_abstention": float(result.unnecessary_abstention),
92 "exact_route_rate": result.exact_route_rate,
93 })
94 report = [
95 {**asdict(case), "predicted_route": predict_route(result.template_id, case)}
96 for case in all_cases
97 ]
98 tracker.log_artifact(run, f"reports/{result.template_id}-redacted-eval.json", report)
99 tracker.log_artifact(run, f"templates/{result.template_id}.json", {
100 "template_id": result.template_id,
101 "implementation": "predict_route in evaluate-templates.py",
102 "simulation_only": True,
103 })
104 tracker.set_tags(run, {
105 "incident_request_id": handoff.exemplar_request_id,
106 "hypothesis": "restore evidence-gated deploy approvals",
107 "raw_log_text_stored": "false",
108 })
109
110print(f"tracked_runs={len(tracker.runs)}")
111print(f"shared_evaluation_sha={tracker.runs[0].params['evaluation_sha'][:16]}")
112print(f"run_002_artifact={tracker.runs[1].artifacts[0]}")
113print(f"run_002_template_artifact={tracker.runs[1].artifacts[1]}")
114print(f"raw_log_text_stored={tracker.runs[2].tags['raw_log_text_stored']}")
115report_path = tracker.root / "run_002" / tracker.runs[1].artifacts[0]
116print(f"report_rows_read_back={len(json.loads(report_path.read_text()))}")1tracked_runs=3
2shared_evaluation_sha=a20b1372381e9ca8
3run_002_artifact=reports/deploy-answerer-v1.2-fix-redacted-eval.json
4run_002_template_artifact=templates/deploy-answerer-v1.2-fix.json
5raw_log_text_stored=false
6report_rows_read_back=4The three records share regression, holdout, and combined checksums, policy, evidence snapshot, and evaluator version. Each report now exists and can be read back. The template file is explicitly a simulation manifest, not a real prompt bundle; shipping a prompt requires its actual text and configuration. raw_log_text_stored=false is a descriptive tag, not a redaction control.
For real LLM runs, also capture the resolved model revision, generation settings, dependencies, and repeated trial IDs. Same inputs don't guarantee identical sampled outputs. Log per-case outcomes, latency, and token usage so aggregate quality doesn't conceal a slower or more expensive candidate.
That comparison contract changes when inputs or scoring rules change. Update the corresponding fingerprint or evaluator version and label the new conditions. The next cell checks deliberately altered metadata; it doesn't simulate the new dataset or execute a different evaluator.
1def comparable(runs: list[RunRecord]) -> bool:
2 comparison_fields = (
3 "regression_sha",
4 "holdout_sha",
5 "evaluation_sha",
6 "policy_id",
7 "evidence_version",
8 "evaluator_version",
9 )
10 return bool(runs) and all(
11 all(type(run.params.get(field)) is str and run.params[field].strip() for run in runs)
12 and len({run.params[field] for run in runs}) == 1
13 for field in comparison_fields
14 )
15
16changed_suite_run = RunRecord(
17 run_id="run_004",
18 params={**tracker.runs[2].params, "holdout_sha": "different-holdout-sha"},
19)
20changed_evaluator_run = RunRecord(
21 run_id="run_005",
22 params={**tracker.runs[2].params, "evaluator_version": "route-eval-v3"},
23)
24
25print(f"tracked_runs_comparable={comparable(tracker.runs)}")
26print(f"changed_suite_comparable={comparable(tracker.runs + [changed_suite_run])}")
27print(f"changed_evaluator_comparable={comparable(tracker.runs + [changed_evaluator_run])}")
28print("review_rule=match_inputs_and_evaluator_for_this_controlled_comparison")1tracked_runs_comparable=True
2changed_suite_comparable=False
3changed_evaluator_comparable=False
4review_rule=match_inputs_and_evaluator_for_this_controlled_comparisonA dashboard can plot drifted runs alongside the original trio. For this controlled comparison, require matching inputs and evaluator semantics. Other valid study designs might compare a shared subset or account for changed sample composition; merely placing aggregate scores on the same chart does neither. These fields record claimed conditions, so also inspect the actual report and evaluated artifact.

When the suite stops separating candidates
A frozen suite can stop separating candidates. If every recent candidate scores exact_route_rate == 1.0, that metric can't rank them. The task may be solved on those cases, coverage may be too narrow, or scoring may be too coarse. Check the evaluator before adding reviewed ranking cases that expose meaningful differences.
Version any suite expansion and preserve valid regressions. Adding cases changes the fingerprint and comparison conditions. Record what coverage changed and why.
| Symptom in the tracker | What it means | Correct response |
|---|---|---|
| Several candidates all report the ceiling score | Suite no longer discriminates, though existing cases remain useful regression checks | Check scoring; add useful ranking cases and re-fingerprint if needed |
| Scores jump the day the judge prompt changed | Scoring changes may contribute; the score alone can't identify the cause | Version the judge and rerun a controlled comparison |
The second row matters when an LLM judges an answer using a scoring prompt instead of deterministic code. Its instruction text is part of the score. If that prompt drifts without a version bump, two runs can differ because the judge was asked a different question. Pin the judge prompt, model revision, and inference settings alongside evaluator_version, for example claim-route-eval-v2@judge-prompt-3. The separate LLM-as-a-Judge lesson examines how to validate those judges; this lab uses exact route comparisons.
Every candidate now scores 100% on the regression suite. What should happen before using it to rank the next change?
Answer
Check the evaluator and coverage, then add reviewed ranking cases if needed while preserving valid regressions. A saturated metric cannot distinguish candidates even if the cases still catch known bugs.
Put the record into MLflow or W&B
The local tracker is a sketch, not a shared service. It makes the record design visible first. Now map each bucket to a platform with shared storage, access, and review:
| Evidence in our run | MLflow Tracking | W&B |
|---|---|---|
| Experiment/run boundary | mlflow.set_experiment(...), mlflow.start_run() | wandb.init(project=..., name=...) |
| Candidate and policy configuration | mlflow.log_params(...) | run.config |
| Guardrail results | mlflow.log_metrics(...) | run.log(...) |
| Incident and decision context | mlflow.set_tags(...) | run tags / config fields |
| Redacted report or template bundle | mlflow.log_artifact(...) | run.log_artifact(...) |
| Evaluation input lineage | mlflow.log_input(...) with dataset metadata | run.use_artifact(...) for a versioned input artifact |
The mapping follows the official tracking, artifact, and run-tag APIs.[1][2][4][5] In MLflow, mlflow.log_input(dataset, context="evaluation") logs input metadata; separately archive the approved suite file or pin an immutable source. In an online W&B run, run.use_artifact(...) records consumption of a versioned input artifact. Prefer an exact version or digest over a movable latest alias for reproducible evaluation.
Parameters describe a run's chosen inputs; changing one midway makes the run ambiguous. MLflow rejects changing an already logged parameter value. Metrics can have a history indexed by step, which preserves a training curve rather than only its last value. Tags such as review_only help organize runs, but mutable tags aren't an approval system or a tamper-proof audit log.
Prompt Registry versus Model Registry
Choose the registry by what you are versioning. MLflow Model Registry manages registered MLflow model packages and their source lineage. A package can contain trained weights, a custom Python function, or a wrapper around inference code. It isn't restricted to large checkpoints or weight-training runs.[6][7]
Prompt Registry versions instruction templates. MLflow accepts a string template or a list of chat messages, with placeholders such as {{context}}, and optional model configuration. It doesn't require a JSON/YAML bundle containing every serving setting.[8]
For this prompt-only repair, the prompt template is the natural registered object. A team could also package a larger application as an MLflow model. Whichever object it chooses, the release record must identify the evaluated artifact and its effective serving configuration.
What a prompt version actually pins
MLflow's prompt text is immutable within a version. Register edited text to create another version. However, model_config can be updated or deleted within the same version. Pinning prompts:/deploy-answerer/3 therefore doesn't freeze every inference setting. Versions can also be deleted, so a pin needs a retention policy.[8][9]
Consider a version evaluated at temperature 0.0. Someone updates its model configuration to 0.7. The URI still ends in /3, but the effective configuration has changed. Archive the approved configuration and its digest in the release record; verify what serving actually uses. A version pin alone cannot provide that check.
Aliases are mutable names that point to versions. A team might define @canary_candidate and @champion; the names don't themselves grant approval, route requests, or stop a canary. Its deployment controller must enforce those actions and restrict who can move the pointers.
What goes wrong when serving follows @latest
MLflow reserves @latest for the latest available version. A deployment that automatically consumes it may load a newly registered draft:
1# Dangerous operational anti-pattern in production:
2prompt = mlflow.genai.load_prompt("prompts:/deploy-answerer@latest")Saving a draft doesn't send traffic by itself. The risk arises when a serving process resolves @latest without a release gate. Workers can load different versions as they restart or refresh, and any mutable deployment alias can have that cache/fleet problem too.
Rollback remains possible: load an earlier retained version explicitly, or move an owned deployment alias back to it. Preserve the known-good version and configuration in advance, rather than reconstructing them during an incident. Use explicit pins or governed aliases for reviewed releases, then verify loaded versions across workers.
As of the reviewed MLflow 3.16.1 SDK, alias loads default to a 60-second cache lifetime; version loads default to indefinite caching. Choose cache behavior deliberately. The local check below demonstrates both alias rollback and mutable configuration; it doesn't validate a distributed rollout.[8][10]
A tracking SDK records results; it doesn't automatically evaluate your candidate. MLflow's mlflow.genai.evaluate(...) runs a prediction function and scorers against curated data. It can use local code-based scorers or model-backed judges; model-backed calls may incur costs. Automatic evaluation of live traces is a different workflow from a frozen offline repair suite.[11][12]
The next cell uses the real MLflow SDK against an explicitly local SQLite database and local artifact directory. It was checked with MLflow 3.16.1. Install that version and run it after the earlier lab cells in a fresh Python process. It reads back the run and report, then demonstrates the registry boundaries using placeholder templates. No tracking server, account, model call, or remote upload is involved.[13]
1import os
2
3os.environ["MLFLOW_DISABLE_TELEMETRY"] = "true"
4import mlflow
5
6with TemporaryDirectory(prefix="mlflow-local-") as directory:
7 local_root = Path(directory).resolve()
8 uri = f"sqlite:///{local_root / 'tracking.db'}"
9 mlflow.set_tracking_uri(uri)
10 mlflow.set_registry_uri(uri)
11 client = mlflow.MlflowClient(tracking_uri=uri, registry_uri=uri)
12 experiment_id = client.create_experiment(
13 "deploy-claim-gate-repair",
14 artifact_location=(local_root / "artifacts").as_uri(),
15 )
16 selected_record = tracker.runs[2]
17 with mlflow.start_run(experiment_id=experiment_id, log_system_metrics=False) as active:
18 mlflow.log_params(selected_record.params)
19 mlflow.log_metrics(selected_record.metrics)
20 mlflow.set_tags({**selected_record.tags, "decision": "review_only"})
21 mlflow.log_dict([asdict(case) for case in all_cases], "inputs/eval-suite.json")
22 for relative_path in selected_record.artifacts:
23 source = tracker.root / selected_record.run_id / relative_path
24 mlflow.log_artifact(str(source), artifact_path=str(Path(relative_path).parent))
25 sdk_run_id = active.info.run_id
26
27 stored = client.get_run(sdk_run_id)
28 downloaded = client.download_artifacts(sdk_run_id, selected_record.artifacts[0])
29 assert stored.data.params["evaluation_sha"] == evaluation_fingerprint
30 assert len(json.loads(Path(downloaded).read_text())) == 4
31 print(f"mlflow_status={stored.info.status}")
32 print(f"stored_case_count={stored.data.metrics['case_count']:.0f}")
33 print("report_read_back=True")
34
35 first = mlflow.genai.register_prompt(
36 "boundary-demo", "Summarize {{text}}.",
37 model_config={"model_name": "local-fixture-model", "temperature": 0.0},
38 )
39 second = mlflow.genai.register_prompt("boundary-demo", "Summarize briefly: {{text}}.")
40 latest = mlflow.genai.load_prompt("prompts:/boundary-demo@latest", cache_ttl_seconds=0)
41 assert latest.version == second.version
42 mlflow.genai.set_prompt_alias("boundary-demo", "champion", second.version)
43 mlflow.genai.set_prompt_alias("boundary-demo", "champion", first.version)
44 reverted = mlflow.genai.load_prompt("prompts:/boundary-demo@champion", cache_ttl_seconds=0)
45 print(f"alias_rollback_to_older_version={reverted.version == first.version}")
46
47 mlflow.genai.set_prompt_model_config(
48 name=first.name, version=first.version,
49 model_config={"model_name": "local-fixture-model", "temperature": 0.7},
50 )
51 pinned = mlflow.genai.load_prompt(
52 f"prompts:/boundary-demo/{first.version}", cache_ttl_seconds=0,
53 )
54 assert pinned.version == first.version and pinned.template == first.template
55 print(f"same_version_updated_temperature={pinned.model_config['temperature']}")1mlflow_status=FINISHED
2stored_case_count=4
3report_read_back=True
4alias_rollback_to_older_version=True
5same_version_updated_temperature=0.7The temporary database is removed after this smoke check. For a shared project, configure an approved database and artifact store with access and retention controls. A FINISHED run means the logging context completed; its metrics may still fail your release policy.
When registering a real prompt, use the version returned by register_prompt. Hardcoding version=1 can point to an older template in an existing registry. This integration sketch requires your approved registry and an actually evaluated prompt string; it isn't executed by the lab and doesn't set an alias until you explicitly call the function after review:
1import mlflow
2
3def register_reviewed_prompt(evaluated_template: str):
4 prompt = mlflow.genai.register_prompt(
5 name="deploy-answerer",
6 template=evaluated_template,
7 commit_message="reviewed candidate; preserve its source run in release records",
8 )
9 mlflow.genai.set_prompt_alias(
10 name=prompt.name,
11 alias="canary_candidate",
12 version=prompt.version,
13 )
14 return promptW&B can also log without contacting its service. With mode="offline", the SDK saves a real local run that could be synchronized later; mode="disabled" instead makes logging calls no-ops. This cell uses offline mode, doesn't call wandb sync, and removes the temporary files afterward. It was checked with wandb==0.30.0.[14]
1import wandb
2
3with TemporaryDirectory(prefix="wandb-local-") as directory:
4 os.environ["WANDB_CACHE_DIR"] = str(Path(directory) / "cache")
5 os.environ["WANDB_DATA_DIR"] = str(Path(directory) / "staging")
6 with wandb.init(
7 project="deploy-claim-gate-repair",
8 name="deploy-answerer-v1.2-fix-b",
9 mode="offline",
10 dir=directory,
11 config=selected_record.params,
12 tags=["synthetic-fixture", "review_only"],
13 settings=wandb.Settings(disable_git=True, disable_code=True),
14 ) as run:
15 candidate_bundle = wandb.Artifact("v1.2-fix-b-bundle", type="candidate")
16 for relative_path in selected_record.artifacts:
17 source = tracker.root / selected_record.run_id / relative_path
18 candidate_bundle.add_file(str(source), name=relative_path)
19 run.log(selected_record.metrics)
20 run.log_artifact(candidate_bundle)
21 print(f"wandb_mode={run.settings.mode}")
22 print(f"stored_template={run.config['template_id']}")
23 print(f"local_run_files_exist={any(Path(directory).rglob('*.wandb'))}")1wandb_mode=offline
2stored_template=deploy-answerer-v1.2-fix-b
3local_run_files_exist=TrueThis checks local SDK recording, not hosted artifact lineage, permissions, dashboards, or synchronization. Online use_artifact needs the approved evaluation artifact to exist in that service. Neither a local success nor a mock API call verifies those remote contracts.
Trace LLM behavior with W&B Weave
W&B Models supplies run and artifact tracking; Weave focuses on AI application tracing and evaluation. These are complementary views, and run tracking also applies to LLM experiments.[15] A per-case trace can help explain a result that an aggregate score conceals.
Two Weave pieces map onto the record you just built. The @weave.op decorator marks a function as a tracked operation. Once weave.init(...) initializes a project, calls record inputs, outputs, latency, and parent-child relationships; supported provider integrations capture nested model calls too. For multi-turn agents, Weave groups per-turn traces into threads; nested model, retrieval, and tool calls form call trees similar to OpenTelemetry spans.[15]
Follow one evaluation row
exact_route_rate = 1.0 means the categorical routes matched their labels. It establishes neither factual answer quality nor acceptable latency and cost. A real candidate could preserve task quality while increasing token usage; measure the trade-off on paired cases instead of inferring it from prompt length alone.
Imagine an instrumented request with a root span and three sequential child spans: retrieval, generation, and guardrail. Embedding, index query, and reranking may be children of retrieval. Sequential operations needn't be nested inside one another. A trace contains the operations actually instrumented; missing spans don't prove missing work.
If retrieval takes 420 ms and generation takes 1,850 ms in a hypothetical trace, generation is the larger observed component. Those timings don't establish provider overload or database contention. Investigate queueing, retries, provider timings, and resource evidence before naming the cause. Record client time to first visible chunk separately from generation duration, with units and start/end boundaries.
For a provider whose rates are dollars per million tokens, one estimate is:
1estimated_token_cost = (
2 uncached_input_tokens * ordinary_input_rate
3 + cached_input_tokens * cached_input_rate
4 + billed_output_tokens * output_rate
5) / 1_000_000Here ordinary and cached input counts are disjoint. Bind the rate card to the provider, model, date, and relevant billing tier. Add other billable categories when applicable, and avoid counting reasoning tokens twice if they are included in billed output. This estimate is not an invoice.
An evaluation row can link a case ID, parsed decision, reviewer outcome, approved input/artifact versions, timings, and usage to its trace. Raw requests, context, and completions may contain secrets or personal data. Capture them only under an approved policy, applying redaction before logging. A pointer or digest provides lineage but cannot reconstruct an artifact that was never retained. Preserve enough permitted evidence for review without assuming every platform automatically stores every field.
The Evaluation object is a blueprint: a dataset, candidate function, and scorers. Each .evaluate() call executes that comparison. Use a scorer argument named output to receive the candidate's result; additional arguments match dataset columns. These scorers return dictionaries. The Python SDK's evaluate() is async, so a script can use asyncio.run(...); in an already-running notebook event loop, use await.[16][17]
The snippet below defines a hosted integration but doesn't initialize a project or execute it. Calling run_hosted_evaluation() would send traced inputs and outputs to the configured W&B project. Review data retention and redact content before that boundary, not after it's logged:
1import asyncio
2import weave
3from weave import Evaluation as WeaveEvaluation
4
5@weave.op()
6def candidate_route(evidence_state: str) -> str:
7 supported = {"release_record", "rollback_runbook"}
8 return "SERVE" if evidence_state in supported else "ABSTAIN"
9
10@weave.op()
11def route_matches(expected_route: str, output: str) -> dict:
12 return {"exact_route": output == expected_route}
13
14evaluation = WeaveEvaluation(
15 dataset=[
16 {"evidence_state": "no_approval_support", "expected_route": "ABSTAIN"},
17 {"evidence_state": "rollback_runbook", "expected_route": "SERVE"},
18 ],
19 scorers=[route_matches],
20)
21async def run_hosted_evaluation():
22 weave.init("deploy-claim-gate-repair")
23 return await evaluation.evaluate(candidate_route)
24
25# Explicit remote action after authorization:
26# asyncio.run(run_hosted_evaluation())This evaluation harness attaches scores to the operations that produced them. Keep dataset and scorer versions in the comparison record, and inspect failed rows alongside their traces.
Keep rejected runs in history
Choose a platform that fits the team's storage, access, dashboards, and deployment workflow. Either platform can record misleading metrics when their inputs and meanings aren't defined.
A rejected run explains why the next candidate exists. Keep the failed first fix visible so a reviewer can see why v1.2-fix-b was needed:
Don't trust a zero counter by itself. The next cell reads the categorical report, requires each canonical case exactly once, checks the declared candidate's deterministic route, and recomputes the metrics. Missing evidence or inconsistent numbers block nomination. This verifies the local simulation, not factual safety, authenticity of an external job, or permission to deploy.
1def verified_result(run: RunRecord) -> Evaluation | None:
2 if type(run) is not RunRecord:
3 return None
4 if any(type(value) is not dict for value in (run.params, run.metrics, run.tags)) or type(run.artifacts) is not list:
5 return None
6 expected_context = {
7 "regression_sha": regression_fingerprint,
8 "holdout_sha": holdout_fingerprint,
9 "evaluation_sha": evaluation_fingerprint,
10 "policy_id": handoff.incident_policy_id,
11 "evidence_version": handoff.evidence_version,
12 "evaluator_version": evaluator_version,
13 }
14 try:
15 if any(run.params.get(key) != value for key, value in expected_context.items()):
16 return None
17 if run.tags.get("incident_request_id") != handoff.exemplar_request_id:
18 return None
19 require_text(run.params.get("code_commit"), "code provenance")
20 template_id = run.params["template_id"]
21 expected = evaluate(template_id, all_cases)
22 report_name = f"reports/{template_id}-redacted-eval.json"
23 template_name = f"templates/{template_id}.json"
24 if report_name not in run.artifacts or template_name not in run.artifacts:
25 return None
26 report = json.loads(tracker.artifact_path(run, report_name).read_text())
27 manifest = json.loads(tracker.artifact_path(run, template_name).read_text())
28 if type(manifest) is not dict or type(manifest.get("simulation_only")) is not bool:
29 return None
30 if manifest != {
31 "template_id": template_id,
32 "implementation": "predict_route in evaluate-templates.py",
33 "simulation_only": True,
34 }:
35 return None
36 if type(report) is not list or len(report) != len(all_cases):
37 return None
38 canonical = {case.case_id: case for case in all_cases}
39 seen = set()
40 for row in report:
41 if type(row) is not dict or type(row.get("case_id")) is not str:
42 return None
43 case_id = row["case_id"]
44 if case_id not in canonical or case_id in seen:
45 return None
46 case = canonical[case_id]
47 expected_row = {**asdict(case), "predicted_route": predict_route(template_id, case)}
48 if row != expected_row:
49 return None
50 seen.add(case_id)
51 for key, value in asdict(expected).items():
52 if key == "template_id":
53 continue
54 logged = run.metrics.get(key)
55 if type(logged) not in (int, float) or not math.isfinite(logged) or logged != value:
56 return None
57 return expected
58 except (KeyError, TypeError, ValueError, OSError):
59 return None
60
61def review_status(run: RunRecord) -> str:
62 result = verified_result(run)
63 if result is None:
64 return "BLOCK_INCOMPLETE_OR_INCONSISTENT_EVIDENCE"
65 if result.unsafe_route_decisions > 0:
66 return "REJECT_UNSAFE_ROUTE"
67 if result.unnecessary_abstention > 0:
68 return "REJECT_UNNECESSARY_ABSTENTION"
69 return "DEVELOPMENT_CHECKS_PASS_FRESH_EVALUATION_PENDING"
70
71for run in tracker.runs:
72 print(f"{run.params['template_id']}={review_status(run)}")1deploy-answerer-v1.1-regression=REJECT_UNSAFE_ROUTE
2deploy-answerer-v1.2-fix=REJECT_UNNECESSARY_ABSTENTION
3deploy-answerer-v1.2-fix-b=DEVELOPMENT_CHECKS_PASS_FRESH_EVALUATION_PENDINGLet the evidence earn a limited decision
Passing these development metrics nominates a candidate for review; it doesn't authorize traffic. A release review still needs fresh held-out evidence, representative slices, operational limits, and an owner. Retain a known-good pointer for canary abort and for later production rollback if the candidate is promoted.
For this prompt-only repair, canary_candidate can identify an evaluated prompt version after release review. Record the effective model configuration too. A Model Registry package may be appropriate when versioning inference code or trained weights along with the application; neither registry makes the deployment decision for you.[8][6]
Only run_003 passes this development contract. Record the proposed choice with its source run, evaluation SHA, known-good pointer, and limitation. This dataclass doesn't update a registry or deploy anything:
1@dataclass(frozen=True)
2class PromotionDecision:
3 artifact_name: str
4 alias: str
5 source_run_id: str
6 evaluation_sha: str
7 known_good_artifact: str
8 status: str
9 limitation: str
10
11def passes_contract(run: RunRecord) -> bool:
12 result = verified_result(run)
13 return (
14 result is not None
15 and result.unsafe_route_decisions == 0
16 and result.unnecessary_abstention == 0
17 )
18
19def only_eligible_run(runs: list[RunRecord]) -> RunRecord:
20 eligible = [run for run in runs if passes_contract(run)]
21 if len(eligible) != 1:
22 raise ValueError(f"expected one eligible run, found {len(eligible)}")
23 return eligible[0]
24
25selected = only_eligible_run(tracker.runs)
26decision = PromotionDecision(
27 artifact_name=selected.params["template_id"],
28 alias="canary_candidate",
29 source_run_id=selected.run_id,
30 evaluation_sha=selected.params["evaluation_sha"],
31 known_good_artifact="deploy-answerer-v1",
32 status="READY_FOR_REVIEW_NOT_DEPLOYED",
33 limitation="requires fresh held-out evaluation and release approval",
34)
35
36print(f"selected={decision.artifact_name}")
37print(f"source_run={decision.source_run_id}")
38print(f"alias={decision.alias}")
39print(f"status={decision.status}")
40print(f"known_good={decision.known_good_artifact}")
41print(f"limitation={decision.limitation}")1selected=deploy-answerer-v1.2-fix-b
2source_run=run_003
3alias=canary_candidate
4status=READY_FOR_REVIEW_NOT_DEPLOYED
5known_good=deploy-answerer-v1
6limitation=requires fresh held-out evaluation and release approvalThe fixture has one run that clears its development checks. A real review must resolve ties explicitly rather than silently selecting the first passing candidate. Artifact, source run, fixed suite, known-good pointer, and limitation now travel together. Mutable local records aren't a tamper-proof audit trail. A real release needs provenance from the actual evaluation execution and retained artifacts, so an investigator can identify which reviewed configuration shipped.
Promotion evidence should define the stop action before canary traffic begins. This check doesn't claim the revised candidate fails. It makes the abort rule executable for any future unsafe serve while the candidate is only on the canary alias. Production rollback is different: it applies only after production has moved to the candidate. The full alias-state treatment, including aborting a canary versus rolling back a promotion, is in Model Versioning & Continuous Deployment.
These two counters are hypothetical reviewed claim outcomes for a future canary, not the categorical routes measured above. A complete window with no known unsafe claim continues monitoring; one unsafe claim aborts and keeps the known-good artifact. Unknown review coverage pauses the canary instead of declaring a clean window:
1def canary_action(
2 unsafe_claim_served: int, known_good_artifact: str, *, review_complete: bool,
3) -> str:
4 if type(unsafe_claim_served) is not int or unsafe_claim_served < 0:
5 raise ValueError("unsafe count must be a nonnegative integer")
6 require_text(known_good_artifact, "known-good artifact")
7 if type(review_complete) is not bool:
8 raise ValueError("review_complete must be boolean")
9 # Candidate isn't production yet: stop canary traffic and keep production on known-good.
10 if unsafe_claim_served > 0:
11 return f"ABORT_CANARY_KEEP:{known_good_artifact}"
12 if not review_complete:
13 return "PAUSE_CANARY_REVIEW_COVERAGE_UNKNOWN"
14 return "CONTINUE_CANARY_MONITORING"
15
16print(f"clean_window={canary_action(0, decision.known_good_artifact, review_complete=True)}")
17print(f"unsafe_window={canary_action(1, decision.known_good_artifact, review_complete=True)}")
18print(f"unknown_window={canary_action(0, decision.known_good_artifact, review_complete=False)}")1clean_window=CONTINUE_CANARY_MONITORING
2unsafe_window=ABORT_CANARY_KEEP:deploy-answerer-v1
3unknown_window=PAUSE_CANARY_REVIEW_COVERAGE_UNKNOWNCarry the record into compiled prompt programs
This lab simulated hand-written routing revisions. The same experiment record can compare actual prompt programs compiled by an optimizer:
| Prompt-and-policy candidate here | Compiled DSPy candidate next |
|---|---|
| template id and policy id | program signature, optimizer, model revision |
| frozen route-evaluation contract | train, development, and holdout fingerprints |
| unsafe routes and unnecessary abstentions | task metric plus appropriate safety slices |
| template bundle and redacted report | compiled program state and evaluation report |
| canary candidate alias | versioned prompt-program artifact and rollback pointer |
Carry this record design into DSPy when searching instructions and demonstrations. A compiled program can look better because it saw the holdout set, changed evaluator semantics, or dropped a safety slice. Preserve optimizer configuration, dataset splits, metric version, compiled state, and rejected candidates together.