Quay lại hướng dẫn
Kiến trúc·25 tháng 5, 2026·Cập nhật 23 tháng 9, 2026·Đọc 14 phút

Hướng dẫn triển khai RAG — generation tăng cường truy xuất trong production với Claude và Gemini

Xây dựng hệ thống RAG dùng trong môi trường sản xuất thực sự hoạt động — không chỉ là bản demo 100 dòng. Bao gồm chiến lược chia đoạn, lựa chọn mô hình embedding, xếp hạng truy xuất, cấu trúc prompt, kiểm soát ảo giác và cách giữ chi phí hàng tháng ở mức ba chữ số ngay cả với 10.000 truy vấn/ngày.

Được xem xét lần cuối vào .

Hầu hết bản demo RAG đều thất bại khi đưa vào production. Chúng hoạt động với bộ câu hỏi demo 10 câu rồi hỏng ngay khi người dùng hỏi điều gì đó mới. Hướng dẫn này trình bày kiến trúc và các mẫu có thể đứng vững: chunking tôn trọng ranh giới ngữ nghĩa, retrieval kết hợp bắt được cả truy vấn mơ hồ lẫn nguyên văn, cấu trúc prompt ngăn hallucination, và kỷ luật chi phí giữ hóa đơn ổn định khi mở rộng.

Năm quyết định quan trọng

  1. Chiến lược chunking — ảnh hưởng đến chất lượng retrieval nhiều hơn model
  2. Model embedding — quyết định trần recall
  3. Chiến lược retrieval — chỉ vector là chưa đủ
  4. Cấu trúc prompt — quyết định model có hallucinate hay không
  5. Model generation + caching — xác định kinh tế đơn vị

1. Chunking — nhàm chán tốt hơn thông minh

Đừng suy nghĩ quá nhiều về việc này. Dùng recursive character splitter với 1000-1500 ký tự mỗi chunk và phần chồng lấp 100-200 ký tự. Splitter cố gắng ngắt ở ranh giới đoạn văn trước, sau đó đến câu rồi từ. Ổn định trên nhiều lĩnh vực:

chunk.py
# Recursive character splitter with overlap — the boring choice that wins
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1200,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
# 1200 chars ≈ 300-400 tokens — fits 5+ chunks in a Sonnet context window
# with room for system prompt + question + answer

Những thứ trông thông minh hơn nhưng hiếm khi hữu ích: model nhận biết câu, splitter nhận biết markdown, sliding window với embedding. Chúng không tệ — chỉ tốt hơn đôi chút nhưng đòi hỏi nỗ lực rất lớn. Hãy dành nỗ lực đó cho retrieval.

2. Model embedding — text-embedding-3-large là lựa chọn mặc định

Kunavo không cung cấp embedding — hãy gọi trực tiếp một nhà cung cấp embedding cho bước này. /v1/embeddings là wire format đã được triển khai nhưng không có model nào được bật phía sau, vì vậy yêu cầu đến đó sẽ thất bại; GET /v1/models luôn là nguồn chính xác về những gì có thể gọi. Trên thực tế, việc này không làm pipeline RAG tốn thêm gì ngoài một khóa thứ hai: lệnh gọi embedding và lệnh gọi generation vốn là các yêu cầu riêng biệt, nên hãy embed bằng OpenAI, Voyage hoặc Cohere và generate bằng Kunavo.

text-embedding-3-large đạt điểm cân bằng giữa đa ngôn ngữ và độ chính xác cho hầu hết trường hợp sử dụng production, theo mức giá được OpenAI công bố — hãy kiểm tra trên trang giá của họ thay vì ở đây, vì chúng tôi không bán lại dịch vụ đó và không nên trích dẫn một con số cho nó. Hãy re-embed khi thay đổi chiến lược chunking hoặc chính model; không cần làm vậy khi nội dung cập nhật (chỉ cần thêm chunk mới).

Các trường hợp embedding nhỏ hơn hoạt động tốt: retrieval văn bản ngắn hoàn toàn bằng tiếng Anh (FAQ, thẻ sản phẩm). Với các trường hợp đó, text-embedding-3-small rẻ hơn 4 lần với mức suy giảm chất lượng không đáng kể. Với nội dung đa ngôn ngữ hoặc kỹ thuật, hãy tiếp tục dùng large.

3. Retrieval — vector thuần túy thua hybrid

