1. RAG là gì và tại sao kiểm thử nó lại khó
RAG (retrieval-augmented generation) là kiến trúc ghép một bộ truy hồi tài liệu với một mô hình ngôn ngữ lớn. Khi người dùng đặt câu hỏi, hệ thống tìm các đoạn tài liệu liên quan nhất rồi nhồi chúng vào ngữ cảnh để mô hình sinh câu trả lời. Mục tiêu là để câu trả lời bắt nguồn từ tri thức thật thay vì trí nhớ mờ nhạt của mô hình. Trong y tế, RAG thường trả lời về phác đồ, tương tác thuốc hay hướng dẫn nội bộ bệnh viện. Chính vì đầu ra chạm tới sức khoẻ con người, sai một câu có thể gây hại thật.
Kiểm thử RAG khó vì đầu ra không tất định và không có một đáp án chuỗi ký tự duy nhất. Cùng một câu hỏi có thể sinh nhiều diễn đạt đúng khác nhau, nên so sánh chuỗi cứng là vô nghĩa. Hơn nữa, lỗi có thể nằm ở tầng truy hồi (lấy sai đoạn) hoặc ở tầng sinh (bịa dù đoạn đúng). Nếu chỉ nhìn câu trả lời cuối, ta không biết trách khâu nào. Vì thế nguyên tắc cốt lõi của bài này là ĐO TÁCH LỚP: kiểm retrieval riêng, kiểm generation riêng, rồi mới kiểm end-to-end.
2. Chất lượng truy hồi: đo tầng retriever trước tiên
Nếu retriever lấy sai đoạn, không mô hình nào cứu được câu trả lời. Vì thế bước đầu là dựng một tập câu hỏi kèm nhãn đoạn tài liệu đúng (ground-truth passages). Với mỗi câu hỏi, ta chạy retriever và so danh sách top-k trả về với nhãn. Ba thước đo phổ biến là recall@k (có bắt được đoạn đúng trong top-k không), MRR (đoạn đúng nằm càng cao càng tốt) và nDCG (thưởng cho thứ hạng và mức liên quan). Đo tầng này tách biệt giúp bạn biết nâng cấp embedding hay thêm rerank có thật sự cải thiện gốc rễ.
// eval-retrieval.ts — đo tầng retriever tách biệt với ground-truth passages
import { retrieve } from "../rag/retriever";
import goldens from "./retrieval-goldens.json"; // [{ q, relevantDocIds: string[] }]
function recallAtK(returned: string[], relevant: string[], k: number) {
const top = new Set(returned.slice(0, k));
const hit = relevant.filter((id) => top.has(id)).length;
return hit / relevant.length;
}
function reciprocalRank(returned: string[], relevant: string[]) {
const idx = returned.findIndex((id) => relevant.includes(id));
return idx === -1 ? 0 : 1 / (idx + 1);
}
const K = 5;
let sumRecall = 0, sumMrr = 0;
for (const g of goldens) {
const docs = await retrieve(g.q, { k: K }); // trả về mảng docId theo thứ hạng
sumRecall += recallAtK(docs, g.relevantDocIds, K);
sumMrr += reciprocalRank(docs, g.relevantDocIds);
}
const recall = sumRecall / goldens.length;
const mrr = sumMrr / goldens.length;
console.log({ "recall@5": recall.toFixed(3), mrr: mrr.toFixed(3) });
// Oracle-first: gate CI chặn merge nếu recall@5 < baseline (vd 0.85) hoặc MRR tụt.
if (recall < 0.85) throw new Error("Retrieval regression: recall@5 dưới ngưỡng");- recall@k: retriever có lấy được ít nhất một đoạn đúng trong top-k không.
- MRR: đoạn đúng nằm càng cao trong danh sách càng tốt.
- nDCG: thưởng cho cả thứ hạng lẫn mức độ liên quan của từng đoạn.
- Đo trên tập câu hỏi có nhãn cố định, chạy lại mỗi khi đổi embedding/chunking.
3. Faithfulness & groundedness: câu trả lời có bám nguồn không
Faithfulness (còn gọi groundedness) đo mức độ mọi khẳng định trong câu trả lời được hỗ trợ bởi ngữ cảnh đã lấy về. Một câu trả lời có thể đúng ngữ pháp, trôi chảy, nhưng vẫn chứa một mệnh đề không hề có trong nguồn — đó là ảo giác. Cách kiểm phổ biến là tách câu trả lời thành các mệnh đề nguyên tử, rồi với mỗi mệnh đề hỏi: đoạn nguồn có suy ra được điều này không? Nếu một mệnh đề không được nguồn hỗ trợ, điểm faithfulness giảm. Đây là oracle mạnh nhất của RAG vì nó bắt đúng loại lỗi nguy hiểm nhất trong y tế.
// eval-faithfulness.ts — tách mệnh đề & kiểm từng cái có được nguồn hỗ trợ
import { askRag } from "../rag/pipeline";
import { splitClaims, entails } from "./nli"; // entails: dùng NLI hoặc LLM-judge có ràng buộc
type Case = { q: string };
export async function faithfulness(c: Case) {
const { answer, contexts } = await askRag(c.q); // contexts = đoạn thật đã đưa vào prompt
const claims = splitClaims(answer); // mệnh đề nguyên tử
const support = await Promise.all(
claims.map(async (cl) => ({
claim: cl,
grounded: await entails(contexts.join("\n"), cl), // nguồn có suy ra mệnh đề?
})),
);
const grounded = support.filter((s) => s.grounded).length;
return {
score: claims.length ? grounded / claims.length : 1,
ungrounded: support.filter((s) => !s.grounded).map((s) => s.claim), // để review
};
}
// Oracle: mọi mệnh đề y tế (liều, chống chỉ định) PHẢI grounded === true,
// nếu không → fail cứng, không tính điểm trung bình cho nhóm câu hỏi an toàn.
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!