Chào mừng bạn đến với bài học cuối cùng trong lộ trình Kỹ Sư AI Thực Chiến. Sau khi đã nghiên cứu, lập trình và tinh chỉnh mô hình ngôn ngữ lớn thành công, bước thử thách cuối cùng để đưa sản phẩm trí tuệ nhân tạo ra thế giới là đưa hệ thống vận hành trên môi trường Production (MLOps).
Đối với ứng dụng Generative AI, MLOps giải quyết 3 bài toán sống còn: Serving mô hình hiệu năng cao chịu tải lớn, Đánh giá chất lượng tự động hóa, và Giám sát gỡ lỗi thời gian thực. Bài học này sẽ hướng dẫn bạn cơ chế quản lý bộ nhớ đệm PagedAttention của vLLM, cách thiết lập bộ chỉ số đánh giá RAG tự động bằng Ragas, cơ chế ghi log suy luận với Phoenix Tracing, và tự lập trình hệ thống đánh giá RAG bằng Python để xuất báo cáo chất lượng.
bge-m3 và một mô hình chat — cùng thiết lập của Bài 16.
Kiến thức cần có: Bài 14 tới 16 cho toàn bộ đường ống RAG mà bài này đi chấm điểm. Bài 16 cho ý tưởng dùng LLM chấm điểm (Cross-Encoder) — Faithfulness ở đây dùng đúng kỹ thuật đó.
20.1 Serving mô hình hiệu năng cao với vLLM
Nếu ta deploy mô hình LLM lên server bằng mã nguồn thô của thư viện Transformers (Hugging Face), hệ thống sẽ chạy cực kỳ chậm và nhanh chóng bị lỗi tràn bộ nhớ VRAM (Out of Memory - OOM). Nguyên nhân chính là do kích thước của bộ đệm Key-Value (KV Cache) lưu trữ các token trung gian của mỗi request phình to liên tục theo thời gian thực và phân bổ bộ nhớ bị phân mảnh.
Thư viện vLLM giải quyết triệt để vấn đề này bằng thuật toán PagedAttention (lấy cảm hứng từ cơ chế Quản lý Bộ nhớ ảo phân trang của Hệ điều hành):
- Phân trang KV Cache: PagedAttention phân chia KV Cache của mỗi request thành các khối bộ nhớ cố định (Blocks). Các khối này không cần nằm liên tiếp nhau trên VRAM vật lý.
- Bảng tra cứu trang (Page Table): Hệ thống duy trì bảng ánh xạ các token logic của request với các block vật lý, cho phép chia sẻ các khối KV Cache chung giữa các request song song (ví dụ: chia sẻ phần prompt hệ thống hoặc lịch sử hội thoại chung).
- Tối ưu hóa: Nhờ loại bỏ phân mảnh bộ nhớ, vLLM có thể ép sát hiệu năng VRAM vật lý, giúp tăng tốc độ suy luận (Throughput) lên từ 10 đến 20 lần so với thông thường.
gpu_memory_utilization = 0.90. Nếu bạn deploy vLLM chung
trên một máy chủ đang chạy các tác vụ khác (ví dụ: chạy thêm ứng dụng nhúng embeddings hoặc FastAPI), hệ
thống sẽ lập tức crash OOM. Giải pháp: Cần cấu hình giới hạn chỉ số này phù hợp (ví dụ: 0.70 hoặc 0.80)
để chừa khoảng trống bộ nhớ dự phòng cho các ứng dụng phụ trợ chạy ổn định.
20.2 Đánh giá hệ thống RAG tự động bằng Ragas
Đối với ứng dụng RAG, việc cử con người ngồi đọc hàng vạn câu trả lời của mô hình để chấm điểm chất lượng là vô cùng tốn kém và không khả thi. Do đó, bộ khung đánh giá tự động Ragas (Retrieval Augmented Generation Assessment) đề xuất phương án sử dụng LLM làm giám khảo đánh giá dựa trên 3 chỉ số cốt lõi:
| Chỉ số (Metric) | Bản chất đo lường | Công thức / Cơ chế |
|---|---|---|
| Faithfulness | Độ trung thực của câu trả lời so với ngữ cảnh tài liệu tìm thấy (Tránh ảo tưởng). | Tỷ lệ các khẳng định trong câu trả lời có thể suy diễn trực tiếp từ Context: \[Faithfulness = \frac{\text{Số khẳng định có cơ sở}}{\text{Tổng số khẳng định trong câu trả lời}}\] |
| Answer Relevance | Mức độ câu trả lời giải quyết trực tiếp và đúng trọng tâm câu hỏi của người dùng. | Tính trung bình Cosine Similarity giữa embedding của câu hỏi gốc và embedding của các câu hỏi giả lập do LLM sinh ngược lại từ câu trả lời. |
| Context Recall | Khả năng của bộ truy xuất tìm kiếm đầy đủ thông tin từ tài liệu gốc (Ground Truth). | Tỷ lệ các thông tin chính trong Ground Truth được phủ trong Context tìm được: \[Context Recall = \frac{\text{Số sự thật trong Ground Truth được phủ}}{\text{Tổng số sự thật trong Ground Truth}}\] |
20.3 Tracing & Giám sát luồng suy luận với Phoenix
Khi một Agent hoạt động sai hoặc đưa ra câu trả lời lạc đề, rất khó để biết lỗi xảy ra ở bước nào (do RAG quét sai tài liệu, do LLM dịch sai câu hỏi, hay do Agent gọi nhầm công cụ). Chúng ta cần một hệ thống Tracing (Theo vết) giống như "hộp đen máy bay" để ghi chép lại mọi hành động.
Tích hợp thư viện Arize Phoenix hoặc LangSmith cho phép ứng dụng tự động ghi nhận:
- Thời gian thực thi (Latency) của từng Node.
- Số lượng Token tiêu thụ ở mỗi bước (để kiểm soát chi phí API).
- Chuỗi Prompt thô và kết quả phản hồi thô ở từng lượt gọi LLM phục vụ khâu gỡ lỗi (Debugging).
20.4 Dự án thực hành bài 20: Bộ đánh giá RAG — và phép đánh giá chính bộ đánh giá
Đây là bài cuối của lộ trình, nên dự án khép lại bằng câu hỏi khó nhất mà một kỹ sư AI phải trả lời: làm sao biết hệ thống của mình thật sự tốt? Và ngay sau đó là câu khó hơn nữa: làm sao biết cái thước đo của mình có đo được gì không?
Tập kiểm thử của dự án có đúng hai mẫu, và điều đó là cố ý: một câu trả lời đúng, và một câu trả lời sai rành rành (hỏi định nghĩa LoRA, trả lời rằng LoRA là kỹ thuật tăng temperature). Một chỉ số mà bạn chưa bao giờ thấy nó thất bại là một chỉ số bạn không có lý do gì để tin.
"""Lesson 20 project: a RAG evaluator, and an evaluation of the evaluator.
Run: python3 rag_evaluator.py
Optional: Ollama with `bge-m3` and a chat model, for the semantic metrics.
The test set deliberately contains one CORRECT answer and one that is plainly
WRONG. That is the whole design: a metric you have never seen fail is a metric
you have no reason to trust. Every score below is checked against which sample
it belongs to.
"""
import json
import math
import re
import urllib.error
import urllib.request
OLLAMA = "http://localhost:11434"
CHAT_PREFERRED = ["qwen2.5:14b-instruct", "qwen2.5:7b", "llama3.1", "llama3.2"]
EMBED_PREFERRED = ["bge-m3", "nomic-embed-text", "mxbai-embed-large"]
TEST_DATASET = [
{
"label": "correct answer",
"question": "Thuật toán PagedAttention trong vLLM dùng để làm gì?",
"context": "vLLM sử dụng thuật toán PagedAttention để phân chia bộ đệm Key-Value "
"(KV Cache) thành các khối cố định trên bộ nhớ VRAM, giúp loại bỏ hiện "
"tượng phân mảnh bộ nhớ vật lý.",
"answer": "PagedAttention dùng để phân chia bộ đệm KV Cache thành các khối bộ nhớ "
"cố định giúp tối ưu hóa VRAM và tránh phân mảnh.",
"ground_truth": "PagedAttention dùng để giải quyết vấn đề phân mảnh bộ nhớ KV Cache "
"bằng cách chia nhỏ nó thành các khối cố định trên GPU VRAM.",
},
{
"label": "WRONG answer",
"question": "Định nghĩa kỹ thuật LoRA?",
"context": "LoRA là kỹ thuật đóng băng ma trận trọng số gốc của LLM và đưa vào các "
"ma trận hạng thấp song song để huấn luyện, giúp giảm hàng trăm lần số "
"lượng tham số cập nhật.",
"answer": "LoRA là kỹ thuật tăng nhiệt độ temperature để mô hình ngôn ngữ lớn hoạt "
"động sáng tạo hơn.",
"ground_truth": "LoRA là kỹ thuật tinh chỉnh tham số hiệu quả bằng cách huấn luyện "
"các ma trận phân rã hạng thấp song song và đóng băng trọng số gốc.",
},
]
STOPWORDS = {"và", "để", "của", "là", "trong", "cho", "có", "các", "được",
"bằng", "với", "ra"}
# ---------------------------------------------------------------------------
# Keyword metrics - cheap, no dependencies, and worth measuring critically
# ---------------------------------------------------------------------------
def keywords(text):
return {w for w in re.findall(r"\b\w+\b", text.lower()) if w not in STOPWORDS}
def keyword_faithfulness(answer, context):
"""What share of the answer's words appear in the retrieved context?"""
answer_words = keywords(answer)
return len(answer_words & keywords(context)) / len(answer_words) if answer_words else 0.0
def keyword_context_recall(context, ground_truth):
"""What share of the ground truth's words the retrieved context covers."""
truth_words = keywords(ground_truth)
return len(truth_words & keywords(context)) / len(truth_words) if truth_words else 0.0
def keyword_answer_relevance(question, answer):
"""Jaccard overlap between question and answer words."""
q, a = keywords(question), keywords(answer)
return len(q & a) / len(q | a) if (q | a) else 0.0
# ---------------------------------------------------------------------------
# Semantic metrics - what Ragas actually does
# ---------------------------------------------------------------------------
def post(path, payload):
request = urllib.request.Request(
f"{OLLAMA}{path}", data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(request) as response:
return json.loads(response.read())
def installed_models():
try:
with urllib.request.urlopen(f"{OLLAMA}/api/tags", timeout=5) as response:
return [m["name"] for m in json.loads(response.read())["models"]]
except urllib.error.URLError:
return []
def pick(names, preferred):
for wanted in preferred:
for name in names:
if name == wanted or name.startswith(wanted):
return name
return None
def embed(text, model):
return post("/api/embeddings", {"model": model, "prompt": text})["embedding"]
def cosine(v1, v2):
dot = sum(a * b for a, b in zip(v1, v2))
n1 = math.sqrt(sum(a * a for a in v1))
n2 = math.sqrt(sum(b * b for b in v2))
return 0.0 if n1 == 0 or n2 == 0 else dot / (n1 * n2)
def semantic_answer_relevance(question, answer, model):
"""Cosine between the question and the answer in embedding space."""
return cosine(embed(question, model), embed(answer, model))
def llm_faithfulness(answer, context, model):
"""LLM as judge: is every claim in the answer supported by the context?"""
prompt = (
f'Ngữ cảnh: "{context}"\n'
f'Câu trả lời: "{answer}"\n\n'
"Mọi khẳng định trong câu trả lời có được suy ra trực tiếp từ ngữ cảnh không? "
"Chỉ trả về DUY NHẤT một số nguyên từ 0 đến 10, không giải thích."
)
payload = {"model": model, "stream": False, "options": {"temperature": 0.0},
"messages": [{"role": "user", "content": prompt}]}
reply = post("/api/chat", payload)["message"]["content"]
match = re.search(r"\d+", reply)
return int(match.group()) / 10 if match else 0.0
# ---------------------------------------------------------------------------
# Evaluating the evaluator
# ---------------------------------------------------------------------------
def report_table(title, rows, note=None):
print(f"=== {title} ===")
print(f" {'sample':<16}{'faithfulness':>14}{'context recall':>16}{'answer relevance':>18}")
for label, faith, recall, relevance in rows:
print(f" {label:<16}{faith:>13.2%} {recall:>15.2%} {relevance:>17.2%}")
if note:
print(f" {note}")
print()
def main():
print("The test set holds one correct answer and one that is plainly wrong.")
print("A metric that cannot tell them apart is not measuring anything.\n")
keyword_rows = [
(s["label"],
keyword_faithfulness(s["answer"], s["context"]),
keyword_context_recall(s["context"], s["ground_truth"]),
keyword_answer_relevance(s["question"], s["answer"]))
for s in TEST_DATASET
]
report_table("Metrics computed from word overlap only", keyword_rows)
good, bad = keyword_rows[0], keyword_rows[1]
print("=== Does each keyword metric separate right from wrong? ===")
# Context recall is excluded on purpose: it scores the RETRIEVER (did the
# context cover the ground truth), so it has no opinion about the answer.
for index, name in ((1, "faithfulness "), (3, "answer relevance")):
gap = good[index] - bad[index]
verdict = "ok" if gap > 0 else "INVERTED - scores the wrong answer higher"
print(f" {name}: {good[index]:.2%} vs {bad[index]:.2%}"
f" gap {gap:+.2%} {verdict}")
print(f" context recall : {good[2]:.2%} vs {bad[2]:.2%}"
f" not applicable - it grades the retriever, not the answer")
print(" Faithfulness works: the wrong answer shares almost no words with its")
print(" context. Answer relevance is inverted - the metric is broken.\n")
names = installed_models()
embed_model = pick(names, EMBED_PREFERRED)
chat_model = pick(names, CHAT_PREFERRED)
if not embed_model or not chat_model:
print("Ollama with an embedding model and a chat model is needed for the")
print("semantic metrics. Try: ollama pull bge-m3 && ollama pull qwen2.5:7b")
return
print(f"embedding model: {embed_model}\nchat model: {chat_model}\n")
print("=== The same two metrics, computed the way Ragas actually does ===")
for sample in TEST_DATASET:
relevance = semantic_answer_relevance(sample["question"], sample["answer"],
embed_model)
faithfulness = llm_faithfulness(sample["answer"], sample["context"],
chat_model)
print(f" {sample['label']:<16} relevance {relevance:.4f}"
f" faithfulness {faithfulness:.2f}")
print()
print("=== What the numbers say ===")
print(" Embedding relevance puts the correct answer back on top, but only")
print(" just. That is the right behaviour, not a weakness: the wrong answer")
print(" IS about LoRA, it is simply false. Relevance asks 'does this address")
print(" the question', never 'is this true'.")
print(" Falsity is faithfulness's job, and that is where the gap is wide.")
print(" Pick the metric that can fail on the defect you actually fear.")
if __name__ == "__main__":
main()
Ba chỉ số tính bằng độ trùng từ khóa
=== Metrics computed from word overlap only ===
sample faithfulness context recall answer relevance
correct answer 75.00% 60.87% 8.00%
WRONG answer 17.65% 69.57% 15.79%
Nhìn bảng này một mình thì thấy ổn: có số, có phần trăm, xuất được báo cáo. Nhưng câu hỏi đúng phải là: chỉ số nào phân biệt được câu đúng với câu sai?
=== Does each keyword metric separate right from wrong? ===
faithfulness : 75.00% vs 17.65% gap +57.35% ok
answer relevance: 8.00% vs 15.79% gap -7.79% INVERTED - scores the wrong answer higher
context recall : 60.87% vs 69.57% not applicable - it grades the retriever, not the answer
Faithfulness works: the wrong answer shares almost no words with its
context. Answer relevance is inverted - the metric is broken.
Nếu bạn triển khai bộ đánh giá này lên CI và đặt ngưỡng "Answer Relevance phải trên 10%", bạn vừa dựng một cái cổng chặn câu trả lời đúng và cho câu trả lời sai đi qua. Và nó sẽ chạy im lặng như thế hàng tháng trời.
Còn Context Recall thì không phải bị đảo — nó đơn giản là không trả lời câu hỏi này. Chỉ số đó đo xem đoạn ngữ cảnh tìm được có phủ được thông tin trong đáp án chuẩn hay không, tức nó chấm bộ truy xuất chứ không chấm câu trả lời. Đưa nó vào cùng một bảng với hai chỉ số kia rồi lấy trung bình cộng như bản đầu của dự án là trộn hai thứ đo hai bộ phận khác nhau vào một con số duy nhất.
Cũng hai chỉ số đó, tính theo đúng cách Ragas làm
Mục 20.2 định nghĩa Answer Relevance bằng Cosine Similarity giữa embedding, và Faithfulness bằng
tỷ lệ khẳng định suy ra được từ ngữ cảnh — tức cần một mô hình đọc hiểu, không phải phép đếm từ. Dự án cài
đặt đúng như vậy: embedding bge-m3 cho Relevance, và LLM chấm điểm cho Faithfulness (đúng kỹ
thuật Cross-Encoder của Bài 16):
=== The same two metrics, computed the way Ragas actually does ===
correct answer relevance 0.7254 faithfulness 0.80
WRONG answer relevance 0.6965 faithfulness 0.00
=== What the numbers say ===
Embedding relevance puts the correct answer back on top, but only
just. That is the right behaviour, not a weakness: the wrong answer
IS about LoRA, it is simply false. Relevance asks 'does this address
the question', never 'is this true'.
Falsity is faithfulness's job, and that is where the gap is wide.
Pick the metric that can fail on the defect you actually fear.
Việc phát hiện sai sự thật là nhiệm vụ của Faithfulness, và đúng ở đó khoảng cách mới rộng: $0.80$ so với $0.00$. Bài học vận hành: đừng chọn chỉ số vì nó nghe hay, hãy chọn chỉ số có thể thất bại đúng trên loại lỗi mà bạn thật sự sợ.
Cách chạy dự án này trên máy bạn
-
python3 rag_evaluator.py. Phần chỉ số từ khóa chạy được ngay. Muốn có phần đối chứng ngữ nghĩa thì bật Ollama kèmollama pull bge-m3và một mô hình chat. - Các con số từ khóa sẽ giống hệt trong bài (thuật toán tất định). Điểm Faithfulness do LLM chấm thì tùy mô hình — cái phải giống là khoảng cách rộng giữa câu đúng và câu sai.
-
Rồi thử phá nó theo ba cách:
-
Thêm một mẫu thứ ba vào
TEST_DATASET: một câu trả lời đúng sự thật nhưng lạc đề (trả lời về PagedAttention cho câu hỏi về LoRA). Xem Faithfulness và Relevance tách vai ra sao — đây là trường hợp Relevance phải bắt được còn Faithfulness thì không. -
Bỏ danh sách
STOPWORDS. Mọi chỉ số từ khóa đổi số ngay, vì các từ nối như "của", "là" bắt đầu được tính. Một chỉ số mà kết quả phụ thuộc vào danh sách stopword bạn tự gõ tay thì không nên đem so với một ngưỡng cố định. - Sửa câu trả lời ở mẫu 2 thành một câu đúng. Cả hai chỉ số phải đảo chiều theo. Nếu bạn đổi dữ liệu mà điểm số không đổi, chỉ số của bạn không hề đọc dữ liệu — đúng bài học của Bài 12.
-
Thêm một mẫu thứ ba vào
Tóm tắt bài học & Kết thúc lộ trình
-
Đạt được: Hiểu cơ chế PagedAttention của vLLM và vì sao cấu hình
gpu_memory_utilizationmặc định dễ gây OOM khi dùng chung máy chủ. - Đạt được: Tự cài đặt ba chỉ số Ragas theo cả hai cách — đếm từ khóa và ngữ nghĩa thật — rồi đối chiếu chúng trên một tập kiểm thử có cài sẵn một câu trả lời sai.
- Đạt được: Phát hiện chỉ số Answer Relevance kiểu Jaccard chấm câu sai ($15.79\%$) cao gấp đôi câu đúng ($8.00\%$) — và hiểu vì sao một bộ đánh giá chưa được kiểm thì còn nguy hiểm hơn không có bộ đánh giá nào.
- Đạt được: Phân biệt được Relevance đo "có đúng trọng tâm không" còn Faithfulness mới đo "có đúng sự thật không", và chọn chỉ số theo loại lỗi mình sợ.
Kết thúc lộ trình: Bạn đã đi trọn 20 bài, từ cú pháp Python đầu tiên tới một hệ thống AI có RAG, có Agent, có mô hình tinh chỉnh và có bộ đánh giá tự động. Nếu có một điều đáng mang theo từ cả lộ trình này, thì đó là điều bài cuối vừa chứng minh: mọi con số đều phải được đặt câu hỏi, kể cả con số do chính bạn viết ra. Một hệ thống AI không có phép kiểm biết cách thất bại thì không phải một hệ thống đang hoạt động — nó chỉ là một hệ thống chưa ai phát hiện ra là hỏng.
Tải file code thực hành minh họa bài học
File Python rag_evaluator.py — mã nguồn thuật toán so khớp thực thể và từ khóa giao thoa tự
động tính toán điểm số Faithfulness, Context Recall và Answer Relevance (chạy
python rag_evaluator.py):
📖 Tài liệu tham khảo
- vLLM GitHub Repository — Thư viện serving mô hình ngôn ngữ lớn hiệu năng cao hàng đầu thế giới với thuật toán PagedAttention
- Ragas: Evaluation Framework for RAG — Thư viện mã nguồn mở chuyên dụng để đánh giá tự động các đường ống RAG
- Arize Phoenix Tracing — Công cụ mã nguồn mở giúp quan sát, tracing prompt, token usage và gỡ lỗi AI Agents
Bình luận