CI/CD cho LLM agent: golden set, giám sát online & bánh đà cải thiện
TL;DR
- CI = unit test cho known unknowns: chạy golden dataset (20–50 case lúc đầu, 100–200 khi trưởng thành) trước khi deploy để chặn regression trên failure mode đã biết. CI pass không chứng minh chất lượng production — nó giống smoke test.
- CD = giám sát online cho unknown unknowns: log trace đầy đủ, chấm bất đồng bộ 1–5% traffic bằng 5–7 evaluator, tính success rate đã hiệu chỉnh + khoảng tin cậy 95%, alert khi dải tin cậy chạm ngưỡng.
- Guardrail nằm trên đường trả lời → phải tất định, mili-giây, false positive < ~1%; khi trip thì Reject / Retry / Fallback.
- Pin phiên bản model cho cả agent lẫn judge; nâng cấp model = một PR phải qua CI; tái kiểm định judge vài tuần/lần để bắt judge drift.
- CI và CD nối thành bánh đà: lỗi mới ở production → error analysis → thêm case vào golden set + evaluator mới → sửa → qua CI → deploy lại.
Cách đọc trang này
Phần diễn giải ý của sách được viết gọn bằng lời của sổ tay. Phần lớn độ dài còn lại là tư liệu tự soạn: ví dụ tính tay, code minh hoạ (không phải code trong sách), bài tập có lời giải, một case study hư cấu từ đầu đến cuối và 4 lab tương tác. Mọi con số trong ví dụ/case study là số liệu minh hoạ, trừ khi ghi rõ là của sách.9.1Vì sao LLM agent cần CI/CD
Câu chuyện mở đầu của sách · GPT-4 "tự đổi tính"
Một nghiên cứu năm 2023 chạy cùng bộ bài toán trên API GPT-4 ở hai thời điểm cách nhau khoảng 3 tháng: accuracy nhận diện số nguyên tố tụt từ 84% xuống 51%. Nhà cung cấp đã thay model phía sau endpoint mà không thông báo. Team gọi endpoint đó trong production chỉ biết khi người dùng phàn nàn — trong khi một bộ regression chạy hằng đêm trên tập test cố định lẽ ra đã bắt được.Làm agent chạy tốt một lần đã khó; giữ nó chạy tốt theo thời gian là bài toán khác hẳn. Có ít nhất ba lực kéo chất lượng đi xuống mà không cần bạn sửa dòng code nào: nhu cầu người dùng thay đổi (họ hỏi những thứ mới), phân bố dữ liệu trôi (mùa vụ, chiến dịch marketing, thị trường mới), và nhà cung cấp model âm thầm cập nhật. Chương 9 mượn hai thực hành từ software engineering — Continuous Integration và Continuous Deployment — nhưng gán cho mỗi cái một vai rất cụ thể:
Ví dụ của sách cho hai vai này đều lấy từ trợ lý bất động sản (Example 3-1): CI xác nhận một lỗi về địa danh đã sửa tháng trước không quay lại sau khi sửa prompt; còn CD phát hiện một tính năng mới khiến LLM "chế" ra một tên khu phố không tồn tại — thứ không test nào lường trước.
Khác gì CI/CD cho ML truyền thống?
| Khía cạnh | ML truyền thống | LLM agent |
|---|---|---|
| Dữ liệu | Tập gán nhãn lớn; CI chạy lại sau mỗi lần retrain | Ít ví dụ, chọn quanh failure mode đã biết; logic chính nằm trong prompt + foundation model, không phải weights của bạn |
| Kiểu lỗi | Data drift, concept drift, bug feature engineering | Nhạy prompt, provider cập nhật ngầm, hallucination, judge drift, lỗi suy luận nhiều bước |
| Đo chất lượng | Accuracy, F1, AUC trên nhãn rõ | Chủ quan, đa chiều (tone, faithfulness, relevance) → evaluator tuỳ biến, thường LLM-as-judge, trên mẫu traffic |
| Tính tất định | Cố định version → cùng input, cùng output | Output dao động (temperature > 0) → test phải chịu được biến thiên hoặc chạy lặp; có codebase đánh dấu test LLM là flaky và cho chạy lại |
Một trợ lý đặt vé xe khách liên tỉnh (hư cấu, gọi là VéXe) chạy ổn định 3 tháng với success rate khoảng 93% (minh hoạ). Trong một quý, ba chuyện sau xảy ra mà repo không có commit nào:
- Nhu cầu người dùng đổi: gần Tết, tỷ lệ câu hỏi "đổi vé / hoàn vé" tăng từ 8% lên 31% traffic. Agent vốn yếu ở nhánh hoàn vé (tỷ lệ đúng 78%) → success rate tổng tụt vì trộn traffic đổi, dù từng nhánh không đổi.
- Phân bố dữ liệu trôi: nhà xe thêm tuyến mới với tên bến viết tắt ("BX MĐ mới") — retriever chưa có tài liệu, LLM đoán bừa giờ chạy.
- Provider cập nhật model phía sau alias đang gọi: JSON tool call thỉnh thoảng thiếu trường
seat_class.
Bài học: (1) và (2) là unknown unknowns — chỉ monitoring trên traffic thật mới thấy. (3) lẽ ra CI chạy hằng đêm với model đã pin sẽ bắt ngay (hoặc không xảy ra vì pin version). Không có lực nào trong ba lực này xuất hiện trong git log.
Nhầm lẫn thường gặp · "CI/CD" ở đây không phải pipeline build
Trong software thường, CI/CD gợi tới build, lint, test đơn vị, deploy container. Chương này dùng chữ đó cho đánh giá hành vi: CI = cổng đánh giá trước khi merge; CD = đánh giá liên tục sau khi deploy. Pipeline build vẫn cần, nhưng một agent có thể build xanh, unit test xanh mà vẫn trả lời sai chính sách hoàn tiền — đó là thứ cổng eval phải bắt.Với mỗi tình huống, cho biết CI hay CD là tuyến phòng thủ chính: (a) bug "agent trả lời tiếng Anh khi khách viết tiếng Việt không dấu" đã sửa tuần trước; (b) một nhóm người dùng bắt đầu dán ảnh chụp màn hình hoá đơn vào chat; (c) đổi embedding model của retriever; (d) provider tăng context window, team muốn nhồi thêm tài liệu vào prompt.
Xem lời giải
(a) CI — lỗi đã biết, đã sửa → thêm vào golden set làm regression test. (b) CD — kiểu input mới chưa ai lường; chỉ thấy qua log/trace và đọc mẫu hằng ngày. (c) CI trước (đây là thay đổi code, phải qua golden set phủ nhánh RAG) rồi CD theo dõi faithfulness sau deploy. (d) Là một thay đổi prompt/cấu hình → CI; nhưng nên kèm error analysis mới vì nhiều context hơn có thể đẻ lỗi mới (loãng thông tin) mà golden set chưa có.
9.2Golden dataset: thiết kế lưới an toàn
Golden dataset là tập input chọn tay kèm output tham chiếu (reference) hoặc tiêu chí kiểm tra, được tạo qua review + gán nhãn (Ch.3) và đồng thuận nhóm về "thế nào là đúng" (Ch.4). Mỗi thay đổi — sửa prompt, đổi model, thêm tool — đều chạy toàn bộ golden input qua agent rồi chấm bằng evaluator tự động (Ch.5). Sách liên hệ cách làm này với kiểu "assertion-based unit test": lấy input thật từ production, đặt vài tiêu chí cho mỗi input, và kiểm agent vẫn thoả chúng. CI ở đây là reference-based: so output mới với reference đã được người xác nhận.
Golden set tốt gồm những gì?
Ví dụ phủ những việc chính agent phải làm được.
Mỗi bug nghiêm trọng đã sửa trở thành một regression test.
Những ca hiểm tìm ra trong error analysis.
Đi qua từng tool, nhánh RAG, nhánh prompt khác nhau.
Golden set ≠ mẫu ngẫu nhiên
Nó được thiết kế có chủ đích để stress-test những hành vi bạn muốn giữ. Sách gợi ý bắt đầu với ~20–50 ví dụ; hệ thống trưởng thành thường có ~100–200. Chỉ thêm ví dụ khi đã xác nhận một failure mode mới (qua error analysis hoặc monitoring) — không phải vì "có thêm data". Định kỳ audit để loại/cập nhật ví dụ đã cũ (không còn đi qua code path đang chạy).Ví dụ check CI (trợ lý bất động sản của sách)
| Check | So với cái gì | Loại evaluator |
|---|---|---|
| Tone khớp persona khách hàng | Output mẫu đã được người duyệt | LLM judge |
| Tham số tool (vd: tra lịch trống) đúng schema | Định nghĩa schema của tool | Code |
| Email người nhận có tồn tại | Cơ sở dữ liệu khách hàng | Code |
Bất kỳ check nào fail trên golden case tương ứng → build fail → chặn merge. Để ý: hai trong ba check là code, rẻ và tất định; chỉ tiêu chí chủ quan (tone) mới cần judge.
Team VéXe vừa làm xong error analysis (Ch.3) trên 300 trace và có bảng failure mode sau (minh hoạ): sai giờ chạy do đoán (41 trace), tool book_seat thiếu tham số (27), trả lời sai chính sách hoàn vé (19), giọng quá cứng với khách lớn tuổi (11), nhầm bến cùng tên ở hai tỉnh (6).
- Liệt kê hành vi cốt lõi cần giữ: tra lịch, đặt ghế, đổi vé, hoàn vé, hỏi giá, hỏi bến → chọn 3 case "bình thường" cho mỗi hành vi = 18 case.
- Mỗi failure mode đã sửa → 2–3 case regression, chọn trace thật tiêu biểu nhất (không chọn trùng kiểu): 5 mode × ~2.4 = 12 case. Đánh dấu
critical: truecho hoàn vé và thiếu tham số tool (gây thiệt hại tiền/đặt sai). - Edge case từ ghi chú error analysis: tin nhắn không dấu, hỏi 2 tuyến trong 1 câu, ngày "mùng 3 Tết" (lịch âm), bến trùng tên → 10 case.
- Phủ nhánh: đảm bảo mỗi tool và nhánh RAG (chính sách, lịch chạy) có ít nhất 1 case đi qua → bổ sung 8 case cho nhánh chưa ai chạm tới.
- Gắn check cho từng case: cái gì kiểm bằng code (schema tool, ngày hợp lệ, số tiền hoàn đúng công thức), cái gì cần judge (giọng điệu, đầy đủ thông tin). Tổng 48 case, ~110 check.
- Lưu tách khỏi prompt trong
evals/golden/, version cùng repo, mỗi case ghi nguồn (trace id / sự cố) để sau này audit.
Điều cố ý KHÔNG làm: không lấy mẫu ngẫu nhiên 200 trace cho "đủ lớn" — phần lớn sẽ là câu hỏi dễ, pass mãi, chỉ làm CI chậm và che mất tín hiệu.
Một định dạng lưu trữ đơn giản (JSONL, mỗi dòng một case) — tự soạn cho sổ tay:
{"id":"gs-007","kind":"core","critical":false,"input":"Còn ghế chuyến 7h sáng mai Hà Nội - Hải Phòng không?",
"checks":[{"type":"tool_called","name":"search_trips"},{"type":"json_schema","tool":"search_trips","schema":"schemas/search_trips.json"}],
"source":"trace:8f2c1a","last_audit":"2026-09-01"}
{"id":"gs-022","kind":"past_failure","critical":true,"input":"Mình huỷ vé trước 20 tiếng thì được hoàn bao nhiêu?",
"reference":"Hoàn 70% giá vé (huỷ trước 12–24h theo chính sách v3).",
"checks":[{"type":"regex","must_match":"70\\s*%"},{"type":"judge","criterion":"policy_faithful","judge":"judge-policy@v4"}],
"source":"incident:INC-117","last_audit":"2026-09-01"}
{"id":"gs-041","kind":"edge","critical":false,"input":"ve di vinh mung 3 tet con ko",
"checks":[{"type":"code","fn":"checks.lunar_date_resolved"},{"type":"judge","criterion":"tone_friendly","judge":"judge-tone@v2"}],
"source":"trace:c19e77","last_audit":"2026-08-15"}
Nhầm lẫn thường gặp · "Reference-based" không có nghĩa là so khớp chuỗi
Reference là chuẩn đã được người xác nhận, còn phép so thì tuỳ tiêu chí: có thể là regex tìm con số quan trọng, JSON schema, một hàm tra DB, hoặc judge so ý nghĩa với output mẫu. So khớp nguyên văn với LLM gần như luôn fail vô lý — đó là lý do khi nâng cấp model, sách khuyên phân biệt "regression thật" với "chỉ đổi format mà evaluator quá nhạy".Bảo trì: thêm, sửa, cho nghỉ hưu
| Sự kiện | Hành động với golden set |
|---|---|
| Monitoring/error analysis xác nhận failure mode mới | Thêm 5–10 case tiêu biểu (sách dùng đúng quy mô này trong ví dụ bánh đà) + evaluator tương ứng |
| Chính sách/dữ liệu nghiệp vụ đổi (vd bảng giá, điều kiện hoàn) | Cập nhật reference; ghi changelog để biết CI đỏ vì chuẩn đổi chứ không phải agent hỏng |
| Tính năng bị gỡ, case không còn đi qua code path nào | Cho nghỉ hưu (archive, không xoá) — giữ lịch sử |
| Hai case kiểm cùng một hành vi | Gộp/bỏ bớt — mỗi case phải đại diện một hành vi cụ thể |
Bạn có 10 ứng viên cho golden set của một chatbot CSKH ngân hàng, nhưng chỉ muốn thêm 5. Ứng viên: (1) "số dư tài khoản bao nhiêu" — câu hỏi phổ biến nhất; (2) trace từng khiến bot đọc to số thẻ đầy đủ (sự cố PII tháng trước); (3) "số dư của tôi là bao nhiêu vậy" — biến thể của (1); (4) câu hỏi về khoản vay đi qua tool loan_calc chưa case nào chạm; (5) khách viết lẫn tiếng Anh–Việt; (6) 20 câu chào hỏi "hi", "alo"; (7) khách hỏi lãi suất của một sản phẩm đã ngừng bán năm ngoái; (8) trace bot xác nhận chuyển tiền khi khách chỉ hỏi phí; (9) câu hỏi về tỷ giá (tính năng đã gỡ); (10) khách tức giận chửi thề.
Xem lời giải
Chọn (2) lỗi nghiêm trọng đã xảy ra, critical; (8) lỗi nghiêm trọng (hành động sai), critical; (4) phủ nhánh tool chưa được test; (1) tính năng cốt lõi (chỉ 1 case, không cần (3) trùng hành vi); và (7) hoặc (10) làm edge case — (7) tốt hơn nếu error analysis đã thấy bot bịa lãi suất sản phẩm cũ. Loại (6): 20 case trùng một hành vi tầm thường; loại (9): tính năng đã gỡ = case chết; loại (3): trùng (1).
9.3Cổng CI trong thực tế: runner, ngưỡng & tính không tất định
Sách chỉ ra nguyên tắc (chạy golden set mỗi thay đổi, fail thì chặn merge) và ghi chú rằng có codebase đánh dấu test LLM là flaky để chạy lại vài lần. Phần này là tư liệu tự soạn, đi sâu vào câu hỏi thực hành: đặt ngưỡng thế nào khi chính agent đã "ồn"?
Trực giác: hai loại sai của một cái cổng
Cổng CI là một bộ phân loại nhị phân trên thay đổi: "có regression" hay "không". Nó sai theo hai cách:
PR không làm hỏng gì nhưng CI đỏ vì agent "xui" ở lần chạy này. Hậu quả: dev mất niềm tin, bấm "re-run" cho tới khi xanh, rồi dần phớt lờ CI.
PR làm hỏng thật nhưng CI xanh vì golden set quá nhỏ hoặc ngưỡng quá lỏng. Hậu quả: bug ra production, monitoring phải gánh.
Golden set 50 case. Bản hiện tại thật sự pass 46 case; nhưng agent chạy temperature > 0 nên mỗi case có xác suất ~4% cho kết quả ngược ở một lần chạy (minh hoạ).
- Luật "100% phải pass" trên 50 case "chắc chắn đúng": nếu cả 50 đều đúng thật, xác suất CI xanh = 0,9650 ≈ 13%. Tức 87% PR vô tội bị chặn. Luật "không được fail case nào" chỉ khả thi khi case được làm tất định (temperature 0, check bằng code) hoặc chạy lặp.
- Luật ngưỡng tổng ≥ 44/50: bản gốc (46 đúng) bị chặn nhầm ≈ 25% số lần chạy; bản làm hỏng 3 case (43 đúng) lọt xanh ≈ 5%. Nâng ngưỡng lên 45 thì lọt còn 0,6% nhưng chặn nhầm lên 51% — không có ngưỡng nào tốt cho cả hai.
- Chạy 3 lần, lấy đa số cho từng case: xác suất một case bị lật giảm từ 4% xuống 3·0,04²·0,96 + 0,04³ ≈ 0,47%. Với ngưỡng ≥ 44: chặn nhầm còn ≈ 0,1%, lọt ≈ 2,6%. Giá phải trả: chi phí CI ×3.
- Kết luận thiết kế: tách case critical (phải pass, check tất định, hoặc chạy lặp lấy đa số) khỏi phần còn lại (ngưỡng tổng có dung sai, so với baseline đo cùng điều kiện).
Mẫu thiết kế cổng hai tầng
- Tầng 1 — critical, không thương lượng. Case PII, hành động có hậu quả tiền bạc, lỗi từng gây sự cố. Check bằng code nếu được; nếu cần judge thì chạy k=3 lấy đa số. Fail một case = đỏ.
- Tầng 2 — tổng thể, có dung sai. Pass rate trên phần còn lại không được thấp hơn baseline (đo trên nhánh main, cùng golden set, cùng judge đã pin) quá Δ điểm %. Δ chọn theo độ ồn đo được, không đoán.
- Báo cáo diff theo case chứ không chỉ con số: case nào lật từ xanh sang đỏ, kèm output cũ/mới — reviewer mất 2 phút là biết regression thật hay đổi format.
- Baseline cập nhật khi merge vào main, lưu cùng hash của golden set + version model/judge để không so táo với cam.
Runner kiểu pytest — tự soạn, chạy được với một hàm agent() và các check giả lập của bạn:
# evals/test_golden.py — chạy: pytest evals/ -q
import json, os, pathlib, statistics
import pytest
from myagent import agent # hàm agent(input: str) -> dict {"text":..., "tool_calls":[...]}
from evals.checks import run_check # run_check(check: dict, case: dict, out: dict) -> bool
GOLDEN = [json.loads(l) for l in pathlib.Path("evals/golden/golden.jsonl").read_text().splitlines() if l.strip()]
K = int(os.getenv("EVAL_REPEATS", "3")) # số lần chạy mỗi case để lấy đa số
BASELINE = json.loads(pathlib.Path("evals/baseline.json").read_text()) # {"pass_rate": 0.9, "golden_hash": "..."}
MAX_DROP = float(os.getenv("EVAL_MAX_DROP", "0.03")) # dung sai tầng 2: 3 điểm %
def case_passes(case) -> bool:
votes = []
for _ in range(K):
out = agent(case["input"])
votes.append(all(run_check(c, case, out) for c in case["checks"]))
return sum(votes) > K / 2 # đa số
CRITICAL = [c for c in GOLDEN if c.get("critical")]
REGULAR = [c for c in GOLDEN if not c.get("critical")]
@pytest.mark.parametrize("case", CRITICAL, ids=[c["id"] for c in CRITICAL])
def test_critical(case):
assert case_passes(case), f"critical case {case['id']} failed (nguồn: {case.get('source')})"
def test_regular_pass_rate(record_property):
results = {c["id"]: case_passes(c) for c in REGULAR}
rate = statistics.mean(results.values())
record_property("pass_rate", rate)
failed = [cid for cid, ok in results.items() if not ok]
pathlib.Path("eval-report.json").write_text(json.dumps({"pass_rate": rate, "failed": failed}, indent=2))
assert rate >= BASELINE["pass_rate"] - MAX_DROP, (
f"pass rate {rate:.1%} < baseline {BASELINE['pass_rate']:.1%} - {MAX_DROP:.0%}; fail: {failed}")
Nối vào GitHub Actions — chỉ chạy khi file ảnh hưởng hành vi agent đổi, pin model qua biến môi trường, đăng báo cáo lên PR (YAML tự soạn):
# .github/workflows/eval-gate.yml
name: eval-gate
on:
pull_request:
paths: ["prompts/**", "agent/**", "tools/**", "evals/**", "models.lock.yaml"]
jobs:
golden:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
AGENT_MODEL: ${{ vars.AGENT_MODEL_PINNED }} # vd "provider-model-2026-06-01", KHÔNG dùng alias
JUDGE_MODEL: ${{ vars.JUDGE_MODEL_PINNED }}
EVAL_REPEATS: "3"
EVAL_MAX_DROP: "0.03"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements.txt
- name: Leakage check (golden vs prompt)
run: python evals/check_leakage.py --golden evals/golden/golden.jsonl --prompts prompts/
- name: Verify pinned versions
run: python evals/check_pins.py models.lock.yaml
- name: Run golden set
run: pytest evals/ -q --junitxml=eval-junit.xml
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with: { name: eval-report, path: "eval-report.json" }
Nhầm lẫn thường gặp · "Cứ bấm re-run tới khi xanh"
Re-run thủ công là chạy lặp có chọn lọc: bạn dừng khi thấy xanh, nên xác suất lọt regression tăng vọt (giống p-hacking). Nếu cần chạy lặp, hãy để runner chạy cố định k lần và áp một luật đã khai báo trước (đa số, hoặc ≥ m/k), không để người quyết định khi nào dừng.Mô phỏng nhiều lần chạy CI cho hai kịch bản: bản không regression (pass thật = baseline) và bản có regression (pass thật = bản mới). Mỗi case có xác suất "lật" kết quả mỗi lần chạy. Xem cổng chặn nhầm bao nhiêu và lọt bao nhiêu.
Golden set 120 case, baseline pass thật 90%, flake 3%/case/lần. Team chấp nhận chặn nhầm tối đa 5% và muốn bắt được regression làm hỏng ≥ 6 điểm % (còn 84%) với xác suất ≥ 95%. Dùng Lab 1 thử: k=1 với ngưỡng nào? Có cần k=3 không?
Xem lời giải
Với k=1, trung bình quan sát của baseline ≈ 108 − 3%·108 + 3%·12 ≈ 105 case (87,6%), độ lệch chuẩn ≈ 1,9 case (1,6 điểm %); bản regression (101 case đúng thật) quan sát ≈ 98,5 case (82,1%). Tính chính xác (minh hoạ): ngưỡng ≥ 85% (102/120) cho chặn nhầm ≈ 3,5% và lọt ≈ 4,1% — vừa đủ đạt yêu cầu, không còn biên an toàn; ngưỡng 84% lọt 14%; ngưỡng 86% chặn nhầm 18%. Với k=3, flake hiệu dụng ≈ 0,26%, hai phân phối tách hẳn: ngưỡng 86–87% cho cả chặn nhầm lẫn lọt ≈ 0%. Kết luận: k=1 dùng được nhưng mong manh (chỉ cần flake tăng lên 4% là vỡ), k=3 hoặc chuyển thêm check sang code tất định cho biên an toàn. Bài học chính: ngưỡng phải đặt theo phân phối quan sát của baseline, không theo pass rate "thật".
9.4Ba lỗi thường gặp với CI
CI giống smoke test: fail thì chắc chắn có chuyện; pass chỉ nghĩa là chưa phá thứ đã biết — không nói gì về hàng nghìn query thật.
Ví dụ golden bị chép vào few-shot của agent hoặc prompt của judge → điểm đẹp nhưng vô nghĩa. Sách gọi đây là cách phổ biến nhất khiến team vô tình làm hỏng CI.
Mỗi ví dụ phải đại diện một hành vi cụ thể cần giữ. Thừa → tốn bảo trì, chậm debug khi fail.
- Golden case
gs-022(hoàn vé trước 20 tiếng → 70%) fail ở bản prompt v12. - Dev sửa nhanh: thêm vào system prompt một few-shot "Khách: huỷ trước 20 tiếng được hoàn bao nhiêu? → Trợ lý: 70%…". CI xanh, pass rate 94% → 96%.
- Thực tế agent vẫn không hiểu bảng chính sách: hỏi "huỷ trước 30 tiếng" vẫn sai. Case gs-022 giờ chỉ kiểm tra khả năng chép.
- Cách phát hiện: script so n-gram giữa input golden và nội dung prompt chạy mỗi commit → báo trùng 14/15 từ → CI đỏ ở bước "leakage check" trước cả khi chạy agent.
- Cách sửa đúng: few-shot dùng ví dụ khác (vd "huỷ trước 5 tiếng"), hoặc tốt hơn — cho agent tool tra bảng chính sách; gs-022 giữ nguyên làm bài kiểm tra tổng quát hoá.
Script kiểm tra leakage đơn giản bằng n-gram (tự soạn, ~30 dòng):
# evals/check_leakage.py
import argparse, json, pathlib, re, sys
def ngrams(text: str, n: int = 6) -> set[tuple[str, ...]]:
toks = re.findall(r"\w+", text.lower())
return {tuple(toks[i:i + n]) for i in range(len(toks) - n + 1)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--golden", required=True)
ap.add_argument("--prompts", required=True) # thư mục chứa prompt + few-shot + prompt của judge
ap.add_argument("--n", type=int, default=6)
ap.add_argument("--max-overlap", type=float, default=0.5)
a = ap.parse_args()
prompt_grams = set()
for p in pathlib.Path(a.prompts).rglob("*"):
if p.is_file() and p.suffix in {".txt", ".md", ".j2", ".yaml", ".json"}:
prompt_grams |= ngrams(p.read_text(errors="ignore"), a.n)
leaks = []
for line in pathlib.Path(a.golden).read_text().splitlines():
if not line.strip():
continue
case = json.loads(line)
g = ngrams(case["input"], a.n)
if g and len(g & prompt_grams) / len(g) >= a.max_overlap:
leaks.append(case["id"])
if leaks:
print("LEAKAGE: golden cases xuất hiện trong prompt:", leaks)
sys.exit(1)
print("no leakage")
if __name__ == "__main__":
main()
Nhầm lẫn thường gặp · "Golden set lấy từ production thì cũng là leakage?"
Không. Leakage là khi chính case kiểm tra nằm trong những gì model được thấy lúc trả lời (prompt, few-shot, prompt của judge). Lấy input thật từ production để làm test là điều sách khuyến khích. Cũng lưu ý leakage phía judge: nếu few-shot của judge chứa golden case kèm nhãn, judge sẽ chấm "thuộc bài" trên đúng những case đó.(a) CI xanh 100% suốt 4 tháng, nhưng CSAT giảm dần. (b) Golden set có 640 case, CI chạy 50 phút, mỗi lần đỏ mất nửa ngày tìm nguyên nhân. (c) Judge "tone" được viết lại, prompt của judge thêm 6 ví dụ lấy từ file golden để "calibrate tốt hơn". Mỗi tình huống là lỗi nào trong ba lỗi? Làm gì?
Xem lời giải
(a) Lỗi ① (và golden set đóng băng): CI chỉ bảo vệ thứ đã biết. Việc cần làm: đọc trace production ngẫu nhiên, error analysis, bổ sung failure mode mới vào golden set + evaluator online. (b) Lỗi ③: gộp case trùng hành vi, cho nghỉ hưu case chết, tách tầng critical chạy mỗi PR và bộ đầy đủ chạy hằng đêm. (c) Lỗi ②, leakage phía judge: dùng ví dụ calibration từ một tập dev riêng, không lấy từ golden set; chạy check_leakage trên cả thư mục prompt của judge.
9.5Pin version & xem lại prompt khi model mạnh lên
Hai ý của sách gắn với nhau: (1) luôn pin đúng version có ngày của model, cho cả agent lẫn judge; (2) khi có version mới, đừng chỉ "thả vào chạy CI" mà hãy xem lại prompt. Prompt hay tích tụ các chỉ dẫn "vá" cho điểm yếu của model cũ (bắt buộc số câu, câu mở, câu kết…). Model mới có thể tự làm tốt → gỡ bớt giúp prompt rẻ hơn (ít token), nhanh hơn, đôi khi chính xác hơn vì model được dùng phán đoán của nó thay vì bám ràng buộc lỗi thời.
Sách kể trường hợp của Voiceflow (cuối 2023): một khách hàng dùng agent phân loại intent để định tuyến yêu cầu. Khi thay bản GPT-3.5 mới hơn vào, accuracy trên dữ liệu khách hàng giảm khoảng 10 điểm % với cùng prompt. Họ phải viết lại vài phần prompt (đổi cách gọi tên nhãn, thêm một ví dụ mẫu, thêm câu nhấn mạnh tầm quan trọng) mới lấy lại accuracy cũ. Không có regression test trên dữ liệu thật thì bản kém đã ra production.
Tháng 10/2024, model phía sau một alias không đánh số bị thay; function call bắt đầu trả về text thường thay vì lời gọi có cấu trúc. Team đã pin version không hề hấn gì; team dùng alias phải chẩn đoán từ khiếu nại khách hàng.
- Pin version ở cả agent và judge — nếu cả hai cùng trôi, gần như không thể chẩn đoán regression.
- Đặt tuổi tối đa cho một version (sách gợi ý kiểu ~6 tháng sau khi có bản mới); nâng cấp đi qua cùng cổng CI như mọi thay đổi.
- Provider đôi khi đổi hành vi ngay cả sau cùng một chuỗi version. Metric CI đổi mà phía bạn không đổi gì → nghi API behavioral drift; chạy lại golden set trên version đã pin để xác nhận.
Prompt hiện tại (v9, 610 token) của một bộ tóm tắt review có 7 chỉ dẫn định dạng tích tụ qua năm. Có version model mới. Kết quả (minh hoạ):
| Cấu hình | Golden pass (k=3) | Token prompt | Latency p50 | Ghi chú |
|---|---|---|---|---|
| Model cũ + prompt v9 | 44/50 | 610 | 2,1 s | baseline |
| Model mới + prompt v9 | 41/50 | 610 | 1,6 s | 3 case fail |
| Model mới + v10 (gỡ 4 chỉ dẫn) | 45/50 | 380 | 1,4 s | giữ bản này |
- Chạy CI với prompt cũ: 3 case fail. Đọc diff: 2 case fail vì model mới viết "Ưu điểm:" thay vì gạch đầu dòng "+" — regex của evaluator bắt ký tự "+" → đổi format, không phải regression. 1 case fail thật: bỏ sót nhược điểm "pin nhanh hết".
- Sửa evaluator cho 2 case format: chấm "có tách ưu/nhược rõ ràng" bằng judge đã kiểm định thay vì regex ký tự. Ghi vào changelog của evaluator (vì đổi thước đo phải minh bạch).
- Thử gỡ chỉ dẫn: bỏ "đúng 4–5 câu", "mở đầu bằng tên sản phẩm", "kết bằng khuyến nghị", "không dùng từ 'tuyệt vời'". Pass tăng lên 45/50 — model mới tự cân đối tốt hơn khi bớt ràng buộc cứng; case nhược điểm bị sót giờ pass.
- Error analysis lại trên 100 output mới: phát hiện lỗi mới — model mới hay thêm câu "Nhìn chung đây là lựa chọn đáng cân nhắc" dù review tiêu cực. Thêm failure mode "kết luận lạc quan giả", 4 golden case + judge mới.
Manifest pin version và script kiểm tra tuổi version (tự soạn):
# models.lock.yaml — mọi model mà hệ thống gọi, kể cả judge
agent:
primary: { id: "provider-a/chat-large-2026-03-14", pinned_on: "2026-03-20", newer_available_since: "2026-07-02" }
fallback: { id: "provider-b/chat-small-2026-05-01", pinned_on: "2026-05-10", newer_available_since: null }
judges:
tone: { id: "provider-a/chat-large-2026-03-14", validated_on: "2026-08-28", tpr: 0.93, tnr: 0.88 }
policy: { id: "provider-a/chat-large-2026-03-14", validated_on: "2026-08-28", tpr: 0.95, tnr: 0.86 }
policy:
max_age_after_newer_days: 180 # quá hạn → CI cảnh báo, phải lên kế hoạch nâng cấp
judge_revalidate_every_days: 28
# evals/check_pins.py
import re, sys, datetime as dt, yaml
ALIAS = re.compile(r"(latest|preview)$|^[^0-9]*$") # id không có ngày = alias
cfg, today, bad = yaml.safe_load(open(sys.argv[1])), dt.date.today(), []
for group in ("agent", "judges"):
for name, m in cfg[group].items():
if ALIAS.search(m["id"]):
bad.append(f"{group}.{name}: '{m['id']}' là alias, phải pin version có ngày")
newer = m.get("newer_available_since")
if newer and (today - dt.date.fromisoformat(newer)).days > cfg["policy"]["max_age_after_newer_days"]:
print(f"WARN {group}.{name}: đã có bản mới hơn {newer}, quá hạn nâng cấp")
v = m.get("validated_on")
if v and (today - dt.date.fromisoformat(v)).days > cfg["policy"]["judge_revalidate_every_days"]:
print(f"WARN judges.{name}: cần tái kiểm định (lần cuối {v})")
sys.exit("\n".join(bad) if bad else 0)
Nhầm lẫn thường gặp · "Model mới điểm benchmark cao hơn thì prompt của mình chắc cũng tốt hơn"
Benchmark công khai đo phân phối task của họ. Prompt của bạn đã được tinh chỉnh cho quirk của model cũ, và evaluator của bạn có thể nhạy với format cũ. Sự cố Voiceflow là minh chứng: "mới hơn" không có nghĩa "drop-in". Luôn coi nâng cấp là một PR có CI, error analysis và (nên có) canary.Thứ Hai CI hằng đêm của nhánh main đỏ: pass rate 91% → 83%, không ai merge gì từ thứ Sáu. Agent và judge đều pin version. (a) Liệt kê 3 giả thuyết theo thứ tự nên kiểm tra. (b) Thí nghiệm nào tách được "agent đổi" với "judge đổi"?
Xem lời giải
(a) ① API behavioral drift sau cùng chuỗi version (sách nêu khả năng này) — chạy lại golden set vài lần xem có tái lập; ② phụ thuộc ngoài đổi: dữ liệu tool/DB/tài liệu RAG mà golden case tra cứu đã cập nhật (vd bảng giá mới) → reference cũ lỗi thời; ③ hạ tầng: timeout, rate limit khiến output bị cắt — xem log lỗi trước khi xem nội dung. (b) Dùng output đã lưu từ lần chạy xanh gần nhất: chấm lại chúng bằng judge hôm nay. Nếu output cũ giờ cũng bị chấm thấp → judge đổi. Nếu judge chấm output cũ như cũ, còn output mới thấp → agent (hoặc dữ liệu tool) đổi. Vì thế nên lưu output + điểm của mỗi lần CI làm artifact.
9.6CD bước 1: observability — log gì, đọc thế nào
CI lo những gì đã biết; CD theo dõi production — nơi người dùng hỏi những thứ không có trong golden set. Mọi thứ bắt đầu từ log. Với mỗi request (hoặc một mẫu đại diện), sách liệt kê cần ghi: input + metadata (user/session id, cờ tính năng), mọi LLM call trung gian (prompt, response, suy luận trung gian), mọi tool call (tên, tham số, kết quả, lỗi), tài liệu được retrieve, output cuối, feedback người dùng (thumb, chỉnh sửa, câu hỏi tiếp) và kết quả evaluator nếu có.
Bản ghi đầy đủ cho một request của người dùng.
Một bước trong trace: LLM call, tool call, truy vấn DB.
Nhóm các trace liên quan trong một hội thoại nhiều lượt.
Công cụ sách nhắc: LangSmith, Arize Phoenix, W&B Weave, hoặc tích hợp OpenTelemetry. Khuyến nghị: xem một mẫu trace production mỗi ngày — vừa luyện cảm giác chất lượng, vừa thấy pattern mới sớm.
Một trace tối thiểu nhưng đủ để tái dựng tương tác (JSON tự soạn):
{
"trace_id": "tr_9c41", "session_id": "ss_22a0", "ts": "2026-09-21T08:14:03Z",
"input": {"text": "đổi vé 7h mai sang chuyến 9h được không", "channel": "zalo", "flags": {"promo_banner": true}},
"versions": {"prompt": "v12", "agent_model": "provider-a/chat-large-2026-03-14", "router_policy": "r-2026-09-10"},
"spans": [
{"type": "llm", "name": "plan", "latency_ms": 820, "tokens_in": 1840, "tokens_out": 96},
{"type": "tool", "name": "search_trips", "args": {"route": "HN-HP", "date": "2026-09-22", "after": "09:00"},
"latency_ms": 140, "status": "ok", "result_count": 3},
{"type": "tool", "name": "change_ticket", "args": {"ticket_id": "T-5521", "new_trip": "HP0900"},
"status": "error", "error": "fee_confirmation_required"},
{"type": "llm", "name": "respond", "latency_ms": 1210, "tokens_out": 142}
],
"output": "Chuyến 9h còn 4 ghế. Đổi vé mất phí 20.000đ, bạn xác nhận nhé?",
"guardrails": [{"name": "pii", "tripped": false}, {"name": "price_format", "tripped": false}],
"feedback": {"thumb": null, "followup_within_60s": true},
"evals": {"sampled": true, "policy_faithful": "pass", "tone": "pass"}
}
- Lấy mẫu có chủ đích: 10 trace ngẫu nhiên hoàn toàn + 5 trace bị evaluator flag + 5 trace có tín hiệu người dùng xấu (hỏi lại trong 60 giây, thumb down, chuyển sang người thật). Mẫu ngẫu nhiên là bắt buộc — sách nhấn mạnh chỉ xem trace bị flag sẽ bỏ sót lỗi mà evaluator chưa biết.
- Ghi chú tự do (open coding, Ch.3) cho từng trace; không cố gán vào failure mode sẵn có.
- Cuối tuần gom ghi chú: nếu một kiểu ghi chú mới xuất hiện ≥ 3 lần trong tuần → ứng viên failure mode mới → đưa vào vòng error analysis đầy đủ.
- Ví dụ phát hiện: 3 trace trong tuần có span
change_ticketlỗifee_confirmation_requirednhưng output vẫn nói "đã đổi xong" — evaluator "policy_faithful" không bắt vì không đọc span tool. Failure mode mới: báo thành công khi tool lỗi → evaluator code: nếu span tool cuối có status error thì output không được chứa cụm xác nhận thành công.
Nhầm lẫn thường gặp · "Log output cuối là đủ"
Nhiều lỗi agent chỉ nhìn thấy ở span giữa: tool lỗi bị che, tham số sai nhưng may mắn ra kết quả đúng, retrieve sai tài liệu. Ví dụ 9.6 không thể phát hiện nếu chỉ log input/output. Đồng thời ghi version (prompt, model, router policy) vào mỗi trace — thiếu nó thì không thể so trước/sau khi deploy.Một team log mỗi request gồm: timestamp, user_id, input, output, latency_ms. Họ muốn (a) biết lỗi tăng sau deploy thứ Ba có phải do prompt mới; (b) phát hiện agent gọi tool đặt lịch với ngày trong quá khứ; (c) biết người dùng có hài lòng. Bổ sung tối thiểu trường nào?
Xem lời giải
(a) versions (prompt id, model id đã pin, commit) trên mỗi trace. (b) spans tool với tên + tham số + status — ngày quá khứ nằm trong tham số, không trong output. (c) feedback (thumb, chỉnh sửa, follow-up, chuyển người thật) và session_id để nối các lượt — hài lòng thường thể hiện qua hành vi lượt sau. Thêm evals để lưu điểm evaluator khi trace được lấy mẫu.
9.7CD bước 2: evaluator trên production, hiệu chỉnh & alert
Có log rồi, ta chạy evaluator tự động (Ch.5) trên mẫu trace. Cấu hình điển hình theo sách: 5–7 evaluator, trộn code-based và LLM-as-judge, mỗi cái theo một failure mode đã biết. Judge chậm và đắt nên thường chấm 1–5% traffic, bất đồng bộ sau khi request xong. Với mỗi failure mode, tính success rate đã hiệu chỉnh theo TPR/TNR đã đo của judge cùng khoảng tin cậy 95% bằng bootstrap, đưa lên dashboard và alert khi dải tin cậy vượt ngưỡng.
Evaluator "policy_faithful" có TPR = 0,95 (judge nói pass khi output thật sự pass) và TNR = 0,85 (judge nói fail khi output thật sự fail), đo lúc kiểm định (Ch.5). Hôm nay chấm 200 trace (1% của 20.000 request), judge nói pass 176 trace (minh hoạ).
- Tỷ lệ thô: pobs = 176/200 = 0,88.
- Quan hệ giữa thô và thật: pobs = θ·TPR + (1−θ)·(1−TNR), trong đó θ là success rate thật.
- Giải ra θ: θ = (pobs + TNR − 1) / (TPR + TNR − 1) = (0,88 + 0,85 − 1) / (0,95 + 0,85 − 1) = 0,73 / 0,80 ≈ 0,913. Con số thật (≈ 91,3%) cao hơn tỷ lệ thô (88%): judge đánh fail nhầm 5% output tốt (≈ 4,6% tổng số), và dù nó cũng cho pass nhầm 15% output hỏng, output hỏng chỉ chiếm ~9% nên hiệu ứng này nhỏ hơn (≈ 1,3%).
- Độ bất định: với 200 mẫu, sai số chuẩn của pobs ≈ √(0,88·0,12/200) ≈ 0,023; chia cho mẫu số 0,80 → ≈ 0,029. Dải 95% xấp xỉ 91,3% ± 5,6 điểm. Bootstrap (resample cả 200 trace và tập nhãn dùng để đo TPR/TNR) cho dải rộng hơn chút vì TPR/TNR cũng là ước lượng.
- Hệ quả cho alert: dải ±5,6 điểm/ngày là quá rộng để bắt sụt 3 điểm. Gộp cửa sổ trượt 7 ngày (1.400 mẫu) thu hẹp còn ≈ ±2,1 điểm — đổi lại chậm phát hiện hơn vài ngày. Lab 2 cho bạn thử đánh đổi này.
Tính rate hiệu chỉnh + bootstrap (tự soạn, numpy):
import numpy as np
rng = np.random.default_rng(0)
def corrected(p_obs, tpr, tnr):
"""Hiệu chỉnh tỷ lệ pass thô theo độ chính xác đã biết của judge (kẹp vào [0,1])."""
return float(np.clip((p_obs + tnr - 1) / (tpr + tnr - 1), 0, 1))
def bootstrap_ci(judge_prod, human_lab, judge_lab, B=2000, alpha=0.05):
"""judge_prod: 0/1 judge chấm trên trace production (không có nhãn người).
human_lab, judge_lab: 0/1 trên tập kiểm định có nhãn người (dùng để ước TPR/TNR)."""
judge_prod, human_lab, judge_lab = map(np.asarray, (judge_prod, human_lab, judge_lab))
stats = []
for _ in range(B):
jp = rng.choice(judge_prod, size=len(judge_prod)) # resample production
idx = rng.integers(0, len(human_lab), len(human_lab)) # resample tập kiểm định
h, j = human_lab[idx], judge_lab[idx]
tpr = (j[h == 1] == 1).mean() if (h == 1).any() else np.nan
tnr = (j[h == 0] == 0).mean() if (h == 0).any() else np.nan
if np.isnan(tpr) or np.isnan(tnr) or tpr + tnr - 1 <= 0.05:
continue # judge gần như ngẫu nhiên: bỏ mẫu
stats.append(corrected(jp.mean(), tpr, tnr))
lo, hi = np.quantile(stats, [alpha / 2, 1 - alpha / 2])
return float(np.median(stats)), float(lo), float(hi)
def should_alert(ci_hi, threshold=0.90):
return ci_hi < threshold # chỉ báo khi CẢ dải tin cậy nằm dưới ngưỡng (ít báo nhầm)
Đọc dashboard: alert khi khoảng tin cậy chạm ngưỡng
Nhầm lẫn thường gặp · "Alert khi điểm trung bình dưới ngưỡng"
Với mẫu nhỏ, điểm trung bình dao động vài điểm % mỗi ngày → alert kiểu này kêu liên tục và bị tắt tiếng. Có ba mức nhạy: (1) trung bình < ngưỡng — nhạy, nhiều báo nhầm; (2) cận trên của dải < ngưỡng — chắc chắn đã tụt, ít báo nhầm nhưng chậm; (3) cận dưới < ngưỡng — cảnh báo sớm ("có thể đang tụt") dùng cho kênh vàng, không đánh thức ai lúc 3 giờ sáng. Chọn mức theo mức độ nghiêm trọng của failure mode.Mô phỏng 60 ngày. Mỗi ngày lấy mẫu một phần traffic, judge (TPR/TNR không hoàn hảo) chấm, dashboard hiệu chỉnh và tính dải 95%. Từ ngày drift, success rate thật tụt. Xem alert đến sớm/muộn thế nào và báo nhầm bao nhiêu lần trước drift.
Failure mode "hoàn tiền sai" có success rate thật 96%; team muốn phát hiện khi nó tụt xuống 92% trong vòng 3 ngày. Judge TPR 0,94, TNR 0,90. Traffic 8.000 request/ngày. Ước lượng tỷ lệ lấy mẫu tối thiểu nếu dùng cửa sổ 3 ngày và luật "cận trên < 94%".
Xem lời giải
Muốn khi θ = 92% thì cận trên dải 95% nằm dưới 94%, tức nửa độ rộng ≲ 2 điểm. Nửa độ rộng ≈ 1,96·√(pobs(1−pobs)/n) / (TPR+TNR−1). Ở θ = 0,92: pobs = 0,92·0,94 + 0,08·0,10 ≈ 0,873; mẫu số 0,84. Cần 1,96·√(0,873·0,127/n)/0,84 ≤ 0,02 → √(0,111/n) ≤ 0,00857 → n ≥ ~1.510 mẫu trong cửa sổ 3 ngày → ~505/ngày → ≈ 6,3% traffic. Chưa tính bất định của TPR/TNR (bootstrap sẽ đòi thêm). Kết luận thực tế: 1% không đủ cho failure mode hiếm/nhạy này; hoặc lấy mẫu phân tầng (chấm 100% trace có intent "hoàn tiền", 1% phần còn lại) — một kỹ thuật hữu ích khi failure mode tập trung ở một intent.
9.8Guardrails: kiểm tra ngay trên đường trả lời
Phần lớn evaluator chạy sau sự việc. Guardrail thì khác: nó chạy trong lúc thực thi, trước khi output tới người dùng. Vì nằm trên critical path nên phải nhanh và có tỷ lệ báo nhầm rất thấp — điều này gần như loại LLM judge (vài giây mỗi lần gọi, người dùng sẽ cảm nhận). Thứ hợp: check tất định vài mili-giây như regex, JSON schema, blocklist/allowlist, kiểm độ dài/format, parser nhẹ (parse SQL, parse AST Python). Về bản chất, guardrail chỉ là một hàm được gọi trên mọi output: kiểm nhanh, log khi nó kích hoạt, và có phản ứng định sẵn.
Kiểm tham số tool, context trung gian khi agent đang làm việc (vd ngày đặt lịch không ở quá khứ, số tiền hoàn ≤ giá vé).
Kiểm câu trả lời cuối: PII, schema, độc hại/an toàn. Ở phía input: phát hiện prompt injection (Ch.8).
Bỏ output, trả một thông báo an toàn định sẵn. Ví dụ của sách Email nháp có chuỗi giống số an sinh xã hội → chặn, nhờ người dùng kiểm tra dữ liệu nguồn.
Gọi LLM lại. LLM không tất định nên lần sau thường "sạch". Ví dụ của sách SQL sinh ra không parse được → thử lại 1 lần; vẫn lỗi → nhờ người dùng diễn đạt lại.
Chuyển sang model hoặc logic đơn giản hơn để vẫn phục vụ được, dù kém "thông minh" (vd template trả lời cố định, model khác, hoặc chuyển người thật).
Thứ tự ưu tiên: không thương lượng trước, sở thích sau
| Guardrail | Loại | Ngưỡng | Phản ứng khi trip |
|---|---|---|---|
| PII (số thẻ, CCCD, số điện thoại người khác) trong output | An toàn — không thương lượng | Không bao giờ nới | Reject + log |
| Tham số tool sai schema / số tiền hoàn > giá vé | Đúng đắn — không thương lượng | Không bao giờ nới | Retry 1 lần → Reject |
| SQL/JSON không parse được | Đúng đắn | — | Retry 1 lần → Fallback |
| Câu trả lời > 250 từ trên kênh chat mobile | Sở thích chất lượng | Có thể chỉnh dần | Chỉ log (không chặn) lúc đầu |
Wrapper guardrail có chính sách cấu hình được (tự soạn — khác ví dụ trong sách, dùng schema + PII kiểu Việt Nam):
import json, re, time
from dataclasses import dataclass
from typing import Callable, Optional
PHONE_VN = re.compile(r"(?<!\d)(?:\+?84|0)(?:3|5|7|8|9)\d{8}(?!\d)") # SĐT di động VN
CCCD = re.compile(r"(?<!\d)\d{12}(?!\d)") # căn cước 12 số
@dataclass
class Verdict:
ok: bool
rule: Optional[str] = None
severity: str = "none" # "block" (không thương lượng) | "retry" | "log"
def check(output: str, allowed_phones: set[str]) -> Verdict:
for m in PHONE_VN.findall(output):
if m not in allowed_phones: # số hotline công ty thì cho phép
return Verdict(False, "pii_phone", "block")
if CCCD.search(output):
return Verdict(False, "pii_cccd", "block")
try:
data = json.loads(output)
except ValueError:
return Verdict(False, "not_json", "retry")
if not {"answer", "citations"} <= data.keys():
return Verdict(False, "schema_missing_keys", "retry")
if len(data["answer"].split()) > 250:
return Verdict(False, "too_long", "log") # sở thích: chỉ log
return Verdict(True)
def guarded(call: Callable[[str], str], fallback: Callable[[str], str], log: Callable[..., None],
req: str, allowed_phones: set[str], max_retry: int = 1) -> str:
for attempt in range(max_retry + 1):
t0 = time.perf_counter()
out = call(req)
v = check(out, allowed_phones)
log(rule=v.rule, attempt=attempt, check_ms=(time.perf_counter() - t0) * 1000, ok=v.ok)
if v.ok or v.severity == "log":
return out
if v.severity == "block":
return json.dumps({"answer": "Xin lỗi, câu trả lời chứa thông tin nhạy cảm nên đã bị chặn. "
"Mình chuyển bạn tới nhân viên hỗ trợ.", "citations": []}, ensure_ascii=False)
return fallback(req) # retry hết lượt → đường dự phòng
Team thêm guardrail PII chặn mọi chuỗi 10–11 chữ số (minh hoạ). Chạy thử trên 5.000 trace production hợp lệ trước khi bật, như sách khuyên:
- Nó trip trên 180/5.000 trace hợp lệ = 3,6% báo nhầm: mã đơn hàng 10 số, số tài khoản ngân hàng của chính shop để khách chuyển khoản, hotline.
- Nếu bật: 100.000 × 3,6% = 3.600 người/ngày thấy câu "nội dung đã bị chặn" dù không có gì sai — họ sẽ trải nghiệm như tính năng bị hỏng. Sách nói thẳng: guardrail báo nhầm quá nhiều còn tệ hơn không có.
- Sửa: regex SĐT theo đầu số di động, allowlist số của shop, loại chuỗi có tiền tố "ĐH" (mã đơn). Chạy lại: 31/5.000 = 0,62% — dưới ngưỡng 1% của sách, nhưng vẫn là 620 người/ngày.
- Kiểm tra độ nhạy: trộn 200 output có chèn PII thật (tổng hợp) → bắt 197/200 = 98,5%. 3 case lọt là SĐT viết cách "09 1234 5678" → thêm biến thể có khoảng trắng, chạy lại cả hai phép đo.
- Bật ở chế độ log-only 3 ngày trên production, xác nhận tỷ lệ trip thật ≈ tỷ lệ đo offline, rồi mới chuyển sang Reject.
Nhầm lẫn thường gặp · "Retry luôn cứu được"
Retry hiệu quả khi lỗi là ngẫu nhiên (lần này model lỡ tay sinh JSON thiếu ngoặc). Khi lỗi là hệ thống — input khiến model luôn sai (câu hỏi mơ hồ, tài liệu thiếu) — retry chỉ nhân đôi latency và chi phí rồi vẫn fail. Đo "tỷ lệ retry thành công" riêng cho từng rule; rule nào retry-thành-công < 30% thì nên chuyển sang Fallback/Reject thẳng. Lab 3 cho thấy tham số "tương quan lỗi" thay đổi bức tranh thế nào.Mô phỏng 20.000 request đi qua một guardrail. So sánh các chính sách khi trip về: tỷ lệ người dùng nhận output lỗi, bị từ chối, latency p50/p95 và chi phí tương đối.
LLM chính 1,8 s; check guardrail 5 ms; 6% output trip (giả sử đều là lỗi thật và retry độc lập). Chính sách "Retry 1 lần → Fallback (0,6 s)". (a) Latency trung bình? (b) Tỷ lệ request đi tới fallback? (c) Nếu lỗi tương quan mạnh (retry chỉ thành công 40%), (b) thành bao nhiêu và p95 latency thay đổi ra sao?
Xem lời giải
(a) 1,805 + 0,06 × 1,805 + 0,06 × 0,06 × 0,6 ≈ 1,805 + 0,108 + 0,002 ≈ 1,915 s. (b) 0,06 × 0,06 = 0,36%. (c) Retry thất bại 60% → 0,06 × 0,6 = 3,6% đi fallback (gấp 10 lần). p95: vì 6% request phải chạy lại (≥ 5% tail), p95 rơi vào nhóm retry ≈ 1,8 + 1,8 = ~3,6 s trong cả hai trường hợp; với tương quan mạnh, nhóm 3,6% đi tiếp fallback có latency ~4,2 s nhưng vẫn nhỏ hơn 5% nên chủ yếu đẩy p99. Bài học: tỷ lệ trip ≥ 5% là đủ để retry kéo p95 lên gấp đôi — cần theo dõi p95 theo từng rule.
9.9Judge drift: khi chính "giám khảo" trôi
TPR/TNR bạn đo lúc kiểm định judge (Ch.5) không đứng yên. Sách nêu hai nguồn trôi, và nhấn mạnh rằng nếu không chủ động kiểm tra thì độ chính xác của judge sẽ xuống cấp mà không ai nhận ra:
Judge chạy trên alias → provider cập nhật là điểm số đổi qua đêm, dù bạn không đụng gì.
Criteria drift (Ch.4): càng xem data, team càng hiểu "đúng" khác đi; judge vẫn chấm theo chuẩn cũ.
- Pin version judge ở cả CI và production (id có ngày, không alias).
- Tái kiểm định định kỳ (vài tuần/lần): lấy trace mới, gán nhãn người, tính lại TPR/TNR; tụt dưới ngưỡng → sửa prompt/few-shot của judge, kiểm định lại rồi mới deploy.
- Đổi LLM của judge → chạy lại toàn bộ quy trình kiểm định.
Dashboard báo "policy_faithful" tụt từ 90% xuống 82% trong một tuần. Agent không đổi. Thật ra success rate thật vẫn 90% — chỉ có judge đổi (minh hoạ).
- Lúc kiểm định: TPR = 0,95, TNR = 0,85. Dashboard dùng hai số này để hiệu chỉnh.
- Tiêu chí team đã đổi: từ tháng trước, "trả lời đúng chính sách nhưng không nêu thời hạn" bị coi là fail; judge mới được sửa few-shot vội (không kiểm định lại) và giờ khắt khe hơn — TPR thật chỉ còn 0,88.
- Tỷ lệ thô: pobs = 0,90·0,88 + 0,10·0,15 = 0,807.
- Dashboard hiệu chỉnh bằng TPR cũ: (0,807 + 0,85 − 1)/(0,95 + 0,85 − 1) = 0,657/0,80 ≈ 82% → báo động giả, team mất 2 ngày đi tìm "regression".
- Nếu đã tái kiểm định (TPR = 0,88): 0,657/0,73 = 90% — đúng sự thật. Bài học: mọi thay đổi judge = một phiên bản judge mới, phải kiểm định lại và cập nhật TPR/TNR dùng trong công thức hiệu chỉnh.
Script tái kiểm định định kỳ (tự soạn):
# evals/revalidate_judge.py — chạy mỗi 4 tuần (cron) trên trace mới đã được người gán nhãn
import json, sys
from sklearn.metrics import confusion_matrix
MIN_TPR, MIN_TNR = 0.85, 0.80
def revalidate(labels_path: str, judge_fn, judge_id: str):
rows = [json.loads(l) for l in open(labels_path) if l.strip()] # {"trace": ..., "human": 0/1}
human = [r["human"] for r in rows]
judge = [judge_fn(r["trace"]) for r in rows] # judge_fn dùng đúng judge_id đã pin
tn, fp, fn, tp = confusion_matrix(human, judge, labels=[0, 1]).ravel()
tpr, tnr = tp / (tp + fn), tn / (tn + fp)
report = {"judge": judge_id, "n": len(rows), "tpr": round(tpr, 3), "tnr": round(tnr, 3),
"ok": tpr >= MIN_TPR and tnr >= MIN_TNR}
print(json.dumps(report, ensure_ascii=False))
if not report["ok"]:
sys.exit("Judge dưới ngưỡng: sửa prompt/few-shot, kiểm định lại trước khi dùng tiếp")
return report # ghi tpr/tnr mới vào models.lock.yaml để dashboard hiệu chỉnh đúng
Nhầm lẫn thường gặp · "Pin version rồi thì judge không drift"
Pin chặn được nguồn trôi thứ nhất (model đổi), không chặn được nguồn thứ hai (tiêu chí của team đổi). Judge đã pin vẫn "đúng" với chuẩn tháng trước. Đó là lý do cần cả hai biện pháp: pin và tái kiểm định với nhãn mới.Bạn có 6 judge trong production. Mỗi lần kiểm định cần ~150 trace gán nhãn người, mỗi trace mất ~1 phút. Team chỉ có 3 giờ gán nhãn/tuần. Lập lịch sao cho mỗi judge được kiểm định ít nhất 4 tuần/lần và ưu tiên hợp lý.
Xem lời giải
Mỗi lần kiểm định ≈ 150 phút = 2,5 giờ → mỗi tuần chỉ làm được ~1 judge (3 giờ). 6 judge × 4 tuần = cần 1,5 lần/tuần → không đủ. Cách xử lý: (1) giảm cỡ mẫu còn ~100 trace cho judge có lịch sử ổn định (TPR/TNR biến động < 2 điểm qua 3 lần) → 1,7 giờ; (2) dùng chung nhãn: một trace gán nhãn theo nhiều tiêu chí cùng lúc cho nhiều judge; (3) ưu tiên judge gắn với failure mode critical (chính sách, an toàn) mỗi 2–4 tuần, judge "sở thích" (tone) mỗi 6–8 tuần; (4) kích hoạt kiểm định ngoài lịch khi criteria đổi hoặc judge prompt đổi.
9.10Mở rộng: triển khai an toàn — shadow, canary, rollback
Sách tập trung vào CI (trước deploy) và monitoring (sau deploy). Giữa hai thứ đó, thực hành DevOps phổ biến là triển khai từng bậc — phần này là mở rộng của sổ tay, dùng đúng các công cụ của chương (evaluator, guardrail, trace) nhưng áp vào một nhóm traffic nhỏ trước khi mở rộng.
Bản mới nhận bản sao request thật, output không gửi cho ai. Chấm offline, so với bản đang chạy. Tốt cho model/router; không dùng được khi bản mới gọi tool có tác dụng phụ (gửi email, đặt vé) trừ khi tool được giả lập.
Một phần nhỏ user thật dùng bản mới. So cùng thời gian với nhóm đối chứng để loại nhiễu mùa vụ. Cần đủ mẫu mới kết luận được (xem 9.7).
Quy tắc định trước: guardrail trip gấp đôi, hoặc cận trên rate hiệu chỉnh dưới ngưỡng → tự quay về. Quyết định trước khi deploy, không lúc 2 giờ sáng.
Prompt v13 chạy canary 5% trong 48 giờ; mỗi nhóm được chấm 100% trace bằng evaluator code và 20% bằng judge (minh hoạ).
| Chỉ số | Đối chứng (v12) | Canary (v13) | Cổng |
|---|---|---|---|
| Guardrail trip | 0,31% | 0,34% | ≤ 2× đối chứng ✓ |
| policy_faithful (hiệu chỉnh, CI 95%) | 92,0% [91,1; 92,9] | 91,4% [88,9; 93,7] | cận trên ≥ 90% ✓ nhưng dải rộng |
| Latency p95 | 3,1 s | 2,6 s | ≤ +10% ✓ |
| Chuyển người thật | 4,2% | 4,9% | ≤ +1 điểm ✓ (sát) |
- Mọi cổng qua, nhưng dải của canary rộng (±2,4 điểm) vì mẫu nhỏ — chưa đủ để nói v13 "không kém hơn".
- Tín hiệu yếu cùng hướng (chuyển người thật +0,7 điểm) → đọc 30 trace canary có chuyển người thật: 6 trace khách hỏi về phí đổi vé, v13 trả lời chung chung.
- Quyết định: mở rộng lên 25% để thu hẹp dải, đồng thời mở error analysis nhỏ cho "phí đổi vé". Không mở 100% ngay.
Cho mỗi thay đổi, chọn shadow, canary, hoặc cả hai: (a) đổi model đích cho câu hỏi FAQ (chỉ sinh text); (b) thêm tool refund_now tự hoàn tiền; (c) đổi luật router gửi 20% traffic sang model rẻ hơn.
Xem lời giải
(a) Shadow trước (không rủi ro, so output được hoàn toàn) rồi canary nhỏ để đo phản ứng người dùng. (b) Không shadow thật vì tool có tác dụng phụ — shadow với tool giả lập để kiểm tham số (inter-step guardrail), rồi canary rất nhỏ (1%) có guardrail "số tiền ≤ giá vé" và giới hạn tổng tiền hoàn/ngày. (c) Shadow routing là lý tưởng: chạy luật mới song song, ghi quyết định, chấm offline những request mà hai luật bất đồng — xem mục áp dụng cho model-routing bên dưới.
9.11Bánh đà cải thiện liên tục
CI và CD không phải hai công đoạn rời: chúng nối thành vòng lặp. Deploy kèm monitoring → phát hiện vấn đề ở production → chẩn đoán bằng error analysis → cải thiện agent và cập nhật evaluator → xác nhận bản sửa qua CI → deploy lại. Mỗi vòng làm hệ thống vững hơn và bộ eval đầy đủ hơn.
Một vòng quay theo sách (trợ lý bất động sản)
| Bước | Điều xảy ra (diễn giải) |
|---|---|
| ① Deploy | Bản mới có tính năng nhắm theo khu phố; qua mọi check CI đã có (tone, tool bịa, thiếu ràng buộc SQL). |
| ②③ Monitor & phát hiện | Sau khoảng một tuần, tỷ lệ lỗi địa danh mơ hồ tăng vọt; metric sản phẩm xác nhận: session ngắn hơn, hoàn thành task thấp hơn ở vài khu. |
| ④ Chẩn đoán | Đọc trace bị flag: LLM đảo từ chỉ hướng trong tên khu phố, tạo ra một khu không tồn tại → định nghĩa failure mode mới về từ chỉ hướng trong địa danh. |
| ⑤ Cập nhật eval | Thêm 5–10 ví dụ vào golden set; viết evaluator code kiểm tên địa điểm sinh ra có trong danh sách khu phố hợp lệ. |
| ⑥ Sửa + CI | Đưa danh sách khu phố hợp lệ vào system prompt; qua CI; redeploy → dashboard thấy tỷ lệ lỗi giảm. |
Bối cảnh. ShopMate là trợ lý CSKH cho một shop mỹ phẩm online, ~15.000 hội thoại/ngày, 4 tool (order_status, return_policy, product_search, handoff_human). Golden set v11 có 64 case (14 critical). Production chấm 2% traffic bằng 6 evaluator. Toàn bộ số liệu là minh hoạ.
Ngày 0 — PR #482: "trả lời ngắn gọn hơn + dùng tool chính sách đổi trả mới"
- Leakage check xanh; check_pins xanh (model agent & 2 judge đều id có ngày).
- Tầng 1 (critical, k=3): 13/14. Case
gs-051fail: khách hỏi đổi son đã mở nắp — bản mới trả lời "được đổi trong 7 ngày" nhưng bỏ điều kiện "chưa dùng". Đọc diff: regression thật do chỉ dẫn "ngắn gọn" khiến model cắt điều kiện. - Sửa prompt: "ngắn gọn nhưng luôn giữ đủ điều kiện của chính sách". Chạy lại: 14/14.
- Tầng 2: 46/50 = 92% so với baseline 47/50 = 94%; dung sai 3 điểm → xanh. Hai case lật đỏ là tone (judge chê "cụt lủn") — reviewer đọc, chấp nhận là đánh đổi có chủ đích, ghi vào PR.
- Merge. Baseline mới lưu kèm hash golden set + version.
Ngày 1–2 — Canary 5%
Guardrail trip 0,5% (đối chứng 0,4%), latency p95 giảm 18% nhờ output ngắn, rate hiệu chỉnh "policy_faithful" 93% [90; 96] vs 94% [93; 95]. Không cổng nào đỏ → mở 50% rồi 100% vào ngày 3.
Ngày 9–11 — Monitoring bắt được thứ CI không thể biết
- Ngày 9: shop chạy flash sale "combo 3 món giảm 30%". Guardrail
promo_code_allowlist(mã giảm giá trong output phải thuộc danh sách đang hoạt động) trip tăng từ 0,2% lên 3,5% — tín hiệu sớm nhất, xuất hiện sau vài giờ. - Ngày 10: tỷ lệ chuyển người thật tăng từ 5% lên 6,6%; CSAT giảm nhẹ. Rate hiệu chỉnh "policy_faithful" cửa sổ 7 ngày vẫn chưa dưới ngưỡng (bị pha loãng bởi các ngày trước).
- Ngày 11: cận trên dải 95% của evaluator "promo_accuracy" (cửa sổ 3 ngày) = 86% < ngưỡng 88% → alert.
Ngày 11–12 — Chẩn đoán
Error analysis trên 60 trace bị flag + 40 trace ngẫu nhiên (bắt buộc có trace ngẫu nhiên để không bị thiên lệch). Open coding cho ra 2 cụm: (a) bot tự bịa mã theo mẫu "COMBO30" khi khách hỏi "mã combo là gì" (23 trace); (b) bot gộp điều kiện combo với miễn phí vận chuyển — hai khuyến mãi không được dùng chung (14 trace). Nguyên nhân gốc: agent không có tool đọc khuyến mãi đang chạy; nó suy từ banner trong context.
Ngày 12–13 — Quay bánh đà
- Golden set v12: +8 case (4 bịa mã, 4 gộp khuyến mãi), 3 case đánh dấu critical vì liên quan tiền.
- Evaluator mới:
promo_code_valid(code, tra DB khuyến mãi) vàpromo_stacking(judge, kiểm định trên 120 nhãn: TPR 0,92, TNR 0,89). - Sửa agent: thêm tool
active_promotions(), prompt yêu cầu chỉ nêu mã lấy từ tool. CI: 72 case, critical 17/17, tầng 2 xanh. - Canary 10% 24 giờ (rút ngắn vì đang có sự cố): guardrail trip về 0,3%. Rollout 100%.
- Ngày 20: "promo_accuracy" về 95% [93; 97]. Postmortem ghi: guardrail allowlist bắt sớm hơn evaluator 2 ngày → đề xuất cảnh báo khi tỷ lệ trip của bất kỳ guardrail nào tăng gấp 3 trong 2 giờ.
Bài học rút ra
- CI của ngày 0 làm đúng việc của nó (bắt regression "cắt điều kiện") nhưng không thể biết về flash sale ngày 9 — đó là việc của CD.
- Guardrail không chỉ chặn lỗi mà còn là cảm biến sớm: tỷ lệ trip là một time series đáng theo dõi.
- Cửa sổ 7 ngày quá chậm cho sự cố theo chiến dịch; failure mode gắn với tiền nên có cửa sổ ngắn + lấy mẫu dày hơn khi có sự kiện marketing.
- Vòng bánh đà để lại tài sản lâu dài: 8 golden case, 2 evaluator, 1 tool — lần flash sale sau CI sẽ bắt ngay.
Chatbot tư vấn bảo hiểm: sau khi thêm tính năng so sánh gói, thumb-down tăng 40% nhưng 6 evaluator đều phẳng. Mô tả 6 bước bánh đà bạn sẽ làm, cụ thể tới mức "thêm gì vào đâu".
Xem lời giải
① Deploy đã xảy ra; ② Monitor: tín hiệu nằm ở metric sản phẩm, evaluator phẳng → theo sách, đây là dấu hiệu có lỗi chưa được phát hiện; ③ Phát hiện: tách thumb-down theo intent — tập trung ở intent "so sánh gói"; ④ Chẩn đoán: đọc 50 trace thumb-down + 30 trace ngẫu nhiên cùng intent, open coding → ví dụ phát hiện bảng so sánh bỏ sót "thời gian chờ" (waiting period) — thứ khách quan tâm nhất; ⑤ Cập nhật eval: failure mode "so sánh thiếu trường bắt buộc", 6–8 golden case, evaluator code kiểm bảng so sánh có đủ các trường bắt buộc; ⑥ Sửa: template so sánh bắt buộc các trường, qua CI, canary, xác nhận thumb-down về mức cũ. Kiểm tra thêm khả năng UX (bảng hiển thị vỡ trên mobile) trước khi kết luận lỗi AI — sách nhắc metric sản phẩm giảm có thể do UX.
9.12Pitfalls thường gặp
- Golden set đóng băng trong khi traffic tiến hoá → CI mất giá trị dần. Ví dụ: golden set soạn trước khi có kênh Zalo; nửa năm sau 40% traffic đến từ Zalo với câu cực ngắn, không dấu — CI không có case nào như vậy. Mỗi vòng bánh đà phải thêm ví dụ từ lỗi thật.
- Metric xung đột (ngắn gọn ↔ đủ thông tin chính xác): xếp thứ ưu tiên trước khi xung đột xảy ra; an toàn luôn đứng đầu. Ví dụ: ShopMate ngày 0 — "ngắn gọn" cắt mất điều kiện chính sách.
- Metric AI tách rời metric sản phẩm: satisfaction giảm có thể do AI hoặc do UX — điều tra cả hai; nếu do AI (vd dài dòng khiến người dùng bỏ đi), định nghĩa failure mode + evaluator mới.
- Chỉ nhìn monitoring tự động: judge bỏ sót lỗi tinh vi. Đọc cả trace ngẫu nhiên, không chỉ trace bị flag. Engagement giảm mà metric evaluator phẳng → có lỗi chưa ai thấy.
- Dùng alias model cho agent hoặc judge → hai thứ cùng trôi, regression gần như không thể chẩn đoán.
- Leakage: ví dụ golden lọt vào few-shot (của agent hoặc judge) → CI xanh giả.
- (Mở rộng) Re-run CI tới khi xanh: biến CI thành trò may rủi; cố định k và luật đa số trong runner.
- (Mở rộng) Alert trên trung bình ngày với mẫu nhỏ: kêu liên tục rồi bị tắt tiếng; dùng dải tin cậy và cửa sổ phù hợp mức nghiêm trọng.
- (Mở rộng) Bật guardrail thẳng ở chế độ chặn: không đo báo nhầm trên trace hợp lệ và không chạy log-only trước.
9.13Áp dụng cho dự án model-routing
Router của bạn quyết định mỗi request LLM đi tới model nào để cân bằng chất lượng, chi phí và latency. Về mặt CI/CD, router có hai điểm đặc biệt so với một agent thường: (1) nó phụ thuộc vào nhiều model đích cùng lúc, nên mọi thay đổi ngầm ở bất kỳ provider nào cũng làm lệch "bảng năng lực" mà router dựa vào; (2) lỗi của router thường không lộ ra ở output — route nhầm lên model đắt vẫn cho câu trả lời tốt, chỉ là tốn tiền; route nhầm xuống model rẻ có khi vẫn "tạm được". Vì thế cần đo cả quyết định lẫn kết quả. Phần này nối với bản blueprint bạn đã viết (playground/jcode-evaluation-study/04-blueprint-for-model-routing-eval.md): Eval A (độ đúng quyết định route), Eval B (kết quả/cost–quality), Eval C (cấu hình + sức khoẻ provider), và các nguyên tắc negative control, coverage lock, pinned manifest.
1 · Golden set cho router = nhãn quyết định, không chỉ nhãn output
Mỗi case lưu tập tier chấp nhận được (không chỉ một đáp án), vì thường có nhiều lựa chọn đúng. Từ đó định nghĩa bốn chỉ số khác nhau về hậu quả:
| Chỉ số | Định nghĩa | Hậu quả khi sai | Loại cổng |
|---|---|---|---|
| Under-routing | chọn tier yếu hơn tier tối thiểu chấp nhận | chất lượng hỏng, user thấy | Cứng, như tầng critical |
| Over-routing | chọn tier mạnh hơn tier tối đa cần thiết | tốn tiền/latency, user không thấy | Mềm, có dung sai |
| Negative control | request "bẫy" (dài nhưng dễ, có từ khoá "phức tạp" nhưng đơn giản) phải không bị đẩy lên | router học vẹt đặc trưng bề mặt | Cứng |
| Expected cost | chi phí trung bình/request trên golden set theo bảng giá đã pin | ngân sách vỡ | Mềm (≤ +x%) |
Coverage lock: mỗi nhóm request (FAQ, code, trích xuất, suy luận nhiều bước, ngôn ngữ, độ dài) có số case tối thiểu; CI fail nếu PR xoá case làm một nhóm tụt dưới mức khoá — tránh "sửa" golden set cho vừa router.
PR hạ ngưỡng độ khó từ 0,62 xuống 0,55 để đẩy thêm request lên tier M. Golden set router 180 case (minh hoạ).
- Quyết định: accuracy (chọn ∈ tập chấp nhận) 88% → 90%. Nhìn riêng: under-routing 4,4% → 1,7% (tốt), over-routing 7,8% → 8,3%, negative control 20/20 vẫn pass.
- Chi phí: expected cost/request trên golden set $0,0021 → $0,0024 (+14%). Cổng mềm cho phép +10% → đỏ.
- Kết quả (Eval B): chạy 40 case vừa đổi route qua model thật và chấm bằng judge đã pin: 7 case trước đó fail ở tier S giờ pass ở tier M.
- Quyết định của reviewer: đánh đổi +14% chi phí lấy −2,7 điểm under-routing là hợp lý; nâng hạn mức chi phí bằng một commit riêng có ghi lý do (không lặng lẽ nới ngưỡng trong cùng PR). Merge → shadow 3 ngày trước khi bật.
Cổng CI cho router (tự soạn):
# evals/router_gate.py — chạy trong CI khi router/** hoặc models.lock.yaml đổi
import json, sys, collections
from router import route # route(request: dict) -> "S" | "M" | "L"
TIER = {"S": 0, "M": 1, "L": 2}
PRICE = {"S": 0.0004, "M": 0.0020, "L": 0.0120} # $/request trung bình, lấy từ bảng giá đã pin
GATES = {"max_under": 0.02, "max_over": 0.10, "max_cost_increase": 0.10}
COVERAGE_LOCK = {"faq": 25, "code": 25, "extract": 20, "multistep": 25, "long_ctx": 15, "neg_control": 20}
cases = [json.loads(l) for l in open("evals/router_golden.jsonl") if l.strip()]
base = json.load(open("evals/router_baseline.json")) # {"cost": 0.0021, ...}
cov = collections.Counter(c["group"] for c in cases)
errors = [f"coverage {g}: {cov[g]} < {m}" for g, m in COVERAGE_LOCK.items() if cov[g] < m]
under = over = neg_fail = 0
cost = 0.0
for c in cases:
t = route(c["request"])
lo, hi = TIER[c["min_tier"]], TIER[c["max_tier"]] # tập chấp nhận = [min_tier, max_tier]
under += TIER[t] < lo
over += TIER[t] > hi
neg_fail += c["group"] == "neg_control" and TIER[t] > hi
cost += PRICE[t]
n = len(cases); cost /= n
report = {"n": n, "under": under / n, "over": over / n, "neg_fail": neg_fail, "cost": cost,
"cost_delta": cost / base["cost"] - 1}
print(json.dumps(report, indent=2))
if report["under"] > GATES["max_under"]: errors.append("under-routing vượt ngưỡng")
if neg_fail: errors.append(f"{neg_fail} negative control bị đẩy lên tier cao")
if report["over"] > GATES["max_over"]: errors.append("over-routing vượt ngưỡng")
if report["cost_delta"] > GATES["max_cost_increase"]: errors.append("chi phí dự kiến tăng quá hạn mức")
sys.exit("\n".join(errors) if errors else 0)
2 · Pin mọi thứ — và coi cập nhật model đích là một PR
- Manifest pin (
models.lock.yaml) chứa id có ngày của mọi model đích + judge + bảng giá tại thời điểm đo. Mỗi trace ghirouter_policy+ id model thực sự được gọi. - Provider ra version mới cho tier M: PR đổi manifest → chạy lại Eval B cho các nhóm request thường đi tier M và các nhóm "biên" giữa S/M, M/L. Nếu tier M mới mạnh lên, ngưỡng router có thể nên dịch (đẩy bớt từ L xuống M) — đó là "xem lại prompt khi model mạnh lên" phiên bản router.
- Behavioral drift dưới cùng version (sách cảnh báo có xảy ra): chạy hằng đêm một bộ "canary prompts" nhỏ cho từng model đích; nếu điểm của một tier lệch mà không ai đổi gì → nghi provider, không nghi router.
- Giá cũng là version: provider đổi giá làm lệch cổng chi phí dù hành vi không đổi — lưu bảng giá cùng manifest, đổi giá = PR.
3 · Monitoring online cho router
| Tín hiệu | Cách đo | Alert gợi ý |
|---|---|---|
| Phân phối route | share S/M/L theo giờ; PSI so với tuần baseline | PSI > 0,1 cảnh báo vàng, > 0,25 đỏ |
| Chi phí | $/request thực tế theo tier và tổng | +20% so với dự kiến trong 24h |
| Chất lượng theo tier | rate hiệu chỉnh + dải 95% cho từng tier (lấy mẫu phân tầng: tier S chấm dày hơn vì rủi ro under-routing) | cận trên < ngưỡng của tier |
| Escalation / fallback | % request bị guardrail đẩy lên tier cao hơn | gấp đôi baseline trong 2h |
| Latency | p95 theo tier và end-to-end (kể cả thời gian router) | vượt SLO |
Baseline tuần trước: S 60% · M 30% · L 10%. Hôm nay: S 45% · M 35% · L 20% (minh hoạ). Không có PR nào vào router.
- PSI = Σ (hiện tại − baseline)·ln(hiện tại/baseline) = (−0,15)·ln 0,75 + 0,05·ln 1,167 + 0,10·ln 2 ≈ 0,043 + 0,008 + 0,069 = 0,12 → trôi vừa phải, cảnh báo vàng.
- Chi phí: 0,6·0,0004 + 0,3·0,002 + 0,1·0,012 = $0,00204 → 0,45·0,0004 + 0,35·0,002 + 0,2·0,012 = $0,00328: +61% mỗi request dù PSI chỉ "vừa" — vì dịch chuyển rơi vào tier đắt nhất.
- Tách nguyên nhân: chạy router (đã pin) trên golden set: phân phối trên golden set không đổi → router không đổi hành vi. Vậy input đổi: tách share theo nhóm request → nhóm "long_ctx" tăng từ 6% lên 17% traffic (một khách hàng doanh nghiệp mới gửi hợp đồng dài).
- Hành động: không phải bug router; nhưng là tín hiệu kinh doanh — xem nhóm long_ctx có thật cần tier L không (chạy Eval B trên mẫu: tier M đạt 91% so với L 94%) → cân nhắc luật riêng cho khách hàng này. Thêm 10 request long_ctx mới vào golden set để coverage phản ánh traffic mới.
Nhập phân phối route baseline và hiện tại (%), giá và success rate từng tier. Lab tính PSI, thay đổi chi phí/request và success rate kỳ vọng — giả định chất lượng mỗi tier không đổi (nên đây chỉ là phần do dịch chuyển phân phối).
4 · Guardrail của router = Retry/Fallback theo tier
- Trước khi route (inter-step, tất định): request vượt context window của tier → không được chọn tier đó; request chứa PII + tier dùng provider không đạt yêu cầu dữ liệu → loại.
- Sau khi model trả lời: output fail schema/parse → escalate lên tier kế (đây chính là Fallback của sách, nhưng đi lên thay vì xuống). Giới hạn 1 lần escalate để chặn chi phí nổ.
- Mỗi lần escalate là một nhãn miễn phí: "tier S không đủ cho request này". Gom định kỳ, error analysis, và đưa các mẫu tiêu biểu vào golden set với
min_tier = M— bánh đà của router. - Budget guardrail: nếu chi phí giờ hiện tại vượt X lần kế hoạch, tạm khoá tier L cho các nhóm không critical — quyết định trước, không lúc sự cố.
5 · Shadow routing trước mọi thay đổi policy
- Chạy policy ứng viên song song trên 100% traffic, chỉ ghi quyết định (rẻ: không gọi model).
- Đo bất đồng: % request hai policy chọn khác nhau, theo nhóm và theo hướng (ứng viên chọn thấp hơn hay cao hơn).
- Phân xử offline một mẫu bất đồng: chạy cả hai model, chấm bằng judge đã kiểm định (hoặc người) → ước lượng chất lượng và chi phí của policy mới chỉ trên vùng khác biệt — hiệu quả hơn nhiều so với canary mù.
- Chỉ khi ước lượng trên vùng bất đồng đạt cổng mới canary; case bất đồng đã phân xử được thêm vào golden set.
Nhầm lẫn thường gặp · "Success rate end-to-end không đổi thì router mới tốt bằng router cũ"
Router mới có thể đẩy 15% traffic xuống tier S mà success rate tổng chỉ giảm 0,5 điểm — trong dải nhiễu của monitoring. Nhưng trên đúng 15% bị đổi route, chất lượng có thể giảm 4 điểm. Luôn đo trên vùng bất đồng (shadow) hoặc phân tầng theo route, không chỉ trên tổng — tổng pha loãng tín hiệu.Router phục vụ 200.000 request/ngày; tier S chiếm 65%. Bạn có ngân sách chấm judge 3.000 trace/ngày. (a) Phân bổ 3.000 mẫu giữa S/M/L thế nào? (b) Viết 3 luật alert, mỗi luật ghi rõ tín hiệu, cửa sổ, ngưỡng và ai nhận. (c) Provider tier M thông báo version mới — liệt kê các bước trước khi đổi manifest.
Xem lời giải
(a) Lấy mẫu phân tầng theo rủi ro, không theo tỷ lệ traffic: ví dụ S 1.500 (rủi ro under-routing cao nhất, cần dải hẹp), M 900, L 600 — với cửa sổ 3 ngày, dải 95% mỗi tier khoảng ±1,3–1,8 điểm (giả định judge có TPR + TNR − 1 ≈ 0,8). Khi tổng hợp rate toàn hệ thống, cân lại trọng số theo share traffic thật. (b) Ví dụ: ① "Chất lượng tier S": rate hiệu chỉnh cửa sổ 3 ngày, cận trên < 85% → on-call; ② "Chi phí": $/request trượt 6h > +20% so với kế hoạch → kênh team + FinOps; ③ "Phân phối route": PSI theo ngày so tuần baseline > 0,25 → kênh team (không page), kèm bảng share theo nhóm request để phân biệt input drift với router drift. (c) Pin id mới vào nhánh PR; chạy canary prompts + Eval B trên các nhóm đi tier M và nhóm biên; so cost/latency với bảng giá mới; error analysis 100 output; xem có nên dịch ngưỡng router; shadow/canary; cập nhật baseline và ghi changelog manifest.
Tóm tắt áp dụng · 7 việc cho router của bạn
- Golden set router với
min_tier/max_tier, negative control, coverage lock. - Cổng CI tách under-routing (cứng) và over-routing/chi phí (mềm).
models.lock.yamlcho mọi model đích + judge + bảng giá; cập nhật = PR chạy Eval B.- Canary prompts hằng đêm cho từng model đích để bắt behavioral drift của provider.
- Dashboard theo tier: share + PSI, $/request, p95, rate hiệu chỉnh với lấy mẫu phân tầng.
- Escalation = guardrail + nguồn nhãn miễn phí cho bánh đà.
- Shadow routing + phân xử vùng bất đồng trước mọi thay đổi policy.
9.14Checklist triển khai
- Golden set 20–50 case (cốt lõi + lỗi cũ + edge case + phủ nhánh), metadata nguồn/critical/audit, lưu tách khỏi prompt.
- Cổng CI hai tầng mỗi PR: critical không thương lượng; tổng thể có dung sai so với baseline; k lần chạy cố định; judge pin version.
- Leakage check tự động (golden vs prompt agent + prompt judge).
- Pin version mọi model; chính sách tuổi version; nâng cấp = PR qua CI + error analysis lại.
- Log trace đầy đủ (spans, versions, feedback); đọc mẫu trace ngẫu nhiên hằng ngày.
- 5–7 evaluator online trên 1–5% traffic (phân tầng khi cần); rate hiệu chỉnh + dải 95%; alert theo dải.
- Guardrail tất định, đo báo nhầm < 1% trên trace hợp lệ, log-only trước; Reject/Retry/Fallback định sẵn; theo dõi tỷ lệ trip như một time series.
- Tái kiểm định judge vài tuần/lần và mỗi khi judge hoặc tiêu chí đổi.
- (Mở rộng) Shadow/canary với cổng và luật rollback định trước.
- Quay bánh đà: mỗi lỗi mới từ production → golden case + evaluator → sửa → CI → deploy.