Bài 16: RAG — Retrieval Augmented Generation từ A đến Z
1. Vấn đề của LLM thuần túy
LLM như GPT-4 hay Claude là những mô hình cực kỳ mạnh mẽ, nhưng chúng mang theo ba giới hạn cốt lõi khi ứng dụng vào thực tế:
Hallucination (Ảo giác): LLM không "biết" theo nghĩa tra cứu — chúng sinh ra văn bản dựa trên xác suất thống kê. Khi không có thông tin chắc chắn, chúng có xu hướng tạo ra câu trả lời nghe có vẻ đúng nhưng thực tế sai.
Knowledge Cutoff (Giới hạn thời gian): Dữ liệu training có ngày cắt. GPT-4o có thể không biết sự kiện xảy ra sau tháng 4/2024. Đây là vấn đề nghiêm trọng với các lĩnh vực thay đổi nhanh như tài chính, pháp luật, y tế.
Private Data (Dữ liệu nội bộ): Tài liệu công ty, codebase nội bộ, email, database riêng — tất cả đều không có trong training data. LLM hoàn toàn "mù" với thông tin này.
RAG ra đời để giải quyết cả ba vấn đề trên.
2. RAG là gì và tại sao hiệu quả?
Retrieval-Augmented Generation (RAG) là kiến trúc kết hợp giữa information retrieval (tìm kiếm thông tin) và text generation (sinh văn bản). Thay vì dựa hoàn toàn vào parametric memory (kiến thức bên trong weights), RAG cung cấp cho LLM non-parametric memory — kho tài liệu bên ngoài có thể cập nhật liên tục.
Tại sao RAG hiệu quả hơn fine-tuning cho nhiều use case?
| Tiêu chí | RAG | Fine-tuning |
|---|---|---|
| Cập nhật dữ liệu | Gần như real-time | Cần train lại |
| Chi phí | Thấp (chỉ embedding + inference) | Cao (GPU hours) |
| Trích dẫn nguồn | Tự nhiên | Khó |
| Kiểm soát nội dung | Dễ (sửa corpus) | Phức tạp |
| Phù hợp với | Q&A, search, chatbot doanh nghiệp | Tone, style, domain-specific tasks |
3. RAG Pipeline: Hai Phase chính
RAG gồm hai giai đoạn hoàn toàn tách biệt:
Indexing Phase (Offline — chạy một lần hoặc định kỳ)
Tài liệu thô → Load → Clean → Chunk → Embed → Lưu vào Vector Store
Query Phase (Online — chạy mỗi khi user hỏi)
User query → Embed query → Tìm top-k chunks → Re-rank → Ghép vào prompt → LLM → Response
4. Document Loading
Bước đầu tiên là đưa tài liệu vào hệ thống. LangChain cung cấp hơn 100 document loaders:
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
WebBaseLoader,
BSHTMLLoader,
JSONLoader,
)
# Load PDF
pdf_loader = PyPDFLoader("annual_report.pdf")
pdf_docs = pdf_loader.load() # List[Document]
# Load Word
word_loader = Docx2txtLoader("policy.docx")
word_docs = word_loader.load()
# Web scraping
web_loader = WebBaseLoader(
web_paths=["https://docs.python.org/3/library/functions.html"],
bs_kwargs={"parse_only": SoupStrainer(class_="body")}, # chỉ lấy phần body
)
web_docs = web_loader.load()
# Mỗi Document có: page_content (str) và metadata (dict)
print(pdf_docs[0].metadata)
# {'source': 'annual_report.pdf', 'page': 0}
5. Text Chunking Strategies
Chunking là bước ảnh hưởng lớn nhất đến chất lượng RAG. Chunk quá nhỏ mất ngữ cảnh, chunk quá lớn thì nhiễu.
Fixed-size Chunking
from langchain_text_splitters import CharacterTextSplitter
splitter = CharacterTextSplitter(
chunk_size=1000, # ký tự mỗi chunk
chunk_overlap=200, # overlap để giữ ngữ cảnh
separator="\n\n",
)
chunks = splitter.split_documents(docs)
Recursive Character Splitter (khuyến nghị cho text thông thường)
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Thử split theo: ["\n\n", "\n", " ", ""] theo thứ tự
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len,
)
chunks = splitter.split_documents(docs)
Semantic Chunking (thông minh nhất, tốn kém nhất)
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
# Split dựa trên sự thay đổi ngữ nghĩa
semantic_splitter = SemanticChunker(
embeddings=OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95,
)
chunks = semantic_splitter.split_documents(docs)
6. Embedding Models
Embedding chuyển đổi text thành vector số học nắm bắt ngữ nghĩa.
from langchain_openai import OpenAIEmbeddings
from langchain_huggingface import HuggingFaceEmbeddings
# OpenAI — chất lượng cao, có phí
openai_embeddings = OpenAIEmbeddings(
model="text-embedding-3-small", # 1536 dims, rẻ hơn large
# model="text-embedding-3-large", # 3072 dims, tốt hơn
)
# Sentence Transformers — miễn phí, chạy local
local_embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3", # đa ngôn ngữ, hỗ trợ tiếng Việt tốt
# model_name="intfloat/multilingual-e5-large",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True},
)
# Test embedding
vector = openai_embeddings.embed_query("RAG là gì?")
print(f"Dimension: {len(vector)}") # 1536
7. Vector Stores
Vector store là cơ sở dữ liệu chuyên biệt để lưu và tìm kiếm embeddings.
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# Tạo vector store từ documents
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db", # lưu xuống disk
collection_name="my_rag_collection",
)
# Load lại từ disk
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings,
collection_name="my_rag_collection",
)
8. Retrieval: Cosine Similarity và MMR
# Similarity search thuần (top-4 chunks giống nhất)
retriever_basic = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4},
)
# MMR — Max Marginal Relevance: cân bằng relevance + diversity
# Tránh trả về 4 chunks gần giống nhau
retriever_mmr = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 4, # số chunks trả về
"fetch_k": 20, # fetch 20, rồi chọn 4 đa dạng nhất
"lambda_mult": 0.5, # 0=max diversity, 1=max relevance
},
)
# Score threshold — chỉ lấy chunks đủ liên quan
retriever_threshold = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.7, "k": 6},
)
9. Re-ranking với Cross-Encoder
Bi-encoder (dùng để embed) nhanh nhưng kém chính xác. Cross-encoder so sánh query với từng document trực tiếp — chậm hơn nhưng chính xác hơn nhiều. Kết hợp cả hai là best practice.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
# Cross-encoder model cho re-ranking
reranker_model = HuggingFaceCrossEncoder(
model_name="BAAI/bge-reranker-v2-m3"
)
compressor = CrossEncoderReranker(model=reranker_model, top_n=3)
# Pipeline: lấy 10 chunks, re-rank, giữ top 3
reranking_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 10}),
)
10. Generation: Ghép Context vào Prompt
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_template("""
Bạn là trợ lý AI hữu ích. Dựa vào ngữ cảnh dưới đây để trả lời câu hỏi.
Nếu ngữ cảnh không đủ thông tin, hãy nói rõ bạn không biết.
Đừng bịa đặt thông tin không có trong ngữ cảnh.
Ngữ cảnh:
{context}
Câu hỏi: {question}
Trả lời:""")
def format_docs(docs):
return "\n\n---\n\n".join(
f"[Nguồn: {doc.metadata.get('source', 'N/A')}]\n{doc.page_content}"
for doc in docs
)
# LCEL chain
rag_chain = (
{"context": retriever_mmr | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
response = rag_chain.invoke("RAG pipeline hoạt động như thế nào?")
print(response)
11. Advanced RAG Techniques
HyDE — Hypothetical Document Embeddings
Thay vì embed query trực tiếp (ngắn, ít thông tin), dùng LLM sinh ra tài liệu giả định rồi embed tài liệu đó:
from langchain.retrievers import HyDERetriever
from langchain_openai import ChatOpenAI
hyde_retriever = HyDERetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=ChatOpenAI(model="gpt-4o-mini"),
prompt_key="web_search",
)
docs = hyde_retriever.invoke("Tại sao RAG tốt hơn fine-tuning?")
Corrective RAG (CRAG)
Sau khi retrieve, dùng một LLM nhỏ để đánh giá độ liên quan của từng chunk. Nếu tất cả chunks đều kém liên quan, fallback sang web search:
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
class GraphState(TypedDict):
question: str
documents: List
generation: str
web_search_needed: bool
def grade_documents(state):
"""Dùng LLM judge để chấm từng document"""
grader_prompt = "Tài liệu này có liên quan đến câu hỏi không? Trả lời 'yes' hoặc 'no'."
# ... implement grading logic
return state
# Build CRAG graph với LangGraph
workflow = StateGraph(GraphState)
# Thêm các nodes: retrieve → grade → (web_search nếu cần) → generate
RAPTOR — Recursive Abstractive Processing
Xây dựng cây phân cấp: cluster documents → summarize từng cluster → cluster summaries → summarize tiếp. Cho phép answer cả câu hỏi chi tiết lẫn câu hỏi tổng quát.
12. Full Code: RAG Pipeline Hoàn chỉnh
# pip install langchain langchain-openai langchain-chroma chromadb pypdf
import os
from langchain_community.document_loaders import PyPDFDirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
os.environ["OPENAI_API_KEY"] = "your-api-key"
# ── INDEXING PHASE ──────────────────────────────────────────────
# 1. Load tài liệu
loader = PyPDFDirectoryLoader("./documents/")
raw_docs = loader.load()
print(f"Loaded {len(raw_docs)} pages")
# 2. Chunking
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
add_start_index=True, # ghi lại vị trí trong doc gốc
)
chunks = splitter.split_documents(raw_docs)
print(f"Created {len(chunks)} chunks")
# 3. Embedding + lưu vào ChromaDB
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
)
print("Vectorstore ready!")
# ── QUERY PHASE ─────────────────────────────────────────────────
# 4. Retriever với MMR
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 5, "fetch_k": 20},
)
# 5. Prompt template
prompt = ChatPromptTemplate.from_messages([
("system", """Bạn là chuyên gia phân tích tài liệu.
Chỉ trả lời dựa trên ngữ cảnh được cung cấp.
Luôn trích dẫn nguồn tài liệu (tên file và trang).
Ngữ cảnh:
{context}"""),
("human", "{question}"),
])
# 6. LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)
def format_docs_with_sources(docs):
formatted = []
for doc in docs:
src = doc.metadata.get("source", "unknown")
page = doc.metadata.get("page", "?")
formatted.append(f"[{src}, trang {page}]\n{doc.page_content}")
return "\n\n---\n\n".join(formatted)
# 7. RAG Chain
rag_chain = (
{
"context": retriever | format_docs_with_sources,
"question": RunnablePassthrough(),
}
| prompt
| llm
| StrOutputParser()
)
# 8. Sử dụng
if __name__ == "__main__":
questions = [
"Chính sách bảo hành sản phẩm là bao lâu?",
"Quy trình hoàn tiền như thế nào?",
]
for q in questions:
print(f"\nQ: {q}")
print(f"A: {rag_chain.invoke(q)}")
print("-" * 60)
Tổng kết
RAG là kỹ thuật không thể thiếu để xây dựng ứng dụng LLM thực tế. Pipeline chuẩn gồm: Load → Chunk → Embed → Store (offline) và Retrieve → Re-rank → Generate (online). Với các kỹ thuật nâng cao như HyDE, RAPTOR, và Corrective RAG, bạn có thể đạt được độ chính xác production-grade. Bài tiếp theo sẽ đi sâu hơn vào Vector Databases — thành phần cốt lõi của mọi hệ thống RAG.