RAG

RAG Là Gì? Từ Retrieval Cơ Bản Đến Kiến Trúc Production

RAG không đơn giản là đưa document vào vector database rồi gửi kết quả cho LLM. Bài viết đi từ embedding, cosine similarity và chunking đến hybrid retrieval, reranking, evaluation và kiến trúc RAG production.

Minh họa cho bài RAG Là Gì? Từ Retrieval Cơ Bản Đến Kiến Trúc Production
Mục lục bài viết

Giả sử bạn xây một chatbot nội bộ cho công ty.

Trong kho tài liệu có một PDF ghi rất rõ:

Chính sách hoàn tiền mới nhất cho phép khách hàng yêu cầu hoàn tiền trong vòng 30 ngày.

Người dùng hỏi:

“Tôi mua sản phẩm 20 ngày trước, bây giờ có được hoàn tiền không?”

Document đúng đang tồn tại trong hệ thống. LLM đủ mạnh. Vector database vẫn chạy bình thường. Không có exception nào trong log.

Nhưng chatbot lại trả lời:

“Theo chính sách, thời hạn hoàn tiền là 14 ngày.”

Điều gì đã sai?

Có thể chunk chứa chính sách mới chưa bao giờ được retrieve. Có thể embedding tìm nhầm một tài liệu cũ có ngữ nghĩa gần giống. Có thể cả chính sách 14 ngày và 30 ngày cùng xuất hiện trong top-k nhưng tài liệu cũ được xếp cao hơn. Có thể context quá dài khiến thông tin quan trọng bị chìm. Hoặc retrieval hoàn toàn đúng nhưng LLM vẫn tổng hợp sai.

Đây là điểm khiến nhiều hệ thống RAG hoạt động rất đẹp trong demo nhưng trở nên khó kiểm soát khi bước vào production.

RAG không đơn giản là:

Text
Document
   ↓
Vector Database
   ↓
LLM
   ↓
Answer

Một hệ thống thực tế giống một chuỗi quyết định hơn:

Text
Document có được parse đúng không?
            ↓
Chunk có giữ đúng ngữ nghĩa không?
            ↓
Embedding có biểu diễn đúng nội dung không?
            ↓
Retriever có tìm được evidence cần thiết không?
            ↓
Ranking có đưa evidence tốt lên đủ cao không?
            ↓
Context có được xây dựng hợp lý không?
            ↓
LLM có dùng đúng evidence đó không?
            ↓
Answer có thực sự grounded vào source không?

Chỉ cần một mắt xích hoạt động kém, câu trả lời cuối cùng có thể sai.

Đó cũng là mental model quan trọng nhất của bài này:

RAG không phải một feature được thêm vào LLM. RAG là một information-retrieval system đặt phía trước quá trình generation.

Khi nhìn theo cách đó, rất nhiều vấn đề tưởng như là “LLM hallucination” thực chất lại bắt đầu từ document parsing, chunking, retrieval hoặc ranking.

Ý tưởng gốc của Retrieval-Augmented Generation kết hợp parametric memory nằm trong trọng số của model với một non-parametric memory bên ngoài mà hệ thống có thể truy xuất tại thời điểm inference. Đây chính là khác biệt nền tảng giữa việc để model chỉ dựa vào những gì đã học trong training và việc cung cấp cho nó evidence động trước khi sinh câu trả lời. Nguồn research ban đầu cũng xây dựng RAG theo hướng đi từ embedding, similarity và retrieval trước khi mở rộng sang chunking, hybrid search, GraphRAG, Agentic RAG và evaluation.

Bài viết này sẽ đi theo đúng trục đó, nhưng tập trung vào một câu hỏi duy nhất:

Một RAG system thực sự hoạt động như thế nào, và điều gì phải thay đổi khi đưa nó từ demo lên production?


RAG thực chất đang giải quyết bài toán gì?

Một LLM thông thường có thể được hình dung như một hàm:

P(y∣x;θ)P(y \mid x;\theta)

Trong đó:

  • xx là prompt;
  • yy là output;
  • θ\theta là tập trọng số của model.

Thông tin mà model “biết” chủ yếu được mã hóa gián tiếp trong θ\theta. Vấn đề là trọng số không phải một database mà application có thể dễ dàng cập nhật mỗi khi công ty sửa một điều khoản hợp đồng, thêm một tài liệu mới hoặc thay đổi giá sản phẩm.

Nếu hôm nay công ty ban hành:

Text
Refund policy v3
Effective date: 2026-08-01
Refund window: 30 days

chúng ta không muốn fine-tune lại model chỉ để nó biết con số 30 days.

RAG thay đổi bài toán thành:

Dq=Retrieve⁡(q,D)D_q = \operatorname{Retrieve}(q, D)

sau đó:

P(y∣q,Dq;θ)P(y \mid q,D_q;\theta)

Trong đó:

  • qq là câu hỏi;
  • DD là kho tri thức bên ngoài;
  • DqD_q là tập evidence được retrieve;
  • LLM sinh câu trả lời dựa trên cả query lẫn evidence.

Mental model có thể rút gọn như sau:

Text
Without RAG

Question
   │
   ▼
┌───────┐
│  LLM  │───► Answer
└───────┘
     ▲
     │
Model weights

Với RAG:

Text
With RAG

                     ┌──────────────────┐
                     │ External Knowledge│
                     └────────┬─────────┘
                              │
Question ──► Retriever ───────┘
                │
                ▼
          Relevant Evidence
                │
                ▼
           ┌─────────┐
Question ─►│   LLM   │──► Answer
Evidence ─►│         │
           └─────────┘

Điểm quan trọng là RAG không chuyển toàn bộ database vào model.

Nó cố gắng tìm một tập evidence nhỏ nhưng đủ tốt để model xử lý.

Đó là lý do retrieval quality trở thành vấn đề sống còn.

