Phần II · Analyze Chương 4

Đánh giá cộng tác: làm cho con người đồng ý về "thế nào là lỗi"

Chapter 4 — Collaborative Evaluation Practices

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:

🎭
Chủ quan
subjective criteria

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.

🔁
Không nhất quán
intra-rater noise

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.

🌊
Criteria drift
evolving standards

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

Nhãn Pass/Fail do người gán Đếm failure mode xếp ưu tiên (Ch.3) LLM judge kiểm định (Ch.5) Quyết định sửa roadmap, CI (Ch.9) người A ≠ người B thứ hạng lỗi bị đảo judge học sai chuẩn sửa nhầm chỗ Nhiễu sinh ra ở ô đầu tiên không biến mất — nó được "khuếch đại" ở mọi bước sau.
Vì sao chương này đứng trước chương về evaluator tự động: không có nhãn đáng tin thì không có gì để so.
Ví dụ 4.1a · Một câu trả lời, ba phán đoán (tự dựng)

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

  1. Người A (PM) — Pass: trả lời đúng câu hỏi, có giá, có lựa chọn.
  2. 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.
  3. 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ừ.
  4. 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.
Ví dụ 4.1b · Tự đo độ nhất quán của chính mình (intra-rater)

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: PassTuần 2: Fail
Tuần 1: Pass102
Tuần 1: Fail26
  1. Đồng ý với chính mình: (10 + 6)/20 = 80%.
  2. Tỉ lệ Pass hai lần đều là 60% → đồng ý ăn may Pe = 0.6² + 0.4² = 0.52.
  3. "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.
  4. 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).
Ví dụ 4.1c · Nhiễu nhãn đảo thứ tự ưu tiên sửa lỗi

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%).

  1. Tỉ lệ "sai giá" đo được ≈ 0.08·0.9 + 0.92·0.01 = 7.2% + 0.9% ≈ 8.1%.
  2. Tỉ lệ "giọng điệu" đo được ≈ 0.10·0.6 + 0.90·0.02 = 6% + 1.8% = 7.8%.
  3. 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.
Bài tập 4.1

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
  1. 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.
  2. 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.
  3. 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".
  4. 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.

Cần nhãn Pass/Fail làm ground truth Có đủ cả 3 điều kiện? ① một người cả team tin tưởng ② tiêu chí đúng chuyên môn họ ③ khối lượng vừa sức một người Không Benevolent dictator 1 người = nguồn sự thật nhanh · gọn · không xung đột Quy trình cộng tác ≥ 2 người · đo κ · căn chỉnh domain rộng / tiêu chí tranh cãi
Yếu tố quyết định không phải số nhân viên, mà là có giao được quyền phán quyết cho một người hay không.
① Người được tin tưở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.

② Đúng chuyên mô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.

③ Vừa sức

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 AIAi có thể là "benevolent dictator"
Trợ lý sức khoẻ tinh thầnNhà 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àngGiám đốc CSKH
Công cụ giáo dụcGiá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).

Ví dụ 4.2 · Chấm ba tình huống theo 3 điều kiện (tự dựng)
Tình huốngKết luận
App ăn kiêng cho người tiểu đường, 1 chuyên gia dinh dưỡng, ~40 trace/tuầnBenevolent 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.
  1. Đi lần lượt từng điều kiện, đánh ✓/✗/~ (một phần).
  2. 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.
  3. "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ọ.
Bài tập 4.2

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.

CHUẨN BỊ ĐO & CĂN CHỈNH (lặp) CHỐT 1 · Lập team (≥ 2 người) 2 · Rubric nháp + ví dụ P/F 3 · 20–50 trace, cài ca biên 4 · Gán nhãn ĐỘC LẬP 5 · Đo κ từng tiêu chí 6 · Alignment session 7 · Sửa rubric rõ hơn mỗi vòng 8 · Lặp lại trace mới tới khi κ ≳ 0.6 κ đạt ✓ 9 · Chốt rubric cuối + bộ nhãn gold → Chương 5 Gán nhiều tiêu chí trong một lượt đọc được — nhưng đo κ và họp căn chỉnh TỪNG tiêu chí riêng.
Ba pha: chuẩn bị → vòng lặp đo/căn chỉnh → chốt bộ gold. Vòng giữa là nơi rubric thực sự tốt lên.
  1. 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.
  2. 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.
  3. 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.
  4. 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ồ.
  5. Đ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.
  6. 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).
  7. 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.
  8. 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.
  9. 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.

