🤖 AI✍️ Khoa📅 19/04/2026☕ 13 phút đọc

RAG Architecture và Retrieval Algorithms

1. Tại sao cần RAG? — Vấn đề cốt lõi của LLMs

LLMs là cỗ máy được "đóng băng kiến thức" tại thời điểm training. GPT-4 training cutoff cuối 2023 — hỏi nó về sự kiện tháng này? Sảng. Hỏi về data internal công ty bạn? Không bao giờ biết.

Hai hướng giải quyết:

  1. Fine-tuning: Train lại model với data mới. Tốn kém (hàng trăm GB GPU-hours), khó update, và — quan trọng nhất — Fine-tuning không phải để nhét kiến thức mới. Nó dạy model phong cách và kỹ năng, không phải fact. (Chi tiết ở bài MLOps.)
  2. RAG: Tại runtime, tìm kiếm những đoạn tài liệu liên quan, nhét vào Context Window, để LLM "đọc tài liệu rồi trả lời". Giống học sinh đi thi open-book.

RAG giải quyết được:

  • Knowledge cutoff (thêm tài liệu mới bất cứ lúc nào)
  • Hallucination (grounded bởi tài liệu thực)
  • Kiểm soát nguồn (trả lời kèm citation)
  • Data privacy (không gửi toàn bộ corpus lên API)

2. Architecture Overview — Hai Pipeline độc lập

Hiểu lầm phổ biến là coi RAG như 1 pipeline tuyến tính. Thực ra là 2 pipeline bất đồng bộ chạy ở các thời điểm khác nhau:

flowchart TB
  subgraph ING["INGESTION PIPELINE (offline/batch)"]
    D["Raw Documents"] --> L["Loader & Parser"]
    L --> C["Clean Text"]
    C --> CH["Chunking Strategy"]
    CH --> E["Embedding Model"]
    E --> V["Vector Store + Metadata"]
  end

  subgraph RET["RETRIEVAL PIPELINE (real-time)"]
    Q["User Query"] --> QT["Query Transformation (optional)"]
    QT --> QE["Embedding Model"]
    QE --> ANN["ANN Search in Vector Store"]
    ANN --> RK["Top-K Candidates"]
    RK --> RR["Re-ranking (optional)"]
    RR --> TN["Top-N Chunks"]
    TN --> PA["Prompt Assembly (+ User Query)"]
    PA --> LLM["LLM Generation"]
    LLM --> R["Response with Citations"]
  end
═══════════════════════════════════════════════════════════
INGESTION PIPELINE (chạy offline / batch)
═══════════════════════════════════════════════════════════

   [Raw Documents]                                         
   PDF, Docx, HTML, DB, API, Slack, Confluence...         
        │                                                   
        ▼ Document Loader & Parser                         
   [Clean Text]   (strip HTML tags, OCR PDF, extract tables)
        │                                                   
        ▼ Chunking Strategy                                 
   [Chunks]  chunk_1, chunk_2, ... chunk_N                 
        │                                                   
        ▼ Embedding Model (text → vector)                  
   [Embeddings]  [[0.12, -0.45, ...], [0.78, 0.23, ...]]  
        │                                                   
        ▼ Upsert vào Vector Database                       
   [Vector Store + Metadata]                               
   {id, vector, text, source_url, chunk_index, created_at}


═══════════════════════════════════════════════════════════
RETRIEVAL PIPELINE (chạy real-time, mỗi user query)
═══════════════════════════════════════════════════════════

   [User Query]    "Quy trình onboarding của công ty là gì?"
        │                                                   
        ▼ Query Transformation (optional)                  
   [Rewritten Query] "Employee onboarding process steps"   
        │                                                   
        ▼ Embedding Model (cùng model với ingestion!)      
   [Query Vector]                                          
        │                                                   
        ▼ ANN Search trong Vector Store                    
   [Top-K Candidates]  (K=20, rough retrieval)             
        │                                                   
        ▼ Re-ranking (optional, Cross-Encoder)             
   [Top-N Chunks]  (N=4-5, precision retrieval)            
        │                                                   
        ├── [User Query]                                    
        ▼ Prompt Assembly                                   
   [Augmented Prompt]                                      
   "Context: [chunk1][chunk2]...\nQuestion: ..."           
        │                                                   
        ▼ LLM Generation                                    
   [Response với Citations]                                

