LLM 4 — RAG: kiến trúc & thực chiến

13 thg 7, 2026 7 lượt xem
#ai
#claude
#llm
#rag
#vector-search
#retrieval

Mở đầu: khi LLM không biết dữ liệu của bạn

Một mô hình ngôn ngữ lớn (LLM) như Claude rất giỏi suy luận và diễn đạt, nhưng nó chỉ biết những gì có trong dữ liệu huấn luyện tính đến thời điểm cắt (knowledge cutoff). Nó không biết:

  • Quy định nội bộ ngân hàng bạn ban hành tuần trước.
  • Hồ sơ khách hàng, sản phẩm, hợp đồng nằm trong hệ thống của bạn.
  • Bất kỳ tài liệu nào riêng tư hoặc mới hơn thời điểm huấn luyện.

Nếu cứ hỏi thẳng, mô hình có hai lựa chọn tệ: trả lời "tôi không biết", hoặc bịa ra (hallucinate) một câu nghe rất thuyết phục nhưng sai. Với ngân hàng, câu trả lời sai về lãi suất hay quy trình có thể gây hậu quả pháp lý.

RAG (Retrieval-Augmented Generation) giải quyết đúng vấn đề này: thay vì hỏi mô hình "chay", ta tìm trước những đoạn tài liệu liên quan, nhét chúng vào prompt làm ngữ cảnh, rồi yêu cầu mô hình trả lời chỉ dựa trên ngữ cảnh đó. Nói ngắn gọn: retrieval (tìm) + augmented generation (sinh có bổ trợ).

Vì sao chọn RAG thay vì fine-tune?

Có hai cách chính để "dạy" mô hình dữ liệu riêng: fine-tune (huấn luyện lại một phần trọng số) và RAG (nạp ngữ cảnh lúc chạy). RAG thắng trong đa số ca dùng doanh nghiệp vì:

  • Cập nhật tức thì: thêm/sửa/xóa tài liệu chỉ cần cập nhật index — không phải huấn luyện lại. Fine-tune muốn cập nhật kiến thức phải chạy lại cả một quy trình tốn kém.
  • Rẻ hơn nhiều: bạn chỉ trả tiền embedding + token ngữ cảnh, không trả tiền GPU huấn luyện.
  • Truy vết được nguồn: RAG biết câu trả lời dựa trên đoạn nào → hiện được trích dẫn. Fine-tune "trộn" kiến thức vào trọng số, không chỉ ra được nguồn.
  • Kiểm soát rò rỉ: dữ liệu nhạy cảm nằm trong store của bạn, chỉ nạp khi cần.

Fine-tune vẫn hữu ích khi bạn muốn thay đổi phong cách/định dạng đầu ra hoặc dạy một kỹ năng ổn định. Nhưng để mô hình "biết thêm sự thật mới", RAG gần như luôn là lựa chọn đầu tiên. (Xem thêm bài Tool use & agents để biết khi nào nên kết hợp RAG với công cụ.)


Bức tranh tổng thể: hai pipeline

RAG gồm hai luồng tách biệt. Luồng offline (ingest) chạy trước, biến tài liệu thô thành các vector có thể tìm kiếm. Luồng online (truy vấn) chạy mỗi lần người dùng hỏi.

Điểm mấu chốt: vector store là cầu nối giữa hai luồng. Offline ghi vào, online đọc ra. Ta sẽ đi qua từng bước.


Pipeline offline (ingest)

Mục tiêu: biến kho tài liệu thành các vector tìm kiếm được. Bốn bước.

1. Thu thập & làm sạch

Kéo tài liệu từ nguồn (kho file, wiki, cơ sở dữ liệu, hệ thống văn bản), rồi làm sạch: bỏ header/footer lặp lại, chuyển bảng biểu về text đọc được, chạy OCR cho ảnh scan, gỡ ký tự rác. Rác đầu vào → rác đầu ra: một chunk toàn ký tự vô nghĩa sẽ làm nhiễu kết quả tìm kiếm.