Độ tương đồng vector rất tốt với việc khớp ngữ nghĩa mơ hồ (“làm thế nào để hủy” → “chính sách hủy đăng ký”). Nó rất tệ với các thuật ngữ nguyên văn (“SKU-A92837” hoặc “đơn hàng #4729”). Cách khắc phục là hybrid: tìm kiếm từ khóa BM25 song song, sau đó reciprocal rank fusion:

retrieve.py
# Hybrid retrieval: vector similarity + BM25 keyword scoring
# Pure vector misses literal keywords (product SKUs, IDs, dates)
def retrieve(question: str, k: int = 5) -> list[dict]:
    q_embed = embed([question])[0]
    vector_hits = vector_db.similarity_search(q_embed, k=k * 2)
    keyword_hits = bm25_search(question, k=k * 2)

    # Reciprocal rank fusion — simple, robust
    scores: dict[str, float] = {}
    for rank, hit in enumerate(vector_hits):
        scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
    for rank, hit in enumerate(keyword_hits):
        scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)

    top_ids = sorted(scores, key=scores.get, reverse=True)[:k]
    return [chunk_by_id[i] for i in top_ids]

Đây là mức cải thiện chất lượng lớn nhất sau khi chọn một model embedding phù hợp. Theo dõi recall@5 trên một bộ đánh giá 100 câu hỏi được giữ lại — nếu chỉ số tăng từ 70% lên 90% sau khi thêm BM25, bạn vừa loại bỏ một phần ba các lỗi “Tôi không biết”.

4. Cấu trúc prompt — ba mẫu ngăn hallucination

answer.py
# Final RAG prompt structure — three sections, citations enforced
SYSTEM_PROMPT = """You answer based exclusively on the supplied Context.
- Cite the [doc_id] for each factual claim.
- If the context doesn't answer the question, say "I don't have that
  information" — do not extrapolate or use general knowledge.
- Be concise. No throat-clearing."""

def answer(question: str) -> dict:
    chunks = retrieve(question, k=5)
    context = "\n\n---\n\n".join(
        f"[doc:{c['id']}] {c['text']}" for c in chunks
    )
    resp = client.chat.completions.create(
        model="claude-sonnet-4-6",
        messages=[
            {"role": "system", "content": [{
                "type": "text",
                "text": SYSTEM_PROMPT,
                "cache_control": {"type": "ephemeral"},
            }]},
            {"role": "user", "content": f"# Context\n{context}\n\n# Question\n{question}"},
        ],
        max_tokens=600,
    )
    return {
        "text": resp.choices[0].message.content,
        "sources": [c["id"] for c in chunks],
    }

Ba điều bắt buộc trong system prompt:

  • Trích dẫn nguồn — yêu cầu model gắn [doc:42]vào mỗi khẳng định. Nếu ID được trích dẫn không tồn tại, bạn đã bắt được một hallucination
  • Từ chối rõ ràng — “nếu ngữ cảnh không trả lời, hãy nói tôi không có thông tin đó.” Nếu không có điều này, model sẽ hoàn tất câu trả lời bằng kiến thức thế giới
  • Output súc tích — câu trả lời ngắn có tương quan với câu trả lời chính xác. Tối đa 600 token là mặc định production tốt

5. Model generation + caching — lớp chi phí

Claude Sonnet 4.6 là lựa chọn mặc định. Claude Haiku 4.5 là phương án dự phòng rẻ hơn nếu bạn chịu áp lực chi phí. Luôn bọc system prompt trong cache_control — system prompt giống nhau ở mọi lần gọi, nên caching giảm phần đó xuống còn 10% mức giá input.

Chi phí thực tế mỗi truy vấn ở quy mô production (5K context, 500 output, bật caching):

Mô hìnhChi phí mỗi truy vấnỞ mức 10.000 truy vấn/ngàyChất lượng so với Sonnet
claude-sonnet-4-6~$0,007~$70/ngàyMức cơ sở
claude-haiku-4-5~$0,001~$10/ngày~85%, rẻ hơn 4 lần

Ở mức 10.000 truy vấn/ngày → $70/ngày trên Sonnet, $10/ngày trên Haiku. Chọn Sonnet cho các trường hợp có rủi ro cao (hướng tới khách hàng, pháp lý, y tế), Haiku cho xử lý nội bộ/hàng loạt.