Bộ 40 trace chung (gợi ý tự dựng) 14 ca dễ 16 ca biên / mơ hồ 10 lỗi đã biết kiểm tra không ai "khó tính vô cớ" nơi rubric thực sự bị thử: judge phân vân, trace bị tranh cãi từ error analysis Ch.3: ai cũng phải bắt được Trộn thứ tự trước khi phát — người gán không được biết trace thuộc nhóm nào.
Ca biên chiếm tỉ lệ lớn nhất có chủ đích: κ trên bộ này sẽ thấp hơn κ "thật" trên production — đó là tính năng, không phải lỗi.

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 đohọ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".

Ví dụ 4.3a · Một con số κ gộp che giấu tiêu chí hỏ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-PP-FF-PF-F% đồng ýκ
Đúng sự thật28111095%0.87
Đủ ý22431182.5%0.62
Giọng điệu1876967.5%0.32
Gộp 120 nhãn6812103081.7%0.59
  1. Nhìn con số gộp 0.59: "gần 0.6, thêm một vòng là xong".
  2. 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.
  3. 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.
Ví dụ 4.3b · Phiếu gán nhãn tối thiểu & ngân sách thời gian

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)
  1. 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.
  2. 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.
  3. 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.
Bài tập 4.3

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

Đồng ý do may rủi Pe = p² + (1−p)² 50%60%70% 80%90%100% 50%60%70% 80%90%100% Tỉ lệ trace được gán Pass (hệ thống càng tốt → càng lệch phải) Po = 85% bạn đo được Pass 50% → chance 50%: 85% là rất tốt Pass 90% → chance 82% 85% chỉ hơn "ăn may" 3 điểm
Nghịch lý: hệ thống càng được sửa tốt, nhãn càng dồn về Pass, và % đồng ý càng vô nghĩa.
Ví dụ 4.4 · Cùng Po = 85%, bốn giai đoạn của một sản phẩm

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ạnp (Pass)Pe = p² + (1−p)²κ = (0.85 − Pe)/(1 − Pe)Đọc
Prototype50%0.500.70substantial
Beta70%0.580.64substantial
Ra mắt80%0.680.53moderate
Trưởng thành90%0.820.17slight
  1. 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ì.
  2. 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.
  3. 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.
▶ LAB 1 · Nghịch lý tỉ lệ Pass🎮

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.

Bài tập 4.4

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: PassB: FailTổng A
A: Pass70878
A: Fail71522
Tổng B7723100
  1. Po — đồng ý quan sátĐường chéo: (70 + 15) / 100 = 0.85.
  2. 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.
  3. κ(0.85 − 0.651) / (1 − 0.651) = 0.199 / 0.349 ≈ 0.57 → chỉ "moderate", dù 85% nghe rất đẹp.
Po = 0.85 — đồng ý quan sát được Pe ≈ 0.65 · "ăn may" cũng có 0.20 giành được 0.15 còn lệch 0 1 1 − Pe ≈ 0.35: phần có thể vượt may rủi κ = xanh / (xanh + đỏ) = 0.20 / 0.35 ≈ 0.57
Kappa bỏ qua phần xám (ai cũng đạt được nhờ tỉ lệ nhãn), chỉ chấm trên phần còn lại.
▶ LAB 2 · Máy tính κ từ ma trận 2×2🎮
B: PassB: Fail A: Pass A: Fail

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

Ví dụ 4.5 · Tính κ từ danh sách nhãn thô (10 trace)

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
  1. Đế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.
  2. 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.
  3. κ = (0.80 − 0.58)/(1 − 0.58) = 0.22/0.42 ≈ 0.52.
  4. 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").
Bài tập 4.5

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

00.20.4 0.60.81 SlightFairModerate SubstantialAlmost perfect ≥ 0.7 · train model / báo cáo tuân thủ ≥ 0.6 · kiểm định LLM judge ~0.5 · đủ để lặp rubric κ < 0 → bất đồng có hệ thống tiêu chí chủ quan: ~0.4 đã là tiến bộ
Thang Landis & Koch (1977). Ngưỡng cần đạt phụ thuộc vào việc nhãn sẽ được dùng để làm gì.

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.

IAA ≈ trần của evaluator tự động

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.

κ thấp → dừng gán thêm

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ó tiêu chí khó đồng thuận hơn

