Ở bài trước, chúng ta đã tách chất lượng RAG thành các dimension:
Retriever
├── Contextual Recall
├── Contextual Precision
└── Contextual Relevancy
Generator
├── Faithfulness
└── Answer Relevancy
Bài này đi thêm một bước: làm sao biến những metric đó thành một benchmark có thể chạy lặp lại để so sánh các phiên bản RAG?
Mục tiêu không phải chỉ chạy DeepEval cho một query. Mục tiêu là xây một pipeline:
Golden dataset
-> RAG version A / B / C
-> Runtime test cases
-> DeepEval metrics
-> Per-case scores
-> Aggregate + failure analysis
-> Engineering decision
01Benchmark RAG thực chất đang benchmark cái gì
Trước khi viết code, cần xác định unit of comparison. Giả sử bạn muốn thử:
Embedding A vs Embedding B
Chunk size 512 vs 1024
Top-k 5 vs 10
No reranker vs reranker
Prompt v1 vs prompt v2
LLM A vs LLM B
Nếu cùng thay cả sáu thứ một lúc, benchmark có thể nói “System B tốt hơn System A”, nhưng không nói được vì sao. Và một kết luận không nói được vì sao thì cũng không hướng dẫn được bước tiếp theo.
Một benchmark tốt nên version hoá các hyperparameter:
retriever:
embedding_model: ...
chunk_size: 512
chunk_overlap: 64
top_k: 8
reranker: ...
generator:
model: ...
prompt_version: v3
temperature: ...
evaluation:
deepeval_version: ...
judge_model: ...
threshold: ...
Tư duy này biến benchmark từ “run thử vài câu” thành một controlled experiment.
02Kiến trúc benchmark chúng ta sẽ xây
Sơ đồ ở đầu bài (Hình 1) là toàn bộ pipeline: cùng một golden dataset đi qua hai phiên bản RAG, sinh ra hai bộ test case, được chấm bằng đúng cùng một bộ metric và cùng một judge, rồi mới đưa vào so sánh.
Điểm mấu chốt là dataset phải giữ nguyên giữa các phiên bản. Nếu A chạy trên dataset này còn B chạy trên dataset khác, comparison không còn sạch.
03Golden và cách viết expected_output
DeepEval phân biệt Golden với LLMTestCase, và đây là một distinction rất hữu ích khi benchmark. Golden chứa input và reference tĩnh:
from deepeval.dataset import Golden
golden = Golden(
input="Self-attention khác attention truyền thống như thế nào?",
expected_output=(
"Self-attention tính quan hệ giữa các token trong cùng "
"một sequence, trong khi attention tổng quát có thể "
"liên kết query và key/value từ các nguồn khác nhau."
)
)
Nó không chứa actual_output và retrieval_context, vì hai field này phụ thuộc vào RAG version đang chạy. Với cùng một golden:
Golden #42
├── run RAG A -> Test Case A42
└── run RAG B -> Test Case B42
Đó chính là điều ta muốn khi regression testing.
expected_output nên viết thế nào
expected_output không nhất thiết phải là một đoạn văn đẹp. Nó cần biểu diễn các facts và requirement mà answer lý tưởng phải chứa. Thay vì:
"Đây là một câu trả lời dài, tự nhiên, đầy đủ..."
có thể dùng cấu trúc rõ ràng:
- Early cancellation is allowed.
- A 10% fee applies.
- Refund excludes setup fee.
Điều này giúp con người bảo trì dataset dễ hơn, giúp metric dễ diễn giải hơn, và giữ cho reference nhất quán giữa các case. Đặc biệt vì Contextual Recall và Contextual Precision dùng chính reference này để judge chất lượng retrieval.
04Dataset tốt phải chứa những loại query nào
Đừng chỉ lấy 20 câu hỏi dễ. Một RAG benchmark nên cố gắng bao phủ nhiều failure mode. Với knowledge-base QA, các nhóm đáng có gồm:
Direct lookup — "Mức hoàn tiền của gói A là bao nhiêu?" — kiểm tra retrieval cơ bản.
Multi-fact — "So sánh chính sách hoàn tiền của gói A và B." — kiểm tra retriever có lấy đủ facts từ nhiều nơi không.
Paraphrase — "Nếu huỷ giữa kỳ thì tôi có lấy lại phí không?" trong khi document dùng từ early termination refund — kiểm tra semantic retrieval.
Ambiguous query — "Policy này áp dụng thế nào?" — kiểm tra cách hệ thống xử lý khi thiếu context.
Negative / not-in-KB — "Công ty có hỗ trợ chương trình X không?" khi KB không có thông tin — kiểm tra generator có biết abstain hay tự hallucinate.
Long-tail entity — "Quy trình dành cho account thuộc chương trình ABC-Enterprise-2024?" — kiểm tra entity retrieval.
Conflict — hai document chứa thông tin khác version — kiểm tra hệ thống xử lý version, date và source thế nào.
Multi-hop — câu trả lời yêu cầu nối hai hoặc nhiều đoạn evidence.
Đừng để dataset chỉ phản ánh happy path
Có thể chia dataset theo category:
Dataset
├── factual_simple
├── paraphrase
├── multi_document
├── multi_hop
├── ambiguity
├── no_answer
├── conflicting_sources
└── long_tail
Sau này, ngoài aggregate score overall faithfulness = 0.91, ta còn xem được:
faithfulness / no_answer = 0.62
faithfulness / factual_simple = 0.98
faithfulness / multi_hop = 0.84
Đây mới là failure analysis hữu ích. Một improvement có thể chỉ thêm 1% tổng thể nhưng thêm 20% ở nhóm no_answer, và điều đó có thể đáng giá hơn rất nhiều.
05RAG function phải trả cả answer lẫn retrieved chunks
Một lỗi phổ biến khi bắt đầu evaluation là application chỉ expose:
def rag(query) -> str:
return answer
Nhưng muốn đánh giá retrieval, ta cần retrieval_context. Một interface phù hợp hơn:
def rag_pipeline(query: str):
chunks = retrieve(query)
answer = generate(query, chunks)
return {
"answer": answer,
"retrieval_context": [chunk.text for chunk in chunks],
}
Lưu ý retrieval_context phải là những chunk thật sự được đưa vào generation nếu mục tiêu là đo end-to-end RAG. Nếu retriever trả 20 candidates nhưng reranker chỉ chọn 5 chunk đưa cho LLM, hãy xác định rõ metric đang đo raw retrieval hay final context after reranking. Nếu không, score rất dễ bị diễn giải sai.
Chuyển Golden thành LLMTestCase tại runtime
from deepeval.test_case import LLMTestCase
result = rag_pipeline(golden.input)
test_case = LLMTestCase(
input=golden.input,
actual_output=result["answer"],
expected_output=golden.expected_output,
retrieval_context=result["retrieval_context"],
)
Điểm quan trọng: expected_output đến từ golden, trong khi actual_output và retrieval_context đến từ run hiện tại.
Golden RAG Runtime
├── input ├── actual_output
└── expected_output └── retrieval_context
= LLMTestCase
06Khởi tạo bộ năm metric và chuyện chọn hard gate
Ví dụ cấu hình:
from deepeval.metrics import (
AnswerRelevancyMetric,
FaithfulnessMetric,
ContextualRecallMetric,
ContextualPrecisionMetric,
ContextualRelevancyMetric,
)
metrics = [
ContextualRecallMetric(threshold=0.75),
ContextualPrecisionMetric(threshold=0.75),
ContextualRelevancyMetric(threshold=0.75),
FaithfulnessMetric(threshold=0.85),
AnswerRelevancyMetric(threshold=0.80),
]
Các threshold ở đây chỉ là ví dụ để minh hoạ pipeline. Không nên copy các số trên vào production rồi coi chúng là chuẩn.
Vì sao Faithfulness thường được xem là metric critical
Trong nhiều RAG QA use case, retrieval hơi noisy vẫn có thể chấp nhận được nếu answer cuối grounded. Nhưng answer tự thêm unsupported fact thì nguy hiểm hơn nhiều. Vì thế một policy có thể là:
Faithfulness >= 0.90 -> hard gate
Answer Relevancy >= 0.80 -> hard gate
Contextual Precision -> monitor
Contextual Relevancy -> monitor
Hoặc với một domain khác, Contextual Recall mới là critical, nếu thiếu một requirement có thể làm answer sai hẳn.
Điểm quan trọng: gate phải phản ánh failure cost, không phải metric nào nổi tiếng hơn.
07Chạy evaluation và đưa vào CI/CD
Với notebook hoặc script, evaluate() là interface thuận tiện:
from deepeval import evaluate
from deepeval.dataset import EvaluationDataset
dataset = EvaluationDataset(goldens=goldens)
for golden in dataset.goldens:
result = rag_pipeline(golden.input)
dataset.add_test_case(
LLMTestCase(
input=golden.input,
actual_output=result["answer"],
expected_output=golden.expected_output,
retrieval_context=result["retrieval_context"],
)
)
evaluate(
test_cases=dataset.test_cases,
metrics=metrics,
)
DeepEval chạy từng test case qua các metric và tạo một test run. Về mặt khái niệm, N test case nhân M metric cho N × M metric evaluation. Với N = 500 và M = 5 thì có tới 2.500 lượt đánh giá, và mỗi metric LLM-as-a-Judge có thể cần một hoặc nhiều judge call. Vì vậy benchmark lớn cần tính cả evaluation cost, latency, rate limit, caching và parallelism.
deepeval test run cho CI
Nếu muốn biến quality thành regression test, có thể dùng pytest-style flow:
import pytest
from deepeval import assert_test
from deepeval.metrics import (
AnswerRelevancyMetric,
FaithfulnessMetric,
)
from deepeval.test_case import LLMTestCase
@pytest.mark.parametrize("golden", dataset.goldens)
def test_rag(golden):
result = rag_pipeline(golden.input)
test_case = LLMTestCase(
input=golden.input,
actual_output=result["answer"],
expected_output=golden.expected_output,
retrieval_context=result["retrieval_context"],
)
assert_test(
test_case=test_case,
metrics=[
FaithfulnessMetric(threshold=0.90),
AnswerRelevancyMetric(threshold=0.80),
],
)
Chạy:
deepeval test run test_rag.py
Tư duy lúc này rất gần software test:
code change
-> run RAG benchmark
-> critical metrics pass?
├── yes -> continue
└── no -> regression
Nhưng không nhất thiết mọi metric đều phải block CI. Nếu Faithfulness và Answer Relevancy là critical còn Writing Style chỉ là experimental, có thể cấu hình metric style ở chế độ score-only hoặc flaky, tuỳ workflow. Điều này giúp tránh tình trạng một subjective quality score dao động nhẹ làm deployment fail liên tục. Một CI gate tốt nên tập trung vào high-cost failures.
08Benchmark hai phiên bản RAG A và B
Giả sử muốn so:
RAG A RAG B
chunk_size = 512 chunk_size = 512
top_k = 5 top_k = 10
no reranker reranker = cross-encoder
Dùng cùng goldens, và giữ evaluator ổn định:
same dataset
same metric config
same judge model
same thresholds
same evaluation steps
same DeepEval version nếu có thể
Nếu judge thay giữa A và B, bạn đã đổi measuring instrument giữa experiment.
Một ví dụ kết quả
| Metric | RAG A | RAG B |
|---|---|---|
| Contextual Recall | 0.86 | 0.94 |
| Contextual Precision | 0.90 | 0.78 |
| Contextual Relevancy | 0.88 | 0.76 |
| Faithfulness | 0.91 | 0.95 |
| Answer Relevancy | 0.89 | 0.91 |
Nếu chỉ lấy trung bình:
A = (0.86 + 0.90 + 0.88 + 0.91 + 0.89) / 5 = 0.888
B = (0.94 + 0.78 + 0.76 + 0.95 + 0.91) / 5 = 0.868
Ta có thể vội kết luận “A tốt hơn B”. Nhưng quality vector kể một câu chuyện sâu hơn: B tăng mạnh ở Recall, tăng ở Faithfulness và Answer Relevancy, nhưng giảm ở Precision và Relevancy.
Một giả thuyết rất tự nhiên:
top-k tăng
├── retrieve đủ evidence hơn
└── cũng kéo nhiều noise hơn
reranker
└── chưa đủ tốt để xử lý candidate pool lớn
Kết luận engineering có thể không phải “quay về A”, mà là “giữ candidate recall của B, rồi tuning reranker và final top-k”. Đây là giá trị thật của component-level evaluation.
09So sánh theo per-case, không chỉ aggregate
Giả sử aggregate Faithfulness của A là 0.91 và của B là 0.95. Nghe có vẻ B thắng. Nhưng xem per-case:
| Case | A | B |
|---|---|---|
| q1 | .92 | .98 |
| q2 | .89 | .96 |
| q3 | .95 | .97 |
| q4 | .91 | .42 |
| q5 | .88 | .99 |
B có một catastrophic failure ở q4.
Vì thế nên xem mean, median, pass rate, minimum hoặc low percentile, và số lượng catastrophic failure. Ví dụ:
Mean Faithfulness: B > A
nhưng số case Faithfulness < 0.5:
A = 0 cases
B = 7 cases
Tuỳ domain, A có thể vẫn an toàn hơn.
Paired analysis
Vì A và B chạy cùng query, mỗi case có một cặp score, và ta có thể xét hiệu số của từng case:
q1: +0.10
q2: +0.04
q3: -0.30
q4: +0.02
Điều này hữu ích hơn chỉ so mean(B) - mean(A), vì ta nhìn được B cải thiện query nào và làm hỏng query nào. Một model upgrade có thể tăng mean nhưng regression mạnh ở một nhóm critical.
Failure analysis nên là một phần chính của benchmark
Sau mỗi run, đừng chỉ lưu score. Hãy lưu query, actual_output, expected_output, retrieval_context, metric score, metric reason, experiment config và category. Một failure record có thể trông như:
query_id: q_0042
category: no_answer
query: "Công ty có hỗ trợ chương trình X không?"
faithfulness: 0.31
answer_relevancy: 0.94
contextual_relevancy: 0.83
failure:
type: unsupported_claim
notes:
- Retriever did not contain evidence that program X exists.
- Generator answered confidently instead of abstaining.
Pattern Answer Relevancy cao đi cùng Faithfulness thấp cực kỳ quan trọng: answer nghe rất đúng chủ đề nhưng evidence không support. Đây chính là kiểu hallucination dễ đánh lừa người dùng nhất.
10Map score pattern thành engineering hypothesis
Một bảng debug hữu ích:
| Pattern | Hypothesis |
|---|---|
| Recall thấp | embedding, query, index hoặc top-k chưa lấy đủ evidence |
| Recall cao, Precision thấp | candidate tốt nhưng ranking hoặc reranking kém |
| Recall cao, Relevancy thấp | top-k hoặc chunking kéo quá nhiều noise |
| Retrieval tốt, Faithfulness thấp | generator không grounded |
| Faithfulness cao, Answer Relevancy thấp | prompt trả lời lan man, không bám intent |
| Relevancy thấp và Faithfulness thấp | context nhiễu có thể làm generator hallucinate |
| Reference metrics tốt nhưng custom correctness thấp | domain requirement chưa được generic metrics phản ánh |
Metric không tự sửa hệ thống. Metric giúp bạn tạo ra một diagnostic hypothesis tốt hơn.
11Benchmark từng thành phần: retrieval, reranker, generator, prompt
Hyperparameter retrieval
DeepEval đặc biệt hữu ích khi thử chunk_size, chunk_overlap, top_k, embedding model, hybrid search và reranker. Ví dụ một grid:
Experiment 1: chunk=256, top_k=5
Experiment 2: chunk=512, top_k=5
Experiment 3: chunk=512, top_k=10
Experiment 4: chunk=512, top_k=20
Ta có thể quan sát trend:
top_k tăng
├── Recall tăng
├── Relevancy giảm
└── generation cost tăng
Mục tiêu không phải maximize một metric đơn lẻ, mà là tìm operating point: đủ recall, noise chấp nhận được, latency và cost chấp nhận được, và generator vẫn grounded.
Reranker
Reranker nên được nhìn đặc biệt qua Contextual Precision:
| Configuration | Recall | Precision | Relevancy |
|---|---|---|---|
| Vector top-10 | .93 | .64 | .75 |
| Vector 50 rồi rerank top-10 | .94 | .88 | .89 |
Điều này gợi ý rằng candidate generation vốn đã đủ recall, và reranker cải thiện ordering cùng khả năng lọc noise. Nếu Recall tụt sau rerank, reranker có thể đang loại mất evidence quan trọng.
Generator model
Giữ retrieval context cố định rồi chạy nhiều generator:
same query
same retrieved chunks
├── LLM A
├── LLM B
└── LLM C
Khi đó Faithfulness, Answer Relevancy và custom G-Eval phản ánh generator rõ hơn, vì retrieval variance đã được loại bớt. Đây là cách thiết kế experiment tốt hơn việc để mỗi model chạy cả một retrieval pipeline khác nhau.
Prompt
Prompt change thường ảnh hưởng Faithfulness, Answer Relevancy, format, abstention behaviour và citation behaviour. Ví dụ Prompt A:
Answer the question using the context.
và Prompt B:
Answer only from the supplied context.
If the context is insufficient, say that the information
is not available. Do not infer unsupported facts.
Một benchmark trên subset no_answer có thể cho Prompt A đạt Faithfulness .61 còn Prompt B đạt .93, trong khi answer relevancy gần như giữ nguyên. Đây là bằng chứng cụ thể rằng prompt B cải thiện grounding.
Tách hẳn retriever và generator
Đây là một kỹ thuật experiment rất hữu ích. Để đánh giá retriever, đóng băng generator và tập trung vào ba metric retrieval, rồi thử embedding, chunking, top-k và reranker. Để đánh giá generator, đóng băng retrieval context, chạy cùng query với cùng context qua các prompt hoặc model khác nhau, rồi đo Faithfulness, Answer Relevancy và custom G-Eval. Cách này giảm confounding variables.
12Khi nào cần custom G-Eval, khi nào cần DAG
Generic RAG metrics không biết business rule của bạn. Ví dụ một customer-support RAG cần:
phải nêu refund period
phải nêu exception
không được hứa compensation
Faithfulness chỉ hỏi “claim có được context support không”. Một answer có thể hoàn toàn faithful nhưng thiếu exception quan trọng. Ta có thể thêm G-Eval:
from deepeval.metrics import GEval
from deepeval.test_case import SingleTurnParams
completeness = GEval(
name="Policy Completeness",
criteria=(
"Evaluate whether the answer includes every material condition, "
"exception, and limitation required by the expected output."
),
evaluation_steps=[
"List the material requirements in expected_output.",
"Check which requirements appear in actual_output.",
"Penalize omission of any condition that changes eligibility.",
"Ignore harmless wording differences.",
],
evaluation_params=[
SingleTurnParams.ACTUAL_OUTPUT,
SingleTurnParams.EXPECTED_OUTPUT,
],
threshold=0.85,
)
Đây là cách kết hợp generic system metrics với business-specific metric.
Khi business rule là một hard gate
Nếu rule có dạng “nếu answer chứa forbidden claim thì fail bất kể phần còn lại tốt thế nào”, đừng average nó với style. DAG phù hợp hơn:
Forbidden claim?
├── YES -> score 0
└── NO
-> Required disclaimer?
├── NO -> score 0.4
└── YES -> Evaluate quality
Đây là cách tránh một con số rất nguy hiểm về mặt ý nghĩa:
Compliance = 0
Style = 1
Helpfulness = 1
Average = 0.67
13Offline benchmark, online evaluation và calibrate threshold
Offline có thể có expected_output, có curated dataset và có labels, nên dùng được đủ năm metric cộng custom metric. Production thì phần lớn không có expected_output, nên thường dựa vào bộ referenceless: Contextual Relevancy, Faithfulness và Answer Relevancy.
Không nên ép offline và online dùng đúng cùng một metric set.
Nếu chưa có expected_output
Nếu dataset chỉ có input còn runtime sinh ra retrieval_context và actual_output, hãy bắt đầu với RAG Triad:
metrics = [
ContextualRelevancyMetric(),
FaithfulnessMetric(),
AnswerRelevancyMetric(),
]
Điều này phù hợp với historical production traces, unlabeled test set và online monitoring. Sau đó chọn một subset để human label và xây reference-based benchmark dần. Không cần chờ có 10.000 golden answers mới bắt đầu evaluation.
Calibrate threshold bằng human evaluation
Sau khi có metric, cần xác định score nào thực sự acceptable. Quy trình:
- lấy một sample đủ đại diện;
- cho human reviewer label;
- chạy DeepEval trên cùng sample;
- so sánh judge với humans;
- chọn threshold;
- kiểm tra false pass và false fail.
Ví dụ với threshold = 0.70:
| Human Accept | Human Reject | |
|---|---|---|
| Metric Pass | 320 | 42 |
| Metric Fail | 20 | 118 |
Nếu 42 false pass là quá cao, tăng threshold. Nhưng khi tăng threshold thì false pass giảm còn false fail tăng — đó là trade-off, và không tồn tại một threshold tối ưu cho mọi business.
Với tiếng Việt, nên calibrate trên dữ liệu tiếng Việt
Judge model là một learned evaluator, nên scoring behavior có thể khác theo ngôn ngữ, domain, style và độ phức tạp của query. Nếu production traffic chủ yếu tiếng Việt thì một English calibration set không phải bằng chứng đủ mạnh cho ngưỡng dùng với tiếng Việt. Tốt hơn là lấy sample giống production bằng tiếng Việt, cho người gán nhãn, chạy metric rồi mới calibrate.
14Version mọi thứ, log cả reason, và vòng lặp từ production
Một benchmark result nên đi kèm:
experiment_id: rag_v7
rag:
embedding_model: ...
top_k: 10
reranker: ...
generator_model: ...
prompt_version: v4
dataset:
version: 2026-09-07
num_cases: 500
evaluation:
deepeval_version: ...
judge_model: ...
metrics:
faithfulness:
threshold: 0.90
answer_relevancy:
threshold: 0.80
Nếu không, sau ba tháng bạn nhìn RAG v7 = 0.91 và không còn biết 0.91 được đo bằng gì.
Đừng chỉ log score, hãy log reason
Một lợi ích lớn của LLM-based metric là metric có thể cung cấp reason. Thay vì chỉ Faithfulness = 0.47, failure analysis có thể có danh sách unsupported claims cụ thể. Reason không nên được coi là ground truth tuyệt đối, nhưng nó cực kỳ hữu ích để cluster failure, debug prompt, inspect retrieval và build regression cases.
Một workflow tốt:
Low score
-> Inspect metric reason
-> Inspect retrieved chunks
-> Classify root cause
-> Add / update regression case
Từ production incident quay lại evaluation dataset
Dataset tốt không bao giờ “xong”.
Mỗi lỗi production đáng chú ý nên có cơ hội trở thành một regression test, để lỗi đó không quay lại trong version sau. Đây là cách evaluation dataset trở thành bộ nhớ của những failure mà team đã học được.
15Report, decision rule và chi phí của evaluation
Một benchmark report nên có gì
Không cần một dashboard phức tạp ngay từ đầu. Một report tốt có thể gồm experiment metadata (RAG version, dataset version, judge model, metric config, timestamp), một bảng metric summary:
| Metric | Mean | Median | Pass Rate | p10 |
|---|---|---|---|---|
| Contextual Recall | ||||
| Contextual Precision | ||||
| Contextual Relevancy | ||||
| Faithfulness | ||||
| Answer Relevancy |
cùng với breakdown theo category, danh sách worst cases, danh sách regression (case pass ở A nhưng fail ở B) và danh sách improvement (case fail ở A nhưng pass ở B). Đây là những dữ liệu có giá trị hơn một overall average.
Decision rule tốt hơn “average cao hơn là thắng”
Ví dụ:
Candidate B được deploy nếu:
1. Faithfulness pass rate không giảm.
2. Không xuất hiện critical regression.
3. Contextual Recall tăng >= 3%.
4. p10 Answer Relevancy không giảm quá 2%.
5. Latency vẫn trong budget.
Đây là một decision rule đa mục tiêu. RAG production gần như luôn có nhiều objective cùng lúc: quality, latency, cost, safety, coverage và groundedness. Không metric đơn nào đại diện được toàn bộ.
Cost của evaluation cũng cần benchmark
LLM-as-a-Judge không miễn phí. Với 500 case nhân 5 metric nhân số judge call, chi phí evaluation có thể trở nên đáng kể.
PR suite Nightly suite
├── 50 critical cases ├── 1,000 cases
└── 3 hard-gate metrics └── full metric set
Một cấu trúc benchmark thực tế
evals/
│
├── datasets/
│ ├── rag_core.jsonl
│ ├── no_answer.jsonl
│ └── regression.jsonl
│
├── metrics/
│ ├── rag_metrics.py
│ └── domain_metrics.py
│
├── experiments/
│ ├── config_a.yaml
│ └── config_b.yaml
│
├── runners/
│ ├── run_eval.py
│ └── compare.py
│
└── reports/
Điểm quan trọng là separation giữa dataset, metric, RAG config, evaluation runner và report. Nhờ vậy bạn có thể thay một layer mà không làm mất reproducibility của layer khác.
Code skeleton hoàn chỉnh
from dataclasses import dataclass
from typing import List
from deepeval import evaluate
from deepeval.dataset import EvaluationDataset, Golden
from deepeval.test_case import LLMTestCase
from deepeval.metrics import (
ContextualRecallMetric,
ContextualPrecisionMetric,
ContextualRelevancyMetric,
FaithfulnessMetric,
AnswerRelevancyMetric,
)
@dataclass
class RAGResult:
answer: str
retrieval_context: List[str]
def rag_pipeline(query: str) -> RAGResult:
chunks = retrieve(query)
answer = generate(
query=query,
contexts=[c.text for c in chunks],
)
return RAGResult(
answer=answer,
retrieval_context=[c.text for c in chunks],
)
goldens = [
Golden(input="...", expected_output="..."),
Golden(input="...", expected_output="..."),
]
dataset = EvaluationDataset(goldens=goldens)
for golden in dataset.goldens:
result = rag_pipeline(golden.input)
dataset.add_test_case(
LLMTestCase(
input=golden.input,
actual_output=result.answer,
expected_output=golden.expected_output,
retrieval_context=result.retrieval_context,
)
)
metrics = [
ContextualRecallMetric(threshold=0.75),
ContextualPrecisionMetric(threshold=0.75),
ContextualRelevancyMetric(threshold=0.75),
FaithfulnessMetric(threshold=0.90),
AnswerRelevancyMetric(threshold=0.80),
]
evaluate(
test_cases=dataset.test_cases,
metrics=metrics,
)
Các threshold chỉ mang tính minh hoạ. Trong benchmark thật, hãy calibrate chúng.
16Checklist trước khi tin một kết quả benchmark
- A và B có chạy trên cùng goldens không?
- Dataset có edge cases không?
-
expected_outputcó phản ánh facts thật sự cần thiết không? -
retrieval_contextcó đúng là context runtime không? - Judge model có giữ nguyên giữa các experiment không?
- Metric configuration có được version không?
- Threshold có được calibrate hay chỉ dùng default?
- Có xem per-case distribution không?
- Có xem failure reason không?
- Có phân tích theo category không?
- Có kiểm tra critical regressions không?
- Có tách retriever và generator khi cần không?
- Có tính evaluation cost không?
- Production failures có được đưa ngược về regression set không?
Nếu chưa trả lời được những câu này, con số benchmark vẫn cần được xem thận trọng.
Mục tiêu cuối cùng không phải là công bố RAG B = 0.887 cao hơn RAG A = 0.881 rồi tuyên bố B thắng. Một benchmark tốt phải trả lời được: B tốt hơn ở đâu, B tệ hơn ở đâu, failure nào biến mất, failure mới nào xuất hiện, trade-off là gì, và có phù hợp với production objective không. DeepEval cung cấp measuring tools; decision vẫn thuộc về kiến trúc hệ thống và business requirement.
Evaluation là một feedback loop, không phải bước cuối dự án
Một pipeline benchmark RAG tốt không dừng ở metric.measure(test_case). Nó là một vòng: golden dataset được chọn lọc, chạy qua RAG, sinh runtime test case, đi qua DeepEval để ra score và reason, được tổng hợp và phân tích phân bố, dẫn tới failure analysis, rồi vừa quay lại thay đổi RAG vừa bổ sung regression case vào chính dataset đó.
Những gì nên mang theo:
- xác định unit of comparison trước khi viết code, và mỗi lần chỉ đổi một biến;
- dataset và evaluator phải cố định giữa các phiên bản, nếu không comparison mất nghĩa;
- dataset phải phủ edge case, và nên chia theo category để failure analysis có chỗ bám;
retrieval_contextphải đúng là context runtime, và phải nói rõ trước hay sau rerank;- gate nên phản ánh chi phí của lỗi, không phải mức độ nổi tiếng của metric;
- đọc per-case và phần đuôi, không chỉ đọc trung bình;
- log cả reason chứ không chỉ score, vì reason mới dẫn được tới nguyên nhân gốc;
- mỗi lỗi production đáng chú ý nên trở thành một regression case.
Khi những phần đó được kết hợp, evaluation mới thực sự giúp trả lời câu hỏi có giá trị: phiên bản RAG mới tốt hơn ở điểm nào, tệ hơn ở điểm nào, và chúng ta có đủ bằng chứng để đưa nó vào production hay chưa.
Tài liệu tham khảo
- DeepEval — Introduction to LLM Evals
- DeepEval — Datasets / Goldens
- DeepEval — Single-Turn Test Case
- DeepEval — RAG Evaluation Quickstart
- DeepEval — RAG Evaluation Guide
- DeepEval — RAG Triad
- DeepEval — Metrics
- DeepEval — Faithfulness
- DeepEval — Answer Relevancy
- DeepEval — Contextual Recall
- DeepEval — Contextual Precision
- DeepEval — Contextual Relevancy
- DeepEval — G-Eval
- DeepEval — DAG