Cách Mình Xây Dựng RAG

Retrieval Augmented Generation - cung cấp context cho AI để trả lời chính xác

LLM không biết hết mọi thứ -- nhất là dữ liệu riêng của mình. RAG (Retrieval Augmented Generation) giải quyết chuyện này: thay vì hy vọng model biết, mình tìm context liên quan rồi đưa thẳng vào prompt.

Đừng bắt LLM phải nhớ -- đưa câu trả lời cho nó rồi bảo nó sử dụng.

Không RAG vs. Có RAG

Không có RAG Có RAG
LLM bịa thông tin LLM trích dẫn nguồn
Không dùng được dữ liệu riêng Tìm kiếm trong data của mình
Bị giới hạn bởi knowledge cutoff Luôn cập nhật
Hallucinations phổ biến Phản hồi có căn cứ

Bốn Giai Đoạn Của Pipeline RAG

  1. Xử lý tài liệu -- Chia tài liệu thành chunks tìm kiếm được (chunking)
  2. Embedding -- Chuyển chunks thành vectors cho similarity search
  3. Retrieval -- Tìm chunks liên quan cho query
  4. Generation -- Trả lời câu hỏi dựa trên context đã truy xuất

Mỗi giai đoạn có quyết định và đánh đổi riêng. Đi từng cái.

Giai Đoạn 1: Xử Lý Tài Liệu

Kích Thước Chunk Ảnh Hưởng Lớn

Tài liệu phải chia nhỏ để tìm kiếm được. Chia to hay nhỏ thì tùy bài toán:

Kích thước Chunk Được cái Mất cái
Nhỏ (200-500 ký tự) Retrieval chính xác hơn Có thể thiếu context xung quanh
Trung bình (500-1000 ký tự) Cân bằng tốt Đây là lựa chọn mặc định
Lớn (1000-2000 ký tự) Nhiều context hơn Matching kém chính xác

Chưa biết chọn gì thì bắt đầu với 500-1000 ký tự, overlap 100-200 ký tự giữa các chunks.

Chia Kiểu Nào?

Chiến lược Phù hợp cho Cách hoạt động
Fixed size Văn bản chung Cắt mỗi N ký tự
Paragraph-based Bài viết, blogs Chia theo ngắt đoạn tự nhiên
Sentence-based Nội dung dày đặc Mỗi 2-5 câu một chunk
Semantic Tài liệu kỹ thuật Chia theo section/heading

Metadata -- Đừng Quên

Luôn lưu metadata cùng chunks:

  • Source: Từ đâu ra?
  • Section: Phần nào trong tài liệu?
  • Date: Tạo/cập nhật khi nào?
  • Thuộc tính lọc: Category, author, v.v.

Không có metadata thì sau này không trích dẫn nguồn được, không lọc kết quả được. Mất công lắm.

Giai Đoạn 2: Embedding

Chuyển text chunks thành vectors để so sánh ý nghĩa ngữ nghĩa.

Chọn Model

Model Dimensions Dùng khi
text-embedding-3-small 1536 Đa số trường hợp, giá rẻ
text-embedding-3-large 3072 Cần chất lượng cao, chấp nhận tốn hơn
Multilingual models Khác nhau Nội dung tiếng Việt hoặc đa ngôn ngữ

Mấy điểm cần nhớ:

  • Nội dung tiếng Việt thì cần model có multilingual support, đừng dùng model chỉ tối ưu cho tiếng Anh
  • Dimensions cao = chất lượng tốt hơn nhưng lưu trữ nhiều hơn, search chậm hơn
  • Batch embedding requests (50-100 items/lần) để giảm latency

Lưu Ở Đâu?

Mình dùng Supabase + pgvector vì đơn giản, tích hợp luôn với stack sẵn có:

Lựa chọn Phù hợp cho
Supabase + pgvector Setup nhanh, dùng chung với DB chính
Pinecone Scale lớn, managed service
Weaviate Filtering phức tạp
FAISS Local/embedded use cases

Giai Đoạn 3: Retrieval

Đây là bước tìm đúng chunks cho query.

Similarity Search Cơ Bản

Embed query, tìm chunks có cosine similarity cao nhất. Hai tham số quan trọng:

  • Limit: Lấy bao nhiêu chunks (thường 3-10)
  • Threshold: Điểm similarity tối thiểu (thường 0.7-0.8)
Threshold Hiệu quả
0.8+ Precision cao, ít kết quả -- có thể bỏ sót content liên quan
0.7 Cân bằng, dùng làm mặc định
0.6 Recall cao, nhiều noise hơn -- nhưng bắt được edge cases

Hybrid Search -- Vector + Keyword

Vector search tìm theo nghĩa, keyword search tìm exact matches (tên riêng, mã, thuật ngữ cụ thể). Kết hợp cả hai thì bao phủ cả ý nghĩa lẫn chi tiết.

Ví dụ: user hỏi "HDBSCAN clustering settings" -- vector search hiểu ý "cấu hình thuật toán phân cụm", keyword search bắt đúng chữ "HDBSCAN". Thiếu một cái là miss.

Chọn Chiến Lược Retrieval