"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]
Ví dụ 4.6a · Cùng κ = 0.62, khác n
  1. 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".
  2. 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õ.
  3. 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òngnộ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á?).
Ví dụ 4.6b · Từ bảng κ tới quyết định
Tiêu chíκ (CI95)Dùng nhãn để…Quyết định
Đúng số liệu0.87 (0.67–1.00)kiểm định judgeChốt. Chuyển sang gán quy mô lớn.
Đủ ý0.62 (0.34–0.85)kiểm định judgeThêm một vòng 40 trace mới, không cần họp dài.
Giọng điệu0.32 (0.01–0.60)lặp rubricDừ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.
Bài tập 4.6

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

Có item bị thiếu nhãn? không phải ai cũng gán mọi trace Nhãn có thứ tự (1–5)? lệch 1 bậc ≠ lệch 4 bậc Bao nhiêu người gán? nhãn phân loại, đủ mọi item Không Không Krippendorff's α mọi số người, mọi thang · α ≥ 0.8 Weighted κ (2 người) ≥ 3 người: α với thang ordinal 2 người → Cohen's κ ≥ 3 người → Fleiss' κ
Cây tự vẽ tóm lại khuyến nghị của sách. Cohen's κ vẫn là mặc định cho phần lớn tình huống: 2 người, Pass/Fail, gán đủ.

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.

Ví dụ 4.7a · Fleiss' κ tính tay: 3 người × 6 trace

Lan, Minh, Tú gán 6 trace (số người nói Pass trong ngoặc):

TraceLanMinh#PassPi = tỉ lệ cặp trùng
1PPP33 cặp/3 = 1
2PPP31
3PPF21 cặp/3 = 0.333
4PFF10.333
5FFF01
6PPP31
  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.
  2. o = (1 + 1 + 0.333 + 0.333 + 1 + 1)/6 = 4.667/6 ≈ 0.778.
  3. Tỉ lệ Pass chung trên 18 nhãn: 12/18 = 0.667 → P̄e = 0.667² + 0.333² ≈ 0.556.
  4. κFleiss = (0.778 − 0.556)/(1 − 0.556) = 0.222/0.444 = 0.50 → moderate.
Ví dụ 4.7b · Weighted κ: cùng 50% trùng, hai câu chuyện trái ngược

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
  1. Cả hai cặp trùng đúng 4/8 = 50%. κ thường: X ≈ 0.36, Y ≈ 0.37 — gần như bằng nhau.
  2. 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).
  3. 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.
Ví dụ 4.7c · Krippendorff's α với ô trống (8 trace, 3 người)
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)
  1. 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.
  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).
  3. Do = (2 + 2)/21 ≈ 0.190. De = 2·13·8 / (21·20) = 208/420 ≈ 0.495.
  4. α = 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.
Bài tập 4.7

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.

Judge L — dễ dãi Judge S — khắt khe Judge: PJudge: F Judge: PJudge: F Gold P (80)Gold F (20) Gold P (80)Gold F (20) 7821010 6614317 κ = 0.56 · TPR 97.5% · TNR 50% lọt 10/20 lỗi ra người dùng κ = 0.56 · TPR 82.5% · TNR 85% 14 câu tốt bị đẩy đi review Cùng một κ — nhưng bạn sẽ triển khai hai judge này theo hai cách hoàn toàn khác nhau.
Số liệu tự dựng. TPR = tỉ lệ trace Pass (theo gold) được judge nói Pass; TNR = tỉ lệ trace Fail được judge nói Fail.
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
Ví dụ 4.8 · Chọn judge nào? Tuỳ bạn dùng nó làm gì
  1. 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".
  2. 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.
  3. 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.
▶ LAB 3 · Judge vs gold: κ che mất gì?🎮

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.
Bài tập 4.8

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:

Trace: nhà đầu tư hỏi về duplex có số tiền thuê + "hợp gia đình, có sân sau" A · Pass Có thông tin đầu tư (tiền thuê, thị trường) B · Fail Giọng văn như đang bán nhà cho người mua để ở C · Pass "Gia đình" = sức hút với người thuê → liên quan
Cả ba đều hợp lý → lỗi nằm ở rubric (không nói cách xử lý câu trả lời "pha trộn"), không nằm ở người gán.

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.

Chuẩn bị

• 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).

Ba câu hỏi cho mỗi ca lệch

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

