Sửa một dòng prompt cũng là deploy: canary 5-20-50-100% tại khách hàng
Một lần sửa prompt chỉ đổi cách trình bày cũng có thể làm độ chính xác lệch khoảng 5%, nên tại khách hàng, mọi thay đổi như vậy đều cần một feature flag để chia lưu lượng, một nhóm control để so sánh và một nút tắt luôn sẵn sàng.
Canary chỉ được lên nấc khi không vượt guardrail nào so với control và đã gom đủ mẫu, kể cả câu hỏi hiếm.
Đồ hoạ: FDE Times
Tóm tắt nhanh
- Prompt hoặc model chạy tốt ở staging vẫn có thể làm trải nghiệm tệ đi khi lên production, nên lần thay đổi nào cũng cần flag và nhóm control.
- Ghi log variation và prompt_version ngay lúc gọi LLM. Nếu ghi lúc tải trang, số liệu canary sẽ sai.
- Canary nhỏ có thể bỏ sót những câu hỏi hiếm làm model trả lời sai, nên mỗi nấc chỉ được tăng khi đã gom đủ mẫu, không phải khi dashboard trông ổn.
Thử hình dung: khách hàng nhờ bạn sửa prompt để chatbot hỗ trợ “lịch sự hơn”. Bạn thêm vài câu chào, đổi gạch đầu dòng thành đánh số, chạy thử trên staging thấy ổn cả. Hai ngày sau, đội chăm sóc khách hàng báo rằng bot đang trả lời sai chính sách hoàn tiền.
Tình huống giả định này không xa thực tế. LaunchDarkly cảnh báo rằng chỉ cần sửa nhẹ prompt hoặc đổi model, hệ thống có thể chạy tốt ở staging mà vẫn làm trải nghiệm người dùng tệ đi trên production. Một phân tích trên blog tianpan.co ghi nhận những thay đổi nhỏ về định dạng prompt đã làm độ chính xác lệch khoảng 5%.
Vì thế, đổi gạch đầu dòng thành đánh số cũng là deploy. Bài này hướng dẫn bạn tự dựng toàn bộ quy trình: một feature flag chia lưu lượng giữa prompt cũ và prompt mới, phần log để so sánh hai nhóm, một hàm quyết định rollback và lịch tăng dần qua các nấc 5-20-50-100%.
Code trong bài là Python minh họa, đã được rút gọn và không dùng SDK của hãng nào. Khi làm thật, bạn thay phần này bằng công cụ flag mà khách hàng đang dùng.
Canary thực chất là một phép so sánh
Google SRE Workbook định nghĩa canarying là triển khai thay đổi cho một phần dịch vụ trong thời gian có giới hạn, và đánh giá thay đổi đó. Phần nhận thay đổi gọi là canary, phần còn lại gọi là control. Vì vậy, nếu không có nhóm control để đặt cạnh, bạn mới chỉ đang chạy thử, chưa phải đang làm canary.
Bạn cần chuẩn bị: Python 3, một hàm gọi LLM sẵn có (trong bài gọi là call_llm), một nơi ghi log có thể truy vấn được, và hai phiên bản prompt đã được đặt tên rõ ràng, ví dụ v1.3 và v2.0.
Bước 1: Gom prompt và model vào variation
Đừng sửa thẳng chuỗi prompt trong code. Hãy khai báo mỗi cấu hình thành một variation có tên, để khi rollback bạn chỉ cần đổi lựa chọn chứ không phải deploy lại.
FLAG = {
"name": "support_bot_prompt_v2",
"enabled": True, # nút tắt khẩn cấp
"rollout_percent": 5,
"internal_users": {"[email protected]"},
}
VARIATIONS = {
"control": {"model": "model-hien-tai", "prompt_version": "v1.3"},
"canary": {"model": "model-hien-tai", "prompt_version": "v2.0"},
}
Trong ví dụ này, hai variation chỉ khác nhau ở prompt. Nếu bạn muốn đổi model, hãy làm ở một flag riêng. Khi cả prompt và model cùng đổi trong một lần, bạn sẽ không biết thay đổi nào gây ra kết quả.
Bước 2: Chia người dùng ổn định, nhóm nội bộ đi trước
Flagsmith mô tả quy trình của họ với tính năng LLM như sau: đầu tiên bật tính năng trên production nhưng chỉ cho người dùng nội bộ, sau đó mở cho 5% người dùng, tiếp theo là 50% để chạy A/B, rồi mới lên 100%. Đoạn code dưới đây làm theo đúng thứ tự đó.
import hashlib
def bucket(user_id: str, flag_name: str) -> int:
h = hashlib.sha256(f"{flag_name}:{user_id}".encode()).hexdigest()
return int(h[:8], 16) % 100
def pick_variation(user_id: str, email: str) -> str:
if not FLAG["enabled"]:
return "control"
if email in FLAG["internal_users"]:
return "canary"
if bucket(user_id, FLAG["name"]) < FLAG["rollout_percent"]:
return "canary"
return "control"
Kiểm tra: chạy hàm với 10.000 user_id giả và đếm số lần rơi vào canary, kết quả nên quanh mức 500. Gọi lại với cùng một user_id thì phải luôn ra cùng một variation. Nếu không ổn định như vậy, một người có thể nhận câu trả lời theo prompt cũ ở lượt hỏi này và prompt mới ở lượt sau, khiến dữ liệu của bạn bị nhiễu.
Bước 3: Ghi log đúng thời điểm
FeatBit khuyên nên ghi lại variation vào đúng lúc LLM thực sự chạy, đừng ghi lúc trang vừa tải. Họ cũng khuyên ghi kèm promptVersion, để nếu ai đó lặng lẽ sửa prompt giữa chừng thì sự thay đổi ấy không bị lẫn vào kết quả canary.
import time
def answer(user_id, email, question):
variation = pick_variation(user_id, email)
cfg = VARIATIONS[variation]
started = time.time()
reply = call_llm(cfg["model"], load_prompt(cfg["prompt_version"]), question)
log_event({
"user_id": user_id,
"flag": FLAG["name"],
"variation": variation,
"prompt_version": cfg["prompt_version"],
"model": cfg["model"],
"latency_ms": int((time.time() - started) * 1000),
})
return reply
call_llm, load_prompt và log_event là những hàm bạn đã có sẵn. Kiểm tra: mỗi dòng log phải có đủ bốn trường variation, prompt_version, model và latency. Nếu thiếu một trường, bạn sẽ không so sánh được hai nhóm một cách đáng tin. Khi làm thật, bạn nên ghi thêm loại câu hỏi (ví dụ refund) để bước sau đếm được mẫu theo từng loại.
Bước 4: Theo dõi ít chỉ số, đặt ngưỡng trước
Google SRE khuyên chỉ chọn vài chỉ số quan trọng nhất để đánh giá canary, có lẽ không quá một tá. Với một chatbot hỗ trợ khách hàng, bạn có thể bắt đầu với tỷ lệ lỗi, latency, tỷ lệ người dùng phải chuyển sang nhân viên thật, và điểm chất lượng chấm trên một mẫu hội thoại.
Hàm dưới đây trả về một trong ba quyết định: rollback, chờ thêm, hoặc lên nấc. Guardrail được kiểm tra trước, vì một lỗi rõ ràng thì nên dừng ngay. Còn muốn lên nấc thì canary phải gom đủ mẫu, cả tổng số lẫn loại câu hỏi hiếm.
MIN_TOTAL = 500 # tổng số hội thoại tối thiểu ở canary
MIN_REFUND = 20 # số câu hỏi hoàn tiền tối thiểu
def decide(canary: dict, control: dict) -> str:
if canary["error_rate"] > control["error_rate"] * 1.5:
return "rollback"
if canary["p95_latency_ms"] > control["p95_latency_ms"] * 1.3:
return "rollback"
if canary["handoff_rate"] > control["handoff_rate"] + 0.03:
return "rollback"
if canary["quality_score"] < control["quality_score"] - 0.05:
return "rollback"
if canary["n_total"] < MIN_TOTAL or canary["n_refund"] < MIN_REFUND:
return "wait"
return "promote"
Mọi ngưỡng trong đoạn trên chỉ là ví dụ. Hãy thống nhất ngưỡng thật với khách hàng trước khi bật canary, đừng đợi có số liệu rồi mới đặt. Các công cụ thương mại cũng làm theo hướng này: guarded rollout của LaunchDarkly cho AI Configs cho phép gắn chỉ số guardrail và tự động rollback về variation trước đó.
Bước 5: Mỗi nấc chạy bao lâu?
Tại sao bắt đầu ở 5% mà không phải 1%? Bài viết trên tianpan.co cho rằng với tính năng LLM, canary 1% có thể không chạm tới phần đuôi của phân phối input, tức những trường hợp hiếm mà model trả lời kém đi.
Tác giả còn lưu ý rằng canary cho LLM có thể phải chạy hàng giờ, thậm chí hàng ngày, mới gom đủ dữ liệu đã được gán nhãn hoặc gán nhãn gián tiếp.
Quay lại tình huống giả định ở đầu bài. Hệ thống của khách có 2.000 hội thoại mỗi ngày, và câu hỏi hoàn tiền chiếm 2% lưu lượng. Ở mức 5%, canary nhận 100 hội thoại mỗi ngày, trong đó chỉ khoảng 2 câu về hoàn tiền.
Với ngưỡng MIN_REFUND = 20, nấc 5% cần khoảng 10 ngày mới cho phép decide() trả về “promote”. Ở mức 1%, con số này là 0,4 câu mỗi ngày, gần như không thấy gì.
Vì vậy, thời gian chạy mỗi nấc nên tính theo số mẫu bạn cần, đừng tính theo số giờ, và nếu 10 ngày là quá lâu thì đó là chuyện cần bàn với khách ngay từ đầu.
| Nấc | Ai nhận canary | Điều kiện để lên nấc tiếp (gợi ý) |
|---|---|---|
| Nội bộ | Nhóm QA của khách | Không còn lỗi nghiêm trọng khi đọc thủ công |
| 5% | Một phần nhỏ người dùng thật | Đủ mẫu cho các loại câu hỏi hiếm, guardrail không bị vượt |
| 20% | Rộng hơn, dữ liệu so sánh rõ hơn | Chỉ số chất lượng không thua control |
| 50% | Chạy A/B, hai nhóm gần bằng nhau | Khách hàng ký duyệt kết quả |
| 100% | Toàn bộ người dùng | Giữ flag thêm một thời gian để có thể rollback |
Flagsmith chỉ nêu các nấc 5%, 50% và 100%. Nấc 20% trong bảng là bước đệm được thêm vào để bạn có dữ liệu so sánh rõ hơn trước khi chạy A/B ở 50%.
Bước 6: Diễn tập nút tắt
Flagsmith viết rằng chỉ cần thấy vấn đề là họ có thể tắt flag ngay lập tức. Với code ở trên, chỉ cần đặt FLAG["enabled"] = False là mọi người dùng quay về control. Hãy thử thao tác này một lần trên staging cùng kỹ sư của khách, và ghi lại mất bao nhiêu giây để thay đổi có hiệu lực.
Ba lỗi thường gặp
Lỗi đầu tiên là đổi prompt và model trong cùng một flag, nên khi kết quả xấu thì không biết nên quay lại cái nào. Lỗi thứ hai là chỉ ghi log ở nhóm canary, đến lúc so sánh thì nhóm control không có dữ liệu.
Lỗi thứ ba là tăng từ 5% lên 50% chỉ sau một buổi chiều vì dashboard trông ổn, trong khi các câu hỏi hiếm vẫn chưa xuất hiện lần nào. Hàm decide() có nhánh “wait” chính là để chặn lỗi này.
Kỹ năng này xuất hiện thế nào khi đi làm
Yêu cầu kiểu “sửa giúp prompt cho bot lịch sự hơn” chính là loại thay đổi mà quy trình trên nhắm tới. Trước khi nhận việc, bạn nên chuẩn bị sẵn câu trả lời cho hai câu hỏi: nếu sai thì tắt bằng cách nào, và mất bao lâu.
Khi đọc JD, hãy tìm các cụm như progressive delivery, feature flag, A/B testing hoặc LLM evaluation. Trong CV, đừng chỉ ghi “dùng LaunchDarkly”. Hãy viết cụ thể: đã tách prompt_version vào log, đặt ngưỡng guardrail và số mẫu tối thiểu, đưa thay đổi lên 100% qua bốn nấc, và kill switch có hiệu lực trong bao nhiêu giây.
Lần tới khách hàng nhờ đổi model, bạn nên trả lời bằng một lịch triển khai theo từng nấc, đừng chỉ báo là đã sửa xong.
5 nguồn
- Chapter 16 - Canarying Releases (Google SRE Workbook)
- Progressive Delivery for Building LLM-Powered Features
- Guarded Rollouts for AI Configs · 2025-07-24
- Why Gradual Rollouts Don't Work for AI Features (And What to Do Instead) · 2026-04-15
- LLM Guardrails With Feature Flags: Route, Compare, and Roll Back Safely · 2026-06-13