Skip to main content

LLMOps

What LLMOps Is

LLMOps is the operational discipline for running LLM-based systems in production: deploying models and prompts safely, controlling costs, serving traffic reliably, and improving the system over time without breaking it.

It has two layers:

  • Prompt and model lifecycle — versioning, testing, releasing, rolling back prompts and models
  • Inference operations — serving infrastructure, cost control, latency, GPU efficiency

Both are required. Most "LLMOps" articles only cover the first.


Prompt Lifecycle Management

Prompts are code — treat them as code

Every prompt lives in Git. No prompt changes in production without a PR, a diff, and a passing eval suite.

Directory structure:

prompts/
rag-answer/
v1.0.0.md ← system prompt text
v1.0.0.eval.json ← eval results attached to this version
v1.1.0.md
v1.1.0.eval.json
judge-groundedness/
v1.0.0.md
workflow-planner/
v1.0.0.md

Semver for prompts:

  • Patch (1.0.x): wording tweaks that don't change behavior
  • Minor (1.x.0): behavior change, requires eval re-run
  • Major (x.0.0): structural change, requires golden set review

Prompt registry

A runtime service (or simple config) that maps prompt_name + version → prompt text. The application fetches the prompt at request time by name, never hardcodes it.

# Fetches from registry (Redis, DB, or config file)
prompt = registry.get("rag-answer", version="1.1.0")
# Or use "latest-stable" alias for auto-promotion
prompt = registry.get("rag-answer", version="stable")

Benefits: rollback is a config change (flip stable alias), no redeploy needed.

Prompt testing (unit level)

Before running the full golden set, run cheap prompt unit tests:

# Test: does the prompt produce structured output?
response = llm.complete(prompt + test_input)
assert is_valid_json(response)
assert "citations" in json.loads(response)

# Test: does the prompt refuse out-of-scope?
response = llm.complete(prompt + out_of_scope_input)
assert "cannot answer" in response.lower()

Fast, cheap, runs in < 30s. Catches obvious breakage before the expensive golden set.


Model Release Engineering

Canary releases

Never flip 100% of traffic to a new model/prompt immediately.

Week 1: 5% of traffic → new model, 95% → old model
Week 2: Watch metrics (groundedness, latency, cost, error rate)
Week 3: If metrics hold → 25% → 50% → 100%
If regression → flip back to 0%, investigate

Canary routing implemented via feature flags (see below) — no redeploy needed to adjust split.

Shadow mode

New model runs alongside the old model on the same traffic. Results compared offline — users always see the old model's response. Zero risk to users, maximum signal.

User request → Old model → Response to user
↘ New model → Response to eval pipeline (not shown to user)

Use shadow mode before any canary. If shadow results are worse, don't canary.

Feature flags for model routing

# LaunchDarkly, Unleash, or a simple DB flag
flag = feature_flags.get("rag_generator_model", tenant_id=tenant_id)

if flag == "frontier":
model = "claude-sonnet-4-6"
elif flag == "slm":
model = "qwen3-4b-self-hosted"
else:
model = DEFAULT_MODEL

This enables:

  • Per-tenant model routing (give premium tenants Frontier, free tier gets SLM)
  • A/B testing across cohorts
  • Emergency rollback without redeploy

A/B testing for prompts and models

For a statistically valid A/B test:

  • Minimum sample size: ~500 interactions per variant (use a power calculator)
  • Primary metric: groundedness rate or task completion rate
  • Guard metrics: latency p95, cost per query, error rate
  • Duration: run for at least 2 weeks to account for day-of-week effects
  • Decision: if primary metric improves AND guard metrics don't degrade → promote

Rollback playbook

When production regresses:

# 1. Flip feature flag to old model (< 1 minute, no redeploy)
feature_flags set rag_generator_model=frontier-v1 --env prod

# 2. Flip prompt registry alias to last green version (< 1 minute)
registry alias set rag-answer stable=v1.0.2