2. Chunk — cắt tài liệu thành đoạn

Không thể embed cả tài liệu 50 trang thành một vector — sẽ mất chi tiết. Ta cắt thành chunk nhỏ (thường 200–800 token). Chất lượng chunk quyết định phần lớn chất lượng RAG:

  • Cắt theo ngữ nghĩa, không cắt giữa câu/giữa ý. Ưu tiên ranh giới tự nhiên: đoạn văn, mục, tiêu đề.
  • Overlap nhẹ (ví dụ 10–15%) giữa các chunk để không mất ý ở ranh giới.
  • Gắn metadata cho mỗi chunk: nguồn, tiêu đề mục, ngày ban hành, phòng ban — sẽ dùng để lọc và trích dẫn.

Chi tiết cách chọn kích thước chunk và mô hình embedding nằm ở bài Embeddings & vector search.

3. Embed — biến chunk thành vector

Mỗi chunk được đưa qua một mô hình embedding (ví dụ Voyage) để ra một vector số chiều cao. Các đoạn văn gần nghĩa sẽ có vector gần nhau trong không gian. Đây là nền tảng của tìm kiếm ngữ nghĩa. (Xem bài Embeddings ở trên.)

4. Lưu vào vector store (pgvector)

Vector cùng metadata được ghi vào một vector store. Ở đây ta dùng pgvector — extension của PostgreSQL — vì nó cho phép lưu vector cùng chỗ với dữ liệu quan hệ và metadata, dùng SQL quen thuộc, và hỗ trợ cả tìm kiếm vector lẫn full-text.

-- Bảng lưu chunk (minh hoạ)
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE doc_chunks (
    id          BIGSERIAL PRIMARY KEY,
    doc_id      TEXT        NOT NULL,   -- id tài liệu gốc
    title       TEXT,                    -- tiêu đề để trích dẫn
    section     TEXT,                    -- mục/điều khoản
    content     TEXT        NOT NULL,   -- nội dung chunk
    embedding   vector(1024),           -- vector embedding
    tsv         tsvector                 -- cho full-text search
);

-- Index cho tìm kiếm vector (cosine) và full-text
CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON doc_chunks USING gin (tsv);

Việc ingest này chạy một lần khi thêm tài liệu (và chạy lại khi tài liệu thay đổi). Đây là điểm quan trọng ở phần "Lỗi thường gặp" bên dưới: quên cập nhật index thì RAG trả lời theo dữ liệu cũ.


Pipeline online (truy vấn)

Đây là luồng chạy mỗi lần người dùng hỏi. Sáu bước.

1. Embed câu hỏi

Câu hỏi được đưa qua cùng mô hình embedding đã dùng lúc ingest (bắt buộc cùng mô hình, cùng số chiều) để ra vector câu hỏi.

2. Retrieve top-k

Tìm các chunk có vector gần nhất với vector câu hỏi — thường là top-k (k = 5–20). Với pgvector, đây là một câu SQL sắp xếp theo khoảng cách cosine:

-- Tìm 8 chunk gần nghĩa nhất với vector câu hỏi (minh hoạ)
SELECT id, title, section, content,
       1 - (embedding <=> :query_vec) AS score
FROM doc_chunks
ORDER BY embedding <=> :query_vec   -- <=> là khoảng cách cosine
LIMIT 8;

3. Hybrid search (vector + full-text)

Tìm kiếm thuần ngữ nghĩa (vector) đôi khi bỏ sót khi câu hỏi chứa từ khóa chính xác (mã sản phẩm, số điều khoản, tên riêng). Ngược lại, tìm kiếm từ khóa (BM25 / full-text) bỏ sót khi diễn đạt khác nhau. Hybrid search kết hợp cả hai rồi trộn điểm — cho kết quả bền hơn hẳn.

