FDE PulseViệc làm FDE đang mở 316Mới đăng 7 ngày qua 10Chủ đề nổi bật: Đào tạo kỹ năng FDE tại Đông Nam Á

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

Thực hành: đo retrieval của RAG bằng hit rate và MRR trước khi khách hàng tự phát hiện lỗi

Hai con số, một bộ câu hỏi chuẩn và vài chục dòng Python đủ để bạn biết hệ thống tìm sai ở đâu, trước khi phải đọc câu trả lời sai của LLM.

Đồ hoạQuy trình đo retrieval bằng hit rate và MRR
  1. 1Dựng golden setBắt đầu với vài chục câu hỏi, gắn nhãn bằng passage ID, có cả câu dễ lẫn câu khó
  2. 2Chạy retriever với top kLấy danh sách passage ID đã xếp hạng cho từng câu hỏi
  3. 3Tính hit rate@kKiểm có/không: top k có ít nhất một đoạn liên quan hay không
  4. 4Tính MRRLấy 1/thứ hạng của đoạn liên quan đầu tiên, rồi tính trung bình
  5. 5Đối chiếu thêm recallPhát hiện các câu cần nhiều đoạn mà MRR không thấy được

Phải đo retriever riêng, trên một golden set cân bằng, thì mới biết lỗi nằm ở bước tìm kiếm hay ở bước sinh câu trả lời.

Đồ hoạ: FDE Times

Tóm tắt nhanh

  • Đo retriever tách riêng khỏi bước sinh câu trả lời, nếu không bạn sẽ không biết lỗi nằm ở tìm kiếm hay ở LLM.
  • Hit rate là phép kiểm có/không theo từng câu hỏi, MRR thưởng cho việc đặt kết quả đúng lên đầu, và cả hai đều bỏ qua các đoạn liên quan phía sau.
  • Golden set nên bắt đầu từ vài chục câu, tăng dần lên 50-200, gắn nhãn bằng passage ID và có đủ câu khó.
Chia sẻLinkedInFacebookX

Trong tài liệu ví dụ của LlamaIndex có một dòng output dễ khiến người đọc giật mình: với cùng một câu hỏi, hit rate và MRR đều đạt 1.0, trong khi precision chỉ có 0.5. Không có gì mâu thuẫn ở đây. Mỗi chỉ số đang trả lời một câu hỏi khác nhau.

Chỗ này quan trọng với FDE vì RAG ở site khách hàng hỏng theo một kiểu quen thuộc: câu trả lời sai, và không ai biết lỗi nằm ở bước tìm tài liệu hay ở bước LLM viết câu trả lời. Pinecone đặt tên bài hướng dẫn về đánh giá RAG của họ là “đừng để khách hàng báo cho bạn trước”.

Muốn làm được vậy, bạn cần đo retriever một cách riêng biệt, bằng những con số giải thích được cho người không làm kỹ thuật.

Bài này hướng dẫn bạn làm việc đó trên laptop: dựng một golden set nhỏ, tự tính hit rate và MRR bằng Python thuần để hiểu bản chất, sau đó chạy cùng phép đo bằng RetrieverEvaluator của LlamaIndex.

Bạn cần chuẩn bị gì?

Bạn cần một hệ thống RAG đang chạy được, dù chỉ là bản thử nghiệm, và một retriever nhận vào câu hỏi rồi trả về danh sách đoạn văn có ID, theo thứ tự xếp hạng. Ngoài ra cần Python, và LlamaIndex nếu muốn làm bước 3.

Mọi đoạn code bên dưới đều là bản rút gọn để minh hoạ cách làm, nên trước khi dùng thật bạn hãy đối chiếu tên hàm với tài liệu của phiên bản đang cài.

LlamaIndex tách bước đánh giá retrieval ra khỏi bước sinh câu trả lời: RetrieverEvaluator chấm chất lượng của bất kỳ module Retriever nào được định nghĩa trong thư viện. Bạn nên giữ đúng tư duy đó. Hãy đo phần tìm kiếm cho chắc trước, rồi mới đụng đến prompt.

Bước 1: golden set quyết định con số đáng tin đến đâu

Golden set là danh sách câu hỏi, mỗi câu đi kèm các đoạn văn mà bạn biết chắc là liên quan. Kunal Ganglani, trong cẩm nang đánh giá retrieval trên production của mình, khuyên bắt đầu với vài chục câu hỏi rồi tăng dần lên 50-200.

Ông cũng khuyên gắn nhãn bằng passage ID thay vì nội dung văn bản, để nhãn vẫn dùng được khi bạn đổi cách chia chunk.

