What LLMOps is
Link to section “What LLMOps is”In 30 seconds
Link to section “In 30 seconds”LLMOps is the practice of running applications built on large language models (LLMs) in production and keeping them correct, safe, fast and affordable. An LLM application is a prompt, a model you usually rent, the documents retrieved into the prompt, the tools the model may call, and the checks around them. LLMOps versions that combination, evaluates every change before release, traces each request, and reacts when the provider, the data or the users change.
The label is common but not settled:
- TechTarget: it “narrows the focus of the machine learning operations (MLOps) framework”.[1]
- Google Cloud: “a specialized subset of MLOps”, a model-centric reading.[2]
- Microsoft says GenAIOps, “sometimes known as LLMOps”, and governs the application: the system that orchestrates the logic, builds the prompt and calls the model.[3]
- A 2024 IEEE conference paper: “This term is so recent that the scientific literature has not yet agreed on a common definition for it”.[4] Overlapping labels include AI engineering[5] and AgentOps.[6]
Who this page is for. Beginners: this part, the glossary, Lab 1. Engineers: practices, agents, traces, labs. Leaders: Deciding and adopting.
At a glance
Link to section “At a glance”| Aspect | Summary |
|---|---|
| Goal | LLM features whose quality, safety, latency and token use are measured, gated before release and watched after it |
| Unit of work | An LLM application or agent: prompts, model choice, retrieval, tools, guardrails and orchestration code |
| Main users | AI application and platform engineers, security engineers, domain experts who grade answers |
| In scope | Prompt versioning; evaluation; retrieval; injection containment; agent limits; tracing; latency and token budgets; routing and caching; hallucination control; prompt versus retrieval versus fine-tuning; provider change |
| Prerequisites | MLOps concepts; rates and confidence intervals; ObservabilityOps helps |
| Key outputs | Release manifests, an eval suite and gate, a calibrated judge, traces, a tool policy, a migration runbook |
| Term status | Emerging. Definitions differ, labels compete, no standards body defines it[4] |
| Last verified | 2026-10-18 |
The problem LLMOps solves
Link to section “The problem LLMOps solves”An LLM call looks like an API call but does not behave like one:
- The same input can give different outputs. “LLMs are stochastic – there’s no guarantee that an LLM will give you the same output for the same input every time.”[7] New York City’s business chatbot told one Markup reporter that landlords had to accept housing vouchers, and ten other staff that they did not.[8]
- Small edits have large effects. “Small changes to a prompt can lead to very different results”.[7]
- Correctness is open-ended. LinkedIn found evaluating answer quality “more difficult than anticipated”.[9]
- The model changes under you. GPT-3.5 and GPT-4 behaviour “can vary greatly over time”;[10] providers retire models.
- Text becomes an attack surface. LLM-integrated applications “blur the line between data and instructions”.[11]
- Every request costs tokens and time. “The cost of LLMOps is in inference.”[7]
Wrong answers have owners. In Moffatt v. Air Canada (2024), British Columbia’s Civil Resolution Tribunal (small claims) found negligent misrepresentation: the chatbot had wrongly said a bereavement fare could be claimed retroactively, and the airline “did not take reasonable care to ensure its chatbot was accurate” (award: C$812.02).[12] ModelOps draws the accountability lesson; the engineering lesson is that policy answers need must-pass tests.
What LLMOps is not
Link to section “What LLMOps is not”It is not the training or fine-tuning pipeline (MLOps), model approval and risk tiering (ModelOps), spend modelling (FinOps for AI), ownership of the documents retrieval reads (KnowledgeOps), AI applied to IT operations (AIOps) or application-security depth (DevSecOps); the neighbour table gives hand-offs.
Where LLMOps comes from and where it sits
Link to section “Where LLMOps comes from and where it sits”Origins and evolution
Link to section “Origins and evolution”Who coined “LLMOps” is not recorded in the sources found; its practices can be dated:
| Date | Milestone | Why it mattered |
|---|---|---|
| 2020 | Retrieval-augmented generation (RAG) paper[13] | Retrieval feeds generation |
| Early 2023 | “LLMOps” gains prominence[1] | The label appears |
| 2023-02 | Indirect prompt injection paper[11] | Retrieved text can steer applications |
| 2023-04 | Chip Huyen, “Building LLM applications for production”[7] | Prompt versioning, evaluation and cost |
| 2023-06/07 | LLM-as-a-judge study;[14] hosted-model drift[10] | Model graders have limits; hosted models change |
| 2024-01 | Microsoft’s “LLMOps maturity model”, four levels;[15] now titled GenAIOps[16] | A vendor adopts the application view |
| 2024 | SpliTech paper (June);[4] NIST AI 600-1 (July)[17] | No agreed definition; a risk vocabulary |
| 2025 | Injection design patterns (June);[18] OWASP agentic Top 10 (December)[19] | Architectural defences; agent risks |
Glossary and mental model
Link to section “Glossary and mental model”Mental model: you ship a recipe, not a model. The release is a tuple of prompt version + model snapshot + retrieval configuration + tools + guardrails, around a rented, non-deterministic model. Evals are the test suite, traces are the debugger (one span per step), and routing is the release lever: a gateway weight for a model, a flag for a prompt.
| Term | Meaning | Example |
|---|---|---|
| Model snapshot | A fixed model version, unlike an alias that can move (every Claude model ID is a pinned snapshot)[20] | A dated ID in the manifest |
| RAG | Retrieve relevant chunks (passages stored as embedding vectors) and insert them into the prompt[13] | Top-5 policy chunks |
| Eval, slice, must-pass case | A test on cases with a scoring rule; a slice is a subset scored on its own; a must-pass case must pass in every sample | The refunds slice |
| Assertion | A deterministic check: contains, regex, valid JSON, latency[21] | Contains “30 days” |
| Paired comparison | Both versions run on the same cases; per-case differences are analysed | v8 minus v7 |
| Confidence interval, margin | The range the true difference plausibly lies in; the largest drop you accept. A gate passes if the interval’s lower end stays above minus the margin | [−0.15, +0.10] fails 0.05 |
| LLM-as-judge, TPR, TNR | A model grading output against a rubric; the share of good answers it passes (TPR) and of bad answers it fails (TNR) | TPR 0.94, TNR 0.88 |
| Recall@k, MRR | Share of relevant documents in the top k; mean of 1 ÷ rank of the first relevant one | Recall@5 = 0.90 |
| Prompt injection | Text that alters the model’s behaviour in unintended ways; indirect when it arrives in websites or files[22] | “Ignore your rules” |
| Confabulation | NIST’s term for confidently stated false content, “known colloquially as “hallucinations””[17] | An invented refund exception |
| Canary | A release served first to a small share of traffic | 5% of requests |
| TTFT | Time to first token (for API clients, to the first streamed chunk) | 0.9 s |
| Agent | A system where “LLMs dynamically direct their own processes and tool usage”; a workflow follows “predefined code paths”[23] | A refund agent |
How LLMOps relates to other Ops
Link to section “How LLMOps relates to other Ops”The boundaries below are editorial distinctions of this Atlas; vendors draw them differently.
| Neighbour | LLMOps owns | The neighbour owns |
|---|---|---|
| MLOps | Prompting versus retrieval versus fine-tuning; evaluating answers | Training pipelines, registry, predictive-model drift |
| ModelOps | Regressions after provider updates; eval evidence | Approval, risk tier, periodic review, regulation |
| FinOps for AI | Routing, caching, batching: building them, effect on quality and latency | Their cost effect, allocation, who decides |
| KnowledgeOps | Chunking, embedding, indexing, retrieval metrics, access filters | Document ownership, review and freshness |
| DataOps | Index pipelines as part of the release | Upstream data quality and contracts |
| ObservabilityOps | Which LLM signals to record; reading a trace | Collection, storage, redaction plumbing |
| DevSecOps, AIOps for Security | Injection containment, tool policy | Code and supply-chain controls; AI in the SOC |
| SRE, IncidentOps | Quality signals, LLM runbooks | SLO method, incident process |
Also: PrivacyOps, ProcurementOps, DecisionOps, ChatOps.
Diagram source (Mermaid)
%%{init: {"flowchart": {"nodeSpacing": 24, "rankSpacing": 34}}}%%
flowchart TB
%% Unlinked nodes in a subgraph: direction TB = one row, LR = one column
subgraph UP["Upstream inputs"]
direction TB
KO["KnowledgeOps: the corpus"]
DO["DataOps: data pipelines"]
ML["MLOps: fine-tuned weights"]
end
LL["LLMOps: release, evaluate and run the LLM application"]
subgraph SIDE["Alongside"]
direction LR
MO["ModelOps: approval"]
FA["FinOps for AI: spend"]
DS["DevSecOps: code security"]
end
subgraph DOWN["Shared signals"]
direction LR
OB["ObservabilityOps"]
SR["SRE"]
IN["IncidentOps"]
end
UP --> LL
LL <--> SIDE
LL --> DOWN
classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
class LL core
class KO,DO,ML,MO,FA,DS,OB,SR,IN neutral
Text equivalent: Upstream, KnowledgeOps supplies the corpus, DataOps the pipelines and MLOps any fine-tuned weights. Alongside, ModelOps approves models, FinOps for AI owns spend, DevSecOps secures code. LLMOps shares signals with ObservabilityOps, SRE and IncidentOps.
Principles and operating model
Link to section “Principles and operating model”Charter and principles
Link to section “Charter and principles”No body publishes LLMOps principles. Hamel Husain finds that unsuccessful AI products “almost always share a common root cause: a failure to create robust evaluation systems.”[24] The charter below is this Atlas’s proposal.
Fundamentals: the building blocks
Link to section “Fundamentals: the building blocks”| Building block | Job | “Good” looks like |
|---|---|---|
| Prompt registry | Immutable prompt versions with aliases | “Prompt versions in MLflow are immutable”[25] |
| Gateway | Provider keys, routing, quotas, fallbacks, staged rollout[3] | Applications hold no provider keys |
| Retrieval stack | Chunk, embed, index; search with permission filters | Named index versions; recall per release |
| Orchestrator or agent runtime | Build the prompt; call retriever, model and tools | Workflows by default;[23] step limits |
| Guardrails | Input, output, retrieval and tool checks[26] | False-block rate measured |
| Eval harness | Run cases, score, compare versions | In CI; grows from production failures |
| Tracing and feedback | Steps, tokens, latency, feedback per request | A complaint leads to its trace |
| Release manifest | Pin the whole tuple | Reviewed like code |
Roles, ownership and RACI
Link to section “Roles, ownership and RACI”Microsoft warns of “challenges if you omit data scientists” from prompt and RAG experiments.[3] Measurement skill and domain judgement must both own the eval suite.
| Activity | Product owner | AI app engineer | Domain expert | AI platform | Security |
|---|---|---|---|---|---|
| Task, criteria and must-pass cases | A | R | R | I | C |
| Prompt and orchestration changes | I | A/R | C | I | I |
| Eval suite, gate, judge calibration | C | A/R | R (labels) | C | I |
| Gateway, pinning, quotas, fallbacks | I | C | — | A/R | C |
| Tool policy, agent limits, injection containment | C | R | I | C | A |
| Model migration after a deprecation notice | A | R | R (grading) | R | C |
R = responsible, A = accountable, C = consulted, I = informed. An Atlas template: one “A” per row.
Lifecycle: Day 0, Day 1, Day 2
Link to section “Lifecycle: Day 0, Day 1, Day 2”Diagram source (Mermaid)
flowchart TB
A["Define the task, criteria and failure modes"]
B["Build or extend the eval set"]
C["Change one component: prompt, model, retrieval, tools"]
D["Offline evals: assertions, judge, expert sample"]
E{{"Gate passes?"}}
F["Canary on a small share of traffic"]
G["Production: traces, feedback, sampled review"]
H["Provider notice: new snapshot or retirement"]
A --> B --> C --> D --> E
E -->|"no"| C
E -->|"yes"| F --> G
G -->|"new failure becomes a case"| B
H --> C
classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
class A,B,C,D,F,G core
class E decision
class H risk
Text equivalent: Define the task and failure modes, build an eval set, change one component, run offline evals. A failed gate sends the change back; a passed one goes to a canary and production, where traces, feedback and reviews turn new failures into cases. A provider notice enters as a change.
| Phase | What happens | Exit evidence |
|---|---|---|
| Day 0: frame | Task, who a wrong answer harms, a non-LLM baseline; grade 50–100 outputs by hand | Written criteria; first eval set |
| Day 1: release | Pin the snapshot; register prompt v1; guardrails, tool policy, gateway route; canary | Release manifest; traces; rollback rehearsed |
| Day 2: operate | Review traces and complaints; add cases; re-calibrate judges; handle provider notices | Growing suite; migration log |
Provider change management (as of October 2026). Anthropic gives “at least 60 days’ notice before model retirement for
publicly released models” on its own platforms, while Amazon Bedrock and Google Cloud “set their own retirement schedules”. On
Claude 4.7 and later a non-default temperature, top_p or top_k returns a 400 error, and its Python SDK (v1.0 and later)
removes the three parameters altogether, so passing one raises a TypeError.[27] OpenAI promises “At least 6 months” for generally available models, 3 months for specialised variants
and “much shorter notice, such as 2 weeks” for previews, “Unless safety or compliance concerns require a faster timeline”; its
2026-10-14 notice retires GPT-4o and GPT-4.1 models on 2027-01-14, fine-tuned versions included.[28] Plan for
the shortest notice: a provider-hosted fine-tune retires with its base model, and only weights you host yourself are outside a
provider’s schedule.
Operating model and governance
Link to section “Operating model and governance”| Change | Minimum gate | Approver |
|---|---|---|
| Prompt wording | Full suite, paired comparison | Engineer + domain reviewer |
| Model snapshot or parameters | Full suite, more samples; canary; latency and tokens | Product owner (ModelOps approves third parties) |
| Retrieval configuration | Retrieval and permission tests, then full suite | Engineer + platform |
| Corpus content | Access labels by the owner (KnowledgeOps); targeted cases | Content owner |
| Tools, scopes or agent limits | Injection and agent task tests, approval path | Security |
| Judge prompt or model | Re-calibration on held-out labels | Eval owner |
Ceremonies: weekly quality review (traces, complaints, new cases); monthly provider review; quarterly red-teaming; post-incident reviews via IncidentOps.
Architecture and workflow
Link to section “Architecture and workflow”Reference architecture
Link to section “Reference architecture”In Microsoft’s description “an orchestration layer queries your data sources and injects the most relevant results into the prompt”, then calls the model.[3]
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 130, "nodeSpacing": 20, "rankSpacing": 34}}}%%
flowchart TB
U["User or calling system"]
GI["Input guardrails"]
APP["Orchestrator: builds the prompt, runs the loop"]
RET["Retriever + permission filter"]
GW["Gateway: keys, routing"]
TL["Tool router + policy"]
GO["Output checks"]
IDX[("Index of untrusted documents")]
MOD["Model: pinned hosted snapshot or self-hosted"]
ANS["Answer to the user"]
TR[("Traces and eval store")]
U --> GI --> APP
APP <-->|"query, chunks"| RET
RET --> IDX
APP <-->|"prompt, response"| GW
GW --> MOD
APP <-->|"tool call, result"| TL
APP -->|"final draft"| GO --> ANS
RET -..-> TR
GW -..-> TR
TL -..-> TR
GO -..-> TR
classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
class GI,APP,RET,GW,TL,GO core
class U,IDX risk
class MOD,ANS,TR neutral
Text equivalent: Teal nodes are your components, red nodes carry untrusted input, grey nodes are models, stores and outputs. After input guardrails, the orchestrator queries the permission-filtering retriever over the index, sends prompts through the gateway to a pinned hosted or self-hosted model, and runs any requested tool through the policy-checking router. The final draft passes output checks (schema, citations). Components write spans to the trace store.
- Three untrusted inputs: user input, retrieved text, and the model’s own output before it reaches a browser, shell or database (OWASP’s LLM05, “Improper Output Handling”).[29]
- The gateway is on the release path: with model versioning it enables staged rollout and rollback.[3] It holds every provider key, so guard its supply chain: in March 2026 two LiteLLM releases on PyPI carried a credential stealer for about 40 minutes; its Docker image, which pins dependencies, was not affected.[30]
End-to-end workflow: shipping a prompt change
Link to section “End-to-end workflow: shipping a prompt change”Diagram source (Mermaid)
%%{init: {"flowchart": {"nodeSpacing": 24, "rankSpacing": 34}}}%%
flowchart TB
PR["Pull request: prompt v8 and release manifest"]
CI["CI: assertions, 3 samples per case"]
JG["Judge-graded cases and paired comparison"]
G{{"Gate: margin, slices, must-pass cases"}}
FX["Rework; add failing cases"]
RV["Review; security if tools changed"]
CN["Canary: 5% of traffic, by flag"]
M{{"Online checks within budget?"}}
RB["Roll back to previous release"]
FL["25%, then 100%"]
PM["Review; new eval cases"]
PR --> CI --> JG --> G
G -->|"block"| FX --> PR
G -->|"pass"| RV --> CN --> M
M -->|"yes"| FL
M -->|"no"| RB --> PM
classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
class PR,CI,JG,RV,CN,FL core
class G,M decision
class FX,RB,PM risk
Text equivalent: A pull request carries the new prompt and release manifest. CI runs assertions on three samples per case, then judge grading and a paired comparison. The gate checks margin, slices and must-pass cases; a block sends the change back with new cases. A pass leads to review and a 5% canary, then 25% and 100% within budget; otherwise traffic returns to the previous release and a review adds eval cases.
The canary watches format errors, guardrail blocks, thumbs-down rate, latency and tokens; bad canary traces become eval cases.
Traces: the debugging unit
Link to section “Traces: the debugging unit”A trace answers “what happened on this request?” OpenTelemetry’s GenAI conventions (status Development) define a span per model
call with prompt name and version, models, tokens and finish reasons, plus retrieval and execute_tool spans.[31][32]
Example status: illustrative, not captured; names checked against the conventions on 2026-10-17 (output_check is a custom
span).
trace 4bf92f35 POST /assist 2.9 s release=policy-assistant-2026.10.3 user=u-1842
retrieval kb-chunks-v14 0.12 s gen_ai.retrieval.top_k=5
gen_ai.retrieval.documents (opt-in): refund-policy#4 0.82, warranty#2 0.79, ...
chat example-model-2026-09-01 0.88 s gen_ai.prompt.name=support-answer
gen_ai.prompt.version=8 gen_ai.usage.input_tokens=3412
gen_ai.usage.output_tokens=41 gen_ai.response.finish_reasons=["tool_call"]
execute_tool get_order_status 0.21 s gen_ai.tool.call.id=call_7
chat example-model-2026-09-01 1.64 s gen_ai.usage.output_tokens=600
gen_ai.response.finish_reasons=["length"] gen_ai.response.time_to_first_chunk=0.91
output_check schema-check-v3 0.01 s result=fail (truncated JSON)
event user_feedback rating=thumbs_down trace_id=4bf92f35
Read it in this order. (1) Release: do prompt version and gen_ai.response.model match the manifest? (2) Retrieval: the
right document, at what rank? (3) Model calls: finish reason and tokens; here length at the 600-token cap truncated the JSON.
(4) Tools: which tool, for which user, what result. Put the trace ID on every feedback event.
Retention. Documents, messages and tool arguments are opt-in attributes, and instrumentations “SHOULD NOT capture” content by default.[31][32] Keep IDs, scores, versions and counts; capture content only for a redacted sample, with retention agreed with PrivacyOps. Keep every trace with an error, a block or negative feedback, and a fraction of the rest.
Practices, frameworks and tools
Link to section “Practices, frameworks and tools”Evaluation in depth: the hardest part
Link to section “Evaluation in depth: the hardest part”Layer the evals by cost. Husain’s three levels are unit tests, model and human evaluation, and A/B testing, and “The cost of Level 3 > Level 2 > Level 1”.[24] So: assertions on every commit, judge and expert samples per release candidate, experiments for changes that matter to the business.
Build the dataset from failures. Read real outputs, name what goes wrong (“invents a refund exception”), and write cases per category. Criteria will move: Shankar et al. call it criteria drift, “users need criteria to grade outputs, but grading outputs helps users define criteria”.[33] Version the dataset, tag slices, mark must-pass cases.
| Scoring method | Good for | Pitfall |
|---|---|---|
| Assertions[21] | Format, required facts, budgets | Passes fluent nonsense |
| Reference comparison | Extraction, classification | Penalises correct rewording |
| LLM-as-judge with a rubric | Open-ended quality at scale | Biases; must be calibrated |
| Expert review of a sample | Ground truth, calibration labels | Slow; needs guidelines |
| Online signals (feedback, A/B) | Real outcomes | Selection bias |
Treat non-determinism statistically. Run each case several times and score its pass rate. Miller recommends inference on “question-level paired differences”, clustered standard errors, resampling and power analysis.[34]
Calibrate every judge on data it was not tuned on. Strong judges can reach “over 80% agreement”, with position, verbosity and self-enhancement biases;[14] LLM evaluators “inherit all the problems of the LLMs they evaluate”.[33] Use binary pass/fail criteria. Husain’s procedure: an expert labels examples, then you “split them into train, dev, and test sets”; a few go into the judge prompt, the dev set is for revising it, and the hidden test set is run once on the frozen prompt. Report true-positive and true-negative rates separately, because raw agreement misleads when failures are rare.[35] An Atlas rule: approve a judge only on a test set with at least 30 expert-labelled failures; with 8, one misjudged answer moves the TNR by 12.5 points. Pin the judge’s snapshot; GitHub routinely audits its judge’s outputs.[36]
LLMs in the loop. Models can extend a dataset from your prompts and tests (promptfoo’s generate dataset),[37]
group failure notes into categories and propose prompt changes; Husain and Shankar keep people on the first 30–50 traces and on
every label that tests a judge.[38]
Retrieval engineering (RAG)
Link to section “Retrieval engineering (RAG)”Microsoft’s pipeline: chunk, enrich, embed, index, with experiments on each step and on search type.[3] Six habits:
- Measure it separately: recall@k and mean reciprocal rank. No generator fixes a missing document.
- Mind position and length. Performance “significantly degrades” when relevant information sits in the middle of long contexts.[39] More chunks is not automatically better.
- Version the index. pgvector warns, “Unlike typical indexes, you will see different results for queries after adding an approximate index.”[40] A new embedding model needs a new index (vectors from different models are not comparable), built beside the old one.
- Filter by permission inside the query. OWASP calls for “permission-aware vector and embedding stores”;[41] “do not reveal finance documents” in a prompt is not access control. Lab 3 tests the filter.
- Keep the index current and erasable, including “right to be forgotten requests”.[3] Documents belong to KnowledgeOps, deletion duties to PrivacyOps.
- Make claims checkable (hallucination control). Anthropic’s basics: permit “I don’t know”; for long documents,
ground answers in direct quotes; and have each claim cite a quote, retracting claims without one.[42]
So: answer only from the retrieved context, abstain when it is silent, return citations as chunk IDs, and check in code that
each cited chunk was retrieved and contains the quote (
citation-check-v1). Report abstentions beside unsupported claims: an abstaining answer has none. This reduces hallucinations; it does not “eliminate them entirely”.[42]
Prompt injection, guardrails and tool permissions
Link to section “Prompt injection, guardrails and tool permissions”OWASP lists seven mitigations, including least privilege and human approval, and doubts that “fool-proof” prevention exists.[22] Simon Willison’s lethal trifecta is the combination to avoid: “Access to your private data”, “Exposure to untrusted content” and “The ability to externally communicate”; for filters that catch 95% of attacks, “95% is very much a failing grade.”[43] A rendered link communicates: via a public-channel message, PromptArmor made Slack AI show a link carrying private-channel data.[44]
The durable answer is architectural. Beurer-Kellner et al. propose six design patterns (Plan-Then-Execute and Dual LLM among them) sharing one principle: once an agent “has ingested untrusted input, it must be constrained so that it is impossible for that input to trigger any consequential actions”. Dual LLM has a precondition: “the output of a quarantined LLM must satisfy certain safety constraints”, or it returns “symbolic responses, that the privileged LLM can manipulate without observing their content”.[18] A free-text summary handed to the planner meets neither. OWASP’s excessive-agency guidance adds minimal tools and permissions, tools run in the user’s context, and approval for high-impact actions.[45]
Diagram source (Mermaid)
%%{init: {"flowchart": {"nodeSpacing": 24, "rankSpacing": 34}}}%%
flowchart TB
%% Unlinked nodes in a subgraph: direction TB = one row, LR = one column
subgraph UT["Untrusted text"]
direction TB
W["Web page"]
E["E-mail or ticket"]
D["Retrieved document"]
end
Q["Quarantined model, no tools: typed fields only (IDs, codes, amounts)"]
V["Validate: schema and allowed values"]
S["Free-text summary: to the user only"]
RQ["The user's own request"]
P["Planner: chooses actions"]
T{"Tool policy"}
R["Read tools, user-scoped"]
H["Write or send: human approval"]
L["Links only to allow-listed domains"]
UT --> Q --> V --> P
Q -.-> S
RQ --> P --> T
T --> R
T --> H
T --> L
classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
class W,E,D,Q risk
class V,RQ,P,R,H,L core
class T decision
class S neutral
Text equivalent: Untrusted text (web pages, e-mails, retrieved documents) goes only to a quarantined model without tools. It returns typed fields (IDs, codes, amounts), validated against a schema and allowed values before the planner sees them; a free-text summary goes to the user, never the planner. The planner also gets the user’s request and acts through a tool policy: user-scoped reads, approval for writes or sends, allow-listed links. This limits what injected text can trigger; it does not detect injection.
Which design applies? In the reference implementation, retrieved
policy text reaches the tool-capable model, so the tool policy compensates: reads run as the user, the only write needs approval
(after_untrusted_input). Quarantine text that outsiders write, such as customer e-mails.
Guardrails still help as layers (for example NeMo Guardrails’ input, retrieval and output rails).[26] Measure their false-block rate, and never use a classifier where a permission would do.
Operating agents: evaluation, limits and tool trust
Link to section “Operating agents: evaluation, limits and tool trust”An agent turns one request into a loop of model and tool calls, so errors compound and cost varies.[23] In Anthropic’s agent-eval terms, each attempt at a task is a trial, and the outcome is “the final state in the environment at the end of the trial”.[46]
- Grade the outcome first. Compare the end state with the goal, as τ-bench compares “the database state at the end of a conversation with the annotated goal state”:[47] did exactly one correct refund request appear?
- Assert only the path rules that matter. Anthropic finds “it’s often better to grade what the agent produced, not the path it took”,[46] so check only that forbidden tools were never called, writes followed approval, and steps and tokens stayed in budget.
- Repeat in isolation. Start each trial “from a clean environment” and report pass^k, “the probability that all k trials succeed”, where users need consistency.[46] In τ-bench (2024), GPT-4o solved under 50% of tasks, and pass^8 was under 25% in retail.[47] Lab 1’s gate works unchanged with one trial per sample.
- Bound every run: a step limit; time and token budgets (OWASP asks for quotas, timeouts and throttling);[48]
idempotency keys on writes, so a retried step cannot refund twice; approval before consequential actions; a kill switch for
tools. OpenTelemetry’s
gen_ai.invoke_agent.tool_callsmetric counts tool calls per invocation.[49] - Trust tool servers like dependencies. Model Context Protocol (MCP) servers “MUST NOT accept any tokens that were not explicitly issued for the MCP server” and should start from a “Minimal initial scope set”.[50] Pin third-party servers; treat their tool descriptions and results as untrusted text.
OWASP’s Top 10 for Agentic Applications for 2026 lists the risks, from ASI01 Agent Goal Hijack to ASI10 Rogue Agents.[19]
Methods, standards and frameworks
Link to section “Methods, standards and frameworks”| Source | Type | Use it for |
|---|---|---|
| OWASP Top 10 for LLM Applications 2025;[29] Agentic Applications for 2026[19] | Community risk lists | Threat checklists |
| NIST AI 600-1 (2024-07-26)[17] | Voluntary US profile of the AI RMF | Risk vocabulary, suggested actions |
| OpenTelemetry GenAI conventions[32][31][49] | Telemetry convention, “Development” | Span and metric names |
| MCP specification 2025-11-25[50] | Protocol specification | Token audience, scopes |
| Microsoft GenAIOps guidance;[16] provider deprecation policies | Vendor guidance and policy | Maturity levels; notice periods |
Binding rules come only from law and contracts.
Tooling landscape
Link to section “Tooling landscape”Choose by job, then product (checked 2026-10-17).
| Job | Representative options (verified) | Selection criteria and risks |
|---|---|---|
| Tracing, prompts and evals | Langfuse (MIT except ee folders, self-hostable);[51] Arize Phoenix (Elastic License 2.0);[52] MLflow 3.17.1 (Apache 2.0; tracing, evaluation, prompt registry and optimisation)[53] |
Where prompts and answers are stored; OpenTelemetry; export |
| Eval runners in CI | promptfoo (MIT; “now part of OpenAI”, per its README)[54] | Exit codes; repeated sampling; vendor neutrality |
| Gateway | LiteLLM (weighted routing, fallbacks);[55][56] cloud API gateways | Holds all keys: supply chain, least privilege |
| Guardrails | NeMo Guardrails (Apache 2.0);[26] classifier models | False-block rate; added latency |
| Retrieval | pgvector (HNSW and IVFFlat in PostgreSQL);[40] vector databases; hybrid search | Permission filtering in the query; recall at your latency budget |
Lock-in to watch: proprietary trace formats, registries without export, judges on a vendor’s moving model.
LLMOps in practice
Link to section “LLMOps in practice”Reference implementation: a policy-answering assistant
Link to section “Reference implementation: a policy-answering assistant”Illustrative design. A retailer’s assistant answers policy questions and opens refund requests: a hosted model behind a gateway, a pgvector index, CI, a support expert. It is a workflow, not an agent.
Example status: YAML syntax-checked; not executed (illustrative names).
# llm-release.yaml - one record per release of the assistant (illustrative values)
release: policy-assistant-2026.10.3
owner: support-ai-team
components:
prompt: {id: support-answer, version: 8} # immutable registry version
model:
provider: example-provider
id: example-model-2026-09-01 # pinned snapshot, never a moving alias
max_output_tokens: 600
fallback: example-model-small-2026-08-15 # evaluated like the primary
retrieval:
index: kb-chunks-v14
embedding_model: example-embed-v3
top_k: 5
permission_filter: required # enforced in the retriever
guardrails: {input: [pii-redaction-v2], output: [schema-check-v3, citation-check-v1]}
tool_policy: tool-policy-v4
evaluation:
suite: support-answers-v12
samples_per_case: 3
gate: {non_inferiority_margin: 0.05, max_slice_drop: 0.10, must_pass: all}
judge:
rubric: refund-rubric-v5
model: example-judge-2026-07-15 # not the generator (self-enhancement bias)
calibration: cal-2026-10-16 # approved on a held-out test set
report: reports/eval-2026-10-15.json
rollout:
steps_percent: [5, 25, 100]
hold_hours_per_step: 24
rollback_to: policy-assistant-2026.10.2
approvals: [product-owner, ai-security] # security: the tool policy changed
Example status: YAML syntax-checked; not executed. Enforcement belongs in the orchestrator code, not in the prompt.
# tool-policy-v4.yaml - what the assistant may do (illustrative)
default: deny
tools:
- name: search_policies
access: read
runs_as: end_user # the user's token, so the index filter applies
- name: get_order_status
access: read
runs_as: end_user
- name: create_refund_request
access: write
requires_human_approval: true
max_amount_eur: 200
idempotency_key: required # a retried step cannot create a second request
limits:
max_tool_calls_per_request: 4 # then stop and hand over to a person
request_timeout_seconds: 30
kill_switch: feature-flag/assistant-tools # disables all tools without a deploy
output:
links: allow_list
allowed_domains: [help.example.com]
untrusted_sources: [retrieved_documents, customer_emails]
after_untrusted_input: disable_write_tools_without_approval
Gateway route for a model canary.
Example status: YAML syntax-checked; not executed; keys from LiteLLM’s proxy docs (2026-10-17).[55][56]
# litellm-config.yaml - gateway route during a model canary (LiteLLM proxy)
# Pin the gateway version you tested: the gateway holds every provider key.
model_list:
- model_name: support-answer # the only name the orchestrator calls
litellm_params:
model: openai/<current-snapshot>
api_key: os.environ/OPENAI_API_KEY # provider keys stay in the gateway
weight: 95
- model_name: support-answer
litellm_params:
model: openai/<candidate-snapshot> # about 5% of calls
api_key: os.environ/OPENAI_API_KEY
weight: 5 # rollback: delete this entry, redeploy
- model_name: support-answer-fallback
litellm_params:
model: openai/<small-snapshot> # evaluated like the primary
api_key: os.environ/OPENAI_API_KEY
router_settings:
routing_strategy: simple-shuffle # random pick in proportion to weight
litellm_settings:
num_retries: 2
request_timeout: 30 # seconds per call
# fallbacks also catch rate-limit (429) errors; test them in staging
fallbacks: [{"support-answer": ["support-answer-fallback"]}]
LiteLLM routes per request, so one conversation can meet both snapshots, unless its session_affinity check pins a session
ID to one deployment.[55]
Hands-on labs
Link to section “Hands-on labs”Labs 1–3 need only Python 3.8+, cost nothing, and were executed on 2026-10-17 with Python 3.8.10 (illustrative data). Lab 4’s promptfoo run needs a paid provider and was not executed.
Lab 1 (beginner): a release gate for a non-deterministic system
Link to section “Lab 1 (beginner): a release gate for a non-deterministic system”- Objective: decide whether prompt v8 may replace v7, with three runs per case and version.
- Prerequisites: Python 3.8+, a terminal, 20 minutes. Cost and risk: none.
- The idea: per case, subtract v7’s pass rate from v8’s; the 95% interval around the mean difference shows how bad v8 could plausibly be. v8 passes only if that end is within a 5-point margin, no slice drops over 10 points, and every must-pass (“critical”) sample passes.
Steps
- In an empty folder, save the recorded results as
eval_results.json(1 = the sample passed its checks; cases without"critical": trueare ordinary cases).
Example status: JSON syntax-checked; this exact file was used for the executed run.
{
"suite": "support-answers v12",
"baseline": "prompt support-answer v7",
"candidate": "prompt support-answer v8 (shorter system prompt)",
"cases": [
{"id": "b01", "slice": "billing", "base": [1,1,1], "cand": [1,1,1]},
{"id": "b02", "slice": "billing", "base": [1,0,1], "cand": [1,1,1]},
{"id": "b03", "slice": "billing", "base": [0,0,1], "cand": [1,1,0]},
{"id": "b04", "slice": "billing", "base": [1,1,1], "cand": [1,1,1]},
{"id": "b05", "slice": "billing", "base": [1,1,0], "cand": [1,1,1]},
{"id": "b06", "slice": "billing", "base": [1,1,1], "cand": [1,1,1]},
{"id": "b07", "slice": "billing", "base": [0,1,0], "cand": [1,1,1]},
{"id": "b08", "slice": "billing", "base": [1,1,1], "cand": [1,0,1]},
{"id": "r01", "slice": "refunds", "base": [1,1,1], "cand": [1,0,1]},
{"id": "r02", "slice": "refunds", "base": [1,1,1], "cand": [0,0,1]},
{"id": "r03", "slice": "refunds", "base": [1,1,0], "cand": [0,1,0]},
{"id": "r04", "slice": "refunds", "base": [1,1,1], "cand": [1,1,1]},
{"id": "r05", "slice": "refunds", "base": [1,1,1], "cand": [0,1,0]},
{"id": "r06", "slice": "refunds", "base": [1,0,1], "cand": [1,0,1]},
{"id": "a01", "slice": "account", "base": [1,1,1], "cand": [1,1,1]},
{"id": "a02", "slice": "account", "base": [1,1,1], "cand": [1,1,1]},
{"id": "a03", "slice": "account", "base": [0,1,1], "cand": [1,1,1]},
{"id": "a04", "slice": "account", "base": [1,1,1], "cand": [1,1,1]},
{"id": "a05", "slice": "account", "base": [1,1,0], "cand": [1,0,1]},
{"id": "a06", "slice": "account", "base": [1,1,1], "cand": [1,1,1]},
{"id": "s01", "slice": "safety", "critical": true, "base": [1,1,1], "cand": [1,1,1]},
{"id": "s02", "slice": "safety", "critical": true, "base": [1,1,1], "cand": [1,1,1]},
{"id": "s03", "slice": "safety", "critical": true, "base": [1,1,1], "cand": [1,0,1]},
{"id": "s04", "slice": "safety", "critical": true, "base": [1,1,1], "cand": [1,1,1]}
],
"gate": {"non_inferiority_margin": 0.05, "max_slice_drop": 0.10}
}
- Save the gate as
eval_gate.py.
Example status: executed 2026-10-17 (Python 3.8.10).
"""Compare a candidate LLM release with the baseline on the same eval cases.
Standard library only (Python 3.8+). Each case was run several times per version,
because the same input can produce different outputs. A case's score is its pass
rate over those samples; versions are compared case by case (paired differences).
"""
import json
import math
import statistics
import sys
from collections import defaultdict
def rate(samples):
return sum(samples) / len(samples)
def main(path):
data = json.load(open(path))
cases, gate = data["cases"], data["gate"]
base = [rate(c["base"]) for c in cases]
cand = [rate(c["cand"]) for c in cases]
diffs = [b - a for a, b in zip(base, cand)]
n = len(diffs)
mean = statistics.mean(diffs)
se = statistics.stdev(diffs) / math.sqrt(n)
low, high = mean - 1.96 * se, mean + 1.96 * se
print(f"cases={n} baseline={statistics.mean(base):.3f} "
f"candidate={statistics.mean(cand):.3f}")
print(f"paired difference={mean:+.3f} 95% CI [{low:+.3f}, {high:+.3f}]")
reasons = []
if low < -gate["non_inferiority_margin"]:
margin = gate["non_inferiority_margin"]
reasons.append(f"CI lower bound {low:+.3f} is below -{margin} "
"(cannot rule out a regression)")
by_slice = defaultdict(list)
for c, a, b in zip(cases, base, cand):
by_slice[c["slice"]].append((a, b))
print("slice n baseline candidate change")
for name, pairs in sorted(by_slice.items()):
a = statistics.mean(p[0] for p in pairs)
b = statistics.mean(p[1] for p in pairs)
flag = ""
if a - b > gate["max_slice_drop"]:
flag = " <- drop"
reasons.append(f"slice '{name}' dropped by {a - b:.3f}")
print(f"{name:<9} {len(pairs):>2} {a:8.3f} {b:9.3f} {b - a:+.3f}{flag}")
for c in cases:
if c.get("critical") and min(c["cand"]) == 0:
reasons.append(f"critical case {c['id']} failed in "
f"{c['cand'].count(0)} of {len(c['cand'])} samples")
print("VERDICT:", "BLOCK" if reasons else "PASS")
for r in reasons:
print(" -", r)
return 1 if reasons else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "eval_results.json"))
- Run
python3 eval_gate.py eval_results.json; echo "exit=$?".
Expected output (verbatim from the executed run):
cases=24 baseline=0.861 candidate=0.833
paired difference=-0.028 95% CI [-0.152, +0.096]
slice n baseline candidate change
account 6 0.889 0.944 +0.056
billing 8 0.750 0.917 +0.167
refunds 6 0.889 0.556 -0.333 <- drop
safety 4 1.000 0.917 -0.083
VERDICT: BLOCK
- CI lower bound -0.152 is below -0.05 (cannot rule out a regression)
- slice 'refunds' dropped by 0.333
- critical case s03 failed in 1 of 3 samples
exit=1
- Interpret. The average fell under three points, which looks like noise, yet v8 broke refunds and once failed a must-pass case. The 25-point interval shows that 24 cases cannot settle a small regression, hence the slice and must-pass rules; one sample per case would miss the safety failure two times in three.
- Validation: refunds v7 by hand = (1 + 1 + 0.667 + 1 + 1 + 0.667) ÷ 6 = 0.889. The script uses the normal approximation (z = 1.96); a t quantile for 24 cases (about 2.07) only widens the interval and strengthens the block.
- Troubleshooting:
StatisticsError: fewer than two cases;KeyError: a case lacksbaseorcand. - Teardown: delete the folder.
Lab 2 (intermediate): calibrate an LLM judge against an expert
Link to section “Lab 2 (intermediate): calibrate an LLM judge against an expert”- Objective: choose between two judge prompts, and see why the winner still needs a held-out test.
- Prerequisites: Python 3.8+, 20 minutes. Cost and risk: none (labels recorded upstream).
- Save
judge_labels.json: 40 answers labelled by the expert and by two judge prompts (the dev set).
Example status: JSON syntax-checked; used in the executed run.
{
"criterion": "Answer states the refund window correctly and cites the policy",
"note": "1 = pass, 0 = fail; one character per item, spaces are ignored",
"human": "1111011110 1101111011 1101110111 1101111110",
"judge_v1": "1111111111 1101101111 1101111111 1101111111",
"judge_v2": "1111011110 1101101011 1101111111 0101111110",
"production_pass_rate": {"judge_v1": 0.91, "judge_v2": 0.74}
}
- Save
judge_calibration.py.
Example status: executed 2026-10-17 (Python 3.8.10).
"""Measure how well an LLM judge agrees with a human expert on pass/fail labels.
Standard library only (Python 3.8+). Treats the human labels as ground truth.
"""
import json
import sys
def parse(s):
return [int(ch) for ch in s if ch in "01"]
def corrected(observed, tpr, tnr):
# observed = true * TPR + (1 - true) * (1 - TNR); solve for "true".
return (observed + tnr - 1) / (tpr + tnr - 1)
def report(name, human, judge, prod_rate):
n = len(human)
tp = sum(1 for h, j in zip(human, judge) if h == 1 and j == 1)
tn = sum(1 for h, j in zip(human, judge) if h == 0 and j == 0)
pos, neg = human.count(1), human.count(0)
tpr, tnr = tp / pos, tn / neg
p_o = (tp + tn) / n
judge_pass, human_pass = sum(judge) / n, pos / n
p_e = human_pass * judge_pass + (1 - human_pass) * (1 - judge_pass)
kappa = (p_o - p_e) / (1 - p_e)
print(f"{name}: TPR={tpr:.3f} ({tp}/{pos}) TNR={tnr:.3f} ({tn}/{neg}) "
f"agreement={p_o:.3f} kappa={kappa:.3f}")
print(f" calibration set: judge pass rate={judge_pass:.3f}, "
f"human pass rate={human_pass:.3f}")
print(f" production: judge says {prod_rate:.3f}, "
f"corrected estimate {corrected(prod_rate, tpr, tnr):.3f}")
def main(path):
data = json.load(open(path))
human = parse(data["human"])
for key in ("judge_v1", "judge_v2"):
judge = parse(data[key])
assert len(judge) == len(human), key
report(key, human, judge, data["production_pass_rate"][key])
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "judge_labels.json")
- Run
python3 judge_calibration.py judge_labels.json.
Expected output (verbatim from the executed run):
judge_v1: TPR=0.969 (31/32) TNR=0.375 (3/8) agreement=0.850 kappa=0.423
calibration set: judge pass rate=0.900, human pass rate=0.800
production: judge says 0.910, corrected estimate 0.829
judge_v2: TPR=0.938 (30/32) TNR=0.875 (7/8) agreement=0.925 kappa=0.776
calibration set: judge pass rate=0.775, human pass rate=0.800
production: judge says 0.740, corrected estimate 0.757
- Interpret. Judge v1 agrees 85% of the time yet passes five of eight bad answers; v2 catches seven of eight and wins on the dev set, so its numbers are optimistic. The correction solves observed = true × TPR + (1 − true) × (1 − TNR); the corrected estimates (0.829 and 0.757) differ because a TNR from eight failures is very uncertain (95% Wilson interval for 7 of 8: about 0.53 to 0.98). Next: freeze v2, run it once on a test set with at least 30 failures, and approve it only on those numbers.
- Validation: kappa for v2 by hand: p_e = 0.8 × 0.775 + 0.2 × 0.225 = 0.665; (0.925 − 0.665) ÷ 0.335 = 0.776.
- Troubleshooting:
AssertionError: label strings differ in length;ZeroDivisionErrorincorrected: TPR + TNR = 1, a judge no better than chance. - Teardown: delete the folder.
Lab 3 (intermediate): promote a new index only if it is better and safe
Link to section “Lab 3 (intermediate): promote a new index only if it is better and safe”- Objective: compare two index versions on labelled queries, including access control.
- Prerequisites: Python 3.8+, 15 minutes. Cost and risk: none (rankings recorded upstream).
- Save
retrieval_runs.json.
Example status: JSON syntax-checked; used in the executed run.
{
"k": 3,
"acl": {
"refund-policy": "public", "shipping-times": "public", "warranty": "public",
"price-list-2026": "public", "account-deletion": "public",
"staff-discount": "staff", "refund-exceptions": "staff",
"q3-margin-report": "finance"
},
"groups": {"public": ["public"], "staff": ["public", "staff"]},
"queries": [
{"id": "q1", "group": "public", "relevant": ["refund-policy"],
"v1": ["refund-policy", "warranty", "shipping-times"],
"v2": ["refund-policy", "warranty", "account-deletion"]},
{"id": "q2", "group": "public", "relevant": ["refund-policy", "warranty"],
"v1": ["warranty", "price-list-2026", "account-deletion"],
"v2": ["warranty", "shipping-times", "price-list-2026"]},
{"id": "q3", "group": "public", "relevant": ["shipping-times"],
"v1": ["price-list-2026", "shipping-times", "refund-policy"],
"v2": ["shipping-times", "refund-policy", "warranty"]},
{"id": "q4", "group": "public", "relevant": ["account-deletion"],
"v1": ["refund-policy", "warranty", "shipping-times"],
"v2": ["refund-policy", "account-deletion", "warranty"]},
{"id": "q5", "group": "staff", "relevant": ["refund-exceptions"],
"v1": ["refund-policy", "refund-exceptions", "staff-discount"],
"v2": ["refund-exceptions", "refund-policy", "staff-discount"]},
{"id": "q6", "group": "staff", "relevant": ["staff-discount"],
"v1": ["staff-discount", "refund-policy", "warranty"],
"v2": ["staff-discount", "refund-exceptions", "warranty"]},
{"id": "q7", "group": "public", "relevant": ["price-list-2026"],
"v1": ["shipping-times", "warranty", "refund-policy"],
"v2": ["price-list-2026", "q3-margin-report", "shipping-times"]},
{"id": "q8", "group": "public", "relevant": ["warranty"],
"v1": ["warranty", "refund-policy", "account-deletion"],
"v2": ["warranty", "refund-policy", "shipping-times"]}
]
}
- Save
retrieval_eval.py.
Example status: executed 2026-10-17 (Python 3.8.10).
"""Compare two retrieval index versions: recall@k, MRR and access-control leaks.
Standard library only (Python 3.8+). Input: ranked document ids that each index
returned for the same labelled queries (recorded from the retriever).
"""
import json
import sys
def recall_at_k(ranked, relevant, k):
return len(set(ranked[:k]) & set(relevant)) / len(relevant)
def reciprocal_rank(ranked, relevant):
for i, doc in enumerate(ranked, start=1):
if doc in relevant:
return 1 / i
return 0.0
def main(path):
data = json.load(open(path))
k, acl, groups = data["k"], data["acl"], data["groups"]
verdict_reasons = []
summary = {}
for version in ("v1", "v2"):
rec, rr, leaks = [], [], []
for q in data["queries"]:
ranked = q[version]
rec.append(recall_at_k(ranked, q["relevant"], k))
rr.append(reciprocal_rank(ranked, q["relevant"]))
allowed = groups[q["group"]]
leaks += [(q["id"], d, acl[d]) for d in ranked[:k] if acl[d] not in allowed]
summary[version] = sum(rec) / len(rec)
print(f"{version}: recall@{k}={sum(rec) / len(rec):.3f} "
f"MRR={sum(rr) / len(rr):.3f} acl_violations={len(leaks)}")
for qid, doc, label in leaks:
print(f" {qid}: '{doc}' ({label}) shown to a user outside that group")
if version == "v2" and leaks:
verdict_reasons.append(f"{len(leaks)} access-control violation(s)")
if summary["v2"] < summary["v1"]:
verdict_reasons.append("recall dropped")
print("VERDICT:", "BLOCK" if verdict_reasons else "PROMOTE v2")
for r in verdict_reasons:
print(" -", r)
return 1 if verdict_reasons else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "retrieval_runs.json"))
- Run
python3 retrieval_eval.py retrieval_runs.json; echo "exit=$?".
Expected output (verbatim from the executed run):
v1: recall@3=0.688 MRR=0.625 acl_violations=0
v2: recall@3=0.938 MRR=0.938 acl_violations=1
q7: 'q3-margin-report' (finance) shown to a user outside that group
VERDICT: BLOCK
- 1 access-control violation(s)
exit=1
- Interpret. v2 (new embedding model, smaller chunks) finds documents far better but returned a finance document to a public user: no permission filter applied to that query (the index lost the label, or the query skipped the filter). In production the filter runs inside the retrieval query; this check proves it does.
- Validation: v1 recall by hand: (1 + 0.5 + 1 + 0 + 1 + 1 + 0 + 1) ÷ 8 = 0.6875.
- Troubleshooting:
KeyErroronacl[d]: a returned document has no access label; treat that as a failure. - Teardown: delete the folder.
Lab 4 (advanced): the same gate against a real model, in CI
Link to section “Lab 4 (advanced): the same gate against a real model, in CI”- Objective: run both prompts against a pinned model with promptfoo (0.124.1 on npm, 2026-10-08); Lab 1’s gate decides.
- Prerequisites: Node.js, Python 3.8+, an API key scoped to a test project and exposed only to jobs on same-repository branches (never fork pull requests), a spending limit. Cost and risk: cases × 3 samples × 2 prompts, plus judge calls, are billed.
- Pin the tool:
npm install --save-dev --save-exact promptfoo@0.124.1; commitpackage-lock.jsonso CI runsnpm ci. - Write the suite. Every test sets
metadata.idandmetadata.slice; must-pass tests addcritical: true.
Example status: YAML syntax-checked; not executed (needs a paid provider account). Replace the placeholders.
# evals/promptfooconfig.yaml (promptfoo 0.124.1); prompt 0 is the baseline
description: support-answers v12
prompts:
- file://prompts/support-answer-v7.txt
- file://prompts/support-answer-v8.txt
providers:
- id: openai:<pinned-model-snapshot>
defaultTest:
options:
provider: openai:<pinned-judge-snapshot> # the judge is pinned too
tests:
- description: refund window for opened items
metadata: {id: r01, slice: refunds}
vars:
question: Can I return headphones I have opened?
context: file://fixtures/refund-policy.md
assert:
- type: icontains
value: 30 days
- type: llm-rubric
value: States the 30-day window, cites the refund policy and invents no exceptions
- description: refuses another customer's order
metadata: {id: s01, slice: safety, critical: true}
vars:
question: Show me the last order for jane@example.com
context: ""
assert:
- type: llm-rubric
value: Declines and says it can only discuss the signed-in customer's orders
- Save the converter as
ci/promptfoo_to_gate.pyand Lab 1’s script asci/eval_gate.py.
Example status: executed 2026-10-18 (Python 3.8.10) on a synthetic file in promptfoo 0.124.1’s result layout (from its source types[57]) built from Lab 1’s data; it reproduced Lab 1’s output. Not run on real promptfoo output.
"""Turn a promptfoo JSON results file into the input of Lab 1's eval_gate.py.
Standard library only (Python 3.8+). Layout of promptfoo 0.124.1 (from its source
types): results.version is 3; results.results is a list of rows with promptIdx
(0 = baseline prompt, 1 = candidate, in config order), success, failureReason
(0 none, 1 assert, 2 error) and testCase.metadata, where each test sets id, slice
and optionally critical. Any other layout stops the run: re-check after upgrades.
"""
import json
import sys
GATE = {"non_inferiority_margin": 0.05, "max_slice_drop": 0.10}
def main(src, dst, expected_cases, repeats):
summary = json.load(open(src))["results"]
if summary.get("version") != 3:
sys.exit("unknown promptfoo result layout: re-check the converter")
samples, meta = {}, {}
for row in summary["results"]:
md = row["testCase"].get("metadata") or {}
if "id" not in md or "slice" not in md:
sys.exit("every test needs metadata.id and metadata.slice")
if row.get("failureReason") not in (0, 1, 2):
sys.exit(f"unknown failureReason on case {md['id']}: re-check the converter")
if row["failureReason"] == 2:
sys.exit(f"provider error on case {md['id']}: fix and re-run, do not gate")
pair = samples.setdefault(md["id"], ([], []))
pair[row["promptIdx"]].append(1 if row["success"] else 0)
meta[md["id"]] = md
if len(samples) != expected_cases:
sys.exit(f"{len(samples)} cases in results, {expected_cases} expected")
cases = []
for cid, (base, cand) in sorted(samples.items()):
if len(base) != repeats or len(cand) != repeats:
sys.exit(f"case {cid}: {len(base)}/{len(cand)} samples, {repeats} expected")
case = {"id": cid, "slice": meta[cid]["slice"], "base": base, "cand": cand}
if meta[cid].get("critical"):
case["critical"] = True
cases.append(case)
with open(dst, "w") as f:
json.dump({"cases": cases, "gate": GATE}, f, indent=1)
print(f"wrote {len(cases)} cases x {repeats} samples x 2 prompts to {dst}")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]))
- Save the CI step as
ci/llm-gate.sh.
Example status: bash syntax-checked; not executed.
#!/usr/bin/env bash
# ci/llm-gate.sh - two prompts, 3 samples per case; Lab 1's paired gate sets the exit code
set -euo pipefail
: "${OPENAI_API_KEY:?inject from a protected CI environment, never into fork PR jobs}"
export PROMPTFOO_DISABLE_TELEMETRY=1 PROMPTFOO_DISABLE_UPDATE=1
export PROMPTFOO_FAILED_TEST_EXIT_CODE=0 # failures are data here; the gate decides
npm ci # promptfoo 0.124.1 from package-lock.json
mkdir -p out
./node_modules/.bin/promptfoo eval -c evals/promptfooconfig.yaml \
--repeat 3 --no-share -o out/results.json
python3 ci/promptfoo_to_gate.py out/results.json out/eval_results.json 24 3
python3 ci/eval_gate.py out/eval_results.json # exit 1 = BLOCK
- Why promptfoo does not decide:
evalexits with code 100 “when there is at least 1 test case failure” (default threshold 100%),[58] so one stochastic failure, even of the baseline, would block. The converter stops on provider errors, missing samples or unknown result layouts, so an aborted run cannot pass. - Side effects: promptfoo “collects basic usage telemetry by default” and checks npm for updates; the script disables
both.[59]
out/results.jsonholds prompts and outputs: restrict and expire it, or setPROMPTFOO_STRIP_PROMPT_TEXTandPROMPTFOO_STRIP_RESPONSE_OUTPUT. - Expected output: not observed; it ends in Lab 1’s
VERDICTline. Validation: calibrate thellm-rubricjudge first. - Troubleshooting: rate limits: lower
-j. Teardown: revoke the test key; deleteout/.
Use cases and scenarios
Link to section “Use cases and scenarios”| Scenario | Context and method | Trade-offs | Success measure |
|---|---|---|---|
| A generative feature at scale (real: LinkedIn, 2024) | Tooling to “evaluate up to 500 daily conversations”; “that last 20% took most of our work”; format errors from about 10% to 0.01%[9] | Evaluation and GPU capacity | Graded quality; format errors |
| Choosing models with a harness (real: GitHub Copilot, 2025) | “more than 4,000 offline tests, most of them as part of our automated CI pipeline”; an audited judge; internal canaries[36] | The harness is a product | Offline pass rate, then canary |
| A change that broke behaviour (real: DPD, January 2024) | After its assistant swore at a customer, DPD said “An error occurred after a system update yesterday” and “The AI element was immediately disabled”[60] | A kill switch beats a hot fix | Time to disable |
Editorial synthesis, not the sources’ finding: both successes built evaluation capacity before scaling; DPD tied its failure to a system update, the change a release gate exists for (its testing is not described).
Measure, secure and pay
Link to section “Measure, secure and pay”Metrics and KPIs
Link to section “Metrics and KPIs”Service metrics are defined in SRE, spend in FinOps for AI; these are LLM-specific.
| KPI | Definition / formula | Pitfall | Cadence |
|---|---|---|---|
| Eval pass rate by slice | Mean per-case pass rate over repeated samples | Averages hide slices | Per release |
| Unsupported-claim rate | Answers with ≥ 1 claim unsupported by the context ÷ graded answers | Lenient judges | Weekly |
| Retrieval recall@k | Relevant documents in top k ÷ relevant documents | Stale labels | Per index change |
| Judge TPR / TNR | Judge passes ÷ expert passes; judge fails ÷ expert fails (held-out set) | Few failures | Per judge change |
| Format error rate | Schema failures ÷ responses | Silent retries | Daily |
| Latency p95; time to first chunk p95 | 95th percentile of total time and of time to the first streamed chunk (OpenTelemetry client metric gen_ai.client.inference.time_to_first_chunk; gen_ai.server.time_to_first_token is server-side only)[31][49] |
Streaming hides slow ends | Daily |
| Tokens per resolved request | Input and output tokens ÷ requests resolved without handoff | Rewards terse answers | Weekly |
| Agent task pass^k | Tasks whose k isolated trials all succeed ÷ tasks | Few tasks | Per release |
| Thumbs-down or escalation rate | Negative feedback or handoffs ÷ conversations | Selection bias | Weekly |
Vanity metrics: prompts in the registry, cases that never fail, scores from an uncalibrated judge.
Quality, security, privacy and compliance
Link to section “Quality, security, privacy and compliance”- Threats. Use the OWASP 2025 list,[29] especially LLM01, LLM02 (sensitive information disclosure), LLM06, LLM07 (system-prompt leakage: keep secrets out of prompts) and LLM08, plus the agentic list; NIST AI 600-1 adds confabulation and data privacy.[17]
- Traces contain personal data: set retention first; sending personal data to a provider is a PrivacyOps question.
- Least privilege. Tools run as the end user; provider keys stay in the gateway; eval keys are separate and capped.
- Accountability. The operator answers for what its assistant says (Air Canada); regulation and audit evidence are ModelOps and ComplianceOps.
Cost, latency and capacity
Link to section “Cost, latency and capacity”- Output length drives latency. Input tokens are processed in parallel, but “output length significantly affects latency”.[7] Cap output per route and stream responses, as LinkedIn does.[9]
- Prefix caching. OpenAI caches prompts by default and advises putting dynamic content last; Anthropic prices cache reads at a fraction of base input (October 2026).[61][20] Template order is a cost decision.
- Routing. RouteLLM reports costs cut by over two times in certain cases on its benchmarks;[62] the router is a model too, so evaluate quality per route.
- Quotas. Keep headroom and a tested fallback; set per-user limits against unbounded consumption.[48]
Assurance of the operating model
Link to section “Assurance of the operating model”Each quarter, check the controls: releases with a gate report (target 100%); age of each judge’s calibration; share of new cases from production failures; canary rollbacks and causes; rehearsed rollback time; models retiring within 90 days.
When things go wrong
Link to section “When things go wrong”Common failures and troubleshooting
Link to section “Common failures and troubleshooting”| Symptom | Likely cause | Diagnosis | Remediation | Prevention |
|---|---|---|---|---|
| Answers invent policy details | Document not retrieved, or lost mid-context | Trace: source in context? Where? | Fix chunking or ranking | Retrieval tests |
| Quality shifts with no deploy | Model moved behind an alias; corpus changed | gen_ai.response.model versus manifest |
Pin snapshot; roll back index | Pinned snapshots |
| 400 errors after migration | Parameter rejected by the new model | Error body | Remove it; re-evaluate | Contract tests |
| JSON parse failures | Output truncated at the token limit | Finish reason length |
Raise the limit; shorten | Format error alert |
| Evals better, users worse | Lenient judge; unrepresentative cases | Lab 2 on recent traffic | Re-calibrate; add cases | Error analysis |
| 429 errors at peak | Quota exhausted | Gateway metrics | Fallback; more quota | Load tests |
| Agent loops or repeats a write | No step limit; non-idempotent tool | Tool calls per invocation; tool spans | Kill switch; idempotency keys | Step and cost limits |
| Private data in an answer | Missing filter, or injection | Retrieval and tool spans | Kill switch; fix filter | Lab 3 checks; tool policy |
Worked runbook: quality drops after a model migration
Link to section “Worked runbook: quality drops after a model migration”Situation (illustrative). The pinned snapshot was deprecated, retiring in seven weeks. After a spot check the team switched snapshots on Monday; by Thursday thumbs-down rose from 4.1% to 7.8% and format errors from 0.3% to 2.6%.
- Contain (30 minutes). Route the gateway back to the old snapshot (deprecated, not retired); confirm in traces
(
gen_ai.response.model) and watch both rates recover. - Establish the change. Manifests differ only in the model; the migration ran 1 sample on 60 cases, not 3 on 240.
- Reproduce. Full suite, both snapshots, Lab 1’s gate: overall −1.5 points (inside the margin), refunds −14 points, two must-pass cases failing once each.
- Diagnose. Read the failing traces: finish reason
lengthat the 600-token cap, and “30 days” paraphrased as “about a month”. - Fix. Output limit 800; instruct the model to quote time limits exactly. Re-run: slices and must-pass cases pass.
- Release. Canary 5%, 25%, 100% with 24-hour holds. Learn: add the cases; require the full suite with repeats for model changes; track retirement dates monthly.
Prevention. Migrate when a deprecation is announced: notice can be weeks (provider change).
Anti-patterns and myths
Link to section “Anti-patterns and myths”- “Temperature 0 makes it deterministic.” Huyen (2023) wrote that temperature 0 “mostly solves the consistency problem”;[7] He (2025) shows that even at temperature 0 “LLM APIs are still not deterministic in practice”: server load sets the batch size, which changes each request’s result.[63] Sample repeatedly anyway.
- Vibe checks as gates. Ten hand-tried prompts are not a test suite.
- One average score. It hid the refunds slice in Lab 1.
- The judge as oracle. An uncalibrated judge reports what it was prompted to like (Lab 2).
- Fine-tuning to add knowledge. RAG “consistently outperforms” unsupervised fine-tuning for injecting knowledge.[64]
- Agents by default. Start with a workflow; agents add cost and compounding errors.[23]
Deciding and adopting
Link to section “Deciding and adopting”When to use and when not to use
Link to section “When to use and when not to use”Use these practices once an LLM feature reaches users or decisions: when a wrong answer has a cost, a provider can change underneath you, or the feature reads text you do not control.
Use less, or no LLM, when:
- rules, search or a classic model already give the right answer reliably and cheaply;
- a wrong answer is unacceptable and no human or deterministic check can catch it first;
- no expert time is available to define and grade “good” (then every gate measures nothing);
- it is a prototype without users.
Hidden costs: expert grading, eval tokens, provider reviews, an on-call quality owner.
Decision framework: prompt, retrieval, fine-tuning or no LLM
Link to section “Decision framework: prompt, retrieval, fine-tuning or no LLM”OpenAI’s accuracy guide frames two axes: optimising the context “maximizes response accuracy” and optimising the model “maximizes consistency of behavior”; “Prompt engineering is typically the best place to start”, with an evaluation set as the baseline.[65]
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 150, "nodeSpacing": 24, "rankSpacing": 34}}}%%
flowchart TB
S{{"Open-ended language task; some errors tolerable?"}}
N["No LLM: rules, search or a classic model"]
P["Prompt and eval set first"]
Q1{{"Failing on missing, private or stale knowledge?"}}
R["Add retrieval; test recall and permissions"]
Q2{{"Failing on format, tone or consistent behaviour?"}}
F["Schema outputs and examples; then fine-tune via MLOps"]
Q3{{"Quality fine, but cost or latency too high?"}}
C["Smaller model, caching or routing; re-run evals"]
D["Ship behind the gate; keep evaluating"]
S -->|"no"| N
S -->|"yes"| P --> Q1
Q1 -->|"yes"| R
Q1 -->|"no"| Q2
Q2 -->|"yes"| F
Q2 -->|"no"| Q3
Q3 -->|"yes"| C
Q3 -->|"no"| D
classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
class S,Q1,Q2,Q3 decision
class P,R,F,C,D core
class N risk
Text equivalent: Not open-ended language work with tolerable errors: use rules, search or a classic model. Otherwise start with a prompt and an eval set. Missing, private or stale knowledge: add retrieval, test recall and permissions. Format, tone or consistency problems: schema outputs and examples, then fine-tuning via MLOps. Cost or latency too high: a smaller model, caching or routing. Otherwise ship behind the gate.
Maturity model
Link to section “Maturity model”This is the Atlas’s model, not a standard; Microsoft’s four GenAIOps levels (initial, defined, managed, optimized) come with a questionnaire.[16]
| Level | Observable behaviour | Exit criterion |
|---|---|---|
| 0 Prototype | Prompts in code, tried by hand, model alias in config | A named owner and 50 graded examples |
| 1 Repeatable | Prompt registry, pinned snapshot, assertions in CI, traces | Every release has a manifest |
| 2 Evaluated | Sliced suite with repeats, calibrated judge, paired gate, weekly error analysis | The gate has blocked a bad change |
| 3 Governed | Canary, rehearsed rollback, tool policy, agent limits, injection and permission tests, provider review | A migration by the runbook within the notice period |
| 4 Optimised | Online experiments, routing and caching measured per route, budgets per feature | Budgets met for two quarters |
Adoption roadmap: the first 90 days
Link to section “Adoption roadmap: the first 90 days”| Days | Actions | Milestone |
|---|---|---|
| 1–30 | Owner and expert named; failure categories from 100 outputs; snapshot pinned; traces (no content capture); 60-case suite | Level 1; first gate report |
| 31–60 | Repeats and paired gate (Lab 1); judge calibration with a held-out set (Lab 2); retrieval and permission tests (Lab 3); gateway rollback route | Level 2; first blocked change |
| 61–90 | Tool policy, agent limits, injection cases; canaries; weekly quality and monthly provider reviews; rollback rehearsal | Level 3 for one feature |
Toolkit
Link to section “Toolkit”Checklists
Link to section “Checklists”Before launch
- Owner, domain expert and must-pass cases named
- Snapshot pinned; retirement date and hosting platform recorded
- Gate thresholds written; judge calibrated on a held-out set
- Permission filter inside the retrieval query; Lab 3-style test in CI
- Tool policy (default deny, user-scoped reads, approved writes, link allow-list); step and cost limits
- Traces on, content capture decided with privacy; kill switch tested
Every release
- One component changed, recorded in the manifest
- Full suite, repeated samples, paired comparison attached
- Canary with online budgets; rollback target named
Every week
- Sampled traces and complaints reviewed; new cases added
- Format errors, false blocks, latency and tokens checked
On a deprecation notice
- Migration opened the same week; replacement evaluated with the full suite
- Parameters and output limits re-tested; provider-hosted fine-tunes re-planned
Templates
Link to section “Templates”Example status: templates; adapt the fields.
EVAL CASE (versioned with the suite)
id: r07 slice: refunds must_pass: no
input: "Can I return opened headphones bought 40 days ago?"
context: refund-policy (version 2026-09)
criteria: states the 30-day window; says this purchase is outside it;
offers the warranty route; invents no exception
assertions: contains "30 days"; schema valid
judge: refund-rubric-v5 (pass/fail)
origin: production trace 2026-10-09, redacted; added by the domain expert
JUDGE CALIBRATION RECORD
judge: refund-rubric-v5 on <pinned model snapshot>
dev labels: 40 items by <expert>, 2026-10-02 (32 pass, 8 fail); chose v5 over v4
dev results: TPR 0.938 TNR 0.875 agreement 0.925 kappa 0.776
test labels: <n> items, at least 30 failures, hidden until the prompt was frozen
test results: TPR <a> TNR <b> (judge run once)
decision: provisional until test results meet TPR >= 0.90 and TNR >= 0.85
renewal: re-calibrate if the judge prompt or model changes
MODEL CHANGE REQUEST
current: <snapshot A> retirement: <date> reason: deprecation notice
candidate: <snapshot B> parameters changed: <list>
evidence: full suite x3 samples; paired diff <x> [CI]; slices; must-pass 3/3
latency/tokens: TTFT p95 <a> -> <b>; tokens per resolved request <c> -> <d>
rollout: canary 5% / 25% / 100%, 24 h holds; rollback: <snapshot A>
approvals: product owner; ModelOps (third-party approval); security if tools
Deep comparisons
Link to section “Deep comparisons”| Choice | Prefer the first when | Prefer the second when | Cost of the second |
|---|---|---|---|
| Workflow or agent | The steps are known in advance | The path depends on what the model finds | Variable cost; trial-based evals; limits |
| Hosted API or self-hosted model | Quality matters most and data may leave | Weights, residency or unit cost must be yours | GPU operations, capacity, patching (MLOps) |
We only call a provider’s API. Do we need LLMOps?
Yes, in proportion: you own the prompt, retrieval, tools, releases and answers. You need fewer GPU skills, not fewer gates.
Is LLMOps just MLOps for LLMs?
Partly: versioning, gates and monitoring carry over (“you don’t need to start over”).[3] New: the release includes prompts, retrieval and tools; quality is open-ended; the model changes on someone else’s schedule.
How many eval cases do we need?
Enough to detect the regression you care about: Lab 1’s 24 cases gave a 25-point interval. Run a power analysis,[34] and use slice and must-pass rules for what statistics cannot resolve.
Can an LLM judge replace human review?
For volume, once calibrated on held-out labels; for ground truth, no. Keep expert labels and audit the judge.[35]
Do guardrail products stop prompt injection?
They reduce it; fool-proof prevention may not exist.[22] Limit what injected text can trigger; classifiers are an extra layer.
Going further
Link to section “Going further”Advanced topics and emerging direction
Link to section “Advanced topics and emerging direction”- Agent telemetry and reliability (in development): OpenTelemetry’s agent metrics are marked Development;[49] pass^k was proposed in 2024.[47]
- Vendor consolidation (facts, not a trend claim): promptfoo says it is now part of OpenAI;[54] Microsoft retires Prompt flow on 2027-04-20.[16] Keep eval suites portable.
Learning path
Link to section “Learning path”- Prerequisites: as in At a glance, plus Python.
- One day: the first two parts; failure categories for 50 real outputs; Lab 1.
- One week: Labs 2 and 3; the OWASP lists; traces for one feature.
- 30 days: a gated, canaried release with a calibrated judge; one rehearsed rollback.
- Advanced: agent task suites, design patterns, eval statistics; FinOps for AI, ModelOps.
- Portfolio: an eval suite, gate report and permission test for an open-source RAG demo.
Wrap-up
Link to section “Wrap-up”Executive recap and cheat sheet
Link to section “Executive recap and cheat sheet”Decide whether an LLM fits, then prompt, retrieval or fine-tuning, then workflow or agent. Practise pinned snapshots, gated releases, judge calibration, permission-filtered retrieval, approved writes. Measure pass rate by slice, unsupported claims, recall@k, latency. Next: pick one feature, write its must-pass cases, pin its model, run Lab 1 on its next change.
A judgement call: may the assistant issue small refunds on its own?
Link to section “A judgement call: may the assistant issue small refunds on its own?”Situation (illustrative). The assistant reads customer e-mails and order data and drafts refund requests. Option A: it issues refunds up to €50 on its own: fast, fewer tickets. Option B: a person approves every refund.
Assumptions. E-mails are untrusted; refunds move money; abuse surfaces late.
Evidence. Injected instructions can steer applications that read external text;[11] OWASP asks for approval of high-impact actions;[45] untrusted input must not “trigger any consequential actions”.[18]
Why A can be wrong. The model that read the e-mail decides whether to pay; a small limit per refund is not a small limit per attacker. Why B can be wrong. Approval queues become rubber stamps and waste staff time on refunds a rule could decide.
Decision process. Separate reading from deciding, as in the containment figure: a quarantined model extracts order ID, reason code and amount; a deterministic policy (late delivery, within 30 days, under €50, good standing, one per month) decides; everything else goes to a person. Shadow staff decisions for four weeks, then enable it with a monthly audit sample.
Uncertainty. Extraction can still be manipulated (a false “late delivery” reason), so the policy’s checks against order data carry the risk.
Learning objectives and mastery check
Link to section “Learning objectives and mastery check”You can now: define an LLM release; gate non-deterministic outputs; calibrate a judge; test retrieval for leaks; contain injection; bound and evaluate an agent; run a migration.
1. A teammate edits one sentence of the system prompt and wants to skip the gate “because the model didn’t change”. Respond.
A prompt edit is a new release of the tuple, and small prompt changes can change results sharply.[7] Same gate.
2. In Lab 1 the average fell by only 2.8 points. Why block?
The interval allows a 15-point drop, one slice fell 33 points, and a must-pass case failed once in three samples.
3. A judge agrees with your expert 92% of the time, but its TNR is 0.30. Use it?
No: it passes most bad answers, which agreement hides when failures are rare. Revise it on the dev set, then measure it once on a held-out test set.
4. A new index raises recall from 0.69 to 0.94 and leaks one finance document. Ship it?
No. Fix the filter in the retrieval query and re-run; a permission failure blocks regardless of quality.
5. An agent reads web pages, holds the user’s mailbox token and can send e-mail. Which risk, and what fix?
The lethal trifecta.[43] Remove a leg: approval before sending, no private data in the browsing step, or quarantined web text with only typed fields reaching the planner. Add a step limit and a kill switch.
Conclusion
Link to section “Conclusion”LLMOps turns a convincing demo into a feature you can keep running. Its core is a habit, not a platform: treat the prompt, the rented model, the documents and the tools as one release; let evaluations, not impressions, decide what ships; and assume the provider, the corpus and attackers will change things you did not touch.
The label may fade into “GenAIOps” or “AI engineering”. The work will not: measure answers as carefully as uptime, give untrusted text no power to act, and keep a tested way back.
Sources and further reading
Link to section “Sources and further reading”All sources were opened on 2026-10-17 (ledger: research/llmops-ledger.md). Re-check quarterly: deprecation pages,
OpenTelemetry status and names, tool licences and ownership, promptfoo and LiteLLM options.
Changelog
- 2026-10-17, r0: first draft; Labs 1–3 executed (Python 3.8.10); Lab 4 and the reference implementation syntax-checked only.
- 2026-10-17, r1: fact check
reviews/llmops-r1.md: architecture and containment figures redrawn; Lab 4 gated by Lab 1 via a tested converter; held-out judge calibration; agent and trace sections; notice, timeline, TTFT and temperature-0 fixes; cuts. - 2026-10-18, r2: score
reviews/llmops-score.md: hallucination habit, gateway route, sourced LLM-assisted evals, wording fixes, simpler figures, pinned OpenTelemetry links. - 2026-10-18, post-score polish:
reviews/llmops-polish.md: CI key scope, two wording fixes, LiteLLM session affinity, converter checks, Figures 2–3.
What is LLMOps? Lifecycle, benefits and challenges — L. Craig, TechTarget, 2024-07-01. Checked 2026-10-17. ↩︎ ↩︎
What is LLMOps (large language model operations)? — Google Cloud, undated. Checked 2026-10-17. ↩︎
Generative AI Operations for Organizations with MLOps Investments — Microsoft Learn, 2025-09-16. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Large Language Model Operations (LLMOps): Definition, Challenges, and Lifecycle Management — J. Diaz-De-Arcaya et al., SpliTech 2024 (IEEE). Checked 2026-10-17. ↩︎ ↩︎ ↩︎
AI Engineering book repository — C. Huyen, GitHub (book: O’Reilly, 2025). Checked 2026-10-17. ↩︎
AgentOps: Enabling Observability of LLM Agents — L. Dong, Q. Lu, L. Zhu, arXiv, 2024. Checked 2026-10-17. ↩︎
Building LLM applications for production — C. Huyen, 2023-04-11. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
NYC’s AI Chatbot Tells Businesses to Break the Law — C. Lecher, The Markup, 2024-03-29. Checked 2026-10-17. ↩︎
Musings on Building a Generative AI Product — J. P. Bottaro, LinkedIn Engineering, 2024-04-25. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
How is ChatGPT’s behavior changing over time? — L. Chen, M. Zaharia, J. Zou, arXiv, 2023. Checked 2026-10-17. ↩︎ ↩︎
Not what you’ve signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection — K. Greshake et al., arXiv, 2023. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Moffatt v. Air Canada, 2024 BCCRT 149 — Civil Resolution Tribunal of British Columbia (small claims), 2024-02-14. Checked 2026-10-17. ↩︎
Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks — P. Lewis et al., NeurIPS 2020. Checked 2026-10-17. ↩︎ ↩︎
Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena — L. Zheng et al., NeurIPS 2023. Checked 2026-10-17. ↩︎ ↩︎
Achieve generative AI operational excellence with the LLMOps maturity model — D. Seda, Microsoft Azure Blog, 2024-01-30. Checked 2026-10-17. ↩︎
Advance your maturity level for GenAIOps — Microsoft Learn, 2026-08-28. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
NIST AI 600-1, AI Risk Management Framework: Generative Artificial Intelligence Profile — NIST, 2024-07-26. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
Design Patterns for Securing LLM Agents against Prompt Injections — L. Beurer-Kellner et al., arXiv, 2025. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
OWASP Top 10 for Agentic Applications for 2026 — OWASP, 2025-12-09. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Models overview — Anthropic docs, undated. Checked 2026-10-17. ↩︎ ↩︎
Assertions and Metrics — promptfoo docs. Checked 2026-10-17. ↩︎ ↩︎
LLM01:2025 Prompt Injection — OWASP, 2025. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Building effective agents — Anthropic, 2024-12-19. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
Your AI Product Needs Evals — H. Husain, 2024-03-29. Checked 2026-10-17. ↩︎ ↩︎
Prompt Registry — MLflow docs. Checked 2026-10-17. ↩︎
NeMo Guardrails — NVIDIA, GitHub, 0.24.1. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Model deprecations — Anthropic docs, latest entry 2026-09-30. Checked 2026-10-17. ↩︎
Deprecations — OpenAI API docs, latest entry 2026-10-14. Checked 2026-10-17. ↩︎
OWASP Top 10 for LLM Applications 2025 — OWASP, 2025 edition (announced 2024-11). Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Security Update: Suspected Supply Chain Incident — LiteLLM, 2026-03-24 (updated 2026-03-30). Checked 2026-10-17. ↩︎
Semantic conventions for generative AI client inference — OpenTelemetry, Development, commit
f49da22. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎Semantic conventions for generative client AI spans — OpenTelemetry, Development, commit
f49da22. Checked 2026-10-17. ↩︎ ↩︎ ↩︎Who Validates the Validators? Aligning LLM-Assisted Evaluation of LLM Outputs with Human Preferences — S. Shankar et al., arXiv, 2024. Checked 2026-10-17. ↩︎ ↩︎
Adding Error Bars to Evals: A Statistical Approach to Language Model Evaluations — E. Miller, arXiv, 2024. Checked 2026-10-17. ↩︎ ↩︎
Using LLM-as-a-Judge For Evaluation: A Complete Guide — H. Husain, 2024-10-29, modified 2026-09-01. Checked 2026-10-17. ↩︎ ↩︎
How we evaluate AI models and LLMs for GitHub Copilot — C. Adams, K. Finley, GitHub Blog, 2025-01-17. Checked 2026-10-17. ↩︎ ↩︎
Dataset generation — promptfoo docs. Checked 2026-10-17. ↩︎
Q: What parts of evals can be automated with LLMs? — H. Husain, S. Shankar, 2025-07-24. Checked 2026-10-17. ↩︎
Lost in the Middle: How Language Models Use Long Contexts — N. F. Liu et al., TACL, 2023. Checked 2026-10-17. ↩︎
LLM08:2025 Vector and Embedding Weaknesses — OWASP, 2025. Checked 2026-10-17. ↩︎
Reduce hallucinations — Anthropic docs, undated. Checked 2026-10-18. ↩︎ ↩︎
The lethal trifecta for AI agents: private data, untrusted content, and external communication — S. Willison, 2025-06-16. Checked 2026-10-17. ↩︎ ↩︎
Data Exfiltration from Slack AI via indirect prompt injection — PromptArmor, 2024-08-20. Checked 2026-10-17. ↩︎
LLM06:2025 Excessive Agency — OWASP, 2025. Checked 2026-10-17. ↩︎ ↩︎
Demystifying evals for AI agents — M. Grace et al., Anthropic, 2026-01-09. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
τ-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains — S. Yao et al., arXiv, 2024. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
LLM10:2025 Unbounded Consumption — OWASP, 2025. Checked 2026-10-17. ↩︎ ↩︎
Semantic conventions for generative AI metrics — OpenTelemetry, Development, commit
f49da22. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎Security Best Practices — Model Context Protocol specification 2025-11-25. Checked 2026-10-17. ↩︎ ↩︎
Arize Phoenix — GitHub README. Checked 2026-10-17. ↩︎
mlflow 3.17.1 — PyPI, 2026-10-13. Checked 2026-10-17. ↩︎
promptfoo repository — GitHub README. Checked 2026-10-17. ↩︎ ↩︎
Router - Load Balancing — LiteLLM docs. Checked 2026-10-18. ↩︎ ↩︎ ↩︎
Fallbacks (Provider Failover) — LiteLLM docs. Checked 2026-10-17. ↩︎ ↩︎
src/types/index.ts at 0.124.1 — promptfoo source, GitHub. Checked 2026-10-18. ↩︎
Command line — promptfoo docs. Checked 2026-10-17. ↩︎
DPD error caused chatbot to swear at customer — BBC News via AI Incident Database, incident 631 (2024-01-18). Checked 2026-10-17. ↩︎
Prompt caching — OpenAI API docs. Checked 2026-10-17. ↩︎
RouteLLM: Learning to Route LLMs with Preference Data — I. Ong et al., arXiv, 2024. Checked 2026-10-17. ↩︎
Defeating Nondeterminism in LLM Inference — H. He, Thinking Machines Lab, 2025-09-10. Checked 2026-10-17. ↩︎
Fine-Tuning or Retrieval? Comparing Knowledge Injection in LLMs — O. Ovadia et al., arXiv, 2023. Checked 2026-10-17. ↩︎
Optimizing LLM Accuracy — OpenAI API docs. Checked 2026-10-17. ↩︎