# 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.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-feature-flag-canary-cho-prompt-va-model/

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.

```python
FLAG = {
"name": "support_bot_prompt_v2",
"enabled": True,           # nút tắt khẩn cấp
"rollout_percent": 5,
"internal_users": {"qa@khachhang.vn"},
}

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ự đó.

```python
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.

```python
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.

**Điểm mấu chốt:** Sửa một dòng prompt cũng là deploy: cần flag, cần nhóm control và cần sẵn nút tắt.

## 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.

```python
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.

**Thử ngay tuần này:**

- Chạy hàm bucket() với 10.000 user_id giả để kiểm tra tỷ lệ canary gần đúng con số đặt trong flag và mỗi người dùng luôn rơi vào cùng một variation
- Thêm hai trường variation và prompt_version vào log của một tính năng LLM bạn đang làm, rồi viết một truy vấn so sánh latency giữa hai nhóm
- Chọn tối đa 5 chỉ số guardrail cho tính năng đó, ghi sẵn ngưỡng rollback và số mẫu tối thiểu cho từng loại câu hỏi hiếm trước khi bật canary

## Nguồn

- [Chapter 16 - Canarying Releases (Google SRE Workbook)](https://sre.google/workbook/canarying-releases/)

- [Progressive Delivery for Building LLM-Powered Features](https://flagsmith.com/blog/progressive-delivery-llm-powered-features)

- [Guarded Rollouts for AI Configs](https://launchdarkly.com/changelog/guarded-rollouts-for-ai-configs/)

- [Why Gradual Rollouts Don't Work for AI Features (And What to Do Instead)](https://tianpan.co/blog/2026-04-15-gradual-rollouts-fail-ai-features)

- [LLM Guardrails With Feature Flags: Route, Compare, and Roll Back Safely](https://www.featbit.co/blogs/llm-guardrails-feature-flags)
