Insight Hub
Code-Based Eval: Chỉ Viết Assertion Cho Thứ Máy Đo Được

Code-Based Eval: Chỉ Viết Assertion Cho Thứ Máy Đo Được

Code-based eval là lớp rẻ nhất và nhanh nhất trong bộ eval, nhưng chỉ đúng khi lỗi thật sự mechanically checkable. Bài này chỉ cách chọn đúng loại assertion cho từng category lỗi từ error analysis, và khi nào cần dừng lại, nhường phần còn lại cho LLM-as-judge.

Thuộc chuỗi bài: LLM Evals: Đo lường chất lượng AI trước khi nó âm thầm phá sản phẩm

Sau khi error analysis chỉ ra taxonomy lỗi, câu hỏi tiếp theo là viết evaluator cho từng category thế nào. Với phần lớn category, câu trả lời rẻ nhất là một đoạn code, không phải một lệnh gọi model. Nhưng "rẻ nhất" không có nghĩa là "dùng được cho mọi category" - assertion chỉ hoạt động khi cái cần đo thật sự mechanically checkable, và biết ranh giới đó ở đâu quan trọng hơn biết cách viết assertion.

Code-based eval là lớp rẻ nhất, nhanh nhất, và xác định nhất trong một bộ eval. Nó cũng dễ bị dùng sai theo hai hướng đối lập: hoặc bị bỏ qua vì trông "quá đơn giản" so với LLM-as-judge, hoặc bị ép dùng cho những thứ nó không đo được, dẫn tới assertion giòn và false regression liên tục.

Từ Category Lỗi Tới Assertion Chạy Được

Category Lỗi
Assert Invariant
Chạy Nhanh, Xác Định
Bắt Regression
Escalate Nếu Cảm Tính

Vì Sao Code-Based Eval Là Lớp Rẻ Nhất, Không Phải Lớp Duy Nhất

Một assertion code chạy trong mili-giây, không tốn phí gọi model, và cho kết quả xác định tuyệt đối - cùng một input luôn ra cùng một kết quả pass/fail. Đó là lý do nó luôn nên là lớp đầu tiên trong bất kỳ pipeline eval nào: rẻ tới mức có thể chạy trên mọi trace, mọi lần deploy, không cần cân nhắc ngân sách. Nhưng cái giá của sự xác định đó là phạm vi hẹp - assertion chỉ trả lời được câu hỏi có sẵn đáp án đúng/sai rõ ràng theo quy tắc, không trả lời được câu hỏi cần phán đoán ngữ cảnh. Nhầm lẫn giữa "rẻ nhất" và "đủ dùng cho mọi thứ" là cách nhanh nhất khiến một bộ eval vừa bỏ sót lỗi thật vừa báo động giả liên tục.

Từ Taxonomy Lỗi Tới Assertion Cụ Thể

Mỗi category trong taxonomy lỗi từ error analysis cần được hỏi một câu trước khi viết bất kỳ dòng code nào: category này có một quy tắc cơ học để phân biệt đúng/sai hay không? Một category như "schema bị lệch ở input biên" gần như luôn mechanically checkable - có một cấu trúc dữ liệu đúng, mọi thứ khác là sai. Một category như "lệch tông giọng" thì không - không có quy tắc code nào định nghĩa được "nghe lạnh lùng" theo nghĩa nhị phân chắc chắn. Bước này quyết định luôn việc category đó thuộc về lớp code-based hay cần escalate sang lớp khác, và làm nó trước khi viết assertion tránh được việc cố nhét một phán đoán chủ quan vào một cấu trúc if/else.

Assertion Chặt Và Assertion Cứng Nhắc, Ranh Giới Nằm Ở Đâu