Nếu evidence cần thiết không nằm trong context, một generator hoàn hảo cũng không thể sử dụng thông tin mà nó chưa từng được nhìn thấy.

RAG không đồng nghĩa với “không hallucination”

Đây là một trong những hiểu nhầm phổ biến nhất.

RAG có thể giúp model sinh câu trả lời factual hơn bằng cách cung cấp nguồn tri thức bên ngoài, nhưng nó không tạo ra một guarantee rằng output luôn đúng. Ngay cả paper RAG ban đầu cũng mô tả retrieval như cách kết hợp parametric và non-parametric memory cho các knowledge-intensive tasks, chứ không phải một cơ chế đảm bảo chân lý tuyệt đối.

Một RAG pipeline vẫn có thể thất bại theo hai nhóm lớn:

Text
Retrieval failure
        │
        ├── không retrieve đúng evidence
        ├── retrieve thiếu evidence
        ├── rank evidence sai
        └── retrieve dữ liệu cũ / nhiễu

Generation failure
        │
        ├── hiểu sai evidence
        ├── bỏ qua evidence
        ├── kết hợp evidence sai
        └── thêm claim không được source hỗ trợ

Phân biệt hai nhóm này cực kỳ quan trọng.

Nếu retrieval recall thấp mà bạn cứ thay prompt của LLM, rất có thể bạn đang tối ưu sai layer.


Một RAG system có hai pipeline, không phải một

Khi nhìn một RAG architecture hoàn chỉnh, nên tách nó thành hai luồng:

  1. Indexing pipeline — biến dữ liệu thô thành knowledge có thể search.
  2. Query pipeline — tìm evidence và dùng evidence đó để tạo câu trả lời.

Một kiến trúc production cơ bản có thể trông như sau:

Text
                    INDEXING PIPELINE

Raw Documents
     │
     ▼
┌──────────────┐
│ Parse/Clean  │
└──────┬───────┘
       ▼
┌──────────────┐
│   Chunking   │
└──────┬───────┘
       │
       ├──────────────► Document Store
       │
       ▼
┌──────────────┐
│  Embedding   │
└──────┬───────┘
       ▼
┌──────────────┐
│ Vector Index │
└──────────────┘


                     QUERY PIPELINE

User Query
    │
    ▼
Query Processing
    │
    ├────────► Lexical Search ───┐
    │                            │
    └────────► Vector Search ────┤
                                 ▼
                           Candidate Set
                                 │
                                 ▼
                          Fusion / Rerank
                                 │
                                 ▼
                          Context Builder
                                 │
                                 ▼
                              LLM
                                 │
                                 ▼
                       Answer + Citations

Hai pipeline này thường chạy ở hai thời điểm hoàn toàn khác nhau.

Indexing có thể diễn ra khi document được upload, khi một data source thay đổi hoặc theo batch schedule.

Query pipeline chạy khi user đặt câu hỏi.

Điều đó dẫn đến một implication quan trọng: RAG latency mà người dùng cảm nhận không nhất thiết phải bao gồm toàn bộ processing cost của document.

Parsing, chunking và document embedding thường có thể được thực hiện trước.

Online request chủ yếu phải trả chi phí cho:

Text
query embedding
+
retrieval
+
optional reranking
+
LLM generation

Đây cũng là lý do kiến trúc production nên tách rõ ingestion/indexing khỏi serving.


Từ text đến vector: retrieval hoạt động như thế nào?

Để hiểu RAG, không cần bắt đầu bằng LangChain hay bất kỳ framework nào.

Ta có thể bắt đầu với ba document:

Text
D1 = "PostgreSQL supports B-tree indexes."
D2 = "Redis can store data in memory."
D3 = "RAG retrieves external knowledge before generation."

Một embedding model biến mỗi đoạn text thành một vector:

Text
D1 → [0.12, -0.42, 0.91, ...]
D2 → [0.73,  0.13, 0.02, ...]
D3 → [0.05, -0.61, 0.88, ...]

Query:

Text
"What does RAG retrieve?"

cũng được đưa qua cùng embedding space:

Text
Q → [0.03, -0.58, 0.84, ...]

Bây giờ retrieval trở thành một bài toán hình học.

Nguồn research ban đầu mô tả đúng flow cốt lõi này: document được chunk, chuyển thành embedding, query cũng được embed vào cùng vector space, sau đó hệ thống so sánh query vector với document vectors để tìm các candidate gần nhất.

Cosine similarity

Một metric thường gặp là cosine similarity:

cosine(q,d)=q⋅d∥q∥∥d∥\text{cosine}(q,d)=\frac{q\cdot d}{\lVert q\rVert\lVert d\rVert}

Nó đo mức độ hai vector cùng hướng với nhau.

Ví dụ:

Text
similarity(Q, D1) = 0.42
similarity(Q, D2) = 0.18
similarity(Q, D3) = 0.91

Retriever sẽ ưu tiên D3.

Điểm cần hiểu không phải là 0.91 tương đương “91% đúng”.

Similarity score không phải probability.

Nó chỉ là một tín hiệu ranking trong embedding space.

🔬 Deep Dive — cosine similarity và dot product có thể trở thành cùng một ranking

Nếu vectors được normalize:

∥q∥=1,∥d∥=1\lVert q\rVert=1,\qquad \lVert d\rVert=1

thì:

cosine(q,d)=q⋅d\text{cosine}(q,d)=q\cdot d

Do denominator bằng 1.

Vì vậy trong một số vector-search systems, dot product trên normalized embeddings có thể cho cùng thứ tự ranking với cosine similarity.

Đây là một ví dụ cho thấy lựa chọn metric không nên được nhìn tách rời embedding model. Bạn cần biết model được train như thế nào và vendor khuyến nghị similarity function nào.


Một vector store từ scratch

