LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnApplied LLM EngineeringModel Versioning & Deployment
⚙️MediumMLOps & Deployment

Model Versioning & Deployment

Turn an evaluated LLM change into an immutable release bundle, promote it through measured traffic, and roll back without losing lineage.

22 min read
Learning path
Step 81 of 196 in the full curriculum
Prompt Optimization with DSPySemantic Caching & Cost Optimization

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

In our worked incident, trace req_205 shows deploy-answerer claiming that payment-tests are approved for SHIP-4412 even though the admitted ReleaseOps evidence doesn't support that claim. The on-call points production back to the known-good template, and new requests return to expected behavior. A review question remains: which prompt, hosted-model snapshot, evidence corpus, and serving image produced the bad answer?

The DSPy compile passed its frozen holdout, but that compiled JSON still isn't a production release. A live answer can change through the prompt program, hosted-model pin, ReleaseOps corpus, serving image, decoding settings, or schema. A label such as v1 can't tell you which combination was active.

The operational question is blunt: which complete, evaluated release produced this response, and how quickly can traffic return to the last known-good release?

Release identity changes when any behavior-producing field changes. The stable and candidate deploy-answerer bundles keep the hosted-model pin, evidence gate, ReleaseOps corpus, and evaluation contract pinned, but change the compiled prompt program, compile run, and serving image, producing different release IDs.
Pinned fields stay shared and reviewable. Changing compiled prompt state, its source run, or the serving image creates a new release bundle and a new release ID.

A release is more than model weights

deploy-answerer combines a hosted answer model, a releaseops-evidence-classifier-v2 gate, and prompt state. The gate decides which ReleaseOps excerpts are trusted enough to admit. The prompt program must constrain the answer to those excerpts and abstain when admitted evidence doesn't support the requested claim. If you roll back only one component while a newer prompt, corpus, or image remains active, you can land on a mixed release that was never evaluated.

Call that complete set a release bundle. Store every input that can change visible output or operational safety in one immutable manifest. Store the evaluation contract and resulting report with it, too. Data, code, configuration, and serving infrastructure otherwise drift along separate paths, leaving reviewers to guess which change caused a behavior shift. The bundle keeps those dependencies reviewable.[1]Reference 1Hidden Technical Debt in Machine Learning Systems.https://research.google/pubs/hidden-technical-debt-in-machine-learning-systems/[2]Reference 2Challenges in Deploying Machine Learning: a Survey of Case Studies.https://arxiv.org/abs/2011.09926

Bundle fieldWhy it belongs in the release
Answer-model and evidence-gate identifiersDetermine model behavior
Prompt-program artifact and compile runTie optimized instructions and demonstrations to their source experiment
Tokenizer, prompt version, and decodingChange the text the model sees and how it samples
Policy and corpus versionsDecide which evidence is available and when it's sufficient to serve
Serving image and schemaChange runtime behavior and API compatibility
Evaluation-suite hash and evaluator versionDeclare the comparable scoring contract required before promotion

Treat the manifest as a version tuple, not as one model number. Its coordinates cover prompt state, evidence, policy, schema, evaluator, runtime, and decoding. Change one coordinate and the release identity must change, even when weights stay put.

Compatibility sits inside that tuple. A candidate can pass answer-quality checks and still fail when its input, output, or tool schema no longer matches the router. Pin those contracts, test them with the same release ID, and diagnose an interface failure separately from a model-quality regression.

We'll use two full bundles. stable is what's serving today: the hand-edited deploy-answerer-v1 template. candidate swaps in the DSPy-compiled program after development selection and an independent holdout gate. The hosted-model pin, evidence gate, ReleaseOps corpus, and eval suite stay fixed, giving us a clean comparison: if behavior changes later, the compiled prompt or the image that loads it changed.

Read the two constructors before running the cell. Which fields should differ? Candidate changes only its compile run, prompt version, and serving image; every other behavior-producing field stays pinned.

define-release-bundles.py
1from dataclasses import asdict, dataclass, replace 2import hashlib 3import json 4 5@dataclass(frozen=True) 6class ReleaseBundle: 7 service: str 8 answer_model: str 9 evidence_gate: str 10 prompt_compile_run: str 11 tokenizer: str 12 prompt_version: str 13 policy_version: str 14 corpus_version: str 15 serving_image: str 16 input_schema: str 17 eval_suite: str 18 evaluator_version: str 19 decoding: str 20 21stable = ReleaseBundle( 22 service="deploy-answerer", 23 answer_model="hosted-model@2026-04-01", 24 evidence_gate="releaseops-evidence-classifier-v2@sha256:41d8", 25 prompt_compile_run="manual-template-v1", 26 tokenizer="deploy-tokenizer@sha256:91aa", 27 prompt_version="deploy-answerer-v1.json", 28 policy_version="[email protected]", 29 corpus_version="releaseops-records@sha256:corpus-5", 30 serving_image="registry.example/deploy-answerer@sha256:image-a", 31 input_schema="deploy-answer.v2", 32 eval_suite="deploy-grounding-suite@sha256:suite-7", 33 evaluator_version="claim-evidence-eval-v2", 34 decoding="temperature=0", 35) 36 37candidate = replace( 38 stable, 39 prompt_compile_run="dspy-mipro-v2-run-052", 40 prompt_version="compiled_a_hosted-2026-04-01.json", 41 serving_image="registry.example/deploy-answerer@sha256:image-b", 42) 43 44print(f"stable_prompt={stable.prompt_version}") 45print(f"candidate_prompt={candidate.prompt_version}") 46print(f"candidate_compile_run={candidate.prompt_compile_run}")
Output
1stable_prompt=deploy-answerer-v1.json 2candidate_prompt=compiled_a_hosted-2026-04-01.json 3candidate_compile_run=dspy-mipro-v2-run-052