Chiến lược Khi nào dùng
Simple similarity Queries đơn giản, rõ ràng
Hybrid (vector + keyword) Queries có thuật ngữ hoặc tên cụ thể
Query expansion Queries mơ hồ hoặc ngắn quá
Multi-query Câu hỏi phức tạp cần nhiều góc nhìn

Giai Đoạn 4: Generation

Lấy context rồi, giờ sinh câu trả lời.

Đưa Context Cho LLM Đúng Cách

Cách trình bày context cho LLM ảnh hưởng nhiều đến chất lượng câu trả lời.

Nên làm:

  • Phân cách rõ ràng giữa các chunks
  • Ghi nguồn cho mỗi chunk
  • Chỉ dẫn rõ: "chỉ dùng context được cung cấp"
  • Chỉ dẫn khi không có câu trả lời: "nói thẳng là không biết"

Đừng:

  • Đổ raw chunks không cấu trúc gì
  • Để LLM tự đoán cách ghi nguồn
  • Quên hướng dẫn cách xử lý context

Khi Không Có Câu Trả Lời

Context truy xuất ra mà không chứa câu trả lời thì sao?

Đúng: "Mình không có thông tin về điều đó trong knowledge base."

Sai: Bịa đại dựa trên training data -- user tưởng đó là sự thật, nguy hiểm.

System prompt phải nói rõ: không biết thì nói không biết, đừng bịa.

Patterns Nâng Cao

Query Expansion

User hỏi mơ hồ thì tạo thêm các cách diễn đạt khác trước khi tìm:

  • Gốc: "Làm sao deploy?"
  • Mở rộng: "quy trình deployment", "releasing to production", "CI/CD pipeline"

Tìm với tất cả biến thể, gom lại, bỏ trùng.

Conversational RAG

Trong hội thoại nhiều lượt, query hiện tại hay tham chiếu context trước đó.

"Thế giá cả thì sao?" -- giá cả của cái gì? Phải nhìn lại context trước mới biết.

Giải pháp: chuyển query thành dạng standalone trước khi search. Dùng lịch sử hội thoại để giải quyết mấy cái tham chiếu kiểu này.

Chunk Overlap

Overlap giữa chunks để tránh cắt câu giữa chừng. Không overlap thì thông tin nằm đúng ranh giới hai chunks sẽ bị mất -- nửa này nửa kia, search không ra. Có overlap thì nội dung ranh giới nằm trong cả hai chunks, chắc ăn hơn.

Lỗi Thường Gặp

Chunks to quá hoặc nhỏ quá -- Kết quả không liên quan, thiếu context, hoặc vượt token limits. Bắt đầu với 500-1000 ký tự rồi test với nội dung thực.

Không lưu metadata -- Đến lúc cần trích dẫn nguồn hoặc lọc theo ngày/category thì hết cách. Luôn lưu source, date, và thuộc tính lọc.

Bỏ qua similarity threshold -- Kéo cả đống kết quả low-similarity vào context, toàn noise. Đặt threshold 0.7-0.8 rồi test thực tế.

Không xử lý "không có kết quả" -- Retrieval trả về rỗng mà không xử lý, LLM sẽ hallucinate thoải mái. Phải có logic riêng cho trường hợp này.

Nhồi quá nhiều context -- Vượt token limits, context quan trọng bị chôn vùi giữa đống rác. Giới hạn 5-10 chunks liên quan là đủ -- chất lượng hơn số lượng.

Checklist Đánh Giá

Đang ổn nếu:

  • Chunks truy xuất liên quan đến query
  • Phản hồi trích dẫn nguồn chính xác
  • LLM nói "không biết" khi context thiếu câu trả lời
  • Queries khác nhau trả về contexts khác nhau
  • Latency chấp nhận được (< 3s tổng)

Cần xem lại nếu:

  • Chunks không liên quan lẫn vào kết quả
  • LLM bỏ qua context, bịa thông tin
  • Cùng context trả về cho mọi query
  • Retrieval chậm (> 5s)
  • Không cập nhật hoặc xóa tài liệu được

Tham Khảo Nhanh

Pipeline Tóm Gọn

  1. Chunk -- Chia tài liệu (500-1000 ký tự, có overlap)
  2. Embed -- Chuyển thành vectors (OpenAI text-embedding-3-small)
  3. Store -- Lưu kèm metadata (Supabase + pgvector)
  4. Search -- Tìm chunks tương tự (cosine similarity > 0.7)
  5. Generate -- Trả lời với context (Claude kèm trích dẫn nguồn)

Quyết Định Chính

Quyết định Khuyến nghị
Kích thước chunk 500-1000 ký tự
Chunk overlap 100-200 ký tự
Embedding model text-embedding-3-small cho đa số
Retrieval limit 5-10 chunks
Similarity threshold 0.7-0.8

Lưu Gì Cùng Mỗi Chunk

  • Unique ID
  • Content text
  • Tên/URL tài liệu nguồn
  • Section hoặc heading
  • Ngày tạo/cập nhật
  • Embedding vector
  • Thuộc tính lọc (category, author, v.v.)