Bỏ qua mọi framework, một retriever nhỏ có thể được viết như sau:

Python
import numpy as np


class SimpleVectorStore:
    def __init__(self):
        self.texts = []
        self.vectors = []

    def add(self, texts, vectors):
        self.texts.extend(texts)
        self.vectors.extend(vectors)

    def search(self, query_vector, top_k=3):
        query = np.asarray(query_vector, dtype=np.float32)
        docs = np.asarray(self.vectors, dtype=np.float32)

        dot = docs @ query

        doc_norms = np.linalg.norm(docs, axis=1)
        query_norm = np.linalg.norm(query)

        scores = dot / (doc_norms * query_norm + 1e-12)

        indices = np.argsort(scores)[::-1][:top_k]

        return [
            {
                "text": self.texts[i],
                "score": float(scores[i]),
            }
            for i in indices
        ]

Không có magic nào ở đây.

Pipeline chỉ là:

Text
query
  ↓
embedding model
  ↓
query vector
  ↓
similarity(query, every document vector)
  ↓
sort
  ↓
top-k

Nguồn research cũng dùng một implementation NumPy tương tự để phơi bày phần toán học bên dưới vector retrieval.

Nhưng implementation này có một vấn đề lớn.

Giả sử:

Text
N = 10,000,000 vectors
d = 1,024 dimensions

Mỗi query mà so sánh với toàn bộ N vectors sẽ có cost xấp xỉ:

O(Nd)O(Nd)

Với vài trăm vectors, điều này chẳng đáng quan tâm.

Với hàng triệu hoặc hàng trăm triệu vectors, exact scan có thể trở thành bottleneck.

Đó là lý do production vector indexes thường sử dụng Approximate Nearest Neighbor — ANN.

Thay vì kiểm tra mọi vector, ANN xây một cấu trúc dữ liệu giúp search một phần không gian có khả năng chứa nearest neighbors.

Một thuật toán phổ biến là HNSW.

Trade-off lúc này trở thành:

Text
                 Exact Search

High recall  ◄────────────────────► High compute
                     │
                     │
                     ▼
                   ANN

Lower latency / compute
        ↕
Potentially miss some true neighbors

Từ đây xuất hiện một parameter mà production engineer phải quan tâm nhưng tutorial RAG đơn giản thường bỏ qua:

retrieval không chỉ có quality; retrieval còn có latency–recall trade-off.

Nếu ANN configuration quá aggressive, database có thể rất nhanh nhưng evidence đúng không bao giờ lọt vào candidate set.

LLM phía sau không thể sửa một document đã bị loại trước khi nó được nhìn thấy.


Chunking: retrieval unit quan trọng hơn nhiều người nghĩ

Giả sử một policy document dài 50 trang được embed thành một vector duy nhất.

Query:

“Employee probation period là bao lâu?”

Document có thể chứa câu trả lời, nhưng vector của cả 50 trang phải đồng thời đại diện cho:

  • probation;
  • salary;
  • leave;
  • termination;
  • insurance;
  • equipment;
  • remote work;
  • security policy;
  • hàng chục chủ đề khác.

Semantic signal cần tìm bị pha loãng.

Giải pháp hiển nhiên là chia document thành chunks.

Nhưng ngay lập tức ta gặp bài toán ngược lại.

Giả sử source:

Text
The Phoenix X200 reactor was introduced in 2024.

It requires inspection every six months and must
be shut down immediately when pressure exceeds
the configured safety threshold.

Nếu chunk boundary rơi đúng giữa hai đoạn:

Text
Chunk A
The Phoenix X200 reactor was introduced in 2024.
Text
Chunk B
It requires inspection every six months...

Chunk B chứa câu trả lời về inspection nhưng từ It đã mất referent.

Đây chính là vấn đề được nhấn mạnh trong research: chunk nhỏ thường cho semantic signal tập trung hơn, nhưng có nguy cơ mất context; chunk lớn giữ được context nhưng có thể chứa quá nhiều chủ đề và làm retrieval kém chính xác.

Chunk size không có một con số “chuẩn”

Đừng bắt đầu bằng câu hỏi:

“Chunk nên là 500 hay 1.000 token?”

Hãy bắt đầu bằng:

Đơn vị thông tin nhỏ nhất mà người dùng thường muốn retrieve là gì?

Một API reference có cấu trúc khác hợp đồng pháp lý.

Một FAQ khác source code.

Một academic paper khác Slack conversation.

Chunk boundary tốt thường dựa trên structure trước khi dựa trên số token:

Text
Document
 ├── Section
 │    ├── Paragraph
 │    ├── Paragraph
 │    └── Table
 │
 └── Section
      ├── Subsection
      └── Paragraph

Token limit vẫn cần thiết để kiểm soát kích thước, nhưng nó nên là guardrail chứ không nhất thiết là semantic boundary duy nhất.


Parent–Child Chunking: search nhỏ, trả context lớn

Một pattern hữu ích là tách retrieval unit khỏi generation context.

Ví dụ document được chia thành parent chunks 1.200 tokens:

Text
Parent A
├── Child A1 ~ 150 tokens
├── Child A2 ~ 150 tokens
├── Child A3 ~ 150 tokens
└── ...

Chỉ child chunks được dùng để search.

Mỗi child lưu:

Json
{
  "chunk_id": "child-a3",
  "parent_id": "parent-a",
  "text": "..."
}

Query pipeline:

Text
Query
  │
  ▼
Search Child Chunks
  │
  ▼
child-a3 matched
  │
  ▼
parent_id = parent-a
  │
  ▼
Load Parent A
  │
  ▼
Send richer context to LLM

Ta đang tối ưu hai mục tiêu khác nhau:

Text
Small child
→ precise retrieval

Large parent
→ sufficient generation context

Research ban đầu mô tả chính trade-off này và cách child vector có thể trỏ trở lại parent document để lấy context đầy đủ hơn.

