1. Vì sao test truyền thống không đủ cho output sinh
Test phần mềm truyền thống dựa trên một giả định ngầm: cùng đầu vào cho ra cùng đầu ra. Bạn viết assertEqual(f(x), y) và nếu code đúng thì luôn xanh. Nhưng một mô hình ngôn ngữ (LLM) hay một agent sinh không tuân theo giả định đó: cùng một prompt có thể cho ra hai câu trả lời khác nhau về từ ngữ dù cùng ý; nhiệt độ (temperature) khác nhau tạo độ sáng tạo khác nhau; và mô hình có thể 'ảo giác' (hallucination) — bịa ra thông tin nghe hợp lý nhưng sai. Vì thế bạn không thể so bằng dấu bằng chuỗi cứng.
Giải pháp là một eval harness (bộ khung đánh giá): một hệ thống chuyên biệt để đo chất lượng output sinh một cách có hệ thống, có thể lặp lại, và tích hợp vào CI. Thay vì một assertion 'đúng/sai' cứng, eval harness dùng nhiều loại kiểm khác nhau — từ kiểm cấu trúc rẻ và tất định, tới chấm điểm theo rubric bằng chính LLM — và tổng hợp thành các chỉ số quyết định được. Bài này hướng dẫn xây một harness như vậy, đủ chặt để làm cổng gác (gate) mỗi khi bạn nâng cấp prompt hay đổi mô hình.
2. Golden set và fixtures: nền móng của mọi eval
Nền móng của một eval harness là golden set (bộ vàng): một tập hợp có tuyển chọn gồm các đầu vào tiêu biểu kèm đầu ra kỳ vọng hoặc tiêu chí đánh giá. Golden set không cần khổng lồ, nhưng phải đại diện: gồm các ca thường gặp (happy path), các ca biên, các ca 'bẫy' dễ khiến mô hình ảo giác, và các ca nhạy cảm (an toàn, chính sách). Chất lượng của toàn bộ eval phụ thuộc vào chất lượng của golden set — rác vào thì rác ra.
// golden/support-agent.jsonl — mỗi dòng một ca đánh giá (input + kỳ vọng)
{ "id": "refund-happy", "input": "Tôi muốn hoàn tiền đơn #A1 đã giao 3 ngày trước",
"expect": { "intent": "refund", "must_contain": ["A1"], "must_not_contain": ["số thẻ"],
"schema": "SupportReply", "rubric": ["đúng chính sách 7 ngày", "lịch sự"] } }
{ "id": "refund-expired", "input": "Hoàn tiền đơn #B2 giao 40 ngày trước",
"expect": { "intent": "refund_denied", "must_contain": ["quá hạn"], "schema": "SupportReply" } }
{ "id": "trap-hallucinate", "input": "Chính sách bảo hành 10 năm của bạn là gì?",
// 'Bẫy' ảo giác: hệ thống KHÔNG có chính sách 10 năm → phải từ chối bịa
"expect": { "must_not_contain": ["10 năm"], "rubric": ["không bịa chính sách không tồn tại"] } }Fixtures là phần bối cảnh cố định đi kèm mỗi ca: dữ liệu tài khoản giả, tài liệu tri thức mà agent được phép truy xuất, trạng thái hệ thống ban đầu. Cố định fixtures giúp eval tất định về mặt bối cảnh — cùng một câu hỏi luôn chạy trên cùng một nền dữ liệu, nên nếu điểm số thay đổi thì đó là do prompt/model đổi chứ không phải do bối cảnh trôi. Hãy quản lý golden set và fixtures trong version control như mã nguồn, và review mọi thay đổi của chúng nghiêm túc.
3. Các loại assertion cho output sinh (tháp bốn tầng)
Đánh giá output sinh cần nhiều loại assertion xếp thành một tháp, từ rẻ-tất định ở đáy tới đắt-nhiễu ở đỉnh. Tầng một là cấu trúc: output có phải JSON hợp lệ không, có khớp schema không. Tầng hai là từ vựng: có chứa cụm bắt buộc, có tránh cụm cấm, có khớp regex. Tầng ba là ngữ nghĩa: độ tương đồng (semantic similarity) so với đáp án vàng, để chấp nhận diễn đạt khác nhau nhưng cùng ý. Tầng bốn là rubric: dùng chính LLM chấm điểm theo bộ tiêu chí (LLM-as-judge). Quy tắc: luôn ưu tiên tầng rẻ và tất định trước, chỉ leo lên tầng đắt khi thật cần.
// Ba tầng đầu: rẻ, tất định — chạy trước, loại sớm output hỏng cấu trúc
import { z } from 'zod';
const SupportReply = z.object({
intent: z.enum(['refund', 'refund_denied', 'other']),
message: z.string().min(1),
policyRef: z.string().optional(),
});
function structuralChecks(raw: string, expect: any) {
const parsed = JSON.parse(raw); // Tầng 1a: JSON hợp lệ?
SupportReply.parse(parsed); // Tầng 1b: đúng schema?
for (const s of expect.must_contain ?? [])
if (!parsed.message.includes(s)) throw new Error('thiếu cụm bắt buộc: ' + s); // Tầng 2
for (const s of expect.must_not_contain ?? [])
if (parsed.message.includes(s)) throw new Error('chứa cụm cấm: ' + s); // Tầng 2
return parsed;
}Semantic similarity (tầng ba) thường được đo bằng cách nhúng (embedding) cả output và đáp án vàng thành vector rồi tính độ tương đồng cosine; nếu vượt ngưỡng thì coi là 'cùng ý'. Cách này chấp nhận sự đa dạng diễn đạt mà vẫn giữ được tính tất định tương đối (embedding cùng model cho cùng vector). Tầng bốn — rubric bằng LLM-as-judge — mạnh nhất nhưng cũng nhiễu và đắt nhất, nên ta dành hẳn hai chương sau để nói về cách dùng nó một cách hiệu chuẩn và có kiểm soát bias.
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!