RAG — khi LLM không đủ thông minh một mình
RAG là gì, tại sao cần nó, và những vấn đề thực tế khi build: chunk strategy, embedding drift, model swap, latency. Không phải tutorial, là kinh nghiệm.
LLM biết rất nhiều thứ. Nhưng nó không biết codebase của bạn. Không biết docs nội bộ. Không biết Slack thread từ tháng trước về cái bug production.
Bạn fine-tune thì tốn tiền, tốn thời gian, và mỗi lần data thay đổi lại phải train lại. Context window nhét tất cả vào thì được — nhưng $0.03/1K tokens với 100K tokens mỗi request thì không sustainable.
RAG là cách khác.
RAG là gì
Retrieval-Augmented Generation — thay vì nhét hết context vào prompt, bạn chỉ lấy đúng phần liên quan rồi mới hỏi LLM.
Flow cơ bản:
- Index time — chunk documents → embed thành vector → store vào vector DB
- Query time — embed câu hỏi → similarity search → lấy top-k chunks → nhét vào prompt → gọi LLM
LLM lúc này không cần “nhớ” mọi thứ. Nó chỉ cần đọc đúng tài liệu và trả lời dựa trên đó.
Nghe đơn giản. Build xong mới thấy devil ở trong details.
Khó khăn thực tế
1. Chunking strategy
Chunk như nào là câu hỏi đầu tiên và quan trọng nhất.
Fixed-size chunking — chia mỗi 512 token, overlap 50 token. Đơn giản, dễ implement. Nhưng cắt ngang câu, ngang paragraph. Semantic context bị mất.
Semantic chunking — chia theo paragraph, section, hoặc dùng sentence embeddings để detect topic boundary. Kết quả tốt hơn nhưng phức tạp hơn. Và với structured data như code thì cần strategy khác nữa.
Hierarchical chunking — document → sections → paragraphs, lưu cả 3 level, query trả về chunk nhỏ nhưng kèm parent context. Tốt nhưng storage gấp 3x, latency cao hơn.
Không có one-size-fits-all. Tớ từng dùng fixed-size cho markdown docs, semantic cho prose, function-level cho code. Mỗi loại một pipeline riêng.
2. Embedding quality
Embedding model quyết định chất lượng retrieval. Model tệ → search ra chunk sai → LLM hallucinate dù có context.
Vấn đề: bạn không thấy lỗi này ngay. LLM vẫn trả lời, trả lời tự tin, nhưng từ chunk không liên quan. Silent failure — nguy hiểm nhất trong AI systems.
Cần eval pipeline: golden dataset gồm (question, expected_chunk_ids), đo recall@k và MRR. Không có numbers thì không biết mình đang ở đâu.
3. Reranking
Top-k từ vector search không phải lúc nào cũng là top-k tốt nhất. Vector similarity ≠ semantic relevance trong mọi trường hợp.
Cross-encoder reranker — sau khi lấy top-20 chunks, dùng model nhỏ hơn để score từng cặp (question, chunk) và sắp xếp lại. Kết quả tốt hơn đáng kể. Chi phí: thêm latency ~100-300ms.
Với production system, đây là trade-off cần cân nhắc.
4. Context assembly
Bạn có 5 chunks liên quan. Nhét vào prompt theo thứ tự nào? Chunk quan trọng nhất đặt đầu hay cuối?
LLM có lost-in-the-middle problem — thông tin ở giữa context window bị “quên” nhiều hơn thông tin ở đầu và cuối. Đặt chunk quan trọng nhất ở đầu hoặc cuối prompt, không phải giữa.
Và đừng quên: context window có giới hạn. Nếu tổng chunks vượt quá budget, phải trim. Trim không khéo là mất information.
Vấn đề core: model thay đổi
Đây là vấn đề ít được nói đến nhưng gây đau nhất khi operate.
Embedding drift
Bạn index 100K documents bằng text-embedding-ada-002. 6 tháng sau OpenAI release text-embedding-3-large — tốt hơn, rẻ hơn.
Bạn không thể chỉ đổi model cho query mà giữ nguyên index. Embedding từ 2 model khác nhau không compatible — vector space khác nhau hoàn toàn. Similarity search sẽ trả về rác.
Giải pháp: re-index toàn bộ. Với 100K documents, đó là:
- Chi phí embedding API không nhỏ
- Thời gian downtime hoặc phức tạp khi run song song 2 index
- Risk: nếu re-index lỗi giữa chừng thì sao?
Cách xử lý:
Track embedding_model cho từng document trong metadata. Khi swap model, chạy migration job incremental — process từng batch, mark migrated: true, giữ nguyên index cũ cho đến khi migration xong 100%. Sau đó flip pointer.
documents/
- id: doc_123
content: "..."
embedding_model: "text-embedding-ada-002"
migrated: false
chunks: [...]
Dual-write trong transition period nếu cần zero-downtime. Tốn storage gấp đôi nhưng an toàn.
LLM version change
gpt-4-turbo → gpt-4o — behavior thay đổi. Prompt format optimal cho model cũ có thể không optimal cho model mới. Một số prompt tricks hoạt động với Claude 2 không còn cần thiết với Claude 3.
Mỗi lần swap LLM, cần regression test toàn bộ eval set. Không test thì không biết quality thay đổi như nào.
Version pinning — pin model version trong config, không để "latest". Upgrade có chủ đích, không tự động. Đau một lần khi upgrade còn hơn production bị ảnh hưởng không rõ lý do.
llm:
model: "claude-3-5-sonnet-20241022" # pinned, không phải "claude-3-5-sonnet-latest"
embedding:
model: "text-embedding-3-small"
version: "2024-01-01"
Context length thay đổi
Model mới thường có context window lớn hơn. Bạn có thể tăng số chunks, tăng chunk size — và đột nhiên response quality thay đổi vì behavior của model với long context khác.
Thay đổi context budget cũng cần test lại như thay đổi model.
Một số gotcha khác
Stale index — documents update mà quên re-embed. Cần change detection: hash content, so sánh mỗi lần ingest, chỉ re-embed khi content thay đổi.
Query expansion — câu hỏi ngắn của user thường không khớp tốt với chunks dài. Dùng LLM để rewrite query thành dạng tốt hơn cho retrieval trước khi embed. Thêm một LLM call nhưng retrieval quality tăng rõ rệt.
Metadata filtering — đừng search toàn bộ index nếu bạn biết document thuộc category nào. Filter trước, search sau. Vừa nhanh hơn vừa chính xác hơn.
Tóm lại
RAG không phải plug-and-play. Nó là một system với nhiều moving parts — indexing pipeline, embedding model, vector DB, reranker, prompt engineering, eval.
Mỗi part có failure mode riêng. Silent failure là nguy hiểm nhất vì LLM luôn trả về gì đó nghe có vẻ đúng.
Invest vào eval pipeline từ đầu. Track embedding model version từ ngày đầu. Pin mọi thứ. Test trước khi upgrade.
Và chunk strategy — thử nhiều, đo kỹ, đừng tin intuition.