Ranh giới nằm ở việc assertion đang so khớp với cái gì: một invariant thật sự (tổng tiền đúng, field bắt buộc có mặt, trạng thái cuối khớp với yêu cầu) hay một hình thức bề mặt (đúng từng chữ, đúng thứ tự câu, đúng cách diễn đạt). Assert invariant giữ được độ chặt cần thiết mà vẫn chấp nhận nhiều cách diễn đạt đúng khác nhau - đây chính là điều một unit test tốt cho code truyền thống vẫn làm, chỉ khác là đầu ra của LLM có nhiều biến thể hợp lệ hơn nhiều so với đầu ra của một hàm thuần tuý. Assert bề mặt thì chặt sai chỗ: nó bắt được đúng những lần output thay đổi cách viết, và bỏ lọt đúng những lần output sai về bản chất nhưng tình cờ khớp mẫu câu cũ.

Kiểm Trạng Thái Cuối Cùng, Không Chỉ Lời Agent Nói

Với một agent gọi tool và để lại trạng thái thật trong hệ thống, assertion mạnh nhất không đọc transcript - nó đọc lại chính trạng thái đó. Một agent có thể tường thuật một hành động hoàn toàn chính xác về mặt ngôn ngữ trong khi hành động thật sự đã thất bại hoặc ghi sai dữ liệu, và một assertion chỉ parse văn bản trả lời sẽ không bao giờ phát hiện ra khoảng cách đó. Query lại database, log gọi tool, hay bất kỳ nguồn sự thật nào độc lập với lời agent tự thuật là cách duy nhất để assertion đo đúng cái người dùng thật sự nhận được, không phải cái agent nói nó đã làm.

Ba Assertion, Ba Kiểu Thất Bại Khác Nhau

Cùng một ý tưởng — "viết check để bắt lỗi" — nhưng kết quả rất khác nhau tuỳ vào việc assertion đang so khớp với cái gì.

Chọn một assertion để xem
Case A: Output Booking Có Cấu Trúc
User

Đặt chuyến 9h sáng đi Austin ngày 14.

Agent (raw output)

{ "status": "confirmed", "flight": "AA-2214" }

assert requiredFields(output, ["status", "flight", "confirmationId"])

Kết Quả AssertionKHÔNG ĐẠT

Thiếu field bắt buộc confirmationId. Bất kỳ hệ thống nào đọc output này sẽ hỏng ngay lập tức — không cần phán đoán gì cả, schema đúng hoặc không đúng, chỉ có vậy.

Loại assertion:Schema / Field Bắt BuộcAn toàn mặc định
Ba Kiểu Assertion Sẽ Còn Gặp Lại Nhiều Lần

Schema check và state check luôn an toàn vì chúng xác minh một sự thật. Exact-match trở nên rủi ro ngay khi có nhiều hơn một cách diễn đạt đúng cho cùng một câu trả lời.

Schema / Field Bắt BuộcSo Khớp Chuỗi Chính XácCheck Trạng Thái Hệ Thống Thật

Ba Dạng Assertion Hay Gặp Nhất Trong Thực Tế

Phần lớn assertion trong một bộ eval rơi vào một trong ba dạng: kiểm tra schema/field bắt buộc, kiểm tra trạng thái hệ thống thật, và so khớp chuỗi chính xác. Hai dạng đầu gần như luôn an toàn vì chúng xác minh một sự thật khách quan - cấu trúc đúng hoặc sai, dòng dữ liệu tồn tại đúng hoặc không. Dạng thứ ba là dạng duy nhất mang rủi ro cố hữu: nó ngầm giả định chỉ có một cách diễn đạt đúng, một giả định gần như luôn sai với ngôn ngữ tự nhiên. Nhận ra một assertion đang thuộc dạng nào ngay khi viết nó giúp tránh trước phần lớn false regression sẽ xuất hiện vài tuần sau.

Biến Một Trace Đã Đọc Thành Một Test Case Cố Định

Mỗi trace fail được tìm ra qua error analysis là một cơ hội miễn phí để có một regression test case cụ thể, không phải giả định: lấy chính input đó, chính output sai đó, viết assertion diễn đạt lại đúng lý do nó sai, rồi đưa vào bộ test chạy ở mọi lần đổi prompt hay model. Cách này rẻ hơn nhiều so với việc cố tưởng tượng trước mọi test case có thể xảy ra, vì mỗi test case đều bắt nguồn từ một lỗi đã thật sự xảy ra chứ không phải một lỗi được đoán là có thể xảy ra. Theo thời gian, bộ test case này chính là lớp phòng thủ rẻ nhất chống lại việc sửa một lỗi lại vô tình làm sống lại một lỗi khác đã từng fix.

