Back to skills
SKILL.md
Shadow Mode Deployment
ASecurityShadow 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`;...
- 31 stars
- 0 votes
- 0 copies
- 1 view
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add claude-dev-suite/claude-dev-suite --skill shadow-mode-deployment --agent claude-codeAre you the author of Shadow Mode Deployment?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/claude-dev-suite-shadow-mode-deployment)---
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
Attribution
Comments
Loading comments…