# 3. If infra-level change needed: ArgoCD rollback
argocd app rollback rag-api --revision <last-green-sha>

# 4. Verify: check groundedness on last 100 responses in Langfuse
# 5. Write incident report before investigating root cause

Cost Engineering

Lever 1 — Model cascade routing (biggest impact: 60–90% cost reduction)

Query arrives

SLM classifier: simple or complex?
├── Simple + high confidence → SLM generator (€0.001/query)
└── Complex or low confidence → Frontier generator (€0.05/query)

Confidence threshold is tunable — lower it to save cost, raise it to improve quality. Track both in your observability stack.

Lever 2 — Prompt caching (50–90% off cached input tokens)

Cache the system prompt and static context prefix across requests:

  • Anthropic: cache_control: {"type": "ephemeral"} on system prompt blocks — 5-minute TTL, up to 90% discount
  • OpenAI: automatic prefix caching on inputs > 1024 tokens
  • vLLM (self-hosted): prefix_caching=True in engine args — reuses KV cache for shared prefixes

Track cache hit rate as a first-class metric. A 70% hit rate means 70% of your input tokens cost ~10% of their nominal price.

Lever 3 — Semantic caching (cache answers, not just KV)

Cache full responses keyed by semantic similarity of the query:

# On query arrival:
query_embedding = embed(query)
cached = semantic_cache.lookup(query_embedding, threshold=0.95)
if cached:
return cached.response # free
# else: run the full pipeline and store result
semantic_cache.store(query_embedding, response)

Tools: GPTCache, Redis with vector similarity, Langfuse caching layer.

Huge win on FAQ-like traffic (support bots, regulatory Q&A with repeated questions).

Lever 4 — Batch API for async workloads (50% cost reduction)

Any job that doesn't need a real-time response goes to the batch endpoint:

  • Nightly corpus re-evaluation
  • Bulk document summarization
  • Offline eval suite runs
  • Dataset enrichment

Lever 5 — Context window discipline

Long contexts cost quadratically in some setups. Keep context tight:

  • Retrieve 5–10 chunks, not 50
  • Summarize conversation history instead of passing full history
  • Use parent-document retrieval (small retrieval chunks, larger context chunks) to avoid bloat

Lever 6 — Quantization for self-hosted models

FormatVRAM vs FP16Quality lossUse case
FP16NoneDev, quality benchmarking
INT8 (bitsandbytes / GPTQ)~0.5×MinimalProduction default for 7–13B models
INT4 (GGUF / AWQ / GPTQ)~0.25×SmallEdge, memory-constrained servers
GGUF (llama.cpp)0.25–0.5×SmallCPU inference, local dev

For Project 3: INT8 is the production default. INT4 for models running on commodity hardware.


Inference Servers

Key features:

  • PagedAttention — GPU memory manager for KV cache; eliminates fragmentation
  • Continuous batching — non-negotiable for throughput; don't use naive batching
  • Prefix caching — reuse KV cache for shared prompt prefixes across requests
  • Tensor parallelism — split model across multiple GPUs (--tensor-parallel-size N)
  • Streaming — first-token latency what users feel; always stream
vllm serve Qwen/Qwen3-8B \
--tensor-parallel-size 2 \
--max-model-len 32768 \
--enable-prefix-caching \
--dtype bfloat16

SGLang

Best for structured generation (JSON, tool calls) and complex multi-call programs. RadixAttention for efficient prefix sharing. Choose over vLLM when your workload is heavily structured-output or tool-calling.

TGI (Text Generation Inference)

HuggingFace's production server. Good default for HF-hosted models. Flash Attention 2, continuous batching, token streaming. Less flexible than vLLM for custom configurations.

llama.cpp

CPU inference via GGUF quantized models. Use for: local dev without GPU, edge deployment, ultra-low-cost commodity servers. Not for high-throughput production.

TensorRT-LLM