Anthropic báo cáo rằng riêng cách nhúng ngữ cảnh (contextual embeddings) giảm 35% tỷ lệ retrieval thất bại; kết hợp thêm BM25 (contextual BM25) giảm 49%; và thêm reranking nữa thì giảm 67%. Những con số này cho thấy hybrid + rerank không phải "trang trí" mà là đòn bẩy chất lượng lớn.

4. Rerank (tùy chọn nhưng đáng giá)

Top-k ban đầu được sắp bằng độ tương đồng vector — nhanh nhưng thô. Một reranker (thường là mô hình cross-encoder, ví dụ Voyage rerank) đọc từng cặp (câu hỏi, chunk) và chấm điểm liên quan chính xác hơn nhiều. Quy trình: lấy top-k rộng (ví dụ 20–50), rerank, giữ lại top-n hẹp (ví dụ 5) để nhét vào prompt.

Đánh đổi: reranking thêm độ trễ và chi phí, nhưng loại được nhiễu — quan trọng vì "context nhồi quá nhiều/nhiễu" là một lỗi phổ biến (xem bên dưới).

5. Dựng prompt — chèn ngữ cảnh trong thẻ XML

Đây là bước ghép: đưa các chunk đã chọn vào prompt một cách có cấu trúc. Với Claude, cách hiệu quả là bọc tài liệu trong thẻ XML (<documents>) để mô hình phân biệt rõ đâu là ngữ cảnh, đâu là câu hỏi. Kèm theo các ràng buộc để giảm hallucination: chỉ dùng ngữ cảnh, nói "không tìm thấy" nếu thiếu, và trích dẫn nguồn.

6. Claude generate + trích dẫn

Cuối cùng gọi Claude sinh câu trả lời, kèm yêu cầu chỉ ra chunk nào đã dùng.


Prompt cho RAG: chống bịa đặt

Prompt là nơi bạn "kỷ luật" mô hình. Một khung prompt RAG tốt cho Claude:

Bạn là trợ lý hỏi đáp tài liệu nội bộ. Chỉ trả lời DỰA TRÊN các tài
liệu trong thẻ <documents> dưới đây. Nếu thông tin không có trong tài
liệu, hãy trả lời chính xác: "Không tìm thấy trong tài liệu." Tuyệt đối
không suy đoán hay dùng kiến thức ngoài.

Với mỗi khẳng định, hãy trích dẫn nguồn bằng cách ghi [tên_tài_liệu, mục].

<documents>
  <document index="1" title="Quy định tín dụng 2026" section="Điều 4.2">
    Hạn mức vay tín chấp tối đa bằng 10 lần thu nhập hàng tháng...
  </document>
  <document index="2" title="Biểu lãi suất Q2/2026" section="Bảng 1">
    Lãi suất vay tiêu dùng: 11,5%/năm...
  </document>
</documents>

Câu hỏi: Hạn mức vay tín chấp tối đa là bao nhiêu?

Ba nguyên tắc vàng trong prompt trên:

  1. Chỉ dùng ngữ cảnh — chặn mô hình "sáng tác".
  2. Cho phép nói "không biết" — thà thừa nhận thiếu còn hơn bịa. Đây là kỹ thuật giảm hallucination hiệu quả nhất.
  3. Bắt trích dẫn — vừa để người dùng kiểm chứng, vừa tạo áp lực buộc mô hình bám sát nguồn.

Xem thêm kỹ thuật viết prompt (thẻ XML, few-shot, ràng buộc định dạng) ở bài Prompt engineering.

Citations dựng sẵn của Claude