A human-readable name such as v3 helps people find a release, but it can't prove what went into it. Before running the next cell, predict the result of changing only prompt_version: the release ID should change, because the canonical manifest includes that field.

content-address-the-release.py
1def release_id(bundle: ReleaseBundle) -> str: 2 payload = json.dumps(asdict(bundle), sort_keys=True, separators=(",", ":")) 3 # Short prefix keeps the teaching output readable. Retain the full digest in production. 4 digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:12] 5 return f"{bundle.service}@sha256:{digest}" 6 7stable_id = release_id(stable) 8candidate_id = release_id(candidate) 9prompt_patch_id = release_id( 10 replace(candidate, prompt_version="compiled_a_hosted-2026-04-01.b.json") 11) 12 13print(f"stable={stable_id}") 14print(f"candidate={candidate_id}") 15print(f"prompt_patch={prompt_patch_id}") 16print(f"prompt_patch_is_new_release={prompt_patch_id != candidate_id}")
Output
1stable=deploy-answerer@sha256:32b8ed409b8e 2candidate=deploy-answerer@sha256:e31f2b6d7a2c 3prompt_patch=deploy-answerer@sha256:f94cb4d1892f 4prompt_patch_is_new_release=True

The lab abbreviates each SHA-256 digest to 12 hexadecimal characters so the state transitions stay readable. A production registry should retain the full digest as identity and use short prefixes only for display.

Why should a one-line prompt correction produce a new release ID?

Answer

The prompt can change served behavior even when model weights don't move. Reusing the old release ID would make traces and rollbacks lie about what users saw.

Artifacts stay fixed; aliases move

A registry record stores an immutable bundle. An alias such as production or canary is a mutable pointer used by traffic. Because the record never changes, rollback only needs to move the pointer back; it doesn't need to rebuild or rewrite the bundle.

MLflow's current Model Registry workflow uses aliases and tags to organize model versions, and its documentation marks fixed Model Stages as deprecated. The distinction matters: a model version preserves history, while an alias names the version a loader should resolve now.[3]Reference 3Model Registry Workflows | MLflow AI Platformhttps://mlflow.org/docs/latest/ml/model-registry/workflow/

An MLflow model alias still points to one registered model version. It doesn't automatically include the prompt, corpus, policy, schema, or serving image in this release bundle. Treat it as one component pointer, or register a wrapper artifact whose manifest resolves the complete bundle. A movable model alias isn't complete release identity.

Alias step chart over two fixed release-bundle rails: canary points to candidate release e31f2b6d7a2c before promotion, production starts on stable release 32b8ed409b8e, moves to candidate at promotion, and returns to the retained stable release after an incident; both immutable bundle records remain available throughout.
The fixed rails are stored release IDs; the colored lines are aliases. Canary reaches the candidate first, production moves only after promotion, and rollback returns production to the retained stable bundle without rewriting either manifest.

The small registry below enforces that rule. register() keeps a deep copy of each bundle, and move_alias() accepts only a registered ID. Before running it, predict the alias state: registering candidate should increase the record count while production still resolves to stable.

registry-and-aliases.py
1from copy import deepcopy 2 3class ReleaseRegistry: 4 def __init__(self) -> None: 5 self._bundles: dict[str, ReleaseBundle] = {} 6 self._aliases: dict[str, str] = {} 7 8 def register(self, bundle: ReleaseBundle) -> str: 9 bundle_id = release_id(bundle) 10 existing = self._bundles.get(bundle_id) 11 if existing is not None and existing != bundle: 12 raise ValueError("release digest collision") 13 self._bundles[bundle_id] = deepcopy(bundle) 14 return bundle_id 15 16 def move_alias(self, alias: str, bundle_id: str) -> None: 17 if bundle_id not in self._bundles: 18 raise KeyError(f"unregistered release: {bundle_id}") 19 self._aliases[alias] = bundle_id 20 21 def resolve(self, alias: str) -> str: 22 return self._aliases[alias] 23 24registry = ReleaseRegistry() 25assert registry.register(stable) == stable_id 26assert registry.register(candidate) == candidate_id 27registry.move_alias("production", stable_id) 28 29print(f"registered={len(registry._bundles)}") 30print(f"production={registry.resolve('production')}") 31print(f"candidate_registered={candidate_id in registry._bundles}")
Output
1registered=2 2production=deploy-answerer@sha256:32b8ed409b8e 3candidate_registered=True

A teaching registry can make move_alias() one assignment. A production control plane needs an atomic compare-and-set, the expected previous target, an authorization check, and an audit event. Those checks stop a stale pipeline from overwriting a newer rollout decision.

Promotion begins with controlled evidence

Registration makes a bundle addressable, not approved. Continuous delivery for machine learning adds evaluation gates and monitoring to ordinary build-and-deploy practices because a valid artifact can still produce unacceptable behavior.[4]Reference 4Continuous Delivery for Machine Learning.https://martinfowler.com/articles/cd4ml.html

The gate should speak the same language as the observability, evaluation, and experiment lessons that came before it. Reuse the grounded evidence contract instead of inventing one generic quality score:

  • supported_evidence_f1 asks whether supported deploy claims are served correctly.
  • unsupported_serve_count must remain zero in the frozen high-risk suite. That's the req_205 failure: serving a deploy-approval claim ReleaseOps didn't support.
  • The suite hash includes the rollback slice from the DSPy holdout. A candidate that lifts the mean while missing that floor is a different experiment, not a comparable pass.
  • p95_latency_ms (p95 latency) keeps a behaviorally acceptable release inside its response budget.
  • Schema hash, evaluation-suite hash, and evaluator version keep incomparable evidence out of the decision.
  • evaluation_report preserves the report artifact for a reviewer or incident responder to inspect later.
