$ cat ./blog/gen-ai/benchmark-rag-voi-deepeval.md
seriesRAG từ cơ bản đến nâng caobài 4

Benchmark RAG với DeepEval: Hands-on từ dataset đến regression test

Bài thứ tư trong series RAG từ cơ bản đến nâng cao: biến năm metric thành một benchmark chạy lặp lại được - xây golden dataset có edge case, sửa interface để lấy retrieval context, chọn hard gate, chạy evaluate() và deepeval test run, so sánh hai phiên bản RAG, đọc per-case và failure analysis, rồi đưa evaluation vào vòng lặp phát triển.

~/diagrams/benchmark-rag-voi-deepeval/benchmark-architecture.htmltương tác
mở riêng ↗
HìnhCùng một golden dataset chạy qua hai phiên bản RAG, sinh ra hai bộ test case, đi qua đúng cùng một bộ metric và cùng một judge, rồi mới được ghép cặp để so sánh. Điều kiện để so sánh còn nghĩa lý nằm ở hai chỗ cố định: dataset và evaluator. Đổi một trong hai giữa A và B là đã đổi luôn thí nghiệm.
nhánh phiên bản Aphần phải giữ cố địnhnhánh phiên bản B

Ở 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
~/diagrams/benchmark-rag-voi-deepeval/dataset-categories.htmltương tác
mở riêng ↗
HìnhCùng một benchmark, cùng một metric, nhưng đọc theo hai đường khác nhau. Đường trên đi từ điểm tổng xuống nhóm câu hỏi dễ và kết luận rằng mọi thứ ổn. Đường dưới tách ra nhóm no_answer và lộ ra chỗ hệ thống thực sự đang hỏng. Chỉ khác nhau ở việc có chia dataset theo category từ đầu hay không.
đọc theo điểm tổngbước tách nhómnhóm đang hỏng

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],
    }
~/diagrams/benchmark-rag-voi-deepeval/golden-vs-testcase.htmltương tác
mở riêng ↗
HìnhHàng trên là bốn bước dựng một test case: Golden cấp input và câu trả lời chuẩn, pipeline được chạy để sinh answer cùng danh sách chunk, hai phần ghép lại thành LLMTestCase rồi mới đủ field để chấm. Hàng dưới là hai chỗ phải kiểm lại - hàm chỉ trả về string thì không đo được retrieval, và phải nói rõ retrieval_context là context trước hay sau rerank.
dựng một test casephần sinh lúc chạychỗ phải kiểm lại

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.

~/diagrams/benchmark-rag-voi-deepeval/ab-controlled.htmltương tác
mở riêng ↗
HìnhHàng trên là năm điều kiện của một thí nghiệm có kiểm soát: cùng goldens, chỉ đổi một biến, chạy cả hai cấu hình, chấm bằng cùng một judge, rồi mới đọc kết quả. Hàng dưới là hai cách phổ biến làm hỏng nó - đổi sáu thứ cùng lúc nên không truy được nguyên nhân, và đổi judge giữa chừng nên đổi luôn thiết bị đo.
thí nghiệm có kiểm soátevaluator giữ nguyêncách làm mất kiểm soát

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.

~/diagrams/benchmark-rag-voi-deepeval/per-case-analysis.htmltương tác
mở riêng ↗
HìnhHai hàng là hai hệ thống có cùng mean faithfulness bằng 0.90, đọc theo cùng năm chặng: trung bình, phân bố per-case, case xấu nhất, số case dưới 0.5, và rủi ro thực tế. Trung bình không phân biệt được chúng. Phân bố thì phân biệt rất rõ, và với hallucination thì phần đuôi mới là thứ quyết định.
hệ thống ổn địnhcase xấu nhấthệ thống có đuôi nguy hiểm

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

~/diagrams/benchmark-rag-voi-deepeval/pattern-hypothesis.htmltương tác
mở riêng ↗
HìnhHàng trên là vòng chẩn đoán: đọc quality vector theo cặp thay vì lấy trung bình, rút ra giả thuyết, sửa đúng một chỗ, rồi chạy lại benchmark để kiểm chứng. Hàng dưới là ba pattern hay gặp và chỗ chúng chỉ tới - thiếu evidence, ranking kém, hoặc generator không grounded.
vòng chẩn đoánbước kiểm chứngpattern hay gặp

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.

~/diagrams/benchmark-rag-voi-deepeval/offline-vs-online.htmltương tác
mở riêng ↗
HìnhHai hàng là hai chế độ đánh giá, đọc theo cùng năm chặng: nguồn dữ liệu, thứ có sẵn, metric dùng được, câu hỏi cần trả lời và nhịp chạy. Offline có label nên mở khoá được cả năm metric và dùng để so sánh phiên bản. Production không có reference nên chỉ còn RAG Triad, và mục đích cũng khác: giám sát liên tục thay vì so sánh.
offline benchmarkmetric dùng đượconline evaluation

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:

  1. lấy một sample đủ đại diện;
  2. cho human reviewer label;
  3. chạy DeepEval trên cùng sample;
  4. so sánh judge với humans;
  5. chọn threshold;
  6. 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”.

~/diagrams/benchmark-rag-voi-deepeval/eval-feedback-loop.htmltương tác
mở riêng ↗
HìnhHàng trên là đường đi của một lỗi production: được phát hiện, được người xem lại và phân loại, được viết thành một golden có nhãn, rồi thêm vào dataset để bản sau bắt buộc phải vượt qua. Hàng dưới là điều xảy ra nếu bỏ qua hai bước đó - lỗi cũ quay lại ở phiên bản sau, và dataset đứng yên nên chỉ còn đo happy path.
vòng lặp regressiongolden mới có nhãnnếu bỏ qua bước này

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ể.

~/diagrams/benchmark-rag-voi-deepeval/eval-tiers.htmltương tác
mở riêng ↗
HìnhHai hàng là hai tầng benchmark, đọc theo cùng năm chặng: chạy khi nào, bộ test bao nhiêu case, metric nào, chi phí ra sao và đóng vai trò gì. PR suite nhỏ và rẻ nên chạy được trên mỗi pull request. Nightly suite lớn và đắt nên chạy ngoài giờ. Cách chia này giống hệt smoke test và full regression trong software testing.
PR suitebộ test và metricnightly suite
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_output có phản ánh facts thật sự cần thiết không?
  • retrieval_context có đú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_context phả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

Trương Đức Dũng

AI Engineer - Hà Nội

Tập trung vào LLM, RAG và các hệ thống AI cần chạy ổn định trong môi trường vận hành thật.