Tuy nhiên parent-child không miễn phí.

Nếu năm child chunks đều match cùng một parent, bạn cần deduplicate. Nếu parent quá lớn, context window lại bị lấp đầy. Nếu một answer cần hai phần nằm ở hai parent khác nhau, context builder vẫn phải ghép chúng hợp lý.

Pattern này giải quyết một class problem cụ thể; nó không phải “chunking tốt nhất cho mọi RAG”.


🔬 Deep Dive — Late Chunking thay đổi thứ tự xử lý

Naive chunking thường làm:

Text
Long Document
     │
     ▼
Split into chunks
     │
     ├── Chunk A ─► Encoder ─► Embedding A
     ├── Chunk B ─► Encoder ─► Embedding B
     └── Chunk C ─► Encoder ─► Embedding C

Mỗi chunk được encoder xử lý độc lập.

Late Chunking đảo thứ tự:

Text
Long Document
     │
     ▼
Long-context Transformer
     │
     ▼
Contextualized Token Embeddings
     │
     ├── token representations for A
     ├── token representations for B
     └── token representations for C
             │
             ▼
       Apply boundaries
             │
             ▼
        Mean pooling
             │
     ┌───────┼───────┐
     ▼       ▼       ▼
  Chunk A  Chunk B  Chunk C
 embedding embedding embedding

Các token đã nhìn thấy context rộng hơn trước khi được pooling thành chunk vector.

Paper Late Chunking mô tả chính xác ý tưởng này: encode long text thành token representations trước, sau đó mới áp dụng chunk boundaries và pooling, nhờ đó chunk embeddings giữ được nhiều contextual information hơn. Research đầu vào cũng mô tả flow tương tự.

Điều quan trọng là Late Chunking không giải quyết việc tìm boundary tốt.

Bạn vẫn phải xác định đâu là chunk.

Nó chủ yếu giải quyết:

Text
"Chunk này bị mất surrounding context khi được embedded độc lập."

Ngoài ra, không phải embedding API nào cũng có thể triển khai kỹ thuật này. Jina lưu ý rằng nó phụ thuộc vào khả năng xử lý long context và cách lấy/pooling token-level representations; với API chỉ trả một final embedding, developer không thể đơn giản tự thêm Late Chunking ở client side.

Đây là nuance quan trọng: một kỹ thuật tốt về mặt research không đồng nghĩa nó phù hợp với mọi embedding stack.


Giả sử user tìm:

Text
"ERR_CONN_48219"

Một semantic embedding model có thể hiểu rất tốt:

Text
"How do I reset my password?"

gần nghĩa với:

Text
"I forgot my credentials."

Nhưng ERR_CONN_48219 không phải concept cần paraphrase.

Nó là một identifier.

Các query tương tự còn có:

Text
SKU-8F3A92
CVE-XXXX-XXXX
invoice 0019847
function process_payment_v2
article 37.2

Trong những trường hợp đó, lexical matching rất có giá trị.

Dense và sparse search nhìn relevance theo hai cách khác nhau

Dense / Vector Search Lexical / BM25
Mạnh khi Query và document khác từ nhưng giống nghĩa Có exact terms quan trọng
Ví dụ “forgot password” ↔ “reset credentials” SKU-88291
Representation Dense vector Terms / inverted index
Điểm yếu điển hình IDs, code, rare terms Paraphrase, semantic intent

Elasticsearch hiện vẫn sử dụng BM25 làm default similarity cho full-text ranking, với scoring dựa trên các tín hiệu như term frequency, document frequency và document length.

RAG production vì vậy thường không chọn:

Text
BM25 OR Vector Search

mà chọn:

Text
BM25 AND Vector Search

rồi hợp nhất candidate lists.

Đó là Hybrid Search. OpenSearch cũng mô tả hybrid search theo đúng hướng kết hợp keyword và semantic search.


Vấn đề tiếp theo: hai score không cùng hệ quy chiếu

Giả sử:

Text
Dense Search
Document A: 0.91
Document B: 0.87
Document C: 0.79

BM25:

Text
Document C: 18.7
Document A: 12.4
Document D: 9.1

Ta không thể đơn giản:

Python
final_score = cosine_score + bm25_score

vì hai score có scale và semantics khác nhau.

Một approach là normalize score.

Một approach khác là bỏ score tuyệt đối và chỉ nhìn rank.

Đó là ý tưởng của Reciprocal Rank Fusion — RRF.

RRF(d)=∑i=1n1k+ri(d)RRF(d)=\sum_{i=1}^{n}\frac{1}{k+r_i(d)}

Trong đó:

  • (r_i(d)): rank của document (d) trong result list thứ (i);
  • (k): rank constant làm giảm mức chênh lệch giữa các vị trí.

Ví dụ:

Text
Dense ranking         BM25 ranking

1. Doc A              1. Doc C
2. Doc B              2. Doc A
3. Doc C              3. Doc D

Doc A xuất hiện rất cao trong cả hai hệ thống.

RRF thưởng cho consensus đó mà không cần so sánh trực tiếp 0.91 với 12.4.

OpenSearch hiện cung cấp RRF trực tiếp trong score-ranker processor và dùng 60 làm default rank constant; đây là default của implementation đó chứ không phải một hằng số toán học bất biến mà mọi hệ thống bắt buộc phải sử dụng.

Research đầu vào cũng xác định hybrid retrieval và RRF là lớp optimization nằm sau chunking, dùng ranking position để fusion các result lists có scoring scale khác nhau.

Retrieval và reranking không phải cùng một việc

Hybrid retrieval vẫn thường được dùng để tạo candidate set.

Ví dụ:

Text
10 million documents
        │
        ▼
Cheap first-stage retrieval
        │
        ▼
Top 100 candidates
        │
        ▼
More expensive reranker
        │
        ▼