Offline promotion gate matches one candidate deploy-answerer bundle to a pinned contract, passes release metrics, opens canary, and leaves production pinned.
Offline evidence counts only when release identity and evaluation contract match. Passing that gate can open canary traffic, while production stays pinned to the stable bundle.

Pause at the gate boundary. Candidate's 0.93 F1, zero unsupported serves, and 472 ms p95 all clear. Predict the two failures in the next cell: a weaker F1 should fail its threshold, and an otherwise identical report from evaluator v3 should fail comparability.

offline-promotion-gate.py
1@dataclass(frozen=True) 2class OfflineEvidence: 3 release_id: str 4 eval_suite: str 5 evaluator_version: str 6 input_schema: str 7 evaluation_report: str 8 supported_evidence_f1: float 9 unsupported_serve_count: int 10 p95_latency_ms: int 11 12@dataclass(frozen=True) 13class Decision: 14 allowed: bool 15 reason: str 16 17def offline_gate(bundle: ReleaseBundle, evidence: OfflineEvidence) -> Decision: 18 if evidence.release_id != release_id(bundle): 19 return Decision(False, "evidence belongs to another release") 20 if evidence.eval_suite != bundle.eval_suite: 21 return Decision(False, "evaluation suite changed") 22 if evidence.evaluator_version != bundle.evaluator_version: 23 return Decision(False, "evaluator version changed") 24 if evidence.input_schema != bundle.input_schema: 25 return Decision(False, "schema mismatch") 26 if not evidence.evaluation_report: 27 return Decision(False, "evaluation report missing") 28 if evidence.supported_evidence_f1 < 0.92: 29 return Decision(False, "supported_evidence_f1 below 0.92") 30 if evidence.unsupported_serve_count != 0: 31 return Decision(False, "unsupported answer was served") 32 if evidence.p95_latency_ms > 500: 33 return Decision(False, "p95 latency exceeds 500 ms") 34 return Decision(True, "offline gate passed") 35 36candidate_offline = OfflineEvidence( 37 release_id=candidate_id, 38 eval_suite=candidate.eval_suite, 39 evaluator_version=candidate.evaluator_version, 40 input_schema=candidate.input_schema, 41 evaluation_report="reports/candidate-suite-7-redacted.json", 42 supported_evidence_f1=0.93, 43 unsupported_serve_count=0, 44 p95_latency_ms=472, 45) 46weaker_candidate = replace(candidate_offline, supported_evidence_f1=0.89) 47changed_evaluator = replace(candidate_offline, evaluator_version="claim-evidence-eval-v3") 48 49print(f"candidate={offline_gate(candidate, candidate_offline)}") 50print(f"weak_metric={offline_gate(candidate, weaker_candidate)}") 51print(f"changed_evaluator={offline_gate(candidate, changed_evaluator)}")
Output
1candidate=Decision(allowed=True, reason='offline gate passed') 2weak_metric=Decision(allowed=False, reason='supported_evidence_f1 below 0.92') 3changed_evaluator=Decision(allowed=False, reason='evaluator version changed')

Passing the offline gate permits the next experiment. It doesn't replace production yet. Open a canary alias while production still points to the known-good bundle.

open-canary-only-after-gate.py
1def open_canary(bundle: ReleaseBundle, evidence: OfflineEvidence) -> Decision: 2 decision = offline_gate(bundle, evidence) 3 if decision.allowed: 4 registry.move_alias("canary", release_id(bundle)) 5 return decision 6 7canary_decision = open_canary(candidate, candidate_offline) 8 9print(f"canary_opened={canary_decision.allowed}") 10print(f"canary={registry.resolve('canary')}") 11print(f"production_still_stable={registry.resolve('production') == stable_id}")
Output
1canary_opened=True 2canary=deploy-answerer@sha256:e31f2b6d7a2c 3production_still_stable=True

Managed models need a documented pin

When your team owns weights, a digest can identify them directly. deploy-answerer calls a hosted model, so the bundle records the strongest fixed identifier the provider documents. OpenAI's model documentation describes snapshots as a way to lock a specific version so performance and behavior remain consistent.[5]Reference 5Models | OpenAI APIhttps://platform.openai.com/docs/models In this lab that's hosted-model@2026-04-01, the same pin used during the DSPy evaluation.

A provider can change the model behind a name you thought was stable, shifting outputs with no deploy on your side. Chen, Zaharia, and Zou compared March and June 2023 GPT-4 snapshots on the same tasks. With chain-of-thought prompting, accuracy on a 1,000-question prime-versus-composite set dropped from 84.0% to 51.1%. The product name stayed familiar, but the behavior didn't.[6]Reference 6How Is ChatGPT's Behavior Changing over Time?https://arxiv.org/abs/2307.09009 Pinning a documented snapshot reduces that risk, but it doesn't remove it. Providers can retire snapshots, so keep a golden-set monitor and treat any model-identifier change as a fresh release decision.[7]Reference 7Deprecations | OpenAI APIhttps://developers.openai.com/api/docs/deprecations

That guarantee doesn't automatically transfer to every provider or alias. Check the provider's exact documentation, store the chosen identifier in the bundle, watch deprecation notices, and rerun release gates before changing it.

Replay a failed production trace against the candidate