3. Chunking Strategies — Nghệ thuật cắt tài liệu

Đây là nơi 80% dự án RAG bị fail sớm, và cũng là nơi ít ai đầu tư đúng mức.

3.1 Fixed-size Chunking (Anti-pattern)

Text: "The mitochondria is the powerhouse of the cell. 
       It generates ATP through oxidative phosphorylation.
       This process requires oxygen and produces CO₂."

Chunk size = 50 chars:
  Chunk 1: "The mitochondria is the powerhouse of the ce"   ← câu bị cắt giữa!
  Chunk 2: "ll. It generates ATP through oxidative phosph"  ← context mất
  Chunk 3: "orylation.\nThis process requires oxygen and "

Câu bị cắt ngang = embedding vector bị nhiễu = search không chính xác.

Thư viện như LangChain RecursiveCharacterTextSplitter cắt theo thứ tự ưu tiên: ["\n\n", "\n", ". ", " "] — thử cắt tại paragraph break trước, nếu vẫn quá dài thì xuống sentence, rồi word.

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,          # target size (tokens hoặc chars)
    chunk_overlap=64,        # overlap giữa chunks để tránh mất context
    length_function=len,
)

Chunk overlap là quan trọng: Nếu thông tin nằm ở ranh giới 2 chunks, overlap đảm bảo ít nhất 1 chunk chứa đủ context.

3.3 Semantic Chunking — Cắt theo ý nghĩa

Thay vì cắt theo kích thước, cắt theo khi "ý nghĩa thay đổi đột ngột":

Thuật toán:
  1. Chia text thành sentences
  2. Embed từng sentence → vector
  3. Tính cosine similarity giữa sentence[i] và sentence[i+1]
  4. Nếu similarity đột ngột giảm (ví dụ < 0.7) → đây là chỗ chuyển chủ đề → cắt chunk ở đây
  5. Gộp các sentences liên tiếp thành 1 chunk

Đắt hơn (cần embed toàn bộ sentences ngay khi ingest), nhưng chunks chứa trọn 1 ý = search chính xác hơn đáng kể.

3.4 Hierarchical / Parent-Child Chunking (Best practice cho Production)

Trick thông minh nhất trong cuốn sách RAG:

flowchart TB
  O["Original Document (5000 words)"] --> P["Parent Chunk (1000 words)"]
  P --> C1["Child Chunk 1 (100 words)"]
  P --> C2["Child Chunk 2 (100 words)"]
  P --> C3["Child Chunk 3 (100 words)"]
  C1 --> I1["Indexed"]
  C2 --> I2["Indexed"]
  C3 --> I3["Indexed"]
Original Document: "Hệ thống nghỉ phép của công ty" (5000 words)

                    ┌─────────────────────────────┐
                    │   Parent Chunk (1000 words)  │
                    │   "Chương 1: Quy trình xin  │
                    │    nghỉ phép thường niên..." │
                    └──────────┬──────────────────┘
                               │ chia nhỏ
          ┌────────────────────┼────────────────────┐
          ▼                    ▼                    ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Child Chunk 1    │ │ Child Chunk 2    │ │ Child Chunk 3    │
│ (100 words)      │ │ (100 words)      │ │ (100 words)      │
│ "Nhân viên cần  │ │ "Form đơn xin   │ │ "Sau khi được   │
│  gửi form trước │ │  nghỉ phép điền │ │  duyệt, ERP sẽ  │
│  3 ngày làm..."  │ │  theo mẫu HR..." │ │  tự động..."     │
└──────────────────┘ └──────────────────┘ └──────────────────┘
     [Indexed]             [Indexed]             [Indexed]
     (nhỏ, chính xác)

Vector DB chỉ INDEX và SEARCH trên Child Chunks (nhỏ → embedding chính xác)
Khi tìm thấy Child Chunk, trả về Parent Chunk tương ứng cho LLM (to → đủ context)

