Giao diện cho review thủ công: đọc trace nhanh hơn, đúng chỗ hơn
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:
- Đ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.
- Đị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.
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?"
- 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. - Đố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.
- Đọ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.
- 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.
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ần | 200 × 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.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át | Số lần | Ví dụ ghi chép | Việ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 persona | 6 | "muốn xem riêng khách nhà đầu tư" | Filter metadata |
| Phải cuộn xuống mới bấm được Next | 5 | "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ậm | 1 | "trace dài load 8 giây" | Để sau |
- 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.
- 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).
- "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.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.
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:
Đang xem trace nào, còn bao nhiêu, đã gửi feedback chưa.
Dùng đúng từ trong rubric và tiêu chí eval của team.
Next/Prev, undo, defer; không khoá cứng ghi chú.
Layout, thuật ngữ, hành vi giống nhau giữa các trace.
Nút nhãn tách biệt rõ; bắt buộc ghi lý do khi chọn Fail.
Hiện sẵn danh sách failure mode + giải thích.
Phím tắt cho người quen, vẫn dễ cho người mới.
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ó".
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át | Heuristic bị vi phạm | Sửa |
|---|---|---|
| Không biết đang ở trace số mấy, còn bao nhiêu | Visibility of system status | Header "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 được | User control & freedom | Prev + mở lại nhãn cũ |
| Trace email có layout khác trace lịch hẹn, nút nằm chỗ khác | Consistency | Khung 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 prevention | Bắ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.3 | Recognition > recall | Checklist tag hiện sẵn, có tooltip định nghĩa |
| Mọi thao tác đều phải click | Flexibility & efficiency | Phím tắt P/F/D/N + số 1–9 cho tag |
| Hiện cả system prompt 3.000 token ở đầu mọi trace | Minimalist design | Thu gọn mặc định; output nổi nhất |
- 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.
- Đ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.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:
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ữ".
(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.
| # | Chi tiết trong wireframe | Nguyên tắc / tính năng |
|---|---|---|
| 1 | "Trace 37/120", thanh tiến độ, agent version, persona | Visibility of status · progress indicator · contextual metadata (thấy regression theo version) |
| 2 | Email 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ận | Visual hierarchy · inline annotation (chỉ đúng câu sai) |
| 3 | Tool call thu gọn, bấm để mở tham số/kết quả | Progressive disclosure · minimalist |
| 4 | Danh sách failure tag chỉ hiện khi chọn Fail; thêm tag mới được | Recognition > recall · progressive disclosure · hỗ trợ criteria drift |
| 5 | Ghi chú bắt buộc khi Fail, có template comment | Error prevention · feedback template · open coding |
| 6 | Thanh phím tắt | Keyboard shortcuts — đòn bẩy tốc độ số 1 |
| 7 | Số tool call, retrieve/dùng, token, cost, latency | Tí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"
}
- Append-only +
supersedes: sửa nhãn không xoá lịch sử → đo được ai đổi ý, đổi ở đâu (tín hiệu criteria drift, §10.5). rubric_version: khi định nghĩa "tone" thay đổi, bạn biết nhãn nào cần xem lại.source: bắt buộc nếu muốn ước lượng tỷ lệ lỗi đúng cách — chỉ nhãn cósource = randommới dùng để ước lượng (§10.6).deferlà một verdict riêng, không phải "chưa chấm" — để biết ca nào đang chờ thảo luận.
- 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. - 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.
- 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".
- 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. - 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.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.
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.
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 · 1–4 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ú.
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:
- Buổi review 200 trace, trong đó ~20 trace (10%) reviewer thật sự không chắc.
- Không có Defer → reviewer đoán. Giả sử đoán đúng ~50% → khoảng 10 nhãn sai = 5% dataset nhiễu.
- 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.
- 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.
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).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; 1–6 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.
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.
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.
- 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ự.
- Reviewer chấm lại. So hai lượt: 7/10 giữ nguyên, 3 trace đổi từ Pass → Fail (số minh hoạ).
- 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.
- 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).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:
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
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át | Số trace | Tỷ lệ fail thật trong lát (giả định) | Số Fail |
|---|---|---|---|
| Failure-driven | 50 | 80% (flag khá chính xác) | 40 |
| Uncertainty | 30 | 40% (ca mơ hồ) | 12 |
| Random | 20 | 4% (= tỷ lệ thật) | ~0,8 |
| Cả batch | 100 | ≈ 53 → "53% lỗi" |
- 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.
- Ướ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%.
- 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ố.
- Bài học cho UI và pipeline: lưu
sourcecho mỗi nhãn, và dashboard chỉ tính "tỷ lệ lỗi hệ thống" trênsource = random.
| Trace | Judge tone (3 lần chạy, T=0,7) | Judge budget (LLM) | Check giá (rule) | Tín hiệu |
|---|---|---|---|---|
| tr-201 | Pass · Pass · Pass | Pass | Pass | đồng thuận → không ưu tiên |
| tr-202 | Pass · Fail · Pass | Pass | Pass | chạy lại lệch → uncertainty |
| tr-203 | Pass · Pass · Pass | Pass | Fail (giá $615k > $600k) | rule vs LLM mâu thuẫn → uncertainty |
| tr-204 | Fail · Fail · Fail | Pass | Pass | qua 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".
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.
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 có bất đồng — nếu chúng cùng mù một chỗ, chỗ đó vô hình.(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.
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).
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.
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.
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.
400 trace đã chấm trong tháng, nhóm theo persona khách:
| Persona | Số trace | Fail | Tỷ lệ | Tag trội |
|---|---|---|---|---|
| Mua nhà lần đầu | 210 | 17 | 8% | sai budget (9) |
| Gia đình nâng cấp | 120 | 11 | 9% | rải rác |
| Nhà đầu tư | 70 | 29 | 41% | tone mismatch (24) |
- 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ẻ.
- 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ả.
- 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.
- 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.
- Đọ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.
- 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ẻ.
- 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ẻ.
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.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.
- 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ảngreview_queue. - 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".
- 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.
- 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.
- 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.
- 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.
{
"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.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:
- 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.
- 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.
- 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.| Trace bình thường | Trace 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 call | 3 | 17 (search_listings gọi 12 lần, mỗi lần đổi 1 tham số) |
| Retrieve / dùng | 6 / 2 | 71 / 2 |
| Token · cost | 4,1k · $0,012 | 46k · $0,13 |
| Latency | 3,2 s | 29 s (bước search chiếm 24 s) |
- Reviewer chỉ nhìn email sẽ bấm Pass cả hai.
- 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.
- 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).
- 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.
- Xem cạnh nhau: tài liệu gốc · output trích xuất · prompt đã sinh ra nó.
- Open coding nhanh: ghi chú tự do kiểu "bỏ sót trường ngày", "bỏ qua bảng thứ hai".
- 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.
- 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.
- 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).
- 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.
- 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").
- 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ú.
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
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".
Diễn biến
| Tuần | Thay đổi | Quan sát & con số |
|---|---|---|
| T0 | Export CSV sang Google Sheets | 280 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. |
| T1 | Dùng nền tảng observability sẵn có + nhật ký ma sát | 150 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). |
| T2 | Tự dựng UI tối giản (~60 dòng) + phím P/F/D/N | 70 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ờ. |
| T3 | 5 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ọn | 42 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. |
| T4 | Job batch hằng ngày 40 trace, trộn 50/30/20, thêm check giá rule-based vào pool evaluator | Lá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". |
| T5 | Criteria drift | Sau 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. |
| T6 | Cluster 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.
(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úc và bắ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
- Đọc quá ít trace. Xem vài ví dụ sau mỗi thay đổi rồi thôi. Lỗi tinh vi chỉ lộ sau hàng chục–hàng trăm trace; lỗi hiếm (dưới 1%, như gửi email nhầm người) vẫn có thể rất đắt. Sửa: mỗi buổi review có một mẫu ngẫu nhiên từ các trace đã pass mọi check tự động — cách duy nhất để thấy điểm mù của evaluator.
- Dựa quá nhiều vào bất đồng giữa evaluator. Uncertainty sampling chỉ đa dạng bằng mức độ không tương quan của các evaluator; nếu tất cả là LLM judge có prompt na ná, chúng đồng ý và bất đồng ở cùng chỗ → cả loại lỗi không bao giờ được lấy mẫu. Sửa: thêm ít nhất một evaluator rule-based vào pool (thử trong Lab 2).
- Xem nhẹ UI tuỳ biến. Nền tảng có sẵn là khởi đầu hợp lý, nhưng khi quy trình chín, view theo domain, phím tắt cho annotation hay dùng và diff giữa các phiên bản prompt làm reviewer nhanh và nhất quán hơn rõ rệt. Sửa: nhật ký ma sát → build đúng thứ đang cản.
- Không cho sửa nhãn cũ. Criteria drift âm thầm làm dataset mất nhất quán. Sửa: nhãn append-only có
supersedes,rubric_version; chấm lại mù định kỳ. - Bỏ phần random, hoặc tính tỷ lệ lỗi trên cả batch trộn. Buổi review toàn edge case → bức tranh bi quan sai lệch. Sửa: lưu
source, chỉ ước lượng trên lát random, kèm khoảng tin cậy. - (Tự bổ sung) Phím tắt cướp phím khi đang gõ. Reviewer gõ ghi chú và vô tình kích hoạt Fail/Next. Sửa: bỏ qua phím khi focus ở ô nhập; Esc để thoát.
- (Tự bổ sung) Bulk tag không kiểm tra. Gán nhãn cả cụm sau khi đọc 2–3 trace → hàng chục nhãn sai vào golden set. Sửa: đọc đại diện + trace xa tâm, gắn
source = bulk, kiểm tra ngẫu nhiên. - (Tự bổ sung) Review không dẫn tới hành động. Nhãn nằm trong DB, không ai xem. Sửa: nút golden set/file bug ngay trong UI, triage định kỳ, dashboard "người ≠ judge".
Nối với các chương khác
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.
Criteria drift, calibration giữa reviewer; ca Defer là nguyên liệu cho buổi thống nhất rubric.
Nhãn người = dữ liệu kiểm tra judge (TPR/TNR); bất đồng giữa evaluator = tín hiệu uncertainty.
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.
Nút "+ Golden set" biến trace Fail thành regression test, kèm fixture tool.
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.
Ánh xạ từng ý của chương sang router
| Ý trong chương | Phiê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 hierarchy | Hai 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ắt | O OK · U nên upgrade · D có thể downgrade · S defer. |
| Error prevention | Không cho chọn "upgrade" khi đã ở model mạnh nhất, "downgrade" khi đã ở model rẻ nhất. |
| Metadata ngữ cảnh | Router version, ngưỡng, category, tenant — để thấy regression khi đổi ngưỡng. |
| Failure-driven sampling | Fallback/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. |
| Clustering | Theo 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 set | Quyế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.
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.
- Route sai thường hiếm và khó 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.
- 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.
- 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.
- 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.
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 source và router_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.