Frozen fixtures test failures you thought to write down. A deterministic replay starts with a failure you shipped: pull its recorded trace, then run the exact prompt and tool-call sequence against the candidate while holding the evidence snapshot fixed. With the request and evidence held constant, a behavioral difference points to the release under test rather than a new request or a moved corpus.

The regression question is narrow: would the candidate have made the same mistake on this real request? Replay sits between the offline gate and shadow because it reuses recorded inputs without inventing fixtures or spending live traffic. It's read-only: recorded tool results go back in, rather than tools running again, so ReleaseOps state can't change.

Now apply that question to req_205. Historical deploy-answerer-v1.1-regression served a deploy-approval claim that admitted evidence didn't support; traffic has since returned to known-good deploy-answerer-v1. Replay the same failure against the recovered stable bundle and compiled candidate while keeping the evidence gate and recorded inputs fixed.

Before running the cell, predict the three verdicts: historical should reproduce the failure, while stable and candidate should avoid it. If stable also reproduces it, the supposed recovery isn't a valid comparison.

deterministic-replay.py
1@dataclass(frozen=True) 2class RecordedTrace: 3 request_id: str 4 origin_release: str 5 prompt_version: str 6 evidence_version: str 7 tool_sequence: tuple[str, ...] 8 evidence_supports_claim: bool 9 served_unsupported_claim: bool 10 temperature: float = 0.0 11 seed: int = 0 12 model_snapshot: str = "" 13 14def prompt_would_serve(bundle: ReleaseBundle, evidence_supports_claim: bool) -> bool: 15 # Historical v1.1 bypassed the gate; known-good v1 and the candidate don't. 16 if bundle.prompt_version == "deploy-answerer-v1.1-regression.json": 17 return True 18 return evidence_supports_claim 19 20def replay_against(trace: RecordedTrace, bundle: ReleaseBundle) -> dict[str, object]: 21 would_serve = prompt_would_serve(bundle, trace.evidence_supports_claim) 22 reproduces = would_serve and not trace.evidence_supports_claim 23 return { 24 "candidate_release": release_id(bundle), 25 "prompt_version": bundle.prompt_version, 26 "tool_sequence": trace.tool_sequence, 27 "pins": (trace.temperature, trace.seed, trace.model_snapshot), 28 "candidate_reproduces_failure": reproduces, 29 } 30 31historical_incident = replace( 32 stable, 33 prompt_compile_run="regressed-template-v1.1", 34 prompt_version="deploy-answerer-v1.1-regression.json", 35 serving_image="registry.example/deploy-answerer@sha256:image-regressed", 36) 37failed_trace = RecordedTrace( 38 request_id="req_205", 39 origin_release=release_id(historical_incident), 40 prompt_version=historical_incident.prompt_version, 41 evidence_version="releaseops-records@sha256:corpus-5", 42 tool_sequence=("lookup_release", "fetch_record", "answer"), 43 evidence_supports_claim=False, 44 served_unsupported_claim=True, 45 temperature=0.0, 46 seed=7, 47 model_snapshot=stable.answer_model, 48) 49historical_replay = replay_against(failed_trace, historical_incident) 50stable_replay = replay_against(failed_trace, stable) 51candidate_replay = replay_against(failed_trace, candidate) 52 53print(f"origin_release={failed_trace.origin_release}") 54print(f"historical_reproduces_failure={historical_replay['candidate_reproduces_failure']}") 55print(f"stable_reproduces_failure={stable_replay['candidate_reproduces_failure']}") 56print(f"replayed_against={candidate_replay['candidate_release']}") 57print(f"candidate_prompt={candidate_replay['prompt_version']}") 58print(f"inputs_held_fixed={candidate_replay['tool_sequence']}") 59print(f"pins={candidate_replay['pins']}") 60print(f"candidate_reproduces_failure={candidate_replay['candidate_reproduces_failure']}") 61 62assert historical_replay["candidate_reproduces_failure"] is True 63assert stable_replay["candidate_reproduces_failure"] is False 64assert candidate_replay["candidate_reproduces_failure"] is False
Output
1origin_release=deploy-answerer@sha256:efe48fbcb5a2 2historical_reproduces_failure=True 3stable_reproduces_failure=False 4replayed_against=deploy-answerer@sha256:e31f2b6d7a2c 5candidate_prompt=compiled_a_hosted-2026-04-01.json 6inputs_held_fixed=('lookup_release', 'fetch_record', 'answer') 7pins=(0.0, 7, 'hosted-model@2026-04-01') 8candidate_reproduces_failure=False

The clean replay is evidence against this one historical failure, not a general guarantee. The toy gate reads bundle.prompt_version, so the bad historical bundle disagrees with both safe releases on the same inputs while the evidence gate stays constant. A production replay harness also needs the recorded prompt, retrieved-evidence version, tool inputs and outputs, and sampling pins on the trace (temperature, seed, and model snapshot). Without them, a "replay" quietly becomes a fresh run whose difference you can't attribute. Keep a growing library of failed traces and require future candidates to clear each past production mistake before promotion.

Shadow evaluation must be read-only

Replay gives us one known failure. Offline fixtures still can't cover every real request shape. A shadow sends a sanitized copy of a production request to the candidate while the stable release alone supplies the user-visible answer. It reveals latency or evidence-support problems without exposing candidate text to customers.

The safety boundary is strict: shadow traffic can't execute tools, send messages, change ReleaseOps state, or write production state. Its output is evaluation data only. The envelope below also redacts a ship identifier before queuing the request.