Top 8
        │
        ▼
LLM context

Tại sao cần hai stage?

Vì một model mạnh dùng để so sánh sâu query với từng document thường quá đắt để chạy trên 10 triệu records.

First-stage retriever tối ưu:

“Đừng bỏ sót candidate tốt.”

Reranker tối ưu:

“Trong số candidate này, cái nào thực sự nên đứng đầu?”

Elastic cũng mô tả reranking theo cấu trúc này: first-stage retrieval tạo candidate set, sau đó một semantic reranker có chi phí cao hơn sắp xếp lại tập nhỏ đó.

Đây là một distinction rất quan trọng.

Retriever không nhất thiết phải hoàn hảo ở rank 1.

Nhưng nó phải đủ tốt để document đúng lọt vào candidate window.


Retrieval tốt vẫn chưa đủ: context construction mới là cầu nối tới LLM

Giả sử retrieval trả về:

Text
1. refund_policy_v2.md
2. refund_policy_v3.md
3. faq_refund.md
4. old_support_ticket_291.txt

Nếu application đơn giản concatenate tất cả:

Text
Use the following context:

[refund_policy_v2]
...

[refund_policy_v3]
...

[faq_refund]
...

[old_support_ticket]
...

LLM đang nhận nhiều source có thể mâu thuẫn.

Retriever chỉ trả lời:

“Những document nào có vẻ liên quan?”

Nó không tự động trả lời:

“Document nào là policy hiện hành?”

Đây là lúc metadata trở thành một phần của retrieval architecture.

Ví dụ mỗi chunk nên giữ:

Json
{
  "document_id": "refund-policy-v3",
  "version": 3,
  "effective_from": "2026-08-01",
  "status": "active",
  "department": "commerce",
  "source_type": "policy"
}

Query có thể áp dụng:

Text
status = active

trước khi semantic ranking xảy ra.

💡 Mental model

Có hai loại câu hỏi hoàn toàn khác nhau:

Text
Semantic question:
"Document này có nói về refund không?"

Governance question:
"Document này có được phép xem là policy hiện hành không?"

Embedding chủ yếu giúp câu hỏi thứ nhất.

Metadata, filters và application logic thường phải xử lý câu hỏi thứ hai.

Đây là lý do production retrieval hiếm khi chỉ là:

Python
vector_db.similarity_search(query)

Một pipeline trưởng thành hơn có thể là:

Text
Query
  │
  ▼
Access Control / Metadata Filters
  │
  ▼
Dense + Sparse Retrieval
  │
  ▼
Fusion
  │
  ▼
Reranking
  │
  ▼
Deduplication
  │
  ▼
Version / Freshness Rules
  │
  ▼
Context Packing
  │
  ▼
LLM

Context window lớn không có nghĩa cứ nhét nhiều document hơn là tốt

Khi model hỗ trợ context rất dài, một phản ứng tự nhiên là tăng top_k:

Text
top_k = 5
→ 10
→ 30
→ 100

Nhưng retrieval có mục tiêu là tăng signal-to-noise ratio, không phải tối đa hóa số token.

Giả sử evidence cần thiết chỉ có 1.500 tokens nhưng bạn gửi 40.000 tokens.

Bây giờ model phải phân biệt:

Text
1,500 useful tokens
vs
38,500 potentially irrelevant tokens

Bạn đã chuyển một phần retrieval problem sang generation layer.

Đó không nhất thiết là cải thiện.

Một context builder tốt thường phải cân bằng:

Text
relevance
+
coverage
+
diversity
+
freshness
+
authority
+
token budget

Có thể document thứ 5 ít giống query hơn document thứ 4 một chút, nhưng lại bổ sung một khía cạnh chưa hề xuất hiện trong bốn document đầu.

Vì vậy “top-k highest similarity” không phải lúc nào cũng tương đương “best context”.


Từ Naive RAG đến Production RAG

Đến đây ta có thể so sánh hai hệ thống.

Naive RAG

Text
Documents
   ↓
Fixed-size chunks
   ↓
Embeddings
   ↓
Vector DB

Query
   ↓
Embedding
   ↓
Top-k vector search
   ↓
Concatenate chunks
   ↓
LLM

Production-oriented RAG

Text
                 INGESTION

Sources
   │
   ▼
Parsing + normalization
   │
   ▼
Structure-aware chunking
   │
   ├────► Metadata / ACL / versions
   │
   ▼
Embedding
   │
   ├────► Vector index
   └────► Lexical index


                  SERVING

User Query
    │
    ▼
Query understanding
    │
    ▼
Filters / permissions
    │
    ├──── Dense retrieval ────┐
    │                         │
    └──── Sparse retrieval ───┤
                              ▼
                         Rank Fusion
                              │
                              ▼
                           Rerank
                              │
                              ▼
                Deduplicate / Context Pack
                              │
                              ▼
                             LLM
                              │
                              ▼
                 Grounded Answer + Sources

Sự khác biệt lớn nhất không nằm ở số lượng box.

Nó nằm ở việc mỗi failure mode đã có một layer chịu trách nhiệm rõ ràng.

Failure Layer nên điều tra trước
Document đúng không xuất hiện ingestion / retrieval
Keyword chính xác bị miss lexical retrieval
Semantic paraphrase bị miss embedding / dense retrieval
Document đúng có trong top 50 nhưng không vào top 5 ranking / reranking
Policy cũ được dùng thay policy mới metadata / filtering
Evidence đúng nhưng answer sai generation
Answer thêm claim ngoài source grounding / generation
Request chậm retrieval, reranking hoặc generation profiling

Đây là cách debug RAG hiệu quả hơn rất nhiều so với thay model hoặc prompt một cách ngẫu nhiên.


GraphRAG và Agentic RAG: khi nào pipeline tuyến tính bắt đầu không đủ?

