Giới thiệu: Ba Vực thẳm & vòng lặp Analyze–Measure–Improve
TL;DR
- Sách tách agent (phần do LLM điều khiển: suy luận, gọi tool, sinh output) khỏi application (UI, DB, API, hạ tầng) và chỉ tập trung eval tầng agent.
- LLM app không deterministic và không có spec hình thức → không test theo kiểu unit test truyền thống; output có thể "đúng mà sai vibe" hoặc "rất thuyết phục mà sai hoàn toàn".
- Evaluation = quá trình có hệ thống để hiểu (tìm mẫu lỗi) và đo (tần suất lỗi); gắn vào hệ thống qua 4 cách: monitoring · guardrails · improvement · model migration.
- Mọi lỗi của LLM app rơi vào một trong 3 vực thẳm: Comprehension (bạn ↔ data) · Specification (bạn ↔ agent) · Generalization (data ↔ agent).
- Cách vượt vực: vòng lặp Analyze → Measure → Improve. Lỗi spec thì sửa prompt ngay; lỗi generalization thì đo tần suất rồi mới can thiệp mạnh hơn.
Cách đọc trang này
Phần "sách nói gì" được diễn giải ngắn gọn bằng lời của người tóm tắt. Phần lớn nội dung còn lại — ví dụ, dữ liệu, code, case study, bài tập, lab — là tài liệu tự soạn để luyện tập; mọi con số trong đó là minh hoạ (illustrative), không lấy từ sách. Code Python chạy được bằngpython3, không cần thư viện ngoài.
1.1Agent vs. Application — eval cái gì?
Trực giác. Khi một sản phẩm có LLM "chạy sai", có hai chỗ để đổ lỗi: bộ não (LLM agent — đọc input, suy luận, gọi tool, viết câu trả lời) và cơ thể quanh nó (application — giao diện, database, API, hạ tầng deploy). Sách dùng chữ "agent" theo nghĩa rộng: từ một lần gọi LLM không có tool, đến vòng lặp gọi nhiều tool và ra nhiều quyết định cho mỗi request. Toàn bộ cuốn sách tập trung vào tầng agent, vì lỗi ở tầng application đã có công cụ kiểm thử phần mềm truyền thống lo.
Một phòng khám tư dùng bot chat để đặt lịch. Tuần đầu nhận 6 bug report. Ta tách xem cái nào thuộc tầng agent (sách quan tâm) và cái nào thuộc application:
| # | Bug report | Tầng | Vì sao |
|---|---|---|---|
| 1 | Bot đặt lịch vào Chủ nhật dù phòng khám đóng cửa | Agent | Suy luận sai về ràng buộc lịch (dữ liệu giờ mở cửa có sẵn trong context) |
| 2 | Nút "Xác nhận" không bấm được trên iPhone SE | Application | Lỗi UI, không liên quan LLM |
| 3 | Bot gọi tool cancel_booking khi khách chỉ hỏi "hủy được không?" | Agent | Gọi sai tool — hiểu câu hỏi thành mệnh lệnh |
| 4 | Trả lời mất 25 giây vì DB lịch bác sĩ bị lock | Application | Hạ tầng/DB chậm |
| 5 | Bot trả về JSON thiếu trường doctor_id làm backend crash | Agent | Sai format output (dù hậu quả hiện ra ở backend) |
| 6 | Bot nói "bác sĩ Hà không có lịch" vì API lịch trả mảng rỗng do lỗi timezone | Application | Agent làm đúng với data nó nhận; data sai từ API |
- Hỏi: nếu thay LLM bằng một người thật đọc cùng context, lỗi còn xảy ra không? Nếu còn (bug 2, 4, 6) → gần như chắc là tầng application.
- Hỏi: LLM có nhận đủ thông tin đúng không? Có mà vẫn sai (bug 1, 3, 5) → tầng agent.
- Cẩn thận với "triệu chứng ở chỗ khác": bug 5 làm backend crash, nhưng nguyên nhân gốc là output của agent.
- Kết quả: 3/6 bug thuộc phạm vi eval của sách. 3 cái còn lại xử lý bằng test UI, load test, test tích hợp API.
Hay nhầm
"Agent" ở đây không chỉ là hệ thống tự chủ nhiều bước kiểu AutoGPT. Một lần gọi LLM để tóm tắt email cũng là "agent" theo nghĩa của sách. Và ranh giới agent/application quan trọng vì nó quyết định ai sửa và dùng công cụ gì để bắt: bug 6 ở trên mà đem đi sửa prompt thì mất công vô ích.Xem lời giải
(a) Chủ yếu application — pipeline index không đồng bộ khi xoá trang (data sai đi vào agent). (b) Agent — có đúng tài liệu mà suy luận sai. (c) Tuỳ: nếu Slack giới hạn độ dài tin nhắn và code cắt chuỗi → application; nếu model dừng vìmax_tokens quá thấp do cấu hình gọi model → vùng giáp ranh, nhưng không phải lỗi "trí tuệ" của agent. Bài học: đọc trace đầy đủ mới phân loại chắc được.
1.2Vì sao LLM app khác phần mềm truyền thống
Trực giác. Hàm tinh_thue(gia) luôn trả cùng kết quả cho cùng input, và bạn có spec rõ ("VAT 10%"). LLM app thì ngược lại ở cả hai điểm: (1) cùng input có thể cho output khác nhau giữa các lần chạy; (2) không có spec hình thức nào nói output "phải" là gì. Lỗi cũng đa dạng hơn: có lúc sai rõ ràng, có lúc đúng về nội dung nhưng không phù hợp (giọng điệu lạ, dài dòng), có lúc nghe rất thuyết phục nhưng sai hoàn toàn. Ngay cả ML truyền thống cũng dễ hơn: output là số/nhãn, có ground truth, dùng accuracy/F1 là xong; còn output LLM mở, phi cấu trúc và thường mang tính chủ quan.
Prompt: Tóm tắt email sau cho quản lý. — email khách xin báo giá 200 áo đồng phục, cần trước thứ 6, hỏi thêm có in logo không.
| Lần | Output (rút gọn) | Nhận xét |
|---|---|---|
| 1 | "Khách cần báo giá 200 áo trước thứ 6, hỏi về in logo." | Tốt |
| 2 | • Báo giá 200 áo • Hạn: thứ 6 • Hỏi in logo | Tốt nhưng khác format |
| 3 | "Khách hàng Lan gửi lời chào và mong nhận được phản hồi sớm..." (4 câu) | Dài, lạc trọng tâm — "sai vibe" |
| 4 | "Khách cần báo giá 200 áo trước thứ 6 và đã đồng ý in logo." | Sai mà nghe rất tự tin: khách mới hỏi, chưa đồng ý |
| 5 | "Khách cần báo giá 200 áo trước thứ 6, hỏi về in logo." | Tốt |
- Unit test kiểu
assert out == "Khách cần báo giá..."sẽ fail ở lần 2 dù output tốt → assert chuỗi chính xác vô dụng. - Lần 4 chỉ phát hiện được nếu ai đó (người hoặc evaluator) hiểu ngữ nghĩa: "hỏi" ≠ "đồng ý".
- Lần 3 không sai sự thật; nó sai kỳ vọng — mà kỳ vọng ("ngắn, cho quản lý đọc lướt") chưa hề được viết ra.
- Vậy "test" LLM app cần: đo trên nhiều input và nhiều lần chạy, với tiêu chí dạng thuộc tính ("không bịa cam kết", "≤ 2 câu") thay vì so khớp chuỗi.
| Phần mềm truyền thống | ML truyền thống | LLM application | |
|---|---|---|---|
| Output | Deterministic | Số / nhãn cố định | Văn bản mở, thay đổi giữa các lần chạy |
| Spec | Rõ (yêu cầu nghiệp vụ) | Ground truth labels | Thường không có; hình thành dần khi đọc output |
| Kiểm tra | Unit/integration test | Accuracy, F1, AUC | Error analysis + evaluator tự động (code/LLM-judge) + người |
| Kiểu lỗi | Crash, sai logic | Nhầm lớp | Bịa, lạc đề, sai giọng, sai format, gọi sai tool… |
Hay nhầm: "đặt temperature = 0 là deterministic, test như thường"
Temperature 0 giảm ngẫu nhiên nhưng không giải quyết vấn đề chính: (1) nhiều API vẫn cho output hơi khác giữa các lần do batching/phần cứng; (2) quan trọng hơn, thay đổi input rất nhỏ (một dấu phẩy, thêm chữ ký email) đã có thể đổi output — và input thật thì vô cùng đa dạng; (3) vẫn không có spec để assert. Non-determinism theo input mới là nguồn khó chính.sender; (b) tóm tắt "đủ ý chính"; (c) câu trả lời không chứa số điện thoại; (d) giọng văn "thân thiện nhưng chuyên nghiệp"; (e) SQL sinh ra chỉ chứa câu lệnh SELECT.
Xem lời giải
Code được: (a), (c) (regex), (e) (parse SQL). Cần judgment: (b), (d) → người chấm hoặc LLM-as-judge có rubric (Chương 5). Lưu ý: dù (a)(c)(e) check bằng code, quyết định check những thuộc tính nào vẫn đến từ việc đọc output thật (Analyze).1.3Câu chuyện cảnh báo: chatbot hãng bay
Sách mở đầu bằng vụ chatbot của Air Canada: một hành khách hỏi về giá vé tang lễ (bereavement fare); bot bảo cứ mua vé thường rồi xin hoàn phần giảm giá trong vòng 90 ngày sau chuyến bay — trong khi chính sách thật chỉ cho xin trước chuyến bay. Hãng từ chối hoàn tiền, khách kiện ra toà dân sự British Columbia và thắng (2/2024); toà bác lập luận rằng chatbot là "thực thể pháp lý riêng" và bắt hãng bồi thường. Không ai biết chắc vì sao bot sai (diễn giải sai chính sách, hay lẫn với chính sách hãng khác). Điều chắc chắn: không ai bắt được lỗi trước khi khách tin vào nó. Một bộ check có hệ thống trên các câu hỏi chính sách phổ biến lẽ ra đã phát hiện được.
Sách cũng nhấn mạnh eval không dừng ở lúc launch: eval trước launch bắt lỗi trước khi tới người dùng; monitoring production bắt lỗi chỉ xuất hiện khi phân bố input thật dịch chuyển và người dùng hành xử khó đoán (Chương 9).
Giả sử bạn là kỹ sư của một hãng bay (hư cấu) sắp ra mắt bot. Làm thế nào để có "một bộ check câu hỏi chính sách" trong một buổi chiều?
- Lấy câu hỏi thật: xuất 3 tháng ticket tổng đài, lọc ra 20 chủ đề chính sách hỏi nhiều nhất (hành lý, đổi vé, hoàn vé, vé tang lễ, trẻ em đi một mình…).
- Viết đáp án chuẩn dạng thuộc tính với chuyên gia chính sách: mỗi câu có danh sách cụm phải có và cụm không được có (ví dụ: vé tang lễ → phải có "trước chuyến bay", không được có "sau chuyến bay").
- Diễn đạt lại mỗi câu 3–5 kiểu (không dấu, tiếng Anh, viết sai chính tả, hỏi vòng vo) vì khách thật không hỏi theo FAQ.
- Chạy bot, chấm tự động bằng code đơn giản dưới đây; câu nào FAIL thì người đọc lại trace.
- Chặn launch nếu có bất kỳ FAIL nào ở nhóm "chính sách tiền bạc" — stakes cao, sai một lần cũng đắt.
# policy_eval.py — dữ liệu & câu trả lời đều là minh hoạ
CASES = [
{"id": "P1", "q": "Bay dự đám tang có được giảm giá vé không?",
"must": ["trước chuyến bay"], "must_not": ["sau chuyến bay", "hoàn lại sau"]},
{"id": "P2", "q": "Hành lý xách tay tối đa bao nhiêu kg?",
"must": ["7 kg"], "must_not": ["10 kg"]},
{"id": "P3", "q": "Đổi tên hành khách trên vé được không?",
"must": ["không được đổi tên"], "must_not": ["đổi tên miễn phí"]},
]
ANSWERS = { # output thu được khi chạy bot (giả lập)
"P1": "Bạn cứ mua vé thường, rồi xin hoàn lại sau chuyến bay trong 90 ngày.",
"P2": "Hành lý xách tay tối đa 7 kg, kích thước 56x36x23 cm.",
"P3": "Rất tiếc, vé không được đổi tên, bạn cần mua vé mới.",
}
def check(case, answer):
a = answer.lower()
missing = [m for m in case["must"] if m.lower() not in a]
forbidden = [f for f in case["must_not"] if f.lower() in a]
return (not missing and not forbidden), missing, forbidden
fails = 0
for c in CASES:
ok, missing, forbidden = check(c, ANSWERS[c["id"]])
fails += not ok
print(f"{c['id']} {'PASS' if ok else 'FAIL'} thiếu={missing} cấm={forbidden}")
print(f"Tỉ lệ đạt: {len(CASES) - fails}/{len(CASES)}")
P1 FAIL thiếu=['trước chuyến bay'] cấm=['sau chuyến bay', 'hoàn lại sau']
P2 PASS thiếu=[] cấm=[]
P3 PASS thiếu=[] cấm=[]
Tỉ lệ đạt: 2/3
Check dạng từ khoá rất thô (bot có thể nói "trước chuyến bay" trong câu phủ định), nhưng nó đủ để đẩy P1 tới mắt người trước khi đến tay khách. Evaluator tinh hơn (LLM-as-judge có rubric) là chủ đề Chương 5.
Xem lời giải
Diễn đạt: "ba em mất, em bay về gấp có được giảm k?", "bereavement fare how to claim", "mua vé rồi mới nộp giấy chứng tử được không". Câu lọt check: "Bạn không cần xin trước chuyến bay, cứ nộp giấy tờ sau cũng được" — chứa cụm "trước chuyến bay" (pass must) và không chứa nguyên văn "sau chuyến bay" (pass must_not). Bài học: check từ khoá là lưới thô; câu hỏi tiền bạc nên có thêm judge ngữ nghĩa hoặc người duyệt.1.4Evaluation là gì?
Theo sách, evaluation là quá trình có hệ thống để hiểu và đo chất lượng của một LLM app — gồm cả phần định tính (phát hiện mẫu lỗi) và định lượng (đo tần suất các lỗi đó). Eval tốt cho kết quả diễn giải được (biết con số nghĩa là gì) và hành động được (biết phải sửa gì). Có lúc "metric" là một con số tính tự động (trợ lý du lịch: bao nhiêu % lịch trình có chuyến bay + khách sạn hợp lệ và trong ngân sách); có lúc là đánh giá có phán đoán của chuyên gia hay chính team sản phẩm (rõ ràng, giọng điệu, hữu ích). Mức nghiêm ngặt là một phổ: từ check nhanh lúc prototype tới rubric chặt + lấy mẫu có hệ thống cho production.
| Phiên bản eval | Kết quả | Diễn giải được? | Hành động được? |
|---|---|---|---|
| "Cho LLM chấm chất lượng lịch trình 1–10" | Trung bình 7,4 | Không — 7,4 là tốt hay tệ? | Không — không biết sửa gì |
| "% lịch trình hợp lệ" | 81% | Một phần | Chưa — 19% hỏng vì đâu? |
| Tách theo failure mode | Vượt ngân sách 11% · chuyến bay đã hết chỗ 5% · khách sạn sai thành phố 3% | Có | Có — "vượt ngân sách" là lớn nhất, xem prompt có nêu ngân sách là ràng buộc cứng chưa |
- Bắt đầu từ việc đọc ~50 lịch trình thật để thấy các kiểu hỏng (định tính).
- Mỗi kiểu hỏng thành một check riêng, có thể là code (tổng giá ≤ ngân sách) hoặc judge (hợp lý về di chuyển).
- Báo cáo tỉ lệ theo từng kiểu hỏng (định lượng) → ưu tiên sửa theo tỉ lệ × mức nghiêm trọng.
Bốn cách gắn eval vào hệ thống
Một app thường cần nhiều eval, mỗi cái đo một chiều. Chúng được gắn vào hệ thống theo 4 kiểu:
Chạy nền liên tục, phát hiện drift và suy giảm chất lượng theo thời gian.
Nằm ngay trên đường xử lý: chặn, thử lại hoặc fallback khi output không đạt.
Sinh dữ liệu gán nhãn, few-shot examples, danh sách lỗi dẫn tới đổi kiến trúc.
So sánh các phiên bản model để quyết định lúc nào nên chuyển.
Evaluator: "output JSON hợp lệ, có sender và requests không rỗng". Cùng một hàm check có thể được dùng theo cả 4 cách:
- Guardrail: chạy ngay sau mỗi lần gọi LLM; fail thì retry, fail tiếp thì fallback "chuyển người xử lý".
- Monitoring: log kết quả check; dashboard tỉ lệ fail theo ngày. Tỉ lệ nhảy từ 1% lên 6% sau một đợt email marketing mới → drift.
- Improvement: gom các input làm fail thành bộ ví dụ khó; chọn vài cái làm few-shot hoặc dữ liệu fine-tune.
- Model migration: nhà cung cấp ra model mới rẻ hơn 40% — chạy cả hai trên 1.000 email lưu sẵn, so tỉ lệ fail trước khi chuyển.
# guardrail.py — call_llm là giả lập: lần 1 trả JSON hỏng, lần 2 trả đúng
import json
def call_llm(prompt, attempt):
return '{"sender": "Lan", "requests": ["báo giá"' if attempt == 0 else \
'{"sender": "Lan", "requests": ["báo giá 200 áo trước thứ 6"]}'
def valid(output):
"""Evaluator dạng code: parse được JSON + đủ field + requests không rỗng."""
try:
data = json.loads(output)
except json.JSONDecodeError:
return False, "json_invalid"
if not data.get("sender") or not data.get("requests"):
return False, "missing_field"
return True, "ok"
def guarded_extract(prompt, max_retries=2, log=print):
for attempt in range(max_retries + 1):
out = call_llm(prompt, attempt)
ok, reason = valid(out)
log(f"attempt={attempt} ok={ok} reason={reason}") # log → monitoring
if ok:
return json.loads(out)
return {"sender": None, "requests": [], "fallback": "chuyển người xử lý"}
print(guarded_extract("Trích tên người gửi và yêu cầu..."))
attempt=0 ok=False reason=json_invalid
attempt=1 ok=True reason=ok
{'sender': 'Lan', 'requests': ['báo giá 200 áo trước thứ 6']}
Hay nhầm: "guardrail = eval, có guardrail rồi khỏi cần eval"
Guardrail chỉ bắt được những lỗi đã biết và check được rẻ, nhanh (nằm trên đường chính nên phải nhanh). Lỗi ngữ nghĩa kiểu "bot bịa chính sách" thường cần judge chậm hơn hoặc người đọc — chạy offline/monitoring. Và guardrail chỉ tồn tại vì ai đó đã phân tích lỗi trước để biết cần chặn cái gì. Ngoài ra, pre-launch eval và production monitoring bắt những loại lỗi khác nhau: cần cả hai.Xem lời giải
(a) Monitoring. (b) Guardrail. (c) Model migration. (d) Improvement. Một hệ thống trưởng thành thường có cả bốn, dùng chung một số evaluator.1.5Eval xuyên suốt vòng đời LLM
Eval xuất hiện ở cả ba giai đoạn, nhưng mục tiêu khác nhau — nên không có một framework eval dùng chung cho mọi nơi.
Model nền được huấn luyện từ đầu với một mục tiêu duy nhất: đoán token kế tiếp. Token là đơn vị văn bản nhỏ nhất model đọc/viết mỗi lần — thường là một từ hoặc một mảnh từ; chi phí API cũng tính theo số token vào/ra.
Eval ở đây là intrinsic: model đoán token kế tiếp tốt cỡ nào trên văn bản chưa từng thấy. Metric tiêu biểu là perplexity — mũ của negative log-likelihood trung bình mỗi token; càng thấp, model càng ít "bất ngờ". Kèm theo là phân tích scaling laws (loss giảm thế nào khi tăng compute/data). Những con số này nói model đã hấp thụ thống kê ngôn ngữ — không nói model có hữu ích cho tác vụ của bạn.
Biến model nền thành model dùng được: làm theo chỉ dẫn, bám chủ đề, tránh nội dung nguy hiểm. Thường gồm fine-tune trên cặp prompt–câu trả lời tốt, rồi dùng preference data (người chọn câu nào tốt hơn) để nắn phong cách; các phương pháp mới hơn như DPO, process reward model, và RL với phần thưởng kiểm chứng được (RLVR) cho chuỗi suy luận dài.
Eval chuyển sang benchmark gần tác vụ người dùng (QA, reasoning, safety/bias, coding), do nhà cung cấp tự chạy và công bố. Sách lưu ý: biết model được post-train kiểu gì giúp đoán kiểu lỗi nó sẽ mắc trong app, dù bạn không tự huấn luyện.
Đây là chỗ hầu hết kỹ sư và PM làm việc: model đã có sẵn, việc của bạn là tích hợp vào workflow cụ thể (phân loại email, CSKH, phân tích tài liệu). Đôi khi benchmark công khai trùng tác vụ (trợ lý code ↔ benchmark lập trình), nhưng phần lớn app không có mặt trên leaderboard nào — duyệt hợp đồng, tóm tắt hồ sơ bệnh án, kiểm tra tuân thủ đều cần eval riêng.
→ Trách nhiệm định nghĩa thành công, thiết kế metric và giám sát thuộc về team làm ứng dụng.
Một câu held-out có 5 token. Model A và B gán xác suất cho token đúng ở mỗi vị trí:
| Token | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| Model A | 0,50 | 0,25 | 0,80 | 0,10 | 0,60 |
| Model B | 0,70 | 0,40 | 0,90 | 0,30 | 0,75 |
- Lấy −ln từng xác suất của A: 0,693 · 1,386 · 0,223 · 2,303 · 0,511 → tổng 5,116.
- Chia cho số token: NLL trung bình = 5,116 / 5 ≈ 1,023.
- Perplexity = e1,023 ≈ 2,78 — hiểu nôm na: A "phân vân" như đang chọn giữa ~2,8 token mỗi bước.
- Làm tương tự cho B: ≈ 1,78. B tốt hơn về mô hình hoá ngôn ngữ.
- Nhưng điều đó không nói B tóm tắt email của bạn tốt hơn, tuân thủ format JSON tốt hơn, hay ít bịa chính sách hơn.
import math
model_A = [0.50, 0.25, 0.80, 0.10, 0.60]
model_B = [0.70, 0.40, 0.90, 0.30, 0.75]
def perplexity(probs):
nll = -sum(math.log(p) for p in probs) / len(probs) # NLL trung bình / token
return math.exp(nll), nll
for name, probs in [("A", model_A), ("B", model_B)]:
ppl, nll = perplexity(probs)
print(f"model {name}: NLL/token = {nll:.3f} perplexity = {ppl:.2f}")
# model A: NLL/token = 1.023 perplexity = 2.78
# model B: NLL/token = 0.574 perplexity = 1.78
Hay nhầm: "model đứng đầu leaderboard sẽ tốt nhất cho app của tôi"
Leaderboard đo tác vụ của người khác trên phân bố dữ liệu của người khác. App của bạn có định dạng input riêng (email tiếng Việt lẫn tiếng Anh, hoá đơn scan), định nghĩa "đúng" riêng, và ràng buộc riêng (chi phí, latency). Một model kém hơn 3 điểm benchmark có thể tốt hơn hẳn cho bạn — hoặc ngược lại. Chỉ bộ eval trên dữ liệu của chính bạn mới trả lời được.Xem lời giải
Benchmark reasoning là eval giai đoạn post-training, không đo trích xuất hoá đơn. Đề xuất: đây là bài toán model migration — chạy X và model hiện tại trên bộ eval hoá đơn đang có (ví dụ 300 hoá đơn thật đã gán nhãn), so theo từng failure mode (sai tổng tiền, sai MST, sai ngày) + chi phí + latency. Nếu chưa có bộ eval đó thì chính đây là lý do phải xây.1.6Ba khó khăn khi xây ở tầng ứng dụng
Ở tầng application bạn phải: quyết định sản phẩm cần đạt gì → dịch mục tiêu thành prompt/logic agent → kiểm tra output có chấp nhận được không. Mỗi bước có một cái bẫy riêng:
"Tóm tắt email" nghe đơn giản; chỉ khi đọc output mới lộ ra: chi tiết cỡ nào, format gì, loại trừ gì?
Prompt và logic bằng ngôn ngữ tự nhiên hiếm khi nói hết ý định → hành vi không nhất quán, thiếu sót.
Chạy tốt trên vài test case ≠ chạy tốt trước sự đa dạng của input thật.
Ba cái bẫy này ứng gần như một–một với Ba Vực thẳm ở phần tiếp theo.
1.7Ba Vực thẳm (The Three Gulfs)
Sách dùng mô hình Three Gulfs (phỏng theo Shankar et al. 2025, lấy cảm hứng từ Norman 1988) để gọi tên ba chỗ việc phát triển hay gãy. Ví dụ xuyên suốt: agent xử lý email gửi tới một tổ chức — trích tên người gửi, tóm tắt yêu cầu, phân loại email.
Developer ↔ Data. Không hiểu input thật trông ra sao thì bạn sẽ thiết kế cho một phiên bản tưởng tượng của bài toán.
- Input thật: email forward, reply kèm thread trích dẫn dài, chữ ký nhiều ngôn ngữ, text dán từ PDF lộn xộn → LLM tóm tắt nguyên thread cũ thay vì tin mới, hoặc nhầm khối chữ ký là yêu cầu chính.
- Output thật cũng thuộc vực này: thường ổn, nhưng thỉnh thoảng lấy nhầm tên khi email nhắc nhiều người, bỏ sót chi tiết trong email dài, chép "confidentiality notice" vào tóm tắt. Lỗi hiếm nhưng đủ để người dùng thấy hệ thống "không đáng tin".
Cách vượt: có chiến lược mô tả input và output ở quy mô lớn mà không phải đọc hết từng cái bằng tay.
Developer ↔ LLM Agent. Ý định trong đầu chỉ được prompt/logic nắm bắt một cách lỏng lẻo; ngôn ngữ tự nhiên không chính xác, khe hở nhỏ trong câu chữ → khác biệt lớn trong hành vi.
Chỉ dẫn "trích tên người gửi và tóm tắt các yêu cầu chính" nghe rõ nhưng bỏ ngỏ: đoạn văn hay bullet? Tên hiển thị, địa chỉ email hay cả hai? Có tính yêu cầu ngầm? Ngắn cỡ nào?
Những lựa chọn bạn không nói ra, LLM sẽ tự đoán — và đoán không phải lúc nào cũng khớp ý bạn → hai lần chạy cho output khác format, khác độ chi tiết, xử lý edge case không nhất quán.
Cách vượt: làm kỳ vọng thành tường minh, bám sát data mà agent thực sự gặp.
Data ↔ LLM Agent. Hiểu data rồi, prompt rõ rồi — model vẫn sai trên một số input.
Đã nói rõ "lấy tên từ header"; đa số lần model làm đúng, nhưng khi thân email nhắc tới một người khác (ví dụ người nổi tiếng) thì model trả tên người đó. Hiểu biết về data và prompt đều không sai — model chỉ đơn giản áp dụng chỉ dẫn không nhất quán (có thể do thiên lệch từ dữ liệu huấn luyện).
Lỗi loại này thường chỉ lộ ra khi gặp đủ độ đa dạng của data thật, và không tránh được hoàn toàn dù model mạnh đến đâu.
Cách vượt: đo tần suất bằng evaluator; nếu cao thì can thiệp mạnh hơn (thêm ví dụ, retrieval tốt hơn, fine-tune, hoặc đổi kiến trúc).
① Comprehension — ví dụ, code, bài tập
Thay vì đọc 5 email mẫu sạch đẹp rồi viết prompt, bạn lấy mẫu email thật và đếm các đặc điểm cấu trúc bằng vài regex rẻ tiền. Mục tiêu không phải đo chất lượng, mà là biết "data trông như thế nào" để không thiết kế cho bài toán tưởng tượng.
- Lấy mẫu ngẫu nhiên (ở đây 8 email cho gọn; thực tế vài trăm).
- Liệt kê các đặc điểm nghi ngờ gây khó: forward, thread trích dẫn, chữ ký/disclaimer, tiếng Anh, rác từ PDF, nhiều yêu cầu trong một email.
- Đếm tỉ lệ. Đặc điểm nào ≥ 10% thì prompt phải xử lý tường minh và bộ test phải có đại diện.
- Đọc kỹ vài email của mỗi nhóm — con số chỉ chỉ đường, không thay việc đọc.
import re
from collections import Counter
EMAILS = [ # 8 email giả lập
"Chào anh, em cần báo giá 200 áo đồng phục trước thứ 6. Cảm ơn, Lan",
"---------- Forwarded message ----------\nFrom: Minh\nNhờ chị xử lý giúp đơn #8812",
"Dạ em gửi lại hợp đồng.\n\n> On Mon, Hùng wrote:\n> Anh gửi bản nháp\n> > Em xem nhé",
"Hi team, please confirm the invoice.\n--\nJohn Tran | Sales\nCONFIDENTIAL: This email is intended only for the recipient",
"Anh ơi hủy giúp em đơn hôm qua",
"Kính gửi Quý công ty,\nChúng tôi đề nghị hợp tác... (dán từ PDF) Trang 1/3 Điều 1.1",
"Re: Re: Re: lịch họp\n> > > thứ 3 được không\n> > được\n> ok chốt",
"Gửi bộ phận CSKH: sản phẩm bị lỗi, tôi muốn đổi trả và được hoàn tiền phí ship",
]
FEATURES = {
"forwarded": lambda e: "forwarded message" in e.lower(),
"quoted_thread": lambda e: bool(re.search(r"^>", e, re.M)),
"signature_or_disclaimer": lambda e: bool(re.search(r"^--\s*$|confidential", e, re.M | re.I)),
"english": lambda e: bool(re.search(r"\b(please|the|hi)\b", e, re.I)),
"pdf_artifact": lambda e: bool(re.search(r"Trang \d+/\d+|Điều \d", e)),
"multi_request": lambda e: len(re.findall(r"\b(và|muốn|cần|nhờ)\b", e, re.I)) >= 2,
}
counts = Counter()
for e in EMAILS:
for name, fn in FEATURES.items():
counts[name] += fn(e)
print(f"{'đặc điểm':<26}{'số email':>9}{'tỉ lệ':>8}")
for name in FEATURES:
print(f"{name:<26}{counts[name]:>9}{counts[name] / len(EMAILS):>8.0%}")
đặc điểm số email tỉ lệ
forwarded 1 12%
quoted_thread 2 25%
signature_or_disclaimer 1 12%
english 1 12%
pdf_artifact 1 12%
multi_request 1 12%
Kết luận (trên data minh hoạ): 1/4 email có thread trích dẫn → prompt phải nói rõ "chỉ tóm tắt tin nhắn mới nhất, bỏ phần trích dẫn", và bộ test phải có loại email này. Đây là cầu nối từ Comprehension sang Specification.
Hay nhầm: "Comprehension chỉ là hiểu input"
Vực này gồm cả output: bạn phải biết model thường thành công/thất bại theo những kiểu nào. Đọc 5 output đẹp rồi yên tâm là rơi đúng vào vực — lỗi xảy ra 3–5% chỉ lộ ra khi nhìn ở quy mô lớn.② Specification — ví dụ, bài tập
Prompt v1: Trích tên người gửi và tóm tắt yêu cầu trong email. Ta liệt kê mọi quyết định mà v1 để model tự đoán, rồi trả lời từng cái dựa trên data đã profile ở Ví dụ 1.7:
| Quyết định bị bỏ ngỏ | Model có thể đoán | Spec chọn (và lý do) |
|---|---|---|
| Format tóm tắt | đoạn văn / bullet / 1 câu | Tối đa 3 bullet, mỗi bullet ≤ 15 từ — quản lý đọc lướt trên điện thoại |
| Tên người gửi | display name / email / cả hai | Display name từ header From; không có thì dùng phần trước @ |
| Thread trích dẫn | tóm tắt cả thread | Chỉ tin mới nhất; phần > chỉ dùng làm ngữ cảnh |
| Yêu cầu ngầm | có / không | Có, nhưng đánh dấu "(ngầm)" — ví dụ khách than giao trễ = ngầm muốn được bồi thường |
| Chữ ký, disclaimer | đôi khi chép vào | Bỏ hoàn toàn |
| Email không có yêu cầu | bịa ra một yêu cầu | Trả requests: [], category FYI |
- Mỗi dòng bảng là một quyết định sản phẩm, không phải chi tiết kỹ thuật — nên hỏi PM/chuyên gia domain khi không chắc.
- Spec phải bám data: dòng "thread trích dẫn" chỉ xuất hiện vì profile cho thấy 25% email có thread.
- Sau khi sửa, output khác format giữa các lần chạy gần như biến mất — dấu hiệu lỗi cũ là specification, không phải generalization.
Mẹo phân biệt Specification vs Generalization: "phép thử nhà thầu"
Đưa prompt (và đúng input đó) cho một người thông minh nhưng chưa từng biết dự án. Nếu họ cũng có thể làm "sai" theo cùng cách mà vẫn hợp lý với chỉ dẫn → lỗi specification (chỉ dẫn cho phép cách hiểu đó). Nếu mọi người đọc prompt đều thấy rõ đáp án đúng, mà model vẫn sai → generalization. (Đây là heuristic tự đề xuất, không phải của sách.)Xem lời giải
(1) Chính sách đổi trả (bao nhiêu ngày, điều kiện) — model sẽ bịa nếu không có. (2) Khi nào escalate sang người thật (khách đe doạ/giận dữ?) hay chỉ xin lỗi. (3) Được hứa gì (đổi mới, hoàn tiền, voucher) và không được hứa gì. (4) Giọng điệu với khách giận (xin lỗi bao nhiêu, có nhắc lại lời đe doạ không). (5) Cần thu thập thông tin gì (mã đơn, ảnh/video lỗi). Sách cũng lấy ví dụ tương tự: bot xin lỗi khi lẽ ra phải chuyển người thật là đang theo chỉ dẫn sai.③ Generalization — ví dụ, code, bài tập
Sau khi spec đã ghi "bỏ hoàn toàn chữ ký/disclaimer", bạn vẫn thấy vài tóm tắt có câu "CONFIDENTIAL…". Prompt rõ ràng rồi → nghi generalization. Câu hỏi bây giờ là bao thường xuyên.
- Viết evaluator rẻ: tìm các cụm boilerplate trong tóm tắt (code, không cần LLM).
- Chạy trên 2.000 output (ở đây giả lập với tỉ lệ thật ~8%).
- Báo cáo tỉ lệ kèm khoảng tin cậy (Wilson 95%) để biết con số chắc cỡ nào.
- So với việc chỉ đọc 10 trace và thấy 2 lỗi: ước lượng "20%" nhưng khoảng tin cậy trải từ ~6% đến ~51% — gần như vô nghĩa.
import math, random
def wilson(k, n, z=1.96):
"""Khoảng tin cậy 95% Wilson cho tỉ lệ lỗi k/n."""
if n == 0:
return (0.0, 1.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)
BOILERPLATE = ["confidential", "intended only for", "sent from my iphone", "bảo mật"]
def has_boilerplate(summary: str) -> bool:
s = summary.lower()
return any(b in s for b in BOILERPLATE)
random.seed(7) # giả lập 2.000 tóm tắt, ~8% bị lọt disclaimer
summaries = [
"Khách xin báo giá 200 áo. CONFIDENTIAL: intended only for recipient"
if random.random() < 0.08 else "Khách xin báo giá 200 áo trước thứ 6."
for _ in range(2000)
]
k = sum(has_boilerplate(s) for s in summaries)
lo, hi = wilson(k, len(summaries))
print(f"Lỗi boilerplate: {k}/{len(summaries)} = {k/len(summaries):.1%} (95% CI {lo:.1%}–{hi:.1%})")
lo, hi = wilson(2, 10)
print(f"Chỉ 10 trace, 2 lỗi: 20% (95% CI {lo:.1%}–{hi:.1%})")
Lỗi boilerplate: 180/2000 = 9.0% (95% CI 7.8%–10.3%)
Chỉ 10 trace, 2 lỗi: 20% (95% CI 5.7%–51.0%)
Khi biết lỗi ~9% và ổn định, bạn có cơ sở để chọn can thiệp mạnh hơn — ví dụ đổi kiến trúc: cắt chữ ký bằng code trước khi đưa email cho LLM, thay vì hi vọng model tự bỏ qua.
Hay nhầm: "lỗi generalization thì đợi model mới là hết"
Sách nói rõ: dù model tiến bộ tới đâu, vẫn luôn có input mà nó sai. Model mới có thể giảm tỉ lệ lỗi này nhưng tạo lỗi khác — nên đổi model cũng phải đi qua eval (model migration). Và nhiều lỗi generalization được giải quyết rẻ nhất bằng kiến trúc (lấy tên từ metadata, cắt chữ ký bằng regex) chứ không phải bằng model tốt hơn.Xem lời giải
(a) Không. 1/20 cho khoảng tin cậy Wilson 95% khoảng 0,9%–23,6% — quá rộng. (b) Vì prompt đã rõ → nghi generalization. Viết evaluator (so số trích được với tổng tính lại từ các dòng hàng + VAT, hoặc với nhãn thật trên tập có nhãn), chạy trên vài trăm–vài nghìn hoá đơn, xem lỗi tập trung ở loại hoá đơn nào (ví dụ mẫu có dòng "Tổng trước thuế" nằm cuối). Rồi mới chọn cách sửa: thêm few-shot cho mẫu đó, hoặc hậu kiểm bằng code.Cùng một khung, domain khác
| Chatbot CSKH | Trợ lý code | Bot tư vấn bảo hiểm (tự soạn) | |
|---|---|---|---|
| Comprehension | Câu hỏi đa ngôn ngữ, lỗi chính tả, hỏi nhiều ý một lúc | Yêu cầu mơ hồ, thiếu context, nhắc tới API nội bộ không có docs | Khách dán nguyên hợp đồng 30 trang; hỏi về gói đã ngừng bán |
| Specification | Thế nào là trả lời "tốt"? Xin lỗi hay escalate lên người thật? | Format output, cách xử lý lỗi, coding style | Được phép so sánh với đối thủ không? Khi nào bắt buộc kèm câu miễn trừ? |
| Generalization | Tốt với câu hỏi billing, kém với câu hỏi sản phẩm ít gặp | Python tốt; ngôn ngữ hiếm hoặc sửa nhiều file thì kém | Đúng với gói phổ biến; nhầm điều khoản giữa hai gói tên gần giống nhau |
Sách chốt ý: chiến lược eval cho mỗi app phụ thuộc vào vực nào là nguồn lỗi chính.
Vì sao khung này đáng giá
Nó cho team một từ vựng chung. Thay vì cãi nhau "model không chạy", bạn nói: "đây là lỗi specification — prompt chưa nói rõ" hoặc "đây là lỗi generalization — nói rõ rồi mà model áp dụng không nhất quán". Chẩn đoán đúng → chọn đúng cách sửa, nhanh hơn.14 tình huống lỗi hư cấu từ nhiều domain. Chọn vực thẳm chính; nhận giải thích và điểm. Gợi ý: hỏi (1) team đã nhìn data thật chưa? (2) prompt/spec đã nói rõ chưa? (3) nói rõ rồi mà vẫn sai?
1.8Vì sao eval LLM app khó
Mỗi app phải vượt lại cả ba vực trong bối cảnh riêng của nó — không có bộ eval "mua về dùng luôn". Và yêu cầu chỉ rõ dần sau khi tương tác với output thật, nên:
Khi bắt đầu đọc output (vượt vực Comprehension), bạn chắc chắn sẽ thấy lỗi. Câu hỏi tiếp theo luôn là: lỗi do spec chưa đủ, hay do generalization? Nếu prompt chưa nêu yêu cầu → sửa chỉ dẫn/thiết kế agent. Nếu prompt đã rõ mà model vẫn sai → dùng evaluator tự động để đo tần suất.
Người dùng tự vượt vực được bao nhiêu?
Lập trình viên tự spec chính xác, đọc và debug được code sinh ra (comprehension), chạy test để lộ lỗi generalization (ví dụ lúc sinh được React app chạy, lúc không). Một phần gánh eval do người dùng tự làm.
Người dùng khó nói rõ yêu cầu → app phải tự hỏi lại (thời tiết? mức trang trọng?) để lấp vực spec. Và họ khó nhận ra lỗi generalization: gợi ý đồ đông thường tốt không có nghĩa đồ cưới mùa đông cũng tốt.
Quy tắc của sách: stakes càng cao — vì người dùng khó tự bắt lỗi, hoặc vì lỗi gây hậu quả lớn — thì quy trình lấp vực càng phải chặt. Hình dưới là cách tự vẽ để đặt app của bạn lên bản đồ:
Xem lời giải
(a) Đắt · khó bắt: nhân viên không tự kiểm tra được luật thuế → lo generalization (quy tắc hiếm, giảm trừ gia cảnh) và spec (khi nào phải chuyển HR). (b) Rẻ · dễ bắt: người dùng đọc caption trước khi đăng → check nhanh đủ. (c) Đắt · khó bắt vì không ai duyệt → cần guardrail trên đường chính + monitoring; vực comprehension đáng lo nhất vì email thật rất đa dạng.1.9Vòng đời Analyze → Measure → Improve
Ba Vực thẳm gọi tên các kiểu hỏng; vòng lặp Analyze–Measure–Improve là quy trình làm các vực đó hiện ra và quyết định xử lý thế nào. Đây là xương sống của cả cuốn sách.
Mục tiêu: nhận diện failure modes — chưa đếm.
Đọc trace (bản ghi đầy đủ agent đã làm gì cho một request), hỏi người dùng/chuyên gia domain "đúng" là gì, dựng danh sách các kiểu hỏng. Analyze gắn nhất với vực Comprehension, nhưng trong cùng một buổi đọc trace bạn thường phát hiện cả ba loại vấn đề. (→ Chương 3)
Sách Đọc một loạt tóm tắt email, thấy 2 lỗi lặp: chèn chữ ký không liên quan vào tóm tắt; bỏ sót yêu cầu quan trọng nhất.
Câu hỏi: mỗi failure mode xảy ra bao thường xuyên? Có đủ thấp để hệ thống dùng được?
Những lỗi rõ ràng là spec thì sửa luôn; Measure chủ yếu để định lượng lỗi generalization. Không đo thì bạn dựa vào ấn tượng từ vài trace — dễ sai cả hai chiều (vài ví dụ xấu làm hệ thống tốt trông hỏng; vài ví dụ đẹp làm hệ thống hỏng trông ổn). Công cụ: evaluator tự động chấm trace không cần người — rule bằng code, hoặc LLM-as-judge. (→ Chương 5)
Sách Dù đã làm rõ prompt, chữ ký vẫn lọt; check rule-based trên hàng nghìn output → 8%.
Chọn cách sửa theo nguồn lỗi:
- Lỗi spec → siết prompt, sửa logic agent.
Sách Bỏ sót yêu cầu vì prompt chưa nói mức chi tiết → yêu cầu tóm tắt thành 3 bullet ngắn. - Lỗi generalization → can thiệp mạnh hơn: thêm ví dụ, cải thiện retrieval, fine-tune trên data có nhãn, hoặc đổi kiến trúc.
Sách Nhầm tên vẫn còn dù prompt rõ → lấy tên người gửi từ metadata thay vì để LLM đọc thân email.
Mỗi vòng giúp hiểu rõ hơn bạn đang đối mặt vực nào, và chọn mức nghiêm ngặt phù hợp. (→ Chương 12: chiến lược cải thiện theo mức công sức)
- Analyze — lấy ngẫu nhiên 120 trace của tuần qua, mỗi trace ghi 1 dòng ghi chú tự do ("tóm tắt cả thread cũ", "tên sai: lấy 'Sơn Tùng' trong thân mail", "3 bullet nhưng bỏ mất deadline"…). Gom ghi chú thành 5 failure mode.
- Phân loại vực — với mỗi mode hỏi "prompt đã nói chưa?": F1 tóm tắt cả thread (spec — chưa nói), F2 format lộn xộn (spec), F3 bỏ sót deadline (spec — chưa nói deadline là bắt buộc), F4 lọt chữ ký (generalization — đã nói), F5 nhầm tên (generalization — đã nói "lấy từ header").
- Improve lần 1 (spec) — sửa prompt cho F1–F3 ngay, không cần đo trước: nói rõ "chỉ tin mới nhất", "3 bullet", "luôn nêu deadline nếu có".
- Measure (generalization) — evaluator code cho F4 (tìm cụm boilerplate), F5 (so tên trả về với header
From). Trên 2.000 email: F4 = 8,5%, F5 = 1,2%. - Improve lần 2 — F4 cao → đổi kiến trúc: cắt chữ ký bằng regex trước khi gọi LLM → đo lại còn 0,6%. F5 → lấy tên từ metadata, LLM không còn quyết định → 0%.
- Quay lại Analyze — đọc 120 trace mới sau khi sửa: xuất hiện mode mới F6 "email tiếng Anh bị tóm tắt bằng tiếng Anh" (spec chưa nói ngôn ngữ output). Vòng lặp tiếp tục.
Hay nhầm trong vòng lặp
- "Analyze là đếm lỗi." Không — Analyze chỉ nhận diện và gọi tên; đếm là việc của Measure. Đếm trước khi biết có những kiểu lỗi nào thì chỉ ra con số vô nghĩa kiểu "helpfulness 7,4/10".
- "Phải xây evaluator cho mọi lỗi." Lỗi spec hiển nhiên thì sửa luôn, rẻ hơn. Đầu tư evaluator cho lỗi còn tồn tại khi spec đã rõ.
- "Sửa xong là xong." Mỗi lần sửa có thể sinh lỗi mới (F6 ở trên) → quay lại Analyze.
- "Có evaluator rồi khỏi đọc trace." Evaluator chỉ đo những gì bạn đã biết để đo; lỗi mới chỉ lộ ra khi người đọc trace.
Xem lời giải
(a) Specification → hỏi bác sĩ quy ước viết tắt, ghi vào prompt. (b) Generalization → Measure trước: evaluator so danh sách thuốc trích được với trường thuốc có cấu trúc trong EHR, tách theo độ dài hồ sơ; nếu lỗi tập trung ở hồ sơ dài → chia nhỏ (chunk) hoặc trích danh sách thuốc bằng bước riêng/bằng code. Stakes cao (y tế) → cần tỉ lệ rất thấp và có thể thêm guardrail. (c) Specification → ghi "≤ 5 dòng". Lưu ý (a)(c) sửa ngay, không cần evaluator trước.Chọn một tình huống, rồi quyết định bước tiếp theo qua 3 lượt. Mỗi lựa chọn có hậu quả; lựa chọn đúng quy trình được nhiều điểm hơn.
Đặt tỉ lệ lỗi thật của hệ thống và số trace bạn đọc. Bấm "Rút mẫu" để mô phỏng 20 người khác nhau, mỗi người đọc n trace ngẫu nhiên — xem ước lượng của họ tản mát thế nào.
1.10Case study: bot CSKH "ShopMate" qua 3 vòng lặp
Bối cảnh. Shop bán mỹ phẩm qua website và Zalo, ~3.000 tin nhắn khách/ngày. Team 2 kỹ sư dựng bot trả lời tự động bằng một LLM + tài liệu chính sách (đổi trả, freeship, xuất xứ). Tuần đầu, chủ shop nói: "Khách phàn nàn bot trả lời linh tinh". Không ai định nghĩa "linh tinh" là gì.
Vòng 1 — Tuần 1
- Analyze. Lấy mẫu 150 hội thoại (phân tầng: 50 có khách phàn nàn, 100 ngẫu nhiên). Mỗi hội thoại một dòng ghi chú. Gom thành 6 failure mode: M1 hứa freeship cho đơn dưới ngưỡng; M2 trả lời tiếng Anh khi khách gõ tiếng Việt không dấu; M3 không chuyển người thật khi khách đòi "bóc phốt"; M4 bịa xuất xứ sản phẩm; M5 trả lời quá dài trên Zalo; M6 không hỏi mã đơn khi khách khiếu nại giao hàng.
- Phân vực. Kiểm prompt: không nhắc ngưỡng freeship (M1 → spec), không có quy tắc escalate (M3 → spec), không giới hạn độ dài (M5 → spec), không yêu cầu hỏi mã đơn (M6 → spec). Prompt đã ghi "trả lời bằng ngôn ngữ của khách" (M2 → generalization) và "chỉ dùng thông tin trong tài liệu" (M4 → generalization).
- Improve (spec). Viết lại prompt: ngưỡng freeship 300k, quy tắc escalate (đe doạ, đòi hoàn tiền > 1 triệu, nhắc tới dị ứng), ≤ 4 câu trên Zalo, luôn hỏi mã đơn khi khiếu nại giao hàng.
Vòng 2 — Tuần 2
- Measure. Evaluator: M2 = code (phát hiện ngôn ngữ câu trả lời vs câu hỏi, có xử lý tiếng Việt không dấu); M4 = LLM-judge "mọi thông tin xuất xứ có trong tài liệu?" (đã được đối chiếu với 60 nhãn người chấm). Chạy trên 2.000 hội thoại.
- Kết quả.
| Mode | Vực | Tỉ lệ | Ghi chú |
|---|---|---|---|
| M2 sai ngôn ngữ | Generalization | 6,1% (trên hội thoại không dấu: 19%) | Tập trung ở tin nhắn ngắn không dấu, ví dụ "ship ko", "con hang k" |
| M4 bịa xuất xứ | Generalization | 2,3% | Chủ yếu sản phẩm mới chưa có trong tài liệu |
| M1, M3, M5, M6 | Spec (đã sửa) | < 0,5% | Kiểm lại bằng check nhanh: sửa spec đã hiệu quả |
- Improve (generalization). M2: thêm bước khôi phục dấu tiếng Việt trước khi gọi LLM + 6 few-shot không dấu → đo lại 0,9%. M4: đây thực ra lộ ra một lỗi application — tài liệu không được cập nhật khi thêm sản phẩm; sửa pipeline đồng bộ tài liệu, và thêm guardrail: nếu câu trả lời nhắc xuất xứ mà judge không tìm thấy nguồn → thay bằng "để em kiểm tra và báo lại ạ" + chuyển người.
Vòng 3 — Tuần 4 (sau launch rộng)
- Monitoring cho thấy tỉ lệ escalate tăng từ 4% lên 11% trong đợt sale 9/9. Analyze lại 100 hội thoại đợt sale: mode mới M7 — khách hỏi mã giảm giá chồng nhau, bot không biết quy tắc cộng dồn (spec chưa có) → escalate. Sửa spec, thêm bảng quy tắc voucher.
- Model migration. Nhà cung cấp ra model rẻ hơn 50%. Chạy bộ eval đã tích luỹ (2.000 hội thoại + evaluator M1–M7) cho cả hai: model mới tốt ngang ở M1–M6 nhưng M2 tăng lên 3,4% → giữ model cũ cho hội thoại không dấu, dùng model mới cho phần còn lại (một dạng routing đơn giản).
Bài học rút ra
- "Bot trả lời linh tinh" → sau vòng 1 thành 6 failure mode có tên; 4/6 là spec và được sửa trong một buổi chiều.
- Chỉ 2 mode cần evaluator. Đầu tư đo có trọng tâm, không đo đại trà.
- Có lỗi tưởng là agent (M4) nhưng gốc lại ở application (tài liệu lỗi thời) — trace đầy đủ mới cho thấy.
- Bộ eval tích luỹ qua các vòng chính là thứ cho phép quyết định migration trong một ngày thay vì "đổi thử rồi xem khách có kêu không".
Xem lời giải
M7 chỉ xuất hiện khi phân bố input đổi (đợt sale) — pre-launch eval không có loại câu hỏi này nên không bắt được. Không có monitoring thì M7 lộ ra qua phàn nàn của khách hoặc doanh thu giảm, trễ nhiều ngày. Đúng như sách nói: pre-launch eval và production monitoring bắt những loại lỗi khác nhau; cần cả hai. Sau khi phát hiện, nên thêm hội thoại voucher vào bộ eval pre-launch để lần sau regression được bắt sớm.1.11Checklist chẩn đoán khi app LLM hỏng
Khi gặp app hỏng, câu hỏi đầu tiên: lỗi này thuộc vực nào? Sơ đồ quyết định dưới đây là cách tự vẽ lại checklist của sách:
- Comprehension? Chưa hiểu input trông ra sao hoặc hệ thống thực sự xử lý thế nào → đọc thêm trace trước khi đổi bất cứ thứ gì.
- Specification? Yêu cầu chưa đủ tường minh để LLM làm theo → sửa prompt hoặc logic agent.
- Generalization? Yêu cầu rõ mà model vẫn sai trên một số input → đo tần suất, rồi cân nhắc can thiệp mạnh hơn (thêm ví dụ, retrieval tốt hơn, fine-tune).
Vòng Analyze–Measure–Improve biến chẩn đoán này thành quy trình lặp lại được: phân tích trace để lộ failure mode, đo mức phổ biến, sửa, rồi đưa kết quả vào vòng phân tích tiếp theo. Chương sau bắt đầu bằng bước thực tế đầu tiên: dựng prototype và viết prompt ban đầu — phải có thứ gì đó chạy được, dù thô, mới phân tích được.
1.12Áp dụng cho dự án model-routing
Bối cảnh. Router nhận mỗi request LLM và chọn model phù hợp (ví dụ small / medium / large), cân bằng chất lượng – chi phí – latency. Router là một "agent" theo nghĩa rộng của sách: nó ra quyết định, và quyết định có thể sai. Điểm nguy hiểm: lỗi router thường im lặng — người dùng thấy câu trả lời hơi kém mà không biết vì bị hạ cấp model, còn dashboard chi phí thì trông rất đẹp. Theo ma trận stakes ở 1.8, đó là góc "lỗi đắt · khó bắt".
Ba Vực thẳm của một router
| Vực | Câu hỏi cho router | Dấu hiệu bạn đang rơi vào |
|---|---|---|
| Comprehension | Phân bố request thật ra sao — loại tác vụ, độ dài, ngôn ngữ, độ khó, tỉ lệ multi-turn? Router đang chọn gì cho từng nhóm? | Bạn chỉ biết "60% request đi small" mà không biết 60% đó là những request nào. |
| Specification | "Route đúng" là gì? Ngưỡng chất lượng tối thiểu theo loại tác vụ, ngân sách/request, SLO latency p95, thứ tự ưu tiên khi xung đột. | PM nói router "tiết kiệm quá tay", kỹ sư nói "vẫn ổn" — không ai sai vì chưa có định nghĩa. |
| Generalization | Spec đã rõ, classifier/luật vẫn chọn sai trên một số loại request (hiếm, đa ngôn ngữ, câu ngắn nhưng khó)? | Spec ghi "toán nhiều bước → large", nhưng bài toán viết bằng lời văn tiếng Việt vẫn bị gửi small. |
Quy trình từng bước
- Analyze trace routing thật. Lấy mẫu phân tầng 200–300 request (theo loại tác vụ, độ dài, ngôn ngữ, và cả theo model được chọn). Mỗi trace xem: request, model được chọn, output, latency, chi phí. Ghi chú tự do những lần route "đáng ngờ": câu hỏi code nhiều file đi small; chitchat đi large; request tiếng Việt dài bị escalate vô lý… Gom thành failure mode của router.
- Viết "routing contract" (spec). Không có spec thì không có "route sai". Ví dụ spec tự soạn:
quality_bar: # điểm judge tối thiểu (0-1) theo loại tác vụ chitchat: 0.7 extract: 0.85 code: 0.8 legal: 0.9 # stakes cao → ngưỡng cao budget: avg_cost_per_1k_req: 3.0 USD latency: p95_ms: 4000 tie_break: "quality_bar > latency > cost" oracle: "model rẻ nhất có điểm ≥ quality_bar; nếu không có → model tốt nhất" - Measure offline (counterfactual). Chạy mọi model ứng viên trên cùng tập eval (vài trăm–vài nghìn request đã ẩn danh), chấm bằng judge đã được căn chỉnh với nhãn người (Chương 5). Từ đó tính oracle và so với quyết định của router. Chạy mỗi cấu hình vài lần vì output LLM không deterministic.
- Tách lỗi theo vực rồi mới sửa. Spec lỗi (ngưỡng sai, loại tác vụ thiếu) → sửa contract/luật. Generalization (classifier nhầm dù luật đúng) → thêm dữ liệu huấn luyện router cho nhóm yếu, thêm feature (độ dài, có code block, ngôn ngữ), hiệu chỉnh ngưỡng, hoặc đổi kiến trúc sang cascade (thử small → check → escalate).
- Gắn eval theo cả 4 cách (bảng dưới) và lặp lại vòng A→M→I mỗi khi đổi model pool, đổi luật, hoặc phân bố traffic dịch chuyển.
Metric nên có
| Metric | Định nghĩa | Vì sao cần |
|---|---|---|
| Quality retention | chất lượng TB của router / chất lượng TB nếu luôn dùng model mạnh nhất | Câu hỏi "mất bao nhiêu chất lượng?" |
| Cost savings | 1 − chi phí router / chi phí always-large (tính theo token thật, không theo giá niêm yết × số request) | Câu hỏi "được bao nhiêu tiền?" |
| Under-route rate | % request router chọn model yếu hơn oracle | Lỗi chất lượng im lặng — nguy hiểm nhất |
| Over-route rate | % request router chọn model mạnh hơn oracle | Tiền lãng phí |
| Below-bar rate | % request có output dưới quality_bar | Gắn thẳng với spec; báo cáo theo từng loại tác vụ |
| Latency p95 | kể cả thời gian escalate/retry | Cascade tiết kiệm tiền nhưng có thể phá SLO |
# routing_eval.py — mỗi request đã chạy qua CẢ 3 model, có điểm judge (0-1)
PRICE = {"small": 0.2, "medium": 1.0, "large": 5.0} # $ / 1.000 request (minh hoạ)
QUALITY_BAR = 0.8
DATA = [ # (id, loại, điểm từng model, model router đã chọn)
("r1", "chitchat", {"small": 0.95, "medium": 0.96, "large": 0.97}, "small"),
("r2", "extract", {"small": 0.85, "medium": 0.90, "large": 0.92}, "medium"),
("r3", "code", {"small": 0.40, "medium": 0.82, "large": 0.93}, "medium"),
("r4", "math", {"small": 0.30, "medium": 0.55, "large": 0.90}, "medium"),
("r5", "summarize",{"small": 0.88, "medium": 0.90, "large": 0.91}, "large"),
("r6", "legal", {"small": 0.20, "medium": 0.60, "large": 0.85}, "large"),
("r7", "chitchat", {"small": 0.93, "medium": 0.94, "large": 0.95}, "small"),
("r8", "code", {"small": 0.35, "medium": 0.70, "large": 0.88}, "small"),
]
ORDER = ["small", "medium", "large"]
def oracle(scores):
ok = [m for m in ORDER if scores[m] >= QUALITY_BAR]
return ok[0] if ok else max(scores, key=scores.get)
n = len(DATA)
router_q = sum(s[c] for _, _, s, c in DATA) / n
large_q = sum(s["large"] for _, _, s, _ in DATA) / n
router_cost = sum(PRICE[c] for *_, c in DATA)
large_cost = PRICE["large"] * n
under = [r for r, _, s, c in DATA if ORDER.index(c) < ORDER.index(oracle(s))]
over = [r for r, _, s, c in DATA if ORDER.index(c) > ORDER.index(oracle(s))]
below = [r for r, _, s, c in DATA if s[c] < QUALITY_BAR]
print(f"Chất lượng TB router={router_q:.3f} always-large={large_q:.3f} (giữ lại {router_q / large_q:.0%})")
print(f"Chi phí router=${router_cost:.1f} always-large=${large_cost:.1f} (tiết kiệm {1 - router_cost / large_cost:.0%})")
print(f"Under-route: {under} Over-route: {over}")
print(f"Dưới ngưỡng: {below} = {len(below) / n:.0%}")
Chất lượng TB router=0.782 always-large=0.914 (giữ lại 86%)
Chi phí router=$13.6 always-large=$40.0 (tiết kiệm 66%)
Under-route: ['r4', 'r8'] Over-route: ['r2', 'r5']
Dưới ngưỡng: ['r4', 'r8'] = 25%
- Tiêu đề đẹp: tiết kiệm 66% chi phí, giữ 86% chất lượng. Nếu chỉ nhìn hai số này, dễ kết luận "router tốt".
- Nhìn theo failure mode: 2/8 request dưới ngưỡng, và cả hai là under-route ở loại toán/code — đúng nhóm người dùng khó tự phát hiện là mình bị hạ cấp.
- Over-route r2, r5 chỉ tốn tiền (r5 dùng large cho tóm tắt mà small đã đạt 0,88).
- Phân vực: nếu contract chưa định nghĩa "math" là một loại riêng → r4 là lỗi spec. Nếu đã định nghĩa mà classifier vẫn nhầm → generalization, cần đo trên nhiều request toán hơn (8 request thì CI rất rộng — xem LAB 3).
- Hành động: ưu tiên giảm under-route cho code/math (stakes cao hơn) trước khi tối ưu over-route.
Bốn cách gắn eval vào router
Dashboard tỉ lệ request theo model, theo loại tác vụ; alert khi mix đổi đột ngột; chấm mẫu hằng ngày below-bar rate theo model được chọn.
Cascade: small trả lời → check rẻ (JSON hợp lệ? code compile? judge nhanh) → fail thì escalate lên large. Nhớ tính latency cộng thêm.
Mỗi request oracle ≠ router là một nhãn huấn luyện cho classifier router; nhóm under-route thành tập test khó.
Model mới vào pool → chạy lại ma trận offline (mọi model × tập eval), tính lại oracle và ngưỡng; không "cắm vào rồi xem".
Cạm bẫy thường gặp
- Chỉ báo cáo trung bình. Trung bình che mất nhóm nhỏ bị under-route nặng; luôn tách theo loại tác vụ/ngôn ngữ/độ dài.
- Dùng benchmark công khai thay traffic thật. MMLU không có phân bố request của bạn — vực Comprehension chưa được lấp.
- Oracle không có spec. "Model rẻ nhất đủ tốt" phụ thuộc ngưỡng; đổi ngưỡng là đổi toàn bộ kết luận. Viết contract trước, đo sau.
- Judge thiên vị. Dùng chính model large làm judge có thể ưu ái output của chính nó; căn chỉnh judge với nhãn người trên một tập nhỏ.
- Rò rỉ tập eval. Request trong tập eval bị dùng để huấn luyện classifier router → số đẹp giả tạo. Tách tập theo thời gian.
- Chạy một lần. Output LLM dao động; so hai cấu hình router với chênh lệch 1–2 điểm mà không lặp lại/tính khoảng tin cậy là đọc nhiễu.
- Quên chi phí eval. Chạy mọi model × mọi request tốn tiền; bắt đầu bằng mẫu phân tầng vài trăm request, mở rộng ở nhóm đáng ngờ.
40 request hư cấu, mỗi cái có "độ khó thật". Router ước lượng độ khó (có nhiễu) và gửi sang large nếu ước lượng ≥ ngưỡng, còn lại small. Kéo ngưỡng và độ nhiễu để thấy trade-off chất lượng–chi phí và under-route (quality bar = 0,8; giá large = 10× small).
Xem lời giải
(a) 1.200 lượt / 3 model = 400 request. Không lấy mẫu theo tỉ lệ traffic (sẽ chỉ có 40 request pháp lý) mà phân tầng có trọng số theo rủi ro, ví dụ: chitchat 80, JSON 100, code 110, pháp lý 110. Khi báo cáo tổng thể thì trọng số lại theo tỉ lệ traffic thật. (b) Pháp lý và code: stakes cao và người dùng khó tự phát hiện bị hạ cấp; JSON có thể check phần lớn bằng code (schema). Chitchat dùng judge nhẹ là đủ. Có thể tiết kiệm thêm: với chitchat, nếu small đã đạt ngưỡng thì không cần chạy medium (oracle đã xác định).1.13Nối với các chương sau
| Khái niệm ở Chương 1 | Được đào sâu ở | Bạn sẽ học thêm |
|---|---|---|
| Agent theo nghĩa rộng, prototype đầu tiên | Chương 2 | Các dạng agent, cách viết prompt ban đầu, eval cơ bản |
| Analyze, đọc trace, failure mode | Chương 3–4 | Error analysis có hệ thống, làm cùng chuyên gia domain |
| Measure, evaluator, LLM-as-judge | Chương 5 | Xây và căn chỉnh evaluator tự động |
| Generalization trong hội thoại, RAG, tool | Chương 6–8 | Eval multi-turn, retrieval, gọi tool và agent phức tạp |
| Pre-launch vs production monitoring | Chương 9–11 | CI/CD, giao diện review, phân tích dữ liệu trace |
| Improve theo mức công sức | Chương 12 | Từ sửa prompt tới thay đổi ở mức model |