Phân tích lỗi (Error Analysis): từ trace thô đến taxonomy lỗi
TL;DR
- Pha Analyze: gom ~100 trace → open coding (ghi chú tự do về lỗi đầu tiên) → axial coding (gom thành failure mode) → gán nhãn 0/1 → tính tỷ lệ (prevalence) → lặp 2–3 vòng.
- Điểm dừng không phải một con số cố định mà là bão hoà (saturation): đọc thêm không còn lộ loại lỗi mới.
- Không có trace thật? Đừng bảo LLM "sinh 50 query" — hãy định nghĩa dimension → sinh tuple → mỗi tuple thành một query.
- Sản phẩm quý nhất là taxonomy lỗi đặc thù app của bạn — đừng để LLM hay metric "bán sẵn" nghĩ hộ. LLM chỉ nên hỗ trợ gom nhóm và gán nhãn, không làm open coding.
- Trang này có 4 lab tương tác (open coding, sinh tuple, bão hoà, khoảng tin cậy), 1 case study 20 trace, code Python chạy được và một mục riêng cho dự án model-routing.
Cách đọc trang này
Phần tóm tắt nội dung sách là diễn giải ngắn bằng lời của người tóm tắt. Toàn bộ ví dụ (trace, số liệu, tên khách, bảng đếm), code, bài tập, case study và lab là tài liệu tự viết để minh hoạ — số liệu là giả định, không lấy từ sách.3.1Bức tranh toàn cảnh pha Analyze
Trong vòng đời Analyze → Measure → Improve (chương 1), Analyze là lúc ta nhìn xem hệ thống thực sự làm gì và hỏng ở đâu — trước khi viết bất kỳ evaluator nào. Lý do rất thực dụng: nếu chưa biết hệ thống hỏng theo những kiểu nào, bạn sẽ đo những thứ "nghe hay" (độ lưu loát, "helpfulness") thay vì những thứ đang thật sự làm người dùng khổ.
Ví dụ xuyên suốt của sách (và của các chương sau): một trợ lý CRM bất động sản giúp môi giới tìm listing, phân tích thị trường nhanh, soạn email cho khách và xem lịch dẫn đi xem nhà. Người dùng nói kiểu hội thoại, mơ hồ; hệ thống là agentic — LLM tự chọn bước tiếp theo (query listing → soạn email → xem lịch…), kết quả tool quay lại cho LLM, lặp đến khi xong. Chỉ một yêu cầu ngắn cũng có thể kéo theo nhiều bước và nhiều ràng buộc phải giữ suốt chuỗi. Trong trang này, các ví dụ tự viết đặt bối cảnh CRM đó ở thị trường Việt Nam (quận, tỷ đồng, tên khách Việt) cho dễ hình dung.
Trực giác: bác sĩ đọc bệnh án trước khi kê đơn
Hãy nghĩ tới một phòng khám mới mở. Bác sĩ giỏi không bắt đầu bằng việc mua sẵn 20 loại xét nghiệm "phổ biến". Họ khám trực tiếp vài chục bệnh nhân đầu tiên, ghi chú những gì thấy (open coding), rồi mới nhận ra "à, phần lớn là viêm họng, có một nhóm dị ứng theo mùa, và hai ca rất nặng cần quy trình riêng" (axial coding). Chỉ sau đó họ mới quyết định cần xét nghiệm nào (evaluator, chương 5) và đo tần suất (prevalence). Error analysis chính là "khám lâm sàng" cho hệ thống LLM.
Thuật ngữ nền
Input gốc gửi vào hệ thống — có thể từ người gõ, pipeline tự động, cron job, hay hệ thống khác gọi API.
Mọi bước hệ thống làm cho một query: message LLM, tool call, giá trị trả về, câu trả lời cuối — theo đúng thứ tự.
Ghi chú ngắn, tự do, mô tả điều sai bạn quan sát được trong một trace — chưa phải category.
Một kiểu hỏng tách biệt, lặp lại, có định nghĩa rõ — đủ cụ thể để gán nhãn 0/1.
Tập các failure mode có cấu trúc — từ vựng chung của team về "hệ thống hỏng thế nào".
Điểm mà đọc thêm trace không còn lộ ra loại lỗi mới → đủ để dừng vòng này.
Team CRM có 3 kỹ sư và 1 môi giới cố vấn. Họ dành đúng một buổi chiều cho vòng Analyze đầu tiên:
- 13:00–13:40 — Xuất 100 trace từ log 2 tuần (lấy mẫu phân tầng theo tính năng + 20 trace có thumbs-down) ra Google Sheet, mỗi dòng một trace.
- 13:40–15:30 — Mỗi người đọc ~25 trace, viết open code cho lỗi đầu tiên + cột Đạt/Không đạt. Kết quả: 41 trace "Không đạt", 41 ghi chú thô.
- 15:30–16:10 — Cùng ngồi gom 41 ghi chú thành 6 failure mode (có dùng LLM gợi ý nhóm, rồi sửa tay 3 chỗ).
- 16:10–16:50 — Thêm 6 cột 0/1, gán nhãn lại toàn bộ 100 trace, dùng pivot table đếm.
- 16:50–17:00 — Quyết định: 2 mode sửa ngay bằng prompt, 1 mode cần evaluator tự động (chương 5), 1 mode cần thêm dữ liệu cho vòng 2.
Điểm đáng chú ý: 80% thời gian là đọc. Không có dòng code evaluator nào được viết trong buổi này — và đó là đúng.
Nhầm lẫn thường gặp · "Error analysis = debug"
Debug là đi từ một lỗi đã biết tới nguyên nhân. Error analysis là đi từ nhiều trace chưa biết gì tới bản đồ các loại lỗi và tần suất của chúng. Debug trả lời "vì sao trace này hỏng?"; error analysis trả lời "hệ thống của tôi hỏng theo những kiểu nào, kiểu nào nhiều nhất?". Trong lúc open coding, bạn cố tình không debug — chỉ ghi nhận.Hoạt động nào dưới đây thuộc pha Analyze, hoạt động nào thuộc Measure/Improve? (a) Đọc 80 trace và ghi chú; (b) viết LLM-judge kiểm tra "email có đúng giọng persona"; (c) gom 30 ghi chú thành 5 nhóm; (d) sửa system prompt để luôn hỏi lại khi địa danh mơ hồ; (e) đếm bao nhiêu % trace dính "bịa giá".
Xem lời giải
(a), (c), (e) thuộc Analyze — đếm prevalence ban đầu là bước cuối của Analyze. (b) thuộc Measure (xây evaluator tự động, chương 5). (d) thuộc Improve. Lưu ý (e) nằm ranh giới: đếm thủ công trên bộ trace vừa đọc là Analyze; đo liên tục bằng evaluator trên production là Measure.
3.2Trace — đơn vị để đọc
Sách nhấn mạnh: ta đánh giá trace, không đánh giá từng bước riêng lẻ. Hành vi của một agent chỉ có nghĩa khi nhìn trọn từ đầu tới cuối; một message nhìn riêng có thể hoàn toàn hợp lý mà vẫn dẫn tới kết quả sai. Đọc cả trace giúp tìm ra chỗ lỗi khởi nguồn, thay vì đếm các triệu chứng phía sau như những lỗi độc lập.
Trace dưới đây (tự viết) ở dạng JSON rút gọn — dạng bạn thường thấy khi export từ nền tảng observability:
{"trace_id": "tr-017",
"query": "Gửi chị Lan 2 căn 2PN dưới 3 tỷ ở Thủ Đức đẹp nhất nhé",
"steps": [
{"type": "llm", "out": "Kế hoạch: search_listings → lookup_contact → draft_email"},
{"type": "tool", "name": "search_listings",
"args": {"district": "Thủ Đức", "bedrooms": 2, "max_price": 3.0e9},
"result": "5 listings"},
{"type": "tool", "name": "lookup_contact", "args": {"name": "Lan"},
"result": [{"id": 88, "name": "Trần Lan", "role": "seller"},
{"id": 131, "name": "Nguyễn Thị Lan", "role": "buyer"}]},
{"type": "tool", "name": "draft_email", "args": {"to": 88, "listings": ["L2", "L5"]}},
{"type": "llm", "out": "Em đã soạn email gửi chị Lan với 2 căn đẹp nhất."}]}
- Đọc query, tự hỏi "người dùng muốn gì?" — 3 ràng buộc (2PN, < 3 tỷ, Thủ Đức), 1 hành động (soạn email), 1 người nhận ("chị Lan" — khách của môi giới, tức người mua).
- Đi từng bước, đối chiếu với ý định: kế hoạch ổn; search giữ đủ 3 ràng buộc; lookup trả về 2 contact trùng tên.
- Dừng ở chỗ sai đầu tiên: agent chọn id 88 (vai trò
seller) mà không hỏi lại. Đây là lỗi thượng nguồn nhất. - Không liệt kê hệ quả như lỗi riêng: email "gửi nhầm người" và câu trả lời cuối "đã soạn cho chị Lan" chỉ là triệu chứng kéo theo.
- Viết open code: "lookup 'Lan' ra 2 contact (1 chủ nhà, 1 khách mua); agent lấy cái đầu = chủ nhà, không hỏi lại → email báo giá gửi nhầm người". Đánh Không đạt.
Tính không tất định (nondeterminism)
Vì output của LLM không tất định, chạy cùng một query hai lần có thể ra hai trace khác nhau — một trace chỉ là một đường chạy có thể. Khi một lỗi có vẻ "lúc có lúc không", hãy chạy lại cùng query vài lần để xem tần suất.
Query "căn hộ 2PN dưới 3 tỷ quận 7 cho nuôi mèo" chạy 10 lần. Kết quả: 7 lần SQL có đủ điều kiện pets_allowed = true, 3 lần mất điều kiện này. Nếu bạn chỉ xem 1 trace, xác suất 70% bạn kết luận "không có lỗi". Cách ghi chép hợp lý: open code vẫn ghi "thỉnh thoảng rơi điều kiện thú cưng (3/10 lần chạy lại)" — con số 3/10 giúp đồng đội hiểu đây là lỗi generalization (lúc được lúc không) chứ không phải lỗi specification (prompt không hề nói tới thú cưng) — hai loại lỗi mà chương 1 phân biệt.
Đưa trace về dạng dễ annotate
Sách khuyên xuất trace từ nền tảng observability (Braintrust, LangSmith, Weave…) hoặc log JSON/DB, và nhiều team thấy spreadsheet — mỗi dòng một trace — là dễ gán nhãn nhất. Định dạng không quan trọng bằng việc trace đầy đủ. Đoạn code tự viết dưới đây "làm phẳng" file JSONL như ví dụ trên thành CSV có sẵn cột để ghi chú:
# flatten_traces.py — JSONL trace → CSV để annotate (code minh hoạ, tự viết)
import csv, json, sys
def summarize_step(step: dict) -> str:
if step["type"] == "tool":
args = json.dumps(step.get("args", {}), ensure_ascii=False)
res = step.get("result", "")
if isinstance(res, list):
res = f"{len(res)} kết quả: " + "; ".join(r.get("name", str(r)) for r in res)
return f"[tool] {step['name']}({args}) → {res}"
return f"[llm] {step.get('out', '')}"
def flatten(path_in: str, path_out: str) -> int:
n = 0
with open(path_in, encoding="utf-8") as f, \
open(path_out, "w", newline="", encoding="utf-8") as g:
w = csv.writer(g)
w.writerow(["trace_id", "query", "steps", "final", "open_code", "verdict"])
for line in f:
if not line.strip():
continue
t = json.loads(line)
steps = [summarize_step(s) for s in t["steps"]]
final = t["steps"][-1].get("out", "") if t["steps"] else ""
# để trống 2 cột cuối cho người annotate
w.writerow([t["trace_id"], t["query"], "\n".join(steps), final, "", ""])
n += 1
return n
if __name__ == "__main__":
print(flatten(sys.argv[1], sys.argv[2]), "trace đã ghi")
Mẹo nhỏ: giữ cột steps ở dạng nhiều dòng trong một ô (Google Sheets/Excel hiển thị được), đừng tách mỗi bước ra một cột — trace dài ngắn khác nhau sẽ làm bảng loạn.
Nhầm lẫn thường gặp · "Query chỉ là tin nhắn người dùng gõ"
Query là bất kỳ input gốc nào kích hoạt hệ thống: tin nhắn chat, một job định kỳ ("mỗi sáng tóm tắt lead mới"), webhook từ hệ thống khác. Nếu bạn chỉ lấy mẫu trace từ chat, bạn bỏ sót cả một mảng hành vi — ví dụ job tóm tắt tự động chạy lúc 6h sáng khi dữ liệu lịch chưa đồng bộ.Trace (tự viết): query "Đặt lịch xem căn Q2 cho anh Bình chiều mai, rồi nhắn anh ấy giờ hẹn". Các bước: (1) LLM hiểu "chiều mai" = 13:00–18:00 ngày 24/9 ✓; (2) check_calendar trả về trống 15:00–16:00; (3) create_event tạo lịch 10:00 ngày 24/9; (4) send_sms tới anh Bình "hẹn anh 10h sáng mai"; (5) trả lời: "Đã đặt lịch và nhắn anh Bình". Lỗi đầu tiên ở đâu, open code nên viết gì, những gì là triệu chứng?
Xem lời giải
Lỗi đầu tiên ở bước 3: tạo lịch 10:00 dù người dùng nói "chiều" và calendar báo slot trống là 15:00–16:00. Open code gợi ý: "create_event đặt 10h sáng trong khi user nói chiều và calendar trống 15–16h; bỏ qua cả ý định lẫn kết quả tool". Bước 4 (SMS sai giờ) và 5 (báo "đã đặt") là triệu chứng. Lưu ý thêm: bước 4 là hành động không đảo ngược được (đã nhắn khách) — nếu bạn gán nhãn đầy đủ sau này, đây có thể là một failure mode riêng ("hành động không đảo ngược khi chưa xác nhận").
3.3Bước 1a — Bộ trace ban đầu từ log production
Bao nhiêu trace? Sách không đưa con số cố định: khoảng 100 trace là điểm khởi đầu hợp lý, nhưng tín hiệu thật là saturation. Nếu đã đọc 60 trace và 20 trace gần nhất đều rơi vào những lỗi đã biết, nhiều khả năng bạn đã bão hoà; nếu lỗi mới vẫn liên tục xuất hiện thì đọc tiếp. Dừng quá sớm → sót lỗi; kéo quá lâu → phí công xác nhận điều đã biết. App đơn giản bão hoà sớm; agent nhiều tool, nhiều bước cần khám phá lâu hơn.
Có hai nguồn để lập bộ trace: log production (ưu tiên nếu có) và query tổng hợp (mục 3.4). Với log production, sách gợi ý ba nguyên tắc:
- Phủ đa dạngLấy mẫu trải qua nhiều tính năng, nhiều kiểu input. Dữ liệu nhỏ → random là đủ; dữ liệu lớn → cluster query (theo embedding hoặc keyword) rồi lấy mẫu từ mỗi cụm.
- Săn trải nghiệm tệChủ động lấy trace có tín hiệu xấu: thumbs-down, người dùng diễn đạt lại cùng một yêu cầu nhiều lần, phiên kết thúc bằng việc người dùng chuyển sang kênh khác (gọi hotline, nhắn người thật).
- Hỏi chuyên gia domainMôi giới lâu năm thường biết loại yêu cầu nào khó trước khi dữ liệu xác nhận — hãy nhờ họ chỉ điểm.
- Gán cụm cho query: dùng keyword đơn giản ("email", "lịch|hẹn|xem nhà", "giá|thị trường|so sánh") và phần còn lại là "tìm nhà"; query không khớp gì → "khác/lạ". Phân bố: tìm nhà 58%, email 22%, lịch 12%, phân tích 6%, khác 2%.
- Chia ngân sách 100 trace: 65 trace phân tầng (tối thiểu 10/cụm, phần dư chia theo tỷ lệ), 25 trace có tín hiệu xấu, 10 trace do môi giới cố vấn chọn ("mấy câu kiểu 'nhà gần trường tốt' là bot hay trật").
- Định nghĩa tín hiệu xấu cụ thể: thumbs-down; ≥ 2 lần người dùng gõ lại gần giống nhau trong 3 phút; phiên kết thúc bằng bấm "gọi hỗ trợ".
- Loại trùng: nếu một phiên có 5 trace liên tiếp gần giống nhau (người dùng gõ lại), chỉ giữ trace đầu và trace cuối — đủ để thấy bot sai gì và người dùng phải "chữa cháy" ra sao.
- Gắn cờ nguồn: mỗi trace có cột
source ∈ {strat, bad_signal, expert}. Cột này cực quan trọng khi tính tỷ lệ về sau.
# sample_traces.py — lấy mẫu phân tầng + săn tín hiệu xấu (code minh hoạ, tự viết)
import random, re
from collections import defaultdict
BUCKETS = [("email", r"email|thư"), ("lịch", r"lịch|hẹn|xem nhà"),
("phân tích", r"giá|thị trường|so sánh|comps")]
def bucket_of(query: str) -> str:
q = query.lower()
for name, pat in BUCKETS:
if re.search(pat, q):
return name
return "tìm nhà" if re.search(r"căn|nhà|pn|tỷ", q) else "khác"
def is_bad(t: dict) -> bool:
return t.get("thumbs") == "down" or t.get("rephrased", 0) >= 2 or t.get("escalated", False)
def sample(traces, n_strat=65, n_bad=25, min_per_bucket=10, seed=42):
rng = random.Random(seed)
by_bucket = defaultdict(list)
for t in traces:
by_bucket[bucket_of(t["query"])].append(t)
picked, total = [], len(traces)
# 1) mỗi cụm tối thiểu min_per_bucket, phần còn lại chia theo tỷ lệ
rest = n_strat - min_per_bucket * len(by_bucket)
for b, items in by_bucket.items():
k = min(len(items), min_per_bucket + round(rest * len(items) / total))
picked += [dict(t, source="strat", bucket=b) for t in rng.sample(items, k)]
# 2) săn trace có tín hiệu xấu, không trùng với phần đã lấy
seen = {t["trace_id"] for t in picked}
bad = [t for t in traces if is_bad(t) and t["trace_id"] not in seen]
picked += [dict(t, source="bad_signal", bucket=bucket_of(t["query"]))
for t in rng.sample(bad, min(n_bad, len(bad)))]
return picked
Nhầm lẫn thường gặp · "Mẫu có 40% lỗi nghĩa là production có 40% lỗi"
Không! Khi bạn cố tình lấy nhiều trace có thumbs-down và cân bằng các cụm hiếm, mẫu bị lệch về phía lỗi — đó là chủ đích (để tìm loại lỗi). Nhưng khi ước lượng tỷ lệ, hãy tính riêng trên phầnsource = strat (và nếu muốn chính xác hơn thì đánh trọng số mỗi cụm theo tỷ lệ thật của nó). Pha Analyze trả lời "có những loại lỗi nào"; con số production chính xác là việc của pha Measure.
65 trace phân tầng ở ví dụ 3.4 được chia: tìm nhà 19, email 13, lịch 12, phân tích 11, khác 10 (mỗi cụm tối thiểu 10, phần dư 15 chia theo tỷ lệ). Mode "bịa thông tin listing" xuất hiện ở tìm nhà 3/19, email 2/13, các cụm còn lại 0. (a) Tỷ lệ thô trên 65 trace là bao nhiêu? (b) Tỷ lệ ước lượng cho production khi đánh trọng số theo phân bố thật 58/22/12/6/2 (%)?
Xem lời giải
(a) Thô: 5/65 ≈ 7,7%. (b) Tỷ lệ trong từng cụm: tìm nhà 3/19 ≈ 15,8%; email 2/13 ≈ 15,4%; các cụm khác 0%. Có trọng số: 0,58 × 15,8% + 0,22 × 15,4% ≈ 9,2% + 3,4% = 12,6%. Con số production cao hơn tỷ lệ thô, vì mẫu phân tầng đã "pha loãng" hai cụm lớn (nơi lỗi này tập trung) bằng các cụm nhỏ ít lỗi. Bài học: phân tầng tốt để khám phá; muốn ra con số thì phải hiệu chỉnh (và với cỡ mẫu này khoảng tin cậy còn rất rộng — xem LAB 4).
3.4Bước 1b — Query tổng hợp: dimension → tuple → query
Chưa có trace thật (app mới, hoặc tính năng mới)? Tạo bộ query tổng hợp, chạy app trên đó để sinh trace. Sai lầm phổ biến mà sách cảnh báo: bảo thẳng LLM "sinh N query cho app của tôi". Kết quả là các biến thể na ná nhau, cùng test một năng lực, không bao giờ chạm tới case mơ hồ hay ngoài phạm vi — và trace sinh ra cũng na ná nhau, che mất những lỗi chỉ xuất hiện ở điều kiện khác.
Cách tốt hơn là thêm một lớp gián tiếp: (1) định nghĩa các dimension — trục mà các yêu cầu khác nhau; (2) sinh các tuple — mỗi tuple chọn một giá trị từ mỗi dimension; (3) từ mỗi tuple mới sinh một query ngôn ngữ tự nhiên, bằng một lời gọi LLM riêng. Tách "sinh tuple" khỏi "sinh query" buộc bộ dữ liệu đa dạng một cách có kiểm soát.
Chọn dimension: trục nơi app dễ hỏng
Dimension tốt mô tả nơi app có khả năng hỏng. Muốn tìm ra chúng: nói chuyện với PM, đọc phản hồi người dùng, tự dùng app, hoặc nhờ vài người dùng thử và để lỗi của họ gợi ý dimension. Không cần liệt kê đủ mọi dimension trước khi bắt đầu — có ba dimension đủ đa dạng là sinh dữ liệu được, thêm dần sau.
| Dimension ứng viên | Nhận xét | Giữ? |
|---|---|---|
| Độ rõ ý định: rõ ràng / mơ hồ / ngoài phạm vi | Trực tiếp chạm vào chỗ agent phải quyết định hỏi lại hay đoán — nơi lỗi rất hay xảy ra | Giữ |
| Số ràng buộc trong yêu cầu: 1 / 2–3 / ≥ 4 | Càng nhiều ràng buộc càng dễ "rơi" một cái khi sinh SQL | Giữ |
| Persona khách: mua lần đầu / đầu tư / hạng sang | Ảnh hưởng giọng email và loại tiêu chí (lợi suất vs trường học) | Giữ |
| Độ dài tên khách: ngắn / dài | Không có giả thuyết nào về việc nó làm app hỏng | Bỏ |
| Giờ trong ngày người dùng gửi | Chỉ đáng giữ nếu app có logic phụ thuộc giờ (vd. "chiều nay", lịch trống) — lúc đó nên đổi thành "có tham chiếu thời gian tương đối" | Sửa |
Quy tắc ngón tay cái: với mỗi dimension, hãy nói được một câu giả thuyết dạng "khi giá trị là X, tôi nghĩ app có thể hỏng vì Y". Không nói được → dimension đó chưa đáng giữ.
Từ tuple đến query
Sách gợi ý: tự viết tay khoảng 20 tuple để thấm không gian bài toán (hỏi chuyên gia hoặc đọc query thật nếu bí), rồi dùng LLM scale lên ≥ 100. Một số người thích 10–20 query được chăm chút kỹ và làm giàu dần theo thời gian — cũng ổn nếu dùng làm nền cho cải tiến liên tục chứ không phải test một lần. Sau khi sinh, lọc chất lượng mạnh tay: query gượng gạo hay lệch mục tiêu → vứt, sinh lại; mọi bước sau phụ thuộc vào độ "thật" của bộ này.
# tuples.py — sinh tuple có luật loại trừ + kiểm tra độ phủ (code minh hoạ, tự viết)
import itertools, random
from collections import Counter
DIMS = {
"feature": ["tìm nhà", "phân tích thị trường", "xếp lịch", "soạn email"],
"persona": ["mua lần đầu", "nhà đầu tư", "hạng sang"],
"scenario": ["rõ ràng", "mơ hồ", "ngoài phạm vi"],
}
# tổ hợp vô nghĩa hoặc không muốn test (ví dụ: giả định của team)
EXCLUDE = [{"feature": "phân tích thị trường", "persona": "mua lần đầu", "scenario": "ngoài phạm vi"}]
def excluded(t: dict) -> bool:
return any(all(t[k] == v for k, v in rule.items()) for rule in EXCLUDE)
def make_tuples(n: int, seed: int = 7):
keys = list(DIMS)
all_t = [dict(zip(keys, combo)) for combo in itertools.product(*DIMS.values())]
valid = [t for t in all_t if not excluded(t)]
rng = random.Random(seed)
picked = rng.sample(valid, min(n, len(valid)))
return all_t, valid, picked
all_t, valid, picked = make_tuples(20)
print(len(all_t), "tổ hợp,", len(valid), "hợp lệ,", len(picked), "được chọn")
for dim in DIMS: # mỗi giá trị có xuất hiện ít nhất 1 lần không?
c = Counter(t[dim] for t in picked)
missing = [v for v in DIMS[dim] if c[v] == 0]
print(f"{dim:9s}", dict(c), "THIẾU:" if missing else "", missing or "")
Bước kế tiếp (không in ở đây): với mỗi tuple, gọi LLM với một prompt mô tả app, nêu rõ ba giá trị của tuple, cho 1–2 query mẫu do bạn viết, và yêu cầu viết một query mới mà người dùng thật có thể gõ — đúng mức độ mơ hồ mà tuple chỉ định. Lưu cả tuple lẫn query để về sau có thể cắt tỷ lệ lỗi theo từng dimension (mục 3.7).
Mỗi dòng một dimension, dạng tên: giá trị 1, giá trị 2, …. Luật loại trừ (tuỳ chọn), mỗi dòng dạng tên=giá trị & tên=giá trị. Chọn preset hoặc tự sửa, rồi bấm Sinh.
Sách khuyên: trước khi chạy bộ query, với mỗi query hãy ghi bạn nghĩ hệ thống sẽ làm gì và hỏng ở đâu. So dự đoán với thực tế sẽ lộ ra những giả định bạn không biết mình đang có. Bảng minh hoạ (tự viết):
| Query (từ tuple) | Dự đoán | Thực tế | Giả định bị lộ |
|---|---|---|---|
| "Tìm nhà đáng đầu tư khu Nam" (tìm nhà, đầu tư, mơ hồ) | Sẽ hỏi lại "khu Nam" là quận 7 hay Nhà Bè | Tự chọn quận 7, không hỏi | Tưởng prompt đã dặn hỏi lại khi địa danh mơ hồ — thực ra chỉ dặn cho tên đường |
| "Soạn email mời anh Khoa xem penthouse" (email, hạng sang, rõ ràng) | Ổn | Ổn, nhưng ký tên "Trợ lý AI" | Không ai nghĩ tới chữ ký — thêm vào checklist |
| "Tư vấn giúp khoản vay 70% có nên không" (phân tích, mua lần đầu, ngoài phạm vi) | Sẽ từ chối lịch sự | Đưa lời khuyên tài chính cụ thể | Tưởng model tự biết giới hạn tư vấn tài chính |
Hàng thứ hai cho thấy một điểm tinh tế: dự đoán "ổn" nhưng thực tế lộ ra một lỗi không ai nghĩ tới — đúng loại lỗi mà chỉ đọc trace mới thấy.
Cuối cùng, chạy từng query end-to-end và xuất toàn bộ trace (mọi bước LLM, tool call, input, output). Với bộ trace trong tay, bạn sẵn sàng đọc.
Nhầm lẫn thường gặp · "Phải chạy đủ tích Descartes"
4 × 3 × 3 = 36 tổ hợp thì chạy hết được; nhưng 6 dimension × 4 giá trị = 4.096 tổ hợp. Mục tiêu không phải phủ hết tổ hợp mà là mỗi giá trị của mỗi dimension xuất hiện đủ vài lần và các tổ hợp "nguy hiểm" theo giả thuyết của bạn có mặt. Lấy mẫu + kiểm tra độ phủ từng giá trị (như code trên) là đủ cho pha Analyze.Bạn làm chatbot chăm sóc khách hàng cho một shop thời trang online: tra đơn, theo dõi vận chuyển, đổi trả, hỏi chính sách. Hãy đề xuất 3–4 dimension (mỗi cái kèm giả thuyết hỏng), 2 tuple và query tương ứng, và 1 luật loại trừ.
Xem lời giải
Một đáp án hợp lý:
- Ý định: tra đơn / vận chuyển / đổi trả / chính sách — giả thuyết: đổi trả cần nhiều bước và đụng chính sách, dễ sai nhất.
- Trạng thái đơn: đang xử lý / đang giao / đã giao / đã huỷ / không tồn tại — bot hay trả lời như thể đơn luôn ở trạng thái "đang giao".
- Mức cảm xúc: bình thường / bực bội / doạ bóc phốt — giọng trả lời dễ lệch khi khách gắt.
- Độ đầy đủ thông tin: có mã đơn / chỉ có SĐT / không có gì — bot có thể bịa mã đơn.
Tuple 1: (đổi trả, đã giao, bực bội, chỉ có SĐT) → "Áo nhận hôm qua rách nách, đổi kiểu gì đây, sđt 09xx…". Tuple 2: (vận chuyển, đã huỷ, bình thường, có mã đơn) → "Đơn #A1932 bao giờ tới vậy shop?" (bẫy: đơn đã huỷ). Luật loại trừ: ý định=tra đơn & độ đầy đủ=không có gì & trạng thái=đang giao nếu team cho rằng trạng thái không xác định được khi không có thông tin — thực ra bạn có thể giữ lại để test bot có biết hỏi mã đơn không.
3.5Bước 2 — Open coding: đọc và ghi chú tự do
Quy trình gán nhãn của sách gồm hai bước mượn từ nghiên cứu định tính (grounded theory — dùng một cách thực dụng, không theo đầy đủ phương pháp luận): open coding (đọc trace, ghi quan sát tự do) rồi axial coding (gom quan sát thành category có cấu trúc). "Coding" ở đây đơn giản là gắn nhãn mô tả ngắn cho phần dữ liệu có vẻ quan trọng. Tinh thần: để category tự nổi lên từ những gì thực sự thấy, thay vì bắt đầu bằng một danh sách có sẵn.
- Đứng ở vị trí người dùngHọ có được phục vụ tốt không? Câu trả lời có giải quyết đúng yêu cầu? Chỉ ghi nhận cái sai — chưa đoán nguyên nhân, chưa nghĩ cách sửa.
- Ghi lỗi đầu tiênThường là điểm sai xa nhất về phía thượng nguồn; nó hay kéo theo cả chuỗi lỗi phía sau, và sửa nó thường giải quyết cả chuỗi. Ghi lỗi đầu tiên nhanh hơn và tránh việc liệt kê từng triệu chứng.
- Viết ngắn nhưng đủ ngữ cảnhChữ thường, không cần trau chuốt, một cụm từ hoặc một-hai câu — nhưng đồng đội đọc phải hiểu mà không cần mở lại trace.
- Thêm phán quyết nhị phânCột "chấp nhận được / không". Buộc chọn phe (kể cả case lưng chừng) sắc hơn điểm số mơ hồ và giúp làm rõ bạn coi cái gì là lỗi.
- Bí thì dùng "lăng kính"Tạm nhìn top-down qua các loại lỗi LLM phổ biến (vd. hallucination — thông tin không có căn cứ trong input nhưng được nói như sự thật) hoặc lỗi đặc thù domain (text-to-SQL: join sai; bot CSKH: trái chính sách). Mục đích là giúp nhìn ra, không phải ép trace vào khung.
- Dừng khiđã gán ≥ 20 trace có vấn đề và lỗi mới không còn xuất hiện.
Bảng ghi chép tối giản: ID trace · trace (hoặc tóm tắt) · ghi chú lỗi đầu tiên · Đạt/Không đạt. Sách minh hoạ bằng các quan sát điển hình ở trợ lý CRM — diễn giải ngắn: SQL rơi mất một điều kiện người dùng nói rõ; đề xuất giờ xem nhà vào khung lịch đã bận; email cho nhà đầu tư nhưng giọng như viết cho người mua lần đầu; mô tả số phòng trái với listing vừa lấy về; gửi nhầm người nhận; địa danh trùng tên bị tự chọn âm thầm; đưa giá đề nghị mà không tra dữ liệu so sánh; phớt lờ lỗi tool rồi bịa kết quả; "tóm tắt nhanh" nhưng trả lời rất dài; được bảo soạn lại gửi luôn. Lưu ý: đó chưa phải category — chỉ là quan sát thô; có cái sẽ gộp chung, có cái đứng riêng.
| Tình huống trong trace | Ghi chú tồi | Ghi chú tốt |
|---|---|---|
| Email mô tả căn hộ "có hầm để xe riêng"; listing trả về không có trường parking nào | hallucination | email nói có hầm xe riêng, listing không hề có thông tin đỗ xe → bịa tiện ích |
| Khách hỏi "căn nào gần trường quốc tế", agent trả 5 căn theo giá, không lọc theo trường | trả lời dở | bỏ tiêu chí "gần trường quốc tế", chỉ sort theo giá; user phải hỏi lại |
Agent gọi search_listings 4 lần với cùng tham số rồi mới trả lời | cần thêm cache cho tool | gọi search 4 lần y hệt tham số trước khi trả lời (chậm ~12s), kết quả cuối vẫn đúng |
| Khách hạng sang, email mở đầu "Hey anh ơi 🔥🔥" | tone | giọng suồng sã + emoji với khách hạng sang (tuple persona = luxury) |
| "Tóm tắt 3 lead nóng nhất" → trả về 3 lead, nhưng 1 lead đã chốt hợp đồng tuần trước | sai data | đưa lead đã chốt HĐ (status=closed) vào "lead nóng"; không lọc theo status |
- Nhãn chung chung ("hallucination", "tone") vứt đi thông tin quý nhất: bịa cái gì, so với nguồn nào. Hai tuần sau không ai nhớ trace đó hỏng ra sao.
- Ghi chú chứa cách sửa ("cần thêm cache") là đã nhảy sang chẩn đoán — và có thể sai (có khi vấn đề là prompt khiến model không tin kết quả tool).
- Ghi chú tốt có: hành vi quan sát được + bằng chứng từ trace (so với cái gì) + hậu quả với người dùng khi cần.
- Hàng thứ ba cho thấy open code không chỉ dành cho câu trả lời sai: chậm, lãng phí, vòng vo cũng là trải nghiệm tệ nếu người dùng cảm nhận được.
Bạn đọc trace tr-052 và thấy "có gì đó sai sai" nhưng không gọi tên được: agent trả lời mượt mà về "giá trung bình khu Thảo Điền tăng 8% năm nay". Thử lần lượt các lăng kính:
- Lăng kính "có căn cứ không?" — lật lại các tool call: không có lần gọi
market_statsnào. Con số 8% không xuất phát từ dữ liệu nào trong trace. Gọi được tên rồi! - Viết open code cụ thể: "nêu 'giá Thảo Điền tăng 8%' mà không gọi market_stats; số liệu không có nguồn trong trace".
- Không dán nhãn "hallucination" rồi thôi — lăng kính chỉ giúp bạn nhìn; ghi chú vẫn phải mô tả điều cụ thể xảy ra.
10 trace CRM tự viết. Bước 1: đọc từng trace, chọn Đạt / Không đạt và viết ghi chú lỗi đầu tiên. Bước 2: gán mỗi trace "Không đạt" vào một axial code (có thể thêm code mới), rồi xem biểu đồ prevalence và so với đáp án tham khảo.
Nhầm lẫn thường gặp · "Lỗi đầu tiên = lỗi nghiêm trọng nhất"
Không nhất thiết. Lỗi đầu tiên là lỗi sớm nhất theo thứ tự trong trace (thượng nguồn). Ví dụ: agent hiểu "khu Nam" thành quận 7 mà không hỏi lại (lỗi đầu tiên, nhẹ), rồi sau đó gửi luôn email cho khách (nghiêm trọng hơn). Ghi chú đầu tiên vẫn ghi lỗi địa danh; nếu hành động không đảo ngược là thứ bạn đang theo dõi, bạn sẽ gán nhãn đầy đủ cho mode đó sau (mục 3.8). Còn nếu bạn thấy một lỗi nghiêm trọng phía sau, thêm một câu vào ghi chú cũng không sao — quy tắc "lỗi đầu tiên" là để tiết kiệm công, không phải để giấu thông tin.Viết lại 4 ghi chú tồi sau thành open code tốt (tự bịa chi tiết trace hợp lý nếu cần): (1) "sai"; (2) "model ngu ở câu so sánh"; (3) "cần few-shot cho email"; (4) "ok nhưng hơi dài".
Xem lời giải
- "sai" → "trả giá thuê 25tr/tháng cho căn L8 trong khi listing ghi 18tr; lấy nhầm giá của căn L9 cùng tầng".
- "model ngu…" → "so sánh 2 căn nhưng bảng so sánh bỏ cột phí quản lý mà user hỏi đích danh" (mô tả hành vi, không đánh giá model).
- "cần few-shot…" → "email mời xem nhà thiếu địa chỉ và giờ hẹn, chỉ có lời chào + mô tả căn" (bỏ phần đề xuất sửa).
- "ok nhưng hơi dài" → quyết định phe trước: nếu người dùng hỏi "tóm tắt nhanh" mà nhận 6 đoạn → Không đạt, ghi "user xin tóm tắt nhanh, trả 6 đoạn có tiêu đề; bỏ qua gợi ý độ dài". Nếu người dùng không yêu cầu độ dài và nội dung đúng → Đạt, có thể không cần ghi chú.
Khách hỏi "căn nào dưới 3 tỷ ở Thủ Đức?". Agent trả 4 căn đúng điều kiện, kèm thêm 1 căn 3,15 tỷ với ghi chú "hơi vượt ngân sách nhưng rất đáng cân nhắc". Đạt hay Không đạt? Viết open code.
Xem lời giải
Không có đáp án "đúng" duy nhất — điều quan trọng là chọn phe và viết lý do, vì chính lựa chọn đó định nghĩa tiêu chuẩn của team. Một lập luận hợp lý: ràng buộc ngân sách được nói rõ và agent minh bạch việc vượt ngân sách → Đạt, ghi chú "thêm 1 căn vượt ngân sách 5% nhưng có gắn nhãn rõ". Nếu sản phẩm cam kết "không bao giờ gợi ý vượt ngân sách" (vd. môi giới phàn nàn khách khó chịu) → Không đạt. Ghi lại quyết định này vào định nghĩa mode ở bước axial coding để người khác áp dụng nhất quán (chương 4 bàn kỹ về đồng thuận giữa người gán nhãn).
3.6Bước 3 — Axial coding: cấu trúc hoá thành failure mode
Open coding cho một đống quan sát quý nhưng rời rạc. Axial coding là bước tìm quan hệ giữa các ghi chú và gom chúng thành nhóm cấp cao — các kiểu lỗi tách biệt, lặp lại. Có nhóm hiện ra ngay (bỏ qua ngân sách, bỏ sót số phòng ngủ → vi phạm ràng buộc người dùng). Có nhóm chỉ lộ sự khác biệt sau khi đọc nhiều: bịa đặc điểm căn nhà và bịa hành động của khách nghe như cùng là "bịa", nhưng nguyên nhân khác (một cái bịa chi tiết không có trong dữ liệu listing, một cái hiểu sai/bịa ý định người dùng) và cách sửa khác → tách thành hai mode. Đây chỉ là lượt đầu; gộp/tách lại sau là bình thường.
Ghi chú thô (rút gọn) từ 14 trace Không đạt: (1) SQL mất lọc "cho nuôi thú"; (2) trả căn 3 PN khi hỏi 2 PN; (3) bỏ trần 3 tỷ; (4) email nói có hồ bơi, listing không có; (5) nói căn hướng Đông Nam, listing ghi Tây Bắc; (6) tự tạo lịch xem nhà khách không yêu cầu; (7) email viết "như anh đã đồng ý hôm qua" dù không có cuộc gọi nào; (8) giọng suồng sã với khách hạng sang; (9) email cho nhà đầu tư không nhắc lợi suất; (10) "Phú Mỹ Hưng" hiểu thành phường Phú Mỹ (quận 7) chung chung; (11) có 2 contact "Lan", tự chọn; (12) tool search lỗi 503, vẫn báo "tìm được 3 căn"; (13) calendar báo bận, vẫn đề xuất giờ đó; (14) bỏ tiêu chí gần trường.
- Xếp đống lần 1 (trực giác): {1,2,3,14} "ràng buộc"; {4,5,7,6} "bịa"; {8,9} "giọng"; {10,11} "mơ hồ"; {12,13} "tool".
- Phép thử tách — "sửa có giống nhau không?". Nhóm "bịa": 4, 5 là bịa thuộc tính listing (sửa: bắt buộc trích từ dữ liệu tool); 6, 7 là bịa hành động/ý định của người dùng (sửa: chỉ hành động khi có yêu cầu, xác nhận trước). → tách thành 2 mode.
- Phép thử gộp — "hai người gán nhãn có phân biệt đáng tin được không?". {12} (bỏ qua lỗi tool) và {13} (bỏ qua kết quả tool) — cả hai là "coi output tool như gợi ý thay vì sự thật"; sửa chung một chỗ (xử lý kết quả tool). → gộp thành "Phớt lờ kết quả/lỗi tool".
- Kiểm tra "ràng buộc": 14 (gần trường) là ràng buộc mềm, định tính, 1–3 là ràng buộc cứng, lọc được bằng SQL. Sửa có khác? Có thể (cái đầu cần hiểu ngữ nghĩa, cái sau cần mapping sang SQL). Nhưng mới có 1 trace → tạm giữ chung, ghi chú "theo dõi, có thể tách ở vòng 2".
- Chốt taxonomy v1 (6 mode): Vi phạm ràng buộc (1,2,3,14) · Bịa thông tin listing (4,5) · Bịa hành động/ý định người dùng (6,7) · Sai giọng theo persona (8,9) · Mơ hồ không hỏi lại (10,11) · Phớt lờ kết quả/lỗi tool (12,13).
Codebook: viết định nghĩa đủ để người khác áp dụng
Mỗi mode nên có: tên ngắn, định nghĩa một dòng, tính khi / không tính khi (ranh giới), và 1–2 trace ví dụ. Sách lưu ý cuối axial coding bạn phải có danh sách mode tách biệt kèm ví dụ đại diện — chúng là điểm tham chiếu cho bước gán nhãn. Mẫu codebook tự viết:
| Mode | Định nghĩa | Tính khi | Không tính khi | Ví dụ |
|---|---|---|---|---|
| Vi phạm ràng buộc | Kết quả không thoả điều kiện người dùng đã nói rõ | Lọc sai/thiếu giá, số phòng, khu vực, tiện ích | Người dùng không nêu điều kiện đó; hoặc agent vượt nhẹ và nói rõ (xem Bài tập 3.6) | tr-008, tr-031 |
| Bịa thông tin listing | Nêu thuộc tính căn nhà trái với / không có trong dữ liệu tool | Số phòng, hướng, tiện ích, giá sai so với listing | Nhận định chủ quan có rào đón ("có vẻ thoáng") | tr-014, tr-040 |
| Mơ hồ không hỏi lại | Có ≥ 2 cách hiểu hợp lý nhưng agent tự chọn âm thầm | Địa danh trùng tên, contact trùng tên, "cuối tuần" khi có 2 cuối tuần | Ngữ cảnh đã đủ để phân giải (vd. khách chỉ có 1 người tên Lan) | tr-017, tr-063 |
# codebook.py — codebook + kiểm tra nhất quán nhãn (code minh hoạ, tự viết)
CODEBOOK = {
"constraint": "Kết quả không thoả điều kiện người dùng đã nói rõ",
"fake_listing": "Nêu thuộc tính căn nhà trái với / không có trong dữ liệu tool",
"fake_action": "Thực hiện/khẳng định hành động người dùng không yêu cầu",
"persona_tone": "Giọng/nội dung lệch persona khách",
"ambiguity": "Có ≥2 cách hiểu hợp lý nhưng agent tự chọn không hỏi lại",
"tool_ignored": "Phớt lờ kết quả hoặc lỗi trả về từ tool",
}
def check(rows):
"""rows: list of dict {trace_id, verdict, labels: set[str]} → danh sách vấn đề."""
problems = []
for r in rows:
unknown = r["labels"] - CODEBOOK.keys()
if unknown:
problems.append((r["trace_id"], f"nhãn lạ {unknown} — thêm vào codebook hay gõ nhầm?"))
if r["verdict"] == "fail" and not r["labels"]:
problems.append((r["trace_id"], "Không đạt nhưng chưa gán mode nào → taxonomy thiếu mode?"))
if r["verdict"] == "pass" and r["labels"]:
problems.append((r["trace_id"], "Đạt nhưng có nhãn lỗi → xem lại phán quyết"))
return problems
rows = [{"trace_id": "tr-017", "verdict": "fail", "labels": {"ambiguity"}},
{"trace_id": "tr-021", "verdict": "fail", "labels": set()},
{"trace_id": "tr-030", "verdict": "pass", "labels": {"tone"}}]
for p in check(rows):
print(*p)
Hai dòng "vấn đề" mà code này bắt được chính là hai tín hiệu quan trọng nhất của vòng tinh chỉnh: trace Không đạt nhưng không khớp mode nào (taxonomy thiếu) và nhãn không có trong codebook (định nghĩa trôi dạt).
LLM được giúp ở đâu — và không ở đâu
Đưa các open code + trace, nhờ đề xuất nhóm (tên ngắn + định nghĩa một dòng), dặn chỉ gom từ những gì có trong ghi chú, không bịa loại lỗi mới. Sau đó tự rà: đổi tên, gộp, tách — LLM không biết hệ thống của bạn chạy và được sửa thế nào, nên có thể gộp cái cần tách hoặc ngược lại.
Open coding cần gu và phán đoán của bạn về cái gì là lỗi. LLM có "tiên kiến" mạnh về nhãn chung chung ("hallucination", "instruction following", "tone") và yếu về lỗi đặc thù app — thứ chỉ thấy được khi đọc trace thật.
Cảnh báo đắt giá
Sách kể: team bỏ qua open coding và nhờ LLM đề xuất failure mode dễ dành hàng tháng xây hạ tầng eval trên một taxonomy không khớp phân bố lỗi thật của mình.Bạn đưa 14 ghi chú của ví dụ 3.9 cho LLM. Nó đề xuất 4 nhóm: "Hallucination" (4,5,6,7,12), "Instruction following" (1,2,3,13,14), "Tone" (8,9), "Clarification" (10,11). Rà lại:
- "Hallucination" gộp 3 thứ khác nhau: bịa listing (4,5), bịa hành động (6,7), và bịa kết quả khi tool lỗi (12). Ba cách sửa khác nhau → tách.
- "Instruction following" quá rộng: nhét cả (13) calendar bận — đó là phớt lờ kết quả tool, không phải phớt lờ lời người dùng. Chuyển 13 sang nhóm "phớt lờ tool" cùng 12.
- Đổi tên theo ngôn ngữ của domain: "Tone" → "Sai giọng theo persona" (vì vấn đề không phải giọng tệ, mà là giọng không hợp khách).
- Kết quả: về đúng taxonomy v1 6 mode ở ví dụ 3.9. LLM tiết kiệm thời gian xếp đống, nhưng 3/4 nhóm phải sửa — đúng như sách cảnh báo.
Nhầm lẫn thường gặp · Độ mịn (granularity) của mode
Quá thô: "chất lượng trả lời kém" — gán nhãn được nhưng không chỉ ra phải sửa gì. Quá mịn: "quên lọc thú cưng ở quận 7 cho căn hộ dịch vụ" — mỗi mode chỉ có 1 trace, không đếm được gì. Vừa: "vi phạm ràng buộc người dùng" — đủ nhiều trace để đếm, đủ hẹp để một nhóm cách sửa áp dụng được. Kiểm tra nhanh: mỗi mode nên có ≥ 2–3 trace trong vòng đầu, và bạn nói được "để sửa mode này, tôi sẽ thử …".Ghi chú từ một chatbot CSKH đơn hàng (tự viết): (a) báo đơn "đang giao" dù đơn đã huỷ; (b) hứa hoàn tiền trong 24h, chính sách là 5–7 ngày; (c) bịa mã vận đơn; (d) khách bực, bot trả lời bằng câu mẫu vui vẻ kèm emoji; (e) đổi size nhưng tạo yêu cầu trả hàng hoàn tiền; (f) nói miễn phí đổi trả cho hàng sale, chính sách là không; (g) không hỏi mã đơn, đoán theo tên khách ra đơn của người khác; (h) tool tra đơn timeout, bot nói "đơn của bạn ổn". Gom thành mode, nêu lý do tách/gộp.
Xem lời giải
- Trái chính sách: (b), (f) — sửa bằng cách grounding vào tài liệu chính sách.
- Sai trạng thái đơn / phớt lờ tool: (a), (h) — agent không phản ánh đúng output (hoặc lỗi) của tool tra đơn. Có thể tách (a) "đọc sai trạng thái" và (h) "nuốt lỗi tool" nếu vòng sau nhiều trace hơn.
- Bịa định danh: (c) — bịa mã vận đơn; khác với chính sách vì nguồn đúng là dữ liệu đơn, không phải tài liệu chính sách. Nếu chỉ 1 trace, có thể tạm gộp với nhóm "phớt lờ tool" nhưng đánh dấu theo dõi.
- Nhận diện sai đơn/khách: (g) — tương tự "mơ hồ không hỏi lại" ở CRM.
- Sai hành động: (e) — gọi nhầm loại yêu cầu; sửa ở mô tả tool / mapping ý định.
- Giọng không hợp cảm xúc khách: (d).
Điểm mấu chốt: đừng gộp (b), (c), (f) thành "hallucination" — ba nguồn sự thật khác nhau (chính sách vs dữ liệu đơn) nghĩa là cách sửa khác nhau.
3.7Bước 4 — Gán nhãn có cấu trúc & đếm
Lúc này bạn có hai thứ: bộ trace kèm ghi chú open coding, và danh sách failure mode từ axial coding. Bước tiếp theo nối định tính sang định lượng: đi lại từng trace, đối chiếu ghi chú với danh sách mode, đánh dấu mode nào áp dụng. Nếu bạn tự làm axial coding thì chỉ là áp lại ánh xạ vừa xây; nếu LLM đã giúp gom nhóm thì phải bảo đảm các nhóm đó được áp nhất quán — làm tay, hoặc đưa ghi chú + danh sách mode cho LLM gán rồi rà lại.
Trong spreadsheet: mỗi mode một cột, mỗi trace đánh 1/0. Một trace có thể dính nhiều mode. Prevalence = số trace có mode ÷ tổng số trace — tính bằng công thức spreadsheet, pivot table hoặc Pandas (để cắt theo dimension). Gặp trace không khớp mode nào, hoặc mode cần tách → cứ chỉnh định nghĩa.
| Trace | Ghi chú (rút gọn) | constraint | fake_listing | ambiguity | tool_ignored |
|---|---|---|---|---|---|
| tr-101 | bỏ trần 3 tỷ | 1 | 0 | 0 | 0 |
| tr-102 | ổn | 0 | 0 | 0 | 0 |
| tr-103 | "khu Nam" tự hiểu quận 7; rồi trả căn 3PN khi hỏi 2PN | 1 | 0 | 1 | 0 |
| tr-104 | nói có hồ bơi, listing không có | 0 | 1 | 0 | 0 |
| tr-105 | ổn | 0 | 0 | 0 | 0 |
| tr-106 | search lỗi 503, vẫn liệt kê căn | 0 | 0 | 0 | 1 |
| tr-107 | mất lọc thú cưng | 1 | 0 | 0 | 0 |
| tr-108 | ổn | 0 | 0 | 0 | 0 |
| Prevalence | 3/8 = 37,5% | 1/8 = 12,5% | 1/8 = 12,5% | 1/8 = 12,5% |
- Tỷ lệ trace Không đạt = 5/8 = 62,5% (tr-101, 103, 104, 106, 107).
- Tổng các prevalence = 75% > 62,5% — không mâu thuẫn: tr-103 dính 2 mode nên được đếm hai lần. Prevalence là theo từng mode, không cộng lại thành tỷ lệ lỗi.
- tr-103 có hai nhãn dù open coding "chỉ ghi lỗi đầu tiên": người gán nhãn đã ghi thêm vế thứ hai khi đọc. Không sao — nhãn 0/1 phản ánh những gì đã được quan sát.
# prevalence.py — đếm prevalence, cắt theo dimension, khoảng tin cậy Wilson (minh hoạ, tự viết)
import math
import pandas as pd
def wilson(k: int, n: int, z: float = 1.96):
if n == 0:
return (0.0, 0.0)
p = k / n
denom = 1 + z * z / n
center = (p + z * z / (2 * n)) / denom
half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
return (max(0.0, center - half), min(1.0, center + half))
df = pd.read_csv("labeled_traces.csv") # cột: trace_id, scenario, source, ...
MODES = ["constraint", "fake_listing", "fake_action", "persona_tone", "ambiguity", "tool_ignored"]
est = df[df["source"] == "strat"] # chỉ phần lấy mẫu phân tầng để ước lượng tỷ lệ
rows = []
for m in MODES:
k, n = int(est[m].sum()), len(est)
lo, hi = wilson(k, n)
rows.append({"mode": m, "k": k, "n": n, "prev": k / n, "ci95": f"[{lo:.0%}, {hi:.0%}]"})
print(pd.DataFrame(rows).sort_values("prev", ascending=False).to_string(index=False))
# Lỗi tập trung ở đâu? cắt theo dimension của tuple (dùng toàn bộ mẫu, chỉ để khám phá)
print(df.groupby("scenario")[MODES].mean().round(2))
| Failure mode | Tổng | rõ ràng (n=40) | mơ hồ (n=40) | ngoài phạm vi (n=20) | Mức (tổng) |
|---|---|---|---|---|---|
| Vi phạm ràng buộc người dùng | 18% | 25% | 15% | 5% | |
| Bịa thông tin listing | 11% | 10% | 15% | 5% | |
| Sai giọng theo persona | 7% | 8% | 8% | 5% | |
| Mơ hồ không hỏi lại | 4% | 0% | 10% | 0% | |
| Trả lời việc ngoài phạm vi | 6% | 0% | 0% | 30% |
- Nhìn tổng: "mơ hồ không hỏi lại" chỉ 4% — dễ bị xếp cuối danh sách.
- Nhìn theo scenario: nó là 10% trong nhóm query mơ hồ, và 0% ở nơi khác. Nếu production có nhiều query mơ hồ hơn bộ mẫu, con số thật sẽ cao hơn.
- "Trả lời việc ngoài phạm vi" 6% tổng nhưng 30% trong nhóm ngoài phạm vi: gần 1/3 lần bot không biết từ chối — đáng sửa sớm dù con số tổng nhỏ.
- Cảnh báo cỡ mẫu: 4/40 hay 6/20 là rất ít; khoảng tin cậy rộng (thử LAB 4). Dùng các lát cắt để đặt giả thuyết, không để tuyên bố chắc chắn.
Từ con số tới ưu tiên
Sách nói prevalence cho biết lỗi nào phổ biến nhất — hữu ích để quyết định dồn công vào đâu. Trong thực tế, người tóm tắt khuyên kết hợp thêm mức nghiêm trọng: một lỗi hiếm nhưng không đảo ngược được (gửi email thật cho khách) có thể đáng sửa trước một lỗi phổ biến nhưng vô hại (trả lời hơi dài).
Nhập số trace dính mode (k) và tổng số trace đã gán nhãn (n). Lab tính prevalence và khoảng tin cậy 95% Wilson — để thấy vì sao "3/40" chưa nói được nhiều.
Nhầm lẫn thường gặp · Mẫu số của prevalence
Prevalence trong sách là trên tổng số trace, không phải trên số trace Không đạt. "Bịa listing chiếm 30% các lỗi" (11/37) và "bịa listing xảy ra ở 11% trace" (11/100) là hai câu khác nhau; câu thứ hai mới cho biết người dùng gặp lỗi này thường xuyên thế nào. Khi báo cáo, luôn ghi rõk/n.
Sau khi gán nhãn 60 trace: 24 trace Không đạt. Số trace theo mode: A = 12, B = 9, C = 5, D = 2. (a) Tính prevalence mỗi mode. (b) Vì sao 12+9+5+2 = 28 > 24? (c) Mode D chỉ 2/60 — có nên bỏ khỏi taxonomy?
Xem lời giải
(a) A = 20%, B = 15%, C ≈ 8,3%, D ≈ 3,3%. (b) Có 4 lượt "dư" — tức một số trace dính ≥ 2 mode (multi-label). (c) Không bỏ chỉ vì hiếm: nếu D nghiêm trọng (vd. lộ thông tin khách khác), nó thuộc ô "hiếm nhưng nặng" và cần guardrail. Nếu D nhẹ và định nghĩa trùng lấn với mode khác, cân nhắc gộp. Quyết định dựa trên mức nghiêm trọng + khả năng phân biệt, không chỉ con số.
3.8Bước 5 — Lặp, bão hoà & gán nhãn đầy đủ
Error analysis không làm một lần. Thường có 2–3 vòng đọc trace và tinh chỉnh taxonomy; vòng sau hay lộ lỗi chưa thấy (sách lấy ví dụ: trợ lý hiểu nhầm địa danh trùng tên sang một nơi khác mà không hỏi lại → thêm mode "mơ hồ địa danh"). Tinh chỉnh có bốn dạng:
Hai mode khó phân biệt một cách đáng tin cậy khi gán nhãn.
Một mode đang lẫn các lỗi có nguyên nhân gốc khác nhau.
Viết lại định nghĩa cho dễ áp dụng nhất quán.
Vòng hai lộ lỗi chưa thấy, vd. mơ hồ địa danh → mode riêng.
- Thực tế, khoảng 2 vòng nghiêm túc open coding + gán nhãn lại là đủ để tiến gần bão hoà với cùng bộ dữ liệu; phân tích tiếp trên dữ liệu cũ cho lợi ích giảm dần.
- Nhưng phải quay lại định kỳ với dữ liệu mới: sau khi sửa lỗi, mở rộng tới nhiều người dùng hơn, hay khi hành vi người dùng đổi. Giá trị nằm ở việc nhìn hệ thống hiện tại, không phải nghiền một bộ dữ liệu tĩnh.
- Làm nhiều sẽ nhanh: có trực giác nên nhìn vào đâu, taxonomy ổn định, gán nhãn thành thói quen — có người còn ghi mọi lỗi ngay khi open coding vì đã đủ nhanh.
| Thay đổi | Lý do (bằng chứng) |
|---|---|
| Thêm "Hành động không đảo ngược khi chưa xác nhận" | Vòng 2 có 3 trace gửi email/SMS thật khi người dùng chỉ bảo soạn; v1 đã xếp nhầm vào "bịa hành động" |
| Tách "Vi phạm ràng buộc" → "ràng buộc cứng (lọc SQL)" + "ràng buộc mềm (gần trường, yên tĩnh)" | Vòng 2 có 6 trace ràng buộc mềm; cách sửa khác (cần dữ liệu POI, không phải sửa SQL) |
| Gộp "Sai giọng persona" + "Email thiếu thông tin persona cần" | Hai người gán nhãn bất đồng 5/9 trace giữa hai mode; sửa cùng một chỗ (template email theo persona) |
| Làm rõ "Mơ hồ không hỏi lại" | Thêm "không tính khi lịch sử hội thoại đã phân giải" — trước đó bị gán nhầm 4 lần |
Ghi changelog như vậy giúp mọi con số prevalence về sau luôn gắn với phiên bản taxonomy — so số giữa v1 và v2 mà không biết định nghĩa đã đổi là so táo với cam.
Giới hạn của "chỉ ghi lỗi đầu tiên"
Ghi lỗi đầu tiên giúp gán nhãn nhanh, nhưng sách chỉ ra nó đếm thiếu những mode hay xuất hiện muộn trong trace — vd. sai giọng thường đi sau một lỗi ràng buộc, nên hiếm khi được ghi là "đầu tiên". Hệ quả: khó đo tác động của bản sửa. Nếu bạn sửa mode "sai giọng" mà trước đó chỉ ghi được vài trace, bạn không biết bản sửa có hiệu quả không. Cách xử lý: khi muốn đo một bản sửa, quay lại gán nhãn đầy đủ mọi lần xuất hiện của riêng mode đang sửa/theo dõi — không cần làm với mọi mode.
- Trước sửa, theo nhãn lỗi đầu tiên: sai giọng 3/100. Sau khi thêm hướng dẫn giọng theo persona vào prompt, chạy lại 100 query: sai giọng (lỗi đầu tiên) 2/100. "Giảm 1 điểm" — gần như không nói được gì.
- Gán nhãn đầy đủ riêng mode này trên bộ cũ: 12/100. Trên bộ mới: 4/100.
- Kết luận: bản sửa cắt khoảng 2/3 lỗi sai giọng — một tín hiệu rõ ràng mà nhãn lỗi-đầu-tiên che mất. Chi phí: chỉ phải đọc lại 200 trace với một câu hỏi duy nhất ("giọng có hợp persona không?"), nhanh hơn nhiều so với open coding.
Hệ thống giả lập có K failure mode "ẩn" với tần suất giảm dần (đuôi dài, phân bố kiểu Zipf). Mỗi trace bạn đọc có xác suất lỗi 40%; nếu lỗi, nó thuộc một mode theo phân bố đó. Quan sát đường "số mode khác nhau đã thấy" và khi nào quy tắc "20 trace liên tiếp không có mode mới" kích hoạt.
# saturation.py — kiểm tra bão hoà từ log gán nhãn theo thứ tự đọc (minh hoạ, tự viết)
def saturation_point(first_modes, window=20):
"""first_modes: list mode (hoặc None nếu Đạt) theo đúng thứ tự đã đọc.
Trả về chỉ số trace mà tại đó đã có `window` trace liên tiếp không lộ mode mới."""
seen, streak = set(), 0
for i, m in enumerate(first_modes, start=1):
if m is not None and m not in seen:
seen.add(m)
streak = 0
else:
streak += 1
if streak >= window:
return i, sorted(seen)
return None, sorted(seen)
log = ["constraint", None, "fake_listing", None, "constraint", "tone", None, "ambiguity"] + \
[None, "constraint", "tone", None, "fake_listing"] * 5
print(saturation_point(log)) # → (28, [...4 mode...]) với log minh hoạ này
Nhầm lẫn thường gặp · "Bão hoà = đã tìm ra mọi lỗi"
Bão hoà chỉ nói với bộ dữ liệu và cách lấy mẫu hiện tại, bạn không còn thấy loại lỗi mới. Mode hiếm (0,5% trace) có thể không xuất hiện trong 100 trace — chạy LAB 3 với K lớn và đuôi dốc sẽ thấy rõ. Đó là lý do sách nhấn mạnh quay lại với dữ liệu mới định kỳ, và lý do nên chủ động săn trace có tín hiệu xấu.Bạn đã đọc 70 trace. Mode mới lần lượt xuất hiện ở trace số 2, 5, 9, 14, 22, 31, 48, 66. Theo quy tắc "20 trace liên tiếp không có mode mới", đã bão hoà chưa? Nên làm gì tiếp?
Xem lời giải
Chưa. Mode mới gần nhất ở trace 66 → mới có 4 trace liên tiếp không có mode mới. Khoảng cách giữa các lần phát hiện đang giãn (9 → 17 → 18) nên có thể sắp bão hoà, nhưng hãy đọc tiếp ít nhất tới trace ~86. Nếu vẫn tiếp tục ra mode mới, xem các mode gần đây đến từ đâu (dimension nào, nguồn nào) — có thể nên lấy thêm mẫu tập trung vào vùng đó.
3.9Case study: một vòng error analysis trọn vẹn trên 20 trace
Bối cảnh. Shop bán quần áo qua website, bot "Mây" trả lời khách 24/7. Tool: get_order(order_id | phone | name), track_shipment(tracking_no), get_product(sku), search_policy(q), create_ticket(type ∈ {exchange, return, refund, missing, payment}). Không có tool huỷ đơn và không có tool chuyển nhân viên. Tuần trước lượng khiếu nại qua hotline tăng 30%, chủ shop muốn biết bot hỏng ở đâu.
Bước 1 — Lấy mẫu
Log 7 ngày: 2.400 phiên. Lấy 12 trace ngẫu nhiên + 8 trace có tín hiệu xấu (khách gõ "gặp nhân viên", thumbs-down, hoặc gọi hotline trong 1 giờ sau chat). Cột source ghi rõ để về sau không lấy tỷ lệ thô làm tỷ lệ production.
Bước 2 — Open coding (lỗi đầu tiên + Đạt/Không đạt)
| ID | Query (rút gọn) | Open code | KQ |
|---|---|---|---|
| C01 | Đơn #M1203 bao giờ tới? | — | Đạt |
| C02 | Áo nhận bị rách, đổi được không, sđt 0903… | sđt ra 2 đơn (áo 12/9, quần 18/9); lấy đơn mới nhất = quần, không hỏi lại; tạo ticket đổi nhầm món | Không |
| C03 | Hàng sale có đổi size được không? | policy trả "hàng sale không đổi trừ lỗi NSX", bot trả lời "đổi miễn phí" — tra đúng mà nói ngược | Không |
| C04 | Hoàn tiền bao lâu? | policy 5–7 ngày làm việc, bot hứa "trong 24h" | Không |
| C05 | Đơn #M0999 sao chưa thấy? | get_order trả status=cancelled (khách tự huỷ 10/9), bot nói "đang trên đường giao" | Không |
| C06 | Tư vấn size, cao 1m60 nặng 50kg | — | Đạt |
| C07 | Đổi sang size L đơn #M1188 | khách muốn đổi size, bot tạo ticket type=refund (trả hàng hoàn tiền) | Không |
| C08 | "3 lần rồi chưa ai gọi lại!!! bóc phốt đấy" | khách bức xúc, bot mở đầu "Mây rất vui được hỗ trợ 🥰✨", không xin lỗi, không hướng tới người thật | Không |
| C09 | Tracking GHN456 tới đâu rồi? | track_shipment timeout, bot vẫn khẳng định "đơn đang ổn, sắp tới" | Không |
| C10 | Xin link bảng size quần | — | Đạt |
| C11 | Đơn M1250 giao thiếu 1 áo | — | Đạt |
| C12 | Shop có ship quốc tế không? | policy "chỉ giao trong nước", bot nói "có, phí 250k" | Không |
| C13 | Huỷ đơn M1301 giúp mình | không có tool huỷ, đơn đã shipped, bot vẫn nói "đã huỷ thành công" | Không |
| C14 | Mã SALE50 không dùng được | — | Đạt |
| C15 | Đổi màu áo đơn M1210 | policy đổi trong 7 ngày, đơn giao 20 ngày trước, bot nói "được ạ" — không tính hạn | Không |
| C16 | Ở Đà Nẵng bao lâu nhận hàng? | — | Đạt |
| C17 | Sao đơn mình bị huỷ? (không đưa mã/sđt) | tra theo tên "Minh" ra đơn của một khách Minh khác, đọc luôn lý do huỷ + địa chỉ của người đó | Không |
| C18 | Áo này giặt máy được không? | get_product ghi "giặt tay", bot trả lời "giặt máy thoải mái" | Không |
| C19 | Cho mình nói chuyện với nhân viên | khách xin gặp người thật, bot "Mây giúp được mọi thứ ạ!" và tiếp tục hỏi, không có đường chuyển | Không |
| C20 | Đã chuyển khoản mà app báo chưa thanh toán | — | Đạt |
13/20 Không đạt. Nhưng nhớ: 8/20 trace được chọn vì có tín hiệu xấu. Trên 12 trace ngẫu nhiên (C01, C04, C06, C07, C10, C11, C12, C14, C15, C16, C18, C20), tỷ lệ Không đạt là 5/12 ≈ 42% — vẫn cao, nhưng thấp hơn nhiều so với 65%.
Bước 3 — Axial coding
- Xếp đống lần 1: "trái chính sách" {C03, C04, C12, C15}; "nói sai so với dữ liệu" {C05, C09, C18}; "nhận sai đơn/khách" {C02, C17}; "hành động sai" {C07, C13}; "khách bức xúc" {C08, C19}.
- Phép thử tách với "nói sai so với dữ liệu": C05, C18 là đọc ngược kết quả tool; C09 là nuốt lỗi tool. Cách sửa khác (grounding vs xử lý lỗi) — nhưng C09 chỉ có 1 trace. Quyết định: tạm gộp thành "Không trung thực với output tool", ghi chú theo dõi.
- Phép thử tách với "trái chính sách": C15 thực ra là tính sai thời hạn (cần phép tính ngày), khác C03/C04/C12 (nói ngược nội dung chính sách). Cũng chỉ 1 trace → giữ chung, đánh dấu.
- "Hành động sai": C07 gọi sai loại ticket; C13 khẳng định đã làm một việc không có tool. Đặt tên "Sai hoặc bịa hành động", định nghĩa rõ cả hai vế.
- Đặt tên theo domain: "khách bức xúc" → "Xử lý leo thang kém" (không xin lỗi, không chuyển người khi được yêu cầu hoặc khi khách khiếu nại lặp lại).
Bước 4 — Đếm
| Failure mode (taxonomy v1) | Trace | k/20 | Mức nghiêm trọng |
|---|---|---|---|
| Trái chính sách | C03, C04, C12, C15 | 4 (20%) | Cao — tạo kỳ vọng sai, khách khiếu nại |
| Không trung thực với output tool | C05, C09, C18 | 3 (15%) | Cao |
| Nhận sai đơn / sai khách | C02, C17 | 2 (10%) | Rất cao — C17 lộ dữ liệu cá nhân khách khác |
| Sai hoặc bịa hành động | C07, C13 | 2 (10%) | Cao |
| Xử lý leo thang kém | C08, C19 | 2 (10%) | Vừa — nhưng khớp với việc hotline tăng |
Bước 5 — Quyết định
- Sửa ngay (không cần eval phức tạp): nhận sai đơn — bắt buộc có mã đơn hoặc sđt + xác nhận 2 trường trước khi đọc thông tin đơn. Một lỗi lộ dữ liệu là đủ để ưu tiên, dù chỉ 10%.
- Sửa bằng prompt + thêm tool: thêm tool chuyển nhân viên; dặn rõ bot không có khả năng huỷ đơn (C13) và phải trích nguyên văn đoạn chính sách khi trả lời câu hỏi chính sách (C03, C04, C12).
- Cần evaluator (chương 5): "trái chính sách" và "không trung thực với output tool" — kiểu lỗi generalization, sẽ còn lặp lại; check bằng code cho trường hợp tool lỗi (có error mà câu trả lời không nhắc "chưa tra được" → Không đạt), LLM-judge cho trường hợp so nội dung với chính sách.
- Vòng 2: lấy thêm 30 trace tập trung vào đổi trả (để kiểm chứng có nên tách "tính sai thời hạn") và vào phiên có lỗi tool (để xem có nên tách "nuốt lỗi tool").
Vòng 2 (tóm tắt)
30 trace mới: "tính sai thời hạn" xuất hiện 4 lần → tách khỏi "trái chính sách" (sửa bằng cách đưa ngày giao hàng + ngày hôm nay vào context và tính bằng code, không để model tự tính). "Nuốt lỗi tool" xuất hiện 3 lần → tách. Một mode mới: "hỏi lại thông tin khách đã cung cấp" (5/30) — khó chịu nhưng nhẹ. 12 trace cuối không lộ mode mới → coi như bão hoà cho vòng này. Taxonomy v2 có 8 mode; changelog ghi đủ lý do.
Bài học của case study: (1) tỷ lệ thô trên mẫu có săn lỗi luôn cao hơn thực tế; (2) mode ít trace nhưng nghiêm trọng (lộ dữ liệu) có thể là ưu tiên số một; (3) nhiều quyết định "tách hay gộp" nên hoãn tới khi có thêm trace, miễn là ghi chú lại.
3.10Bẫy thường gặp
Sách nói không có một cách "đúng" duy nhất để làm error analysis, nhưng có rất nhiều cách sai. Mỗi bẫy dưới đây kèm một mini-ví dụ tự viết:
- Dữ liệu không đại diện — bộ query không phản ánh độ đa dạng và độ khó của người dùng thật → hoặc không lỗi nghiêm trọng nào lộ ra, hoặc lộ lỗi không giống production. Ví dụ: team test 100 query tiếng Việt chuẩn, nhưng 35% người dùng thật gõ không dấu và viết tắt ("can ho 2pn q7 duoi 3ty").
- Bỏ qua open coding, dùng nhãn chung chung ("hallucination", "instruction following", "verbosity") hoặc metric bán sẵn của vendor — nghe hợp lý nhưng bỏ sót lỗi đặc thù gây hại thật (đề xuất giờ xem nhà không trống, nhầm persona khách). Ví dụ: dashboard báo "faithfulness 0,91" trong khi bot vẫn đọc thông tin đơn của khách khác (case study C17) — không metric chung nào hỏi câu đó.
- Thang Likert 1–5 không có rubric chi tiết — nhiễu, người chấm ít đồng thuận. Quyết định nhị phân theo từng failure mode tái lập tốt hơn. Ví dụ (số minh hoạ): hai người chấm 20 trace, Likert "độ hữu ích" trùng khớp chính xác 7/20; cùng 20 trace với câu hỏi "có vi phạm ràng buộc không?", trùng 18/20.
- Đóng băng taxonomy quá sớm — hạ tầng eval bị khoá quanh một hiểu biết chưa đầy đủ. Ví dụ: xây 6 LLM-judge theo taxonomy v1, vòng 2 tách "vi phạm ràng buộc" thành cứng/mềm → phải viết lại judge và gán nhãn lại dữ liệu hiệu chỉnh.
- Loại chuyên gia domain khỏi việc gán nhãn — nhãn hời hợt hoặc sai. Tool annotate cồng kềnh với họ? Đầu tư giao diện đơn giản hơn (chương 10). Ví dụ: kỹ sư đánh "Đạt" cho email báo "căn sổ hồng riêng" — môi giới biết ngay căn đó chỉ có hợp đồng mua bán, một lỗi pháp lý nghiêm trọng.
- Nhờ LLM sinh taxonomy từ đầu — ra nhãn chung chung, lệch phân bố lỗi thật; có team mất hàng tháng xây trên nền đó.
- (Thêm của người tóm tắt) Lấy tỷ lệ trên mẫu săn lỗi làm tỷ lệ production — xem mục 3.3 và case study.
- (Thêm của người tóm tắt) Sửa lỗi ngay trong lúc open coding — mỗi lần thấy lỗi lại nhảy vào sửa prompt khiến bạn không bao giờ đọc hết mẫu và không có bức tranh tần suất; ghi lại, sửa sau.
Một team kể: "Bọn mình sinh 200 query bằng GPT, chạy app, rồi nhờ một LLM chấm 1–10 về helpfulness. Điểm trung bình 8,4 nên bọn mình tự tin ship." Chỉ ra ít nhất 3 bẫy.
Xem lời giải
- Sinh query trực tiếp (không dimension/tuple) → dữ liệu đơn điệu, không đại diện.
- Bỏ qua open coding → không có taxonomy; "helpfulness" là metric chung chung.
- Thang 1–10 không rubric → nhiễu; trung bình 8,4 che mất 5% trace thảm hoạ.
- Để LLM chấm mà không hiệu chỉnh với người (chương 5) và không có chuyên gia domain.
Cách làm lại: đọc 100 trace, open/axial coding, chọn 2–3 mode quan trọng nhất, gán nhãn nhị phân, rồi mới tính tới evaluator tự động cho từng mode.
3.11Đầu ra của pha Analyze & kết nối các chương
Ba đầu ra cụ thể
① Bộ trace có ghi chú tự do về lỗi · ② Taxonomy failure mode gom từ các ghi chú · ③ Số đếm ban đầu cho mỗi mode.Taxonomy là từ vựng chung của team để nói về cách hệ thống hỏng, và nó dẫn dắt mọi quyết định ở pha Measure và Improve. Nếu taxonomy không khớp lỗi người dùng thật gặp, mọi evaluator xây trên nó sẽ đo sai thứ.
| Chương | Dùng đầu ra của chương 3 thế nào |
|---|---|
| Ch.1 · Ba vực thẳm | Error analysis là cách vượt "vực Comprehension" — hiểu dữ liệu và hành vi thật; open code giúp phân biệt lỗi specification vs generalization. |
| Ch.4 · Đánh giá cộng tác | Nhiều người cùng gán nhãn theo codebook → đo đồng thuận, tinh chỉnh định nghĩa mode. |
| Ch.5 · Evaluator tự động | Mỗi failure mode quan trọng → một evaluator (code-based hoặc LLM-judge); nhãn 0/1 của chương này là dữ liệu hiệu chỉnh judge. |
| Ch.6 · Ch.7 · Ch.8 | Cùng quy trình, áp dụng cho hội thoại nhiều lượt, RAG, agent nhiều tool — trace dài hơn, "lỗi đầu tiên" càng quan trọng. |
| Ch.10 · Giao diện review | Làm công cụ annotate đủ nhẹ để chuyên gia domain tham gia. |
| Ch.11 · Ch.12 | Phân tích trace ở quy mô lớn và cải thiện agent dựa trên mode ưu tiên. |
3.12Áp dụng cho dự án model-routing
Hệ thống của bạn nhận mỗi request LLM và quyết định gửi tới model nào (rẻ/nhanh vs mạnh/đắt), cân bằng chất lượng – chi phí – độ trễ. Cám dỗ lớn nhất là nhảy thẳng vào metric ("accuracy của router", "tiết kiệm 40% cost"). Chương 3 bảo: đọc trace routing trước, để biết router hỏng theo những kiểu nào — vì mỗi kiểu hỏng cần một metric và một cách sửa khác nhau.
Bước 1 — Định nghĩa "trace" của router
Trace routing phải đủ để một người đọc trả lời được "quyết định này đúng không, và nếu sai thì sai ở bước nào". Tối thiểu:
{"trace_id": "rt-0412",
"request": {"text": "Tóm tắt hợp đồng đính kèm, nêu rủi ro", "tokens_in": 182000,
"sla_ms": 20000, "caller": "legal-app"},
"features": {"lang": "vi", "task": "summarize", "len_bucket": "xl", "has_code": false},
"router": {"version": "r-2.3", "difficulty": 0.58,
"candidates": {"nano": 0.21, "mid": 0.64, "frontier": 0.59}, "chosen": "mid"},
"calls": [{"model": "mid", "ctx_limit": 128000, "truncated": true,
"latency_ms": 9100, "cost_usd": 0.19, "status": "ok"}],
"output": "…bản tóm tắt…",
"shadow": {"frontier": {"verdict": "pass", "cost_usd": 1.84}},
"feedback": {"thumbs": "down"}}
Hai trường hay bị quên: truncated (nhiều SDK cắt context âm thầm) và shadow — kết quả của model không được chọn trên cùng request, chỉ cần cho mẫu trace dùng để phân tích.
Bước 2 — Bộ trace: production + tổng hợp
- Production: phân tầng theo model được chọn × loại tác vụ (để tier ít được chọn vẫn có mặt), rồi săn tín hiệu xấu riêng của routing: người dùng bấm "regenerate", fallback kích hoạt, cost/request thuộc top 1%, latency vượt SLA, điểm router nằm sát ngưỡng (±0,03).
- Tổng hợp: dimension gợi ý — loại tác vụ (chat, code, tóm tắt, suy luận, trích xuất, dịch), độ khó thật (dễ / khó / gài bẫy "ngắn mà khó", "dài mà dễ"), độ dài context (<4k / 4–32k / 32–128k / >128k), ràng buộc (ưu tiên rẻ / nhanh / chất lượng), ngôn ngữ (vi có dấu / không dấu / en / trộn). Thử preset "model-routing" ở LAB 2.
- Shadow replay: chạy mỗi trace mẫu thêm trên 1–2 model khác và chấm Đạt/Không đạt (người chấm, hoặc sau này judge đã hiệu chỉnh) — đây là "sự thật" để nói under/over-route.
Bước 3 — Open coding các quyết định route
| ID | Request (rút gọn) | Router → kết quả | Open code |
|---|---|---|---|
| R01 | "chào bạn" | 0,05 → nano, ổn | Đạt |
| R02 | bài toán chứng minh 4 bước, prompt 2 dòng | 0,35 → nano, sai bước 3; shadow frontier đúng | prompt ngắn nên điểm khó thấp → nano; bài nhiều bước sai, frontier làm đúng |
| R03 | dịch email sang tiếng Anh trang trọng | 0,62 → frontier, ổn; shadow mid cũng đạt | frontier cho việc mid làm tốt ngang; tốn ~12× cost |
| R04 | tóm tắt hợp đồng 182k token | 0,58 → mid (ctx 128k), truncated | request 182k token gửi model 128k, bị cắt âm thầm; tóm tắt thiếu phụ lục |
| R05 | hỏi điều khoản luật đất đai mới | 0,40 → mid, trích sai điều; shadow frontier đúng | câu pháp lý VN, mid trích sai số điều; điểm khó không phản ánh domain |
| R06 | autocomplete, SLA 2s | 0,55 → frontier, 6,8s | caller đặt SLA 2s, router vẫn chọn frontier p50 ~6s; không đọc sla_ms |
| R07 | phân tích log | mid timeout → frontier timeout → nano | fallback 2 tầng, tổng 14s, trả 3 lần tiền, cuối cùng nano trả lời hời hợt |
| R08 | "viet code python doc file csv" | lang=unknown → mặc định frontier | tiếng Việt không dấu bị nhận "unknown", rơi vào nhánh mặc định đắt nhất |
| R09 | cùng một request gửi 2 lần | 0,49 → nano; 0,51 → mid | request y hệt, điểm dao động quanh ngưỡng 0,5 → lúc nano lúc mid |
| R10 | tìm nguyên nhân từ 2k dòng log | 0,81 → frontier, ổn; shadow mid sai | Đạt |
- Đọc cả trace, không chỉ trường
chosen: R04 "chọn mid" trông hợp lý — chỉ trườngtruncatedmới lộ lỗi. - Lỗi đầu tiên: R08 là over-route, nhưng lỗi đầu tiên nằm ở bước trích đặc trưng (nhận ngôn ngữ), không phải ở router.
- Dùng shadow làm bằng chứng: R03 chỉ là "Không đạt" vì shadow cho thấy mid đủ tốt; R10 là "Đạt" vì shadow cho thấy mid không đủ.
Bước 4 — Axial coding → taxonomy routing v1
| Failure mode | Định nghĩa một dòng | Trace | Hướng sửa (khác nhau → tách) |
|---|---|---|---|
| Under-route: sai độ khó | Chọn model yếu hơn mức cần vì ước lượng độ khó sai | R02 | Thêm đặc trưng cấu trúc (số bước, có công thức), không chỉ độ dài |
| Under-route: điểm yếu domain | Độ khó đúng nhưng model rẻ yếu ở domain đó | R05 | Luật theo domain (vd. pháp lý VN ≥ frontier) |
| Over-route | Chọn model đắt khi model rẻ hơn đạt tương đương | R03 | Hiệu chỉnh ngưỡng; thêm tier giữa |
| Vi phạm ràng buộc cứng | Bỏ qua context window, SLA latency, chính sách dữ liệu | R04, R06 | Lọc cứng candidate trước khi chấm điểm |
| Fallback dây chuyền | Chuỗi fallback làm tăng cost/latency và giảm chất lượng | R07 | Giới hạn độ sâu, fallback theo ngân sách thời gian |
| Sai đặc trưng đầu vào | Bước trích đặc trưng (ngôn ngữ, task) sai → quyết định sai | R08 | Sửa detector, test riêng |
| Bất ổn tại ngưỡng | Cùng request, quyết định khác nhau | R09 | Hysteresis / cache quyết định theo hash request |
Chú ý cặp "under-route": sách nhấn mạnh tách khi cách chẩn đoán/sửa khác nhau — ở đây một cái sửa bộ ước lượng độ khó, cái kia cần luật theo domain. Gộp thành "under-route" chung sẽ khiến bạn sửa sai chỗ.
Bước 5 — Đếm và biến thành metric
Nhãn 0/1 theo từng mode cho ra prevalence như mọi app khác. Với routing, hai mode chính còn có thể quy ra tiền và chất lượng nhờ shadow replay:
# routing_analysis.py — phân loại under/over-route từ shadow replay (minh hoạ, tự viết)
TIERS = ["nano", "mid", "frontier"] # thứ tự tăng dần về năng lực & giá
def classify(t):
"""t: {chosen, verdict: {model: 'pass'|'fail'}, cost: {model: usd}}"""
ok = [m for m in TIERS if t["verdict"].get(m) == "pass"]
if not ok:
return "no_model_passes", 0.0 # không model nào đạt → không phải lỗi router
cheapest_ok = ok[0]
chosen = t["chosen"]
if t["verdict"].get(chosen) != "pass":
return "under_route", 0.0
if TIERS.index(chosen) > TIERS.index(cheapest_ok):
return "over_route", t["cost"][chosen] - t["cost"][cheapest_ok]
return "ok", 0.0
traces = [
{"id": "R02", "chosen": "nano", "verdict": {"nano": "fail", "frontier": "pass"},
"cost": {"nano": 0.0004, "frontier": 0.021}},
{"id": "R03", "chosen": "frontier", "verdict": {"mid": "pass", "frontier": "pass"},
"cost": {"mid": 0.0016, "frontier": 0.019}},
{"id": "R10", "chosen": "frontier", "verdict": {"mid": "fail", "frontier": "pass"},
"cost": {"mid": 0.004, "frontier": 0.045}},
]
counts, waste = {}, 0.0
for t in traces:
label, extra = classify(t)
counts[label] = counts.get(label, 0) + 1
waste += extra
print(t["id"], label, f"+${extra:.4f}" if extra else "")
n = len(traces)
print({k: f"{v}/{n}" for k, v in counts.items()}, f"lãng phí ≈ ${waste:.4f}")
Lưu ý verdict thiếu được coi là không biết (không đạt), nên model chưa chạy shadow sẽ không bao giờ được coi là "rẻ hơn mà vẫn đạt" — bảo thủ có chủ đích. Với một mẫu shadow đủ lớn, bạn có ba metric đầu tiên cho benchmark routing: tỷ lệ under-route (mất chất lượng), tỷ lệ over-route + $ lãng phí/1.000 request, và tỷ lệ vi phạm ràng buộc cứng (nên ≈ 0, đo bằng check code).
Bẫy riêng của routing
- Chỉ chấm "model được chọn có trả lời tốt không" — không bao giờ phát hiện over-route (câu trả lời tốt, chỉ là đắt vô ích).
- Dùng leaderboard công khai thay cho open coding — "model X mạnh hơn ở benchmark Y" không nói gì về traffic tiếng Việt, không dấu, pháp lý của bạn.
- Điểm "route tốt 1–5" — thay bằng câu hỏi nhị phân theo từng mode: under? over? vi phạm ràng buộc?
- Bộ tổng hợp toàn câu dễ — router trông tuyệt vời vì 90% request đi nano và đều đạt; phải có tuple "ngắn mà khó" và "dài mà dễ".
- Quên tính không tất định — model đích không tất định, nên một lần shadow "fail" có thể là xui; với trace sát ranh giới, chạy 3 lần.
- Đóng băng taxonomy khi thêm model mới vào pool — mỗi model mới có thể mang theo mode mới (vd. từ chối quá mức, format JSON khác).
Checklist một vòng error analysis cho router (1–2 ngày)
- Log đủ trường trace routing (bao gồm
truncated,sla_ms, điểm từng candidate). - Lấy ~100 trace: 60 phân tầng (model × task) + 25 tín hiệu xấu + 15 tuple tổng hợp "gài bẫy".
- Shadow replay trên 1–2 tier lân cận; chấm Đạt/Không đạt cho từng output.
- Open coding lỗi đầu tiên (feature → router → model → fallback), cột Đạt/Không đạt.
- Axial coding; tách under-route theo nguyên nhân; viết codebook.
- Gán 0/1, đếm prevalence + $ lãng phí; cắt theo task và ngôn ngữ; ghi
k/nvà khoảng tin cậy. - Quyết định: check cứng cho ràng buộc; sửa đặc trưng; chọn 1–2 mode để xây evaluator (chương 5) và theo dõi trong CI (chương 9).
Viết open code và chọn mode (theo bảng trên) cho: (a) request 6k token, SLA 30s, chọn frontier, trả lời đúng; shadow mid cũng đúng, nano sai. (b) request tiếng Anh xen tiếng Việt, detector ra "en", router chọn nano, trả lời bằng tiếng Anh trong khi người dùng muốn tiếng Việt. (c) request có số CMND, chính sách công ty cấm gửi PII ra provider ngoài, router chọn model của provider ngoài.
Xem lời giải
- (a) "frontier cho request mid làm đạt; dư ~1 tier" → Over-route. (nano sai không quan trọng: rẻ nhất mà đạt là mid.)
- (b) "câu trộn Anh–Việt bị nhận 'en', trả lời tiếng Anh; user muốn tiếng Việt" → Sai đặc trưng đầu vào (lỗi đầu tiên ở detector, không phải ở model).
- (c) "request có CMND, gửi ra provider ngoài trái chính sách dữ liệu" → Vi phạm ràng buộc cứng. Nghiêm trọng dù hiếm → ô "hiếm nhưng nặng", xử lý bằng bộ lọc cứng + check code, không đợi evaluator.