NVIDIA-optimized. Maximum throughput on H100/A100. Complex to set up, but 2–4× throughput improvement over vLLM on the right hardware. Use only when GPU costs dominate and you're on NVIDIA hardware.


Inference Optimization Techniques

Speculative decoding (2–3× speedup)

A small draft model proposes N tokens. The large model verifies them in a single forward pass — accepting or rejecting. Accepted tokens come "for free."

Draft model (Qwen 0.5B) → proposes ["The", "policy", "covers", "damage", "to"]
Verification model (Qwen 8B) → accepts ["The", "policy", "covers"] rejects ["damage"]
Net speedup: ~2.5× on typical insurance text

Best gain when draft and verifier are from the same model family (Qwen 0.5B + Qwen 8B).

Parallelism strategies

StrategyWhen to use
Tensor parallelismModel too large for one GPU; split layers across GPUs; low latency
Pipeline parallelismVery large models (70B+); pipeline stages across GPUs; higher throughput
Data parallelismMultiple independent model replicas; horizontal scaling

For Project 3 (7B–13B open model on 2–4 GPUs): tensor parallelism with --tensor-parallel-size 2.

Continuous batching

vLLM and TGI do this by default. Never use naive static batching (batch fills, waits for all to finish, returns). With continuous batching, new requests join the batch as others complete — maximizes GPU utilization.

Streaming (mandatory)

Always stream tokens to the frontend. First-token latency is what users perceive. Full-response latency is irrelevant to UX if the first token arrives in < 500ms.


Observability for LLMOps

Every request must emit:

MetricHow
Input tokensFrom model response headers
Output tokensFrom model response headers
Cost per requestinput_tokens × input_price + output_tokens × output_price
First-token latencytime_to_first_token - request_start
Full latencyresponse_complete - request_start
Model name + versionTag on every trace
Prompt name + versionTag on every trace
Cache hit (yes/no)From prompt cache or semantic cache
Retrieval score (top chunk)From vector DB
Judge scoreFrom eval pipeline
Tenant IDFor per-tenant cost accounting

Tool: Langfuse (self-hosted for Project 3, cloud for Projects 1–2) + Prometheus/Grafana for infra metrics.

Dashboards to build:

  • Cost per query over time (by feature, by tenant, by model)
  • Cache hit rate (prompt cache + semantic cache)
  • p50 / p95 / p99 latency by model
  • Groundedness rate (rolling 7-day)
  • Token budget burn rate (daily, vs monthly cap)

Incident Response

Kill-switch

Every AI feature has a kill-switch: one config change disables AI output and falls back to a legacy UX (show raw search results, show "feature temporarily unavailable"). No redeploy needed.

if feature_flags.get("rag_answer_enabled") is False:
return fallback_search_results(query)

Incident categories for AI systems

CategorySymptomsFirst action
Mass hallucinationSpike in low groundedness scoresKill-switch → rollback prompt/model
Prompt injection breachUnusual tool calls, data in responses that shouldn't be thereKill-switch → audit logs → investigate retrieval
Cost spikeToken spend 10× normalRate limit immediately → identify runaway request pattern
Judge driftGroundedness scores change without model changeRe-calibrate judge against human labels
Embedding index corruptionRetrieval returns irrelevant chunksRe-index from source
Vendor outageFrontier API returning 500sFailover to self-hosted SLM (if available) or kill-switch

Post-mortem template

Incident: [title]
Date: [date]
Duration: [start → resolution]
Impact: [N users affected, $ cost, SLO breach?]

Root cause category: [ ] prompt [ ] retrieval [ ] model [ ] tool [ ] data [ ] infra

Timeline:
HH:MM — [event]
HH:MM — [event]

Root cause: [one paragraph]

Contributing factors: [list]

Fix applied: [what was done]

Prevention: [what changes to make so this doesn't recur]

Eval set update: [what adversarial cases were added to the test suite]

The last line is mandatory — every incident must produce at least one new test case.