What change to the rubric would have made us agree on this case?— câu hỏi định hướng mà sách đề nghị giữ trong mọi buổi căn chỉnh (trích nguyên văn)
① 5′ công bố κ, chọn tiêu chí ④ 5′ phân việc, lịch vòng sau ② 40′ — 8 ca lệch × 5 phút ③ 10′ chốt 0′5′45′55′60′ Mỗi người nghĩ gì? nghe hết, không ngắt Rubric hở ở đâu? khoanh đúng chữ Sửa gì lần sau? ghi vào decision log Moderator: giữ giờ, kéo về "rubric" mỗi khi cuộc nói chuyện thành "ai đúng". Thư ký: ghi mọi thay đổi rubric + lý do. Không xong trong 5′ → gắn cờ, đi tiếp.
Lịch trình mẫu tự soạn cho một buổi 60 phút, một tiêu chí. Vòng lặp 3 câu hỏi chạy cho từng ca lệch trong khối ②.

5 kỹ thuật sửa rubric

Làm rõ từ ngữ
clarify wording

Thay chữ mơ hồ ("phù hợp", "tailored") bằng danh sách cụ thể theo từng loại khách.

Thêm ví dụ
add examples

Ca biên vừa cãi nhau → thành ví dụ Pass/Fail chính thức trong rubric.

Thêm decision rule
explicit rules

"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…)."

Tách tiêu chí
split the criterion

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

Chấp nhận mơ hồ
irreducible ambiguity

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.

Rubric v1 (mơ hồ)

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?

Rubric v2 (sau họp, diễn giải)

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.

Ví dụ 4.9 · Áp 5 kỹ thuật vào một domain khác (tự dựng)

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:

CaTình huốngNhãnKỹ thuật sửa
#7Họ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: PLà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ỗ.
#12Học viên nâng cao hỏi; bot giải thích rất cơ bản, dài.Hoa: P · Quân: FThê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".
#19Bot vừa sai kiến thức vừa dùng ngôn ngữ dễ hiểu.Hoa: F · Quân: PTá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í.
  1. 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.
  2. Thêm ca #7 và #12 làm ví dụ Pass/Fail chính thức vào rubric v2.
  3. 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.
Bài tập 4.9

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)

You are measuring groupthink, not rubric clarity.— sách, về hậu quả của việc bàn bạc trước khi gán độc lập (trích nguyên văn)

Bảng "triệu chứng → nghi vấn → kiểm tra" (tự soạn)

Triệu chứngNghi vấnKiểm tra nhanh
κ vòng 1 > 0.9 trên tiêu chí chủ quanMất độc lập / collusion qua LLMHỏ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ềuRubric "sống" trong đầu nhóm cũ, không nằm trên giấyCho 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ầnDrift 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ọpTiê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ườiHiểu ngược định nghĩa Pass/FailNhì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".
Ví dụ 4.10 · Phát hiện drift bằng bộ neo 12 trace

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ũ):

Tuần 0 Tuần 8 P P F P F P F P P F P F P P P P F P P P P F P P A07: F→P A03: F→P A12: F→P
  1. Đồng ý với chính mình 9/12 = 75%; Pe = (7/12)(10/12) + (5/12)(2/12) ≈ 0.556 → self-κ ≈ 0.44.
  2. 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.
  3. 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ài tập 4.10

Ở 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

Case study · Bot CSKH của ví điện tử "VíXanh" (hoàn toàn tự dựng)

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)

C1 · Đúng chính sách Pass: mọi con số phí, hạn mức, thời gian xử lý khớp tài liệu chính sách hiện hành. Fail: có ít nhất một thông tin chính sách sai hoặc bịa. C2 · Giải quyết được việc Pass: khách biết chính xác bước tiếp theo để xong việc. Fail: khách vẫn không biết phải làm gì. C3 · Giọng điệu phù hợp Pass: lịch sự, đồng cảm, phù hợp tình huống. Fail: không phù hợp tình huống.

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ách0.790.81 / 0.77 / 0.805
C2 Giải quyết được việc0.480.44 / 0.52 / 0.4910
C3 Giọng điệu0.270.35 / 0.18 / 0.2914

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)

  1. 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.
  2. 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 đề.
  3. 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.
  4. 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ị.
  5. 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)