Before running it, inspect the flags you expect to stay false: side effects must be disabled and candidate output must stay hidden. The candidate may abstain where stable served, and that disagreement is useful evidence rather than a user-visible incident.

shadow-envelope.py
1import re 2 3@dataclass(frozen=True) 4class ShadowEnvelope: 5 candidate_release: str 6 stable_release: str 7 sanitized_text: str 8 side_effects_enabled: bool 9 response_visible_to_user: bool 10 evidence_supports_claim: bool 11 12@dataclass(frozen=True) 13class ShadowComparison: 14 route_agreement: bool 15 candidate_claim_safe: bool 16 candidate_would_serve: bool 17 stable_route: str 18 candidate_route: str 19 dropped: bool 20 21def make_shadow(text: str, evidence_supports_claim: bool) -> ShadowEnvelope: 22 sanitized = re.sub(r"SHIP-\d+", "[SHIP_ID]", text) 23 return ShadowEnvelope( 24 candidate_release=registry.resolve("canary"), 25 stable_release=registry.resolve("production"), 26 sanitized_text=sanitized, 27 side_effects_enabled=False, 28 response_visible_to_user=False, 29 evidence_supports_claim=evidence_supports_claim, 30 ) 31 32def compare_shadow(envelope: ShadowEnvelope, queue_full: bool = False) -> ShadowComparison: 33 if queue_full or envelope.side_effects_enabled: 34 return ShadowComparison(False, False, False, "dropped", "dropped", True) 35 # Stable already answered the user; shadow only scores the candidate copy. 36 stable_route = "serve" # production response already left the system 37 candidate_bundle = ( 38 candidate if envelope.candidate_release == candidate_id else stable 39 ) 40 candidate_would_serve = prompt_would_serve( 41 candidate_bundle, envelope.evidence_supports_claim 42 ) 43 candidate_route = "serve" if candidate_would_serve else "abstain" 44 route_agreement = candidate_route == stable_route 45 candidate_claim_safe = candidate_route == "abstain" or envelope.evidence_supports_claim 46 return ShadowComparison( 47 route_agreement=route_agreement, 48 candidate_claim_safe=candidate_claim_safe, 49 candidate_would_serve=candidate_would_serve, 50 stable_route=stable_route, 51 candidate_route=candidate_route, 52 dropped=False, 53 ) 54 55shadow = make_shadow( 56 "Is payment-tests approved for SHIP-4412 without a ReleaseOps record?", 57 evidence_supports_claim=False, 58) 59comparison = compare_shadow(shadow) 60dropped = compare_shadow(shadow, queue_full=True) 61 62print(f"shadow_text={shadow.sanitized_text}") 63print(f"candidate={shadow.candidate_release}") 64print(f"side_effects_enabled={shadow.side_effects_enabled}") 65print(f"response_visible={shadow.response_visible_to_user}") 66print(f"candidate_route={comparison.candidate_route}") 67print(f"route_agreement={comparison.route_agreement}") 68print(f"candidate_claim_safe={comparison.candidate_claim_safe}") 69print(f"dropped_when_queue_full={dropped.dropped}") 70 71assert comparison.candidate_would_serve is False 72assert comparison.route_agreement is False 73assert comparison.candidate_claim_safe is True 74assert dropped.dropped is True
Output
1shadow_text=Is payment-tests approved for [SHIP_ID] without a ReleaseOps record? 2candidate=deploy-answerer@sha256:e31f2b6d7a2c 3side_effects_enabled=False 4response_visible=False 5candidate_route=abstain 6route_agreement=False 7candidate_claim_safe=True 8dropped_when_queue_full=True

The one-pattern redaction is a teaching fixture. A real shadow path needs schema-aware data minimization that covers every customer identifier and secret before the request reaches a queue, log, or candidate service.

At the service boundary, put the envelope on a bounded worker queue and count dropped or failed comparisons. The compare step is the other half of shadow: after the candidate answers the sanitized copy, record route agreement, claim safety, and latency against the stable path. Feed shadow_drop_rate from dropped or timed-out comparisons. Don't start an untracked background task in request scope and assume its evaluation record will survive a process restart.

side_effects_enabled=False in this teaching envelope is metadata, not an authorization boundary. The worker still needs a read-only tool allowlist and credentials that can't write production state. Reject a shadow request that asks for a side effect.

Why must a shadow evaluation of an agent candidate be read-only?

Answer

Shadow traffic duplicates production inputs. If the candidate can send messages, modify records, or call irreversible tools, evaluation itself creates duplicate side effects instead of passive evidence.

Canary traffic is visible and sticky

Shadow results can justify limited exposure, not automatic promotion. A canary sends a small share of real conversations to the candidate and returns those responses to users. That changes the safety problem: one conversation must remain on one bundle throughout the rollout. Otherwise adjacent turns can receive conflicting answers from stable and candidate.

Use deterministic hashing of a conversation ID to choose new conversations consistently across workers. Python's built-in hash() is process-dependent, so it can't provide that bucket. Hashing alone isn't enough: when exposure widens from 1% to 10%, a higher threshold could move an existing conversation from stable to candidate. Persist the first resolved release ID for the conversation lifetime.

Predict the widening case before running the router. A thread assigned to stable at 10% should still resolve to stable after the threshold becomes 100%; an abort must be able to override that assignment.

