Chuyển đến nội dung chính

Bài 16: RAG — Retrieval Augmented Generation từ A đến Z

Hiểu toàn diện về RAG pipeline — từ document loading, chunking, embedding, vector store, retrieval đến generation. Bao gồm các kỹ thuật nâng cao như HyDE, RAPTOR, Corrective RAG và code hoàn chỉnh với LangChain + ChromaDB + OpenAI.

🧠 AI & ML — Bài 15 Bài 16: RAG — Retrieval Augmented Generation từ A đến Z

AI & LLM: Từ Cơ bản đến Nâng cao

Phần 4: Prompting & RAG

xdev.asia

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íRAGFine-tuning
Cập nhật dữ liệuGần như real-timeCần train lại
Chi phíThấp (chỉ embedding + inference)Cao (GPU hours)
Trích dẫn nguồnTự nhiênKhó
Kiểm soát nội dungDễ (sửa corpus)Phức tạp
Phù hợp vớiQ&A, search, chatbot doanh nghiệpTone, 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.