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ó RAGCó RAG
LLM bịa thông tinLLM trích dẫn nguồn
Không dùng được dữ liệu riêngTìm kiếm trong data của mình
Bị giới hạn bởi knowledge cutoffLuôn cập nhật
Hallucinations phổ biếnPhả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áiMất cái
Nhỏ (200-500 ký tự)Retrieval chính xác hơnCó 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ơnMatching 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ượcPhù hợp choCách hoạt động
Fixed sizeVăn bản chungCắt mỗi N ký tự
Paragraph-basedBài viết, blogsChia theo ngắt đoạn tự nhiên
Sentence-basedNội dung dày đặcMỗi 2-5 câu một chunk
SemanticTài liệu kỹ thuậtChia 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

ModelDimensionsDùng khi
text-embedding-3-small1536Đa số trường hợp, giá rẻ
text-embedding-3-large3072Cần chất lượng cao, chấp nhận tốn hơn
Multilingual modelsKhác nhauNộ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ọnPhù hợp cho
Supabase + pgvectorSetup nhanh, dùng chung với DB chính
PineconeScale lớn, managed service
WeaviateFiltering phức tạp
FAISSLocal/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)
ThresholdHiệu quả
0.8+Precision cao, ít kết quả -- có thể bỏ sót content liên quan
0.7Cân bằng, dùng làm mặc định
0.6Recall 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ượcKhi nào dùng
Simple similarityQueries đơn giản, rõ ràng
Hybrid (vector + keyword)Queries có thuật ngữ hoặc tên cụ thể
Query expansionQueries mơ hồ hoặc ngắn quá
Multi-queryCâ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 địnhKhuyến nghị
Kích thước chunk500-1000 ký tự
Chunk overlap100-200 ký tự
Embedding modeltext-embedding-3-small cho đa số
Retrieval limit5-10 chunks
Similarity threshold0.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.)