sticky-canary-routing.py
1def bucket(conversation_id: str) -> int: 2 digest = hashlib.sha256(conversation_id.encode("utf-8")).hexdigest() 3 return int(digest[:8], 16) % 100 4 5conversation_assignments: dict[str, str] = {} 6aborted_releases: set[str] = set() 7 8def assigned_release(conversation_id: str, canary_percent: int) -> str: 9 existing = conversation_assignments.get(conversation_id) 10 if existing is not None and existing not in aborted_releases: 11 return existing 12 alias = "canary" if bucket(conversation_id) < canary_percent else "production" 13 bundle_id = registry.resolve(alias) 14 if bundle_id in aborted_releases: 15 bundle_id = registry.resolve("production") 16 conversation_assignments[conversation_id] = bundle_id 17 return bundle_id 18 19canary_thread = next( 20 f"thread-{index}" for index in range(1000) if bucket(f"thread-{index}") < 10 21) 22assignments = [assigned_release(canary_thread, canary_percent=10) for _ in range(3)] 23stable_thread = next( 24 f"thread-{index}" for index in range(1000) if bucket(f"thread-{index}") >= 10 25) 26stable_before_widen = assigned_release(stable_thread, canary_percent=10) 27stable_after_widen = assigned_release(stable_thread, canary_percent=100) 28 29print(f"canary_thread={canary_thread}") 30print(f"bucket={bucket(canary_thread)}") 31print(f"same_release_each_turn={len(set(assignments)) == 1}") 32print(f"assigned_to_candidate={assignments[0] == candidate_id}") 33print(f"existing_stable_thread_pinned_after_widen={stable_before_widen == stable_after_widen == stable_id}")
Output
1canary_thread=thread-6 2bucket=6 3same_release_each_turn=True 4assigned_to_candidate=True 5existing_stable_thread_pinned_after_widen=True

The dictionary is a teaching fixture. A real router persists assignments in conversation state or a rollout store, writes the first assignment atomically so concurrent opening turns can't disagree, expires it when the conversation ends, and records the resolved release ID in traces. Stickiness governs normal routing, not recovery: an abort must override it for a failed candidate.

Bar chart of live unsupported serves for deploy-answerer canary windows: the 1 percent window has 0 of 1,200, the 10 percent window has 112 of 8,000 (1.4 percent), which trips the zero-tolerance gate and aborts while production stays on the stable release.
The 1% window records 0 unsupported serves in 1,200 requests. At 10%, 112 of 8,000 requests are unsupported (1.4% against a required 0%), so the controller aborts and leaves production on the stable bundle.

A canary window answers a new question

Offline evidence covers frozen examples. A live window asks a different question: how does candidate behave under real traffic mix, serving latency, error rate, and evidence pressure? Set thresholds and a minimum sample size before sending candidate traffic. A rate from a handful of requests isn't enough to widen exposure. For deploy-answerer, support is a hard safety invariant, so one live unsupported serve fails the window instead of disappearing inside an error budget.

Read the two windows in the figure as a decision, not a scorecard. The 1% sample clears this contract; the 10% sample fails even though its latency and error rate pass. Zero unsupported serves is the controlling condition.

live-canary-window.py
1@dataclass(frozen=True) 2class LiveWindow: 3 name: str 4 request_count: int 5 p95_latency_ms: int 6 error_rate: float 7 unsupported_serve_rate: float 8 shadow_drop_rate: float 9 10def live_gate(window: LiveWindow) -> Decision: 11 if window.request_count < 1_000: 12 return Decision(False, "fewer than 1000 requests observed") 13 if window.p95_latency_ms > 550: 14 return Decision(False, "live latency exceeded 550 ms") 15 if window.error_rate > 0.01: 16 return Decision(False, "error rate exceeded 1%") 17 if window.unsupported_serve_rate > 0.0: 18 return Decision(False, "unsupported serve rate must remain zero") 19 if window.shadow_drop_rate > 0.02: 20 return Decision(False, "shadow telemetry incomplete") 21 return Decision(True, "live window passed") 22 23window_1_percent = LiveWindow("1%", 1_200, 481, 0.002, 0.000, 0.001) 24window_10_percent = LiveWindow("10%", 8_000, 493, 0.003, 0.014, 0.001) 25 26print(f"one_percent={live_gate(window_1_percent)}") 27print(f"ten_percent={live_gate(window_10_percent)}") 28print( 29 "ten_percent_unsupported_count=" 30 f"{round(window_10_percent.unsupported_serve_rate * window_10_percent.request_count)}" 31)
Output
1one_percent=Decision(allowed=True, reason='live window passed') 2ten_percent=Decision(allowed=False, reason='unsupported serve rate must remain zero') 3ten_percent_unsupported_count=112

Both windows exceed the 1,000-request minimum, so this teaching contract can issue a verdict for each. A low-volume window would remain blocked even if every observed rate happened to be zero.

Use the failed signal to choose the next inspection. A request that can't parse points toward schema or serving-image compatibility. A parsed request with an unsupported answer belongs in trace replay against the pinned evidence snapshot. A safe but slow candidate points toward runtime capacity or decoding, while missing telemetry is a reason to stop exposure because the gate can't prove safety.

Abort a canary; roll back a promotion

The right recovery verb depends on which alias moved:

  • If candidate fails while only canary receives traffic, abort the rollout. production never moved.
  • If candidate was promoted and fails later, roll back by repointing production to the retained stable release.

Check the alias state before naming the incident. That distinction keeps a report from calling an unpromoted canary failure a production rollback.

In the next drill, canary failure should set exposure to zero and restore the canary pointer. The separate promoted-release drill should move production to candidate, detect failure, then return it to stable.