Bảng phân rã kinh tế đơn vị (Unit Economics Ledger)
Code-Based Eval Economics

Chi Phí Chấm 10.000 Trace Khi Đẩy Càng Nhiều Xuống Code-Based Check

Kịch bản mẫu: 10.000 trace/ngày, phần lớn được lọc bởi check rẻ trước khi phần còn lại mới cần LLM-as-judge.

Schema / Format Check10.000 trace (100%)
$0,00001 / trace

Chỉ tốn compute, không gọi model nào

Chi phí:= $0,10
Business-Rule / State Check10.000 trace (100%)
$0,00005 / trace

Query trạng thái thật (DB, tool call log)

Chi phí:= $0,50
Escalate Sang LLM-as-Judge1.500 trace (~15%)
$0,02 / trace

Chỉ phần không thể chấm bằng assertion cứng

Chi phí:= $30,00
Chi phí chấm 10.000 trace mỗi ngày (pipeline lai)
$0,10 + $0,50 + $30,00 =
$30,60/ 10.000 trace / ngày
~6,5 lần rẻ hơn chấm toàn bộ bằng judge
So với việc chạy LLM-as-judge trên toàn bộ 10.000 trace mỗi ngày ($0,02 × 10.000 = $200,00) thay vì lọc trước bằng code-based check.

Chi Phí Một Lượt Chạy, Vì Sao Nên Đẩy Càng Nhiều Xuống Lớp Này Càng Tốt

Chênh lệch chi phí giữa một assertion code và một lượt gọi LLM-as-judge không phải vài phần trăm - nó lệch nhau vài bậc độ lớn, vì một bên chỉ tốn compute còn bên kia tốn một lần inference đầy đủ. Hệ quả thực tế: mỗi category lỗi được chuyển thành công từ "cần judge" sang "assertion đo được" không chỉ tiết kiệm tiền, nó còn giải phóng ngân sách judge để dành cho đúng những trường hợp thật sự cần phán đoán chủ quan. Một pipeline eval khoẻ mạnh luôn có xu hướng dồn càng nhiều trace càng tốt qua lớp code-based trước, chỉ escalate lên lớp đắt hơn khi thật sự không còn cách nào khác.

Dấu Hiệu Cần Escalate Sang LLM-as-Judge

Có một dấu hiệu lặp lại đáng tin cậy hơn bất kỳ quy tắc lý thuyết nào: nếu một assertion cứ phải phình to thêm từng điều kiện "hoặc X, hoặc Y, hoặc Z" để bao hết các cách diễn đạt đúng, đó là lúc cái đang được đo không còn là một invariant nữa mà đã trở thành một phán đoán chất lượng. Dấu hiệu khác là khi hai kỹ sư đọc cùng một assertion fail và bất đồng liệu output đó có thật sự sai hay không - bất đồng đó nghĩa là quy tắc code không còn đủ để định nghĩa đúng/sai, và cần một judge có khả năng cân nhắc ngữ cảnh thay vì một dòng if cứng nhắc.

Cạm Bẫy Thường Gặp Khi Viết Code-Based Eval

Vài lỗi lặp lại ở hầu hết các team: viết assertion so khớp với chính xác output của một lần chạy mẫu, biến bộ test thành một danh sách "chỉ chấp nhận đúng câu trả lời này"; kiểm tra chi tiết triển khai nội bộ (tên biến, thứ tự bước gọi tool) thay vì kiểm tra outcome cuối cùng người dùng thấy; để assertion không được cập nhật khi prompt hay model đổi, khiến bộ test dần dần chấm sai một hành vi đã thay đổi có chủ đích; và không version hoá test case cùng với thay đổi prompt, khiến không ai biết một assertion đang bảo vệ hành vi cũ hay hành vi mới. Giữ assertion bám sát invariant, không bám sát hình thức, là cách duy nhất để lớp eval rẻ nhất này vẫn còn đáng tin sau nhiều lần đổi model.