Điều gì hỏng trong production (và cách phát hiện)

  • Trôi phân phối: corpus huấn luyện không còn khớp với câu hỏi thực tế của người dùng. Lấy mẫu 100 truy vấn/tuần, kiểm tra recall@5 thủ công
  • Embedding cũ: tài liệu nguồn đã cập nhật nhưng embedding chưa được làm mới. Theo dõi kích thước index so với kích thước nguồn hằng tuần
  • Trích dẫn giả: model tạo ra các doc ID trông có vẻ hợp lệ. Xác thực mọi [doc:N] với danh sách ID thực tế đã truy xuất trước khi render
  • Vách đá độ trễ: vector DB suy giảm nghiêm trọng sau một triệu vector. Dùng HNSW indexing, phân vùng theo tenant nếu là multi-tenant

Lộ trình production trong 4 tuần

  1. Tuần 1: tạo prototype với 100 tài liệu, 10 câu hỏi kiểm thử, đánh giá thủ công
  2. Tuần 2: mở rộng lên toàn bộ corpus, xây dựng hybrid retrieval, viết bộ đánh giá 100 câu hỏi
  3. Tuần 3: tinh chỉnh chunking + system prompt bằng cách lặp trên bộ đánh giá, triển khai cho người dùng nội bộ
  4. Tuần 4: giám sát, ngân sách chi phí, phát hành công khai với UI trích dẫn nguồn

Câu hỏi thường gặp

Hệ thống RAG production nên sử dụng kích thước chunk bao nhiêu?

1.000–1.500 ký tự mỗi chunk với phần chồng lấp 100–200 ký tự, được chia bằng recursive character splitter, ưu tiên ngắt ở ranh giới đoạn văn, sau đó đến câu rồi từ. Các model nhận biết câu, splitter nhận biết markdown và sliding window tốt hơn đôi chút nhưng đòi hỏi nỗ lực lớn hơn nhiều — nỗ lực đó nên được dành cho retrieval thì hiệu quả hơn.

Pipeline RAG nên sử dụng model embedding nào?

text-embedding-3-large là lựa chọn mặc định cho nội dung đa ngôn ngữ và kỹ thuật; với retrieval văn bản ngắn hoàn toàn bằng tiếng Anh như FAQ và thẻ sản phẩm, text-embedding-3-small rẻ hơn khoảng 4 lần với mức suy giảm chất lượng không đáng kể. Lưu ý rằng Kunavo không cung cấp embedding: /v1/embeddings là wire format đã được triển khai nhưng không có model nào được bật phía sau, vì vậy bước embedding gọi trực tiếp OpenAI, Voyage hoặc Cohere còn bước generation gọi Kunavo. Việc này không làm pipeline tốn thêm gì ngoài một khóa thứ hai, vì dù sao hai bước cũng là các yêu cầu riêng biệt.

Chỉ vector search có đủ cho retrieval RAG không?

Không. Độ tương đồng vector xử lý tốt việc khớp ngữ nghĩa mơ hồ nhưng thất bại với các thuật ngữ nguyên văn như SKU hoặc mã đơn hàng. Chạy tìm kiếm từ khóa BM25 song song và hợp nhất bằng reciprocal rank fusion thường nâng recall@5 từ khoảng 70% lên khoảng 90% — mức cải thiện chất lượng lớn nhất sau khi chọn một model embedding phù hợp.

Làm thế nào để ngăn hệ thống RAG hallucinate?

Ba mẫu trong system prompt: yêu cầu trích dẫn [doc:N] cho mọi khẳng định thực tế, hướng dẫn model nói rằng nó không có thông tin khi ngữ cảnh không trả lời được, và giữ câu trả lời ngắn — tối đa 600 output token là mặc định production tốt. Sau đó xác thực mọi id được trích dẫn against tập kết quả đã truy xuất trước khi render; id không tồn tại là một hallucination đã được bắt.

Một truy vấn RAG production tốn bao nhiêu?

Với 5K context và 500 output token, bật prompt caching: khoảng $0,007 mỗi truy vấn trên Claude Sonnet 4.6 và khoảng $0,001 trên Claude Haiku 4.5 (rẻ hơn 4 lần, đạt khoảng 85% chất lượng). Với 10.000 truy vấn mỗi ngày, chi phí là khoảng $70/ngày trên Sonnet và khoảng $10/ngày trên Haiku.