abort-and-rollback.py
1canary_percent = 10 2failed_window = live_gate(window_10_percent) 3if not failed_window.allowed: 4 aborted_releases.add(candidate_id) 5 canary_percent = 0 6 # Zero traffic isn't enough: any consumer of registry.resolve("canary") must leave the failed ID. 7 registry.move_alias("canary", stable_id) 8 9print(f"canary_percent_after_abort={canary_percent}") 10print(f"canary_alias_after_abort={registry.resolve('canary')}") 11print(f"production_after_abort={registry.resolve('production')}") 12print(f"pinned_canary_thread_restored_stable={assigned_release(canary_thread, canary_percent) == stable_id}") 13 14assert registry.resolve("canary") == stable_id 15assert registry.resolve("production") == stable_id 16 17# Separate rollback drill: a promoted candidate later regresses at wider traffic. 18rollback_drill = deepcopy(registry) 19previous_production = rollback_drill.resolve("production") 20rollback_drill.move_alias("production", candidate_id) 21post_promotion_incident = replace(window_10_percent, name="100%") 22 23if not live_gate(post_promotion_incident).allowed: 24 rollback_drill.move_alias("production", previous_production) 25 26print(f"drill_production_after_rollback={rollback_drill.resolve('production')}") 27print(f"drill_restored_stable={rollback_drill.resolve('production') == stable_id}") 28print(f"actual_production_unchanged={registry.resolve('production') == stable_id}")
Output
1canary_percent_after_abort=0 2canary_alias_after_abort=deploy-answerer@sha256:32b8ed409b8e 3production_after_abort=deploy-answerer@sha256:32b8ed409b8e 4pinned_canary_thread_restored_stable=True 5drill_production_after_rollback=deploy-answerer@sha256:32b8ed409b8e 6drill_restored_stable=True 7actual_production_unchanged=True

Real progressive-delivery controllers encode the same mechanics. Argo Rollouts supports weighted canary steps and pauses, and its analysis can abort an unsuccessful rollout. With traffic routing, keeping the stable replica set available lets traffic move back immediately on abort, at the cost of extra capacity during rollout.[8]Reference 8Argo Rollouts - Kubernetes Progressive Delivery Controllerhttps://argoproj.github.io/argo-rollouts/

Why might keeping the stable deployment warm during a canary be worth its GPU cost?

Answer

An alias change is only a fast recovery if the stable release can serve immediately. If old weights must reload after an incident, rollback can trade a behavior failure for a latency outage.

Record decisions so a later engineer can reconstruct them

A pipeline that moves aliases without recording why still creates mystery during an incident. Store release IDs, evidence references, gate verdicts, rollout windows, actor or controller identity, decision timestamp, and final alias state as append-only events.

The event stream below records candidate's rejection after its 10% window serves unsupported deploy-approval claims. The failed release remains addressable for diagnosis, while traffic stays with the known-good bundle. That pairing matters: recovery changes routing, while evidence preserves the path that led to the decision.

release-decision-record.py
1@dataclass(frozen=True) 2class ReleaseEvent: 3 stage: str 4 release_id: str 5 decision: str 6 evidence: str 7 8events = ( 9 ReleaseEvent("register", candidate_id, "RECORDED", "manifest_sha"), 10 ReleaseEvent("offline_gate", candidate_id, "PASSED", candidate_offline.evaluation_report), 11 ReleaseEvent("canary_1_percent", candidate_id, "PASSED", "live:window-001"), 12 ReleaseEvent("canary_10_percent", candidate_id, "ABORTED", "live:window-010"), 13 ReleaseEvent("production", stable_id, "UNCHANGED", "rollback:not-needed"), 14) 15 16for event in events: 17 print(f"{event.stage}:{event.decision}:{event.evidence}") 18print("release_decision=REJECT_CANDIDATE_AFTER_CANARY_REGRESSION") 19print(f"active_production={registry.resolve('production')}")
Output
1register:RECORDED:manifest_sha 2offline_gate:PASSED:reports/candidate-suite-7-redacted.json 3canary_1_percent:PASSED:live:window-001 4canary_10_percent:ABORTED:live:window-010 5production:UNCHANGED:rollback:not-needed 6release_decision=REJECT_CANDIDATE_AFTER_CANARY_REGRESSION 7active_production=deploy-answerer@sha256:32b8ed409b8e

Where the lab maps onto a live stack

The lab uses plain Python so the state transitions stay visible. A deployed stack usually splits the same responsibilities:

ResponsibilityProduction form
Store immutable model or component versionArtifact store plus model registry
Store prompt, policy, corpus, tokenizer, decoding, schema, and evaluator pinsRelease manifest in source control or deployment registry
Move candidate/production pointersRegistry aliases, deployment config, or feature flags limited to registered releases
Run offline evidence gatesCI job tied to exact manifest digest with a retained report artifact
Shift live traffic and pause on regressionsProgressive-delivery controller and metric analysis
Reconstruct impactRequest trace logs with resolved release ID and rollout event log

Feature flags remain useful, but their values must resolve to registered immutable release IDs. A flag that points at an arbitrary model name makes rapid changes easy and incident reconstruction impossible.

Keep one invariant as you ship: every request, gate, and alias event names the same immutable release ID. Then a failed answer can be replayed, a canary can be stopped, and production can return to known-good without guessing which bundle was active.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.Stable and candidate have identical answer-model and evidence-gate identifiers. The candidate changes prompt_version from deploy-answerer-v1.json to compiled_a_hosted-2026-04-01.json and serving_image from image-a to image-b. How should the release registry treat it before any traffic moves?

Correct answer: Create a new immutable release ID for the full bundle and evaluate that exact identity before moving an alias.

Prompt and serving-image changes can alter behavior, latency, and compatibility even when weights stay fixed. Content-derived identity must cover the complete manifest, and promotion evidence must name that exact release.