golden = [
    {"query": "Hạn mức hoàn tiền tối đa là bao nhiêu?",
     "relevant_ids": {"policy_v3#12"}, "group": "easy"},
    {"query": "Khách doanh nghiệp đổi gói giữa kỳ thì tính phí thế nào?",
     "relevant_ids": {"billing#4", "billing#7"}, "group": "hard"},
    # ... thêm đến vài chục câu
]

Kiểm tra sau bước này: mở file và đếm xem bao nhiêu câu là FAQ dễ, bao nhiêu câu là loại khó, chẳng hạn câu cần ghép hai đoạn hoặc câu dùng từ ngữ khác với tài liệu. Ganglani cảnh báo rằng nếu phần lớn golden set là FAQ dễ thì số đo sẽ đẹp hơn hiệu năng thật của hệ thống.

Nếu tự gắn nhãn tốn quá nhiều công, tài liệu LlamaIndex gợi ý dùng LLM sinh dữ liệu tổng hợp, tức các cặp câu hỏi và ngữ cảnh. Cách này giúp có điểm xuất phát nhanh. Có điều, dữ liệu sinh ra không tự đảm bảo tỷ lệ dễ và khó, nên bạn vẫn nên đọc lại, đếm theo nhóm và tự viết bổ sung câu khó nếu thiếu.

Bước 2: tự tính các con số để hiểu chúng đo gì

Hit rate@k là phép kiểm có hoặc không cho từng câu hỏi: có ít nhất một đoạn liên quan nằm trong top k hay không. MRR, theo cách Weaviate định nghĩa, đo xem hệ thống đưa được kết quả liên quan lên vị trí đầu tốt đến đâu. Điểm của mỗi câu hỏi bằng 1 chia cho thứ hạng của đoạn liên quan đầu tiên.

def hit_rate_and_mrr(golden, retrieve, k=5):
    hits, rr_sum = 0, 0.0
    for item in golden:
        ids = retrieve(item["query"], k)  # list passage ID, đã xếp hạng
        for rank, pid in enumerate(ids, start=1):
            if pid in item["relevant_ids"]:
                hits += 1
                rr_sum += 1 / rank
                break  # chỉ tính đoạn liên quan đầu tiên
    n = len(golden)
    return hits / n, rr_sum / n

Thử một ví dụ giả định với 4 câu hỏi. Đoạn liên quan đầu tiên lần lượt nằm ở hạng 1, hạng 3, không có trong top k, và hạng 2. Hit rate bằng 3/4 = 0.75. MRR bằng (1 + 1/3 + 0 + 1/2) / 4, xấp xỉ 0.458.

Kiểm tra sau bước này: tính tay ví dụ trên rồi đưa đúng 4 trường hợp đó vào hàm, xem kết quả có khớp không. Nếu không khớp, lỗi thường nằm ở chỗ rank được đếm từ 0 thay vì từ 1.

Vì cả hai con số trên đều dừng lại ở đoạn liên quan đầu tiên, bạn nên viết thêm một hàm recall@k đơn giản: với mỗi câu, lấy số đoạn liên quan tìm được trong top k chia cho tổng số đoạn liên quan, rồi lấy trung bình. Tham số group cho phép chấm riêng từng nhóm câu hỏi.

def recall_at_k(golden, retrieve, k=5, group=None):
    items = [i for i in golden if group is None or i["group"] == group]
    total = 0.0
    for item in items:
        found = set(retrieve(item["query"], k)) & item["relevant_ids"]
        total += len(found) / len(item["relevant_ids"])
    return total / len(items)

Với câu đổi gói giữa kỳ, nếu top k chỉ chứa billing#4 thì recall của câu đó là 0.5, dù MRR có thể vẫn đạt 1.0.

Bước 3: chạy cùng phép đo với LlamaIndex

Khi đã hiểu bản chất, bạn có thể để thư viện làm phần còn lại. Theo tài liệu LlamaIndex, evaluator được dựng từ tên các metric và một retriever, sau đó chạy bất đồng bộ trên toàn bộ dataset. Đoạn code dưới đây là phác thảo, bạn cần kiểm tra lại đường dẫn import trong phiên bản của mình.

from llama_index.core.evaluation import RetrieverEvaluator

retriever = index.as_retriever(similarity_top_k=5)
retriever_evaluator = RetrieverEvaluator.from_metric_names(
    ["hit_rate", "mrr"], retriever=retriever
)
eval_results = await retriever_evaluator.aevaluate_dataset(qa_dataset)

Ngoài hit rate và MRR, evaluator còn hỗ trợ precision, recall, AP và NDCG, đều chọn theo tên. Kiểm tra sau bước này: chạy thử với một câu hỏi trong golden set mà bạn đã biết đáp án ở bước 2. Hai cách tính phải cho cùng kết quả. Nếu lệch, hãy xem lại ID của bạn có khớp với node ID mà index tạo ra không.

