AITrợ lý AI cho BanHang, phần 2/2
Đánh giá một hệ RAG: đo trước khi tin câu trả lời
Dựng golden set từ câu hỏi thật, đo retrieval bằng hit@k, recall@k, MRR, kiểm câu trả lời bằng luật và LLM chấm, rồi chặn thay đổi trong CI.
Trợ lý AI của BanHang trả lời khách về chính sách đổi trả bằng RAG (retrieval-augmented generation): tìm vài đoạn tài liệu nội bộ liên quan, rồi đưa chúng cho LLM viết câu trả lời. Bài trước nói về phần tìm kiếm bằng embedding. Bài này nói cách đo cả hệ: dựng golden set (tập câu hỏi có đáp án chuẩn), đo riêng retrieval và generation, rồi biến phép đo thành cổng chặn trong CI. Ví dụ xuyên bài là một sự cố: trợ lý báo thời hạn đổi trả 30 ngày, trong khi chính sách ghi 7 ngày.
Đọc nhanh
- Đo hai tầng riêng. Retrieval: top-k có chứa đoạn đúng không, đoạn đúng đứng thứ mấy. Generation: câu trả lời có đúng sự kiện và bám nguồn không.
- Golden set khoảng 50 câu hỏi từ log chat, mỗi câu có mục tài liệu kỳ vọng và câu trả lời kỳ vọng. Nhãn trỏ tới mục tài liệu, không trỏ tới id chunk.
- Trong dữ liệu dựng của bài, đổi chunk 200 sang 800 token giữ recall@3 ở 1,00 nhưng kéo MRR@5 từ 0,90 xuống 0,63. Cổng chỉ xét recall@k sẽ để thay đổi đó đi qua.
- Con số về thời hạn và tiền kiểm bằng luật. LLM chấm phần còn lại, người chấm mẫu để biết LLM chấm còn đáng tin. Mỗi lỗi production thành một câu mới trong golden set.
1. Sự cố: 7 ngày thành 30 ngày
Sự cố dưới đây là tình huống dựng cho bài. Thứ Sáu 2026-09-25, nhóm trợ lý AI đổi hai thứ trong một lần triển khai: chunk tăng từ 200 lên 800 token, và embedding model đổi sang một model đa ngôn ngữ mới. Một kỹ sư hỏi thử năm câu bằng tay, thấy ổn, rồi triển khai lúc 17:00.
Sáng thứ Hai 2026-09-28, bộ phận chăm sóc khách hàng (CSKH) nhận khiếu nại: trợ lý bảo khách được trả áo trong 30 ngày. Chính sách ghi 7 ngày kể từ ngày nhận hàng, ở mục DT-1. Con số 30 ngày có trong tài liệu, ở mục DT-3, nhưng chỉ cho hàng thời trang của thành viên BanHang Plus. Từ tối thứ Sáu đến sáng thứ Hai, 37 cuộc hội thoại đã nhận con số sai. Nhóm rollback lúc 11:20.
Sau rollback vẫn chưa ai biết lỗi do cỡ chunk hay do model, còn câu nào khác trả lời sai, và bản cũ tốt hơn bao nhiêu. Không có số đo nào từ trước thứ Sáu để so. Phần còn lại của bài dựng bộ đo đủ để chặn bản triển khai đó từ chiều thứ Sáu.
2. Hai tầng: lấy đúng đoạn, rồi trả lời đúng
Câu trả lời sai có hai nguồn gốc. Retriever lấy nhầm đoạn. Hoặc retriever lấy đúng nhưng LLM đọc sai, trộn hai con số, thêm điều nguồn không nói. Hai lỗi sửa ở hai chỗ khác nhau. Chỉ chấm câu trả lời cuối thì biết là sai mà không biết sửa ở đâu.
Tầng retrieval hỏi: top-k có chứa đoạn mang câu trả lời không, và nó đứng thứ mấy. Phép đo này chỉ cần embedding câu hỏi và tra index, không cần LLM sinh câu trả lời, nên rẻ. Tầng generation hỏi: câu trả lời có đúng sự kiện không, có nói gì ngoài nguồn không. Ghép hai tầng cho từng câu thì ra chẩn đoán:
| Retrieval | Câu trả lời | Chẩn đoán |
|---|---|---|
| Đúng | Đúng | Ổn |
| Đúng | Sai | Lỗi generation: prompt, model, ngữ cảnh chứa số mâu thuẫn |
| Sai | Sai | Lỗi retrieval: chunking, embedding, top-k |
| Sai | Đúng | Model trả lời từ hiểu biết sẵn có, sẽ sai khi chính sách đổi. Hoặc golden set thiếu nhãn |
3. Golden set: 50 câu hỏi từ khách thật
Golden set là tập câu hỏi cố định, mỗi câu có đáp án do người xác nhận. Mọi cấu hình chấm trên cùng tập, nên số hôm nay so được với số tuần trước. Câu tự nghĩ ra thường quá sạch: đủ dấu, đúng thuật ngữ, một ý. Khách thật viết khác.
BanHang dựng tập đầu tiên, khoảng 50 câu, theo bốn bước:
- Lấy câu hỏi từ log chat hai đến ba tháng gần nhất, xóa tên, số điện thoại, mã đơn.
- Gom theo ý định, mỗi mục chính sách có ít nhất hai câu. Giữ nguyên cách khách viết: không dấu, viết tắt, "ship".
- Thêm câu cần nhiều mục, câu có con số, và vài câu ngoài phạm vi mà đáp án đúng là "tài liệu không nói".
- Người phụ trách chính sách bên CSKH ghi mục nguồn và câu trả lời kỳ vọng, một người thứ hai kiểm lại. Bất đồng là bình thường: ở TREC-8, người chấm bất đồng về việc một câu có trả lời đúng không, nhưng thứ hạng giữa các hệ vẫn ổn định (Voorhees và Tice, 2000).
Nhãn trỏ tới mục tài liệu (DT-1), không trỏ tới id chunk. Id chunk đổi mỗi lần chia lại tài liệu, nên nhãn theo id chunk không so được chunk 200 với chunk 800, đúng phép so nhóm cần. Chunker chỉ cần ghi vào metadata mỗi chunk những mục nó chứa.
Năm câu của tập, lưu thành golden.json để chạy các script bên dưới:
[
{"id": "q01", "muc_ky_vong": ["DT-1"], "nguon": "su-co-2026-09-28",
"cau_hoi": "Mua áo mặc không vừa thì được trả trong mấy ngày?",
"tra_loi_ky_vong": "Trong 7 ngày kể từ ngày nhận hàng, hàng còn tem, nhãn và hóa đơn."},
{"id": "q02", "muc_ky_vong": ["DT-2"], "nguon": "chat-2026-08",
"cau_hoi": "laptop moi nhan bi soc man hinh thi doi sao",
"tra_loi_ky_vong": "Lỗi nhà sản xuất được đổi máy mới trong 7 ngày, sau đó bảo hành 12 tháng tại hãng."},
{"id": "q03", "muc_ky_vong": ["DT-4"], "nguon": "chat-2026-08",
"cau_hoi": "Trả hàng rồi bao lâu thì nhận lại tiền?",
"tra_loi_ky_vong": "Trong 5 ngày làm việc sau khi kho nhận hàng trả."},
{"id": "q04", "muc_ky_vong": ["DT-5"], "nguon": "chat-2026-08",
"cau_hoi": "Đổi ý không lấy nữa thì có mất phí ship không?",
"tra_loi_ky_vong": "Có. Khách đổi ý chịu phí vận chuyển 30.000 đồng."},
{"id": "q05", "muc_ky_vong": ["DT-2", "DT-3"], "nguon": "chat-2026-09",
"cau_hoi": "Điện thoại có được trả trong 30 ngày như quần áo không?",
"tra_loi_ky_vong": "Không. Điện thoại lỗi được đổi máy mới trong 7 ngày. Mức 30 ngày chỉ áp dụng cho hàng thời trang của thành viên Plus."}
]
nguon ghi câu hỏi đến từ đâu: q01 sinh ra từ sự cố, mục 6 nói tại sao. q05 cần hai mục, nên nó phân biệt được hit@k với recall@k.
4. Đo retrieval: hit@k, recall@k, MRR
Với một câu hỏi, gọi E là tập mục kỳ vọng. Một chunk là chunk đúng nếu nó chứa ít nhất một mục trong E.
hit@k = 1 nếu top-k có ít nhất một chunk đúng, ngược lại 0
recall@k = (số mục của E có mặt trong top-k) / |E|
RR = 1 / (hạng của chunk đúng đầu tiên), bằng 0 nếu top-k không có chunk đúng
MRR@k = trung bình RR trên mọi câu hỏi
hit@k và recall@k cũng lấy trung bình trên mọi câu. MRR là điểm chính của track hỏi đáp TREC-8 (1999): mỗi câu được điểm bằng nghịch đảo hạng của câu trả lời đúng đầu tiên, 0 nếu năm câu trả lời đầu không có câu đúng.
Ví dụ q05 ở cấu hình chunk 200: retriever trả c03 (chứa DT-3), rồi c02 (chứa DT-2). Vậy hit@1 = 1, recall@1 = 1/2 = 0,5, recall@3 = 2/2 = 1, RR = 1. Ở q04, chunk đúng đứng hạng 2: hit@1 = 0, RR = 1/2 = 0,5.
Khi nhãn có nhiều mức, dùng nDCG: DCG@k = Σ (2^rel_i − 1) / log2(i + 1) với i là hạng, nDCG@k = DCG@k / IDCG@k, trong đó IDCG@k là DCG của thứ tự lý tưởng. Nhãn đúng/sai cho gain 2^1 − 1 = 1. q05 ở cấu hình chunk 800 có chunk đúng ở hạng 1 và 3: DCG@5 = 1/log2(2) + 1/log2(4) = 1,5. Thứ tự lý tưởng đặt chúng ở hạng 1 và 2: IDCG@5 = 1 + 1/log2(3) ≈ 1,63, nên nDCG@5 ≈ 0,92. Golden set này chỉ có nhãn đúng/sai, đa số câu một mục, nên bài dùng MRR.
Script dưới tính các chỉ số cho hai cấu hình. Thứ hạng trong TOP5 là dữ liệu dựng tay mô phỏng sự cố, không phải số đo từ hệ thật. Trong hệ thật, TOP5 là kết quả chạy retriever của từng cấu hình trên golden set. Chạy cùng thư mục với golden.json, Python 3.12 trở lên, chỉ thư viện chuẩn. Số in dưới đây chạy bằng Python 3.14.3.
import json
import sys
from pathlib import Path
GOLDEN = {g["id"]: set(g["muc_ky_vong"])
for g in json.loads(Path("golden.json").read_text(encoding="utf-8"))}
# Dữ liệu dựng tay cho bài viết, không phải số đo từ hệ thật.
# Mỗi chunk ghi các mục tài liệu mà nó chứa (DT: đổi trả, VC: vận chuyển, BH: bảo hành).
CHUNK = {
"chunk-200": {"c01": {"DT-1"}, "c02": {"DT-2"}, "c03": {"DT-3"}, "c04": {"DT-4"},
"c05": {"DT-5"}, "c06": {"DT-6"}, "c07": {"VC-1"}, "c08": {"VC-2"},
"c09": {"BH-1"}},
"chunk-800": {"L1": {"DT-1", "DT-2"}, "L2": {"DT-3", "DT-4"}, "L3": {"DT-5", "DT-6"},
"L4": {"VC-1", "VC-2"}, "L5": {"BH-1", "BH-2"}},
}
# Top-5 mà retriever trả về cho từng câu hỏi, theo thứ tự điểm giảm dần.
TOP5 = {
"chunk-200": {"q01": ["c01", "c03", "c06", "c07", "c02"],
"q02": ["c02", "c09", "c01", "c03", "c06"],
"q03": ["c04", "c01", "c05", "c08", "c03"],
"q04": ["c07", "c05", "c01", "c08", "c04"],
"q05": ["c03", "c02", "c01", "c09", "c06"]},
"chunk-800": {"q01": ["L2", "L4", "L1", "L3", "L5"],
"q02": ["L1", "L5", "L2", "L4", "L3"],
"q03": ["L4", "L5", "L2", "L1", "L3"],
"q04": ["L4", "L3", "L2", "L1", "L5"],
"q05": ["L2", "L5", "L1", "L3", "L4"]},
}
NGUONG = {"hit@3": 0.95, "mrr@5": 0.85}
def danh_gia(cau_hinh):
muc_cua = CHUNK[cau_hinh]
tong = {"hit@1": 0.0, "hit@3": 0.0, "recall@3": 0.0, "mrr@5": 0.0}
hang_dung = {}
for q, ky_vong in GOLDEN.items():
top = TOP5[cau_hinh][q]
hang = next((i for i, c in enumerate(top, 1) if muc_cua[c] & ky_vong), None)
tim_thay_top3 = set().union(*(muc_cua[c] for c in top[:3])) & ky_vong
hang_dung[q] = hang
tong["hit@1"] += hang == 1
tong["hit@3"] += hang is not None and hang <= 3
tong["recall@3"] += len(tim_thay_top3) / len(ky_vong)
tong["mrr@5"] += 1 / hang if hang else 0
return {k: v / len(GOLDEN) for k, v in tong.items()}, hang_dung
ket_qua = {ch: danh_gia(ch) for ch in TOP5}
print(f"{'cấu hình':<10}" + "".join(f"{k:>10}" for k in ket_qua["chunk-200"][0]))
for ch, (chi_so, _) in ket_qua.items():
print(f"{ch:<10}" + "".join(f"{v:>10.2f}" for v in chi_so.values()))
print("\nhạng của chunk đúng đầu tiên, chunk-200 -> chunk-800")
cu, moi = ket_qua["chunk-200"][1], ket_qua["chunk-800"][1]
for q in GOLDEN:
tut = (moi[q] or 99) > (cu[q] or 99) # None: không có trong top-5
print(f"{q}: {cu[q]} -> {moi[q]}" + (" TỤT" if tut else ""))
chi_so_moi = ket_qua["chunk-800"][0]
truot = [f"{k} {chi_so_moi[k]:.2f} < {v}" for k, v in NGUONG.items() if chi_so_moi[k] < v]
print("\nCổng chunk-800:", "TRƯỢT, " + "; ".join(truot) if truot else "ĐẠT")
sys.exit(1 if truot else 0)
Kết quả, script thoát với mã 1:
cấu hình hit@1 hit@3 recall@3 mrr@5
chunk-200 0.80 1.00 1.00 0.90
chunk-800 0.40 1.00 1.00 0.63
hạng của chunk đúng đầu tiên, chunk-200 -> chunk-800
q01: 1 -> 3 TỤT
q02: 1 -> 1
q03: 1 -> 3 TỤT
q04: 2 -> 2
q05: 1 -> 1
Cổng chunk-800: TRƯỢT, mrr@5 0.63 < 0.85
hit@3 và recall@3 bằng 1,00 ở cả hai cấu hình. Chỉ nhìn hai cột này thì thay đổi hôm thứ Sáu vô hại. MRR@5 rơi từ 0,90 xuống 0,63, hit@1 từ 0,80 xuống 0,40: chunk đúng vẫn được lấy, nhưng bị đẩy xuống. Danh sách theo câu chỉ ra chỗ hỏng. Ở q01, chunk đứng đầu là L2, gộp DT-3 và DT-4, tức đoạn có "30 ngày". Ngữ cảnh gửi cho LLM chứa cả 7 ngày lẫn 30 ngày, và 30 ngày đứng trước.
recall@3 không giảm vì chunk 800 gộp hai mục vào một chunk: top-3 phủ sáu mục, so với ba mục ở chunk 200. recall@k tự tăng khi chunk to ra. So hai cỡ chunk ở cùng k là so tới 2.400 token ngữ cảnh với tới 600 token. Báo cáo kèm số token ngữ cảnh, hoặc so ở cùng ngân sách token, ví dụ top-12 chunk 200 với top-3 chunk 800.
Tập ở đây có 5 câu, mỗi câu nặng 0,2 điểm. Với 50 câu, mỗi câu nặng 0,02, và đổi model có thể làm vài câu lên xuống ngẫu nhiên. Đọc danh sách câu tụt hạng, đừng chỉ đọc trung bình.
5. Đo generation: con số, bám nguồn, LLM chấm, người chấm
Con số là lỗi đắt nhất của trợ lý này: thời hạn, tiền hoàn, phí. Đó cũng là lỗi kiểm được bằng luật, không cần LLM. Luật có hai chiều: mọi cụm "số + đơn vị" trong câu trả lời kỳ vọng phải có trong câu trả lời, và mọi cụm "số + đơn vị" trong câu trả lời phải có trong mục nguồn kỳ vọng.
import json
import re
from pathlib import Path
GOLDEN = {g["id"]: g for g in json.loads(Path("golden.json").read_text(encoding="utf-8"))}
CHINH_SACH = { # Chính sách đổi trả hư cấu của BanHang, bản 2026-09
"DT-1": "Khách được đổi hoặc trả hàng trong 7 ngày kể từ ngày nhận hàng. Hàng phải còn tem, nhãn và hóa đơn.",
"DT-2": "Điện thoại, laptop, máy tính bảng lỗi do nhà sản xuất được đổi máy mới trong 7 ngày. "
"Sau đó máy được bảo hành 12 tháng tại hãng.",
"DT-3": "Thành viên BanHang Plus được đổi trả hàng thời trang trong 30 ngày.",
"DT-4": "Tiền hoàn về phương thức thanh toán ban đầu trong 5 ngày làm việc sau khi kho nhận hàng trả.",
"DT-5": "Đổi trả do lỗi của BanHang được miễn phí vận chuyển. Khách đổi ý chịu phí vận chuyển 30.000 đồng.",
"DT-6": "Thực phẩm, đồ lót và thẻ quà tặng không được đổi trả.",
}
# "ngày làm việc" đứng trước "ngày" để regex bắt cụm dài hơn.
SO_LIEU = re.compile(r"(\d+(?:\.\d{3})*)\s*(ngày làm việc|ngày|tháng|đồng)")
def con_so(van_ban):
return {f"{so} {don_vi}" for so, don_vi in SO_LIEU.findall(van_ban.lower())}
def kiem_tra(q, tra_loi):
g = GOLDEN[q]
nguon = " ".join(CHINH_SACH[m] for m in g["muc_ky_vong"])
co = con_so(tra_loi)
thieu = con_so(g["tra_loi_ky_vong"]) - co # số phải có mà câu trả lời không nói
ngoai_nguon = co - con_so(nguon) # số câu trả lời nói mà mục nguồn không có
if not thieu and not ngoai_nguon:
return "ĐẠT"
return f"TRƯỢT thiếu: {sorted(thieu)} ngoài nguồn: {sorted(ngoai_nguon)}"
# Câu trả lời dựng tay theo kiểu trợ lý đã trả lời, không phải log thật.
TRA_LOI = [
("q01", "chunk-200", "Bạn được đổi hoặc trả trong 7 ngày kể từ ngày nhận hàng, nhớ giữ tem và hóa đơn."),
("q01", "chunk-800", "Bạn có 30 ngày để đổi trả áo, miễn là hàng còn tem và nhãn."),
("q03", "chunk-800", "Tiền sẽ về tài khoản của bạn trong 5 ngày sau khi kho nhận hàng."),
("q04", "chunk-800", "Có. Nếu đổi ý, bạn chịu phí vận chuyển 30.000 đồng."),
("q05", "chunk-800", "Không. Điện thoại lỗi được đổi máy mới trong 7 ngày, sau đó bảo hành "
"12 tháng tại hãng. Mức 30 ngày chỉ dành cho hàng thời trang của thành viên Plus."),
]
for q, cau_hinh, tra_loi in TRA_LOI:
print(f"{q} {cau_hinh}: {kiem_tra(q, tra_loi)}")
q01 chunk-200: ĐẠT
q01 chunk-800: TRƯỢT thiếu: ['7 ngày'] ngoài nguồn: ['30 ngày']
q03 chunk-800: TRƯỢT thiếu: ['5 ngày làm việc'] ngoài nguồn: ['5 ngày']
q04 chunk-800: ĐẠT
q05 chunk-800: ĐẠT
q01 với chunk 800 trượt cả hai chiều. Điểm đáng chú ý: "30 ngày" có trong L2, chunk mà retriever đưa lên đầu. Phép đo bám nguồn so với ngữ cảnh đã lấy có thể chấm câu này là bám nguồn, vì con số 30 ngày và việc đổi trả hàng thời trang đều có trong ngữ cảnh, chỉ điều kiện thành viên Plus bị bỏ mất. Luật ở đây so với mục kỳ vọng trong golden set nên bắt được. Đúng (correctness) và bám ngữ cảnh (groundedness) là hai phép đo khác nhau.
q03 trượt vì "5 ngày" khác "5 ngày làm việc". Kho nhận hàng trả vào thứ Sáu thì 5 ngày làm việc hết vào thứ Sáu tuần sau, 5 ngày lịch hết vào thứ Tư. Khách được hứa thứ Tư sẽ gọi CSKH vào thứ Năm.
Luật chỉ thấy số viết bằng chữ số. "Một tuần" hay "bảy ngày" lọt qua, nên chuẩn hóa trước khi so, hoặc yêu cầu trong prompt viết số bằng chữ số. Câu không có số, như "thẻ quà tặng có trả được không", cần ba lớp sau.
Bám nguồn (faithfulness). Ragas định nghĩa Faithfulness = số khẳng định được ngữ cảnh ủng hộ / tổng số khẳng định, thang 0 đến 1, dùng LLM để tách câu trả lời thành khẳng định và kiểm từng khẳng định. Câu trả lời có 4 khẳng định, 3 có trong ngữ cảnh, được 0,75. Như q01 cho thấy, chỉ số này nhắm vào việc model bịa thêm, không nhắm vào việc retriever lấy nhầm đoạn.
LLM chấm (LLM-as-a-judge). Một LLM đọc câu hỏi, câu trả lời, thường kèm đáp án kỳ vọng, rồi kết luận. Zheng và cộng sự (2023) đo các thiên lệch của cách làm này trên MT-Bench, với các model năm 2023:
- Thiên lệch vị trí: khi so hai câu trả lời rồi đổi chỗ chúng, GPT-4 với prompt mặc định chỉ giữ nguyên kết luận ở 65,0% trường hợp, và nghiêng về câu đứng trước ở 30,0%.
- Thiên lệch độ dài: phép thử "repetitive list" viết lại các ý cũ cho dài ra mà không thêm thông tin. Claude-v1 và GPT-3.5 bị lừa ở 91,3% trường hợp, GPT-4 ở 8,7%.
- Tự ưu tiên: GPT-4 cho câu trả lời của chính nó tỷ lệ thắng cao hơn khoảng 10%, Claude-v1 khoảng 25%. Tác giả ghi rằng dữ liệu còn ít để kết luận chắc.
- Chấm toán kém. Trên 20 câu hỏi toán, đưa đáp án tham chiếu cho GPT-4 làm số lần chấm sai giảm từ 14 (70%) xuống 3 (15%).
Các con số trên thuộc về model năm 2023. Với judge mình dùng, phải tự đo lại. BanHang dùng bốn quy tắc. Judge luôn nhận đáp án kỳ vọng và mục nguồn, trả về đạt hoặc trượt kèm khẳng định sai, không chấm thang 1 đến 10. Khi so hai cấu hình theo cặp, chấm cả hai thứ tự và chỉ tính khi hai lần trùng kết luận, như paper làm. Judge dùng model khác model sinh câu trả lời. Trước khi tin judge, cho judge và người cùng chấm một mẫu, đo tỷ lệ đồng ý.
Người chấm mẫu. Mỗi tuần CSKH chấm một mẫu hội thoại production, ví dụ 30 cuộc ngẫu nhiên cộng những cuộc khách bấm "không hữu ích". Mẫu này đo lại độ đồng ý giữa judge và người, và cung cấp câu mới cho golden set.
6. Biến phép đo thành cổng chặn
Bộ đo chỉ có ích nếu chạy trước khi thay đổi lên production. BanHang chạy nó trong CI cho mọi pull request đụng tới chunking, embedding model, top-k, prompt, model sinh câu trả lời, và chính tài liệu chính sách. Pipeline dựng index bằng cấu hình của pull request, chạy golden set trên index đó, rồi so với số của nhánh chính.
| Khi nào | Chạy gì | Kết quả |
|---|---|---|
| Mỗi pull request | Chỉ số retrieval; sinh câu trả lời rồi chạy luật con số, trên toàn bộ golden set | Chặn merge nếu dưới ngưỡng |
| Hằng đêm | Faithfulness và LLM chấm trên toàn bộ golden set | Báo cáo, mở issue khi tụt |
| Hằng tuần | Người chấm mẫu production | Câu mới cho golden set, độ đồng ý của judge |
Ngưỡng có hai loại. Sàn tuyệt đối như trong script mục 4: hit@3 ≥ 0,95 và MRR@5 ≥ 0,85, chọn từ số của cấu hình đang chạy ổn, không phải chuẩn chung. Và một luật không khoan nhượng: mọi câu có con số về thời hạn hoặc tiền phải đạt luật con số. Luật là regex: cùng một câu trả lời luôn cho cùng kết luận, và không có thiên lệch vị trí hay độ dài.
Golden set có phiên bản, nằm cùng repo với code, sửa qua pull request do người phụ trách chính sách duyệt. Báo cáo eval ghi phiên bản golden set, vì số của hai phiên bản khác nhau không so trực tiếp được. Khi chính sách đổi, ví dụ 7 ngày thành 10 ngày, tài liệu và golden set đổi trong cùng một pull request. Tách ra thì cổng sẽ chặn đúng thay đổi cần làm.
Mỗi lỗi production thành một câu mới. q01 đi theo đúng quy trình đó: CSKH báo lỗi, nhóm thêm câu với nguon là su-co-2026-09-28, chạy eval trên cấu hình lỗi để thấy nó trượt, rồi mới sửa. Giống viết test tái hiện bug trước khi sửa bug.
Tinh chỉnh prompt nhiều vòng trên cùng 50 câu thì điểm tăng mà hệ chưa chắc tốt hơn. Giữ riêng khoảng 10 câu chỉ để chấm, không dùng khi tinh chỉnh, và làm mới tập từ log chat mỗi tháng.
Thư viện mã nguồn mở Ragas (giấy phép Apache-2.0) có sẵn các chỉ số như Faithfulness, Context Precision, Context Recall, Response Relevancy. Các chỉ số đó gọi LLM bên trong, nên mang theo các thiên lệch ở mục 5. Golden set, ngưỡng và cổng chặn vẫn là việc của nhóm.
Những chỗ hay hiểu sai
- "recall@k cao là retrieval tốt." recall@k không nhìn thứ tự trong top-k và tự tăng khi chunk to ra. Xem cùng MRR và số token ngữ cảnh.
- "Câu trả lời bám ngữ cảnh là câu trả lời đúng." Retriever lấy nhầm đoạn thì câu sai vẫn có thể bám ngữ cảnh, như "30 ngày" ở
q01. Phải so với nguồn kỳ vọng. - "LLM chấm thay được người chấm." Judge có thiên lệch vị trí, độ dài, tự ưu tiên. Người chấm mẫu định kỳ là cách kiểm tra judge.
- "Golden set dựng một lần là xong." Chính sách đổi, cách khách hỏi đổi. Tập phải có phiên bản và lớn lên theo lỗi production.
Đọc tiếp
- Embedding và tìm kiếm ngữ nghĩa tiếng Việt: phần retrieval mà bài này đo.
- Điều tra một truy vấn chậm từ cảnh báo đến bản sửa: cùng cách làm, định nghĩa triệu chứng bằng số trước khi sửa.
Nguồn
- Lianmin Zheng và cộng sự, Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena, NeurIPS 2023 Datasets and Benchmarks. Mục 3.3 và 3.4, bảng 2, 3, 4.
- Shahul Es và cộng sự, Ragas: Automated Evaluation of Retrieval Augmented Generation, 2023.
- Tài liệu Ragas: Faithfulness, danh sách chỉ số, mã nguồn vibrantlabsai/ragas.
- Ellen M. Voorhees, Dawn M. Tice, Building a Question Answering Test Collection, SIGIR 2000: điểm reciprocal rank của TREC-8, độ ổn định khi người chấm bất đồng.
- Christopher D. Manning, Prabhakar Raghavan, Hinrich Schütze, Introduction to Information Retrieval, mục 8.4, Evaluation of ranked retrieval results: định nghĩa NDCG.