Ngoài cách tự dựng trích dẫn trong prompt, Claude còn có tính năng Citations dựng sẵn: bật citations: {enabled: true} trên mỗi khối document, và câu trả lời sẽ được tách thành các khối text, trong đó khối nào có dẫn nguồn sẽ kèm mảng citations với cited_text (đoạn văn gốc được trích), document_index, document_title, và vị trí (theo ký tự, theo trang, hoặc theo khối nội dung). Điều này cho phép hiện chính xác đoạn nào chống lưng cho từng câu — rất hợp với ngân hàng cần truy vết.

Lưu ý: Citations không dùng chung được với structured outputs (output_config.format) — nếu bật cả hai, API trả lỗi 400. Chọn một trong hai tùy nhu cầu.


Ví dụ: luồng RAG rút gọn với Claude

Dưới đây là luồng online rút gọn: retrieve từ pgvector → dựng prompt XML → gọi Claude. Dùng Anthropic SDK (Python).

# (minh hoạ) Luồng RAG online rút gọn
import anthropic
import psycopg  # driver PostgreSQL

client = anthropic.Anthropic()  # đọc ANTHROPIC_API_KEY từ môi trường

def embed_query(text: str) -> list[float]:
    """Embed câu hỏi bằng CÙNG mô hình đã dùng lúc ingest (Voyage)."""
    import voyageai
    vo = voyageai.Client()
    return vo.embed([text], model="voyage-3", input_type="query").embeddings[0]

def retrieve(query_vec: list[float], k: int = 8) -> list[dict]:
    """Retrieve top-k chunk gần nghĩa nhất từ pgvector."""
    sql = """
        SELECT title, section, content,
               1 - (embedding <=> %s::vector) AS score
        FROM doc_chunks
        ORDER BY embedding <=> %s::vector
        LIMIT %s
    """
    with psycopg.connect("postgresql://localhost/kb") as conn:
        rows = conn.execute(sql, (query_vec, query_vec, k)).fetchall()
    return [
        {"title": t, "section": s, "content": c, "score": sc}
        for (t, s, c, sc) in rows
    ]

def build_prompt(chunks: list[dict], question: str) -> str:
    """Dựng ngữ cảnh trong thẻ XML <documents>."""
    docs = "\n".join(
        f'  <document index="{i+1}" title="{c["title"]}" '
        f'section="{c["section"]}">\n    {c["content"]}\n  </document>'
        for i, c in enumerate(chunks)
    )
    return (
        "Chỉ trả lời dựa trên tài liệu trong <documents>. "
        'Nếu thiếu thông tin, trả lời "Không tìm thấy trong tài liệu." '
        "Với mỗi khẳng định, ghi trích dẫn [tiêu đề, mục].\n\n"
        f"<documents>\n{docs}\n</documents>\n\n"
        f"Câu hỏi: {question}"
    )

def answer(question: str) -> str:
    qvec = embed_query(question)
    chunks = retrieve(qvec, k=8)
    # (Tùy chọn) rerank ở đây rồi cắt xuống top-n hẹp trước khi dựng prompt.
    prompt = build_prompt(chunks, question)
    resp = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=1024,
        system="Bạn là trợ lý hỏi đáp tài liệu nội bộ, chính xác và trung thực.",
        messages=[{"role": "user", "content": prompt}],
    )
    return next(b.text for b in resp.content if b.type == "text")

print(answer("Hạn mức vay tín chấp tối đa là bao nhiêu?"))

Đây là "xương sống" của mọi hệ RAG. Trong thực tế bạn sẽ thêm hybrid search, rerank, streaming, và xử lý lỗi — nhưng cấu trúc embed → retrieve → prompt → generate vẫn giữ nguyên.


Mẫu nâng cao

