07 · Eval và observability cho agent — đo cái không tất định
Với code thường, test pass là pass. Với LLM, cùng input có thể lúc đúng lúc sai, output là văn bản tự do, và "đúng" thường là chuyện mức độ. Eval (bộ đánh giá tự động) là unit test của tính năng AI — nhưng là unit test thống kê. Observability (quan sát) là cách bạn biết agent đã làm gì trong production. Hai thứ này là điều phân biệt team "demo AI" với team "vận hành AI", và là chủ đề interviewer mức lead gần như chắc chắn hỏi: "làm sao anh biết đổi prompt này là tốt hơn?"
Đã verify gì, chưa verify gì. Lab chạy thật: mô phỏng thống kê (Python, numpy) cho thấy eval nhỏ kết luận sai bao nhiêu phần trăm, lợi ích của so sánh paired, và việc judge không hoàn hảo làm co chênh lệch; một agent loop dùng SDK Pythonanthropic1.9.0 thật (gọi mock server) được trace theo OpenTelemetry GenAI semantic conventions vớiopentelemetry-sdk1.45 / semconv 0.66b0. Chưa verify: hành vi LLM-as-judge thật (thiên vị vị trí, độ dài...) — dẫn nguồn nghiên cứu; OpenTelemetry cho PHP. Không có Context7.
Câu trả lời 60 giây
"Tôi coi eval là unit test thống kê: một bộ câu hỏi thật, một cách chấm, và một con số kèm khoảng tin cậy. Ưu tiên chấm bằng code và chấm kết quả — ví dụ agent hoàn tiền thì kiểm bảng refunds có đúng một dòng — hơn là chấm câu chữ. LLM-as-judge chỉ dùng khi phải chấm văn bản, và phải hiệu chỉnh với người chấm. Cỡ mẫu quan trọng: tôi mô phỏng hai prompt đúng thật 82% và 86% — với 50 câu, một lần chạy kết luận sai hướng 34% số lần; cần khoảng 500 câu mới xuống dưới 5%. Với agent, tôi đo cả pass^k — chạy k lần đều đúng — vì khách hàng gặp độ ổn định chứ không gặp điểm trung bình. Observability thì mỗi lượt agent là một trace dạng cây: invoke_agent → chat → execute_tool, gắn token, chi phí, stop_reason; transcript lỗi trong production quay về làm câu hỏi mới cho bộ eval."
Thuật ngữ
| Thuật ngữ | Nghĩa ngắn |
|---|---|
| eval / golden set | Bộ đầu vào + đáp án hoặc tiêu chí dùng để chấm tự động. |
| grader | Cách chấm: code (so khớp, kiểm DB), model (LLM-as-judge), người. |
| pass@k / pass^k | Ít nhất 1 trong k lần đúng / cả k lần đều đúng (đo độ ổn định — τ-bench, Sierra 2024). |
| offline / online eval | Chạy trên bộ dữ liệu cố định trước khi deploy / đo trên traffic thật sau deploy. |
| trace / span | Trace: toàn bộ một lượt xử lý; span: một bước trong đó (một lần gọi model, một tool call), lồng nhau thành cây. |
| semantic conventions | Quy ước tên thuộc tính chuẩn của OpenTelemetry (vd gen_ai.usage.input_tokens). |
| faithfulness / groundedness | Câu trả lời có được tài liệu nguồn hỗ trợ hay không (RAG). |
1. Bài toán gốc và bối cảnh ra đời
Test truyền thống giả định hàm tất định và output so khớp được. LLM phá cả hai. Cách làm tự nhiên đầu tiên là "vibe check": thử vài câu, thấy ổn, deploy. Nó không scale: đổi một câu trong prompt sửa được 3 ca nhưng làm hỏng 5 ca khác mà không ai thấy. Benchmark công khai (MMLU, SWE-bench) đo năng lực chung, không đo nhiệm vụ của bạn. Từ 2023–2024, cộng đồng làm sản phẩm (Hamel Husain, Eugene Yan và nhiều người khác) hình thành cách làm eval-driven development: đọc lỗi thật, phân loại, biến thành bộ eval, lặp.
Về quan sát: APM truyền thống (New Relic, Datadog) thấy một HTTP call 12 giây tới API, không thấy prompt, token, tool call, stop_reason. Công cụ chuyên cho LLM (Langfuse, LangSmith, Arize Phoenix, Braintrust…) ra đời, rồi OpenTelemetry đưa ra GenAI semantic conventions để chuẩn hoá tên thuộc tính.
Eval tối ưu cho: ra quyết định (đổi prompt, đổi model, đổi chunking) bằng số liệu. Không thay được: đọc transcript thật bằng mắt — eval chỉ đo những kiểu lỗi bạn đã biết để đo.
2. Mô hình tư duy cốt lõi
Eval = thí nghiệm thống kê có kiểm soát (bộ dữ liệu, cách chấm, con số kèm khoảng tin cậy). Observability = event log có cấu trúc của mỗi lượt agent (trace dạng cây). Hai thứ nối thành một vòng: lỗi thấy trong trace → thành ca eval → sửa → eval xác nhận → deploy → quan sát.
flowchart LR P["Production: trace mọi lượt"] --> F["Lọc ca đáng xem: thumbs down, escalate, lỗi tool, chi phí cao"] F --> A["Đọc transcript, phân loại kiểu lỗi"] A --> D["Thêm vào bộ eval (có đáp án/tiêu chí)"] D --> X["Sửa prompt / tool / retrieval"] X --> E["Chạy eval: so với baseline, có CI"] E -->|"không regression"| DEP["Deploy (canary)"] E -->|"regression"| X DEP --> P
3. Internals và quyết định thiết kế
3.1 Chấm cái gì
| Loại | Chấm | Ví dụ shop | Ưu | Nhược |
|---|---|---|---|---|
| Outcome (kết quả cuối) | Trạng thái thế giới sau khi agent chạy | Bảng refunds có đúng 1 dòng 350 000đ cho đơn 1001; không có dòng nào cho đơn của người khác | Khách quan, chấm bằng code, không phụ thuộc cách diễn đạt | Cần môi trường sandbox để agent chạy thật |
| Trajectory (đường đi) | Chuỗi tool call | Có gọi get_order trước refund_order; không quá 5 vòng | Bắt được lỗi quy trình, chi phí | Nhiều đường đúng — chấm quá chặt sẽ phạt cách làm hợp lệ |
| Response (câu chữ) | Văn bản trả lời | Có xưng "em", không hứa quá chính sách, có trích dẫn | Bắt lỗi giọng điệu, bịa | Thường cần LLM-as-judge |
| Component | Một bộ phận | Recall@10 của retrieval (bài 04), độ chính xác của router | Khoanh vùng lỗi nhanh | Tốt từng phần chưa chắc tốt tổng thể |
3.2 Chấm bằng gì
| Grader | Dùng khi | Rủi ro |
|---|---|---|
| Code: so khớp, regex, JSON schema, truy vấn DB, chạy test | Bất cứ khi nào có thể — rẻ, nhanh, tất định | Chấm quá cứng (đúng ý nhưng khác chữ) |
| LLM-as-judge: rubric (thang điểm có tiêu chí) hoặc so sánh cặp | Tiêu chí mềm: giọng văn, đầy đủ, bám nguồn | Thiên vị vị trí (thích đáp án đứng trước), thiên vị độ dài, tự ưu ái output cùng họ model (Zheng et al., 2023, "Judging LLM-as-a-Judge with MT-Bench"); bản thân judge không tất định; đổi model judge là đổi thước đo |
| Người | Hiệu chỉnh judge, ca nhạy cảm, định nghĩa "đúng" | Chậm, đắt, người chấm không đồng ý với nhau |
Quy tắc cho judge: một tiêu chí mỗi lần chấm (không "chấm 1–10 tổng thể"), yêu cầu lý do trước điểm, đảo vị trí khi so sánh cặp, dùng structured outputs cho điểm, và đo mức đồng ý với người trên 50–100 mẫu trước khi tin.
3.3 Thống kê — lab
Hai prompt: A đúng thật 82%, B đúng thật 86%. Chạy eval một lần với n câu, bao nhiêu phần trăm số lần ta kết luận sai (A ≥ B)?
1) Chạy eval n câu MỘT lần: bao nhiêu % trường hợp kết luận SAI (A >= B)?
n câu A>=B (sai) CI95 ± (điểm %)
30 40.6% 13.7
50 34.0% 10.6
100 24.9% 7.5
200 15.2% 5.3
500 4.6% 3.4
1000 0.8% 2.4
2) Paired: cùng bộ câu hỏi cho A và B (độ khó từng câu tương quan) — nhiễu giảm bao nhiêu?
n=200, không paired (2 bộ câu khác nhau): sai 18.6%
n=200, paired (cùng bộ câu): sai 14.9%
3) LLM-as-judge đồng ý với người 90%: chênh lệch đo được bị co lại
chênh lệch thật 4.0 điểm; judge đo được trung bình 3.2 điểm (điểm tuyệt đối A: thật 82% -> judge 75.6%)
- Với 50 câu — cỡ bộ eval phổ biến của team mới — khoảng tin cậy ±10,6 điểm; một lần chạy chọn sai prompt 34% số lần. Muốn phân biệt chênh 4 điểm đáng tin, cần cỡ 500 câu, hoặc chấp nhận chỉ phát hiện được chênh lệch lớn.
- So sánh paired (cùng câu hỏi cho cả hai phiên bản) giảm sai lầm vì loại được nhiễu do độ khó câu hỏi. Luôn chạy A và B trên cùng bộ, và nhìn vào các câu đổi kết quả (A đúng B sai và ngược lại) — đó là thông tin quý nhất.
- Judge sai 10% làm điểm tuyệt đối lệch (82% → 75,6%) và co chênh lệch (4 → 3,2 điểm) — vẫn đúng hướng nhưng khó phát hiện hơn. Điểm tuyệt đối của judge không so được giữa hai judge khác nhau.
pass@k vs pass^k: agent đúng 90% mỗi lần có pass^8 = 0,9^8 ≈ 0,43 — chạy 8 lần thì chỉ 43% khả năng cả 8 đều đúng. τ-bench (Sierra, 6/2024) đưa pass^k vào đo agent CSKH chính vì lý do này: khách hàng gặp từng lần chạy, không gặp trung bình.
3.4 Dựng bộ eval từ đâu
- Đọc 50–100 transcript thật, ghi chú từng lỗi bằng lời (open coding), rồi gom thành nhóm (thiếu tool, sai chính sách, bịa mã đơn, quá dài…). Đây là bước bị bỏ qua nhiều nhất và đáng giá nhất.
- Mỗi nhóm lỗi → vài ca eval có đáp án/tiêu chí, ưu tiên chấm bằng code.
- Thêm ca âm: câu hỏi phải từ chối, câu hỏi phải trả lời "không có thông tin", câu hỏi chéo tenant.
- Dữ liệu tổng hợp (LLM sinh) chỉ để mở rộng biến thể quanh ca thật, không thay ca thật.
- Version bộ eval như code; giữ một phần holdout không dùng khi tinh chỉnh prompt để tránh overfit.
3.5 Observability: trace theo OpenTelemetry GenAI — lab
Agent loop dùng SDK anthropic Python thật (gọi mock server), mỗi bước là một span với thuộc tính theo semconv GenAI:
invoke_agent cskh [256 ms] {"operation.name": "invoke_agent", "agent.name": "cskh", "conversation.id": "conv_demo_1"}
chat claude-opus-5-5 [89 ms] {"operation.name": "chat", "provider.name": "anthropic", "request.model": "claude-opus-5-5", "response.id": "msg_af946cb4b78041078b78", "response.finish_reasons": ["tool_use"], "usage.input_tokens": 124, "usage.output_tokens": 41}
execute_tool get_order [31 ms] {"operation.name": "execute_tool", "tool.name": "get_order", "tool.call.id": "toolu_6975d508f6254b3498cc"}
chat claude-opus-5-5 [8 ms] {"operation.name": "chat", ..., "response.finish_reasons": ["tool_use"], "usage.input_tokens": 219, "usage.output_tokens": 32}
execute_tool refund_order [30 ms] {"operation.name": "execute_tool", "tool.name": "refund_order", "tool.call.id": "toolu_65d950f11a5a410f94df"}
chat claude-opus-5-5 [96 ms] {"operation.name": "chat", ..., "response.finish_reasons": ["end_turn"], "usage.input_tokens": 295, "usage.output_tokens": 16}
(Tiền tố gen_ai. được lược bớt khi in. Số token do mock ước lượng.) Mỗi span nên có: model, token (tách cache read/write — semconv có
gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_creation.input_tokens), finish_reasons, tool.call.id để nối với
tool_result, id người dùng/tenant (băm), phiên bản prompt. Nội dung prompt/response (gen_ai.input.messages, gen_ai.output.messages)
chứa PII → bật có chủ đích, lấy mẫu, có hạn lưu trữ.
Semconv GenAI vẫn đang thay đổi: gói semconv 0.66b0 dùng trong lab chứa cảgen_ai.system(tên cũ) lẫngen_ai.provider.name(tên mới), cảgen_ai.usage.prompt_tokenslẫngen_ai.usage.input_tokens. Ghim phiên bản semconv và có lớp map ở một chỗ, đừng rải tên thuộc tính khắp code.
Trong Laravel, trace phải đi xuyên qua queue: controller tạo trace, job agent là con của trace đó (truyền trace context trong payload job) — nếu không, mỗi job là một trace mồ côi.
4. Đảm bảo và giới hạn
- Eval là ước lượng: chỉ nói được "B tốt hơn A với độ tin cậy X" trong phân phối của bộ eval. Traffic thật đổi (mùa sale, sản phẩm mới) thì kết luận có thể hết đúng.
- Goodhart: tối ưu mãi theo một bộ eval → overfit bộ đó. Holdout và làm mới dữ liệu từ production.
- Judge LLM không tất định và có thiên vị hệ thống; điểm của judge là thước đo tương đối, không tuyệt đối.
- Trace chỉ thấy những gì bạn ghi; thinking của model mới mặc định ẩn nội dung (chỉ thấy số token).
- Chi phí: mỗi lần chạy eval 500 câu × agent nhiều vòng là tiền thật — cần ngân sách, cache, và Batch API (rẻ 50%, không cần kết quả tức thì).
5. Failure modes
| Lỗi | Hệ quả | Phát hiện | Xử lý |
|---|---|---|---|
| Eval quá nhỏ (lab) | Chọn sai phiên bản 1/3 số lần | Tính CI trước khi kết luận | Tăng cỡ mẫu, paired, chỉ ra quyết định khi chênh vượt CI |
| Chạy một lần với hệ không tất định | "Regression" giả, "cải thiện" giả | Chạy lại ra kết quả khác | Chạy k lần, báo trung bình + pass^k |
| Judge đổi model | Điểm nhảy dù hệ không đổi | Điểm baseline đổi | Ghim model judge; khi đổi, chấm lại baseline |
| Chấm câu chữ thay vì kết quả | Agent nói "đã hoàn tiền" nhưng không gọi tool — vẫn pass | So với trạng thái DB | Outcome grader |
| Eval phạt "không biết" | Tối ưu hoá theo eval làm model bịa nhiều hơn (Kalai et al., 2025) | Tỉ lệ bịa tăng khi điểm tăng | Chấm "không đủ thông tin" là đúng khi đúng là không có |
| Trace mồ côi qua queue | Không nối được request với job agent | Trace thiếu con | Truyền trace context trong payload job |
| Log PII vô thời hạn | Rủi ro pháp lý (Luật Bảo vệ dữ liệu cá nhân 91/2025/QH15, hiệu lực 1/1/2026, thay Nghị định 13/2023; GDPR nếu có khách EU) | Audit log store | Che PII, lấy mẫu, hạn lưu trữ, phân quyền xem |
6. Vận hành trong production
Trong CI
- Bộ eval nhanh (50–100 ca, chấm code) chạy mỗi PR đổi prompt/tool/model; bộ đầy đủ chạy hằng đêm hoặc trước release.
- Cổng chặn: không cho merge khi regression vượt ngưỡng và vượt khoảng tin cậy; in ra danh sách ca đổi kết quả để reviewer đọc.
- Lưu kết quả mỗi lần chạy (phiên bản prompt, model, điểm, chi phí) để thấy xu hướng.
Trên production
- Metric kết quả: tỉ lệ giải quyết không cần người, tỉ lệ chuyển người, thumbs down, khiếu nại lặp lại trong 24h.
- Metric vận hành: số vòng, token, chi phí, latency theo trace; tỉ lệ
max_tokens/refusal; lỗi tool. - Review hằng tuần: lấy mẫu 20–50 trace (ưu tiên ca xấu), đọc và phân loại — nguồn ca eval mới.
- Canary khi đổi prompt/model: 5% traffic, so metric với nhóm chứng.
Gánh nặng cho team
Cần một người (thường là PM hoặc chuyên gia nghiệp vụ, không nhất thiết là kỹ sư) sở hữu định nghĩa "đúng" và gán nhãn. Kỹ sư sở hữu harness chạy eval và pipeline trace. Không có chủ sở hữu nghiệp vụ, bộ eval sẽ đo sai thứ.
7. So sánh và ma trận quyết định
| Cách đánh giá | Chi phí | Tốc độ | Độ tin cậy | Dùng khi |
|---|---|---|---|---|
| Vibe check | Rất thấp | Nhanh | Rất thấp | Chỉ để thăm dò ý tưởng |
| Benchmark công khai | Thấp | Nhanh | Thấp cho nhiệm vụ của bạn | Chọn danh sách model ứng viên |
| Eval offline chấm bằng code | Thấp–TB | Nhanh | Cao (trong phạm vi đo) | Mặc định, chạy trong CI |
| LLM-as-judge | TB | Nhanh | TB, cần hiệu chỉnh | Tiêu chí mềm, số lượng lớn |
| Người chấm | Cao | Chậm | Cao (nếu có hướng dẫn chấm) | Hiệu chỉnh, ca rủi ro cao |
| Online A/B | TB | Chậm (cần traffic) | Cao nhất cho metric kinh doanh | Quyết định cuối cho thay đổi lớn |
| Công cụ observability | Ưu | Nhược |
|---|---|---|
| Log + APM có sẵn (ELK, Datadog) | Không thêm hệ thống | Không có UI xem transcript, cây agent |
| Nền tảng LLM mã nguồn mở tự host (Langfuse, Phoenix) | Dữ liệu ở lại công ty, UI transcript, eval tích hợp | Thêm một hệ thống vận hành |
| SaaS (LangSmith, Braintrust…) | Nhanh, nhiều tính năng eval | Dữ liệu (có PII) ra ngoài; lock-in |
| OTel GenAI → backend bất kỳ | Chuẩn mở, đổi backend được | Semconv chưa ổn định; phải tự làm UI transcript |
Mặc định hợp lý cho team Laravel vừa và nhỏ: ghi trace theo semconv GenAI (qua lớp gateway ở bài 01), đẩy về một nền tảng tự host; eval chấm bằng code trong CI; judge chỉ cho vài tiêu chí mềm; review transcript hằng tuần.
8. Góc nhìn tech lead: tác động tổ chức
- Bộ eval là tài sản lớn nhất và là exit strategy: nó cho phép đổi model, đổi nhà cung cấp, đổi framework mà biết chắc cái gì hỏng. Prompt thì viết lại được; bộ eval tích luỹ từ lỗi thật thì không mua được.
- Quy trình: "không đổi prompt/model/tool mà không có kết quả eval trong PR" — ghi vào CONTRIBUTING, như "không merge mà không có test".
- Vai trò: chuyên gia nghiệp vụ định nghĩa đúng/sai; kỹ sư làm harness; ai đó chịu trách nhiệm review trace hằng tuần (xoay vòng).
- Ngân sách: eval tốn tiền token; đặt ngân sách tháng, dùng Batch API và cache, giới hạn bộ đầy đủ.
- Riêng tư: transcript là dữ liệu cá nhân; chính sách lưu trữ, che PII, ai được xem — quyết định trước khi bật log nội dung.
9. Anti-patterns và bài học thực tế
- "Tôi thử 5 câu thấy tốt hơn" — lab: 50 câu còn sai 34%.
- Chỉ nhìn điểm trung bình — mất thông tin các ca đổi kết quả; nhìn ma trận A đúng/B sai.
- Judge chấm "1–10 chất lượng tổng thể" không rubric, không hiệu chỉnh.
- Bộ eval toàn dữ liệu LLM tự sinh — đo khả năng của model trên câu hỏi do model nghĩ ra, không phải câu hỏi khách hỏi.
- Không có ca âm (phải từ chối, phải nói không biết, chéo tenant).
- Eval xong để đó — không chạy trong CI thì sẽ mục.
- Log toàn bộ prompt/response mãi mãi "để debug".
Điều người làm lâu đều biết: 80% giá trị của eval đến từ bước đọc transcript và phân loại lỗi, không phải từ con số cuối. Con số chỉ nói "tốt hơn hay tệ hơn"; đọc transcript nói "vì sao".
10. Câu hỏi phụ interviewer hay hỏi tiếp
- Làm sao biết đổi prompt là tốt hơn?
- Chạy cả hai phiên bản trên cùng bộ eval (paired), đủ lớn, nhiều lần nếu không tất định; so chênh lệch với khoảng tin cậy; đọc các ca đổi kết quả; canary trên production.
- LLM-as-judge có đáng tin không?
- Đủ tin cho tiêu chí mềm nếu có rubric cụ thể, một tiêu chí mỗi lần, đảo vị trí khi so cặp, và đã đo mức đồng ý với người. Không dùng cho thứ chấm được bằng code.
- Eval agent khác eval chatbot thế nào?
- Chấm kết quả (trạng thái sau khi chạy) và đường đi (tool, số vòng, chi phí), cần môi trường sandbox; đo pass^k vì agent nhiều bước dao động mạnh.
- Cần bao nhiêu ca eval?
- Tuỳ chênh lệch muốn phát hiện: phân biệt 82% vs 86% đáng tin cần ~500 ca (lab); bắt regression lớn thì 50–100 ca đủ. Bắt đầu nhỏ nhưng tính CI.
- Trace nên ghi gì?
- Cây span agent → chat → tool; model, token (tách cache), stop_reason, tool call id, latency, chi phí, phiên bản prompt, tenant (băm). Nội dung prompt có kiểm soát PII.
Phụ lục: lab đã chạy
Môi trường: Python 3.11.15, numpy 2.4.6, anthropic 1.9.0, opentelemetry-sdk 1.45.0, opentelemetry-semantic-conventions 0.66b0.
Output gốc: labs/ai/out/07-*.txt.
pip install numpy anthropic opentelemetry-sdk
python3 labs/ai/py/eval_stats.py # thống kê eval
labs/ai/mockctl.sh start cskh
python3 labs/ai/py/otel_agent_trace.py # trace agent loop theo semconv GenAI
labs/ai/mockctl.sh stop
Chưa verify: thiên vị của LLM-as-judge với model thật; OpenTelemetry PHP. Nguồn: Sierra, "τ-bench" (arXiv 2406.12045, 2024); Zheng et al., "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena" (2023); Kalai et al., "Why Language Models Hallucinate" (2025); OpenTelemetry GenAI semantic conventions (gói semconv 0.66b0).
Bài trước: 06 · MCP chuyên sâu · Bài tiếp: 08 · Bảo mật agent.