Kết quả: precision của search cao (nhờ child) + comprehensiveness của context cao (nhờ parent).


4. Embedding Models — Không phải mọi model đều như nhau

4.1 Chọn Embedding Model nào?

Model               Dim   Max Tokens  MTEB Score  Self-host?
────────────────────────────────────────────────────────────
text-embedding-3-small  1536  8191      62.3        ❌ (API only)
text-embedding-3-large  3072  8191      64.6        ❌ (API only)
bge-m3                  1024  8192      65.0+       ✅ (free)
bge-large-en-v1.5       1024  512       64.2        ✅ (free)
multilingual-e5-large   1024  514       61.5        ✅ (multilingual, VN)
jina-embeddings-v3      1024  8192      65.3        ✅ (free for local)

Rule of thumb:

  • Nếu data thuần tiếng Anh, enterprise, không muốn tự host: text-embedding-3-large
  • Nếu cần multilingual (tiếng Việt), tự host: multilingual-e5-large hoặc bge-m3
  • Lưu ý cực kỳ quan trọng: Embedding model phải nhất quán giữa Ingestion và Retrieval. Đổi model = phải re-embed toàn bộ corpus. Đây là technical debt đắt tiền.

4.2 Distance Metrics trong Vector DB

Cosine Similarity:   cos(θ) = (A·B) / (||A|| × ||B||)
  → Đo góc giữa 2 vectors, bỏ qua magnitude
  → Tốt nhất cho text (2 câu ý nghĩa giống nhau nên có cùng hướng)

L2 (Euclidean):      d = sqrt(Σ(aᵢ - bᵢ)²)
  → Khoảng cách tuyệt đối trong không gian n chiều
  → Tốt cho image embeddings

Inner Product (IP):  dot(A, B) = Σ(aᵢ × bᵢ)
  → Tương đương Cosine khi vectors đã L2-normalized
  → Nhanh hơn Cosine (1 ít phép tính hơn)

Production tip: Nếu embedding model của bạn output L2-normalized vectors (phổ biến), configure Vector DB dùng Inner Product thay vì Cosine — tốc độ nhanh hơn, kết quả giống hệt.


5. Vector Database Internals — HNSW sâu bên trong

Tìm kiếm vector gần nhất trong hàng triệu vectors bằng brute-force là $O(N \cdot d)$. Với 10M documents mỗi 1024 chiều = 10 tỷ phép nhân. Không khả thi real-time.

Giải pháp: Approximate Nearest Neighbor (ANN) — chấp nhận hy sinh vài % accuracy để đổi lấy tốc độ.

5.1 HNSW (Hierarchical Navigable Small World)

HNSW là cấu trúc dữ liệu graph phân tầng, được dùng trong hầu hết Vector DB hiện đại (pgvector, Milvus, Qdrant, Weaviate):

flowchart TB
  subgraph L2["Layer 2 (thưa nhất)"]
    A2["A"] --- E2["E"]
  end
  subgraph L1["Layer 1 (trung bình)"]
    A1["A"] --- B1["B"] --- D1["D"] --- E1["E"] --- F1["F"]
  end
  subgraph L0["Layer 0 (dày nhất)"]
    A0["A"] --- B0["B"] --- C0["C"] --- D0["D"] --- E0["E"] --- F0["F"] --- G0["G"] --- H0["H"] --- I0["I"] --- J0["J"]
  end
  E2 --- E1
  B1 --- B0
  E1 --- E0
Layer 2 (thưa nhất):   A ─────────────────── E
                                              │
Layer 1 (trung bình):  A ──── B ──── D ───── E ──── F
                              │              │
Layer 0 (dày nhất):  A─B─C─B─D─C─D─E─F─E─G─F─H─I─J
                     (chứa tất cả nodes)

Quá trình build index (Insertion):

Insert node X:
  1. Random level = floor(-ln(random()) × m_L)  ← xác suất giảm theo cấp số nhân
     → X tồn tại ở Layer 0 đến Layer random_level
  2. Từ Entry point của Layer cao nhất:
     - Greedy search tìm ef_construction nodes gần nhất ở layer hiện tại
     - Kết nối X với M nodes gần nhất (M = hyperparameter, default 16)
     - Xuống layer dưới, tiếp tục

