LLMOps 是供應鏈管理:鎖定無法控制的輸入
LLMOps Is Supply Chain Work: Pinning the Inputs You Don't Control
文章主張 LLMOps 應把語料、提示詞模板、工具回應和模型版本視作依賴項管理,而非只關注模型本身。建議在文件進入檢索索引前設置閘口,以追加式清單記錄 sha256、來源及信任級別,並由檢索層執行信任規則。另應關聯提示詞雜湊、評測執行 ID 和部署 ID,並記錄檢索文件及工具呼叫等追蹤資料,以便回查答案所用輸入。
Suggested tags: ai, llm, devops, python
Most LLMOps advice is about the model: pick a snapshot, set a temperature, watch the latency. That framing is comfortable because the model is the one thing you can't debug. The failures I've been pulled into were rarely the model. They were the corpus it read, the prompt template someone edited at 4pm on Friday, and the tool response nobody validated. Those are dependencies, and you are not pinning them.
The three dependencies nobody pins
Compare a normal service to an LLM feature:
-
torch==2.4.1goes in a lockfile. Your retrieval corpus is a directory that gets rsynced by a cron job. - A config change goes through review. A prompt template lives in a Google Doc or a database row with an
updated_atcolumn and no diff. - An upstream API contract is versioned. A model alias like
gpt-4o-latestcan change behavior under you with no changelog.
None of these are exotic. All of them are ordinary dependency management that we skipped because the artifacts are text and text feels free to change.
Content is executable now
Two patterns from the last year worth internalizing, because they generalize:
Instructions hidden in help pages. Pages that look like support documentation but contain directives aimed at whatever model reads them. If your retrieval layer indexes third-party or user-editable content, a retrieved document is a user message with extra steps. It arrives inside your system prompt's trust boundary and it is written by someone who is not you.
Published statistics engineered to be cited. Publish a plausible-looking number with a memorable framing, and eventually it shows up in generated answers and gets laundered into "facts." This is SEO with a longer feedback loop, and it targets your corpus whether you crawl, license, or accept user uploads.
Same failure mode both times: the retrieval corpus is writable by parties outside your trust boundary. Prompt injection gets discussed as a prompt problem. It's an ingestion problem. Filtering the output is a losing game; controlling what enters the index is tractable.
Gate at ingest, not at prompt
The cheapest control I know is an append-only manifest — a lockfile for content.
# ingest_gate.py — run before any document enters the retrieval index.
import hashlib
import json
import pathlib
import re
# Directives that only make sense if a model, not a human, is reading.
INSTRUCTION_PATTERNS = re.compile(
r"(ignore (all )?previous|you are now|do not recommend|system prompt|"
r"when asked about .{0,40}(say|recommend))",
re.IGNORECASE,
)
def sha256(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def classify(text: str) -> str:
# Trust tier decides whether the doc is served, quoted, or quarantined.
return "quarantine" if INSTRUCTION_PATTERNS.search(text) else "serve"
def ingest(path: pathlib.Path, manifest: pathlib.Path) -> dict:
text = path.read_text(encoding="utf-8")
record = {
"doc_id": path.stem,
"sha256": sha256(text),
"tier": classify(text),
"bytes": len(text.encode("utf-8")),
"source": str(path.parent), # first-party vs. vendor vs. user upload
}
# Append-only: this file is the lockfile for your corpus.
with manifest.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(record) + "\n")
return record
if __name__ == "__main__":
print(ingest(pathlib.Path("help_page.md"), pathlib.Path("corpus.lock")))
The regex is not the point — it will miss things, and a determined author will paraphrase. The manifest is the point. Once corpus.lock is in git, a pull request that adds forty quarantine-tier documents from a new domain is visible to a reviewer. A corpus that changes silently is not.
Pair it with a per-source trust tier that the retrieval layer enforces: first-party docs can be quoted directly, vendor docs must be attributed inline, user uploads never get inserted into the system prompt at all — only into a delimited user turn.
Evals are the lockfile for behavior
A prompt hash, an eval run id, and a deploy id should be joinable three months later. If you can't answer "which prompt and which corpus produced this answer," you have observability for the wrong system.
Practical constraints I'd argue for:
- Gate on delta, not absolute score. A prompt edit that drops your golden set by two points is noise. One that changes the set of failing cases is signal, even if the total is flat.
- Sample size, honestly. With a 12-case eval, a single flip is an 8-point swing. I'd rather have 150 labeled cases and a fixed seed than 2,000 unlabeled ones and a judge model.
- The judge is not ground truth. LLM-as-judge is useful for triage at scale. For anything a customer will notice, hand-label a stratified sample and report agreement between judge and human.
- Adversarial cases belong in the golden set. Include the poisoned documents you've actually seen. Evals that only contain well-formed questions measure the model, not the system.
Traces matter more as reasoning goes latent
There's a visible trend toward agents that reason in a continuous latent space and only emit text at action boundaries. That's a real efficiency win and a real observability loss: the intermediate tokens you used to read are gone. The final answer tells you less than it used to.
So log the things that survive: the resolved prompt hash, the retrieved document ids and their trust tiers, every tool call with arguments and status, token counts per span, and the model snapshot string the provider actually returned. OpenTelemetry's GenAI semantic conventions give you attribute names that Langfuse, Phoenix, and most vendors will ingest without a custom adapter. Instrument once at the client wrapper, not at every call site.
Rollout mechanics that contain damage
- Shadow traffic first. Run the new prompt or model on live requests, log only. Compare against production output offline.
- Canary by tenant or route, not globally. One enterprise customer with a narrow use case is a better canary than 5% of anonymous traffic.
- Per-task budgets. Token and tool-call ceilings with a hard stop, not a warning. Unbounded retry loops are the most expensive bug class in this space.
- Invalidate response caches on corpus change. A cached answer produced from a document you've since quarantined is a live injection vector.
- A kill switch that degrades to retrieval-without-generation. If the model provider is down, a list of links beats a 500.
Pitfalls
- Versioning the prompt but not the retrieval config. Chunk size, top-k, and reranker settings change behavior as much as the prompt does.
- Trusting a model alias. Pin a dated snapshot. If the provider retires it, you want that to be an incident, not a surprise.
- Treating eval scores as portable. A score from last month's corpus says nothing about this month's.
- Blocking on content moderation at output. It catches tone, not exfiltration or instruction-following.
- Unbounded tool output into context. A 200KB API response will crowd out your instructions and look like a model regression.
Monday checklist
- [ ] Corpus is written to an append-only manifest with
sha256, source, and trust tier; the manifest is in git. - [ ] Retrieval layer enforces trust tiers — user uploads never enter the system prompt.
- [ ] The model is a dated snapshot string, pinned in config, with an owner for retirement.
- [ ] Prompt template, retrieval config, and corpus manifest are hashed into one deploy id.
- [ ] Golden set has ≥100 cases including every adversarial document seen in the last quarter.
- [ ] Eval gate compares failing-case sets, not just aggregate score.
- [ ] Traces record retrieved doc ids, tool calls with arguments, and token counts per span.
- [ ] Per-task token and tool-call ceilings with hard stops.
- [ ] Response cache is keyed on corpus version and invalidated on change.
- [ ] Shadow and canary paths exist, and someone has flipped the kill switch in staging this quarter.
來源:DEV Community · dev.to