Bài 1: LLMOps Overview & Prompt Management¶
Tổng quan¶
Bài này đặt nền cho toàn bộ Module III. Hai câu hỏi trung tâm:
- LLMOps khác MLOps ở đâu? Không có vòng lặp retrain, artifact chính không phải model weights mà là prompt + config + eval set.
- Vì sao prompt cần được quản lý như code? Prompt hardcode rải rác khiến không ai biết version nào đang chạy production, không rollback được, không A/B test được.
Sau bài này bạn sẽ có một prompt registry có versioning, load prompt theo version tại runtime, và chạy được A/B test prompt trên traffic thật.
1. LLMOps vs MLOps¶
Điểm giống¶
Cả hai đều là "đưa mô hình vào production và giữ nó chạy tốt": versioning artifact, test trước khi deploy, monitor sau khi deploy, có đường rollback.
Điểm khác cốt lõi¶
graph LR
subgraph MLOps
D1[Data] --> T1[Train model] --> E1[Evaluate] --> R1[Register weights] --> DEP1[Deploy]
DEP1 -.drift.-> T1
end
subgraph LLMOps
P2[Prompt + RAG + config] --> E2[Evaluate] --> R2[Register prompt version] --> DEP2[Deploy]
DEP2 -.log lỗi.-> P2
end
| Trục | MLOps | LLMOps |
|---|---|---|
| Artifact chính | Model weights (hàng trăm MB–GB) | Prompt + config (vài KB), RAG index |
| "Training" | Bắt buộc, tốn GPU, tính bằng giờ–ngày | Thường không có; cải tiến = sửa prompt/RAG/eval |
| Vòng lặp cải tiến | Data mới → retrain → redeploy (ngày–tuần) | Sửa prompt → eval → deploy (phút–giờ) |
| Nguồn regression | Data drift, distribution shift | Đổi model provider, sửa 1 dòng prompt, provider âm thầm cập nhật model |
| Evaluation | Metric số học ổn định (accuracy, F1, AUC) | Không có accuracy đơn giản; LLM-as-judge, RAGAS, non-deterministic |
| CI | Data validation, model validation | Prompt lint, eval gate, regression test |
| Rollback | Trỏ về weights version cũ | Trỏ về prompt version cũ (+ image cũ nếu code đổi) |
| Monitoring | Prediction drift, feature drift | Cost/token, hallucination rate, latency, user feedback |
Continuous Training (CT) thường không áp dụng cho LLM app
Trong MLOps, CT (tự động retrain khi có data mới) là một trụ cột. Với hầu hết LLM app xây trên API hoặc model có sẵn, bạn không retrain - bạn sửa prompt, sửa retrieval, thêm few-shot. Nếu dự án có fine-tuning (LoRA từ M1, Bài 3), coi nó như một nhánh phụ chạy tách khỏi vòng lặp prompt.
2. Vì sao prompt cần được quản lý¶
Triệu chứng của "prompt không được quản lý"¶
# rag_service.py - prompt nằm ngay trong code, giữa logic
def answer(question: str, context: str) -> str:
prompt = f"""Bạn là trợ lý pháp lý. Dựa vào tài liệu sau, trả lời câu hỏi.
Nếu không có thông tin, nói "Tôi không tìm thấy".
Tài liệu:
{context}
Câu hỏi: {question}"""
return llm(prompt)
Vấn đề khi hệ thống lớn lên:
| Vấn đề | Hệ quả |
|---|---|
| Prompt trộn lẫn với code | Đổi 1 từ trong prompt = deploy lại toàn bộ service |
| Không có version | "Tuần trước chất lượng tốt hơn" - không biết prompt đã đổi gì, không so được |
| Copy-paste giữa nhiều service | Cùng một prompt tồn tại 4 bản hơi khác nhau, sửa chỗ này quên chỗ kia |
| Không tách được người sửa | PM muốn chỉnh giọng văn phải nhờ engineer sửa code |
| Không A/B test được | Muốn thử prompt mới phải deploy, không chạy song song 2 bản được |
| Không rollback nhanh | Prompt mới gây hồi quy lúc 2h sáng - phải revert commit + build + deploy |
Nguyên tắc¶
Prompt là artifact production, không phải hằng số trong code. Nó cần: định danh, version, metadata, lịch sử thay đổi, và cơ chế load động.
3. Prompt Versioning & Templates¶
Tách prompt ra khỏi code¶
Prompt sống trong file riêng (hoặc registry), code chỉ tham chiếu tên + version:
# prompts/rag_answer/v2.yaml
name: rag_answer
version: 2
model: claude-sonnet-5
description: "Trợ lý pháp lý RAG - thêm ràng buộc trích dẫn điều luật"
owner: hung@lingolab.vn
created: 2026-09-01
changelog: "v1 -> v2: bắt buộc trích số điều luật; thêm câu từ chối khi thiếu context"
eval_score: 0.87 # điểm eval subset gần nhất, xem Bài 2
template: |
Bạn là trợ lý pháp lý. Chỉ dùng thông tin trong <tai_lieu>.
Nếu không đủ thông tin, trả lời đúng câu: "Tôi không tìm thấy thông tin này trong tài liệu."
Khi trích dẫn, ghi rõ số điều luật.
<tai_lieu>
{context}
</tai_lieu>
Câu hỏi: {question}
Semantic versioning cho prompt¶
| Loại thay đổi | Bump | Ví dụ |
|---|---|---|
| Major - đổi hành vi, format output, hoặc contract | v1 → v2 |
Đổi từ trả lời tự do sang trả JSON; đổi ngôn ngữ; đổi persona |
| Minor - tinh chỉnh, không đổi contract | v2 → v2.1 |
Sửa câu chữ, thêm 1 few-shot, siết ràng buộc nhỏ |
| Patch - sửa lỗi chính tả, khoảng trắng | v2.1 → v2.1.1 |
Không ảnh hưởng hành vi |
Quy ước đơn giản hoá cho lớp học: chỉ dùng số nguyên tăng dần (v1, v2, v3), mỗi thay đổi có ý nghĩa là một version mới, kèm changelog.
Template + biến¶
Tách phần cố định (instruction, ràng buộc) khỏi phần điền động (context, câu hỏi, few-shot). Dùng một template engine nhẹ:
# prompt_registry/render.py
from string import Template
# hoặc jinja2 nếu cần logic (vòng lặp few-shot, điều kiện)
def render(template: str, **kwargs) -> str:
missing = [k for k in _required_vars(template) if k not in kwargs]
if missing:
raise ValueError(f"Thiếu biến khi render prompt: {missing}")
return Template(template).substitute(**kwargs)
Validate biến là bước rẻ nhất trong CI
90% lỗi prompt production là "quên truyền biến" hoặc "đổi tên biến trong template mà quên sửa code". Một hàm _required_vars() + check ở CI lint chặn được gần hết.
Metadata cần lưu cùng mỗi version¶
modeltarget (prompt tối ưu cho model nào)owner(ai chịu trách nhiệm)eval_scoregần nhất + link tới eval runchangelog(đổi gì so với version trước, vì sao)status:draft/staging/production/deprecated
4. Prompt Registry¶
Registry = nơi lưu tất cả prompt version + metadata, có API để code load theo tên/version, và (lý tưởng) có UI để so sánh.
Hai hướng tiếp cận¶
graph TB
subgraph "Git-based"
F[prompts/*.yaml trong repo] --> PR[Review qua Pull Request]
PR --> TAG[Version = git commit/tag]
TAG --> LOAD1[App load từ file lúc build/khởi động]
end
subgraph "Hosted"
UI[UI: sửa prompt, so sánh version] --> API[(Prompt Hub / PromptLayer / Langfuse)]
API --> LOAD2[App gọi API load prompt lúc runtime]
end
| Git-based | Hosted (LangSmith Prompt Hub / PromptLayer / Langfuse Prompts) | |
|---|---|---|
| Nơi lưu | File YAML/Jinja trong repo | Dịch vụ ngoài, có DB + UI |
| Version | Git commit/tag | Version tự động trong hệ thống |
| Review | Qua PR (diff rõ ràng, có CI) | Qua UI, một số hỗ trợ approval |
| Ai sửa được | Ai biết Git (thường là engineer) | Cả non-engineer qua UI |
| Đổi prompt không deploy code? | Không (trừ khi load động từ config store) | Có - app pull version mới lúc runtime |
| So sánh version | git diff |
UI side-by-side, có playground |
| Phụ thuộc bên ngoài | Không | Có - registry down thì cần fallback cache |
| Phù hợp | Team nhỏ, prompt gắn chặt code, muốn mọi thay đổi qua CI | Team có PM/nhiều người chỉnh prompt, cần đổi nhanh không qua deploy |
Interface tối thiểu của một registry¶
Dù chọn hướng nào, code chỉ nên thấy một interface:
# prompt_registry/__init__.py
from functools import lru_cache
class PromptRegistry:
def get(self, name: str, version: int | str = "production") -> "Prompt":
"""Lấy prompt theo tên + version.
version có thể là số cụ thể (2) hoặc alias ("production", "staging", "latest").
"""
...
def render(self, name: str, version="production", **vars) -> str:
p = self.get(name, version)
return render(p.template, **vars)
@lru_cache
def registry() -> PromptRegistry:
# đọc từ file (git-based) hoặc khởi tạo client (hosted)
...
# Trong service - không còn prompt hardcode
from prompt_registry import registry
def answer(question: str, context: str) -> str:
prompt = registry().render(
"rag_answer", version="production",
question=question, context=context,
)
return llm(prompt)
Registry hosted cần fallback
Nếu load prompt qua API lúc runtime, một sự cố của registry = toàn bộ app chết. Luôn cache version production xuống đĩa/bộ nhớ và dùng bản cache khi API lỗi. Ghi log khi phải fallback.
Git-based registry: bố cục thư mục¶
prompts/
rag_answer/
v1.yaml
v2.yaml
v3.yaml
production.txt # chứa số "3" -> alias production trỏ v3
query_rewrite/
v1.yaml
production.txt
Đổi version production = sửa 1 dòng trong production.txt + PR. Rollback = revert PR đó.
5. Prompt A/B Testing¶
Khi có prompt mới qua được eval gate nhưng bạn vẫn muốn kiểm chứng trên traffic thật trước khi rollout 100%.
Traffic splitting¶
import hashlib
def pick_prompt_version(user_id: str, experiment: dict) -> int:
"""experiment = {"control": 2, "treatment": 3, "treatment_pct": 20}
Sticky theo user_id: cùng user luôn thấy cùng version.
"""
bucket = int(hashlib.sha256(user_id.encode()).hexdigest(), 16) % 100
if bucket < experiment["treatment_pct"]:
return experiment["treatment"]
return experiment["control"]
version = pick_prompt_version(user_id, EXPERIMENT_RAG_ANSWER)
prompt = registry().render("rag_answer", version=version, ...)
# Ghi lại version vào trace/log để phân tích sau
log.info("prompt_ab", user_id=user_id, prompt="rag_answer", version=version)
Nguyên tắc¶
| Nguyên tắc | Vì sao |
|---|---|
Sticky assignment theo user_id (không random mỗi request) |
Trải nghiệm nhất quán; phân tích theo user mới đúng |
| Bắt đầu nhỏ (5–10% treatment) | Giới hạn thiệt hại nếu prompt mới tệ hơn thực tế |
| Định nghĩa primary metric trước khi chạy | Tránh "p-hacking" - nhìn 10 metric rồi chọn cái đẹp |
| Theo dõi guardrail metrics | Prompt mới có thể tăng chất lượng nhưng tăng cost/latency/độ dài → phải chặn |
| Chạy đủ lâu để có significance | Traffic thấp cần vài ngày; đừng kết luận sau 1 giờ |
Đọc kết quả¶
- Primary metric (ví dụ: tỉ lệ thumbs-up, hoặc eval score trên sample production) - treatment có tốt hơn không, chênh lệch có ý nghĩa thống kê không.
- Guardrail: cost/request, p95 latency, độ dài output trung bình - không được xấu đi quá ngưỡng.
- Nếu treatment thắng cả primary lẫn guardrail → promote version đó thành
production. Nếu không → giữ control, ghi lại bài học vàochangelog.
6. Hands-on: Prompt registry cho Vietnamese RAG assistant¶
Dùng lại assistant từ Capstone Module I.
Bước 1 - Tách prompt ra file¶
- Tạo
prompts/rag_answer/v1.yamlvới prompt hiện tại của capstone. - Viết
prompt_registrytối thiểu (git-based):get(),render(), aliasproductionđọc từproduction.txt. - Thay chỗ hardcode trong service bằng
registry().render("rag_answer", ...).
Bước 2 - Tạo version mới + diff¶
- Tạo
v2.yaml: thêm ràng buộc "bắt buộc trích số điều luật" và câu từ chối chuẩn khi thiếu context. git diffgiữa v1 và v2. Điềnchangeloggiải thích vì sao đổi.
Bước 3 - So sánh hai version¶
- Lấy 5 câu hỏi từ tài liệu pháp lý.
- Chạy assistant với
version=1rồiversion=2, in output cạnh nhau. - Ghi nhận: v2 có luôn trích điều luật không? Có từ chối đúng khi hỏi ngoài tài liệu không?
Bước 4 (mở rộng) - A/B scaffold¶
- Thêm
pick_prompt_version()vớitreatment_pct=50. - Log version đã chọn cho mỗi request. Chạy 20 câu hỏi, xác nhận phân bổ ~50/50 và sticky theo
user_id.
Tiêu chí hoàn thành¶
- Không còn prompt hardcode trong service
- Có ≥ 2 version với
changelogvà metadata đầy đủ - Đổi version production chỉ bằng sửa
production.txt+ không cần sửa code - Rollback = revert 1 commit
- Có script so sánh output giữa 2 version
Tóm tắt¶
graph LR
EDIT[Sửa/ thêm prompt] --> VER[Version mới + changelog + metadata]
VER --> REG[(Prompt Registry<br/>git-based hoặc hosted)]
REG --> EVAL[Eval gate - Bài 2]
EVAL -->|pass| AB[A/B test trên traffic thật]
AB -->|treatment thắng| PROD[Promote -> production alias]
AB -->|thua| KEEP[Giữ control, ghi bài học]
PROD -.hồi quy.-> ROLLBACK[Rollback = trỏ alias về version cũ]
| Khái niệm | Cốt lõi |
|---|---|
| LLMOps vs MLOps | Không retrain; artifact là prompt+config; vòng lặp tính bằng giờ |
| Prompt versioning | Semantic version + changelog giải thích "vì sao"; validate biến trong CI |
| Prompt registry | Git-based (mọi thay đổi qua PR) vs Hosted (đổi nhanh, cần fallback cache) |
| Registry interface | Code chỉ thấy get(name, version) / render(...); alias production |
| A/B testing | Sticky theo user_id; primary + guardrail metrics; bắt đầu nhỏ |