Quá trình search (Query):

Query vector q, tìm top-K nearest:
  1. Bắt đầu từ Entry Point ở Layer cao nhất
  2. Greedy traversal: từ node hiện tại, nhìn tất cả neighbors
     → Di chuyển sang neighbor gần q nhất
     → Dừng khi không thể tìm được node nào gần hơn (local minimum)
  3. Xuống layer dưới, tiếp tục từ vị trí tìm được
  4. Ở Layer 0: dùng ef_search (large) để tìm chính xác hơn, trả về K results

Trade-off các hyperparameters:

M (connections per node):
  Tăng M → recall cao hơn, memory nhiều hơn, build chậm hơn
  Default: 16-32

ef_construction (size of dynamic candidate list khi build):
  Tăng → index chất lượng hơn, nhưng build chậm hơn
  Default: 128-200

ef_search (khi query):
  Tăng → recall cao hơn, query chậm hơn
  Tuning: recall=0.99 thường đạt được với ef_search=64-128

5.2 pgvector vs Dedicated Vector DB — Chọn cái nào?

                    pgvector        Qdrant        Milvus/Zilliz
─────────────────────────────────────────────────────────────────
Self-host            ✅              ✅              ✅
Managed cloud        ✅ (RDS)        ✅              ✅
Vector ops/sec       ~1K-10K         ~100K+          ~100K+
Max vectors          ~10M (ok)       Billions        Billions
ACID transactions    ✅ (Postgres)   ❌              ❌
SQL JOIN với data    ✅              ❌              ❌
Metadata filtering   ✅ (WHERE SQL)  ✅              ✅
Multi-tenancy        ✅ (Row-level)  ✅ (collections)✅

Khi nào dùng pgvector:

  • Đang dùng Postgres rồi (<10M vectors)
  • Cần JOIN vector results với relational data
  • Cần row-level security (user A chỉ search documents của user A)
  • Không muốn vận hành thêm 1 infra mới

Khi nào dùng Qdrant/Milvus:

  • 10M vectors

  • Cần performance cực cao (< 10ms p99)
  • Cần các tính năng advanced: sparse vectors, hybrid search built-in, multi-vector per document

6. Advanced Retrieval — Nâng cao precision

Nhược điểm của Vector Search (Dense): dở với keyword exact match. "Mã nhân viên NV2024-0042" → embedding của chuỗi số vô nghĩa này không phân biệt được với "NV2024-0999".

Nhược điểm của BM25/Full-text Search (Sparse): không hiểu ngữ nghĩa. "Cho tôi nghỉ" vs "Tôi muốn xin nghỉ phép" → BM25 thấy khác (ít từ chung), nhưng vector embedding thấy tương đồng.

Hybrid Search: kết hợp cả hai, merge kết quả bằng RRF (Reciprocal Rank Fusion):

RRF Score(doc, query) = Σ_r 1 / (k + rank_r(doc))
  với k = 60 (constant để giảm ảnh hưởng của top ranks) 
  rank_r = rank của doc trong result list r (sparse hoặc dense)

Ví dụ:
  Doc A: Dense rank=1, Sparse rank=5
    score = 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318
  Doc B: Dense rank=3, Sparse rank=1
    score = 1/(60+3) + 1/(60+1) = 0.0159 + 0.0164 = 0.0323
  → Doc B được rank cao hơn (tốt về cả keyword và semantic)

6.2 Query Transformation

User gõ query ngắn gọn, thiếu context → embedding không đủ tốt → search miss.

a) Hypothetical Document Embeddings (HyDE):

Query: "nghỉ phép"
  ↓
LLM: Generate giả định document: 
  "Thủ tục xin nghỉ phép tại công ty ABC bao gồm:
   Điền form PTO-001, gửi trước 3 ngày làm việc,
   manager approve trong vòng 24 giờ..."
  ↓
Embed cái document giả này → search trong Vector DB

Tại sao hoạt động? Document giả dù không chính xác nhưng có style và vocabulary tương tự document thật trong DB → embedding gần nhau hơn embedding của query ngắn.

