Trong suốt 8 bài học trước, chúng ta đã đi qua toàn bộ nền tảng toán học và cấu trúc dữ liệu của một Vector Database: từ Pipeline trích xuất Embeddings, đo lường khoảng cách, thiết kế bộ lưu trữ lai Hybrid Storage, tối ưu hóa chỉ mục IVF/HNSW cho đến kỹ thuật nén PQ và lọc Metadata Filtering.

Bài học thứ 9 này là Dự án Capstone tổng kết. Chúng ta sẽ cùng nhau xâu chuỗi toàn bộ các thành phần này để tự thiết kế và lập trình một hệ thống RAG (Retrieval-Augmented Generation / Thế sinh tăng cường truy vấn) hoàn chỉnh từ số 0 bằng Node.js, cho phép hỏi đáp thông minh dựa trên kho tri thức riêng của bạn.

9.1 Kiến trúc tổng quan của hệ thống RAG

Các mô hình ngôn ngữ lớn (LLM) tuy thông minh nhưng bị giới hạn bởi thời gian huấn luyện và không có quyền truy cập vào dữ liệu tri thức nội bộ của doanh nghiệp. Để giải quyết vấn đề ảo tưởng (hallucination) của LLM, kỹ thuật RAG hoạt động theo luồng xử lý khép kín sau:

  1. Phân đoạn tài liệu (Chunking): Chia các tài liệu văn bản dài thành các đoạn nhỏ dễ tiêu hóa (chunks).
  2. Nhúng & Lập chỉ mục (Indexing): Chuyển các đoạn văn này thành vector nhúng và nạp vào Vector DB cùng với metadata.
  3. Truy vấn (Retrieval): Khi người dùng đặt câu hỏi, hệ thống chuyển câu hỏi thành vector, chạy so khớp để lấy ra Top các đoạn văn bản có độ tương đồng ngữ nghĩa cao nhất.
  4. Tăng cường (Augmentation): Chèn nội dung các đoạn văn bản đó làm ngữ cảnh (Context) trực tiếp vào Prompt mẫu.
  5. Thế sinh (Generation): Gửi Prompt đã làm giàu ngữ cảnh này tới LLM để sinh ra câu trả lời chuẩn xác.
ℹ️ So sánh RAG vs Fine-tuning

Fine-tuning là quá trình huấn luyện lại trọng số của mô hình để học thêm hành vi hoặc phong cách viết mới (tốn kém tài nguyên và dễ bị quên kiến thức cũ). RAG đóng vai trò như một quyển sách tra cứu mở được cấp cho LLM trong lúc làm bài thi, giúp cập nhật thông tin thời gian thực tức thì với chi phí cực kỳ rẻ.

9.2 Thiết kế bộ phân tách văn bản thông minh (Text Chunking)

Nếu nạp toàn bộ một cuốn sách 100 trang vào Vector DB thành một vector duy nhất, thông tin ngữ nghĩa sẽ bị loãng và bộ lọc similarity sẽ không thể hoạt động hiệu quả. Chúng ta cần viết hàm chia nhỏ văn bản dựa trên ngưỡng số lượng ký tự hoặc ngắt câu thông minh:

text_chunker.js
// Hàm phân tách văn bản đơn giản theo ký tự và có overlap (gối đầu) để giữ ngữ cảnh giữa các đoạn
function chunkText(text, chunkSize = 500, overlap = 50) {
  const chunks = [];
  let start = 0;
  
  while (start < text.length) {
    const end = Math.min(start + chunkSize, text.length);
    chunks.push(text.slice(start, end).trim());
    start += (chunkSize - overlap); // Nhảy bước dịch chuyển
  }
  return chunks;
}

9.3 Thiết kế Semantic Retrieval Engine

Tiến trình Retrieval thực hiện chuyển đổi câu hỏi của người dùng thành vector biểu diễn ngữ nghĩa và tìm kiếm trên cơ sở dữ liệu Vector đã được lập chỉ mục:

retrieval_engine.js
// Hàm truy xuất ngữ cảnh tương đồng từ CSDL Vector
async function retrieveContext(userQuery, vectorDb, limit = 3) {
  // 1. Nhúng vector câu hỏi của người dùng
  const queryVector = await getEmbeddings(userQuery);
  
  // 2. Chạy tìm kiếm lân cận gần nhất trong database
  const matches = vectorDb.search(queryVector, limit);
  
  // 3. Nối các đoạn văn bản tìm thấy thành khối ngữ cảnh
  return matches.map(m => m.text).join("\n\n");
}

9.4 Tích hợp Mô hình Ngôn ngữ lớn (LLM APIs)

Sau khi có được context tương đồng nhất từ Vector DB, chúng ta lắp ghép vào prompt và gửi yêu cầu tới API của Gemini hoặc OpenAI để nhận câu trả lời cuối cùng:

llm_integration.js
// Ghép prompt và gọi API để tổng hợp câu trả lời
async function generateAnswer(question, context) {
  const prompt = `Dưới đây là tài liệu ngữ cảnh tham khảo:
---
${context}
---
Hãy trả lời câu hỏi sau một cách khách quan dựa trên ngữ cảnh đã cung cấp. Nếu thông tin không có trong tài liệu, hãy trả lời là "Tôi không tìm thấy thông tin trong tài liệu".

Câu hỏi: ${question}
Trả lời:`;

  // Gọi API OpenAI GPT-4o hoặc Gemini 1.5 Pro
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
    },
    body: JSON.stringify({
      model: "gpt-4o",
      messages: [{ role: "user", content: prompt }]
    })
  });
  
  const data = await response.json();
  return data.choices[0].message.content;
}
🕳️ Cạm bẫy: Nhiễu ngữ cảnh (Garbage In, Garbage Out)

Nếu thuật toán tìm kiếm vector của bạn có Recall thấp và trả về các đoạn văn bản sai lệch, LLM sẽ đọc nhầm các thông tin nhiễu đó và sinh ra câu trả lời sai lệch hoàn toàn. Chất lượng của hệ thống RAG phụ thuộc trực tiếp vào độ chính xác của Vector Database.

9.5 Các bước cải tiến và tối ưu hệ thống RAG nâng cao

Để tối ưu hóa RAG đạt mức độ chính xác cấp độ doanh nghiệp thương mại, bạn có thể bổ sung các kỹ thuật sau:

  • Xếp hạng lại (Re-ranking): Sử dụng một mô hình phụ Cross-Encoder để chấm điểm và phân loại chính xác lại danh sách Top 50 ứng viên thô do Vector DB trả về nhằm lấy ra Top 5 tốt nhất trước khi gửi prompt.
  • Semantic Chunking: Thay vì ngắt theo số ký tự cố định, thuật toán đo lường khoảng cách cosine giữa các câu liên tục. Khi khoảng cách vượt quá một ngưỡng giới hạn, hệ thống nhận biết chủ đề đã thay đổi và ngắt chunk tại đó.
  • Hybrid Search (Tìm kiếm kết hợp): Tìm kiếm Vector thuần (dense retrieval) rất giỏi nắm bắt ý nghĩa nhưng lại yếu với truy vấn đòi hỏi khớp chính xác từ khóa hiếm gặp (mã lỗi, số hiệu sản phẩm, tên riêng chưa từng xuất hiện trong dữ liệu huấn luyện embedding). Hybrid Search chạy song song hai công cụ tìm kiếm độc lập trên cùng một câu hỏi — tìm kiếm từ khóa cổ điển (thường dùng BM25/full-text index) và tìm kiếm Vector ngữ nghĩa — rồi hợp nhất hai danh sách kết quả riêng biệt đó thành một bảng xếp hạng cuối cùng duy nhất.

Vấn đề của việc hợp nhất là: điểm số BM25 (thường vài chục) và điểm Cosine Similarity (từ 0 đến 1) nằm trên hai thang đo hoàn toàn khác nhau, không thể cộng trực tiếp. Kỹ thuật phổ biến nhất để giải quyết là Reciprocal Rank Fusion (RRF) — thay vì hợp nhất theo điểm số thô, RRF chỉ dựa vào thứ hạng (rank) của tài liệu trong mỗi danh sách kết quả riêng lẻ:

$\text{RRFScore}(d) = \sum_{r \in \text{rankers}} \frac{1}{k + \text{rank}_r(d)}$

Trong đó $d$ là một tài liệu ứng viên, $\text{rankers}$ là tập các công cụ tìm kiếm tham gia hợp nhất (ở đây là 2: BM25 và Vector Search), $\text{rank}_r(d)$ là thứ hạng của tài liệu $d$ trong danh sách kết quả của công cụ $r$ (hạng 1 là tốt nhất), và $k$ là một hằng số làm mượt (thường chọn $k = 60$) để tránh tài liệu đứng hạng 1 lấn át quá mức các tài liệu đứng gần đó.

reciprocal_rank_fusion.js
// Hop nhat 2 danh sach ket qua (BM25 keyword search + Vector search) bang RRF
function reciprocalRankFusion(keywordResults, vectorResults, k = 60) {
  const scores = new Map(); // docId -> diem RRF cong don

  // Moi danh sach dong gop 1/(k + rank) cho tung tai lieu, rank bat dau tu 1
  [keywordResults, vectorResults].forEach((resultList) => {
    resultList.forEach((docId, index) => {
      const rank = index + 1;
      const prev = scores.get(docId) || 0;
      scores.set(docId, prev + 1 / (k + rank));
    });
  });

  // Sap xep giam dan theo diem RRF tong hop - tai lieu xuat hien tot o CA HAI
  // danh sach se co diem cao hon tai lieu chi tot o mot danh sach duy nhat
  return Array.from(scores.entries())
    .sort((a, b) => b[1] - a[1])
    .map(([docId]) => docId);
}
ℹ️ Vì sao không chỉ dùng Vector Search thuần?

Tài liệu chứa đúng số hiệu sản phẩm "XJ-4471B" mà người dùng gõ gần như chắc chắn sẽ có thứ hạng khớp từ khóa (BM25) rất cao, nhưng embedding của chuỗi này có thể không nằm gần embedding câu hỏi trong không gian ngữ nghĩa (mô hình nhúng không "học" được ý nghĩa của một mã sản phẩm ngẫu nhiên). RRF đảm bảo tài liệu đó vẫn được đẩy lên cao nhờ điểm BM25, dù điểm Vector Search thấp.

💡 Mẹo tăng tốc RAG bằng nén dữ liệu

Trong các hệ thống RAG lớn có hàng trăm nghìn người dùng, bạn nên áp dụng chỉ mục IVF kết hợp nén vector PQ cho giai đoạn lọc thô. Điều này giúp giảm độ trễ truy vấn xuống chỉ còn dưới 5 mili-giây, sau đó chỉ cần giải nén chính xác lại các vector của Top 50 ứng viên hàng đầu để Re-ranking.

Bảng đối chiếu hiệu năng các giải pháp hỏi đáp tri thức:

Chỉ số so sánh LLM thông thường (Không RAG) RAG đơn thuần (Naive RAG) RAG nâng cao (Metadata + Reranker)
Hiện tượng ảo tưởng (Hallucination) Rất cao (Do tự bịa thông tin). Thấp. Cực kỳ thấp (Kiểm soát chặt chẽ qua ngữ cảnh).
Thời gian cập nhật kiến thức mới Không thể (Phải train lại model). Tức thời (Chỉ cần chèn tài liệu mới vào Vector DB). Tức thời.
Độ phức tạp triển khai Rất thấp (Chỉ gọi API). Trung bình. Cao (Phải quản lý HNSW, Bitset và Cross-Encoder).
Độ fresh của tri thức Tĩnh hoàn toàn. Động. Động.

9.6 Thực hành: Luồng RAG từng bước

Để thấy toàn bộ 5 giai đoạn nói trên chạy thật trên một ví dụ cụ thể, demo dưới đây dùng đúng FlatIndex của vdb-engine.js (Bài 2) để nạp một tài liệu ngắn về chính chủ đề Vector Database, chia thành 6 chunk, gán cho mỗi chunk một toạ độ nhúng (embedding) "đồ chơi" 2D (embedding thật có hàng trăm/nghìn chiều — ở đây giảm chiều xuống 2D chỉ để minh họa), rồi chạy .search() thật với một câu hỏi mẫu để lấy Top-3 chunk gần nhất và ghép Prompt cuối cùng. Bấm "Bước tiếp theo" để đi qua từng giai đoạn của pipeline:

🔗 Luồng RAG từng bước — FlatIndex thật (vdb-engine.js)
Tài liệu mẫu đã sẵn sàng (6 chunk về Vector Database) — bấm "Bước tiếp theo" để bắt đầu Stage 1.
rag_pipeline_demo.js (đúng logic đang chạy ở tab Xem trước)
import { FlatIndex } from './vdb-engine.js';

// Tài liệu mẫu về chính chủ đề Vector Database, đã chia sẵn thành 6 chunk.
// embedding: toạ độ 2D "đồ chơi" — embedding thật có hàng trăm/nghìn chiều,
// ở đây giảm chiều xuống 2D chỉ để minh họa trực quan trên canvas.
const chunks = [
  {
    id: 1,
    text: 'Vector Database là hệ cơ sở dữ liệu chuyên lưu trữ và tìm kiếm các vector embedding đa chiều theo độ tương đồng.',
    embedding: [4.2, 2.5],
  },
  {
    id: 2,
    text: 'Mỗi đoạn văn bản được mô hình embedding chuyển thành một vector số thực, biểu diễn ý nghĩa ngữ nghĩa của đoạn đó.',
    embedding: [1.5, 3.0],
  },
  {
    id: 3,
    text: 'Độ tương đồng giữa hai vector được đo bằng các công thức khoảng cách như Euclidean, Cosine hoặc Dot Product.',
    embedding: [1.0, 6.5],
  },
  {
    id: 4,
    text: 'Các chỉ mục gần đúng như IVF và HNSW giúp tìm Top-K lân cận nhanh hơn nhiều so với tìm kiếm tuyến tính Flat.',
    embedding: [4.0, 6.0],
  },
  {
    id: 5,
    text: 'Trong hệ thống RAG, Vector Database đóng vai trò truy xuất các đoạn ngữ cảnh liên quan để đưa vào Prompt cho LLM.',
    embedding: [5.5, 4.3],
  },
  {
    id: 6,
    text: 'Ngoài văn bản, Vector Database còn được dùng để tìm kiếm hình ảnh, gợi ý sản phẩm và phát hiện gian lận.',
    embedding: [7.5, 1.0],
  },
];

const sampleQuestion = 'Vector Database giúp ích gì cho hệ thống RAG?';
const queryEmbedding = [5.0, 4.0]; // toạ độ toy 2D của câu hỏi, cùng không gian với chunk

// Stage 3-4: nạp toàn bộ chunk vào FlatIndex thật của vdb-engine.js
const ragIndex = new FlatIndex('euclidean');
chunks.forEach((c) => ragIndex.insert(c.id, c.embedding, { text: c.text }));

// Stage 4: tìm Top-3 chunk gần nhất bằng .search() THẬT (không hardcode kết quả)
const topMatches = ragIndex.search(queryEmbedding, 3);
// -> [{ id, vector, metadata: { text }, distance }, ...] đã sắp xếp tăng dần theo distance

// Stage 5: ghép Prompt từ chính các chunk vừa truy xuất thật
const context = topMatches.map((m) => m.metadata.text).join('\n\n');
const prompt = `Dựa trên ngữ cảnh sau:
---
${context}
---
Hãy trả lời câu hỏi: ${sampleQuestion}`;

📖 Tài liệu tham khảo

Tải file code thực hành minh họa bài học

Tải tệp tin code mẫu JavaScript chạy độc lập mô phỏng trọn vẹn quy trình phân đoạn Chunking, nạp vector store, truy vấn context tương đồng và ghép Prompt RAG:

Tải về vectordb_rag_demo.js

Bài viết liên quan trong series

Bài 8: Lọc Metadata (Metadata Filtering) & Thuật toán Single-stage vs Two-stage Quay lại Lộ trình Vector DB Series

Bình luận