Đánh giá cộng tác: làm cho con người đồng ý về "thế nào là lỗi"
TL;DR
- Mọi evaluator đều dựa trên nhãn do người gán. Nhãn nhiễu → đếm lỗi nhiễu, LLM judge học sai chuẩn, quyết định sửa đặt nhầm chỗ.
- Nhiều team chỉ cần một chuyên gia làm chuẩn (benevolent dictator). Khi cần nhiều người: quy trình 9 bước — gán độc lập → đo κ từng tiêu chí → alignment session → sửa rubric → lặp.
- Đo đồng thuận bằng Cohen's κ, không phải % đồng ý thô — nhãn càng dồn về Pass, đồng ý "ăn may" càng cao. Với 3+ người dùng Fleiss' κ; thiếu nhãn dùng Krippendorff's α; thang 1–5 dùng weighted κ (nhưng ưu tiên nhị phân).
- Họp căn chỉnh để sửa rubric cho lần sau (làm rõ chữ, thêm ví dụ, thêm decision rule, tách tiêu chí), không phải để phân xử nhãn cũ ai đúng.
- κ dành cho hai người ngang hàng; so LLM judge với nhãn gold thì dùng TPR/TNR. IAA của người ≈ trần độ chính xác bạn có thể đòi ở judge.
- Đầu ra: rubric + bộ nhãn gold + nhật ký edge case — nguyên liệu cho Chương 5.
Trang này được mở rộng thế nào
Phần tóm tắt nội dung sách được giữ ngắn gọn và diễn giải lại. Phần lớn trang là tài liệu tự soạn: ví dụ tính tay với dữ liệu bịa, đoạn code Python tự viết (đã chạy thử), bài tập có lời giải, một case study xuyên suốt (team "VíXanh"), 4 lab tương tác và phần áp dụng cho dự án model-routing. Mọi con số trong ví dụ đều mang tính minh hoạ.4.1Vì sao phải căn chỉnh phán đoán con người
Chương 3 cho bạn danh sách failure mode bằng cách đọc trace và gán nhãn. Nhưng mỗi nhãn "Pass/Fail" là một phán đoán của con người — và phán đoán thì có ba "bệnh" cố hữu:
Người này thấy "hữu ích", người kia thấy "dài dòng" — cùng một câu trả lời.
Cùng một người chấm lại cùng trace vào hôm khác có thể ra nhãn khác.
"Tốt" dịch chuyển khi team thấy thêm dữ liệu; người gán thường tự sửa lại nhãn cũ.
Sách dẫn hai bằng chứng: Notion vận hành eval với khoảng 70 kỹ sư và lập riêng một nhóm chỉ để viết rubric "thế nào là tốt" cho từng tính năng AI; và nghiên cứu của Shankar và cộng sự (2024) quan sát thấy dev thường xuyên sửa lại nhãn cũ khi xem thêm ví dụ. Bài học: rubric là tài liệu sống, phải tiến hoá cùng dữ liệu — và nếu nhãn không đáng tin thì mọi thứ xây trên nó đều thừa hưởng nhiễu.
Chatbot đặt vé máy bay. Khách hỏi: "Vé Hà Nội – Đà Nẵng ngày 30/4 còn không, rẻ nhất bao nhiêu?". Bot trả lời 6 dòng: liệt kê 3 chuyến, giá rẻ nhất 1.290.000đ, thêm một đoạn "mẹo săn vé dịp lễ" và lời nhắc mang CCCD. Tiêu chí: "hữu ích".
- Người A (PM) — Pass: trả lời đúng câu hỏi, có giá, có lựa chọn.
- Người B (CSKH) — Fail: khách hỏi hai ý, bot trả sáu dòng; đoạn "mẹo săn vé" làm khách phải cuộn — trên mobile là phiền.
- Người C (kỹ sư) — Pass nhưng ghi chú "dài": với anh, dài không phải lỗi, chỉ là điểm trừ.
- Chẩn đoán: không ai "sai". Chữ hữu ích đang gánh hai câu hỏi khác nhau: (1) có trả lời đúng điều được hỏi? và (2) có ngắn gọn so với ngữ cảnh mobile?. Đây chính là loại mơ hồ mà quy trình trong chương này được thiết kế để lộ ra.
Bạn gán 20 trace hôm thứ Hai (12 Pass, 8 Fail). Thứ Hai tuần sau, không xem nhãn cũ, bạn gán lại đúng 20 trace đó. Kết quả (minh hoạ):
| Tuần 2: Pass | Tuần 2: Fail | |
|---|---|---|
| Tuần 1: Pass | 10 | 2 |
| Tuần 1: Fail | 2 | 6 |
- Đồng ý với chính mình: (10 + 6)/20 = 80%.
- Tỉ lệ Pass hai lần đều là 60% → đồng ý ăn may Pe = 0.6² + 0.4² = 0.52.
- "Self-κ" = (0.80 − 0.52)/(1 − 0.52) ≈ 0.58. Bạn còn chưa đồng ý hẳn với chính mình — vậy đừng ngạc nhiên nếu hai người khác nhau đạt κ thấp hơn.
- Bốn trace bị lật hai chiều (2 P→F, 2 F→P) → nhiễu ngẫu nhiên, không phải drift. Nếu cả 4 cùng lật về một phía, đó là dấu hiệu tiêu chuẩn của bạn đã dịch chuyển (xem 4.10).
Giả sử thật sự có 8% trace mắc lỗi "sai giá" và 10% mắc lỗi "giọng điệu". Người gán nhận ra lỗi "sai giá" khá chắc (bỏ sót 10%, gán nhầm 1% trace tốt), nhưng lỗi giọng điệu thì mơ hồ (bỏ sót 40%, gán nhầm 2%).
- Tỉ lệ "sai giá" đo được ≈ 0.08·0.9 + 0.92·0.01 = 7.2% + 0.9% ≈ 8.1%.
- Tỉ lệ "giọng điệu" đo được ≈ 0.10·0.6 + 0.90·0.02 = 6% + 1.8% = 7.8%.
- Thực tế giọng điệu phổ biến hơn (10% > 8%), nhưng bảng đếm lại xếp "sai giá" lên đầu. Nhãn nhiễu không chỉ làm con số lệch — nó đổi quyết định.
Hiểu nhầm thường gặp
"Nhiễu nhãn chỉ làm số liệu kém chính xác chút ít, trung bình sẽ tự triệt tiêu." — Sai khi nhiễu không đối xứng. Người gán thường bỏ sót lỗi tinh vi nhiều hơn là gán nhầm lỗi cho trace tốt, nên tiêu chí càng mơ hồ thì càng bị đếm thiếu có hệ thống. Trung bình không cứu được bias.Team bạn gán 300 trace cho tiêu chí "trả lời đúng trọng tâm". Sau hai tháng, một người mới vào gán lại 30 trace cũ và bất đồng với nhãn gốc ở 9 trace. Hãy liệt kê ít nhất ba giả thuyết giải thích con số này và cách kiểm chứng từng giả thuyết.
Xem lời giải
- Rubric mơ hồ (người mới hiểu khác): xem 9 ca lệch có tập trung vào một kiểu tình huống không (vd. câu hỏi nhiều ý). Nếu có → alignment session + decision rule.
- Criteria drift ở team cũ: nhờ chính người gán gốc gán lại 30 trace đó mà không xem nhãn cũ. Nếu họ cũng lật nhiều và lật một chiều → tiêu chuẩn đã dịch.
- Người mới thiếu ngữ cảnh (chưa biết chính sách nội bộ): kiểm tra các ca lệch có đòi kiến thức domain không có trong rubric → bổ sung rubric, không phải "đào tạo miệng".
- Nhiễu ngẫu nhiên: 9/30 với n nhỏ có khoảng tin cậy rộng; tính κ và bootstrap CI (mục 4.6) trước khi kết luận.
4.2Một chuyên gia có đủ không? — "Benevolent dictator"
Trực giác: đồng thuận chỉ là vấn đề khi có nhiều người phán. Nếu một người đáng tin làm "nguồn sự thật" duy nhất, sẽ không có xung đột nhãn để giải quyết. Sách mượn thuật ngữ từ quản trị mã nguồn mở — nơi một maintainer có tiếng nói cuối cùng — và cho rằng ở công ty nhỏ-vừa, chỉ định một chuyên gia domain chính thường hiệu quả hơn là họp hội đồng.
Họ định nghĩa "chấp nhận được" và giúp team hiểu sản phẩm có khớp điều người dùng muốn không. Một người = không có xung đột nhãn.
Đọc trace cùng họ giúp lôi ra những kỳ vọng ngầm mà chính họ chưa nói thành lời được từ đầu.
Nhanh, gọn; và chuyên gia thấy mình "góp tay" tạo ra sản phẩm → thành người ủng hộ nó.
| Sản phẩm AI | Ai có thể là "benevolent dictator" |
|---|---|
| Trợ lý sức khoẻ tinh thần | Nhà tâm lý học |
| Phân tích văn bản pháp lý | Luật sư |
| Chatbot hỗ trợ khách hàng | Giám đốc CSKH |
| Công cụ giáo dục | Giáo viên trưởng / người thiết kế chương trình |
| Startup nhỏ | Founder / CEO |
Hai cái bẫy khi chọn "người phán quyết"
Dev độc lập mặc nhiên là chuyên gia — hãy trung thực về giới hạn hiểu biết và thường xuyên đối chiếu với phản hồi người dùng thật. Lấy sếp làm proxy cho tiện dễ thành thảm hoạ nếu phán đoán của họ phản ánh sở thích cá nhân chứ không phải nhu cầu người dùng. Mô hình này hiệu quả chính vì nó cắt đứt sự mơ hồ bằng một nguồn sự thật — nên nguồn đó phải đúng người.Khi nào hết phù hợp: domain quá rộng để một người đánh giá toàn diện; tiêu chí bị các bên (legal, product, CSKH) tranh cãi; hoặc bạn cần chứng minh quy trình khách quan với bên ngoài (audit, khách hàng doanh nghiệp).
| Tình huống | ① | ② | ③ | Kết luận |
|---|---|---|---|---|
| App ăn kiêng cho người tiểu đường, 1 chuyên gia dinh dưỡng, ~40 trace/tuần | ✓ | ✓ | ✓ | Benevolent dictator. Mời chuyên gia đọc trace cùng dev mỗi tuần. |
| Trợ lý soạn hợp đồng B2B, 3 luật sư (thương mại, lao động, SHTT), mỗi người mạnh một mảng | ~ | ✗ | ✓ | Không một ai đủ chuyên môn toàn bộ → quy trình cộng tác, hoặc chia "dictator theo mảng" (mỗi luật sư phán tiêu chí thuộc mảng mình). |
| Chatbot ngân hàng, bị kiểm toán yêu cầu chứng minh quy trình đánh giá | ✓ | ✓ | ✗ | Dù có người giỏi, cần ≥2 người + số liệu κ để làm bằng chứng khách quan. |
- Đi lần lượt từng điều kiện, đánh ✓/✗/~ (một phần).
- Chỉ cần một ✗ ở ① hoặc ② là đã nên nghĩ tới nhiều người; ✗ ở ③ đôi khi giải được bằng cách lấy mẫu ít hơn thay vì thêm người.
- "Dictator theo mảng" là biến thể hữu dụng: mỗi tiêu chí có đúng một người chủ, tránh được họp hội đồng cho mọi thứ.
Hiểu nhầm thường gặp
"Một người phán thì khỏi cần viết rubric." — Vẫn cần. (1) LLM judge ở Chương 5 cần rubric thành văn để làm prompt và để so; (2) chính chuyên gia cũng drift — rubric + bộ neo giúp họ tự kiểm (ví dụ 4.1b); (3) khi họ nghỉ phép hoặc rời công ty, kiến thức không đi theo họ.Startup 6 người làm trợ lý viết mô tả sản phẩm cho shop thời trang. CEO muốn tự làm người phán. Nhưng CEO không bao giờ tự bán hàng online, trong khi Head of Sales từng quản lý 3 shop. Bạn đề xuất gì?
Xem lời giải
Điều kiện ② ("tiêu chí đúng chuyên môn") nghiêng về Head of Sales. Đề xuất: Head of Sales là benevolent dictator cho các tiêu chí về chất lượng bán hàng (đúng thông tin sản phẩm, lôi cuốn, đúng giọng thương hiệu); CEO giữ quyền với các tiêu chí chiến lược (vd. không nhắc tên đối thủ). Để tránh "sếp làm proxy theo sở thích", thêm một kiểm tra thực tế: định kỳ so nhãn của người phán với tín hiệu người dùng (tỉ lệ shop giữ nguyên mô tả, tỉ lệ sửa tay).
4.3Quy trình gán nhãn cộng tác 9 bước
Trực giác: quy trình này giống "kiểm thử rubric". Rubric là một đặc tả; mỗi người gán là một "bản cài đặt" độc lập của đặc tả đó. Nếu hai bản cài đặt cho kết quả khác nhau trên cùng input, lỗi nằm ở đặc tả. Vòng lặp 9 bước là cách tìm và vá các lỗ hổng đó cho tới khi đặc tả đủ chặt.
- Lập team gán nhãnHai người trở lên có chuyên môn hoặc góc nhìn liên quan (team lớn: đại diện engineering, product, legal, CSKH). Nói rõ vai trò và phạm vi ngay từ đầu.
- Viết rubric nhápMỗi tiêu chí một rubric: định nghĩa làm việc + vài ví dụ Pass/Fail. Thường là "chính thức hoá" các failure mode từ error analysis.
- Chọn bộ trace chung20–50 trace đại diện, cố tình đưa vào ca mơ hồ. Ai cũng đồng ý ca dễ; ca khó mới là nơi thử rubric.
- Gán nhãn độc lậpKhông trao đổi. Bàn bạc lúc này làm nhãn tương quan với nhau → mất khả năng phát hiện chỗ mơ hồ.
- Đo IAA cho từng tiêu chíκ thấp ở tiêu chí nào → rubric đó mơ hồ. κ cao → tiêu chí đó sẵn sàng.
- Alignment sessionMỗi buổi tập trung một tiêu chí. Mục tiêu: hiểu nguồn gốc bất đồng và sửa rubric (mục 4.9).
- Sửa rubricLàm rõ định nghĩa, thay/thêm ví dụ, điều chỉnh tiêu chuẩn. Rubric phải chính xác hơn sau mỗi vòng.
- LặpTrace mới → gán độc lập → đo κ → sửa, tới khi mỗi tiêu chí quan trọng đạt κ khoảng 0.6 trở lên.
- Chốt rubric & nhãnTài liệu rubric cuối + bộ nhãn đồng thuận (gold standard): vừa chứng minh con người áp dụng được rubric, vừa là ground truth để chấm LLM judge.
Chọn bộ trace chung: đừng lấy ngẫu nhiên thuần tuý
Nếu 85% trace production là Pass dễ, lấy ngẫu nhiên 40 trace sẽ cho ~34 ca dễ — cả team đồng ý hết, κ trông ổn, và bạn chẳng học được gì. Hãy phân tầng: một phần ca dễ (để kiểm tra người gán không "khó tính vô cớ"), nhiều ca biên, và một ít lỗi đã biết từ error analysis.
Nguồn ca biên hay dùng: trace mà hai reviewer ở Chương 3 từng ghi chú khác nhau; trace có câu hỏi nhiều ý; trace mà một LLM phân loại thử cho độ tự tin thấp; trace gần ranh giới chính sách (vd. số tiền sát hạn mức). Đoạn code tự viết dưới đây chọn mẫu theo tỉ lệ và xoay vòng theo loại request để không dồn vào một chủ đề:
import random
def pick_shared_set(traces, n=40, mix=(0.35, 0.40, 0.25), seed=42):
"""Chọn bộ trace chung: dễ / ca biên / lỗi đã biết theo tỉ lệ mix.
traces: list dict có 'id', 'bucket' ∈ {'easy','borderline','known_fail'},
và 'category' (loại request) để trải đều."""
rng = random.Random(seed)
buckets = ("easy", "borderline", "known_fail")
chosen = []
for bucket, share in zip(buckets, mix):
pool = [t for t in traces if t["bucket"] == bucket]
k = min(len(pool), round(n * share))
# xoay vòng theo category để không dồn vào một loại request
by_cat = {}
for t in rng.sample(pool, len(pool)):
by_cat.setdefault(t["category"], []).append(t)
while k and any(by_cat.values()):
for cat in list(by_cat):
if by_cat[cat] and k:
chosen.append(by_cat[cat].pop()); k -= 1
rng.shuffle(chosen) # trộn để người gán không đoán được nhóm
return [t["id"] for t in chosen]
# Chạy thử trên 400 trace giả (70% dễ, 20% biên, 10% lỗi):
# easy 14 · borderline 16 · known_fail 10 · tổng 40
Gán nhiều tiêu chí một lượt — nhưng đo riêng từng tiêu chí
Đọc một trace tốn công; gán luôn 3–4 tiêu chí trong một lượt đọc là hợp lý. Nhưng khi đo và họp, tách từng tiêu chí: hai người có thể cùng nhận ra lỗi sự thật mà có trực giác hoàn toàn khác về "giọng quá trang trọng".
Hai người gán 40 trace × 3 tiêu chí (ma trận nhầm lẫn bịa, thứ tự P-P / P-F / F-P / F-F):
| Tiêu chí | P-P | P-F | F-P | F-F | % đồng ý | κ |
|---|---|---|---|---|---|---|
| Đúng sự thật | 28 | 1 | 1 | 10 | 95% | 0.87 |
| Đủ ý | 22 | 4 | 3 | 11 | 82.5% | 0.62 |
| Giọng điệu | 18 | 7 | 6 | 9 | 67.5% | 0.32 |
| Gộp 120 nhãn | 68 | 12 | 10 | 30 | 81.7% | 0.59 |
- Nhìn con số gộp 0.59: "gần 0.6, thêm một vòng là xong".
- Nhìn từng dòng: "đúng sự thật" đã sẵn sàng, "đủ ý" tạm ổn, còn "giọng điệu" chỉ ở mức fair — rubric này cần một alignment session riêng.
- Nếu chỉ báo 0.59, team sẽ đi lặp cả ba rubric (tốn công vô ích cho hai cái đã ổn) hoặc tệ hơn, chốt luôn và cho LLM judge học một định nghĩa "giọng điệu" chưa ai thống nhất.
Mỗi người nhận một bảng tính riêng (không chia sẻ), cột:
trace_id | link_trace | factual (P/F) | complete (P/F) | tone (P/F) | note (bắt buộc khi F hoặc phân vân)
- Cột note là vàng: trong alignment session, ghi chú "phân vân vì câu 2 hỏi hai ý" giá trị hơn nhiều so với một chữ F.
- Ngân sách (minh hoạ): 40 trace × ~2 phút × 3 tiêu chí gộp một lượt ≈ 80 phút/người. Hai người → ~2.7 giờ công cho vòng 1, cộng 1 giờ họp × 3 người (thêm moderator) → tổng ~6 giờ công/vòng.
- Thường cần 2–3 vòng → 12–18 giờ công. So với chi phí một LLM judge học sai chuẩn chạy trong CI hàng tháng, đây là khoản đầu tư rẻ.
Hiểu nhầm thường gặp
"Gán độc lập = chia dữ liệu cho mỗi người một phần cho nhanh." — Không. Bước 4 đòi mọi người gán cùng một bộ trace, mỗi người tự làm. Chia việc (mỗi người một phần) là việc của giai đoạn sau, khi rubric đã đạt κ tốt — và lúc đó vẫn nên cho một phần nhỏ chồng lấn để theo dõi drift. Ngoài ra, vòng lặp phải dùng trace mới: gán lại trace cũ sau khi đã họp về chúng chỉ đo trí nhớ, không đo độ rõ của rubric.Team bạn có 3 tiêu chí. Vòng 1 cho κ: A = 0.81, B = 0.44, C = 0.58. Chỉ có ngân sách cho một buổi alignment 60 phút tuần này. Sắp xếp buổi đó thế nào, và vòng 2 cần gán lại tiêu chí nào?
Xem lời giải
Dành buổi họp cho B (thấp nhất, xa ngưỡng nhất); nếu còn thời gian, 10 phút cuối xem nhanh 2–3 ca lệch của C. A đã sẵn sàng — không họp. Vòng 2 dùng bộ trace mới, gán lại B và C (A có thể gán kèm cho rẻ, vì đọc trace rồi thì gán thêm một cột tốn ít công, và giúp phát hiện drift). Đo κ riêng từng tiêu chí.
4.4Đo đồng thuận: vì sao % đồng ý đánh lừa bạn
Percent agreement (Po, observed agreement) = số item hai người gán cùng nhãn / tổng item. Dễ tính, dễ hiểu — nhưng không trừ phần trùng hợp ngẫu nhiên. Nếu 95% trace là Pass và cả hai người "bấm Pass cho nhanh" mà không đọc, họ vẫn đạt 95%.
Điều trớ trêu: hệ thống càng được sửa tốt, nhãn càng dồn về Pass, nên đồng ý ngẫu nhiên càng cao. Đầu dự án tỉ lệ Pass ~50% thì may rủi cho ~50%; khi Pass lên 90%, may rủi đã cho 0.9² + 0.1² = 82%.
Giả sử hai người luôn có cùng tỉ lệ Pass p. Giữ nguyên 85% đồng ý, chỉ thay đổi p:
| Giai đoạn | p (Pass) | Pe = p² + (1−p)² | κ = (0.85 − Pe)/(1 − Pe) | Đọc |
|---|---|---|---|---|
| Prototype | 50% | 0.50 | 0.70 | substantial |
| Beta | 70% | 0.58 | 0.64 | substantial |
| Ra mắt | 80% | 0.68 | 0.53 | moderate |
| Trưởng thành | 90% | 0.82 | 0.17 | slight |
- Cùng một con số "85%" trên dashboard, nhưng ở giai đoạn trưởng thành nó gần như không nói gì.
- Thậm chí không phải mọi Po đều khả thi: với p = 95%, mỗi người chỉ gán 5% Fail nên tối đa chỉ lệch nhau 10% → Po không thể dưới 90%. Một con số 92% lúc đó gần như chạm đáy.
- Hệ quả thực tế: team cũ báo "đồng ý 85%" hồi prototype và "đồng ý 85%" sau một năm — tưởng chất lượng nhãn giữ nguyên, thực ra đã sụp.
Giả định: hai người có cùng tỉ lệ Pass và bất đồng chia đều hai phía. Đường xanh: κ theo p khi giữ nguyên Po; chấm: vị trí hiện tại.
Dashboard báo: "Hai reviewer đồng ý 96% trên 500 trace tuần này". Trace được gán Pass ở mức 97%. (a) Tính Pe và κ (giả sử hai người cùng tỉ lệ Pass). (b) Bạn sẽ đề xuất thay đổi gì cho dashboard?
Xem lời giải
(a) Pe = 0.97² + 0.03² = 0.9409 + 0.0009 = 0.9418. κ = (0.96 − 0.9418)/(1 − 0.9418) = 0.0182/0.0582 ≈ 0.31 — chỉ "fair". (b) Báo κ thay cho (hoặc cạnh) %; và vì Fail quá hiếm, nên lấy mẫu dồn (oversample) các trace có khả năng Fail cao (theo judge, theo phản hồi người dùng) vào bộ chấm chéo — nếu không, mỗi tuần chỉ có ~15 Fail để đo đồng thuận, quá ít.
4.5Cohen's Kappa — đồng ý vượt mức may rủi
Công thức
κ = (Po − Pe) / (1 − Pe) · với nhãn nhị phân: Pe = P(PassA)·P(PassB) + P(FailA)·P(FailB)
Pe là xác suất trùng nhãn nếu mỗi người gán ngẫu nhiên nhưng giữ đúng tỉ lệ nhãn của mình. Đọc bằng lời: "trong phần đồng ý mà may rủi không tự cho không, hai người giành được bao nhiêu?" κ = 1 hoàn hảo · κ = 0 ngang ăn may · κ < 0 bất đồng có hệ thống (có thể hiểu rubric ngược nhau).
Ví dụ tự dựng: 100 trace, tiêu chí "câu trả lời đủ ý"
(Sách có một ví dụ tính tay tương tự về tóm tắt email; ở đây dùng số liệu khác để bạn tự đối chiếu.)
| B: Pass | B: Fail | Tổng A | |
|---|---|---|---|
| A: Pass | 70 ✓ | 8 | 78 |
| A: Fail | 7 | 15 ✓ | 22 |
| Tổng B | 77 | 23 | 100 |
- Po — đồng ý quan sátĐường chéo: (70 + 15) / 100 = 0.85.
- Pe — đồng ý do may rủiA gán Pass 78%, B gán Pass 77%. Pe = 0.78 × 0.77 + 0.22 × 0.23 ≈ 0.601 + 0.051 = 0.651.
- κ(0.85 − 0.651) / (1 − 0.651) = 0.199 / 0.349 ≈ 0.57 → chỉ "moderate", dù 85% nghe rất đẹp.
Ngoài Po, Pe, κ, lab còn tính κmax (κ cao nhất có thể khi giữ nguyên tỉ lệ Pass của mỗi người) và khoảng tin cậy 95% bằng bootstrap (1.000 lần lấy mẫu lại).
Thực tế bạn có hai cột nhãn, không có sẵn ma trận. A và B gán 10 trace:
trace: 1 2 3 4 5 6 7 8 9 10
A: P P F P P F P P P F
B: P P F F P P P P P F
- Đếm trùng: lệch ở trace 4 (A=P, B=F) và trace 6 (A=F, B=P) → 8/10 trùng → Po = 0.80.
- Tỉ lệ Pass: A có 7 P (0.7), B có 7 P (0.7). Pe = 0.7·0.7 + 0.3·0.3 = 0.49 + 0.09 = 0.58.
- κ = (0.80 − 0.58)/(1 − 0.58) = 0.22/0.42 ≈ 0.52.
- Nhận xét: với n = 10, chỉ cần một trace lật là κ nhảy ±0.2. Đừng ra quyết định từ 10 trace — dùng 30–50 và xem khoảng tin cậy (mục 4.6).
Code tự viết: Cohen's κ có xử lý ca biên
Hàm dưới đây nhận hai list nhãn bất kỳ (không chỉ P/F). Chú ý ca biên: nếu cả hai người chỉ dùng đúng một nhãn cho mọi item, Pe = 1 và κ không xác định — trả về NaN thay vì giả vờ là 1.0.
from collections import Counter
def cohen_kappa(a, b):
"""κ cho 2 người gán cùng một danh sách item (nhãn phân loại bất kỳ)."""
n = len(a)
assert n == len(b) and n > 0
po = sum(x == y for x, y in zip(a, b)) / n
ca, cb = Counter(a), Counter(b)
pe = sum(ca[l] / n * cb[l] / n for l in set(a) | set(b))
if pe == 1: # cả hai dùng đúng 1 nhãn duy nhất
return float("nan") # κ không xác định — đừng báo 1.0
return (po - pe) / (1 - pe)
A = list("PPFPPFPPPF"); B = list("PPFFPPPPPF")
print(round(cohen_kappa(A, B), 3)) # 0.524
Hiểu nhầm thường gặp
- "κ = 0.57 nghĩa là 57% trace được đồng ý thật sự." — Không. κ là tỉ lệ của phần đồng ý vượt may rủi trên phần có thể vượt; nó không phải phần trăm trace.
- "κ thấp thì chắc chắn có người gán ẩu." — Chưa chắc. Khi hai người có tỉ lệ Pass khác nhau nhiều (A: 90%, B: 70%), κ bị chặn trên bởi κmax < 1 dù họ đồng ý tối đa có thể. Khi đó vấn đề là ngưỡng khác nhau (một người khắt khe hơn hẳn) — và đó là tín hiệu tốt để đem vào alignment session.
- "κ âm là lỗi tính toán." — Không. κ âm nghĩa là hai người trùng nhau ít hơn ngẫu nhiên — thường vì hiểu ngược một chữ trong rubric (vd. "Pass = có lỗi" vs "Pass = đạt").
A gán Pass 90/100 trace, B gán Pass 70/100. (a) Số trace đồng ý tối đa có thể là bao nhiêu? (b) Tính κmax. (c) Thực tế họ đồng ý 76 trace. κ là bao nhiêu và bạn diễn giải thế nào?
Xem lời giải
(a) Đồng ý tối đa = min(90, 70) Pass chung + min(10, 30) Fail chung = 70 + 10 = 80 → Po,max = 0.80.
(b) Pe = 0.9·0.7 + 0.1·0.3 = 0.63 + 0.03 = 0.66. κmax = (0.80 − 0.66)/(1 − 0.66) = 0.14/0.34 ≈ 0.41.
(c) κ = (0.76 − 0.66)/0.34 ≈ 0.29. Nhưng họ đã đạt 76/80 mức tối đa có thể! Vấn đề chính không phải đọc ẩu mà là ngưỡng khác nhau: B đánh Fail gấp ba A. Alignment session nên hỏi: "20 trace B đánh Fail mà A đánh Pass có điểm chung gì?" → thường ra một decision rule.
4.6Đọc điểm κ, đặt ngưỡng & đừng quên khoảng tin cậy
Sách khuyên nhắm κ ≥ 0.6 trước khi coi nhãn là đáng tin, nhưng ngưỡng thực tế tuỳ mục đích: ~0.5 là đủ để quyết định tách/gộp failure category; ≥ 0.6 để kiểm định LLM judge; ≥ 0.7 khi nhãn đi vào train model hoặc báo cáo tuân thủ. Với tiêu chí chủ quan, 0.6 rất khó; ~0.4 đã là tiến bộ thật.
Nếu người với người chỉ đạt κ 0.7, đừng mong LLM judge "chuẩn" hơn thế — chính nhãn so sánh đã nhiễu. Nhớ con số này khi chấm judge ở Chương 5.
Gán tiếp chỉ đẻ ra thêm dữ liệu mâu thuẫn. Quay lại đọc từng trace lệch; thường một cuộc trao đổi ngắn đủ để thêm một decision rule.
"Căn hộ có 3 phòng ngủ không" → dễ đồng ý. Giọng văn, persona, "phù hợp" → khó. Cách chữa hay dùng: tách tiêu chí thành các câu hỏi hẹp hơn.
Khoảng tin cậy: κ = 0.62 trên 40 trace thực ra là "khoảng 0.35 đến 0.85"
Phần này là bổ sung tự soạn (sách không đi sâu). Bộ trace chung thường chỉ 20–50 item, nên κ dao động rất mạnh. Cách đơn giản nhất để thấy độ bất định: bootstrap — lấy mẫu lại có hoàn lại các item, tính κ nhiều lần, lấy phân vị 2.5% và 97.5%.
import random
from collections import defaultdict
def bootstrap_ci(a, b, rounds=2000, seed=0):
"""Khoảng tin cậy 95% cho κ bằng cách lấy mẫu lại các item."""
rng, pairs, ks = random.Random(seed), list(zip(a, b)), []
for _ in range(rounds):
s = [rng.choice(pairs) for _ in pairs]
k = cohen_kappa([x for x, _ in s], [y for _, y in s])
if k == k: # bỏ NaN
ks.append(k)
ks.sort()
return ks[int(.025 * len(ks))], ks[int(.975 * len(ks))]
def per_criterion_report(rows):
"""rows: list of dict {trace, criterion, annotator, label} (2 người)."""
by = defaultdict(dict) # (criterion, trace) -> {annotator: label}
for r in rows:
by[(r["criterion"], r["trace"])][r["annotator"]] = r["label"]
for c in sorted({c for c, _ in by}):
pairs = [tuple(v.values()) for (cc, _), v in by.items() if cc == c and len(v) == 2]
a, b = [x for x, _ in pairs], [y for _, y in pairs]
lo, hi = bootstrap_ci(a, b)
po = sum(x == y for x, y in pairs) / len(pairs)
print(f"{c:<12} n={len(pairs):3d} %agree={po:.0%} "
f"kappa={cohen_kappa(a, b):.2f} CI95=[{lo:.2f}, {hi:.2f}]")
Chạy trên dữ liệu của ví dụ 4.3a (40 trace × 3 tiêu chí):
complete n= 40 %agree=82% kappa=0.62 CI95=[0.34, 0.85]
factual n= 40 %agree=95% kappa=0.87 CI95=[0.67, 1.00]
tone n= 40 %agree=68% kappa=0.32 CI95=[0.01, 0.60]
- n = 40 (22/4/3/11): CI95 ≈ [0.34, 0.85]. Có thể thật ra chỉ "fair", cũng có thể "almost perfect". Kết luận hợp lý: "chưa đủ bằng chứng là đạt 0.6".
- n = 200 (nhân 5 mọi ô: 110/20/15/55): cùng κ = 0.62 nhưng CI95 ≈ [0.50, 0.73]. Hẹp hơn rõ.
- Hệ quả thực hành: đừng ăn mừng khi vòng 2 tăng từ 0.55 lên 0.62 trên 40 trace — khác biệt đó nằm gọn trong nhiễu. Hãy nhìn xu hướng qua các vòng và nội dung các ca lệch (còn lệch vì cùng một lý do, hay các lý do đã được vá?).
| Tiêu chí | κ (CI95) | Dùng nhãn để… | Quyết định |
|---|---|---|---|
| Đúng số liệu | 0.87 (0.67–1.00) | kiểm định judge | Chốt. Chuyển sang gán quy mô lớn. |
| Đủ ý | 0.62 (0.34–0.85) | kiểm định judge | Thêm một vòng 40 trace mới, không cần họp dài. |
| Giọng điệu | 0.32 (0.01–0.60) | lặp rubric | Dừng gán. Alignment session riêng; cân nhắc tách tiêu chí. |
Và nhớ "trần": nếu sau cùng giọng điệu chỉ đạt κ ≈ 0.55, thì một LLM judge "khớp người 90%" trên tiêu chí này là điều đáng nghi, không phải đáng mừng.
Hiểu nhầm thường gặp
"Ngưỡng 0.6 là chuẩn vàng khoa học." — Thang Landis & Koch là quy ước, không phải định luật. Ngưỡng hợp lý phụ thuộc hậu quả của nhãn sai: nhãn để chọn việc sửa tuần này chịu được 0.5; nhãn đi vào báo cáo tuân thủ cần cao hơn và cần khoảng tin cậy đi kèm.Vòng 1: κ = 0.48 (n = 30). Vòng 2 (30 trace mới, rubric v2): κ = 0.58. PM hỏi: "Rubric v2 có tốt hơn không?" Bạn trả lời thế nào, và cần thêm bằng chứng gì?
Xem lời giải
Với n = 30, CI của mỗi κ rộng cỡ ±0.25, hai khoảng chồng lấn nhiều → chưa kết luận được từ con số. Bằng chứng tốt hơn: (1) các ca lệch ở vòng 2 có còn rơi vào đúng kiểu mơ hồ đã vá không (nếu không → v2 đã sửa được lỗ đó); (2) chạy thêm một vòng hoặc gộp n lớn hơn; (3) nhờ một người mới chỉ cầm rubric v2 gán 20 trace — nếu họ khớp tốt với nhóm, rubric đã "tự đứng được".
4.7Khi có > 2 người, thang thứ tự, hoặc thiếu nhãn
Cohen's κ giả định: đúng 2 người, nhãn phân loại, và cả hai gán mọi item. Lệch khỏi một trong ba giả định → đổi chỉ số.
Khi nào: đúng 2 người, nhãn phân loại (Pass/Fail), cả hai gán tất cả item.
Mặc định cho phần lớn tình huống: đơn giản, ai cũng hiểu. Code ở mục 4.5.
Khi nào: 3 người trở lên cùng gán các trace.
Mở rộng ý tưởng của Cohen: lấy trung bình mức đồng ý trên từng item (bao nhiêu cặp người gán trùng nhau), rồi trừ phần may rủi tính từ phân bố nhãn chung của mọi người. Cách đọc giống Cohen (> 0.6 là tốt).
Khi nào: nhãn có thứ tự, ví dụ thang 1–5.
Cohen thường coi "4 vs 5" tệ ngang "1 vs 5". Weighted κ dùng ma trận trọng số: lệch càng xa phạt càng nặng. Với quadratic weights, lệch 4 bậc bị phạt gấp 16 lần lệch 1 bậc.
Khi nào: không phải ai cũng gán mọi item (nghỉ ốm, chia việc), nhiều người, hoặc thang đo bất kỳ (nhị phân, thứ tự, khoảng, tỉ lệ).
α = 1 − Do/De: đo bất đồng thay vì đồng ý, chỉ xét các cặp nhãn thực sự tồn tại → xử lý thiếu dữ liệu tự nhiên.
Chuẩn khắt khe hơn κ: α ≥ 0.8 là tin cậy, 0.67–0.8 chỉ đủ cho kết luận tạm. Đừng hoảng nếu α ra thấp hơn κ bạn quen.
Lan, Minh, Tú gán 6 trace (số người nói Pass trong ngoặc):
| Trace | Lan | Minh | Tú | #Pass | Pi = tỉ lệ cặp trùng |
|---|---|---|---|---|---|
| 1 | P | P | P | 3 | 3 cặp/3 = 1 |
| 2 | P | P | P | 3 | 1 |
| 3 | P | P | F | 2 | 1 cặp/3 = 0.333 |
| 4 | P | F | F | 1 | 0.333 |
| 5 | F | F | F | 0 | 1 |
| 6 | P | P | P | 3 | 1 |
- Mỗi trace có 3 cặp người (Lan–Minh, Lan–Tú, Minh–Tú). Pi = số cặp trùng nhãn / 3.
- P̄o = (1 + 1 + 0.333 + 0.333 + 1 + 1)/6 = 4.667/6 ≈ 0.778.
- Tỉ lệ Pass chung trên 18 nhãn: 12/18 = 0.667 → P̄e = 0.667² + 0.333² ≈ 0.556.
- κFleiss = (0.778 − 0.556)/(1 − 0.556) = 0.222/0.444 = 0.50 → moderate.
Thang 1–5, 8 câu trả lời. Người gốc chấm: 5 4 4 3 2 5 1 3. Hai cặp khác nhau:
Cặp X (lệch 1 bậc): 4 4 3 3 2 5 2 4
Cặp Y (lệch 3–4 bậc): 1 4 1 3 2 5 5 5
- Cả hai cặp trùng đúng 4/8 = 50%. κ thường: X ≈ 0.36, Y ≈ 0.37 — gần như bằng nhau.
- Quadratic weighted κ: X ≈ 0.82 (các lệch đều chỉ 1 bậc — thực ra khá nhất quán), Y ≈ −0.27 (lệch cực đoan, như hai người dùng thang ngược nhau).
- Bài học kép: (1) nếu buộc phải dùng thang, κ thường che mất cấu trúc lệch; (2) cặp X gợi ý rằng nếu quy về nhị phân (vd. ≥ 4 = Pass) họ có thể đồng ý tốt — thêm một lý do để bắt đầu bằng Pass/Fail.
trace: 1 2 3 4 5 6 7 8
Lan: P P F F P – F P
Minh: P P F – F P F P
Tú: P – P F P P F P (– = không gán)
- Trong mỗi trace có m nhãn, mỗi cặp có thứ tự (m·(m−1) cặp) được tính trọng số 1/(m−1). Trace có 3 nhãn góp 3 "đơn vị"; trace có 2 nhãn góp 2.
- Cộng dồn ma trận coincidence: P–P = 11, F–F = 6, P–F = 2, F–P = 2 → tổng n = 21 giá trị ghép được (P: 13, F: 8).
- Do = (2 + 2)/21 ≈ 0.190. De = 2·13·8 / (21·20) = 208/420 ≈ 0.495.
- α = 1 − 0.190/0.495 ≈ 0.615 — theo chuẩn của Krippendorff, dưới 0.67 nên chưa đủ cho cả kết luận tạm. Cùng dữ liệu, cách đọc "κ-style" sẽ nói "substantial"; đó là lý do sách nhắc đừng hoảng khi α trông khắt khe hơn.
Code tự viết cho hai chỉ số trên (nhãn phân loại), đã chạy ra đúng 0.50 và 0.615:
from collections import Counter, defaultdict
from itertools import permutations
def fleiss_kappa(table):
"""table: mỗi phần tử là list nhãn của 1 item (mọi item cùng số người gán)."""
N, n = len(table), len(table[0])
cats = sorted({l for row in table for l in row})
# đồng ý trên từng item = tỉ lệ cặp người gán trùng nhãn
P_i = [sum(c * (c - 1) for c in Counter(row).values()) / (n * (n - 1)) for row in table]
P_bar = sum(P_i) / N
p_j = {c: sum(row.count(c) for row in table) / (N * n) for c in cats}
P_e = sum(p * p for p in p_j.values())
return (P_bar - P_e) / (1 - P_e)
def krippendorff_alpha_nominal(units):
"""units: list các list nhãn, None = người đó không gán item này."""
o = defaultdict(float) # ma trận coincidence
for u in units:
v = [x for x in u if x is not None]
m = len(v)
if m < 2:
continue # item chỉ có 1 nhãn: bỏ qua
for i, j in permutations(range(m), 2):
o[(v[i], v[j])] += 1 / (m - 1)
cats = sorted({c for c, _ in o})
n_c = {c: sum(o[(c, k)] for k in cats) for c in cats}
n = sum(n_c.values())
D_o = sum(o[(c, k)] for c in cats for k in cats if c != k) / n
D_e = sum(n_c[c] * n_c[k] for c in cats for k in cats if c != k) / (n * (n - 1))
return 1 - D_o / D_e
t = [list("PPP"), list("PPP"), list("PPF"), list("PFF"), list("FFF"), list("PPP")]
u = [list("PPP"), ["P", "P", None], list("FFP"), ["F", None, "F"],
list("PFP"), [None, "P", "P"], list("FFF"), list("PPP")]
print(round(fleiss_kappa(t), 3), round(krippendorff_alpha_nominal(u), 3)) # 0.5 0.615
Khi dùng thật, các thư viện như statsmodels (Fleiss) hay gói krippendorff trên PyPI tiện hơn; code trên để bạn hiểu cơ chế và kiểm chứng số liệu.
Hiểu nhầm thường gặp
"Có 3 người thì tính Cohen's κ cho từng cặp rồi lấy trung bình là xong." — Làm vậy vẫn có ích để chẩn đoán (xem ai lệch với ai — ví dụ một người khắt khe hơn hẳn hai người còn lại), nhưng con số báo cáo chính nên là Fleiss' κ hoặc α, vì chúng dùng một mô hình may rủi chung cho cả nhóm. Tốt nhất báo cả hai: Fleiss/α để kết luận, ma trận κ từng cặp để tìm nguyên nhân.Chọn chỉ số cho mỗi tình huống: (a) 2 PM gán Pass/Fail cho cùng 50 trace; (b) 4 chuyên gia chấm thang 1–5 "độ an toàn", mỗi trace chỉ 2 người chấm do chia ca; (c) 3 kỹ sư gán Pass/Fail đủ 40 trace; (d) 2 bác sĩ chấm mức độ khẩn cấp Thấp/Trung bình/Cao.
Xem lời giải
(a) Cohen's κ. (b) Krippendorff's α (thiếu nhãn + thang thứ tự; dùng hàm khoảng cách ordinal/interval). (c) Fleiss' κ (kèm κ từng cặp để chẩn đoán). (d) Weighted κ — ba mức có thứ tự, "Thấp vs Cao" tệ hơn "Thấp vs Trung bình"; nhưng cân nhắc xem có thể quy về câu hỏi nhị phân "có cần xử lý ngay không?" hay không.
4.8Đừng dùng κ để chấm LLM judge
Ý chính của sách
κ đo đồng thuận giữa hai người ngang hàng — không ai được coi là "đúng". Điều đó hợp lý khi hai người đang cùng phát triển rubric. Nhưng khi đã có nhãn gold, câu hỏi đổi thành "judge có ra đúng đáp án không?" — một bài toán phân loại. Bạn cần biết judge sai theo hướng nào: quá dễ dãi (lọt lỗi) hay quá khắt khe (báo động giả, tốn công review). κ không cho biết; TPR/TNR, precision/recall thì có (Chương 5).Trực giác: κ là một con số đối xứng — hoán đổi vai trò judge và gold, κ không đổi. Nhưng hậu quả của hai kiểu sai thì không đối xứng chút nào. Ví dụ dưới đây dựng hai judge có cùng κ mà tính cách trái ngược.
def judge_report(gold, pred, name):
"""gold/pred: list 'P'/'F'. Coi 'P' là lớp dương (positive)."""
tp = sum(g == "P" and p == "P" for g, p in zip(gold, pred))
fn = sum(g == "P" and p == "F" for g, p in zip(gold, pred))
fp = sum(g == "F" and p == "P" for g, p in zip(gold, pred))
tn = sum(g == "F" and p == "F" for g, p in zip(gold, pred))
tpr, tnr = tp / (tp + fn), tn / (tn + fp)
print(f"{name}: kappa={cohen_kappa(gold, pred):.2f} TPR={tpr:.1%} TNR={tnr:.1%} "
f"lọt lỗi={fp} báo động giả={fn}")
gold = ["P"] * 80 + ["F"] * 20
lenient = ["P"] * 78 + ["F"] * 2 + ["P"] * 10 + ["F"] * 10
strict = ["P"] * 66 + ["F"] * 14 + ["P"] * 3 + ["F"] * 17
judge_report(gold, lenient, "Judge L (dễ dãi)")
judge_report(gold, strict, "Judge S (khắt khe)")
# Judge L (dễ dãi): kappa=0.56 TPR=97.5% TNR=50.0% lọt lỗi=10 báo động giả=2
# Judge S (khắt khe): kappa=0.56 TPR=82.5% TNR=85.0% lọt lỗi=3 báo động giả=14
- Judge làm "cổng chặn" trong CI (Ch.9) — mục tiêu là không để lỗi lọt: Judge L lọt một nửa số lỗi → không dùng được, dù κ "moderate".
- Judge để lọc trace cho người review (Ch.10) — chấp nhận báo động giả để bắt lỗi: Judge S chỉ lọt 3/20 lỗi, đổi lại người review đọc thêm 14 trace tốt. Thường là đổi chác chấp nhận được.
- Judge để ước lượng tỉ lệ lỗi trên production: cả hai đều lệch, nhưng khi biết TPR/TNR bạn có thể hiệu chỉnh con số ước lượng (Chương 5 bàn kỹ). Với κ thì không làm được.
Tính trên 1.000 trace giả định. Thử: giữ TPR/TNR của Judge L, kéo tỉ lệ Pass lên 95% — κ đổi dù judge không hề đổi. Đó là thêm một lý do để báo TPR/TNR (thuộc tính của judge) thay vì κ (phụ thuộc cả phân bố dữ liệu).
Hiểu nhầm thường gặp
"Vậy κ không dùng được gì ở Chương 5." — Vẫn dùng: κ giữa người với người cho bạn trần kỳ vọng. Nếu người–người đạt κ 0.62 cho "đủ ý", một judge có TNR 98% trên tiêu chí này đáng nghi (có thể bộ gold quá dễ, hoặc rò rỉ ví dụ vào prompt). κ là để hiệu chuẩn kỳ vọng, TPR/TNR là để đo judge.Gold có 200 trace, 150 Pass / 50 Fail. Judge J nói Pass cho 140 trace gold-Pass và 20 trace gold-Fail. (a) Tính TPR, TNR. (b) Nếu dùng J làm cổng chặn, mỗi 1.000 request production (giả sử cùng tỉ lệ 25% Fail) có bao nhiêu lỗi lọt qua?
Xem lời giải
(a) TPR = 140/150 ≈ 93.3%; TNR = (50 − 20)/50 = 60%. (b) 1.000 request → 250 lỗi thật; J chỉ bắt 60% → 100 lỗi lọt qua, và chặn nhầm 750 × 6.7% ≈ 50 câu tốt. Nếu tính κ, bạn chỉ thấy một con số "moderate" chung chung — không thấy con số 100 lỗi lọt, thứ quyết định có triển khai hay không.
4.9Alignment session: sửa rubric, không phân xử nhãn
Trực giác: mỗi ca lệch là một bug report cho rubric. Buổi họp là buổi triage bug: tái hiện (mỗi người đã nghĩ gì), khoanh vùng (chữ nào trong rubric cho phép hiểu khác), và vá (thay đổi gì để lần sau không tái diễn). Không ai "thắng" một bug report.
Ca điển hình trong sách (trợ lý CRM bất động sản)
Tiêu chí: "câu trả lời hợp với persona khách". Rubric ban đầu chỉ nói đại ý: nhà đầu tư cần thông tin đầu tư, người mua lần đầu cần thông tin về quy trình mua; kèm một ví dụ Pass và một ví dụ Fail rõ ràng. Nghe hợp lý — nhưng không nói gì về câu trả lời pha trộn. Một trace: khách là nhà đầu tư hỏi về căn duplex; câu trả lời có cả tiền thuê hiện tại lẫn câu kiểu "hợp với gia đình, có sân sau". Ba người chấm ba kiểu:
Trong sách, tiêu chí "hợp persona" chỉ đạt κ khoảng 0.3–0.4 ở vòng đầu. Khi soi kỹ, người gán đồng ý về việc có thông tin hợp mục tiêu khách hay không, nhưng lệch về việc ngôn từ có đang nói với nhầm đối tượng không. Tách thành hai tiêu chí giúp thấy ngay phần nào cần sửa.
• Công bố κ từng tiêu chí, khoanh tiêu chí κ thấp để tập trung (vd. "chính xác: 0.74 — ổn; giọng theo persona: 0.38 — họp về cái này").
• Lập danh sách trace lệch: nhãn từng người + ghi chú.
• Chỉ định moderator trung lập — tốt nhất là người không gán nhãn (project lead, team kế bên).
① Mỗi người đã nghĩ gì? (nghe hết, không ngắt lời)
② Rubric hỏng ở chữ/khoảng trống nào?
③ Sửa gì để lần sau mọi người đồng ý?
timebox ~5 phút/ca; không xong thì gắn cờ, đi tiếp.
5 kỹ thuật sửa rubric
Thay chữ mơ hồ ("phù hợp", "tailored") bằng danh sách cụ thể theo từng loại khách.
Ca biên vừa cãi nhau → thành ví dụ Pass/Fail chính thức trong rubric.
"Nói tới tiện ích gia đình với nhà đầu tư → chỉ Pass nếu nối rõ với giá trị đầu tư (nhu cầu thuê, tỉ lệ trống thấp…)."
"Hợp persona" → "có thông tin đúng mục tiêu" + "không dùng giọng nhắm nhầm đối tượng". Đo κ riêng.
Có ca người hợp lý vẫn bất đồng (vd. chỉ nói "gần trường tốt" với nhà đầu tư) → ghi lại, đặt luật tie-break.
Pass: câu trả lời hợp với mục tiêu của khách.
Fail: câu trả lời lệch mục tiêu của khách.
→ Câu trả lời "pha trộn" thì sao? Một chi tiết đúng đã đủ chưa? Câu trả lời trung tính, chung chung thì sao?
Nhà đầu tư: cần số liệu tài chính / thị trường / độ ổn định người thuê. Mô tả đối tượng người thuê chỉ OK khi gắn với giá trị đầu tư.
Người mua lần đầu: khả năng chi trả, vay vốn, hướng dẫn quy trình.
+ ví dụ Pass/Fail cho ca biên vừa tranh luận.
Bot hỗ trợ học viên của một nền tảng học lập trình. Tiêu chí: "Giải thích phù hợp trình độ". κ vòng 1 = 0.35. Ba ca lệch tiêu biểu:
| Ca | Tình huống | Nhãn | Kỹ thuật sửa |
|---|---|---|---|
| #7 | Học viên tuần 1 hỏi về vòng lặp; bot giải thích đúng nhưng dùng từ "iterator protocol". | Hoa: F · Quân: P | Làm rõ từ ngữ: định nghĩa "phù hợp" = không dùng thuật ngữ chưa có trong các bài học viên đã học (hệ thống biết tiến độ), trừ khi giải thích ngay tại chỗ. |
| #12 | Học viên nâng cao hỏi; bot giải thích rất cơ bản, dài. | Hoa: P · Quân: F | Thêm decision rule: "giải thích quá cơ bản cho học viên đã qua bài đó = Fail nếu dài hơn 2 đoạn". |
| #19 | Bot vừa sai kiến thức vừa dùng ngôn ngữ dễ hiểu. | Hoa: F · Quân: P | Tách tiêu chí: tính đúng đắn đã có tiêu chí riêng — "phù hợp trình độ" chỉ xét cách diễn đạt. Hoa đang gộp hai tiêu chí. |
- Ghi vào decision log:
2026-09-12 · C-level · #7 → rule R1 (thuật ngữ ngoài syllabus) · đề xuất: Hoa · duyệt: moderator. - Thêm ca #7 và #12 làm ví dụ Pass/Fail chính thức vào rubric v2.
- Vòng 2 dùng 30 trace mới; kỳ vọng κ tăng vì hai nguồn lệch lớn nhất đã có luật. Nếu vẫn thấp, xem ca lệch mới có cùng kiểu không.
Tránh & xử lý bế tắc
Bỏ phiếu đa số giải quyết được một nhãn nhưng không dạy được gì về rubric — 2-1 là xong trace đó, nhưng không ai biết vì sao họ lệch. Chỉ dùng làm bước cuối để chốt nhãn khi rubric đã ổn. Đừng để buổi họp thành đấu khẩu; ghi lại mọi thay đổi rubric kèm lý do. Nếu vẫn bế tắc → escalate lên lead/chuyên gia cấp cao để phán quyết, rồi ghi phán quyết vào rubric. Bất đồng dai dẳng thường báo hiệu tiêu chí định nghĩa kém hoặc khác biệt giá trị giữa các bên — cả hai đều đáng được lộ ra.Lưu giữ mọi thứ: bộ gold và tài liệu dùng lại cho kiểm định evaluator tự động, fine-tune, audit khi bị chất vấn phương pháp, và để hiểu vì sao rubric trông như hiện tại khi cần sửa tiếp.
Hiểu nhầm thường gặp
"Sau buổi họp, sửa lại nhãn cũ cho khớp rubric mới là xong vòng." — Sửa nhãn cũ để làm bộ gold thì được (đó là bước chốt nhãn), nhưng không được đo lại κ trên chính những trace đã bàn: mọi người đã nghe đáp án. κ của rubric mới phải đo trên trace mới, gán độc lập.Tiêu chí "Không hứa hẹn quá mức" cho bot bán bảo hiểm. Ca lệch: bot nói "Gói này giúp bạn yên tâm tuyệt đối về chi phí y tế". An: Fail (hứa hẹn tuyệt đối). Bình: Pass (câu quảng cáo thông thường, không nêu số cụ thể). Viết một decision rule và một cặp ví dụ Pass/Fail để lần sau hai người đồng ý.
Xem lời giải
Decision rule: "Fail nếu câu trả lời dùng từ tuyệt đối (tuyệt đối, 100%, chắc chắn, không bao giờ, mọi chi phí) gắn với quyền lợi bảo hiểm, trừ khi trích nguyên văn điều khoản. Ngôn từ cảm xúc không tuyệt đối (yên tâm hơn, hỗ trợ tốt) → Pass."
Ví dụ Fail: "Gói này chi trả mọi chi phí nằm viện." (sai với điều khoản có giới hạn).
Ví dụ Pass: "Gói này giúp bạn yên tâm hơn: chi trả nội trú tới 200 triệu/năm theo điều khoản 4.2."
Ca gốc → Fail theo rule mới. Ghi vào decision log kèm tên người đề xuất.
4.10Cạm bẫy thường gặp (và cách phát hiện sớm)
- Bỏ qua gán độc lập. Bàn trước rồi mới gán → κ cao nhưng đo "groupthink", không đo độ rõ của rubric. Người mới chỉ cầm rubric có thể hiểu khác hẳn. (Không áp dụng nếu chỉ có một người gán.)
- Rubric đầu tiên quá sơ sài. Vòng đầu κ thấp, người gán nản. Đầu tư định nghĩa + vài ví dụ trước khi bắt đầu; tách chữ "phù hợp" thành các khía cạnh cụ thể (từ vựng, giọng, mức chi tiết).
- Chỉ báo % đồng ý. Ăn mừng 90% trong khi may rủi đã cho 80%. Luôn tính κ (hoặc chỉ số phù hợp).
- Lờ drift theo thời gian. Sau vài trăm trace, vì bối cảnh kinh doanh mới, vì mệt, hay đơn giản vì đã thấy nhiều ví dụ, người gán dễ dãi hoặc khắt khe dần. Định kỳ gán lại bộ neo 10–15 trace từ đầu dự án. Drift tự nó không xấu — xấu là không biết nó tồn tại.
- "Thông đồng" qua LLM (synthetic-annotator collusion). Hai người cùng nhờ một LLM chấm hộ → κ rất cao, nhưng đó là LLM nhất quán với chính nó chứ không phải người đồng ý về tiêu chí.
- Tranh luận nhãn cũ. Câu hỏi đúng là "sửa rubric thế nào thì lần sau ta đã đồng ý?", không phải "trace này lẽ ra Pass hay Fail?".
Bảng "triệu chứng → nghi vấn → kiểm tra" (tự soạn)
| Triệu chứng | Nghi vấn | Kiểm tra nhanh |
|---|---|---|
| κ vòng 1 > 0.9 trên tiêu chí chủ quan | Mất độc lập / collusion qua LLM | Hỏi thẳng quy trình; so ghi chú: câu chữ giống hệt nhau, cùng cấu trúc gạch đầu dòng là dấu hiệu LLM. |
| κ tốt nhưng người mới vào gán lệch nhiều | Rubric "sống" trong đầu nhóm cũ, không nằm trên giấy | Cho người mới gán 20 trace chỉ với rubric; ca lệch chỉ ra chỗ rubric thiếu. |
| Tỉ lệ Fail của một người giảm dần theo tuần | Drift về phía dễ dãi (mệt, quen) | Gán lại bộ neo; nếu lật một chiều → đọc lại rubric, cân nhắc giới hạn số trace/phiên. |
| Cùng một kiểu ca bị tranh luận ở mọi buổi họp | Tiêu chí gánh hai khái niệm, hoặc khác biệt giá trị | Thử tách tiêu chí; nếu vẫn lệch → escalate để có phán quyết. |
| κ âm hoặc gần 0 ở một cặp người | Hiểu ngược định nghĩa Pass/Fail | Nhìn ma trận 2×2: nếu đường chéo phụ lớn bất thường → kiểm tra xem có ai hiểu "Pass = có lỗi". |
Chị Lan gán 12 trace neo ở tuần 0 (7 Pass, 5 Fail). Tuần 8, sau ~600 trace, chị gán lại (không xem nhãn cũ):
- Đồng ý với chính mình 9/12 = 75%; Pe = (7/12)(10/12) + (5/12)(2/12) ≈ 0.556 → self-κ ≈ 0.44.
- Quan trọng hơn con số: cả 3 lần lật đều Fail → Pass, không có chiều ngược lại → drift một chiều về phía dễ dãi.
- Hành động: mở lại 3 trace, hỏi chị Lan tuần 8 đã dựa vào đâu để cho Pass. Nếu là do chính sách mới (vd. công ty nới quy định hoàn tiền) → cập nhật rubric và nhãn gold cũ. Nếu do "quen mắt" → đọc lại rubric trước mỗi phiên, giới hạn ~50 trace/phiên.
def drift_check(before, after, ids):
"""So nhãn của CÙNG một người trên bộ neo ở hai thời điểm."""
k = cohen_kappa(before, after)
f2p = [i for i, b, a in zip(ids, before, after) if b == "F" and a == "P"]
p2f = [i for i, b, a in zip(ids, before, after) if b == "P" and a == "F"]
print(f"self-kappa = {k:.2f} | Fail→Pass: {f2p} | Pass→Fail: {p2f}")
if len(f2p) >= 2 and not p2f:
print("⚠ lệch một chiều về phía DỄ DÃI — đọc lại rubric trước khi gán tiếp")
elif len(p2f) >= 2 and not f2p:
print("⚠ lệch một chiều về phía KHẮT KHE — kiểm tra xem tiêu chuẩn có vừa đổi?")
elif f2p or p2f:
print("ℹ lật hai chiều — nhiễu ngẫu nhiên, xem các ca này có phải ca biên")
ids = [f"A{i:02d}" for i in range(1, 13)]
week0 = list("PPFPFPFPPFPF")
week8 = list("PPPPFPPPPFPP")
drift_check(week0, week8, ids)
# self-kappa = 0.44 | Fail→Pass: ['A03', 'A07', 'A12'] | Pass→Fail: []
# ⚠ lệch một chiều về phía DỄ DÃI — đọc lại rubric trước khi gán tiếp
Hiểu nhầm thường gặp
"Dùng LLM hỗ trợ gán nhãn là cấm tuyệt đối." — Không hẳn. Vấn đề là đo đồng thuận khi cả hai người cùng dựa vào một LLM. Nếu team muốn dùng LLM để tăng tốc, hãy (1) giữ vòng đo κ hoàn toàn thủ công, hoặc (2) chỉ cho một người dùng và so với người không dùng, hoặc (3) coi LLM là "người gán thứ ba" riêng và đo κ của nó như một thành viên — rồi chuyển sang TPR/TNR khi đã có gold.Ở bộ neo 15 trace, anh Minh lật 4 trace: 2 F→P và 2 P→F. Chị Lan lật 3 trace, cả 3 đều P→F, đúng tuần sau khi công ty ban hành quy định mới về quảng cáo tài chính. Diễn giải từng trường hợp.
Xem lời giải
Minh: lật hai chiều cân bằng → nhiều khả năng nhiễu ngẫu nhiên trên ca biên; kiểm tra 4 trace có phải ca biên không, nếu phải thì thêm ví dụ vào rubric. Lan: lật một chiều về khắt khe, trùng thời điểm có chính sách mới → đây là drift có lý do chính đáng. Hành động: cập nhật rubric thành văn theo chính sách mới, thông báo cả team, và gán lại các nhãn gold liên quan — nếu không, gold cũ sẽ dạy LLM judge chuẩn đã lỗi thời.
4.11Case study: team VíXanh đi từ κ 0.27 tới bộ gold
Bối cảnh. VíXanh là một ví điện tử giả tưởng; bot CSKH trả lời trong app. Sau error analysis (Ch.3) trên 150 trace, ba failure mode nổi bật: (1) nói sai phí/hạn mức/chính sách; (2) đẩy khách sang tổng đài dù bot tự giải quyết được; (3) giọng điệu lệch khi khách vừa mất tiền. Team muốn biến ba điều này thành rubric để sau đó xây LLM judge.
Vì sao không dùng một người phán? Chị Lan (trưởng CSKH) nắm chính sách tốt nhất, nhưng tiêu chí giọng điệu đang bị CSKH và Marketing tranh cãi, và bộ phận quản trị rủi ro yêu cầu quy trình có số liệu đồng thuận. → Quy trình cộng tác.
Đội hình. Lan (trưởng CSKH), Minh (PM), Tú (kỹ sư ML) gán nhãn. Hà (EM team thanh toán, không gán) làm moderator.
Rubric v1 (bước 2)
Vòng 1 — 40 trace (14 dễ · 16 biên · 10 lỗi đã biết), gán độc lập
| Tiêu chí | Fleiss' κ | κ từng cặp (Lan–Minh / Lan–Tú / Minh–Tú) | Số trace có lệch |
|---|---|---|---|
| C1 Đúng chính sách | 0.79 | 0.81 / 0.77 / 0.80 | 5 |
| C2 Giải quyết được việc | 0.48 | 0.44 / 0.52 / 0.49 | 10 |
| C3 Giọng điệu | 0.27 | 0.35 / 0.18 / 0.29 | 14 |
Hà đọc 14 ca lệch của C3 và gom nhóm theo ghi chú: 6 ca bot xin lỗi 3–4 lần liền (Tú: Fail vì sáo rỗng; Lan: Pass vì "xin lỗi không bao giờ thừa"); 5 ca bot dùng "nha bạn 😊" khi khách báo mất tiền (Lan: Fail; Minh: Pass vì đúng giọng thương hiệu); 3 ca trả lời cộc lốc với khách đang giận. Ở C2, 7/10 ca lệch xoay quanh câu "vui lòng liên hệ tổng đài".
Alignment session 1 (60′, C3) và 2 (30′, C2)
- Nhận ra C3 gánh hai khái niệm: "có ghi nhận vấn đề của khách" và "có dùng giọng suồng sã ở tình huống nhạy cảm". Lan và Minh không bất đồng về đồng cảm — họ bất đồng về emoji. → Tách tiêu chí thành C3a và C3b.
- Decision rule cho C3a: lời xin lỗi lặp lại không được tính thêm; điều được tính là một câu ghi nhận vấn đề.
- Decision rule cho C3b: Marketing muốn giọng thân thiện, CSKH lo bị coi là coi nhẹ khách. Không đồng thuận sau 5 phút → escalate lên Head of CX. Phán quyết: cấm emoji và từ đệm suồng sã khi chủ đề liên quan tới mất tiền, gian lận hoặc khiếu nại; các chủ đề khác vẫn được.
- Decision rule cho C2 (buổi 2): chuyển tổng đài chỉ Pass khi (a) yêu cầu vượt quyền bot và (b) có kênh cụ thể + thông tin cần chuẩn bị.
- Thư ký ghi 4 mục vào decision log, thêm 6 ví dụ Pass/Fail lấy từ chính các ca lệch.
Rubric v2 (trích phần đã đổi)
Vòng 2 — 30 trace mới
C1 0.82 · C2 0.62 · C3a 0.58 · C3b 0.84. C3a vẫn dưới ngưỡng: 5 ca lệch đều là câu ghi nhận chung chung kiểu "Mình hiểu cảm giác của bạn". Buổi mini 20′ thêm luật: câu ghi nhận phải nhắc lại vấn đề cụ thể (vd. "khoản 500.000đ bị trừ hai lần") → rubric v3.
Vòng 3 — 30 trace mới
C1 0.80 · C2 0.64 · C3a 0.72 · C3b 0.86 → mọi tiêu chí ≥ 0.6. Chốt.
Chốt (bước 9)
- Bộ gold 100 trace (40 + 30 + 30). Với trace còn lệch, cả nhóm gán lại theo rubric v3 qua thảo luận; chỉ 4 trace phải chốt bằng đa số, 2 trace escalate lên chị Lan. Mỗi trace gold lưu: nhãn, phiên bản rubric, người chốt, ghi chú.
- Bộ neo: 12 trace từ vòng 1 (đủ Pass/Fail cho mọi tiêu chí), gán lại mỗi tháng.
- Chi phí (ước lượng): gán 100 trace × 3 người × ~2 phút ≈ 10 giờ; ba buổi họp (60′ + 30′ + 20′) × 4 người ≈ 7.3 giờ; chuẩn bị, tổng hợp ≈ 3 giờ → ~20 giờ công.
- Kỳ vọng cho Ch.5: trần của C2 ≈ 0.64 → một judge C2 "khớp gold 97%" sẽ bị soi kỹ xem có rò rỉ ví dụ không.
Bài học rút ra
- Con số κ thấp nhất (C3) không phải do người gán kém — do rubric gộp hai khái niệm.
- Một bất đồng thực chất là khác biệt giá trị (Marketing vs CSKH) và chỉ giải được bằng escalate — rubric giờ ghi lại phán quyết đó cho mọi người sau.
- Mỗi vòng dùng trace mới; không ai được "học thuộc" đáp án.
- Ghi chú của người gán (cột note) là nguyên liệu chính để Hà gom nhóm ca lệch trong 20 phút.
Tự làm người gán nhãn thứ ba
Dưới đây là 8 trace bịa của bot VíXanh. Hãy gán C2 · Giải quyết được việc theo rubric v2 ở trên, rồi so với hai người gán mô phỏng: Lan (khắt khe với câu trả lời chung chung nhưng quen tay chấp nhận chuyển tổng đài) và Minh (dễ dãi). Lab tính κ từng cặp và cho bạn xem nhãn theo rubric v2 kèm lý do.
Trong case study, giả sử vòng 3 cho C3a = 0.52 thay vì 0.72, và 6 ca lệch không có điểm chung rõ ràng. Hạn ra mắt LLM judge là tuần sau. Bạn đề xuất gì?
Xem lời giải
Ca lệch không có mẫu chung → phần mơ hồ còn lại có thể là không khử được. Đề xuất: (1) chốt C1, C2, C3b và đưa vào judge ngay; (2) với C3a, ghi nhận mơ hồ + đặt luật tie-break (vd. "nếu có câu nhắc lại vấn đề dù ngắn → Pass"), dùng nhãn C3a ở mức "tham khảo" (ngưỡng ~0.5 đủ cho lặp rubric, chưa đủ cho kiểm định judge); (3) không dùng judge C3a làm cổng chặn trong CI cho tới khi κ người–người ≥ 0.6; (4) lên lịch một vòng nữa với người gán mới để kiểm tra rubric có "tự đứng được" không.
4.12Kết quả cần có & nối sang các chương khác
Định nghĩa + ví dụ cụ thể cho từng failure mode, đủ rõ để người mới dùng không cần kèm.
Nhãn đồng thuận — ground truth để kiểm định LLM judge ở Chương 5.
Các ca biên và quyết định đã chọn, để lý lẽ không mất khi team đổi người.
Sách tóm lại: domain hẹp, tiêu chí rõ, chuyên gia có thời gian → benevolent dictator (nhanh, gọn); chủ quan, phức tạp hoặc quy mô lớn → quy trình cộng tác. Rubric và bộ gold là cây cầu giữa phán đoán con người và đo lường tự động.
Mẫu lưu trữ tối thiểu (tự soạn)
# gold.jsonl — một dòng / (trace, tiêu chí)
{"trace_id": "T0412", "criterion": "C2", "label": "F", "rubric_version": "v3",
"decided_by": "consensus", "annotators": {"Lan": "F", "Minh": "P", "Tu": "F"},
"note": "chuyển tổng đài khi bot tự huỷ được gói trả góp (<24h)"}
# decision_log.md — một dòng / thay đổi rubric
2026-09-12 | C3 → tách C3a/C3b | lý do: lệch emoji vs đồng cảm | đề xuất: Hà | duyệt: Head of CX
2026-09-19 | C3a + luật "nhắc lại vấn đề cụ thể" | lý do: 5 ca "mình hiểu cảm giác" | đề xuất: Tú
Trường annotators giữ nguyên nhãn gốc của từng người — sau này muốn tính lại κ hay nghiên cứu vì sao lệch vẫn làm được. Trường rubric_version cho phép gán lại có chọn lọc khi rubric đổi.
| Chương | Nối với chương 4 thế nào |
|---|---|
| Ch.3 Error analysis | Failure mode từ open/axial coding → rubric nháp (bước 2); trace bị ghi chú khác nhau → nguồn ca biên (bước 3). |
| Ch.5 Evaluator tự động | Rubric → prompt judge; bộ gold → đo TPR/TNR; κ người–người → trần kỳ vọng. |
| Ch.9 CI/CD | Chỉ tiêu chí đã đạt κ đủ cao mới nên thành cổng chặn tự động. |
| Ch.10 Giao diện review | Công cụ gán nhãn nên hỗ trợ: ẩn nhãn người khác, bắt ghi chú khi Fail, bộ neo định kỳ. |
4.13Áp dụng cho dự án model-routing
Hệ thống của bạn gửi mỗi request LLM tới một model phù hợp (nhỏ / vừa / lớn), cân bằng chất lượng – chi phí – độ trễ. Chi phí và độ trễ đo tự động được; chất lượng thì cần nhãn người. Câu hỏi cốt lõi của eval: "route này có chấp nhận được không?" — tức câu trả lời của model được chọn có đủ tốt cho request này không. Chương 4 giúp bạn biến câu hỏi mơ hồ đó thành nhãn đáng tin.
1. Ai là người gán?
Nếu chỉ mình bạn quyết "đạt/không đạt" → bạn là benevolent dictator (và như sách nhắc, hãy trung thực về giới hạn hiểu biết — với request về y khoa hay pháp lý, bạn không phải chuyên gia). Cách làm thực tế: bạn là người phán chính, nhưng chạy một vòng hiệu chuẩn với một đồng nghiệp trên 40 request để kiểm tra rubric có đứng được khi rời khỏi đầu bạn không — vì cuối cùng một LLM judge sẽ phải áp dụng nó.
2. Rubric nháp v0 cho "route chấp nhận được"
40 request lấy mẫu phân tầng (14 tier nhỏ, 14 vừa, 12 lớn; dồn các request mà router phân vân). Bạn và đồng nghiệp gán độc lập trên sheet đã ẩn tên model.
| Tiêu chí | % đồng ý | κ | Đọc |
|---|---|---|---|
| R1 Đúng | 92.5% | 0.78 | ổn |
| R2 Đủ yêu cầu | 77.5% | 0.42 | cần họp |
| R3 Format | 97.5% | 0.90 | ổn |
| ACCEPTABLE (AND) | 77.5% | 0.55 | bị R2 kéo xuống |
- Nếu chỉ báo một con số "acceptable κ = 0.55", bạn biết là chưa ổn nhưng không biết sửa chỗ nào. Bảng từng tiêu chí chỉ ra ngay: R1 và R3 đã tốt, R2 là thủ phạm. (Tỉ lệ acceptable trong mẫu chỉ ~55% vì ta cố tình dồn ca router phân vân — production sẽ cao hơn nhiều.)
- 9 ca lệch R2: 6 ca là prompt nhiều ý viết liền một đoạn (một người đếm 3 yêu cầu, người kia đếm 2); 3 ca là yêu cầu ngầm ("viết email cho sếp" → có cần giọng trang trọng không?).
- Sửa rubric: thêm bước liệt kê yêu cầu tường minh trước khi đọc câu trả lời (đã đưa vào v0 ở trên), và luật "yêu cầu ngầm chỉ tính khi mục đích được nêu".
- Vòng 2 với 40 request mới; mục tiêu R2 ≥ 0.6. Khi đạt, bộ 80 request (sau khi chốt nhãn) thành hạt nhân gold routing set.
- Bạn gán 20 câu trả lời của model nhỏ khi thấy tên model: 12/20 acceptable.
- Một tuần sau, cùng 20 câu, sheet đã ẩn tên và trộn với câu của model lớn: 15/20 acceptable.
- 3 câu lật đều theo hướng "biết là model rẻ → khắt khe hơn". Đây là bias kỳ vọng: nhãn đang đo định kiến của bạn về model, không đo câu trả lời. Chiều ngược lại cũng xảy ra (dễ dãi với model rẻ vì "rẻ thế là được rồi") — dù chiều nào, router sẽ được tối ưu theo định kiến đó.
3. Code tự viết: tạo sheet ẩn danh & báo cáo κ từng tiêu chí
import csv, random, hashlib
from collections import defaultdict
CRITERIA = ["correct", "covers_ask", "format_ok"] # + 'acceptable' = AND của 3 cái
def make_blind_sheet(logs, n=40, seed=7, path="sheet.csv", key_path="key.csv"):
"""logs: dict {req_id, tier, category, prompt, answer, model}.
Chọn mẫu phân tầng theo (tier, category), ẨN tên model, xáo thứ tự."""
rng = random.Random(seed)
strata = defaultdict(list)
for r in logs:
strata[(r["tier"], r["category"])].append(r)
picked, keys = [], list(strata)
while len(picked) < n and any(strata.values()):
for k in keys:
if strata[k] and len(picked) < n:
picked.append(strata[k].pop(rng.randrange(len(strata[k]))))
rng.shuffle(picked)
with open(path, "w", newline="") as f, open(key_path, "w", newline="") as g:
w, wk = csv.writer(f), csv.writer(g)
w.writerow(["item", "prompt", "answer"] + CRITERIA + ["note"])
wk.writerow(["item", "req_id", "tier", "model"])
for r in picked:
item = hashlib.sha1(r["req_id"].encode()).hexdigest()[:8] # id mờ
w.writerow([item, r["prompt"], r["answer"]] + [""] * (len(CRITERIA) + 1))
wk.writerow([item, r["req_id"], r["tier"], r["model"]])
return path, key_path
def load(path):
with open(path) as f:
return {row["item"]: row for row in csv.DictReader(f)}
def report(sheet_a, sheet_b, key_path):
"""Hai người điền P/F vào bản sao của sheet; key chỉ mở SAU khi gán xong."""
A, B, K = load(sheet_a), load(sheet_b), load(key_path)
items = sorted(set(A) & set(B))
for c in CRITERIA + ["acceptable"]:
get = (lambda s, i: s[i][c]) if c != "acceptable" else \
(lambda s, i: "P" if all(s[i][x] == "P" for x in CRITERIA) else "F")
a, b = [get(A, i) for i in items], [get(B, i) for i in items]
po = sum(x == y for x, y in zip(a, b)) / len(items)
print(f"{c:<11} %agree={po:.0%} kappa={cohen_kappa(a, b):.2f}")
print("\nCa lệch 'covers_ask' (đem vào alignment session):")
for i in items:
if A[i]["covers_ask"] != B[i]["covers_ask"]:
print(f" {i} tier={K[i]['tier']:<6} A={A[i]['covers_ask']} B={B[i]['covers_ask']}"
f" noteA={A[i]['note']!r}")
Đã chạy thử với log giả lập 500 request và hai sheet điền tự động; đầu ra có dạng covers_ask %agree=88% kappa=0.66 kèm danh sách item lệch và tier tương ứng — tier chỉ hiện ra sau khi gán xong, qua file key.
4. Cạm bẫy riêng của eval routing
- Thấy tên model khi gán. Bias kỳ vọng làm nhãn đo định kiến thay vì chất lượng. Luôn ẩn tên + trộn tier.
- Chỉ báo % đồng ý. Router tốt → phần lớn route acceptable (85–95%) → đúng vùng mà % đồng ý vô nghĩa (LAB 1). Báo κ và dồn mẫu các ca router phân vân.
- Collusion qua LLM trong pool. Nếu cả hai người dùng cùng một LLM gợi ý nhãn — và LLM đó lại là một model trong pool routing — κ đẹp giả, và nhãn nghiêng về phong cách của chính model đó.
- Drift "dễ tính dần với model rẻ". Sau vài trăm câu của model nhỏ, ngưỡng "chấp nhận được" tụt dần. Giữ bộ neo 10–15 request gán lại mỗi 2–3 tuần; dùng
drift_checkở mục 4.10. - Trộn chi phí/độ trễ vào nhãn chất lượng. "Câu này ổn nhưng model lớn thì phí" — không thuộc rubric. Nhãn người chỉ đo chất lượng; đánh đổi được tính ở tầng phân tích.
- Một κ cho mọi loại request. Tách báo cáo theo loại (code, trích xuất, chat…) — "đủ yêu cầu" với code và với chat có thể cần luật khác nhau.
5. Nhãn thứ hai: "regret" — model lớn có tốt hơn đáng kể không?
"Acceptable" trả lời "route này có hỏng không". Để chỉnh ngưỡng router, bạn còn muốn biết "route rẻ có làm người dùng thiệt không". Gợi ý (tự soạn): với một mẫu request đã route về tier nhỏ, chạy thêm model lớn, cho người gán xem hai câu trả lời thứ tự ngẫu nhiên, không tên, hỏi: "Có câu nào tốt hơn đáng kể đến mức người dùng sẽ nhận ra không? (A / B / Ngang nhau)". Đây là nhãn 3 lớp → vẫn dùng Cohen's κ (hoặc Fleiss nếu ≥ 3 người). Định nghĩa "đáng kể" sẽ là tiêu chí khó đồng thuận nhất — chuẩn bị tinh thần cho vài vòng alignment và các decision rule kiểu "chỉ tính khi khác biệt làm thay đổi hành động của người dùng".
Checklist áp dụng ngay
- Viết rubric v0 (R1–R3) thành văn, commit vào repo cạnh code eval.
- Tạo sheet ẩn danh 40 request, phân tầng tier × loại, dồn ca router phân vân.
- Nhờ một người gán độc lập; tính κ từng tiêu chí + bootstrap CI.
- Họp 60′ cho tiêu chí thấp nhất; ghi decision log; vòng 2 trên request mới.
- Chốt gold routing set + bộ neo 12 request; dùng κ người–người làm trần khi chấm judge ở Ch.5.
Prompt: "Tóm tắt email này thành 3 gạch đầu dòng bằng tiếng Anh, và gợi ý một câu trả lời ngắn." Model nhỏ trả về 4 gạch đầu dòng tiếng Anh, nội dung đúng, và một câu trả lời gợi ý dài 5 câu. Gán R1, R2, R3 và ACCEPTABLE theo rubric v0. Chỗ nào có thể gây lệch giữa hai người gán, và bạn thêm luật gì?
Xem lời giải
Liệt kê yêu cầu tường minh trước: (1) tóm tắt, (2) đúng 3 gạch đầu dòng, (3) tiếng Anh, (4) gợi ý một câu trả lời, (5) "ngắn". R1 = Pass (nội dung đúng). R2 = Pass nếu coi (4) đã đáp ứng — có câu trả lời gợi ý. R3 = Fail: 4 gạch thay vì 3 là vi phạm ràng buộc định dạng. ACCEPTABLE = Fail.
Chỗ dễ lệch: "ngắn" thuộc R2 hay R3, và 5 câu có còn "ngắn" không. Luật đề xuất: "ràng buộc về số lượng/độ dài thuộc R3; từ định tính như 'ngắn' → Fail nếu vượt 2× mức thông thường của loại nội dung (câu trả lời email ngắn: ≤ 2 câu)". Ghi vào decision log với chính ví dụ này.