C2 · Giải quyết được việc Pass: (a) nêu bước cụ thể khách tự làm được để xong việc, HOẶC (b) chuyển tiếp đúng cách: yêu cầu vượt quyền bot (hoàn tiền, mở khoá do nghi gian lận, tra soát) + nêu kênh cụ thể + thông tin cần chuẩn bị. Fail: hướng dẫn chung chung không làm theo được; chuyển tổng đài khi bot tự giải quyết được; tuyên bố đã làm một việc bot không có quyền làm. R2.1 Giọng điệu KHÔNG xét ở tiêu chí này. R2.2 Thiếu một bước khiến khách không thể hoàn tất → Fail. C3a · Ghi nhận vấn đề (chỉ xét khi khách phàn nàn/bực) Pass: có ít nhất một câu ghi nhận vấn đề. Lời xin lỗi lặp lại không cộng điểm. C3b · Không suồng sã khi nhạy cảm Fail: có emoji hoặc từ đệm suồng sã ("nha", "nè", "hihi") khi chủ đề là mất tiền / gian lận / khiếu nại. Chủ đề khác: không xét.

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.

00.20.4 0.60.81.0 ngưỡng 0.6 Vòng 1 · 40 trace · v1 Vòng 2 · 30 trace · v2 Vòng 3 · 30 trace · v3 C1 0.79 C2 0.48 C3 0.27 C3b 0.86 C1 0.80 C3a 0.72 C2 0.64 tách C3 → C3a + C3b (nét đứt)
Số liệu minh hoạ. Tiêu chí sự thật (C1) ổn định ngay từ đầu; tiêu chí chủ quan cần tách và thêm luật mới vượt ngưỡng.

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

  1. 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.
  2. 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.
  3. Mỗi vòng dùng trace mới; không ai được "học thuộc" đáp án.
  4. 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.

▶ LAB 4 · Tự gán nhãn & đo κ với 2 đồng nghiệp🎮
Gán đủ 8 trace rồi bấm "Chấm & so sánh".
Bài tập 4.11

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

📐
Rubric tinh chỉnh

Đị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.

🏅
Bộ nhãn gold

Nhãn đồng thuận — ground truth để kiểm định LLM judge ở Chương 5.

📓
Nhật ký edge case

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ươngNối với chương 4 thế nào
Ch.3 Error analysisFailure 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ự độngRubric → prompt judge; bộ gold → đo TPR/TNR; κ người–người → trần kỳ vọng.
Ch.9 CI/CDChỉ tiêu chí đã đạt κ đủ cao mới nên thành cổng chặn tự động.
Ch.10 Giao diện reviewCô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.

Log routing request + tier + answer Lấy mẫu phân tầng tier × loại, dồn ca biên Ẩn tên model id mờ, xáo thứ tự Gán độc lập bạn + 1 người, 3 tiêu chí κ từng tiêu chí + bootstrap CI Alignment → rubric v(n+1) Gold routing set → judge (Ch.5), CI (Ch.9) κ thấp batch mới κ ≥ 0.6 ở mọi tiêu chí → chốt
Khác biệt so với quy trình chung: bước "ẩn tên model" — thứ quan trọng riêng với eval routing.

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"

R1 · Đúng (correct) Pass: không có lỗi sự thật/logic/code chạy sai ảnh hưởng tới mục đích của request. Fail: có ít nhất một lỗi như vậy. Lỗi chính tả, văn phong không tính. R2 · Đáp ứng đủ yêu cầu (covers_ask) Trước khi đọc câu trả lời: liệt kê các yêu cầu TƯỜNG MINH trong prompt. Pass: đáp ứng mọi yêu cầu tường minh. Fail: bỏ sót ≥ 1. Yêu cầu ngầm chỉ tính khi prompt nêu rõ mục đích (vd. "để gửi cho khách"). R3 · Đúng format/ràng buộc (format_ok) Pass: đúng định dạng được yêu cầu (JSON hợp lệ, độ dài, ngôn ngữ, schema). Không có yêu cầu format → Pass. ACCEPTABLE = R1 ∧ R2 ∧ R3 (tính tự động, không gán tay) KHÔNG gán: chi phí, độ trễ (đo tự động); "model lớn có hay hơn không" (nhãn riêng, xem mục 5).
Ví dụ 4.13a · Vòng hiệu chuẩn 40 request (số liệu minh hoạ)

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 Đúng92.5%0.78ổn
R2 Đủ yêu cầu77.5%0.42cần họp
R3 Format97.5%0.90ổn
ACCEPTABLE (AND)77.5%0.55bị R2 kéo xuống
  1. 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.)
  2. 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?).
  3. 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".
  4. 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.