Khi RAG cơ bản chưa đủ tốt, có nhiều mẫu để nâng chất lượng:

  • Query rewriting / expansion: câu hỏi người dùng thường mơ hồ hoặc thiếu từ khóa. Dùng một lượt gọi LLM để viết lại hoặc mở rộng câu hỏi (thêm từ đồng nghĩa, làm rõ ý) trước khi embed → retrieve trúng hơn.
  • Multi-query: sinh nhiều biến thể của câu hỏi, retrieve cho từng biến thể, rồi gộp và khử trùng kết quả. Bao phủ rộng hơn một câu hỏi đơn.
  • Metadata filtering: kết hợp lọc theo metadata với tìm vector — ví dụ "chỉ tìm trong tài liệu phòng Tín dụng, ban hành sau 2025". Với pgvector đây là mệnh đề WHERE bình thường đặt cạnh ORDER BY embedding.
  • Hierarchical / parent-child chunk: embed chunk nhỏ để tìm chính xác, nhưng khi trả về thì nạp cả chunk cha (đoạn lớn hơn) làm ngữ cảnh — vừa trúng vừa đủ ngữ cảnh.
  • Contextual retrieval: trước khi embed, dùng Claude sinh 50–100 token ngữ cảnh mô tả chunk đó nằm ở đâu trong tài liệu (công ty nào, quý nào, mục nào) rồi ghép vào đầu chunk. Chính kỹ thuật này cho các con số giảm 35–67% ở trên. Prompt caching giúp làm việc này rẻ (bộ nhớ đệm tài liệu gốc, chỉ ~1 USD/triệu token tài liệu).
  • Agentic RAG: thay vì luôn retrieve, để mô hình tự quyết định khi nào cần tìm, tìm gì, và có cần tìm lại không — retrieval trở thành một công cụ (tool) mà agent gọi. Xem bài Tool use & agents và bài chuyên sâu RAG & agents.

Đánh giá RAG

RAG có hai chỗ có thể hỏng nên phải đánh giá tách bạch:

  • Retrieval (khâu tìm): đo bằng recall@k — trong top-k trả về, có bao nhiêu % chứa đoạn đúng cần thiết? Nếu recall thấp, mọi thứ phía sau vô nghĩa vì mô hình không có nguyên liệu đúng.
  • Generation (khâu sinh): đo bằng
    • faithfulness (trung thực): câu trả lời có bám sát ngữ cảnh không, hay bịa thêm?
    • relevance (liên quan): câu trả lời có đúng trọng tâm câu hỏi không?

Một mẹo thực chiến: nếu chất lượng kém, hãy xác định hỏng ở đâu trước. Kiểm tra top-k retrieve ra có chứa đoạn đúng không. Nếu mà trả lời vẫn sai → lỗi ở generation (prompt, mô hình). Nếu không → lỗi ở retrieval (chunk, embedding, k, hybrid). Chi tiết cách đo, xây bộ eval và dùng LLM làm giám khảo nằm ở bài Đánh giá.


Lỗi thường gặp

Đây là những cái bẫy khiến RAG "chạy được mà không dùng được":

  • Chunk kém: cắt giữa câu, chunk quá to (loãng) hoặc quá nhỏ (mất ngữ cảnh), không có overlap. Đây là nguyên nhân số một khiến retrieval tệ.
  • Chọn sai top-k: k quá nhỏ bỏ sót đoạn đúng; k quá lớn nhồi nhiễu vào prompt. Cân bằng bằng cách retrieve rộng rồi rerank xuống hẹp.
  • Nhồi context quá nhiều / nhiễu: nhét cả chục chunk dài vào prompt làm mô hình "loãng chú ý", dễ bám vào đoạn sai. Ít mà tinh (đã rerank) thường tốt hơn nhiều mà tạp.
  • Không trích dẫn: mất khả năng kiểm chứng, và mô hình dễ trôi khỏi nguồn. Luôn bắt trích dẫn.
  • Quên cập nhật index: tài liệu đổi mà không chạy lại ingest → RAG trả lời theo bản cũ. Cần quy trình đồng bộ (theo dõi thay đổi, re-embed phần đổi).
  • Dùng khác mô hình embedding giữa ingest và query: vector không cùng không gian → kết quả tìm kiếm vô nghĩa. Phải cùng một mô hình, cùng số chiều.