Một lỗi phổ biến khác là nhìn mọi kỹ thuật RAG mới như một “upgrade tier”:

Text
Naive RAG
   ↓
Hybrid RAG
   ↓
GraphRAG
   ↓
Agentic RAG

rồi giả định càng xuống dưới càng tốt.

Không phải vậy.

Mỗi architecture giải quyết một class problem khác nhau.

GraphRAG phù hợp với whole-dataset questions

Giả sử bạn có 50.000 báo cáo nội bộ.

Query:

“Ba chủ đề chiến lược lớn nhất xuất hiện xuyên suốt toàn bộ dataset là gì?”

Vector retrieval gặp một vấn đề thú vị.

Query không chứa một entity cụ thể để hệ thống tìm vài chunks giống nhất.

Câu trả lời cần aggregate information across the corpus.

Microsoft GraphRAG xây một structured representation bằng cách trích xuất entities và relationships, thực hiện community detection rồi tạo community reports ở nhiều cấp độ.

Global Search sau đó có thể sử dụng các community reports theo mô hình map-reduce để xử lý những câu hỏi đòi hỏi hiểu dataset ở cấp tổng thể. Microsoft cũng lưu ý đây là phương pháp resource-intensive hơn.

Research đầu vào cũng đưa GraphRAG vào đúng bối cảnh này: vector chunks hoạt động tốt cho local semantic retrieval nhưng gặp khó với các câu hỏi cần tổng hợp các quan hệ hoặc thông tin phân tán trên phạm vi lớn.

Vì vậy đừng hỏi:

“GraphRAG có tốt hơn vector RAG không?”

Hãy hỏi:

“Query distribution của tôi có đủ nhiều global/multi-hop questions để justify thêm indexing complexity và cost không?”

Nếu sản phẩm chủ yếu hỏi:

Text
"What is the refund period?"

GraphRAG có thể là overengineering.


Agentic RAG giải quyết decision-making, không phải đơn giản là thêm Agent

Một linear RAG giả định:

Text
Retrieve
→ Generate

Nhưng đôi khi system cần tự quyết định:

Text
Có cần retrieval không?
     │
     ├── No ─► answer
     │
     └── Yes
           │
           ▼
       retrieve
           │
           ▼
     evidence đủ tốt?
        │        │
       Yes       No
        │        │
        ▼        ▼
     answer   rewrite query
                 │
                 └──► retrieve again

Đây là vùng mà các hướng như CRAG và Self-RAG trở nên thú vị.

CRAG đề xuất retrieval evaluator để đánh giá chất lượng retrieved documents và kích hoạt các corrective actions, bao gồm web retrieval trong thiết kế gốc và decompose-then-recompose để lọc knowledge hữu ích.

Self-RAG đi theo hướng khác: model được huấn luyện để retrieve, generate và tự critique thông qua các reflection tokens, cho phép retrieval diễn ra thích ứng thay vì luôn lấy một số passage cố định.

Điểm nuance ở đây rất quan trọng:

Self-RAG không đơn giản đồng nghĩa với việc lấy một LLM bất kỳ rồi thêm ba node “retrieval grader”, “hallucination grader” và “answer grader”.

Đó là một architectural pattern phổ biến được lấy cảm hứng từ self-reflective RAG, nhưng paper Self-RAG gốc mô tả một cơ chế training và reflection tokens cụ thể.

Research đầu vào gom các ý tưởng corrective/self-reflective retrieval dưới umbrella “Agentic RAG”. Điều này hữu ích để xây mental model, nhưng khi implement, developer nên phân biệt rõ paper technique với workflow pattern.

⚠️ Cẩn thận với vòng lặp tự sửa

Một hệ thống:

Text
retrieve
→ grade
→ rewrite
→ retrieve
→ grade
→ rewrite
...

trông thông minh hơn linear RAG.

Nhưng mỗi vòng có thể tăng:

  • latency;
  • LLM calls;
  • token cost;
  • failure surface;
  • độ khó trace;
  • tính không deterministic.

Agentic retrieval chỉ đáng dùng khi giá trị của corrective behavior đủ lớn để bù cho complexity đó.


Evaluation: đừng chỉ chấm câu trả lời cuối cùng

Giả sử một test query có expected answer:

Text
30 days

System trả:

Text
Customers can request refunds within 30 days.

Bạn biết answer đúng.

Nhưng bạn vẫn chưa biết RAG pipeline tốt ở đâu.

Có thể retriever đưa đúng document lên rank 1.

Cũng có thể retriever hoàn toàn sai nhưng LLM tình cờ biết đáp án từ parametric memory.

Nếu chỉ chấm final answer, hai case đó trông giống nhau.

Đây là lý do evaluation nên được tách theo pipeline.

Text
                     RAG EVALUATION

                  ┌─────────────────┐
                  │ Final Answer    │
                  └────────┬────────┘
                           │
               ┌───────────┴───────────┐
               ▼                       ▼
       Retrieval Quality        Generation Quality
               │                       │
         ┌─────┴─────┐           ┌─────┴─────┐
         ▼           ▼           ▼           ▼
      Recall     Precision   Faithfulness  Relevance

Retrieval recall

Câu hỏi:

Evidence cần thiết có được retrieve không?

Nếu ground-truth relevant documents là:

Text
{A, B, C}

và system retrieve:

Text
{A, C, X, Y}

thì recall đang đo mức độ system tìm đủ những thứ cần thiết.

Recall thấp thường là tín hiệu kiểm tra:

  • chunking;
  • embedding;
  • query representation;
  • metadata filter;
  • ANN settings;
  • candidate size.

Retrieval precision

Câu hỏi:

Trong những gì được retrieve, bao nhiêu phần thực sự hữu ích?

Bạn có thể đạt recall cao bằng cách retrieve 100 chunks.

Nhưng nếu 95 chunks là noise thì context quality lại xấu.