2.A release bundle calls a hosted model using a provider's floating model name rather than a documented fixed snapshot. What risk does that introduce?

Correct answer: The bundle no longer fixes one behavior-producing component; pin a documented snapshot and re-evaluate any identifier change.

A provider can move a floating alias without a deployment in your system. Recording the strongest documented fixed identifier keeps release evidence attributable; changing that identifier requires fresh gates.

3.A candidate bundle declares eval_suite=suite-7, evaluator_version=claim-evidence-eval-v2, and input_schema=deploy-answer.v2. Its evidence has the same release_id and metrics supported_evidence_f1=0.93, unsupported_serve_count=0, and p95_latency_ms=472, but evaluator_version=claim-evidence-eval-v3. What should the offline gate do?

Correct answer: Reject it because the evaluator version changed, so the evidence isn't comparable to the bundle's declared evaluation contract.

Thresholds are meaningful only under the declared scoring contract. A different evaluator can change metric semantics, so matching release and schema IDs can't make that report comparable. Regenerate evidence under one pinned evaluator.

4.A shadow worker receives a sanitized copy of a production request. Which configuration satisfies the shadow safety invariant?

Correct answer: Hide candidate output, restrict tools to reads, and use credentials incapable of writing production state.

Shadow traffic duplicates real requests for measurement, not action. An envelope flag isn't an authorization boundary, so both the tool allowlist and credentials must make writes impossible while candidate output remains hidden.

5.An active conversation was assigned to stable at a 10% canary. The rollout widens to 100% before that conversation ends. How should the router handle its next turn?

Correct answer: Keep its persisted stable release for normal widening, while allowing an abort to override failed assignments.

Widening affects new conversations. Persisting the initial release prevents adjacent turns from switching behavior, but safety recovery remains authoritative: an abort can remove a failed candidate despite stickiness.

6.A live gate requires p95 latency at most 550 ms, error rate at most 1%, zero unsupported serves, and shadow drop rate at most 2%. A window reports 540 ms, 0.8%, 0.1%, and 1%, respectively. What is the decision?

Correct answer: Reject because the support invariant allows no unsupported serves, regardless of the other passing metrics.

Latency, transport errors, and shadow completeness pass their separate thresholds. The support contract is zero tolerance, so a 0.1% unsafe rate still means unsupported claims reached users and must abort the rollout.

7.A 1% canary has observed 600 requests with p95 latency 480 ms, zero errors, zero unsupported serves, and zero shadow drops. The release contract requires at least 1,000 requests before a live verdict. What should the controller do?

Correct answer: Keep the rollout paused because the metric rates come from fewer than the required 1,000 requests.

Zero failures in 600 requests can still miss rare defects. The declared decision rule requires both enough volume and passing metrics, so exposure stays paused until at least 1,000 requests have been observed.

8.Production still points at the stable release while a candidate receives 10% of conversations through a canary alias. The live window fails because the candidate serves unsupported deploy-approval claims. What should be recorded?

Correct answer: Record a canary abort, remove candidate traffic, repoint canary to stable, preserve evidence, and leave production unchanged.

Production never moved, so this is an abort rather than a rollback. Recovery removes canary exposure and its failed alias while retaining the immutable bundle and window evidence for diagnosis.

9.A candidate passes offline gating and a 1% canary, production is moved from stable_id to candidate_id, and a later 100% live window fails the latency threshold. What should the release record show?

Correct answer: Record promotion and rollback events, move production back to stable, and retain both immutable bundles and their evidence.

Because production already pointed at the candidate, recovery is a rollback. Append the rollback and final alias state without erasing the promotion path or mutating either bundle, so later review can reconstruct the incident.

10.A controller can repoint production to stable in seconds, but the stable replicas were scaled to zero and their weights take several minutes to load. What has the rollout failed to guarantee?

Correct answer: Fast recovery, because a quick alias move still waits for stable serving capacity to become ready.

Alias movement is only control-plane recovery. If stable weights and replicas aren't ready, traffic still faces cold-start delay or failure. Retaining warm stable capacity makes the rollback operationally fast.

10 questions remaining.

Next Step
Continue to Semantic Caching & Cost Optimization

You now know that every response must resolve to an exact release bundle. Next you'll reuse prior answers only when cache scope and invalidation include that same release, plus the corpus, tenant, and freshness constraints that produced them.

PreviousPrompt Optimization with DSPy
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Hidden Technical Debt in Machine Learning Systems.

Sculley et al. · 2015

https://research.google/pubs/hidden-technical-debt-in-machine-learning-systems/

Challenges in Deploying Machine Learning: a Survey of Case Studies.

Paleyes, A., Urma, R. G., & Lawrence, N. D. · 2022 · ACM Computing Surveys

https://arxiv.org/abs/2011.09926

Model Registry Workflows | MLflow AI Platform

MLflow · 2026

https://mlflow.org/docs/latest/ml/model-registry/workflow/

Continuous Delivery for Machine Learning.

Sato, D., Wider, A., & Windheuser, C. · 2019

https://martinfowler.com/articles/cd4ml.html

Models | OpenAI API

OpenAI · 2026

https://platform.openai.com/docs/models

How Is ChatGPT's Behavior Changing over Time?

Chen, L., Zaharia, M., & Zou, J. · 2023

https://arxiv.org/abs/2307.09009

Deprecations | OpenAI API

OpenAI · 2026

https://developers.openai.com/api/docs/deprecations

Argo Rollouts - Kubernetes Progressive Delivery Controller

Argo Project · 2026

https://argoproj.github.io/argo-rollouts/

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.