Shadow and canary deployment of RAG pipeline changes: dual-execute new + old, offline LLM-judge comparison, gradual traffic ramp, auto-rollback guardrails, multi-armed bandits, per-component feature flags (retriever, reranker, generator). USE WHEN: user mentions "shadow mode", "canary deployment", "dual execute", "shadow traffic RAG", "feature flags RAG", "multi-armed bandit", "auto rollback", "LaunchDarkly RAG", "Unleash RAG" DO NOT USE FOR: CI pre-merge eval - use `continuous-evaluation`;...
Scanned 9/8/2026
Install to Claude Code
npx -y skills add claude-dev-suite/claude-dev-suite --skill shadow-mode-deployment --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Shadow Mode Deployment?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/claude-dev-suite-shadow-mode-deployment)More formats (shields.io, HTML) on the badges page.
---
name: shadow-mode-deployment
disable-model-invocation: true
description: |
Shadow and canary deployment of RAG pipeline changes: dual-execute new + old,
offline LLM-judge comparison, gradual traffic ramp, auto-rollback guardrails,
multi-armed bandits, per-component feature flags (retriever, reranker,
generator).
USE WHEN: user mentions "shadow mode", "canary deployment", "dual execute",
"shadow traffic RAG", "feature flags RAG", "multi-armed bandit", "auto
rollback", "LaunchDarkly RAG", "Unleash RAG"
DO NOT USE FOR: CI pre-merge eval - use `continuous-evaluation`; offline
eval only - use `rag-evaluation`; drift detection - use `drift-detection`
allowed-tools: Read, Grep, Glob, Write, Edit
---
# Shadow Mode & Canary Deployment for RAG
## Why Shadow Mode for RAG
Offline evaluation diverges from live traffic within weeks because:
- Query distribution shifts (product updates, seasonality).
- Corpus updates introduce content you never tested against.
- Judge LLM scores don't always match user satisfaction.
Shadow mode runs a candidate pipeline alongside production without affecting
user-facing output. Only after shadow metrics match or beat baseline do you
ramp real traffic.
## Stages
| Stage | User Impact | Traffic to Candidate | Purpose |
|---|---|---|---|
| Shadow (dual-execute) | Zero (candidate output discarded) | 100% mirror | Latency + quality comparison |
| Dark canary | Zero (compared offline) | Mirror + log | LLM-judge A/B offline |
| Canary | Small | 1-5% | Real user signal |
| Gradual ramp | Growing | 10 / 25 / 50 / 100% | Confidence building |
| Bandit | Dynamic | Optimal allocation | Revenue / satisfaction maximisation |
## Dual Execution Wrapper
```python
import asyncio, time
from dataclasses import dataclass
@dataclass
class PipelineResult:
answer: str
contexts: list[str]
latency_ms: float
cost: float
async def serve(query: str, user_id: str, flags) -> PipelineResult:
shadow_enabled = flags.is_enabled("rag.shadow_v2", user_id=user_id)
if not shadow_enabled:
return await prod_pipeline.answer(query)
prod_task = asyncio.create_task(prod_pipeline.answer(query))
cand_task = asyncio.create_task(cand_pipeline.answer(query))
prod_result = await prod_task
async def log_shadow():
try:
cand_result = await asyncio.wait_for(cand_task, timeout=5.0)
await log_event({
"query": query, "user_id": user_id,
"prod": prod_result.__dict__,
"cand": cand_result.__dict__,
"ts": time.time(),
})
except asyncio.TimeoutError:
await log_event({"query": query, "user_id": user_id,
"cand_error": "timeout"})
asyncio.create_task(log_shadow())
return prod_result
```
Candidate latency and errors are logged without blocking the user.
## Offline Comparison with an LLM Judge
```python
from anthropic import Anthropic
import json
client = Anthropic()
JUDGE = """You compare two RAG answers to the same question and pick the
better one. Return JSON: {"winner": "A"|"B"|"tie", "reason": str}.
Question: {q}
Answer A: {a}
Answer B: {b}"""
def judge_pair(q, a, b):
msg = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=400,
messages=[{"role": "user", "content": JUDGE.format(q=q, a=a, b=b)}],
)
return json.loads(msg.content[0].text)
# Randomize A/B order per sample to avoid position bias
import random
def judge_shadow_log(entries):
results = {"prod_wins": 0, "cand_wins": 0, "ties": 0}
for e in entries:
if random.random() < 0.5:
v = judge_pair(e["query"], e["prod"]["answer"], e["cand"]["answer"])
mapping = {"A": "prod_wins", "B": "cand_wins", "tie": "ties"}
else:
v = judge_pair(e["query"], e["cand"]["answer"], e["prod"]["answer"])
mapping = {"A": "cand_wins", "B": "prod_wins", "tie": "ties"}
results[mapping[v["winner"]]] += 1
return results
```
Run daily over the previous day's shadow log; require cand_wins >= prod_wins
with p < 0.05 (sign test) before promoting.
## Feature Flags for Independent Components
RAG has several swappable components. Flag them independently so you can isolate
which change caused a regression.
```python
# LaunchDarkly example
import ldclient
ld = ldclient.get()
def build_pipeline(user):
retriever = HybridRetriever() if ld.variation(
"rag.retriever.hybrid", user, False) else DenseRetriever()
reranker_model = ld.variation(
"rag.reranker.model", user, "BAAI/bge-reranker-v2-m3")
reranker = CrossEncoder(reranker_model)
gen_model = ld.variation(
"rag.generator.model", user, "claude-sonnet-4-5-20250929")
return RAGPipeline(retriever, reranker, generator=gen_model)
```
Unleash equivalent:
```python
from UnleashClient import UnleashClient
unleash = UnleashClient(url="https://unleash.example/api", app_name="rag-svc",
custom_headers={"Authorization": UNLEASH_TOKEN})
unleash.initialize_client()
if unleash.is_enabled("rag.reranker.v2", {"userId": user_id}):
reranker = new_reranker
```
## Gradual Ramp
```python
# Ramp config lives outside code
RAMP = {"rag.generator.claude45": [
{"day": 0, "pct": 1},
{"day": 1, "pct": 5},
{"day": 3, "pct": 25},
{"day": 5, "pct": 50},
{"day": 7, "pct": 100},
]}
```
Each step requires:
- Shadow judge win-rate >= 50% over last 24h.
- Latency p95 not worse by more than 10% vs baseline.
- Error rate not increased.
- Manual sign-off for the 50% -> 100% promotion (cheap insurance).
## Auto-Rollback Guardrails
```python
# rollback_watcher.py (runs every 5 minutes)
SLA = {
"faithfulness_avg": (0.85, ">"),
"latency_p95_ms": (4000, "<"),
"error_rate": (0.01, "<"),
}
def check_window(metric, window_minutes=15):
# pull metric from prometheus / ds-monitoring
...
breaches = []
for metric, (thresh, op) in SLA.items():
val = check_window(metric)
bad = (op == ">" and val < thresh) or (op == "<" and val > thresh)
if bad:
breaches.append((metric, val, thresh))
if len(breaches) >= 2:
ld.update("rag.generator.claude45", {"rolloutPercent": 0})
page("rag-oncall", f"AUTO-ROLLBACK: {breaches}")
```
Guardrail design:
- Require at least two breaches to avoid flapping on single-metric noise.
- Cooldown after rollback — do not auto-reenable.
- Keep a single kill-switch flag `rag.emergency_disable_all_experiments`.
## Multi-Armed Bandit for Variant Selection
Once multiple candidates are healthy, a bandit can outperform static A/B splits.
Thompson sampling on a binary reward (user thumbs-up or click-through):
```python
import numpy as np
class ThompsonBandit:
def __init__(self, arms):
self.alpha = {a: 1.0 for a in arms}
self.beta = {a: 1.0 for a in arms}
def pick(self):
samples = {a: np.random.beta(self.alpha[a], self.beta[a]) for a in self.alpha}
return max(samples, key=samples.get)
def record(self, arm, reward_0_1):
self.alpha[arm] += reward_0_1
self.beta[arm] += 1 - reward_0_1
bandit = ThompsonBandit(["control", "reranker_v2", "hybrid_v1"])
variant = bandit.pick()
answer = PIPELINES[variant](query)
# After receiving feedback:
bandit.record(variant, 1.0 if user_thumbs_up else 0.0)
```
Bandits skew traffic; keep a minimum 5% exploration on each healthy arm for
continuous comparison.
## Observability Schema
Per shadow log entry:
```json
{
"query": "...",
"user_id": "u_abc",
"prod": {"answer": "...", "latency_ms": 1820, "cost": 0.004, "model": "sonnet-4.5"},
"cand": {"answer": "...", "latency_ms": 2105, "cost": 0.005, "model": "opus-4.6"},
"prod_faithfulness": 0.91,
"cand_faithfulness": 0.93,
"judge_winner": "cand",
"variant_flags": {"retriever": "hybrid", "reranker": "bge-v2-m3"},
"ts": 1730000000
}
```
Index on `variant_flags.*` to slice metrics by component.
## Rollback Drill
Practice rollback monthly:
1. Create a "red button" test: flip `rag.generator.claude45` from 50% to 0%.
2. Measure time-to-rollback (should be < 30s).
3. Verify no user-facing error spike during transition.
Practice surfaces bugs before real incidents.
## Cost Controls for Shadow Mode
Shadow doubles inference cost. Mitigations:
- Sample traffic: `if hash(user_id) % 100 < 10` runs the candidate on 10%.
- Skip shadow on cached queries.
- Use smaller candidate models when shadowing large ones for latency proxies.
- Cap daily shadow budget; disable shadow automatically when exceeded.
## Anti-Patterns
| Anti-Pattern | Fix |
|---|---|
| Promoting 1% -> 100% in one step | Ramp through 5/25/50 with pauses |
| Auto-rollback on single noisy metric | Require 2+ breaches over 15min window |
| Shadow only the generator | Shadow the whole pipeline; retriever changes often dominate |
| Randomized user assignment without sticky bucketing | Hash user_id so user sees consistent variant |
| Offline judge only, no real-user metric | Capture thumbs-up / click-through; offline signals drift |
| No kill-switch flag | Add `rag.emergency_disable_all_experiments` at every layer |
| Bandit with no exploration floor | Reserve >= 5% for each healthy arm |
| Ignoring cost doubling during shadow | Sample traffic; set daily shadow budget cap |
## Production Checklist
- [ ] Dual-execution wrapper with bounded candidate timeout
- [ ] Shadow log schema agreed and indexed on component flags
- [ ] Daily LLM-judge A/B on shadow log with position randomization
- [ ] Per-component flags (retriever, reranker, generator) in feature-flag system
- [ ] Sticky user bucketing via hash(user_id)
- [ ] Ramp schedule with explicit promotion criteria at each step
- [ ] Auto-rollback watcher with 2-of-N breach rule and cooldown
- [ ] Kill-switch flag for all experiments
- [ ] Real-user reward signal wired (thumbs / click / task success)
- [ ] Shadow cost budgeted and capped; traffic sampled
- [ ] Rollback drill practiced monthly
- [ ] Dashboard shows quality + latency + cost per variant, sliced by flag
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!