Đây chính là tension:

Text
Increase top_k
     │
     ├── potential recall ↑
     └── potential noise  ↑

Faithfulness

Câu hỏi:

Những claim trong generated answer có được retrieved context hỗ trợ không?

Một answer có thể rất relevant với user nhưng vẫn thêm facts không xuất hiện trong source.

Ragas định nghĩa Faithfulness theo hướng kiểm tra các claim của response có thể được suy ra từ retrieved context hay không.

Context Recall và Context Precision

Current Ragas documentation cung cấp cả Context Precision, Context Recall, Response Relevancy, Faithfulness cùng nhiều metric khác cho RAG evaluation. Một số metric có thể dùng LLM, trong khi framework hiện cũng có các biến thể non-LLM; vì vậy gọi toàn bộ Ragas đơn giản là “LLM-as-a-judge” sẽ không còn chính xác.

Ví dụ Context Recall dựa trên reference có thể tách reference thành claims rồi kiểm tra bao nhiêu claim được retrieved context support.

Research đầu vào cũng đề xuất đánh giá riêng context precision, context recall, faithfulness và answer quality khi đưa RAG lên production.


🏗️ Production Note — evaluation dataset quan trọng hơn metric đẹp

Bạn có thể có một dashboard:

Text
Faithfulness:      0.92
Context Recall:    0.89
Answer Relevance:  0.94

nhưng tất cả trở nên ít ý nghĩa nếu evaluation set không đại diện cho traffic thật.

Một bộ test hữu ích nên chứa nhiều loại query:

Text
Exact lookup
"Refund period là bao lâu?"

Paraphrase
"Tôi có bao nhiêu ngày để trả lại hàng?"

Identifier-heavy
"ERR_PAY_1042 nghĩa là gì?"

Multi-hop
"Policy A ảnh hưởng procedure B như thế nào?"

Ambiguous
"How does it work?"

No-answer
"Công ty có văn phòng trên Sao Hỏa không?"

Conflicting sources
"Refund period hiện tại là bao lâu?"

Đặc biệt, no-answer queries rất quan trọng.

Một RAG system không chỉ cần học cách tìm được answer.

Nó còn phải biết khi knowledge base không chứa đủ evidence để trả lời.


Latency và cost: đo từng stage trước khi tối ưu

Một request RAG có thể mất:

Text
Query preprocessing      20 ms
Embedding                80 ms
Vector + lexical search  50 ms
Reranking               300 ms
LLM first token          700 ms
Generation             1,500 ms
--------------------------------
Total                  2,650 ms

Nếu vậy, optimize vector search từ 50 ms xuống 20 ms chỉ tiết kiệm 30 ms.

Trong khi reranking và generation chiếm phần lớn latency.

Đây là lý do observability nên đo stage-level latency:

Text
rag.query.total
rag.embedding
rag.retrieval.dense
rag.retrieval.sparse
rag.fusion
rag.rerank
rag.context_build
rag.generation.ttft
rag.generation.total

Cùng với quality signals:

Text
retrieved_chunk_ids
retrieval_scores
reranked_chunk_ids
source_versions
context_tokens
answer_citations

Nếu không lưu những dữ liệu này, khi user báo:

“RAG trả lời sai.”

team gần như chỉ nhìn thấy input và output, còn toàn bộ nguyên nhân nằm giữa hai đầu đã biến mất.

Semantic caching có thể hữu ích, nhưng đừng cache mù quáng

Research đầu vào đề cập semantic caching như một cách giảm chi phí cho các query gần nghĩa.

Ý tưởng là:

Text
New Query
   │
   ▼
Query Embedding
   │
   ▼
Similar cached query exists?
   │
   ├── Yes ─► reuse
   │
   └── No  ─► normal RAG

Nhưng production implementation phải quan tâm đến invalidation.

Nếu policy vừa đổi từ:

Text
14 days

sang:

Text
30 days

một cached response semantic-match hoàn hảo nhưng stale còn nguy hiểm hơn một cache miss.

Cache key vì vậy có thể phải phụ thuộc vào:

Text
query semantics
+
tenant
+
permissions
+
knowledge-base version
+
relevant policy version

Cost optimization chỉ đúng khi semantics của cache vẫn đúng.


7 sai lầm phổ biến khi xây RAG

1. “Có vector database là có RAG tốt”

Vector database chỉ là một component.

Nếu document parse sai, chunk sai hoặc metadata thiếu thì database chỉ search rất nhanh trên dữ liệu đã bị chuẩn bị sai.

Một pipeline tốt bắt đầu từ data quality.


2. “Embedding mạnh hơn sẽ sửa được mọi retrieval problem”

Embedding không thể sửa:

Text
missing documents
bad OCR
wrong permissions
stale versions
broken chunk boundaries
incorrect metadata

Thậm chí với identifier-heavy search, lexical retrieval có thể quan trọng hơn semantic embedding.


3. “Cứ tăng top-k để recall cao hơn”

Tăng top_k có thể giúp document đúng lọt vào context.

Nhưng nó cũng thêm noise.

Một hướng trưởng thành hơn thường là:

Text
retrieve wider
→ rerank
→ send narrow

thay vì:

Text
retrieve everything
→ send everything to LLM

4. “RAG và fine-tuning là hai cách làm cùng một việc”

Không hoàn toàn.

RAG rất phù hợp khi information:

  • thay đổi thường xuyên;
  • cần citations/provenance;
  • nằm trong private knowledge base;
  • cần update mà không retrain model.

Fine-tuning phù hợp hơn khi muốn thay đổi những pattern như:

  • response behavior;
  • style;
  • task-specific formatting;
  • cách model thực hiện một loại task lặp lại.

Có những hệ thống dùng cả hai.

Câu hỏi không phải:

“RAG hay fine-tuning tốt hơn?”

mà là:

“Tôi đang cố thay đổi knowledge source hay model behavior?”


5. “RAG answer sai nghĩa là prompt chưa tốt”

Prompt có thể là nguyên nhân.

Nhưng trước khi sửa prompt, hãy xem:

Text
Expected evidence có được retrieve không?
        │
       No
        │
        ▼
Prompt không phải bottleneck đầu tiên

Nếu evidence không tồn tại trong context thì prompt engineering ở generation layer có giới hạn rất rõ.


6. “GraphRAG và Agentic RAG luôn tốt hơn Naive RAG”

Chúng cung cấp thêm capability nhưng cũng thêm complexity.

Nếu baseline:

Text
structure-aware chunking
+
hybrid retrieval
+
reranking
+
good metadata
+
evaluation

đã giải quyết tốt query distribution, thêm autonomous loops hay graph indexing có thể chỉ tăng cost và debugging burden.

Advanced architecture nên xuất hiện sau khi bạn xác định failure mode cụ thể mà baseline không giải quyết được.


7. “Chỉ cần final answer accuracy”

Nếu chỉ đo final answer, bạn không biết hệ thống sai ở đâu.

Production RAG cần ít nhất mental model:

Text
Can we retrieve it?
        ↓
Did we rank it?
        ↓
Did we provide it?
        ↓
Did the model use it?
        ↓
Was the final answer useful?

Mỗi câu hỏi là một layer khác nhau.


Một cách thực tế để tiến hóa RAG architecture

Nếu bắt đầu một project mới, không cần xây GraphRAG, Agent, reranker và semantic cache ngay ngày đầu tiên.

Một progression hợp lý hơn là:

Text
Stage 1
Basic RAG
Document → Chunk → Embed → Retrieve → Generate

Mục tiêu ở stage này là chứng minh:

External knowledge thực sự giúp use case.

Sau đó:

Text
Stage 2
Better Data

Parsing
Structure-aware chunking
Metadata
Versioning

Tiếp theo:

Text
Stage 3
Better Retrieval

Dense
+
BM25
+
Hybrid fusion

Khi candidate retrieval đủ tốt:

Text
Stage 4
Better Ranking

Retrieve 50–100
      ↓
Rerank
      ↓
Use top 5–10

Sau đó mới xây:

Text
Stage 5
Evaluation + Observability

Thực tế phần này nên xuất hiện rất sớm, nhưng đến đây nó trở thành requirement bắt buộc trước khi tiếp tục tăng complexity.

Cuối cùng, chỉ khi metrics chỉ ra limitation:

Text
Need corpus-wide aggregation?
→ investigate GraphRAG

Retrieval frequently fails and can benefit from correction?
→ investigate corrective / agentic workflows

Repeated expensive equivalent queries?
→ investigate semantic caching

Đây là tư duy production quan trọng:

Architecture nên phát triển từ measured failure modes, không phải từ danh sách các kỹ thuật RAG đang thịnh hành.


Kết luận: RAG không bắt đầu ở LLM

Quay lại chatbot ở đầu bài.

User hỏi:

“Tôi mua sản phẩm 20 ngày trước, bây giờ có được hoàn tiền không?”

System trả policy cũ 14 ngày dù policy mới 30 ngày đã tồn tại.

Ban đầu vấn đề trông giống:

“LLM trả lời sai.”

Nhưng sau khi hiểu toàn bộ pipeline, ta có một cách debug hoàn toàn khác:

Text
Policy v3 đã được ingest chưa?
        ↓
Chunk chứa "30 days" có tồn tại không?
        ↓
Chunk đó có được embed/index đúng không?
        ↓
Query có retrieve được nó không?
        ↓
Dense và lexical result thế nào?
        ↓
Nó đứng rank bao nhiêu?
        ↓
Policy v2 có bị filter vì stale không?
        ↓
Reranker chọn document nào?
        ↓
Context cuối cùng gửi vào LLM gồm gì?
        ↓
LLM có tạo claim được evidence support không?

Đó chính là bước chuyển từ “gắn vector database vào chatbot” sang engineering một retrieval-augmented system.

RAG hiệu quả không đến từ một component thần kỳ.

Nó đến từ việc từng lớp làm đúng nhiệm vụ của mình:

Text
good data
   ↓
good chunks
   ↓
good retrieval
   ↓
good ranking
   ↓
good context
   ↓
grounded generation
   ↓
continuous evaluation

Và nếu chỉ giữ lại bốn ý sau bài viết này, hãy giữ bốn ý này:

  1. RAG là một retrieval problem trước khi là một generation problem.
  2. Document đúng phải được retrieve và rank đúng trước khi LLM có cơ hội sử dụng nó.
  3. Chunking, hybrid retrieval, reranking và metadata thường có tác động lớn hơn việc liên tục thay model.
  4. GraphRAG hay Agentic RAG chỉ nên được thêm khi một failure mode cụ thể chứng minh rằng pipeline đơn giản không còn đủ.

Một RAG system tốt vì vậy không phải hệ thống có nhiều box nhất trên architecture diagram.

Nó là hệ thống mà khi câu trả lời sai, engineer có thể xác định chính xác mắt xích nào đã sai, đo được nó và cải thiện nó một cách có hệ thống.

Nguồn tham khảo

  • Patrick Lewis et al. — Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, NeurIPS 2020.
  • Michael Günther et al. — Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models.
  • Jina AI — Late Chunking in Long-Context Embedding Models.
  • OpenSearch Documentation — Score Ranker Processor / Reciprocal Rank Fusion.
  • Elastic Documentation — Similarity, BM25 and Re-ranking.
  • Microsoft GraphRAG — Indexing Dataflow and Global Search.
  • Akari Asai et al. — Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection.
  • Shi-Qi Yan et al. — Corrective Retrieval Augmented Generation.
  • Ragas Documentation — RAG Evaluation Metrics.