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
- Xử lý tài liệu -- Chia tài liệu thành chunks tìm kiếm được (chunking)
- Embedding -- Chuyển chunks thành vectors cho similarity search
- Retrieval -- Tìm chunks liên quan cho query
- 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
- Chunk -- Chia tài liệu (500-1000 ký tự, có overlap)
- Embed -- Chuyển thành vectors (OpenAI text-embedding-3-small)
- Store -- Lưu kèm metadata (Supabase + pgvector)
- Search -- Tìm chunks tương tự (cosine similarity > 0.7)
- 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.)