b) Query Expansion:

Query: "nghỉ phép"
  ↓
LLM: Sinh 3-4 query paraphrase:
  1. "Quy trình xin nghỉ phép"
  2. "PTO policy"
  3. "Cách submit đơn nghỉ phép"
  4. "Paid time off procedure"
  ↓
Search song song tất cả 4 queries → merge kết quả (deduplicate)

6.3 Cross-Encoder Re-ranking

Bi-encoder (Vector Search): Encode query và document độc lập → so sánh vector. Nhanh (query chỉ encode 1 lần, compare với N vectors pre-computed), nhưng không thấy được tương tác giữa query và document.

Cross-encoder: Nhận input là [CLS] query [SEP] document [SEP], encode cùng lúc → output 1 score duy nhất. Chậm hơn rất nhiều (phải inference lại với mỗi document), nhưng chính xác hơn đáng kể.

Retrieval → Re-ranking Pipeline:

Query → [Bi-encoder] → Top-50 candidates (fast, ~5ms)
                              │
                              ▼
            [Cross-Encoder Re-ranker]       (~50ms, chạy 50 inferences)
            Input: "query [SEP] candidate_i"
            Output: relevance score 0-1
                              │
                              ▼
                   Top-5 re-ranked results → LLM

Cross-Encoder models phổ biến: BAAI/bge-reranker-v2-m3, cross-encoder/ms-marco-MiniLM-L-12-v2.


7. Contextual Compression và Citation

7.1 Contextual Compression

Chunk retrieved đôi khi chứa nhiều thông tin không liên quan. Trước khi nhét vào LLM, chạy thêm 1 bước compression:

Retrieved Chunk (500 words):
  "Công ty ABC thành lập năm 2005. Hiện có 200 nhân viên.
   Quy trình nghỉ phép: Nhân viên điền form PTO. Manager approve...
   Công ty có văn phòng tại Hà Nội và TP.HCM..."

Query: "Quy trình nghỉ phép"

Sau Compression (LLM extract relevant section):
  "Nhân viên điền form PTO. Manager approve trong 24h..."

Trade-off: thêm 1 LLM call → latency tăng. Thường chỉ áp dụng khi chunks lớn hoặc context window bị giới hạn.

7.2 Structured RAG Response với Citation

Ép model trả về structured response kèm source:

{
  "answer": "Quy trình xin nghỉ phép gồm 3 bước...",
  "sources": [
    {
      "document": "HR Policy v2.1",
      "page": 12,
      "relevance": 0.94,
      "excerpt": "Nhân viên cần submit form PTO-001..."
    }
  ],
  "confidence": "high"
}

UI hiển thị citation → user verify được nguồn → trust tăng đáng kể so với chatbot không có source.


8. Evaluation Framework — Đo lường RAG pipeline

Không thể chỉ dùng "cảm giác" để biết RAG tốt hay không. Framework RAGAS cung cấp các metrics định lượng:

RAGAS Metrics:

1. Context Precision:
   "Trong K chunks retrieved, bao nhiêu % thực sự liên quan?"
   → Đo quality của retrieval
   → Thấp: DB index kém, chunking strategy sai, embedding model yếu

2. Context Recall:
   "Trong tất cả thông tin cần để trả lời, DB của bạn lấy được bao nhiêu %?"
   → Đo coverage của retrieval
   → Thấp: chunking quá nhỏ và mất context, thiếu tài liệu trong DB

3. Faithfulness:
   "Câu trả lời của LLM có 100% từ context không? Có bịa thêm không?"
   → Đo hallucination rate
   → Thấp: model tự sáng tạo ngoài context

4. Answer Relevance:
   "Câu trả lời có trả lời đúng câu hỏi không?"
   → Đo generation quality
   → Thấp: LLM bị distracted bởi irrelevant chunks

CI/CD cho RAG: Tạo bộ 100-200 (question, ground_truth_answer) pairs. Mỗi lần thay đổi pipeline (chunking, embedding model, prompt, K size), chạy RAGAS evaluation script, compare metrics với baseline. Không merge nếu metrics sụt > 5%.