Ví dụ 4.13b · Vì sao phải ẩn tên model (thí nghiệm nhỏ, minh hoạ)
  1. Bạn gán 20 câu trả lời của model nhỏ khi thấy tên model: 12/20 acceptable.
  2. 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. 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

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.
Bài tập 4.13

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.

Tự kiểm tra

1. Hai người gán 200 trace, đồng ý 92%. 90% trace được cả hai gán Pass. Có nên vui không?
Chưa chắc. Với nhãn lệch 90/10, đồng ý do may rủi ≈ 0.9² + 0.1² = 82%. κ ≈ (0.92 − 0.82)/(1 − 0.82) ≈ 0.56 — chỉ "moderate". Phải tính κ trước khi kết luận.
2. Yếu tố quyết định có dùng được "benevolent dictator" hay không là gì?
Không phải số nhân viên, mà là có giao được quyền phán quyết cho một người hay không: người đó được tin tưởng, tiêu chí nằm trong chuyên môn họ, và khối lượng vừa sức. Công ty càng nhỏ thì thường càng dễ.
3. Team đo κ = 0.35 cho tiêu chí "giọng văn phù hợp". Bước tiếp theo nên là gì?
Dừng gán thêm, mở alignment session riêng cho tiêu chí này: xem các trace lệch, tìm chữ mơ hồ trong rubric, thêm ví dụ/decision rule hoặc tách tiêu chí. Gán tiếp chỉ tạo thêm dữ liệu mâu thuẫn.
4. Vì sao phải đo κ riêng từng tiêu chí thay vì một con số gộp?
Con số gộp che mất tiêu chí hỏng: ví dụ 4.3a có κ gộp 0.59 trong khi "đúng sự thật" 0.87 và "giọng điệu" chỉ 0.32. Bạn cần biết rubric nào cần sửa, và alignment session cũng nên đi từng tiêu chí.
5. Ba người gán, một người bỏ dở giữa chừng nên thiếu nhãn ở 30% trace. Dùng chỉ số nào?
Krippendorff's α — chịu được thiếu dữ liệu, nhiều người, mọi loại thang. Nhớ chuẩn của nó khắt khe hơn (≥ 0.8 là tin cậy, 0.67–0.8 cho kết luận tạm).
6. Có nên dùng Cohen's κ để đo LLM judge khớp với nhãn gold không? Vì sao?
Không. κ dành cho hai người ngang hàng. Khi đã có gold, đây là bài toán phân loại — cần biết judge sai theo hướng nào (dễ dãi hay khắt khe) → dùng TPR/TNR. Mục 4.8 có hai judge cùng κ = 0.56 nhưng một cái lọt 10/20 lỗi, cái kia báo động giả 14 lần.
7. Hai người gán đạt κ = 0.93 ngay vòng đầu, với tiêu chí vốn chủ quan. Nên nghi ngờ điều gì?
Có thể họ đã bàn với nhau trước (mất tính độc lập → đo groupthink) hoặc cùng dùng một LLM chấm hộ (synthetic-annotator collusion). κ cao khi đó không chứng minh rubric rõ.
8. Sau alignment session, có nên đo lại κ trên chính những trace vừa bàn để xem rubric v2 tốt hơn không?
Không. Mọi người đã nghe đáp án nên κ sẽ cao giả. Rubric mới phải được đo trên trace mới, gán độc lập. Các trace đã bàn chỉ dùng để chốt nhãn gold.
9. Trên bộ neo 12 trace, một người gán lật 3 trace và cả 3 đều từ Fail sang Pass. Nghĩa là gì, làm gì?
Drift một chiều về phía dễ dãi. Mở lại các trace, hỏi lý do: nếu do chính sách thay đổi → cập nhật rubric và nhãn gold liên quan; nếu do quen mắt/mệt → đọc lại rubric trước mỗi phiên, giới hạn số trace/phiên.
10. Trong eval model-routing, vì sao nên ẩn tên model khi gán nhãn "acceptable"?
Để tránh bias kỳ vọng: biết câu trả lời đến từ model rẻ có thể làm người gán khắt khe (hoặc dễ dãi) hơn, nên nhãn đo định kiến thay vì chất lượng — và router sẽ được tối ưu theo định kiến đó. Ẩn tên, trộn tier, chỉ mở file key sau khi gán xong.