Nhắc về chi phí: khi ngữ cảnh lớn lặp lại (ví dụ cùng bộ tài liệu chung cho nhiều câu hỏi), hãy dùng prompt caching để không trả tiền xử lý lại phần cố định — tiết kiệm đáng kể. Chi tiết ở bài Production.


Use case thực tế: chatbot hỏi đáp tài liệu nội bộ ngân hàng

Hình dung một ngân hàng muốn chatbot cho nhân viên chi nhánh tra cứu quy định, biểu lãi suất, và quy trình nghiệp vụ — thứ thay đổi liên tục và tuyệt đối không được trả lời sai.

Ingest (offline): mỗi khi phòng Pháp chế ban hành văn bản mới, một job tự động: tải văn bản → làm sạch → chunk theo điều/khoản (ranh giới tự nhiên của văn bản pháp lý) → gắn metadata (số hiệu văn bản, ngày hiệu lực, phòng ban) → embed bằng Voyage → ghi vào pgvector. Văn bản hết hiệu lực được đánh dấu để loại khỏi kết quả.

Truy vấn (online): nhân viên hỏi "Hạn mức vay tín chấp cho khách hàng có thu nhập 30 triệu/tháng?". Hệ thống:

  1. Rewrite câu hỏi cho rõ, embed.
  2. Hybrid search (vector + full-text) trong pgvector, lọc metadata chỉ lấy văn bản còn hiệu lực.
  3. Rerank xuống 4 chunk sát nhất.
  4. Dựng prompt XML, yêu cầu Claude chỉ dùng ngữ cảnh, nói "không tìm thấy" nếu thiếu, và trích dẫn số hiệu văn bản + điều khoản.
  5. Claude trả lời: "Hạn mức tối đa là 300 triệu (10 lần thu nhập) [Quy định tín dụng 2026, Điều 4.2]."

Nhờ trích dẫn, nhân viên bấm vào nguồn để kiểm chứng ngay — điều bắt buộc trong môi trường tuân thủ. Nhờ "không tìm thấy", chatbot không bịa khi gặp câu hỏi ngoài phạm vi tài liệu. Nhờ cập nhật index, câu trả lời luôn theo văn bản mới nhất.


Ghi nhớ

  • RAG = retrieve + augmented generation: tìm ngữ cảnh liên quan, nhét vào prompt, cho LLM trả lời dựa trên đó. Rẻ và cập nhật nhanh hơn fine-tune, lại truy vết được nguồn.
  • Hai pipeline: offline (ingest → chunk → embed → lưu pgvector) và online (embed câu hỏi → retrieve → rerank → prompt → Claude generate).
  • Chunk là gốc rễ chất lượng — cắt theo ngữ nghĩa, overlap nhẹ, gắn metadata.
  • Hybrid search + reranking giảm mạnh tỷ lệ tìm sai (Anthropic báo cáo tới 67% khi kết hợp contextual retrieval + rerank).
  • Prompt RAG kỷ luật: bọc tài liệu trong thẻ XML, chỉ dùng ngữ cảnh, cho phép nói "không tìm thấy", luôn trích dẫn.
  • Đánh giá tách bạch retrieval (recall@k) và generation (faithfulness, relevance) để biết hỏng ở đâu.
  • Tránh các bẫy: chunk kém, top-k sai, nhồi context nhiễu, không trích dẫn, quên cập nhật index, lệch mô hình embedding.
  • Prompt caching giúp rẻ khi ngữ cảnh lớn lặp lại — nhớ khi lên production.

Đọc tiếp: Embeddings & vector search, Prompt engineering, Tool use & agents, Đánh giá, Production.

Bài viết liên quan

Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.

13 thg 7, 2026 15

Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.

13 thg 7, 2026 13

Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.

13 thg 7, 2026 13

Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.

13 thg 7, 2026 11