Phần IV · Production & Improve Chương 10

Giao diện cho review thủ công: đọc trace nhanh hơn, đúng chỗ hơn

Chapter 10 — Interfaces for Human Review · bản mở rộng có ví dụ, code, bài tập & lab

TL;DR

  • Evaluator tự động không thay được người: vẫn phải đọc mẫu trace đều đặn để bắt lỗi evaluator bỏ sót và giữ evaluator khớp với định nghĩa "chất lượng" đang thay đổi (criteria drift, Ch.4).
  • Trace LLM không vừa hàng–cột spreadsheet → dùng review UI riêng thiết kế theo heuristic usability; ba đòn bẩy lớn nhất: phím tắt, failure tag có cấu trúc, clustering.
  • UI phải cho sửa nhãn cũ và thêm loại lỗi giữa chừng — chính người review cũng bị criteria drift.
  • Không đọc hết được → trộn 3 chiến lược lấy mẫu: failure-driven · uncertainty · random (khởi điểm ~50/30/20), chỉnh theo tỷ lệ lỗi; chỉ lát random cho ước lượng chất lượng không thiên lệch.
  • Review chỉ có giá trị khi dẫn tới hành động: vào golden set cho CI, hiệu chỉnh LLM judge, file bug, sửa prompt.

Cách dùng trang này

Phần diễn giải nội dung sách được giữ ngắn gọn. Phần lớn trang là tài liệu tự soạn để luyện tay: ví dụ có số liệu minh hoạ (không phải số của sách, trừ khi ghi rõ), code Python/JS tự viết chạy được, bài tập có lời giải, một case study giả định, và 3 lab tương tác (review UI mini có phím tắt, mô phỏng chiến lược lấy mẫu, review quyết định routing). Ví dụ xuyên suốt vẫn là trợ lý bất động sản soạn email cho khách, giống sách.

10.1Vì sao vẫn cần người đọc trace — và vì sao không phải spreadsheet

Trực giác. Hãy nghĩ evaluator tự động (Ch.5) như camera an ninh: chạy 24/7, rẻ, nhưng chỉ nhìn đúng những góc đã được lắp. Người review là thanh tra đi bộ: chậm và đắt, nhưng thấy được thứ không camera nào được lắp để nhìn. Ta cần thanh tra vì hai lý do:

  1. Điểm mù của evaluator — judge chỉ đo những tiêu chí đã viết ra; một kiểu lỗi mới (vd agent nhầm khu học chính) không có judge nào canh.
  2. Định nghĩa chất lượng thay đổi — càng xem dữ liệu, team càng hiểu "tốt" nghĩa là gì (criteria drift, Ch.4). Nhãn của người là thứ giữ judge bám theo định nghĩa mới.

Câu hỏi của chương vì thế không phải "có nên review tay không" mà là review thế nào cho hiệu quả. Và điểm nghẽn đầu tiên là công cụ: một trace LLM gồm query, nhiều tool call có tham số có cấu trúc, tài liệu được retrieve, reasoning trung gian và output cuối — thứ này không sống tốt trong hàng–cột.

Ví dụ xuyên suốt của sách: trợ lý bất động sản nhận yêu cầu "tìm nhà 3 phòng ngủ dưới $600k gần trung tâm, email 2 căn tốt nhất cho khách". Một trace gồm 3 tool call (tìm listing, lọc, xem lịch), 6 listing được retrieve, reasoning chọn căn và một email 2 đoạn. Nhồi vào một hàng spreadsheet, người review phải tự dựng lại trace trong đầu nhiều lần.