Hai con số đẹp vẫn có thể che giấu điều gì?

Quay lại dòng output ở đầu bài: hit rate 1.0, MRR 1.0, recall 1.0, nhưng precision chỉ 0.5 và NDCG khoảng 0.61. Đoạn liên quan đã nằm ở vị trí số một. Tuy vậy, một nửa danh sách trả về là nhiễu, và chính phần nhiễu đó sẽ được đưa vào context của LLM.

Hit rate@k

  • Hỏi: có ít nhất một đoạn liên quan trong top k không?
  • Mỗi câu hỏi chỉ được chấm có hoặc không
  • Có thể trông tốt dù độ bao phủ kém

MRR

  • Hỏi: đoạn liên quan đầu tiên đứng ở hạng mấy?
  • Điểm mỗi câu bằng 1/thứ hạng, rồi lấy trung bình
  • Bỏ qua mọi đoạn liên quan đứng sau đoạn đầu tiên

Ganglani và Weaviate cùng chỉ ra một giới hạn của MRR: nó chỉ xét đoạn liên quan đầu tiên. Với câu hỏi về đổi gói giữa kỳ ở bước 1, cần cả billing#4 lẫn billing#7, hệ thống có thể đạt MRR tối đa mà vẫn bỏ sót một nửa thông tin.

Vì thế, khi golden set có nhiều câu cần ghép nhiều đoạn, bạn nên báo cáo thêm recall@k bằng hàm ở bước 2.

Những lỗi hay gặp

Lỗi đầu tiên là gắn nhãn bằng nội dung văn bản. Chỉ cần đổi chunk size một lần là toàn bộ golden set mất giá trị, và bạn không còn so được trước với sau. Lỗi thứ hai là chỉ báo cáo một mức k.

Hit rate ở k=10 gần như lúc nào cũng cao hơn ở k=3, nên con số chỉ có ý nghĩa khi ghi rõ k bên cạnh.

Lỗi thứ ba khó thấy hơn: golden set toàn câu dễ. Trong demo mọi thứ trông rất ổn, rồi đến tuần đầu tiên người dùng thật đặt những câu hỏi mà bộ đo chưa từng có. Cách sửa là tách kết quả theo nhóm câu hỏi, để nhóm câu khó luôn có điểm riêng.

Kỹ năng này xuất hiện thế nào ở site khách hàng?

Ở site khách hàng, câu bạn nghe nhiều nhất thường là “bot trả lời sai”. Thử hình dung một FDE mang golden set 60 câu vào buổi họp với đội vận hành và nói, với các con số giả định: “Ở nhóm FAQ, hit rate@5 là 0.92.

Ở nhóm câu cần ghép hai đoạn, hit rate@5 vẫn 0.80 nhưng recall@5 chỉ 0.45, tức bot thường chỉ tìm được một nửa thông tin cần thiết.”

Câu tiếp theo mới là thứ khách hàng cần: “Vì vậy tuần này đội sẽ sửa chunking cho bộ tài liệu billing rồi đo lại trên đúng 60 câu này, chưa cần đổi model.” Thay vì tranh luận bot “có vẻ” tốt hay tệ, hai bên thống nhất được việc cần làm và cách kiểm tra kết quả.

Với developer Việt Nam muốn chuyển sang FDE, hãy viết CV theo đúng cấu trúc đó. Thay vì ghi “xây dựng chatbot RAG”, hãy ghi bạn đã dựng golden set bao nhiêu câu, chia mấy nhóm, và chỉ số nào của nhóm khó thay đổi ra sao sau khi đổi chiến lược chunking.

Khi đọc JD, hãy để ý các cụm như “evaluation”, “retrieval quality” hay “offline eval”, vì đó là dấu hiệu nhà tuyển dụng cần đúng kỹ năng này.

Bộ golden set đầu tiên thường chỉ vài chục câu và còn nhiều chỗ chưa hoàn hảo. Nhưng nó cho bạn một mốc cố định để đo lại sau mỗi lần thay đổi hệ thống, và nhờ đó bạn có thể phát hiện lỗi trước khi khách hàng gọi điện báo.

5 nguồn
Đọc tiếp trên lộ trình · Chặng 6: Đo lườngThực hành: viết bộ eval 50 mẫu cho trợ lý hỏi đáp bằng PythonNăm mươi câu hỏi lấy từ lỗi thật, chấm đúng/sai bằng pytest và một LLM judge đã đối chiếu với nhãn chấm tay: đủ để biến cảm giác "có vẻ ổn hơn" thành một con số cả đội cùng tin.