Mục lục bài viết
Bạn xây một hệ thống RAG cho tài liệu nội bộ. Người dùng hỏi một câu mà bạn biết chắc câu trả lời nằm trong knowledge base. Bạn mở file PDF, Ctrl + F, và tìm thấy đúng đoạn đó trong vài giây.
Nhưng hệ thống RAG lại trả lời:
“Tôi không tìm thấy thông tin này trong tài liệu được cung cấp.”
Phản ứng đầu tiên thường là nghi ngờ embedding model chưa đủ tốt. Có thể vector database cấu hình sai. Có thể cần hybrid search. Có thể phải thêm reranker. Hoặc đơn giản hơn: tăng top_k từ 5 lên 20 để retriever lấy nhiều dữ liệu hơn.
Tất cả đều có thể đúng.
Nhưng cũng có một khả năng ít được chú ý hơn: ngay từ lúc ingest dữ liệu, bạn đã biến đoạn thông tin đúng thành một retrieval unit tồi.
Giả sử tài liệu gốc viết:
## Chính sách hoàn tiền
Khách hàng được hoàn toàn bộ chi phí nếu hủy dịch vụ
trong vòng 7 ngày kể từ thời điểm kích hoạt.
Ngoại lệ: chính sách này không áp dụng cho gói Enterprise
đã sử dụng dịch vụ triển khai riêng.
Nếu chunker cắt đúng giữa các ý:
Chunk A:
## Chính sách hoàn tiền
Khách hàng được hoàn toàn bộ chi phí nếu hủy dịch vụ
Chunk B:
trong vòng 7 ngày kể từ thời điểm kích hoạt.
Ngoại lệ: chính sách này không áp dụng cho gói Enterprise
Chunk C:
đã sử dụng dịch vụ triển khai riêng.
thì không chunk nào còn biểu diễn đầy đủ câu hỏi:
“Gói Enterprise có được hoàn tiền trong 7 ngày không?”
Embedding model không được nhìn thấy “sự thật hoàn chỉnh”. Vector database cũng không thể tìm lại một đơn vị thông tin chưa từng được tạo ra đúng cách.
Đó là lý do chunking không chỉ là bước chia văn bản cho vừa context window.
Trong RAG, chunking chính là quá trình quyết định:
Đơn vị tri thức nào sẽ tồn tại trong hệ thống retrieval.
Và nếu đơn vị đó được thiết kế sai, mọi thứ phía sau — embedding, vector search, reranking và cuối cùng là LLM — đều phải làm việc với một biểu diễn đã mất thông tin ngay từ đầu.
1. Chunking thực chất đang quyết định điều gì?
Một pipeline RAG đơn giản thường được mô tả như sau:
INGESTION
Raw Document
│
▼
┌──────────────┐
│ Parser │
└──────┬───────┘
│
▼
┌──────────────┐
│ Chunking │
└──────┬───────┘
│
▼
┌──────────────┐
│ Embedding │
└──────┬───────┘
│
▼
┌──────────────┐
│ Vector Index │
└──────────────┘
QUERY
User Question
│
▼
Query Embedding
│
▼
Similarity Search
│
▼
Relevant Chunks
│
▼
LLM
│
▼
Answer
Nhìn vào diagram này, chunking có vẻ chỉ là một bước preprocessing nằm giữa parser và embedding model.
Nhưng ảnh hưởng của nó lớn hơn nhiều.
Sau khi tài liệu được chunk, vector database thường không còn “tìm kiếm tài liệu” theo nghĩa ban đầu nữa. Nó tìm kiếm các chunk đã được tạo ra từ tài liệu.
Research ban đầu cũng mô tả đúng điểm này: chunk là những đơn vị thông tin nhỏ có thể truy xuất độc lập, được embedding và đưa vào vector store; cách đặt ranh giới chunk sẽ quyết định nội dung nào thực sự trở thành candidate khi truy vấn xảy ra.
Giả sử một document dài 10.000 token được chia thành 20 chunk.
Vector database không nhìn thấy:
Document X
như một thực thể ngữ nghĩa duy nhất.
Nó chủ yếu nhìn thấy:
X_chunk_01
X_chunk_02
X_chunk_03
...
X_chunk_20
với mỗi chunk có embedding riêng.
Với dense retrieval điển hình, quá trình tìm kiếm có thể hình dung đơn giản là:
query
│
▼
Embedding(query)
│
▼
similarity(
query_vector,
chunk_vector
)
│
▼
Top-K chunks
Điều đó tạo ra một implication rất quan trọng:
Chunking thay đổi không gian tìm kiếm trước cả khi vector search bắt đầu.
Một embedding model rất mạnh vẫn không thể tạo representation tốt cho một đoạn văn đã mất chủ thể, mất định nghĩa hoặc bị cắt ngang giữa hai ý.
Một reranker rất mạnh cũng không thể xếp hạng một candidate hoàn chỉnh nếu candidate đó chưa bao giờ tồn tại.
Các component phía sau đôi khi có thể che bớt lỗi chunking — chẳng hạn hai chunk liền nhau vô tình cùng được retrieve — nhưng không nên xem đó là cơ chế sửa lỗi đáng tin cậy.
2. Bài toán thật sự không phải “chunk bao nhiêu token?”
Khi bắt đầu xây RAG, một trong những câu hỏi phổ biến nhất là:
“Chunk size tốt nhất là bao nhiêu? 256, 512 hay 1.024 token?”
Câu hỏi này hợp lý, nhưng nó đặt vấn đề hơi sai.
Không tồn tại một con số chunk size tối ưu cho mọi dataset, bởi vì chunking đang phải cân bằng hai mục tiêu đối nghịch:
Chunk nhỏ
│
├── retrieval chính xác hơn
│
└── nhưng dễ thiếu context
▲
│
TRADE-OFF
│
▼
Chunk lớn
│
├── context đầy đủ hơn
│
└── nhưng retrieval dễ bị loãng
Đây có thể xem là trade-off giữa retrieval granularity và context completeness.
Khi chunk quá nhỏ
Hãy xem đoạn sau:
Redis hỗ trợ eviction policy để quyết định key nào
sẽ bị loại bỏ khi bộ nhớ đạt giới hạn.
Một trong số đó là allkeys-lru, trong đó Redis có thể
loại bỏ các key ít được sử dụng gần đây nhất.
Nếu chunker tạo:
Chunk A:
Một trong số đó là allkeys-lru.
Chunk B:
Các key ít được sử dụng gần đây nhất sẽ bị loại bỏ.
query:
allkeys-lru hoạt động như thế nào?
có thể retrieve Chunk A rất tốt vì chứa exact term allkeys-lru.
Nhưng chunk đó lại không chứa câu trả lời.
Chunk B chứa câu trả lời nhưng không chứa tên policy.
Bạn có retrieval precision ở cấp từ khóa, nhưng mất semantic completeness ở cấp kiến thức.
Khi chunk quá lớn
Bây giờ thử hướng ngược lại.
Một chunk 2.000 token chứa:
Redis installation
Redis persistence
Redis Sentinel
Redis eviction
Redis Cluster
Redis Streams
Redis security
Query chỉ hỏi:
allkeys-lru hoạt động như thế nào?
Embedding của chunk phải nén rất nhiều chủ đề vào một representation duy nhất. Phần liên quan đến eviction chỉ chiếm một tỷ lệ nhỏ trong tổng nội dung.
Ngay cả khi chunk được retrieve, bạn còn đưa hàng nghìn token không liên quan vào generation context.
Vấn đề vì thế xuất hiện ở hai nơi:
LARGE CHUNK
Embedding stage:
nhiều semantic signals
↓
representation kém tập trung
Generation stage:
nhiều irrelevant tokens
↓
signal-to-noise ratio thấp
Chunk size vì vậy không phải một hyperparameter độc lập.
Nó liên quan trực tiếp đến:
- cấu trúc của tài liệu;
- độ dài của một “ý hoàn chỉnh”;
- loại query mà người dùng thường đặt;
- embedding model;
- retrieval strategy;
top_k;- reranking;
- context budget của generation model.
Overlap giúp, nhưng không miễn phí
Một giải pháp quen thuộc là cho các chunk chồng lấn lên nhau:
Document:
AAAA BBBB CCCC DDDD EEEE FFFF
Without overlap:
[AAAA BBBB CCCC]
[DDDD EEEE FFFF]
With overlap:
[AAAA BBBB CCCC DDDD]
[CCCC DDDD EEEE FFFF]
Overlap giúp giảm khả năng một concept bị phá vỡ đúng tại boundary.
Nhưng overlap càng lớn, bạn càng tạo nhiều dữ liệu trùng lặp.
Điều đó có thể dẫn tới:
1 concept
↓
nhiều chunks gần giống nhau
↓
retriever lấy cả 3
↓
top-k bị lãng phí
↓
LLM nhận context lặp lại
Do đó, chunk_overlap = 20% không phải một quy luật tự nhiên.
Nó chỉ là một configuration cần được đo lường giống như bất kỳ hyperparameter retrieval nào khác.
3. Chunking sai phá RAG như thế nào?
Có ba failure mode đặc biệt quan trọng vì chúng xuất hiện từ rất sớm trong pipeline nhưng chỉ biểu hiện thành câu trả lời sai ở rất xa phía downstream.
Semantic fragmentation: một kiến thức bị biến thành nhiều mảnh không đầy đủ
Đây là trường hợp boundary nằm giữa một concept.
Ví dụ:
Original:
OAuth authorization code chỉ nên được trao đổi
từ backend với authorization server. Khi sử dụng
PKCE, client tạo code_verifier và gửi code_challenge
trong authorization request.
Chunker cắt:
Chunk A:
OAuth authorization code chỉ nên được trao đổi
từ backend với authorization server. Khi sử dụng
Chunk B:
PKCE, client tạo code_verifier và gửi code_challenge
trong authorization request.
Query:
PKCE dùng code_verifier như thế nào?
Chunk B vẫn có khả năng match.
Nhưng nếu query là:
PKCE thay đổi authorization code flow ở đâu?
relationship giữa hai nửa trở nên quan trọng hơn.
Chunking đã làm mất chính relationship đó.
Orphaned context: chunk còn câu nhưng mất chủ thể
Đây là lỗi khó phát hiện hơn.
Chunk:
Phương pháp này yêu cầu embedding toàn bộ document
trước khi pooling từng chunk.
Đoạn trên hoàn toàn đúng.
Nhưng:
“Phương pháp này” là phương pháp nào?
Con người đọc đoạn trước có thể biết đó là Late Chunking.
Embedding model xử lý riêng chunk này thì không nhất thiết biết.
LLM retrieve riêng chunk này cũng không biết.
Đây là lý do các đại từ và reference như:
nó
phương pháp này
công ty đó
mô hình trên
trường hợp trước
the former
this approach
the company
đặc biệt nguy hiểm trong retrieval unit độc lập.
Structured-data destruction: đúng text nhưng sai structure
Với Markdown, HTML, source code, table hoặc JSON, semantic boundary không chỉ nằm ở dấu chấm.
Ví dụ bảng:
| Model | Context Window | Price |
|-------|----------------|-------|
| A | 128K | $X |
| B | 1M | $Y |
Nếu chunker chia:
Chunk A:
| Model | Context Window | Price |
|-------|----------------|-------|
| A | 128K
Chunk B:
| $X |
| B | 1M | $Y |
các token vẫn tồn tại.
Nhưng relationship:
Model A
↕
128K
↕
$X
đã bị phá hủy.
Điều tương tự xảy ra khi cắt giữa:
function
class
JSON object
Markdown section
HTML table
legal clause + exception
Đó là lý do chunking phải hiểu structure, không chỉ length.
Còn “Lost in the Middle” thì sao?
Một lý do khác khiến việc đơn giản tăng chunk size hoặc retrieve thật nhiều context không phải lúc nào cũng hiệu quả là khả năng sử dụng long context của LLM không đồng đều.
Nghiên cứu Lost in the Middle của Liu và cộng sự cho thấy nhiều language model đạt hiệu suất tốt hơn khi thông tin quan trọng nằm gần đầu hoặc cuối context và có thể suy giảm khi thông tin cần thiết nằm ở giữa một context dài.
Điểm cần phân biệt là:
Lost in the Middle không phải một “lỗi chunking”.
Nó là một vấn đề về cách model sử dụng long context.
Nhưng chunking tồi có thể làm vấn đề nặng hơn nếu retrieval pipeline gửi quá nhiều đoạn nhiễu vào prompt và chôn evidence quan trọng giữa chúng.
Vì vậy:
Bigger chunks
+
Higher top_k
+
No reranking
↓
Very large context
↓
More irrelevant information
↓
Harder evidence selection
không phải lúc nào cũng tốt hơn.
4. Ba baseline chunking strategy nên hiểu trước
Trước khi dùng semantic chunking, agentic chunking hay bất kỳ kỹ thuật phức tạp nào, có ba baseline quan trọng hơn nhiều.
| Strategy | Boundary dựa trên | Điểm mạnh | Failure chính |
|---|---|---|---|
| Fixed-size | ký tự/token | đơn giản, nhanh, predictable | cắt giữa concept |
| Recursive | paragraph → line → word... | giữ natural boundary tốt hơn | vẫn chưa hiểu semantics thực sự |
| Structure-aware | heading, section, AST, HTML node... | giữ logical structure | phụ thuộc parser/document type |
Fixed-size chunking
Đây là dạng:
mỗi 500 token
→ tạo một chunk
→ overlap 50 token
→ tiếp tục
Ưu điểm lớn nhất của nó là tính dễ dự đoán.
Bạn có thể ước lượng tương đối rõ:
document tokens
÷
chunk size
≈
number of chunks
Ingestion nhanh, implementation đơn giản và dễ dùng làm baseline benchmark.
Nhược điểm cũng rất rõ: thuật toán không biết 500 token thứ 500 có nằm giữa một đoạn giải thích hay không.
Fixed-size vì thế đặc biệt phù hợp với vai trò baseline để so sánh, hơn là assumption rằng đây chắc chắn là kiến trúc cuối cùng.
Recursive splitting
Recursive splitting tốt hơn ở chỗ nó cố tìm boundary tự nhiên trước khi buộc phải cắt cứng.
Một mental model đơn giản:
Document
│
├── Có thể split theo paragraph?
│ │
│ Yes
│ ▼
│ paragraphs
│
└── paragraph vẫn quá lớn?
│
Yes
▼
split nhỏ hơn
│
▼
lines
│
▼
words
LangChain hiện vẫn khuyến nghị RecursiveCharacterTextSplitter như một điểm khởi đầu cho generic text. Splitter mặc định thử các separator theo thứ tự như paragraph, newline, whitespace rồi mới đến boundary nhỏ hơn nếu cần.
Ví dụ tối giản:
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=150,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
Ở đây 1200 chỉ là ví dụ.
Quan trọng hơn là tư duy:
prefer strong natural boundary
↓
fall back gradually
↓
hard cut only when necessary
Nếu bạn cần giới hạn theo token thay vì character, hãy dùng tokenizer-aware length calculation thay vì mặc định coi số ký tự tương đương số token.
Structure-aware chunking
Nếu document đã có cấu trúc tốt, đừng vứt cấu trúc đó đi rồi cố gắng “khám phá lại” bằng embeddings.
Ví dụ Markdown:
# Redis
## Persistence
...
## Eviction
### allkeys-lru
...
### volatile-lru
...
Một structure-aware chunker có thể giữ metadata:
{
"document": "redis-guide",
"h1": "Redis",
"h2": "Eviction",
"h3": "allkeys-lru",
"content": "..."
}
Khi đó chunk không còn là:
các key ít được dùng gần đây...
mà về mặt retrieval có thể được biểu diễn gần với:
Redis
→ Eviction
→ allkeys-lru
→ các key ít được dùng gần đây...
Đối với code, logic tương tự dẫn tới AST-aware splitting: function, class hoặc method thường là boundary có ý nghĩa hơn ký tự thứ 2.000.
Đối với HTML và Markdown, các splitter hiện tại cũng có thể tận dụng heading và syntax metadata thay vì coi toàn bộ file là plain text.
💡 Mental model
Nếu document đã nói cho bạn biết:
“Đây là một section”, “đây là một table”, “đây là một function”
thì đó thường là tín hiệu boundary mạnh hơn:
“Đã đủ 512 token.”
5. Một insight quan trọng hơn chunk size: “retrieve small, read big”
Vấn đề khó nhất của chunking xuất phát từ một mâu thuẫn.
Retriever thường thích unit nhỏ:
small chunk
→ semantic focus cao
→ dễ match query cụ thể
Nhưng LLM thường cần nhiều context hơn:
larger context
→ đủ subject
→ đủ explanation
→ đủ exception
→ reasoning tốt hơn
Nếu cố tìm một chunk size duy nhất thỏa mãn cả hai mục tiêu, bạn đang ép một hyperparameter giải quyết hai bài toán khác nhau.
Một kiến trúc tốt hơn là tách chúng ra.
Parent-child retrieval
Original section
│
▼
┌───────────────────────────────┐
│ Parent: 1 section hoàn chỉnh │
│ ~ larger context │
└───────────────┬───────────────┘
│ split
┌────────┼────────┐
▼ ▼ ▼
Child A Child B Child C
small small small
│ │ │
└────────┼────────┘
▼
Vector Index
QUERY
│
▼
match Child B
│
▼
resolve parent_id
│
▼
retrieve Parent
│
▼
send Parent to LLM
Bạn search trên child vì child có tín hiệu ngữ nghĩa tập trung.
Nhưng bạn generate từ parent vì parent giữ được context.
LlamaIndex mô tả mô hình “small-to-big retrieval” theo đúng cách này: parent chunks được chia thành child chunks, child được index và giữ liên kết tới parent. Research được cung cấp cũng mô tả cùng nguyên tắc: tách retrieval granularity khỏi generation context thay vì tìm một chunk size duy nhất cho cả hai.
Ví dụ:
Parent:
"Redis Eviction Policies" — 1.500 tokens
Children:
- noeviction — 250 tokens
- allkeys-lru — 300 tokens
- volatile-lru — 280 tokens
- allkeys-random — 240 tokens
Query:
allkeys-lru loại key như thế nào?
Retriever match child allkeys-lru.
Generation có thể nhận toàn bộ section Redis Eviction Policies, nhờ vậy model còn hiểu sự khác nhau giữa policy đang hỏi với các policy lân cận.
Sentence Window Retrieval
Sentence Window dùng cùng philosophy nhưng ở granularity nhỏ hơn.
Giả sử:
S1: Redis cung cấp nhiều eviction policy.
S2: allkeys-lru có thể loại các key ít được dùng gần đây.
S3: Policy này xem xét toàn bộ keyspace.
S4: volatile-lru chỉ xét các key có TTL.
Vector index có thể embed riêng:
S2
nhưng metadata lưu:
window = S1 + S2 + S3
Khi S2 match query, generation context được thay bằng cửa sổ xung quanh nó.
SentenceWindowNodeParser của LlamaIndex hoạt động theo nguyên tắc này: node retrieval có thể là một câu, trong khi metadata giữ các câu xung quanh để sử dụng sau retrieval.
Điều này dẫn tới một nguyên tắc kiến trúc rất đáng nhớ:
Retrieval unit không nhất thiết phải bằng generation unit.
Đối với nhiều RAG system, đây là cải tiến quan trọng hơn việc tranh luận xem chunk nên là 500 hay 700 token.
6. Semantic Chunking: khi punctuation không còn nói cho bạn biết topic đổi ở đâu
Recursive splitting giả định rằng structure của text phần nào phản ánh structure của ý tưởng.
Điều đó đúng với:
documentation
blog article
Markdown
technical manual
nhưng không phải lúc nào cũng đúng với:
meeting transcript
conversation
legal narrative
medical notes
OCR text
research notes
Một paragraph dài có thể chứa ba topic.
Ngược lại, năm paragraph ngắn có thể vẫn nói về cùng một concept.
Semantic chunking cố giải quyết bài toán đó bằng cách quan sát semantic distance thay vì chỉ punctuation.
Một phiên bản đơn giản có thể hoạt động như sau:
Sentence 1 ──┐
Sentence 2 │ similar
Sentence 3 ──┘
│
│ large semantic shift
▼
----------- BREAK -----------
│
Sentence 4 ──┐
Sentence 5 │ similar
Sentence 6 ──┘
Pipeline khái niệm:
document
│
▼
split into sentences
│
▼
embed sentences/windows
│
▼
calculate adjacent similarity
│
▼
detect large semantic changes
│
▼
place chunk boundaries
Nếu similarity giữa hai vùng giảm mạnh:
sim(window_i, window_i+1) ↓
đó có thể là dấu hiệu topic vừa chuyển.
Vì sao semantic chunking hấp dẫn?
Bởi vì chunk size trở thành kết quả của semantic structure thay vì input cố định.
Một concept ngắn có thể tạo chunk 180 token.
Một explanation phức tạp có thể tạo chunk 700 token.
Đây thường là behavior hợp lý hơn việc ép cả hai thành 512 token.
Nhưng semantic chunking không có phép màu
Bạn vừa chuyển bài toán từ:
chunk_size = ?
sang:
semantic threshold = ?
Nếu threshold quá nhạy:
một thay đổi nhỏ
→ new chunk
→ fragmentation
Nếu threshold quá lỏng:
nhiều topic khác nhau
→ same chunk
→ semantic dilution
Ngoài ra, ingestion trở nên đắt hơn vì hệ thống phải chạy embedding hoặc một semantic model trong quá trình xác định boundary.
Vì vậy semantic chunking hợp lý khi boundary thật sự khó xác định bằng document structure, chứ không phải mặc định cao cấp hơn nên luôn tốt hơn recursive splitting.
🔬 Deep Dive — Semantic chunking đang tối ưu boundary, không tối ưu retrieval toàn bộ
Ngay cả khi semantic chunking tạo boundary hoàn hảo, retrieval vẫn có thể thất bại vì:
query formulation
embedding model
metadata filtering
vector index
top_k
hybrid retrieval
reranking
Chunking chỉ xác định candidate unit.
Nó không thay thế retrieval architecture.
7. Contextual Retrieval và Late Chunking: hai cách khác nhau để cứu context bị mất
Một vấn đề vẫn chưa được giải quyết hoàn toàn.
Giả sử chunk boundary rất đẹp:
Doanh thu tăng 3% so với quý trước,
chủ yếu nhờ tăng trưởng của dịch vụ cloud.
Đây là một đoạn văn hoàn chỉnh về mặt ngữ pháp.
Nhưng đứng độc lập, nó vẫn thiếu:
doanh thu của ai?
quý nào?
báo cáo nào?
Đây không còn đơn thuần là bài toán tìm boundary.
Đây là bài toán:
Làm thế nào để một chunk nhỏ vẫn giữ được context của document lớn?
Hai hướng tiếp cận đáng chú ý là Contextual Retrieval và Late Chunking.
Contextual Retrieval: thêm context vào text trước khi index
Anthropic giới thiệu Contextual Retrieval vào tháng 9/2024.
Thay vì embed trực tiếp:
Doanh thu tăng 3% so với quý trước.
hệ thống tạo thêm một contextual prefix:
Đoạn này thuộc báo cáo tài chính Q2 của ACME,
đang thảo luận doanh thu cloud.
Doanh thu tăng 3% so với quý trước.
Pipeline trở thành:
Original Document
│
├──────────────┐
│ │
▼ ▼
Chunk LLM
│ │
└──────┬───────┘
▼
Contextualized Chunk
│
┌─────┴─────┐
▼ ▼
Embedding BM25
Ý tưởng quan trọng là prefix không cần chứa toàn bộ document.
Nó chỉ cần bổ sung những thông tin mà chunk sẽ mất khi bị tách ra.
Trong evaluation do Anthropic công bố, Contextual Embeddings giảm top-20 retrieval failure từ 5,7% xuống 3,7%; khi kết hợp Contextual Embeddings với Contextual BM25, con số giảm xuống 2,9%. Thêm reranking đưa failure rate xuống 1,9% trong benchmark của họ. Đây là kết quả trên setup của Anthropic, không nên hiểu thành mức cải thiện bảo đảm cho mọi corpus.
Điểm đáng chú ý là Contextual Retrieval không thực sự loại bỏ chunking.
Nó làm chunk giàu context hơn trước khi retrieval.
Late Chunking: context hóa token trước rồi mới tạo chunk embedding
Late Chunking của Jina AI tiếp cận vấn đề ở tầng khác.
Naive pipeline:
Document
│
▼
Chunk first
│
├── Chunk A → Encoder → vector A
├── Chunk B → Encoder → vector B
└── Chunk C → Encoder → vector C
Chunk B chưa bao giờ được encoder nhìn cùng Chunk A.
Nếu Chunk B chứa:
Công ty này tăng doanh thu 20%.
embedding model có rất ít thông tin để biết “công ty này” là ai.
Late Chunking đảo thứ tự:
Long Document
│
▼
Transformer Encoder
│
▼
Contextualized token embeddings
│
├────────┬────────┬────────┐
│ │ │ │
tokens A tokens B tokens C ...
│ │ │
▼ ▼ ▼
pooling pooling pooling
│ │ │
▼ ▼ ▼
vector A vector B vector C
Transformer nhìn phần document lớn trước khi các token được pooling thành chunk vectors.
Nhờ vậy representation của từ:
"công ty này"
có khả năng đã được contextualize dựa trên entity xuất hiện trước đó.
Jina mô tả Late Chunking đúng theo quy trình này: encode long text thành token-level representations trước, sau đó mới pooling các vùng token tương ứng với chunk boundary.
Các benchmark Jina công bố cho thấy Late Chunking cải thiện nDCG@10 so với naive chunking trên nhiều dataset BEIR mà họ thử nghiệm, với mức cải thiện khác nhau theo dataset; kết quả cũng cho thấy lợi ích có xu hướng rõ hơn ở tài liệu dài hơn.
Hai kỹ thuật có thể nhìn như sau:
| Contextual Retrieval | Late Chunking | |
|---|---|---|
| Context được bổ sung ở đâu? | Text của chunk | Token representation |
| Cơ chế chính | LLM tạo contextual prefix | Long-context embedding encoder |
| Chunk boundary còn cần không? | Có | Có |
| Thay đổi dữ liệu index? | Có, text được contextualize | Vector được contextualize |
| Chi phí chính | Context generation lúc ingestion | Long-context embedding compute |
| Phù hợp khi | chunk khó hiểu khi đứng độc lập | reference phụ thuộc context xa |
Điểm quan trọng nhất:
Cả hai đều không chứng minh rằng chunk boundary không còn quan trọng.
Late Chunking vẫn cần biết token nào thuộc chunk nào.
Contextual Retrieval vẫn cần một chunk đủ coherent để contextualize.
Chúng cải thiện context preservation, không xóa bỏ bài toán segmentation.
8. Vậy nên chọn strategy nào?
Thay vì bắt đầu bằng:
512 tokens + 20% overlap
hãy bắt đầu bằng loại dữ liệu.
DOCUMENT
│
▼
Có structure đáng tin?
/ \
Yes No
│ │
▼ ▼
Structure-aware Generic prose?
/ \
Yes No
│ │
▼ ▼
Recursive Topic shifts
irregular?
/ \
Yes No
│ │
▼ ▼
Semantic Recursive
│
▼
Chunk đứng riêng có thiếu context?
/ \
Yes No
│ │
▼ ▼
Parent-child / Window baseline
hoặc Contextual
Retrieval
Đây chỉ là starting framework.
Một số case thực tế:
| Dataset | Baseline hợp lý |
|---|---|
| Blog / documentation | Recursive + heading metadata |
| Markdown knowledge base | Structure-aware → recursive fallback |
| Source code | AST/language-aware |
| Meeting transcript | Semantic hoặc sentence-based |
| FAQ | Một QA pair có thể là một chunk tự nhiên |
| Legal document | Clause/section-aware + parent context |
| Long financial reports | Structure-aware + contextual retrieval đáng thử |
| Query rất cụ thể nhưng cần paragraph rộng để trả lời | Parent-child / sentence window |
Đừng chọn chunk size bằng cảm giác
Một cách thực tế hơn là biến chunking thành experiment.
Giả sử bạn muốn thử:
Chunk size:
256
512
1024 tokens
Overlap:
0%
10%
20%
Thay vì hỏi:
“Cấu hình nào được cộng đồng dùng nhiều nhất?”
hãy đo:
Evaluation queries
│
┌────────────────┼────────────────┐
▼ ▼ ▼
config A config B config C
256 / 10% 512 / 10% 1024 / 10%
│ │ │
▼ ▼ ▼
same embedding same embedding same embedding
same retriever same retriever same retriever
same top_k same top_k same top_k
│ │ │
└────────────────┼────────────────┘
▼
compare
Giữ các component khác cố định.
Nếu đồng thời thay:
chunker
embedding
top_k
reranker
prompt
LLM
bạn sẽ không còn biết improvement đến từ đâu.
🏗️ Production Note
Chunk configuration tốt nhất cũng có thể thay đổi khi query distribution thay đổi.
Knowledge base cho customer support thường nhận query rất khác knowledge base phục vụ:
code search
legal analysis
financial research
internal wiki
Vì thế evaluation set nên phản ánh query thật, không chỉ các câu hỏi đẹp được tạo thủ công từ document.
9. Đánh giá chunking: đừng chỉ nhìn câu trả lời cuối cùng
Giả sử hệ thống trả lời sai.
Pipeline thực tế là:
Question
│
▼
Retriever
│
▼
Context
│
▼
Generator
│
▼
Answer
Chỉ nhìn answer cuối cùng khiến bạn không biết lỗi nằm ở đâu.
Ví dụ answer sai có thể vì:
A. đúng chunk không được retrieve
B. đúng chunk được retrieve nhưng xếp quá thấp
C. context đúng nhưng LLM bỏ qua
D. LLM sử dụng context nhưng reasoning sai
Chunking chủ yếu tác động mạnh vào A và B, sau đó gián tiếp ảnh hưởng C.
Retrieval metrics trước
Với một evaluation set có relevant evidence, hãy đo ít nhất:
Recall@K
Precision@K
MRR hoặc nDCG
Ví dụ:
Query:
"allkeys-lru hoạt động thế nào?"
Relevant chunk:
eviction-policy-17
Nếu relevant chunk không xuất hiện trong top 20:
Recall@20 = failure
Trước khi tuning prompt, bạn đã biết generation model thậm chí chưa được cung cấp đúng evidence.
Context Precision và Context Recall
Ragas hiện cung cấp các metric dành cho RAG như Context Precision, Context Recall, Faithfulness và Response Relevancy. Context Precision đánh giá việc các context liên quan có được xếp cao hay không, trong khi Context Recall tập trung vào mức độ retrieval bao phủ được thông tin cần thiết.
Một cách đọc kết quả có thể là:
| Symptom | Điều nên kiểm tra |
|---|---|
| Context Recall thấp | boundary, chunk size, query coverage, embedding, top_k |
| Context Precision thấp | chunk quá rộng, retrieval noise, ranking |
| Faithfulness thấp | context thiếu, context mâu thuẫn, generation/prompt |
| Response Relevancy thấp | retrieval lạc đề hoặc generator trả lời lan man |
Nhưng cần tránh một lỗi suy luận:
Context Recall thấp
≠
chunking chắc chắn sai
Nó cũng có thể đến từ embedding model, filtering hoặc retrieval algorithm.
Metric giúp bạn xác định vùng lỗi, không tự động tìm ra root cause.
Một experiment chunking tối thiểu nên log gì?
Một evaluation harness hữu ích có thể tạo bảng:
| Config | Recall@10 | nDCG@10 | Context Precision | Avg retrieved tokens | Index chunks |
|---|---|---|---|---|---|
| 256 / 10% | ... | ... | ... | ... | ... |
| 512 / 10% | ... | ... | ... | ... | ... |
| 1024 / 10% | ... | ... | ... | ... | ... |
| Structure-aware | ... | ... | ... | ... | ... |
| Parent-child | ... | ... | ... | ... | ... |
Bây giờ trade-off trở nên rõ ràng.
Có thể config A tăng Recall nhưng gửi gấp đôi số token vào LLM.
Config B có Precision cao nhưng bỏ sót các query yêu cầu context trải qua hai paragraph.
Config C đạt quality gần như B nhưng ingestion đơn giản hơn rất nhiều.
Khi đó bạn mới có đủ dữ liệu để quyết định engineering trade-off.
10. Những lỗi chunking phổ biến nhất trong production
Tối ưu chunk size trước khi hiểu document
Nếu corpus gồm Markdown có heading rõ ràng mà bạn lập tức chạy fixed-size 500 token, bạn đang bỏ đi một signal segmentation gần như miễn phí.
Hãy ưu tiên:
document structure
↓
semantic coherence
↓
size constraint
thay vì đảo ngược thứ tự đó.
Dùng cùng một chunker cho mọi loại file
Một pipeline:
PDF
Markdown
HTML
Python
JSON
CSV
│
▼
same RecursiveCharacterTextSplitter
rất tiện.
Nhưng convenience không đồng nghĩa correctness.
Code có function boundary.
HTML có DOM.
Markdown có headings.
Table có rows và columns.
Mỗi dạng dữ liệu có definition khác nhau về “một đơn vị thông tin hoàn chỉnh”.
Tăng overlap để chữa mọi vấn đề
Overlap hữu ích với boundary uncertainty.
Nhưng nó không sửa được:
parser làm hỏng table
OCR sai
heading bị mất
document hierarchy biến mất
metadata thiếu
Và overlap quá cao còn tạo candidate gần trùng nhau.
Đưa metadata vào database nhưng không đưa vào retrieval representation
Bạn có thể lưu:
{
"title": "Redis Eviction",
"section": "allkeys-lru"
}
nhưng embedding chỉ:
Keys are removed based on recent usage...
Nếu metadata quan trọng để hiểu chunk, hãy cân nhắc sử dụng nó trong embedding text, contextual representation hoặc retrieval filtering — thay vì chỉ lưu metadata để hiển thị sau cùng.
Chỉ benchmark answer quality
Nếu chỉ hỏi một LLM judge:
“Câu trả lời cuối cùng có tốt không?”
bạn sẽ trộn retrieval quality và generation quality thành một con số.
Chunking experiment tốt nên kiểm tra retrieval trước, sau đó mới kiểm tra end-to-end RAG.
Cho rằng strategy phức tạp luôn tốt hơn
Semantic chunking, Contextual Retrieval và Late Chunking đều rất thú vị.
Nhưng một technical documentation có heading rõ ràng đôi khi chỉ cần:
structure-aware
+
recursive fallback
+
good metadata
+
evaluation
Một architecture đơn giản đạt cùng quality sẽ dễ vận hành, debug và re-index hơn.
11. Chunking tốt không phải là tạo các đoạn văn “đẹp”
Quay lại tình huống ban đầu.
Câu trả lời tồn tại trong document nhưng RAG không tìm thấy nó.
Nếu chỉ nhìn phía generation, ta có thể nghĩ:
LLM chưa đủ thông minh.
Nếu chỉ nhìn vector database:
embedding model chưa đủ tốt.
Nhưng khi hiểu chunking, ta thấy một layer nằm trước tất cả những thứ đó.
Document gốc không đi thẳng vào retrieval.
Nó phải được biến thành một tập các retrieval units.
Knowledge
│
▼
Parsing
│
▼
Chunk boundaries
│
▼
Retrievable units
│
▼
Embeddings
│
▼
Search
│
▼
Context
│
▼
Answer
Nếu boundary làm mất relationship quan trọng, knowledge vẫn còn trên disk nhưng đã không còn tồn tại dưới dạng mà retriever có thể sử dụng hiệu quả.
Đó là lý do câu hỏi tốt hơn không phải:
“Chunk size chuẩn là bao nhiêu?”
Mà là:
“Đơn vị nhỏ nhất nào đủ tập trung để retrieval chính xác, nhưng vẫn giữ đủ context để có ý nghĩa?”
Và khi hai yêu cầu đó mâu thuẫn, đừng cố bắt một chunk giải quyết cả hai.
Hãy tách:
retrieval unit
≠
generation context
bằng parent-child retrieval, sentence window hoặc những architecture tương tự.
Nếu chunk vẫn mất ý nghĩa khi đứng độc lập, hãy cân nhắc contextualization như Contextual Retrieval hoặc Late Chunking.
Cuối cùng, đừng chọn chiến lược bằng intuition.
Hãy build evaluation set, giữ các component khác cố định và đo retrieval.
Bởi vì trong một RAG system tốt, chunking không phải thao tác “cắt tài liệu thành từng đoạn”.
Nó là quá trình thiết kế hình dạng của tri thức mà hệ thống có khả năng tìm lại.
Những điều cần nhớ
- Chunk càng nhỏ không đồng nghĩa retrieval càng tốt; context có thể bị mất.
- Chunk càng lớn không đồng nghĩa reasoning càng tốt; relevance có thể bị pha loãng và context trở nên nhiễu.
- Document structure thường là boundary signal đáng tin cậy hơn một con số token cố định.
- Retrieval unit không nhất thiết phải bằng generation unit.
- Không có chunking strategy tốt nhất cho mọi corpus; strategy tốt nhất là strategy thắng trên evaluation set đại diện cho workload thật.
Nguồn tham khảo
- Anthropic — Introducing Contextual Retrieval. Contextual Retrieval — Anthropic
- Liu et al. — Lost in the Middle: How Language Models Use Long Contexts. Lost in the Middle — arXiv
- Günther et al. — Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models. Late Chunking paper — arXiv
- Jina AI — Late Chunking in Long-Context Embedding Models. Late Chunking — Jina AI
- LangChain — Text Splitters / RecursiveCharacterTextSplitter. LangChain Text Splitters
- LlamaIndex — Sentence Window Node Parser và small-to-big recursive retrieval.
- Ragas — Metrics for Retrieval-Augmented Generation. Ragas Metrics
- Báo cáo nghiên cứu tổng hợp về Chunking và RAG được cung cấp làm dữ liệu đầu vào.



