Xây dựng một hệ thống RAG (Retrieval-Augmented Generation) hoàn chỉnh gồm 5 bước chính: ingestion & chunking dữ liệu, tạo embedding, lưu trữ vào vector database, truy xuất (retrieval) kèm re-ranking, và sinh câu trả lời (generation) có kiểm soát bằng prompt. Bài viết này ghép nối các khái niệm đã trình bày ở RAG là gì, RAG vs Fine-tuning và Vector Database là gì thành một pipeline thực chiến, có code Python và checklist đánh giá chất lượng. Một lưu ý quan trọng trước khi bắt đầu: RAG không phải là "search + generate" đơn giản. Chunking sai lệch, thiếu re-ranking, hoặc bỏ qua đánh giá là ba nguyên nhân phổ biến nhất khiến hệ thống RAG production trả lời sai dù dùng đúng mô hình LLM.
Một pipeline RAG production gồm hai luồng tách biệt: Luồng nạp dữ liệu (Ingestion — chạy offline/định kỳ):
Tài liệu gốc → Parse & làm sạch → Chunking → Embedding → Lưu vào Vector DB
Luồng truy vấn (Query — chạy real-time):
Câu hỏi người dùng → Embedding câu hỏi → Retrieval (top-k) → Re-ranking → Ghép context vào prompt → LLM sinh câu trả lời
Với dữ liệu định dạng phức tạp (PDF, HTML, DOCX, PPTX), nên dùng công cụ parse chuyên dụng như Unstructured.io hoặc Apache Tika thay vì tự viết parser — đây là cách tiếp cận được khuyến nghị trong kiến trúc RAG production tham chiếu năm 2026, vì việc chuẩn hóa định dạng đầu vào ảnh hưởng trực tiếp đến chất lượng chunking phía sau.
Nhiều đội ngũ dành nhiều ngày tinh chỉnh embedding model và vector database nhưng lại bỏ mặc chunking ở cấu hình mặc định — đây là lý do phổ biến khiến chất lượng retrieval ở mức trung bình dù các thành phần khác đã tối ưu. Mức độ ảnh hưởng của chunking lớn đến mức nào? Một nghiên cứu lâm sàng công bố trên PMC Bioengineering (2025) đã dựng 4 pipeline RAG giống hệt nhau — cùng mô hình Gemini 1.0 Pro, cùng dữ liệu, cùng prompt — chỉ khác nhau ở chiến lược chunking. Kết quả: độ chính xác dao động từ 13% đến 87% trên cùng bộ câu hỏi hậu phẫu, chênh lệch tuyệt đối 74 điểm phần trăm chỉ do một biến số duy nhất.
| Chiến lược | Cách hoạt động | Ưu điểm | Khi nào dùng |
|---|---|---|---|
| Fixed-size | Cắt theo số ký tự/token cố định | Đơn giản, nhanh | Baseline, prototype nhanh |
| Recursive character splitting | Thử phân tách theo thứ tự: đoạn văn → dòng → câu → khoảng trắng | Tôn trọng cấu trúc văn bản gốc | Mặc định của LangChain, dùng phổ biến nhất trong production |
| Semantic chunking | Dùng embedding để đo độ tương đồng, cắt tại điểm chủ đề thay đổi | Giữ trọn vẹn ý nghĩa một chủ đề trong một chunk | Văn bản có mạch ý phức tạp, tài liệu kỹ thuật |
| Hierarchical / Parent-child chunking | Index chunk nhỏ để tìm chính xác, nhưng trả về chunk cha lớn hơn cho LLM | Vừa chính xác khi tìm, vừa đủ ngữ cảnh khi sinh câu trả lời | Pattern được áp dụng rộng rãi nhất giai đoạn 2025-2026, xử lý tốt câu hỏi multi-hop |
| Agentic / LLM-based chunking | Dùng LLM đọc và tự xác định ranh giới logic | Nhận diện được chuỗi nhân quả mà rule-based không thấy được | Domain rủi ro cao (y tế, pháp lý), ngân sách cho phép chi phí xử lý cao hơn |
Kích thước chunk tối ưu phụ thuộc vào loại tài liệu: văn bản pháp lý phù hợp với chunk cấp đoạn văn (300-500 token), trong khi tài liệu kỹ thuật thường hoạt động tốt hơn với chunk cấp mục lớn (1.000-2.000 token). Về overlap, khuyến nghị phổ biến là 10-20% (ví dụ chunk 500 token thì overlap 50-100 token) để tránh mất ngữ cảnh ở ranh giới. Tuy nhiên, một phân tích hệ thống công bố tháng 1/2026 dùng retrieval SPLADE kết hợp Mistral-8B trên bộ dữ liệu Natural Questions lại không tìm thấy lợi ích đo được từ overlap, trong khi chi phí index tăng lên — vì vậy nên thử nghiệm với chính dữ liệu của bạn thay vì áp dụng con số mặc định một cách máy móc. Công thức khởi điểm cho phần lớn dự án (đáp ứng khoảng 80% use case): semantic chunking + overlap 10-15% + gắn metadata (nguồn, section, loại tài liệu) + top-k 3-5 + kết hợp re-ranking hoặc MMR.
Việc chọn embedding model ảnh hưởng đến chất lượng retrieval nhiều hơn phần lớn đội ngũ nhận ra. Các mô hình thế hệ mới như text-embedding-3, Voyage voyage-3, BGE, và E5 cho kết quả khác biệt đáng kể trên các tác vụ retrieval theo từng domain cụ thể — không có mô hình nào "tốt nhất" cho mọi loại dữ liệu, nên cần benchmark trên chính tập dữ liệu của bạn trước khi chốt.
python
from openai import OpenAI
client = OpenAI()
def embed_text(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text,
)
return response.data[0].embedding
Phần này đã được trình bày chi tiết ở bài Vector Database là gì — tóm tắt nhanh: dự án dưới 10 triệu vector và đã dùng Postgres nên chọn pgvector; cần filter metadata mạnh và tự vận hành thì chọn Qdrant; cần hybrid search có sẵn thì chọn Weaviate; quy mô trên 100 triệu vector thì chọn Milvus. python
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
client = QdrantClient(url="http://localhost:6333")
client.upsert(
collection_name="rag_documents",
points=[
PointStruct(
id=chunk_id,
vector=embed_text(chunk_text),
payload={"content": chunk_text, "source": doc_source, "section": section_title},
)
],
)
Retrieval thuần vector search thường bỏ sót các câu hỏi chứa tên riêng, mã sản phẩm, hoặc thuật ngữ chính xác — trong khi tìm kiếm từ khóa (BM25) lại bỏ sót các câu hỏi diễn đạt khác cách nhưng cùng nghĩa. Hybrid search kết hợp cả hai đã trở thành tiêu chuẩn mặc định cho RAG production năm 2026, không còn là tùy chọn nâng cao. Re-ranking là kỹ thuật bị đánh giá thấp nhất trong các pipeline RAG production: thêm một cross-encoder re-ranker sau bước retrieval ban đầu thường cải thiện độ chính xác câu trả lời 15-25%, nhưng chưa đến 30% hệ thống RAG production hiện tại có áp dụng bước này. Các lựa chọn phổ biến hiện nay gồm Cohere Rerank v3 và các mô hình cross-encoder của Jina. python
# Retrieval thô: lấy top-20 ứng viên bằng vector search
candidates = client.search(
collection_name="rag_documents",
query_vector=embed_text(user_question),
limit=20,
)
# Re-rank bằng cross-encoder để chọn top-5 chính xác nhất
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
pairs = [(user_question, c.payload["content"]) for c in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
top_chunks = [c.payload["content"] for c, _ in ranked[:5]]
Nguyên tắc cốt lõi: LLM chỉ nên trả lời dựa trên context được cung cấp, không được "đoán" thông tin ngoài phạm vi đó. Prompt cần nêu rõ ràng buộc này và yêu cầu mô hình thừa nhận khi không tìm thấy câu trả lời trong context, thay vì bịa đặt. python
SYSTEM_PROMPT = """Bạn là trợ lý trả lời câu hỏi dựa trên tài liệu được cung cấp.
Chỉ sử dụng thông tin trong phần NGỮ CẢNH bên dưới để trả lời.
Nếu ngữ cảnh không đủ thông tin, hãy nói rõ là không tìm thấy, không suy đoán.
Khi trả lời, trích dẫn nguồn tài liệu tương ứng."""
def build_prompt(question: str, chunks: list[str]) -> str:
context = "nn---nn".join(chunks)
return f"""NGỮ CẢNH:
{context}
CÂU HỎI: {question}
TRẢ LỜI:"""
Ước tính khoảng 70% đội ngũ kỹ thuật đã triển khai hoặc dự kiến triển khai RAG production, nhưng phần lớn trong số đó không có hệ thống đánh giá bài bản — các kỹ thuật đo lường văn bản truyền thống như BLEU hay ROUGE gần như không phản ánh được việc câu trả lời có thực sự bám sát ngữ cảnh truy xuất hay không. RAG có hai điểm có thể sai: retrieval (tìm đúng tài liệu chưa?) và generation (dựa vào tài liệu đó, câu trả lời có đúng không?) — cần đo cả hai, không chỉ đo generation.
| Chỉ số | Đo điều gì | Ngưỡng tham khảo |
|---|---|---|
| Faithfulness | Câu trả lời có bám sát ngữ cảnh truy xuất không (tách từng claim để kiểm chứng) | Trên 0.85 là tốt; dưới 0.70 cần điều tra hallucination |
| Context precision | Trong số chunk truy xuất được, bao nhiêu phần thực sự liên quan | Càng cao càng ít nhiễu trong prompt |
| Context recall | Trong số thông tin cần thiết, retrieval có lấy đủ không | Thấp → retriever bỏ sót tài liệu quan trọng |
| Answer relevancy | Câu trả lời có đúng trọng tâm câu hỏi không | Faithfulness cao nhưng relevancy thấp → mô hình đúng dữ liệu nhưng lạc đề |
Một lưu ý thực tế đáng chú ý: một hệ thống RAG hỏi-đáp pháp lý từng đạt faithfulness 0.91 trên bộ test offline khi ra mắt, nhưng sau ba tuần vận hành, người dùng phản ánh cứ 6 câu trả lời thì có 1 câu bỏ sót điều luật quan trọng — trong khi chỉ số faithfulness trên dashboard vẫn giữ nguyên 0.91. Nguyên nhân hóa ra nằm ở context recall chỉ đạt 0.62: retriever bỏ sót điều luật thứ hai trong câu hỏi multi-hop, còn generator vẫn trả lời mạch lạc dựa trên phần ngữ cảnh không đầy đủ. Đây là lý do vì sao chỉ đo faithfulness (tầng generation) là không đủ — cần theo dõi song song các chỉ số tầng retrieval.
python
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
eval_dataset = Dataset.from_dict({
"question": questions,
"answer": generated_answers,
"contexts": retrieved_contexts,
"ground_truth": reference_answers,
})
results = evaluate(
eval_dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(results)
Cách tiếp cận thực tế cho đội ngũ nhỏ: bắt đầu đo faithfulness và context recall trên một bộ khoảng 50 câu hỏi mẫu ("golden dataset"), đặt ngưỡng chấp nhận, rồi tích hợp bước đánh giá này vào CI/CD để phát hiện regression trước khi ảnh hưởng người dùng thật — thay vì chỉ kiểm tra bằng mắt một vài ví dụ, cách làm này không mở rộng được khi hệ thống lớn lên.
Bài viết thuộc chuỗi nội dung RAG & AI. Xem thêm: RAG là gì? · RAG vs Fine-tuning · Vector Database là gì?