Bỏ qua

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:

  1. 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.
  2. 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

  • model target (prompt tối ưu cho model nào)
  • owner (ai chịu trách nhiệm)
  • eval_score gần nhất + link tới eval run
  • changelog (đổ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ào changelog.

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.yaml với prompt hiện tại của capstone.
  • Viết prompt_registry tối thiểu (git-based): get(), render(), alias production đọ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 diff giữa v1 và v2. Điền changelog giả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=1 rồi version=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ới treatment_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 changelog và 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ỏ