Spreadsheet ID Query Email Tools Notes 8a3f Find 3-bed… Dear Sarah… [{"ad… ok;tone? 8a40 Schedule a… Hi Mark, I… [{"sl… long? ✗ email bị cắt, mở ra là khối text liền ✗ tool call & listing là chuỗi JSON thô ✗ ghi chú nhiều người dồn một ô ✗ đối chiếu ràng buộc giá phải làm bằng mắt ≈ 5 phút / trace Review UI riêng Query: 3 PN < $600k gần trung tâm, email 2 căn ▸ search_listings · 6 kết quả ▸ filter_distance · còn 2 Tới: Sarah & James · 2 căn phù hợp Căn 1 · Oak St · $580k Căn 2 · Pine Ave · $595k giá/địa điểm được highlight Persona mua nhà lần đầu ngân sách chặt Pass Fail ghi chú… ≈ 30 giây / trace
Cùng một trace. Bên phải: query ở trên, tool call thu gọn, email trông như email thật, persona ở sidebar (con số thời gian là ước lượng trong sách).
Ví dụ 10.1 · Ba lần "dựng lại trong đầu" cho một trace

Giả sử bạn mở hàng tr-8a3f trong Google Sheets và muốn trả lời một câu duy nhất: "agent có chọn đúng 2 căn không?"

  1. Giải mã JSON bằng mắt. Cột "Tools" là chuỗi [{"address": …, "price": 580000, …}, …] dài 1.800 ký tự. Bạn copy sang một trang format JSON để đọc được 6 listing.
  2. Đối chiếu với ràng buộc của query. Quay lại cột Query (bị cắt ở "…near downto…"), double-click để đọc đủ: 3 phòng ngủ, < $600k, gần trung tâm. Giờ so từng listing với 3 ràng buộc — trong đầu.
  3. Đọc email xem có phản ánh đúng. Double-click cột Email: một khối text liền không xuống dòng. Tìm giá và địa chỉ trong khối đó, so với kết quả ở B1.
  4. Kết luận: ba lần chuyển ngữ cảnh cho một câu hỏi. Trong UI riêng, B1 là panel tool call đã format, B2 là query nằm cố định trên đầu, B3 là email có giá/địa điểm được highlight — mắt chỉ việc quét từ trên xuống.
Ví dụ 10.2 · Bài toán thời gian (số minh hoạ dựa trên ước lượng 5 phút → 30 giây của sách)

Team cần review 200 trace/tuần để theo kịp traffic.

Spreadsheet (~5 phút/trace)UI riêng (~30 giây/trace)
Thời gian / tuần200 × 5 = 1.000 phút ≈ 16,7 giờ200 × 0,5 = 100 phút ≈ 1,7 giờ
Ai làm được?Cần gần nửa FTE — thường bị bỏMột kỹ sư, một buổi chiều
Hệ quả thực tếReview 20 trace rồi "thôi đủ rồi"Review đủ 200, còn dư sức đọc thêm cụm lạ

Điểm tinh tế sách nhấn mạnh: UI tốt không chỉ làm mỗi trace nhanh hơn, mà làm team muốn đọc nhiều dữ liệu hơn — chừng nào vẫn còn tìm ra failure mode mới. Tốc độ đổi thành độ phủ.

Bắt đầu từ đâu: build hay dùng nền tảng có sẵn?

Có hai đường: tự dựng bằng Streamlit/Gradio (hoặc nhờ AI coding assistant sinh một dashboard HTML có nút pass/fail + ô feedback trong vài phút), hoặc dùng nền tảng trace review có sẵn (sách liệt kê Langfuse, Braintrust, Arize Phoenix, Helicone, W&B Weave…). Lời khuyên thực dụng của sách: nếu đã có nền tảng observability, dùng nó một tuần và ghi lại mọi điểm ma sát — thiếu filter, JSON khó đọc, trang load chậm. Danh sách đó chính là backlog của thứ cần build trước.
Ví dụ 10.3 · Một tuần "nhật ký ma sát" biến thành backlog (dữ liệu giả định)

Reviewer ghi một dòng mỗi khi khựng lại. Cuối tuần gom nhóm và đếm:

Nhóm ma sátSố lầnVí dụ ghi chépViệc cần build
Kết quả tool là JSON thô9"phải copy ra jsonformatter mới thấy giá"Panel tool call có format + thu gọn
Không lọc được theo persona6"muốn xem riêng khách nhà đầu tư"Filter metadata
Phải cuộn xuống mới bấm được Next5"cuộn lên cuộn xuống mỏi tay"Phím tắt N/P/F/D
Ghi chú bị ghi đè2"note của Minh mất sau khi mình sửa"Nhãn append-only theo reviewer
Trang chậm1"trace dài load 8 giây"Để sau
  1. Sắp theo tần suất × chi phí mỗi lần: JSON thô xảy ra 9 lần và mỗi lần tốn ~1 phút → ưu tiên số 1.
  2. Phím tắt chỉ 5 lần ghi chép nhưng xảy ra ở mọi trace mà người ta đã quen không ghi → vẫn đáng làm sớm (sách xếp nó là đòn bẩy số 1).
  3. "Ghi chú bị ghi đè" hiếm nhưng làm hỏng dữ liệu → sửa ngay, rẻ.

Hiểu lầm hay gặp

"Custom UI = một dự án frontend lớn." Không. Bản đầu tiên là một trang hiển thị một trace/màn hình với 3 nút và vài phím tắt — khoảng 50 dòng code (xem §10.4). "Đã có nền tảng observability thì không cần gì thêm." Nền tảng là điểm khởi đầu tốt; nhưng khi quy trình chín, view theo domain (email trông như email, hoá đơn trông như hoá đơn) và phím tắt cho annotation hay dùng mới tạo khác biệt lớn — sách xếp việc xem nhẹ điều này vào pitfalls.
Bài tập 10.1 · Tính ngân sách review

Một chatbot CSKH có 30.000 hội thoại/tuần. Team muốn review 0,5% mỗi tuần. Hiện dùng spreadsheet, mất trung bình 4 phút/trace. Một UI riêng dự kiến giảm còn 45 giây. (a) Mỗi tuần tiết kiệm bao nhiêu giờ? (b) Nếu giữ nguyên số giờ như hiện tại, UI mới cho review được bao nhiêu trace?

Xem lời giải

0,5% × 30.000 = 150 trace/tuần. (a) Spreadsheet: 150 × 4 = 600 phút = 10 giờ; UI: 150 × 0,75 = 112,5 phút ≈ 1,9 giờ → tiết kiệm ≈ 8,1 giờ/tuần. (b) 600 phút ÷ 0,75 = 800 trace (~2,7% traffic). Thường lựa chọn đúng là ở giữa: tăng lên ~300 trace để phủ thêm lát random và các cụm lạ, phần giờ còn lại để sửa lỗi tìm được.

Bài tập 10.2 · Đọc nhật ký ma sát

Nhật ký một tuần có: "không biết trace này đã ai chấm chưa" (7 lần), "không thấy agent version" (3 lần), "muốn so hai bản prompt" (2 lần), "nút Pass và Fail sát nhau, bấm nhầm" (4 lần). Xếp thứ tự ưu tiên và nói mỗi mục ứng với heuristic nào ở §10.2.

Xem lời giải

(1) "Đã ai chấm chưa" — visibility of system status; 7 lần và gây chấm trùng/lãng phí → làm trước (hiện trạng thái nhãn + người chấm). (2) "Bấm nhầm Pass/Fail" — error prevention; 4 lần nhưng mỗi lần làm bẩn dữ liệu một cách âm thầm → ưu tiên cao dù ít (tách nút, màu khác, cho undo). (3) "Không thấy version" — contextual metadata; cần để phát hiện regression theo version. (4) "So hai bản prompt" — tính năng nâng cao (diff prompt), để sau.

10.2Nguyên tắc thiết kế: heuristic usability của Nielsen

Sách dùng một tập con các heuristic usability kinh điển của Jakob Nielsen làm khung khởi đầu, dịch mỗi nguyên tắc sang một quyết định thiết kế cụ thể cho review tool. Tóm lại bằng lời của sổ tay:

Hiện trạng thái hệ thống
visibility of status

Đang xem trace nào, còn bao nhiêu, đã gửi feedback chưa.

Khớp thế giới thật
match real world

Dùng đúng từ trong rubric và tiêu chí eval của team.

Kiểm soát & tự do
user control

Next/Prev, undo, defer; không khoá cứng ghi chú.

Nhất quán
consistency

Layout, thuật ngữ, hành vi giống nhau giữa các trace.

Phòng lỗi
error prevention

Nút nhãn tách biệt rõ; bắt buộc ghi lý do khi chọn Fail.

Nhận ra > nhớ lại
recognition over recall

Hiện sẵn danh sách failure mode + giải thích.

Linh hoạt & hiệu quả
flexibility

Phím tắt cho người quen, vẫn dễ cho người mới.

Tối giản
minimalist design

Tín hiệu hơn nhiễu; làm nổi input và output.

Trực giác: reviewer là người làm một việc lặp lại hàng trăm lần. Mỗi giây ma sát nhân lên hàng trăm; mỗi cơ hội bấm nhầm nhân lên thành hàng chục nhãn sai. Heuristic giúp ta nhìn UI bằng con mắt đó thay vì con mắt "cho có".

Ví dụ 10.4 · Audit một review tool nội bộ "ReviewSheet v0" (giả định)

Một team dựng nhanh trang review bằng Streamlit. Sau 2 buổi dùng, họ ghi nhận các vấn đề sau và map sang heuristic:

Quan sátHeuristic bị vi phạmSửa
Không biết đang ở trace số mấy, còn bao nhiêuVisibility of system statusHeader "Trace 37/120" + thanh tiến độ
Nút ghi là "Label A / Label B"Match real worldĐổi thành đúng tên trong rubric: "Pass" / "Fail – sai budget"…
Bấm Next là mất luôn, không quay lại đượcUser control & freedomPrev + mở lại nhãn cũ
Trace email có layout khác trace lịch hẹn, nút nằm chỗ khácConsistencyKhung cố định: query trên, output giữa, panel nhãn phải
Fail không cần lý do → 40% Fail không có ghi chúError preventionBắt buộc ghi chú hoặc chọn tag khi Fail
Reviewer phải nhớ 9 failure mode từ tài liệu Ch.3Recognition > recallChecklist tag hiện sẵn, có tooltip định nghĩa
Mọi thao tác đều phải clickFlexibility & efficiencyPhím tắt P/F/D/N + số 1–9 cho tag
Hiện cả system prompt 3.000 token ở đầu mọi traceMinimalist designThu gọn mặc định; output nổi nhất
  1. Không sửa hết một lúc. Chọn 2 cái rẻ nhất có tác động lớn nhất: phím tắt và bắt buộc lý do khi Fail.
  2. Đo lại sau một buổi: thời gian/trace và tỷ lệ Fail có ghi chú. Nếu tỷ lệ này từ 60% lên ~100%, dataset dùng được cho phân tích lỗi (Ch.3).

Hiểu lầm hay gặp

"Heuristic là checklist phải đạt đủ 8/8." Không — đó là lăng kính để tìm vấn đề. Một UI đạt 5/8 nhưng có phím tắt tốt vẫn hơn UI đẹp đạt 8/8 mà mọi thao tác đều phải click. Ưu tiên theo tần suất × mức hại; lỗi làm bẩn nhãn (error prevention) thường đáng sửa hơn lỗi chỉ làm chậm.
Bài tập 10.3 · Heuristic nào?

Với mỗi tình huống, gọi tên heuristic liên quan: (a) Reviewer chấm xong 50 trace mới biết mình đang chấm nhầm agent version cũ. (b) Tag "hallucination" bị các reviewer hiểu khác nhau. (c) Người mới mất 10 phút để tìm cách gắn tag. (d) Phím F vừa là Fail vừa là "focus search" tuỳ màn hình. (e) Bấm Pass xong không có dấu hiệu gì là đã lưu.

Xem lời giải

(a) Visibility of system status / contextual metadata — version phải hiện trên header. (b) Match real world + recognition > recall — tag cần định nghĩa hiển thị ngay (tooltip, ví dụ mẫu), dùng đúng thuật ngữ trong rubric. (c) Flexibility — UI phải dễ cho người mới (nút nhìn thấy được), phím tắt chỉ là lớp tăng tốc thêm. (d) Consistency — một phím, một nghĩa ở mọi màn hình. (e) Visibility of system status — cần phản hồi "đã lưu ✓".

10.3Thành phần cốt lõi: một review UI tốt trông thế nào

Hai yêu cầu nền tảng theo sách:

Hiện đủ trace, nhưng gọn
full trace + collapse by default

Input, output và các bước trung gian (tool call, retrieval, reasoning) đều xem được — nhưng phần phụ thu gọn mặc định, bấm mới mở. Người review không phải chọn giữa "thiếu ngữ cảnh" và "ngập chữ".

Ba kiểu feedback
tags · quick rating · open notes

(1) Checkbox/nút nhị phân cho failure mode đã biết (từ phân tích lỗi Ch.3 / cộng tác Ch.4); (2) đánh giá nhanh (thumbs, điểm); (3) ô ghi chú tự do để giải thích và lộ vấn đề chưa có tên.

Wireframe do sổ tay tự thiết kế, gom các nguyên tắc và tính năng của chương. Các số tròn khớp với bảng chú thích bên dưới.

Trace 37 / 120 tr-8a3f · agent v2.3 · persona: nhà đầu tư · kênh: web 1 USER QUERY Tìm nhà 3 PN dưới $600k gần trung tâm, email 2 căn tốt nhất TOOL CALLS · thu gọn mặc định 3 ▸ search_listings(beds=3, max=600k) 6 kết quả · 420ms ▸ filter_distance(downtown, 5km) còn 2 · 35ms ▸ check_calendar(client_id) OK · 90ms EMAIL ĐÃ SINH · output chính 2 Tới: khách A · Tiêu đề: 2 căn phù hợp tiêu chí Chào anh A, hy vọng anh và gia đình vẫn khoẻ… 💬 tone quá xã giao Căn 1 · 123 Oak St · $580k · 2.1 km Căn 2 · 48 Pine Ave · $595k · 3.4 km TÍN HIỆU VẬN HÀNH 7 3 tool calls 6 retrieve · dùng 2 4.1k tokens $0.012 latency 3.2s ĐÁNH GIÁ Pass · P Fail · F Defer · D FAILURE TAGS · hiện khi Fail 4 Tone mismatchT Thiếu budgetB Bịa tính năngH + thêm tag mới giữa chừng GHI CHÚ (bắt buộc khi Fail) 5 Khách nhà đầu tư cần số liệu gọn; 2 đoạn đầu toàn xã giao. template: "Tone quá thân mật" ▾ 8 + Golden set File bug ◀ Prev Next ▶ ↺ mở lại & sửa nhãn cũ 6 Phím tắt: N next · P pass · F fail · D defer · T tone · B budget · / tìm trace giống
Một màn hình = một trace. Output chính nổi bật nhất; phần phụ thu gọn; nhãn có cấu trúc + ghi chú tự do; hành động khép vòng ngay tại chỗ.
#Chi tiết trong wireframeNguyên tắc / tính năng
1"Trace 37/120", thanh tiến độ, agent version, personaVisibility of status · progress indicator · contextual metadata (thấy regression theo version)
2Email là khối nổi nhất, highlight giá & khoảng cách; gạch chân câu có vấn đề + bình luậnVisual hierarchy · inline annotation (chỉ đúng câu sai)
3Tool call thu gọn, bấm để mở tham số/kết quảProgressive disclosure · minimalist
4Danh sách failure tag chỉ hiện khi chọn Fail; thêm tag mới đượcRecognition > recall · progressive disclosure · hỗ trợ criteria drift
5Ghi chú bắt buộc khi Fail, có template commentError prevention · feedback template · open coding
6Thanh phím tắtKeyboard shortcuts — đòn bẩy tốc độ số 1
7Số tool call, retrieve/dùng, token, cost, latencyTín hiệu vận hành cho agent: bắt ca "đúng nhưng tốn gấp 10 lần"
8+ Golden set, File bug, sửa nhãn cũKhép vòng với CI (Ch.9) · user control

Mô hình dữ liệu cho nhãn: quyết định quan trọng hơn giao diện

UI đẹp mà lưu nhãn sai cách thì mọi thứ phía sau (golden set, hiệu chỉnh judge, đo drift) đều hỏng. Đây là schema sổ tay đề xuất cho mỗi lần reviewer bấm một nhãn:

{
  "trace_id": "tr-8a3f",
  "reviewer": "lan",
  "verdict": "fail",                  // pass | fail | defer
  "tags": ["tone_mismatch"],          // axial codes (Ch.3) — danh sách có thể thêm mới
  "note": "Khách nhà đầu tư, 2 đoạn đầu toàn xã giao",   // open code
  "spans": [{"start": 0, "end": 118, "tag": "tone_mismatch"}],  // inline annotation
  "rubric_version": "v2",             // nhãn này theo định nghĩa nào?
  "source": "uncertainty",            // random | uncertainty | failure — dùng khi ước lượng
  "agent_version": "v2.3",
  "supersedes": "lbl-0192",           // sửa nhãn cũ = bản ghi MỚI trỏ về bản cũ
  "ts": "2026-09-21T10:14:03Z"
}
Ví dụ 10.5 · Progressive disclosure trên một trace, từng bước
  1. Mở trace tr-9c12. Màn hình chỉ có query, email (output chính, nổi nhất) và ba nút Pass / Fail / Defer. Tool call đang thu gọn thành 3 dòng tóm tắt.
  2. Email báo giá $615k trong khi ngân sách là $600k — highlight giá làm con số "nhảy ra". Bạn bấm F.
  3. Chỉ lúc này danh sách failure tag mới hiện: Tone mismatch · Thiếu/sai budget · Bịa tính năng · Sai lịch. Bấm 2 chọn "sai budget". Ô ghi chú được focus sẵn, có template gợi ý theo tag: "Giá vượt ngân sách khách".
  4. Muốn biết lỗi đến từ đâu: mở panel search_listings → tool trả đúng 6 căn < $600k, nhưng email lại nhắc căn thứ 7 không có trong kết quả → thực ra là bịa, không phải lọc sai. Thêm tag thứ hai.
  5. N sang trace tiếp. Tổng: ~25 giây, 4 phím, 1 lần mở panel. Với trace Pass, chỉ có B1 → P → xong.

Hiểu lầm hay gặp: tag hay ghi chú — chọn một?

Cần cả hai, vì chúng làm hai việc khác nhau. Tag (axial code) cho phép đếm, lọc, vẽ dashboard, so với judge. Ghi chú tự do (open code) là nơi failure mode mới xuất hiện trước khi có tên. Một UI chỉ có tag sẽ đóng băng hiểu biết của team ở thời điểm tạo danh sách tag; một UI chỉ có ghi chú thì không đếm được gì. Quy trình lành mạnh: ghi chú tự do tích luỹ → định kỳ gom nhóm → thêm tag mới vào UI.
Bài tập 10.4 · Từ ghi chú tự do đến bộ tag

Sau một buổi review, bạn có 10 ghi chú: "chào hỏi dài dòng" · "giá 615k > budget" · "nói căn có hồ bơi, listing không có" · "hẹn xem nhà vào ngày lễ" · "xưng hô quá thân mật với nhà đầu tư" · "bỏ qua phí HOA" · "khoảng cách tới trường thay cho xếp hạng trường" · "lịch trùng với lịch đã có" · "gara 2 xe – bịa" · "email không nhắc số liệu lợi suất". Đề xuất 4–5 tag, gán mỗi ghi chú vào tag.

Xem lời giải

Một cách gom: Tone mismatch (chào hỏi dài dòng; xưng hô quá thân mật; không nhắc lợi suất cho nhà đầu tư — có thể tách thành "thiếu số liệu cho persona"); Sai/thiếu budget (615k > budget; bỏ qua phí HOA); Bịa tính năng (hồ bơi; gara 2 xe); Sai lịch (ngày lễ; trùng lịch); Hiểu sai chỉ số trường (khoảng cách thay cho xếp hạng). Tag cuối cùng chỉ có 1 ghi chú — vẫn giữ nếu hậu quả lớn, và theo dõi tiếp. Không có đáp án duy nhất; tiêu chí là tag loại trừ nhau đủ rõ để hai reviewer gán giống nhau.

10.4Tính năng làm review nhanh hơn và tốt hơn

Sách chia các tính năng nâng cao thành ba nhóm: làm từng lượt review nhanh hơn, thu feedback chất lượng hơn, và giúp điều hướng trong cả tập dữ liệu.

  • Phím tắt — tính năng đòn bẩy nhất. Chỉ cần bộ tối thiểu Next/Pass/Fail/Defer đã có thể gấp đôi throughput; làm cái này trước mọi thứ khác.
  • Visual hierarchy — làm nổi output cuối; highlight thực thể dễ sai (giá, địa điểm, thời gian) để lỗi sự thật "nhảy ra".
  • Progressive disclosure — ban đầu chỉ Pass/Fail/Defer; chọn Fail mới hiện tag chi tiết.
  • Inline annotation — đánh dấu đúng câu, đúng lượt hội thoại có vấn đề.
  • Feedback template — comment hay dùng thành lựa chọn nhanh, gợi ý theo tag đã chọn.
  • Defer — nút "để sau" to, rõ cho ca không chắc → tránh nhãn đoán mò làm bẩn dataset.
  • Metadata ngữ cảnh — segment người dùng, agent version, kết quả evaluator ngay trong UI.
  • Filter theo chiều quan trọng của sản phẩm (vd kênh: text / app / web).
  • Batch review & bulk action — xem một cụm trace giống nhau và gán cùng nhãn; tiện nhưng dùng cẩn thận.
  • Progress — đếm trace đã review theo từng chiều để biết khi nào "đủ".

Vì sao phím tắt là đòn bẩy số 1

Sách ước lượng reviewer dùng hotkey đi qua trace nhanh hơn 2–3 lần người bấm chuột, và khuyên thêm phím tắt trước mọi cải tiến giao diện khác. Lý do nằm ở chỗ thời gian bị mất: không phải lúc đọc, mà lúc thao tác giữa các lần đọc.

Chuột đọc trace · 10 s cuộn 3 s click 2 s tìm tag 4 s Next 3 s 22 s Phím tắt đọc trace · 10 s 11 s → gấp đôi số trace mỗi giờ F · 2 · N ≈ 1 s 0 5 s 10 s 15 s 20 s Số liệu minh hoạ (một trace Fail có 1 tag). Phím tắt không rút ngắn phần đọc — nó xoá "overhead thao tác".
Khi visual hierarchy đã làm phần đọc ngắn lại, overhead thao tác chiếm phần lớn thời gian — và phím tắt xoá gần hết nó.
"Keyboard shortcuts are the single highest-leverage feature for review speed."— Shankar & Husain, Ch.10

Code: một review app tối thiểu (~50 dòng, chỉ thư viện chuẩn Python)

Đây là bản "UI tối giản" ở §10.9, do sổ tay tự viết: một trace mỗi màn hình, tool call thu gọn bằng <details>, phím tắt P/F/D/N/B, bắt buộc lý do khi Fail, nhãn ghi append-only vào labels.jsonl.

# review_app.py — review UI tối thiểu, chỉ dùng thư viện chuẩn Python.
# Chạy: python review_app.py traces.jsonl  → mở http://localhost:8765
import json, sys, time
from http.server import BaseHTTPRequestHandler, HTTPServer

TRACES = [json.loads(l) for l in open(sys.argv[1], encoding="utf-8") if l.strip()]
LABELS = "labels.jsonl"          # append-only: sửa nhãn = ghi bản ghi mới

PAGE = """<!doctype html><meta charset=utf-8><title>Review</title>
<style>body{font:15px system-ui;max-width:860px;margin:auto}
pre{white-space:pre-wrap;background:#f4f4f4;padding:8px}
.out{border:2px solid #36c;padding:10px}</style>
<div id=bar></div><div id=view></div>
<textarea id=note rows=2 cols=80 placeholder="ghi chú (bắt buộc khi Fail)"></textarea>
<p>Phím: <b>P</b> pass · <b>F</b> fail · <b>D</b> defer · <b>N</b>/<b>B</b> next/back</p>
<script>
let T=[], i=0;
fetch('/api/traces').then(r=>r.json()).then(d=>{T=d; show();});
function show(){ const t=T[i];
  bar.textContent=`Trace ${i+1}/${T.length} · ${t.id} · agent ${t.version}`;
  view.innerHTML=`<p><b>Query:</b> ${t.query}</p>`+
    t.tools.map(c=>`<details><summary>▸ ${c.name}</summary><pre>${JSON.stringify(c,null,1)}</pre></details>`).join('')+
    `<div class=out>${t.output.replace(/\\n/g,'<br>')}</div>`; note.value=''; }
async function label(v){
  if(v==='fail' && !note.value.trim()){ note.focus(); return alert('Fail cần lý do'); }
  await fetch('/api/label',{method:'POST',body:JSON.stringify({id:T[i].id,verdict:v,note:note.value})});
  if(i<T.length-1){i++; show();} }
document.addEventListener('keydown',e=>{
  if(e.target===note) return;                 // đang gõ ghi chú thì không bắt phím
  const k=e.key.toLowerCase();
  if(k==='p') label('pass'); if(k==='f') label('fail'); if(k==='d') label('defer');
  if(k==='n'&&i<T.length-1){i++;show();} if(k==='b'&&i>0){i--;show();} });
</script>"""

class H(BaseHTTPRequestHandler):
    def _send(self, body, ctype="application/json"):
        self.send_response(200); self.send_header("Content-Type", ctype + "; charset=utf-8")
        self.end_headers(); self.wfile.write(body.encode("utf-8"))
    def do_GET(self):
        if self.path == "/api/traces": self._send(json.dumps(TRACES, ensure_ascii=False))
        else: self._send(PAGE, "text/html")
    def do_POST(self):
        rec = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        rec["ts"] = time.time(); rec["reviewer"] = self.headers.get("X-Reviewer", "me")
        with open(LABELS, "a", encoding="utf-8") as f: f.write(json.dumps(rec, ensure_ascii=False) + "\n")
        self._send('{"ok":true}')

if __name__ == "__main__":
    print(f"{len(TRACES)} traces → http://localhost:8765")
    HTTPServer(("localhost", 8765), H).serve_forever()

Mỗi dòng của traces.jsonl có dạng {"id","version","query","tools":[{"name",…}],"output"}. Hai điều cố ý làm "đúng" dù bản này rất nhỏ: (1) phím tắt bị tắt khi đang gõ ghi chú; (2) nhãn không bao giờ bị ghi đè. Điều cố ý bỏ qua để giữ ngắn: escape HTML của output (dữ liệu production phải escape trước khi đưa vào innerHTML), đăng nhập, tag, filter — chính là những thứ Lab 1 bên dưới thêm vào.

▶ LAB 1 · Review UI mini có phím tắt🎮

10 trace giả định của trợ lý bất động sản. Bấm vào khung lab để bật phím tắt: P pass · F fail · D defer · N/B tiếp/lùi · 14 bật/tắt tag (sau khi Fail) · Esc thoát ô ghi chú. Fail cần ít nhất một tag hoặc ghi chú.

⌨ tắt — bấm vào khung lab
Chưa có nhãn nào.
Ví dụ 10.6 · Vì sao "Defer" bảo vệ dataset (số minh hoạ)

Sách nhấn mạnh nút Defer phải rõ và dễ bấm để ca không chắc không biến thành nhãn đoán mò. Làm phép tính:

  1. Buổi review 200 trace, trong đó ~20 trace (10%) reviewer thật sự không chắc.
  2. Không có Defer → reviewer đoán. Giả sử đoán đúng ~50% → khoảng 10 nhãn sai = 5% dataset nhiễu.
  3. Dùng dataset đó để đo judge (Ch.5): nếu judge thật sự đúng 95%, bạn chỉ đo được khoảng 90–95% — không phân biệt được judge tốt với judge vừa phải, vì trần đo lường bị nhãn nhiễu kéo xuống.
  4. Có Defer → 20 ca đó vào hàng "chờ thảo luận"; buổi calibration (Ch.4) biến chúng thành ví dụ biên cho rubric — chính những ca giá trị nhất.
Ví dụ 10.7 · Inline annotation và feedback template

Email 3 đoạn cho khách nhà đầu tư. Đoạn 1–2 xã giao, đoạn 3 có số liệu. Thay vì ghi chú chung "tone chưa ổn", reviewer bôi đen đoạn 1–2 và chọn tag Tone mismatch; template gợi ý theo tag hiện ra: "Mở đầu quá dài cho persona cần số liệu". Kết quả lưu thành spans: [{start: 0, end: 214, tag: "tone_mismatch"}]. Lợi ích về sau: khi sửa prompt, bạn có đúng đoạn văn làm ví dụ phản diện; khi viết judge, span là bằng chứng cụ thể cho few-shot (Ch.5).

Hiểu lầm hay gặp

"Phím tắt càng nhiều càng tốt." Không — bộ tối thiểu Next/Pass/Fail/Defer đã cho phần lớn lợi ích; thêm phím cho vài tag hay dùng nhất, không phải cho mọi tag. Và phím tắt không được "cướp" phím khi người dùng đang gõ ghi chú hoặc ô tìm kiếm — lỗi kinh điển khiến reviewer gõ chữ "fine" và vô tình bấm Fail + Next. "Bulk action tiết kiệm thời gian nên cứ dùng." Sách cảnh báo dùng cẩn thận: gán cùng một nhãn cho cả cụm chỉ an toàn sau khi đã đọc vài đại diện và thấy chúng thật sự cùng lỗi (xem §10.7).
Bài tập 10.5 · Thiết kế bảng phím tắt

UI của bạn có: Pass, Fail, Defer, Next, Prev, 6 failure tag, "thêm vào golden set", "file bug", ô tìm kiếm. Thiết kế bảng phím tắt, chỉ ra một xung đột tiềm ẩn và cách tránh.

Xem lời giải

Một phương án: P/F/D cho verdict; J/K hoặc N/B cho next/prev; 16 cho tag (thứ tự theo tần suất, cố định giữa các trace — consistency); G golden set; Shift+B file bug (hành động "nặng" nên cần tổ hợp, tránh bấm nhầm — error prevention); / focus ô tìm kiếm. Xung đột: khi ô tìm kiếm hoặc ghi chú đang focus, mọi phím chữ phải đi vào ô đó, không kích hoạt lệnh; Esc để thoát ra. Thêm: hiển thị bảng phím trên màn hình (recognition > recall).

10.5Case study EvalGen & bài học về criteria drift

Sách dùng EvalGen (Shankar và cộng sự, 2024; bản công khai EvalGen v2 tích hợp trong ChainForge) để minh hoạ các nguyên tắc trên trong một thiết kế tối giản.

Thiết kế tối giản
EvalGen (tích hợp trong ChainForge)

Một output mỗi lần · thumbs up/down · comment tự do · sửa được nhãn cũ · rubric builder tự rút tiêu chí ứng viên từ prompt gốc.

Ưu tiên chỗ judge bất đồng
disagreement-driven

Khi nhiều LLM judge (hoặc biến thể prompt) cho kết quả khác nhau trên một output, output đó được đẩy lên cho người xem — một dạng uncertainty sampling.

Người review cũng bị criteria drift

User study của EvalGen cho thấy người chấm liên tục định nghĩa lại "đúng" hay "chỉn chu", quay lại sửa nhãn cũ và thêm loại lỗi không ai lường trước. Nếu UI không cho sửa nhãn cũ, nhãn của 20 trace đầu và 20 trace cuối sẽ theo hai chuẩn khác nhau mà không ai biết. Hãy coi eval là quá trình tiến hoá, không phải một đợt gán nhãn tĩnh (nối với Ch.4).

Criteria drift nhìn thấy được

Sơ đồ sổ tay tự vẽ: một reviewer chấm 40 trace liên tiếp. Tới trace 18, sau khi thấy nhiều email xã giao dài cho khách nhà đầu tư, cô ấy đổi định nghĩa "tone đạt" từ "lịch sự" thành "hợp persona". Nhãn trước đó giờ theo một chuẩn khác.

Rubric v1 · "tone" = lịch sự Rubric v2 · "tone" = hợp persona Lượt chấm đầu (trace 1 → 40) trace 1 trace 40 ← mở lại & sửa theo v2 Sau khi mở lại nhãn cũ Pass Fail Pass theo v1, Fail theo v2 — cần sửa Nếu UI không cho sửa nhãn cũ: 4 nhãn Pass "lỗi thời" nằm lại trong golden set và trong dữ liệu hiệu chỉnh judge.
Drift không phải lỗi của reviewer — đó là hiểu biết tăng lên. Lỗi là khi hệ thống không cho hiểu biết mới chảy ngược về nhãn cũ.
Ví dụ 10.8 · Đo drift bằng "chấm lại mù" (quy trình tự đề xuất)
  1. Cuối buổi review, UI đưa lại 10 trace đầu tiên của buổi, ẩn nhãn cũ, xáo thứ tự.
  2. Reviewer chấm lại. So hai lượt: 7/10 giữ nguyên, 3 trace đổi từ Pass → Fail (số minh hoạ).
  3. Tự-đồng-thuận 70%, Cohen's κ ≈ 0,4 — thấp. Nhưng nhìn hướng đổi: cả 3 đều Pass → Fail và đều là email cho nhà đầu tư → đây là drift có hướng (tiêu chí siết lại), không phải nhiễu ngẫu nhiên.
  4. Hành động: ghi tiêu chí mới vào rubric (v2), gắn rubric_version, lọc mọi nhãn v1 có persona "nhà đầu tư" để chấm lại. Nếu đổi nhãn là ngẫu nhiên hai chiều thì vấn đề là định nghĩa mơ hồ → cần buổi calibration (Ch.4) chứ không phải rubric mới.
# drift_check.py — phát hiện criteria drift bằng cách chấm lại 10 trace đầu ở cuối buổi.
import math

def cohen_kappa(a, b):
    labels = sorted(set(a) | set(b))
    n = len(a)
    po = sum(x == y for x, y in zip(a, b)) / n                     # đồng thuận quan sát
    pe = sum((a.count(l) / n) * (b.count(l) / n) for l in labels)  # đồng thuận do may rủi
    return (po - pe) / (1 - pe) if pe < 1 else 1.0

def wilson(k, n, z=1.96):
    """Khoảng tin cậy 95% cho tỷ lệ k/n — dùng cho lát random nhỏ (§10.6)."""
    p = k / n
    d = 1 + z * z / n
    c = (p + z * z / (2 * n)) / d
    h = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / d
    return max(0, c - h), min(1, c + h)

# Số liệu minh hoạ: nhãn lượt 1 (đầu buổi) và lượt 2 (cuối buổi) của cùng 10 trace
first  = ["P", "P", "F", "P", "P", "F", "P", "P", "P", "P"]
second = ["P", "F", "F", "P", "F", "F", "P", "F", "P", "P"]
print("tự đồng thuận:", sum(x == y for x, y in zip(first, second)) / 10)   # 0.7
print("kappa:", round(cohen_kappa(first, second), 2))                        # 0.4
flipped = [i for i, (x, y) in enumerate(zip(first, second)) if x != y]
print("trace đổi nhãn:", flipped)       # [1, 4, 7] → mở lại đúng các trace này
print("Wilson 3/40:", [round(x, 3) for x in wilson(3, 40)])   # [0.026, 0.199]

Hiểu lầm hay gặp

"Reviewer đổi ý nghĩa là reviewer không đáng tin." Ngược lại: user study của EvalGen cho thấy đổi ý là bản chất của việc định nghĩa chất lượng — người ta chỉ biết mình muốn gì sau khi thấy đủ ví dụ. Việc cần làm là (1) cho sửa nhãn cũ, (2) phiên bản hoá rubric, (3) phân biệt drift có hướng (hiểu biết tăng) với bất nhất ngẫu nhiên (định nghĩa mơ hồ hoặc reviewer mệt).
Bài tập 10.6 · Drift hay nhiễu?

Hai reviewer, mỗi người chấm lại mù 12 trace của chính mình. Reviewer A: đổi 4 nhãn, cả 4 đều Pass→Fail, đều liên quan "không nhắc phí HOA". Reviewer B: đổi 4 nhãn, 2 Pass→Fail, 2 Fail→Pass, không có điểm chung. Với mỗi người, chẩn đoán và hành động?

Xem lời giải

A: drift có hướng. A đã học ra một tiêu chí mới ("email phải nhắc phí HOA khi có"). Hành động: đưa tiêu chí vào rubric v2, thêm tag "thiếu chi phí ẩn", lọc nhãn cũ liên quan để chấm lại, báo team để mọi người cùng áp dụng. B: bất nhất ngẫu nhiên. Không có tiêu chí mới nào nổi lên; nhiều khả năng định nghĩa đang mơ hồ, hoặc B chấm khi mệt/quá nhanh. Hành động: đưa 4 trace đó vào buổi calibration với người khác (Ch.4), viết ví dụ biên vào rubric, cân nhắc giới hạn độ dài buổi review.

10.6Chọn trace nào để người đọc: ba chiến lược lấy mẫu

Trực giác: bạn có ngân sách 40 lượt đọc/tuần cho hàng chục nghìn trace. Mỗi chiến lược lấy mẫu là một cái "đèn pin" chiếu vào một vùng khác của quần thể: có đèn chỉ soi chỗ đã biết là hỏng, có đèn soi chỗ các evaluator cãi nhau, có đèn soi ngẫu nhiên. Không đèn nào soi được hết.

Hàng nghìn đến hàng triệu output, chỉ một phần nhỏ được người xem. Ba chiến lược, mỗi cái nhìn thấy một phần khác nhau của "quần thể" trace:

Trace production bị flag (guardrail, 👎) evaluator bất đồng evaluator bảo ổn lỗi ẩn — evaluator bảo ổn nhưng sai Failure-driven · ~50% lấy trace đã bị flag precision cao · recall thấp Uncertainty · ~30% evaluator mâu thuẫn / chạy lại lệch → chỗ tiêu chí cần siết Random · ~20% ước lượng không thiên lệch cách duy nhất chạm lỗi ẩn Hàng đợi review batch ngày/tuần → Slack / email
Failure-driven chỉ thấy chấm đỏ; uncertainty thấy chấm cam; chỉ random mới có cơ hội chạm vào "lỗi ẩn" (viền đỏ) mà mọi tín hiệu tự động đều bỏ qua.

Lấy ngẫu nhiên từ luồng production. Không thiên lệch, cho bức tranh chung khi mẫu đủ lớn — và là ước lượng không thiên lệch duy nhất về chất lượng tổng thể.

Yếu: bỏ sót lỗi hiếm; khi phần lớn output đã tốt, người review chủ yếu "xác nhận là ổn".

Mượn ý tưởng từ active learning, nhưng thay vì độ bất định của một model, ta đo sự bất đồng giữa nhiều evaluator. Dấu hiệu:

  • Trace qua một số tiêu chí nhưng trượt tiêu chí khác (chất lượng "lấp lửng").
  • LLM judge cho kết quả khác nhau khi chạy lại với temperature > 0.
  • Check code và LLM judge mâu thuẫn (cách EvalGen dùng).

Ca bất đồng thường là ca mơ hồ → review tay cho nhiều thông tin nhất về chỗ tiêu chí cần siết.

Lấy trace đã có dấu hiệu lỗi: guardrail bắt được lời khuyên tài chính không được yêu cầu, người dùng bấm 👎… Nhanh nhất để hiểu vì sao hỏng, vì trace cho thấy toàn bộ diễn biến.

Yếu: chỉ thấy thứ tín hiệu hiện có được cấu hình để tìm. Một lỗi tinh vi lọt qua mọi guardrail (vd nhầm khu học chính) sẽ không bao giờ xuất hiện ở đây — precision cao, recall thấp.

Uncertainty sampling ≠ active learning cổ điển

Trong active learning, một model tự báo nó kém chắc chắn ở ví dụ nào. Ở đây không có điểm bất định của một model duy nhất; thứ ta đo là sự bất đồng giữa nhiều evaluator (hai LLM judge, hoặc judge vs check rule-based). Hệ quả thực tế: chất lượng của chiến lược này phụ thuộc vào việc các evaluator khác nhau đến đâu — một chủ đề quay lại ở pitfalls.

Trong thực tế sách khuyên trộn cả ba: random làm baseline, uncertainty bắt ca mơ hồ, failure-driven xử lý ca rõ ràng hỏng.

Chỉnh tỷ lệ theo tỷ lệ lỗi

Lỗi hiếm (< ~5%) fail 25 uncertainty 40 random 35 Khởi điểm gợi ý failure-driven 50 uncertainty 30 random 20 Lỗi phổ biến (> ~20%) failure-driven 65 unc. 20 rnd 15 Hàng giữa là gợi ý của sách; hai hàng còn lại là con số minh hoạ cho hướng điều chỉnh.
Lỗi hiếm → failure-driven cạn nhanh, dồn sang random/uncertainty. Lỗi nhiều → có đủ ca hỏng để học, random chủ yếu cho thấy cái đã chạy tốt. Đừng bao giờ bỏ phần random.
"The random portion is easy to skip but provides the only unbiased estimate of overall system quality."— Shankar & Husain, Ch.10
Ví dụ 10.9 · Cái bẫy "tỷ lệ Fail của cả batch" (số minh hoạ)

Hệ thống thật có tỷ lệ lỗi 4%. Batch tuần này 100 trace theo tỷ lệ 50/30/20. Reviewer chấm xong, trưởng nhóm tính "tỷ lệ Fail = Fail/100" và báo cáo lên trên.

LátSố traceTỷ lệ fail thật trong lát (giả định)Số Fail
Failure-driven5080% (flag khá chính xác)40
Uncertainty3040% (ca mơ hồ)12
Random204% (= tỷ lệ thật)~0,8
Cả batch100≈ 53 → "53% lỗi"
  1. 53% là con số về cách ta chọn mẫu, không phải về hệ thống. Nó sẽ tăng nếu flag tốt hơn — dù hệ thống không đổi.
  2. Ước lượng đúng chỉ đến từ lát random: ~1/20 = 5%. Nhưng 20 trace là rất ít: nếu thấy 1/20, khoảng tin cậy Wilson 95% là khoảng 0,9%–24%.
  3. Muốn ước lượng chặt hơn: cộng dồn lát random qua nhiều tuần (4 tuần × 20 = 80 trace; thấy 3/80 → khoảng 1,3%–10,5%), hoặc tăng phần random khi cần báo cáo số.
  4. Bài học cho UI và pipeline: lưu source cho mỗi nhãn, và dashboard chỉ tính "tỷ lệ lỗi hệ thống" trên source = random.
Ví dụ 10.10 · Ba tín hiệu "không chắc" trên cùng một tập trace (giả định)
TraceJudge tone (3 lần chạy, T=0,7)Judge budget (LLM)Check giá (rule)Tín hiệu
tr-201Pass · Pass · PassPassPassđồng thuận → không ưu tiên
tr-202Pass · Fail · PassPassPasschạy lại lệch → uncertainty
tr-203Pass · Pass · PassPassFail (giá $615k > $600k)rule vs LLM mâu thuẫn → uncertainty
tr-204Fail · Fail · FailPassPassqua tiêu chí này, trượt tiêu chí kia → chất lượng lấp lửng

tr-203 là ca đáng giá nhất: LLM judge "đọc" thấy email hợp lý nhưng không so số; check rule-based bắt được. Nếu pool chỉ có LLM judge, tr-203 sẽ không bao giờ được đẩy lên.

Code: dựng batch review theo tỷ lệ trộn

# sampling.py — dựng batch review theo tỷ lệ trộn failure / uncertainty / random.
import random
from collections import Counter

def disagreement(verdicts):
    """Tỷ lệ phiếu thiểu số: 0 = mọi evaluator đồng ý, 0.5 = chia đôi."""
    c = Counter(verdicts)
    return min(c.values()) / len(verdicts) if len(c) > 1 else 0.0

def build_review_batch(traces, n=40, mix=(0.5, 0.3, 0.2), seed=None):
    rng = random.Random(seed)
    n_fail, n_unc = round(n * mix[0]), round(n * mix[1])
    n_rand = n - n_fail - n_unc
    # 1) Random rút TRƯỚC, từ toàn bộ quần thể → lát này không thiên lệch.
    batch = [{**t, "source": "random"} for t in rng.sample(traces, n_rand)]
    picked = {t["id"] for t in batch}
    def take(pool, k, source):
        for t in pool:
            if k == 0: break
            if t["id"] not in picked:
                picked.add(t["id"]); batch.append({**t, "source": source}); k -= 1
        return k                                     # số suất còn thiếu
    flagged = [t for t in traces if t["flags"]]      # guardrail, 👎, lỗi tool...
    rng.shuffle(flagged)
    left = take(flagged, n_fail, "failure")
    unsure = sorted((t for t in traces if disagreement(t["verdicts"]) > 0),
                    key=lambda t: -disagreement(t["verdicts"]))
    left = take(unsure, n_unc + left, "uncertainty")  # thiếu ca fail → dồn sang
    if left:                                          # vẫn thiếu → lấp bằng random,
        rest = [t for t in traces if t["id"] not in picked]   # nhưng gắn nhãn riêng
        take(rng.sample(rest, left), left, "fill")
    rng.shuffle(batch)                               # reviewer không đoán được nguồn
    return batch

def estimate_fail_rate(labeled):
    """Chỉ dùng lát 'random' để ước lượng tỷ lệ lỗi tổng thể."""
    rand = [t for t in labeled if t["source"] == "random"]
    return sum(t["human"] == "fail" for t in rand) / len(rand), len(rand)

if __name__ == "__main__":
    rng = random.Random(7)
    traces = [{"id": f"tr-{i:04d}",
               "flags": ["thumbs_down"] if rng.random() < 0.03 else [],
               "verdicts": [rng.random() < 0.9, rng.random() < 0.88, rng.random() < 0.95]}
              for i in range(5000)]
    b = build_review_batch(traces, n=40, seed=1)
    print(Counter(t["source"] for t in b))           # {'failure': 20, 'uncertainty': 12, 'random': 8}

Ba chi tiết thiết kế đáng chú ý: (1) rút random trước từ toàn bộ quần thể — nếu rút sau, từ "phần còn lại" đã bị loại ca flagged, lát random không còn đại diện; (2) khi thiếu ca failure (lỗi hiếm), suất thừa dồn sang uncertainty — đúng tinh thần "lỗi hiếm → tăng uncertainty/random"; (3) xáo batch để reviewer không biết trace đến từ lát nào, tránh thiên kiến "trace từ hàng failure chắc là hỏng".

▶ LAB 2 · Mô phỏng chiến lược lấy mẫu🎮

Quần thể 2.000 trace giả lập với tỷ lệ lỗi ẩn. Lỗi có 3 loại: A lỗi lộ (tone, người dùng hay bấm 👎 → hay bị flag), B lỗi số liệu (LLM judge kém, check rule-based giỏi), C lỗi ẩn (nhầm khu học chính — không tín hiệu nào bắt). Mỗi chiến lược được 20 lượt review; lặp 300 lần rồi lấy trung bình.

Bấm "Mô phỏng".

Gợi ý thí nghiệm: (1) kéo tỷ lệ lỗi xuống 2%: failure-driven vẫn cho nhiều lỗi nhất mỗi lượt, nhưng gần như toàn loại A đã biết và luôn 0 lỗi C; pool flagged lúc này chỉ còn vài chục trace nên qua các tuần sẽ lặp lại đúng những lỗi đó (mô phỏng chỉ chạy một batch, nên hiện tượng "cạn" theo thời gian bạn phải tự hình dung); (2) tắt rule-based và xem cột B của Uncertainty sụt — minh hoạ pitfall "evaluator tương quan"; (3) đặt random = 0% và xem dòng ước lượng tỷ lệ lỗi biến mất, trong khi "Fail/cả batch" cho con số bi quan sai lệch. Mọi tham số của mô hình (xác suất flag, độ nhạy judge…) là giả định minh hoạ.

Hiểu lầm hay gặp

"Random lãng phí vì phần lớn là trace tốt." Đúng là phần lớn random sẽ Pass — và chính vì vậy nó là cách duy nhất (1) ước lượng tỷ lệ lỗi không thiên lệch, (2) chạm tới lỗi mà mọi tín hiệu tự động đều bỏ qua. Bỏ random, buổi review chỉ toàn edge case và cho bức tranh bi quan sai lệch. "Uncertainty sampling tự động tìm ra mọi ca khó." Nó chỉ thấy chỗ các evaluator bất đồng — nếu chúng cùng mù một chỗ, chỗ đó vô hình.
Bài tập 10.7 · Chỉnh tỷ lệ trộn

(a) Agent đặt lịch có tỷ lệ lỗi ~2%, guardrail bắt được khoảng một nửa số lỗi. Bạn có 50 lượt review/tuần. Đề xuất tỷ lệ trộn và giải thích. (b) Sau một bản prompt mới, tỷ lệ 👎 tăng lên ~25%. Bạn đổi tỷ lệ thế nào trong 2 tuần tới?

Xem lời giải

(a) 2% × traffic lớn thì số trace flagged có thể vẫn nhiều, nhưng loại lỗi flag bắt được sẽ lặp lại nhanh — sau vài buổi, failure-driven chủ yếu cho thấy lỗi đã biết. Theo sách, lỗi hiếm (<5%) → nghiêng về random + uncertainty; ví dụ 25/40/35: ~12 failure (theo dõi lỗi đã biết), ~20 uncertainty (thêm check rule-based vào pool), ~18 random (ước lượng + tìm unknown unknowns). (b) Lỗi phổ biến (>20%) → tăng failure-driven, ví dụ 65/20/15: có nhiều ca hỏng để học nguyên nhân regression; random chủ yếu sẽ cho thấy những gì vẫn chạy tốt. Giữ random ≥ 10–15% để đo xem bản sửa có kéo tỷ lệ lỗi thật xuống không.

Bài tập 10.8 · Báo cáo tỷ lệ lỗi cho sếp

Batch 60 trace (30 failure, 18 uncertainty, 12 random). Kết quả: 24/30, 7/18, 1/12 Fail. Sếp hỏi "tỷ lệ lỗi hệ thống bao nhiêu?". Bạn trả lời gì?

Xem lời giải

Không trả lời 32/60 = 53%. Ước lượng điểm từ lát random: 1/12 ≈ 8%, nhưng khoảng tin cậy Wilson 95% rất rộng (khoảng 1,5%–35%). Câu trả lời trung thực: "ước lượng ~8% nhưng rất không chắc chắn vì mới có 12 mẫu random; cộng dồn 4 tuần sẽ có ~50 mẫu để thu hẹp". Có thể nói thêm: trong 30 ca bị flag, 80% là lỗi thật → flag có precision tốt; 7/18 ca bất đồng là lỗi → nên xem các ca đó để siết tiêu chí.

10.7Nhìn theo nhóm: clustering & search

Review từng trace bắt lỗi cụ thể; nhưng nhiều lỗi chỉ lộ ra khi xem một nhóm trace cùng lúc. Review UI tốt cần công cụ khám phá mẫu hình qua các trace giống nhau — chủ yếu là clustering và search (Ch.11 đi sâu hơn).

🏷️
Cluster theo metadata
persona, feature

Mở cụm "nhà đầu tư": 4/5 email mở bằng hai đoạn xã giao, trong khi nhóm này cần tóm tắt số liệu gọn → lỗi tone có hệ thống, vô hình khi trace nằm rải rác.

🧠
Cluster ngữ nghĩa
embeddings + k-means

Lộ cụm ẩn "căn hộ giá rẻ gần trường tốt": agent bỏ qua phí HOA và dùng khoảng cách tới trường thay cho xếp hạng trường.

🔎
Search & similarity
keyword · filter · semantic

Thấy một email tự ý khuyên tài chính → semantic search tìm các email cùng lỗi dù diễn đạt khác; filter theo agent_version, user_segment, failure tag.

Kết hợp với bulk tag: xem vài trace đại diện mỗi cụm, rồi gán một failure mode chung cho cả cụm — nhanh, nhưng kiểm tra kỹ trước khi áp hàng loạt.

Ví dụ 10.11 · Cluster theo metadata làm lộ lỗi hệ thống (dữ liệu giả định)

400 trace đã chấm trong tháng, nhóm theo persona khách:

PersonaSố traceFailTỷ lệTag trội
Mua nhà lần đầu210178%sai budget (9)
Gia đình nâng cấp120119%rải rác
Nhà đầu tư702941%tone mismatch (24)
  1. Nhìn tổng: 57/400 ≈ 14% Fail — "tạm ổn". Trace nhà đầu tư nằm rải rác, mỗi buổi review chỉ gặp 1–2 cái, trông như lỗi lẻ tẻ.
  2. Nhìn theo cụm: 41% ở nhà đầu tư, và 24/29 cùng một tag. Đây là lỗi có hệ thống: agent viết email xây quan hệ trong khi nhóm này muốn bản tóm tắt số liệu gọn — đúng mẫu hình sách mô tả.
  3. Sửa một chỗ (prompt theo persona, hoặc few-shot riêng cho nhà đầu tư) thay vì vá 29 trace lẻ.

Code: cluster ngữ nghĩa để chọn đại diện

# cluster_review.py — gom trace theo nghĩa rồi chọn vài đại diện mỗi cụm để review.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
from collections import defaultdict

queries = [  # dữ liệu minh hoạ
    "căn hộ giá rẻ gần trường tốt quận 7", "chung cư rẻ gần trường cấp 1 tốt",
    "căn hộ dưới 3 tỷ gần trường quốc tế", "nhà phố cho nhà đầu tư lợi suất cho thuê",
    "đầu tư căn hộ cho thuê lợi suất cao", "so sánh lợi suất cho thuê hai dự án",
    "đặt lịch xem nhà thứ bảy", "hẹn xem căn hộ sáng chủ nhật", "dời lịch xem nhà sang tuần sau",
]
X = TfidfVectorizer(ngram_range=(1, 2)).fit_transform(queries)
km = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X)

clusters = defaultdict(list)
for q, c in zip(queries, km.labels_):
    clusters[c].append(q)
for c, qs in clusters.items():
    print(f"cụm {c} ({len(qs)} trace):", qs)
# Quy trình: đọc 3–5 trace mỗi cụm → nếu ≥ 4/5 cùng lỗi, mới tính tới bulk tag cả cụm.

Chạy thử (scikit-learn 1.8) cho ra ba cụm "gần trường", "lợi suất đầu tư", "lịch hẹn" — nhưng câu "hẹn xem căn hộ sáng chủ nhật" bị kéo vào cụm "gần trường" vì chung chữ "căn hộ". Đó là bài học thật: TF-IDF bắt từ, không bắt ý. Production nên dùng embedding của query/output, và luôn đọc đại diện trước khi tin cụm — cụm là giả thuyết, không phải kết luận.

Ví dụ 10.12 · Quy trình bulk tag an toàn (tự đề xuất)
  1. Mở cụm "căn hộ rẻ gần trường tốt" (38 trace). UI hiển thị 5 đại diện gần tâm cụm + 2 trace xa tâm nhất.
  2. Đọc 7 trace: 6/7 cùng lỗi "dùng khoảng cách tới trường thay cho xếp hạng trường"; 1 trace xa tâm thực ra là câu hỏi về học phí, không cùng lỗi.
  3. Bulk tag chỉ áp cho các trace có độ tương đồng trên ngưỡng với 6 trace lỗi; phần còn lại vào hàng review lẻ.
  4. Gắn source = "bulk" cho các nhãn này để sau có thể lọc riêng; lấy ngẫu nhiên 5 nhãn bulk để kiểm tra lại. Nếu sai > 1/5, huỷ bulk và review lẻ.
Ví dụ 10.13 · Search "tìm thêm cái giống thế này"

Reviewer thấy một email tự ý khuyên "nên vay 80% vì lãi suất sẽ giảm" — lời khuyên tài chính không được yêu cầu. Ba cách tìm thêm:

  • Keyword "lãi suất", "nên vay": ra 14 trace, nhưng bỏ sót câu diễn đạt khác như "đây là thời điểm tốt để xuống tiền".
  • Filter metadata agent_version = v2.4 AND persona = first_time: khoanh vùng xem lỗi mới xuất hiện từ version nào.
  • Semantic search với embedding của câu vi phạm: ra 31 trace, gồm cả các cách nói khác — đúng loại lỗi sách lấy làm ví dụ cho tìm kiếm theo nghĩa.

Hiểu lầm hay gặp

"Mỗi cụm = một failure mode." Không. Cụm là nhóm trace giống nhau về đầu vào/đầu ra; failure mode là kiểu hỏng. Một cụm có thể chứa 0, 1 hay nhiều failure mode, và một failure mode (vd bịa tính năng) có thể trải qua mọi cụm. Cụm giúp bạn chọn chỗ để đọc; tag mới là thứ bạn đếm.
Bài tập 10.9 · Khi nào tin bulk tag?

Cụm "hỏi phí quản lý" có 60 trace. Bạn đọc 5 đại diện: 3 thiếu phí HOA, 2 ổn. Có nên bulk tag cả cụm "thiếu chi phí ẩn" không? Nếu không, làm gì?

Xem lời giải

Không — 3/5 = 60% nghĩa là khoảng 40% nhãn bulk sẽ sai, làm bẩn golden set. Cách làm: (1) tìm đặc điểm phân biệt 3 trace lỗi (vd listing có HOA > 0 nhưng email không nhắc) → chuyển thành check rule-based chạy trên cả 60 trace; (2) review lẻ các trace check bắt được, hoặc bulk tag chỉ nhóm đó sau khi kiểm tra ngẫu nhiên vài cái; (3) check rule-based mới này đồng thời thêm một evaluator độc lập vào pool uncertainty.

10.8Đưa review vào quy trình kỹ thuật

UI chỉ có giá trị nếu được dùng đều và feedback thực sự làm hệ thống thay đổi. Không ai dùng → ma sát vẫn còn quá cao.

Batch định kỳ job ngày/tuần → Slack Alert khẩn an toàn · tool hỏng → on-call Review UI nhãn · tag · ghi chú Golden set → CI trace fail → regression test Hiệu chỉnh LLM judge người Fail, judge Pass → sửa Bug report điền sẵn chi tiết trace, 1 click Cải thiện prompt gom ghi chú → đề xuất prompt mới
Đầu vào: batch định kỳ theo chiến lược trộn + alert khẩn. Đầu ra: bốn đường khép vòng. Thêm dashboard "loại lỗi hay gặp" và "chỗ người ≠ judge" để xếp ưu tiên sửa.
Ví dụ 10.14 · Một nhịp review hằng tuần (quy trình giả định)
  1. Thứ Hai 7:00 — job chọn batch. Chạy build_review_batch(n=40, mix=(0.5,0.3,0.2)) trên trace 7 ngày qua, ghi batch vào bảng review_queue.
  2. Thứ Hai 9:00 — thông báo. Bot gửi Slack: "Batch tuần 38 sẵn sàng: 40 trace (20 flagged · 12 bất đồng · 8 random). Chia 2 người, ~20 phút mỗi người. Link: /review?batch=w38".
  3. Suốt tuần — alert khẩn. Trace có vi phạm an toàn, tool call lỗi hàng loạt hoặc regression lớn → không chờ batch, đẩy ngay cho người trực.
  4. Thứ Tư — reviewer chấm. Dùng phím tắt; Fail cần tag hoặc ghi chú; bấm G đưa ca đáng giữ vào golden set, Shift+B tạo ticket có sẵn trace id, input, output, tag.
  5. Thứ Năm — triage 20 phút. Xem dashboard: tag nào tăng, chỗ người ≠ judge, ca Defer. Chọn 1–2 việc sửa cho sprint.
  6. Thứ Sáu — khép vòng. Golden set mới vào CI (Ch.9); nếu judge lệch người, mở ticket sửa prompt judge (Ch.5).

Code: đo xem LLM judge còn khớp người không

Mỗi nhãn người trong UI cũng là một điểm dữ liệu để kiểm tra judge. Chỉ cần lưu kèm verdict của judge cho cùng trace:

# judge_calib.py — đo judge còn khớp người không, từ nhãn trong review UI.
import json

def calibration(rows):
    """rows: [{'human': 'pass'|'fail', 'judge': 'pass'|'fail'}] (bỏ qua 'defer')."""
    rows = [r for r in rows if r["human"] in ("pass", "fail")]
    tp = sum(r["human"] == "fail" and r["judge"] == "fail" for r in rows)
    fn = sum(r["human"] == "fail" and r["judge"] == "pass" for r in rows)  # judge lọt lỗi
    tn = sum(r["human"] == "pass" and r["judge"] == "pass" for r in rows)
    fp = sum(r["human"] == "pass" and r["judge"] == "fail" for r in rows)  # judge báo động giả
    return {
        "n": len(rows),
        "TPR (bắt được lỗi)": tp / max(1, tp + fn),
        "TNR (tha đúng cái tốt)": tn / max(1, tn + fp),
        "lọt lỗi → cần xem": fn,
    }

if __name__ == "__main__":
    # Số liệu minh hoạ: 60 trace người đã chấm trong tuần
    rows = ([{"human": "fail", "judge": "fail"}] * 14 + [{"human": "fail", "judge": "pass"}] * 6 +
            [{"human": "pass", "judge": "pass"}] * 37 + [{"human": "pass", "judge": "fail"}] * 3)
    print(json.dumps(calibration(rows), ensure_ascii=False, indent=1))
    # TPR = 14/20 = 0.70 → judge lọt 30% lỗi: mở 6 trace FN, xem có chung failure tag không

Kết quả minh hoạ: TPR 0,70, TNR 0,925. Sáu trace "người Fail, judge Pass" chính là đầu vào để sửa prompt judge — đúng tín hiệu sách mô tả (người liên tục đánh Fail cái judge cho Pass → sửa prompt, huấn luyện lại, hoặc thay judge). Lưu ý: nếu các nhãn này đến từ batch trộn (nhiều ca flagged), TPR/TNR vẫn dùng được vì chúng tính có điều kiện trên nhãn người; thứ không dùng được là tỷ lệ lỗi tổng.

Ví dụ 10.15 · Một bản ghi golden set sinh ra từ nút "+ Golden set"
{
  "case_id": "golden-0147",
  "from_trace": "tr-9c12",
  "input": {"query": "3 PN dưới $600k gần trung tâm, email 2 căn", "persona": "first_time"},
  "tool_fixtures": {"search_listings": "fixtures/tr-9c12/search.json"},
  "expected": {"verdict": "pass_required",
               "must_not": ["giá vượt budget", "listing không có trong kết quả tool"]},
  "tags_at_creation": ["budget_wrong", "hallucinated_listing"],
  "rubric_version": "v2",
  "added_by": "lan", "added_at": "2026-09-23"
}

Ghi lại fixture của tool để test chạy lại được trong CI mà không phụ thuộc dữ liệu listing thay đổi (Ch.9); ghi rubric_version để khi tiêu chí đổi, biết case nào cần xem lại.

Hiểu lầm hay gặp

"Golden set chỉ nên chứa trace Fail." Trace Fail là quan trọng nhất (thành regression test), nhưng cũng cần một ít trace Pass điển hình và Pass "khó" — nếu không, một bản sửa làm agent quá thận trọng (từ chối mọi thứ) vẫn qua hết test. "UI xong là xong." Sách nói rõ: nếu không ai dùng UI, ma sát vẫn còn quá cao — và review chỉ đáng giá khi làm hệ thống thay đổi.
Bài tập 10.10 · Đọc bảng calibration

Tuần này: người Fail 25 trace, judge bắt được 12 trong số đó. Người Pass 75 trace, judge đánh Fail 2 trong số đó. Tính TPR, TNR. Judge đang có vấn đề kiểu gì, và bạn làm gì tiếp theo với review UI?

Xem lời giải

TPR = 12/25 = 0,48; TNR = 73/75 ≈ 0,97. Judge quá dễ dãi: hầu như không báo động giả nhưng lọt hơn nửa số lỗi. Tiếp theo: lọc trong UI 13 trace "người Fail, judge Pass", xem tag — nếu tập trung ở một tag (vd "bịa tính năng"), judge đang không kiểm tra tiêu chí đó → thêm tiêu chí/ví dụ few-shot hoặc tách judge riêng. Đồng thời tăng tỷ trọng uncertainty sampling ở cặp judge–rule cho tag đó.

10.9Tiến hoá của một review UI, tín hiệu vận hành & DocWrangler

Sách kể lại hành trình của review UI cho trợ lý bất động sản qua ba giai đoạn. Điểm đáng học là mỗi giai đoạn sửa đúng thứ giai đoạn trước làm hỏng:

  1. Spreadsheet — export log sang Google Sheets. Query bị cắt, email là khối text liền, ghi chú nhiều người dồn một ô, kết quả tool là chuỗi JSON phải dán sang tab khác để đọc. Chậm, không cấu trúc, dễ sai.
  2. UI tối giản — Streamlit/Gradio/AI assistant: một trace mỗi màn hình, input – persona – email tách rõ, nút Pass/Fail + comment, Next/Prev. Đã khác biệt lớn.
  3. UI nâng cao — thêm tool call & retrieval, failure tag (axial code) + ghi chú tự do (open code), highlight thực thể, phím tắt, metadata, cluster view + bulk tag, search "tìm email giống thế này".

Với agent: nhìn cả tín hiệu vận hành

Ngoài text output, hiện số tool call và thứ tự gọi, số kết quả retrieve và có được dùng không, rank của tài liệu vào câu trả lời, token & cost mỗi trace, latency theo bước. Có agent trả lời đúng nhưng tốn gấp 10 lần vì lặp vô ích — chỉ text thì không thấy.
Ví dụ 10.16 · "Đúng nhưng tốn gấp 10 lần" — chỉ thấy khi UI hiện tín hiệu vận hành (giả định)
Trace bình thườngTrace tr-77e0
Email cuốiĐúng, 2 căn phù hợpĐúng, 2 căn phù hợp — giống hệt
Số tool call317 (search_listings gọi 12 lần, mỗi lần đổi 1 tham số)
Retrieve / dùng6 / 271 / 2
Token · cost4,1k · $0,01246k · $0,13
Latency3,2 s29 s (bước search chiếm 24 s)
  1. Reviewer chỉ nhìn email sẽ bấm Pass cả hai.
  2. UI có dải "tín hiệu vận hành" (số tool call, cost, latency theo bước) làm tr-77e0 nổi bật ngay: cost gấp ~10 lần.
  3. Mở chuỗi tool call: agent nới giá từng $5k một vì tool trả "0 kết quả" khi thiếu tham số quận. Đây là lỗi vòng lặp vô ích, sửa bằng mô tả tool rõ hơn hoặc giới hạn số lần gọi (Ch.8).
  4. Thêm tag mới "loop/tốn kém" và một check rule-based "tool call > 8" để đưa ca tương tự vào hàng failure-driven.

Case study: DocWrangler — review khép vòng vào prompt

DocWrangler (Shankar và cộng sự, 2025) là một IDE cho xử lý tài liệu bằng LLM. Sách dùng nó để cho thấy review UI không chỉ để đánh giá mà còn để cải thiện hệ thống: ghi chú nhẹ (nhanh hơn gán nhãn chính thức) tích luỹ thành tín hiệu đủ để đề xuất prompt mới.

  1. Xem cạnh nhau: tài liệu gốc · output trích xuất · prompt đã sinh ra nó.
  2. Open coding nhanh: ghi chú tự do kiểu "bỏ sót trường ngày", "bỏ qua bảng thứ hai".
  3. Cải thiện prompt: gom ghi chú + prompt hiện tại → hệ thống đề xuất prompt mới, hiển thị song song với bản cũ để chấp nhận / từ chối / sửa tiếp.
Ví dụ 10.17 · Vòng "ghi chú → prompt mới" kiểu DocWrangler trên bài toán trích xuất hoá đơn (giả định)
  1. Prompt v1: "Trích xuất số hoá đơn, ngày, tổng tiền, tên nhà cung cấp dưới dạng JSON." Chạy trên 30 hoá đơn scan.
  2. Review cạnh nhau (tài liệu · output · prompt), ghi chú nhanh: "lấy ngày giao hàng thay vì ngày hoá đơn" (×4), "tổng tiền chưa gồm VAT" (×3), "bỏ qua trang 2" (×2), "tên NCC lấy từ logo, sai chính tả" (×1).
  3. Bấm "cải thiện prompt": hệ thống đề xuất v2 thêm ba chỉ dẫn — ưu tiên trường có nhãn "Ngày hoá đơn"; lấy tổng sau thuế; đọc mọi trang trước khi trả lời.
  4. Xem v1 và v2 cạnh nhau: chấp nhận hai chỉ dẫn đầu, sửa chỉ dẫn thứ ba cho cụ thể hơn ("nếu có nhiều trang, tổng tiền thường ở trang cuối").
  5. Chạy lại v2 trên chính 30 hoá đơn đó + một lát mới chưa thấy, để tránh sửa prompt "khớp" riêng với các ca vừa ghi chú.
Bài tập 10.11 · Chọn tín hiệu vận hành cho UI của bạn

Agent hỗ trợ kỹ thuật dùng 4 tool: search_docs, get_ticket, run_diagnostic, escalate_to_human. Chọn 5 tín hiệu vận hành nên hiện trên dải metadata của mỗi trace và nói mỗi cái giúp bắt lỗi gì.

Xem lời giải

(1) Số tool call & thứ tự — bắt vòng lặp, gọi run_diagnostic trước khi đọc ticket. (2) Số tài liệu retrieve / được trích dẫn + rank của tài liệu được dùng — bắt retrieval kém (tài liệu đúng ở rank 9 và bị bỏ qua). (3) Có gọi escalate_to_human không — bắt escalate thừa hoặc thiếu. (4) Token & cost/trace — bắt ca đúng nhưng đắt. (5) Latency theo bước — biết bước nào làm người dùng chờ. Có thể thêm: số lượt hội thoại (Ch.6) và lỗi tool trả về.

10.10Case study tổng hợp: từ spreadsheet đến review UI trong 6 tuần

Case study · "Mái Ấm Bot" — một team giả định, số liệu minh hoạ

Bối cảnh. Startup bất động sản có trợ lý soạn email gợi ý nhà cho khách (giống ví dụ của sách), ~9.000 trace/tuần. Team 3 người (1 PM, 2 kỹ sư), ngân sách review ~4,5 giờ-người/tuần. Judge tự động: 1 LLM judge "tone", 1 LLM judge "đúng yêu cầu".

Thời gian trung bình / trace trace/tuần T0 · spreadsheet 280 s 60 T1 · nền tảng sẵn có 150 s 100 T2 · UI tối giản + phím 70 s 180 T3 · tag + highlight 42 s 240 T6 · cluster + search 38 s 280 Cùng ngân sách ~4,5 giờ-người/tuần; thời gian dư dùng cho triage và sửa lỗi.
Bước nhảy lớn nhất đến từ UI tối giản + phím tắt (T2), đúng như sách dự đoán; các tính năng sau cải thiện ít hơn về tốc độ nhưng nhiều hơn về chất lượng nhãn.

Diễn biến

TuầnThay đổiQuan sát & con số
T0Export CSV sang Google Sheets280 s/trace. Cột Notes có 11 cách viết khác nhau cho cùng một vấn đề tone ("dài dòng", "sến", "hơi thân mật"…) → không đếm được. 38% nhãn Fail không có lý do.
T1Dùng nền tảng observability sẵn có + nhật ký ma sát150 s/trace. 23 dòng ma sát; top 3: JSON tool khó đọc (9), không lọc được persona (6), phải cuộn để bấm Next (5).
T2Tự dựng UI tối giản (~60 dòng) + phím P/F/D/N70 s/trace. Reviewer tự nguyện chấm thêm; số trace/tuần tăng từ 100 lên 180 mà không ai được giao thêm giờ.
T35 failure tag từ phân tích lỗi Ch.3, Fail bắt buộc tag/ghi chú, highlight giá & địa điểm, tool call thu gọn42 s/trace. Nhãn Fail không lý do: 38% → 0%. Lỗi "giá vượt budget" bị phát hiện nhiều gấp đôi nhờ highlight.
T4Job batch hằng ngày 40 trace, trộn 50/30/20, thêm check giá rule-based vào pool evaluatorLát random (8 trace/ngày) tìm ra 2 ca "nhầm khu học chính" — không flag, không judge nào bắt. Lát uncertainty đẩy lên 9 ca rule-giá ≠ LLM-judge, 7 ca là "bỏ qua phí HOA".
T5Criteria driftSau khi xem cụm nhà đầu tư, team đổi định nghĩa tone → rubric v2. Chấm lại mù 15 trace: κ giữa hai reviewer 0,52 (v1) → 0,81 (v2). 34 nhãn cũ được mở lại, 19 đổi Pass→Fail.
T6Cluster view + bulk tag có kiểm tra + semantic search; nút "+ Golden set", "File bug"38 s/trace, 280 trace/tuần. 27 trace vào golden set. TPR của judge tone (so với nhãn người): 0,58 → sửa prompt judge theo rubric v2 → 0,83.

Kết quả & bài học

  • Tốc độ: 280 s → 38 s/trace (~7×); cùng ngân sách giờ, số trace được đọc tăng ~4,7×.
  • Lỗi mới tìm được trong 6 tuần: 4 failure mode chưa có trong danh sách ban đầu — 2 đến từ lát random, 1 từ uncertainty (nhờ check rule-based), 1 từ cluster (tone nhà đầu tư). Failure-driven chủ yếu xác nhận lỗi đã biết, nhưng là nguồn nhanh nhất để hiểu nguyên nhân.
  • Tỷ lệ lỗi báo cáo: batch trộn cho "47% Fail"; lát random cộng dồn 4 tuần (160 trace, 11 Fail) cho ~6,9% (Wilson 95%: ~3,9%–11,9%) — con số team thực sự báo cáo.
  • Sai lầm đã mắc: tuần 4 bulk tag cả cụm "hỏi phí quản lý" mà chỉ đọc 3 trace → 40% nhãn bulk sai, phải huỷ. Từ đó áp quy trình ở Ví dụ 10.12.
Bài tập 10.12 · Phân tích case study

(a) Vì sao T2 giảm thời gian nhiều nhất nhưng T3 mới là bước làm dữ liệu "dùng được"? (b) Nếu team bỏ lát random ở T4, lỗi nào sẽ không bao giờ được phát hiện, và báo cáo tỷ lệ lỗi sẽ sai theo hướng nào?

Xem lời giải

(a) T2 xoá overhead thao tác (phím tắt, một trace/màn hình) — phần lớn thời gian mất ở đó. Nhưng nhãn ở T2 vẫn là Pass/Fail + ghi chú tự do không chuẩn hoá; T3 thêm tag có cấu trúcbắt buộc lý do (error prevention) → nhãn đếm được, so được với judge, đưa được vào golden set. (b) Hai ca "nhầm khu học chính" — không flag, không judge nào bắt, nên chỉ random chạm tới được. Báo cáo sẽ không có ước lượng không thiên lệch; nếu ai đó dùng tỷ lệ Fail của batch, con số sẽ bi quan sai (~47% thay vì ~7%).

10.11Pitfalls thường gặp — và cách sửa

Nối với các chương khác

Ch.3 · Phân tích lỗi

Open code → ghi chú tự do; axial code → failure tag trong UI. Review UI là "nơi làm việc" của phân tích lỗi.

Ch.4 · Đánh giá cộng tác

Criteria drift, calibration giữa reviewer; ca Defer là nguyên liệu cho buổi thống nhất rubric.

Ch.5 · Evaluator tự động

Nhãn người = dữ liệu kiểm tra judge (TPR/TNR); bất đồng giữa evaluator = tín hiệu uncertainty.

Ch.8 · Tool & agent

Tín hiệu vận hành (số tool call, thứ tự, cost) phải hiện trong UI để bắt vòng lặp vô ích.

Ch.9 · CI/CD

Nút "+ Golden set" biến trace Fail thành regression test, kèm fixture tool.

Ch.11 · Phân tích trace

Clustering & search ở quy mô lớn — thứ review từng trace không thấy được.

10.12Áp dụng cho dự án model-routing

Router của bạn gửi mỗi request tới model phù hợp, cân bằng chất lượng / chi phí / latency. "Trace" ở đây là một quyết định route, và câu hỏi review không còn là "output có tốt không" mà là "route này có đúng không" — một câu hỏi so sánh: model rẻ hơn có đủ không, model đắt hơn có đáng không. Mọi thứ trong chương vẫn áp dụng, nhưng cần đổi hình dạng của UI.

Quyết định 12 / 60 rq-51f0 · router v0.4 · score 0,58 (ngưỡng 0,55) → model-L · source: uncertainty REQUEST · category: code Viết hàm Python gộp hai list đã sắp xếp thành một list sắp xếp, độ phức tạp O(n). ĐẶC TRƯNG ROUTER 42 token domain: code độ khó 0,58 sát ngưỡng ±0,03 model-S (rẻ) · counterfactual def merge(a, b): i = j = 0; out = [] while i < len(a) and j < len(b): ... (so sánh, append) return out + a[i:] + b[j:] judge: pass $0,0004 · 0,8 s · 180 tok model-L (đắt) · router đã chọn def merge(a, b): i, j, out = 0, 0, [] while i < len(a) and j < len(b): ... (so sánh, append) out += a[i:]; out += b[j:] judge: pass $0,0060 · 3,1 s · 210 tok Hai output tương đương về chất lượng → route này có lẽ là over-route (đắt gấp 15, chậm gấp 4). O · route OK U · nên upgrade D · có thể downgrade S · defer Tag phụ khi không OK: over-route · under-route · escalate thừa · judge chất lượng sai · request mơ hồ. N/B = tiếp/lùi
Wireframe sổ tay tự thiết kế. Khác biệt then chốt so với review UI thường: hai output đặt cạnh nhau (đã chọn vs counterfactual), cost/latency hiện ngay dưới mỗi output, và nhãn là quyết định so sánh một phím.

Ánh xạ từng ý của chương sang router

Ý trong chươngPhiên bản cho model-routing
Hiện đủ trace, thu gọn phần phụRequest + đặc trưng router + điểm/lý do route nổi bật; prompt hệ thống, log gọi API thu gọn.
Visual hierarchyHai output cạnh nhau, khác biệt được highlight (diff), cost/latency đặt ngay dưới — không bắt reviewer tự nhẩm.
Phím tắtO OK · U nên upgrade · D có thể downgrade · S defer.
Error preventionKhông cho chọn "upgrade" khi đã ở model mạnh nhất, "downgrade" khi đã ở model rẻ nhất.
Metadata ngữ cảnhRouter version, ngưỡng, category, tenant — để thấy regression khi đổi ngưỡng.
Failure-driven samplingFallback/escalate, 👎 của người dùng, retry, output bị judge chất lượng Fail.
Uncertainty samplingĐiểm router sát ngưỡng; judge chất lượng bất đồng giữa hai output; hai router (vd rule + learned) chọn khác nhau.
RandomƯớc lượng không thiên lệch: tỷ lệ route OK, tỷ lệ over/under-route, từ đó tính tiền tiết kiệm được.
ClusteringTheo category/độ dài/ngôn ngữ — router hay lệch có hệ thống ở một cụm (vd mọi câu hỏi SQL đều bị đẩy lên model-L).
Golden setQuyết định đã được người xác nhận → test CI: "request kiểu X phải route về model ≤ Y và chất lượng ≥ Z".

Điểm khác biệt quan trọng: counterfactual tốn tiền

Để biết "model-S có đủ không" cho một request đã được route lên model-L, bạn phải chạy thêm model-S (và ngược lại). Đừng chạy counterfactual cho mọi request — chỉ chạy cho các trace đã được chọn vào batch review (40–60/tuần), chi phí không đáng kể. Với request có side-effect (tool call ghi dữ liệu), chạy counterfactual ở chế độ dry-run hoặc chỉ so phần text.

Code: biến nhãn route thành con số ra quyết định

# routing_review.py — biến nhãn review quyết định route thành con số hành động được.
# Nhãn: ok | upgrade (model rẻ không đủ tốt) | downgrade (model đắt là thừa) | defer
from collections import Counter, defaultdict

PRICE = {"small": 0.0004, "large": 0.0060}   # $/request trung bình — số minh hoạ

def routing_report(labels, weekly_volume):
    lab = [r for r in labels if r["label"] != "defer"]
    c = Counter(r["label"] for r in lab)
    n = len(lab)
    small = [r for r in lab if r["routed"] == "small"]
    large = [r for r in lab if r["routed"] == "large"]
    up_rate = sum(r["label"] == "upgrade" for r in small) / max(1, len(small))
    down_rate = sum(r["label"] == "downgrade" for r in large) / max(1, len(large))
    share_large = len(large) / n          # chỉ đúng nếu labels đến từ lát RANDOM
    saving = weekly_volume * share_large * down_rate * (PRICE["large"] - PRICE["small"])
    extra = weekly_volume * (1 - share_large) * up_rate * (PRICE["large"] - PRICE["small"])
    by_cat = defaultdict(Counter)
    for r in lab: by_cat[r["category"]][r["label"]] += 1
    return {"n": n, "ok_rate": c["ok"] / n,
            "under-route (small→nên large)": round(up_rate, 3),
            "over-route (large→small đủ)": round(down_rate, 3),
            "tiết kiệm nếu sửa over-route ($/tuần)": round(saving, 2),
            "chi thêm nếu sửa under-route ($/tuần)": round(extra, 2),
            "theo loại request": {k: dict(v) for k, v in by_cat.items()}}

if __name__ == "__main__":
    demo = ([{"routed": "small", "label": "ok", "category": "faq"}] * 22 +
            [{"routed": "small", "label": "upgrade", "category": "code"}] * 4 +
            [{"routed": "large", "label": "ok", "category": "code"}] * 9 +
            [{"routed": "large", "label": "downgrade", "category": "faq"}] * 5 +
            [{"routed": "large", "label": "defer", "category": "legal"}] * 2)
    for k, v in routing_report(demo, weekly_volume=200_000).items(): print(k, "=", v)

Chạy với dữ liệu minh hoạ: 40 nhãn hợp lệ, OK 77,5%; under-route 15,4% (4/26 request vào model-S), over-route 35,7% (5/14 request vào model-L). Với 200k request/tuần, sửa over-route tiết kiệm ~$140/tuần, sửa under-route tốn thêm ~$112/tuần. Cột "theo loại request" cho thấy under-route đều là code, over-route đều là faq → hai chỉnh ngưỡng theo category, không phải một ngưỡng chung. Cảnh báo: các tỷ lệ này chỉ ngoại suy được nếu nhãn đến từ lát random — đúng bài học Ví dụ 10.9.

▶ LAB 3 · Review quyết định routing🎮

8 quyết định route giả định (ngưỡng 0,55: điểm ≥ ngưỡng → model-L). Bấm vào khung lab rồi dùng O OK · U nên upgrade · D có thể downgrade · S defer · N/B tiếp/lùi.

Chưa có nhãn.
Ví dụ 10.18 · Tỷ lệ trộn cho router (đề xuất)
  1. Route sai thường hiếmkhó thấy: under-route lộ qua 👎/retry, nhưng over-route không bao giờ sinh tín hiệu lỗi — người dùng hài lòng, chỉ hoá đơn là đắt. → failure-driven gần như mù với over-route.
  2. Vì vậy nghiêng về uncertainty + random, ví dụ 25/40/35: 25% từ fallback/👎/retry; 40% từ điểm sát ngưỡng và ca hai judge chất lượng bất đồng; 35% random để ước lượng tỷ lệ over/under-route và tiền.
  3. Thêm một evaluator rule-based vào pool uncertainty: vd "request < 60 token, category faq mà route lên model-L" — đúng tinh thần pitfall evaluator tương quan.
  4. Mỗi đợt đổi ngưỡng: tạm tăng lát "sát ngưỡng mới" trong 1–2 tuần — đó là vùng quyết định vừa bị dịch chuyển.
Bài tập 10.13 · Thiết kế buổi review routing đầu tiên

Router của bạn có 3 model (S, M, L), ngưỡng hai tầng. Bạn có 90 phút/tuần. (a) Mỗi quyết định mất bao lâu để review nếu UI như wireframe? Bao nhiêu quyết định/tuần? (b) Cần chạy counterfactual cho model nào? (c) Chọn 3 nhãn một-phím và 3 tag phụ.

Xem lời giải

(a) Hai output cạnh nhau + diff: ước ~60–75 s/quyết định (nhiều hơn trace thường vì phải so sánh) → khoảng 70–90 quyết định/tuần. (b) Chỉ cần model liền kề: request ở M → chạy S và L; ở S → chạy M; ở L → chạy M. Không cần so S với L trực tiếp vì câu hỏi là "lên/xuống một bậc có đáng không". (c) Nhãn: O OK · U lên một bậc · D xuống một bậc (+ S defer). Tag phụ: "khác biệt chất lượng nhỏ nhưng quan trọng với user" (vd format), "judge chất lượng chấm sai", "latency là vấn đề chính". Nhớ ghi sourcerouter_version cho mỗi nhãn.

Checklist triển khai cho router

  • Log mỗi quyết định: request, đặc trưng, điểm, ngưỡng, model chọn, router version, cost, latency, 👎/retry/fallback.
  • Job tuần: chọn batch 40–60 theo trộn ~25/40/35, chạy counterfactual chỉ cho batch, lưu cả hai output.
  • UI: hai cột + diff, cost/latency dưới mỗi cột, phím O/U/D/S, cấm nhãn vô nghĩa (upgrade ở model cao nhất).
  • Báo cáo: tỷ lệ OK/over/under chỉ từ lát random kèm khoảng tin cậy; tiền tiết kiệm ước tính; bảng theo category.
  • Khép vòng: quyết định đã xác nhận → golden set cho CI của router; ca "judge chất lượng sai" → sửa judge dùng trong routing eval.

Tự kiểm tra

1. Nêu hai lý do con người vẫn phải đọc trace dù đã có evaluator tự động.
(1) Bắt lỗi mà evaluator bỏ sót — evaluator chỉ đo tiêu chí đã viết ra; (2) giữ evaluator được hiệu chỉnh khi định nghĩa "chất lượng" của team thay đổi (criteria drift, Ch.4).
2. Nếu chỉ được làm một tính năng cho review UI, nên làm gì trước? Vì sao?
Phím tắt (tối thiểu Next/Pass/Fail/Defer). Sách coi đây là đòn bẩy tốc độ lớn nhất — nhanh hơn bấm chuột 2–3 lần — vì nó xoá "overhead thao tác" giữa các lần đọc.
3. Progressive disclosure áp vào việc gán nhãn trông như thế nào?
Ban đầu chỉ hiện Pass / Fail / Defer; danh sách failure tag chi tiết chỉ hiện khi chọn Fail. Tương tự, tool call/retrieval thu gọn mặc định, bấm mới mở.
4. Vì sao nút Defer quan trọng với chất lượng dataset?
Nó cho reviewer lối thoát ở ca không chắc thay vì đoán mò. Nhãn đoán mò là nhiễu, kéo thấp trần đo lường khi dùng dataset để kiểm tra judge; ca Defer lại là nguyên liệu tốt cho buổi thống nhất rubric.
5. EvalGen phát hiện điều gì về người review, và UI cần hỗ trợ gì?
Người review cũng bị criteria drift: định nghĩa lại "đúng", sửa nhãn cũ, thêm loại lỗi mới. UI cần cho sửa nhãn cũ, thêm failure category giữa chừng, cập nhật rubric — nếu không, nhãn đầu và cuối buổi theo hai chuẩn khác nhau.
6. Vì sao failure-driven sampling không đủ một mình?
Nó chỉ thấy những gì tín hiệu hiện có đã được cấu hình để bắt: precision cao, recall thấp. Lỗi lọt qua mọi tín hiệu (unknown unknowns, vd nhầm khu học chính) chỉ lộ ra khi xem cả trace mà hệ thống cho là ổn — tức cần random.
7. Hệ thống có tỷ lệ lỗi ~2%. Chỉnh tỷ lệ trộn so với mốc 50/30/20 thế nào?
Giảm failure-driven (sẽ cạn ca lỗi nhanh), tăng random và uncertainty. Ngược lại, nếu lỗi > ~20% thì tăng failure-driven. Không bao giờ bỏ hẳn random.
8. Batch trộn 50/30/20 có 48% Fail. Có được báo cáo "tỷ lệ lỗi hệ thống là 48%" không?
Không. Con số đó phản ánh cách chọn mẫu (dồn vào ca flagged và ca mơ hồ). Chỉ lát random cho ước lượng không thiên lệch — và cần kèm khoảng tin cậy vì lát này thường nhỏ; cộng dồn qua nhiều tuần để thu hẹp.
9. Pool evaluator gồm 3 LLM judge có prompt gần giống nhau. Vấn đề gì với uncertainty sampling, và sửa ra sao?
Các judge tương quan cao → đồng ý/bất đồng ở cùng chỗ, bỏ trống nguyên loại lỗi (vd lỗi số liệu mà cả ba cùng đọc lướt). Thêm ít nhất một evaluator rule-based để có góc nhìn độc lập.
10. Trong review UI cho model-routing, vì sao failure-driven gần như mù với over-route, và nên bù bằng gì?
Over-route (dùng model đắt khi model rẻ đủ) cho ra output tốt → không có 👎, retry hay fallback nào. Bù bằng random (ước lượng tỷ lệ over-route và tiền) và uncertainty (điểm router sát ngưỡng, rule-based "request ngắn/faq mà lên model đắt"), kèm counterfactual của model rẻ hơn chạy cho batch review.