RAG nâng cao 2 — Chunking & Indexing chiến lược
Vì sao chunking quyết định chất lượng RAG
Trong RAG nâng cao 1 — Tổng quan ta đã thấy pipeline RAG gồm ba lớp: ingest → index → retrieve → generate. Phần lớn kỹ sư dồn công vào lớp retrieve (chọn vector DB, tinh chỉnh top-k) và lớp generate (prompt engineering), nhưng thực tế chất lượng câu trả lời bị đóng khung ngay từ lúc bạn cắt tài liệu và lập chỉ mục. Nếu đơn vị văn bản được embed đã sai — quá vụn hoặc quá thô — thì không có reranker hay prompt nào cứu được: mô hình chỉ nhìn thấy đúng những mảnh mà retriever trả về.
Chunk là đơn vị văn bản nhỏ mà ta biến thành một vector và lưu vào index. Kích thước chunk là một đánh đổi (trade-off) trực tiếp:
- Chunk quá to (ví dụ cả một chương 3.000 từ): một câu hỏi hẹp sẽ khớp với chunk vì trong đó có vài câu liên quan, nhưng phần lớn nội dung là nhiễu (noise). LLM phải đọc hàng nghìn token thừa, vừa tốn chi phí vừa dễ bị "lạc" giữa thông tin không liên quan (hiện tượng lost in the middle). Độ chính xác khớp vector cũng giảm vì một vector duy nhất phải "trung bình hoá" quá nhiều chủ đề.
- Chunk quá nhỏ (ví dụ một câu): vector rất "sắc" và khớp chính xác, nhưng mất ngữ cảnh. Một câu như "Hạn mức này áp dụng cho nhóm khách hàng trên" là vô nghĩa nếu tách khỏi đoạn định nghĩa "nhóm khách hàng" phía trước. Retriever trả về đúng câu nhưng LLM không đủ dữ kiện để trả lời.
Điểm cân bằng phụ thuộc loại tài liệu và loại câu hỏi, không có con số vàng. Kinh nghiệm phổ biến: 200–500 token cho văn bản kỹ thuật/pháp lý, kèm overlap. Nhưng thay vì chọn một con số cố định, bài này lập luận rằng bạn nên chọn chiến lược cắt theo cấu trúc tài liệu và tách rời đơn vị-để-tìm khỏi đơn vị-để-đọc bằng các kỹ thuật index nâng cao.
Các chiến lược chunking
1. Fixed-size + overlap
Cắt theo số token/ký tự cố định (ví dụ mỗi 400 token), cho các chunk chồng lấn (overlap) nhau 10–20% để không cắt đứt một ý ngay ranh giới. Đây là baseline: đơn giản, nhanh, không phụ thuộc định dạng. Nhược điểm: cắt "mù", có thể chặt ngang giữa một câu hoặc một điều khoản. Dùng khi tài liệu là văn bản thô, đồng nhất, không có cấu trúc rõ.
2. Chunking theo cấu trúc (structure-aware)
Tận dụng cấu trúc sẵn có của tài liệu: heading Markdown, section, điều/khoản, đoạn (paragraph), thẻ HTML. Mỗi heading hoặc mỗi điều khoản trở thành một chunk (hoặc gốc để cắt tiếp). Ưu điểm lớn: ranh giới chunk trùng ranh giới ngữ nghĩa mà tác giả đã cố tình đặt ra. Với hợp đồng, quy chế, thông tư ngân hàng — vốn được đánh số "Điều 1, Điều 2, Khoản a…" — đây gần như là lựa chọn mặc định tốt nhất.
3. Recursive chunking
Cắt phân cấp: thử tách theo ranh giới lớn trước (chương → section → đoạn → câu), nếu khối vẫn vượt ngưỡng kích thước thì đệ quy xuống mức nhỏ hơn. Đây là chiến lược "recursive character/token splitter" phổ biến trong LangChain/LlamaIndex: giữ được cấu trúc tối đa nhưng vẫn đảm bảo mọi chunk nằm dưới giới hạn token. Là lựa chọn cân bằng tốt cho tài liệu hỗn hợp.
4. Semantic chunking
Thay vì cắt theo độ dài, cắt theo ranh giới ngữ nghĩa: đi lần lượt qua các câu, tính embedding cho từng câu (hoặc cụm câu trượt), và đặt điểm cắt nơi độ tương đồng giữa hai câu liền kề tụt xuống — tức nơi chủ đề chuyển. Kết quả là mỗi chunk gói trọn một ý thống nhất, kích thước biến thiên. Chi phí: phải embed ở bước tiền xử lý nên tốn hơn, và nhạy với ngưỡng. Hợp cho tài liệu văn xuôi dài, chủ đề đan xen (báo cáo, bài phân tích) hơn là tài liệu đã được đánh số điều khoản.
5. Chunking theo loại tài liệu (document-specific)
Một số tài liệu cần bộ cắt riêng:
- Bảng (table): không cắt ngang hàng/cột. Giữ nguyên bảng, hoặc chuyển mỗi hàng thành một câu tự-mô-tả kèm tên cột, hoặc lưu bảng dưới dạng Markdown/HTML trong một chunk và thêm phần mô tả.
- Hoá đơn / biểu mẫu: trích theo trường (field) — số hoá đơn, ngày, tổng tiền — rồi tạo chunk cấu trúc key-value thay vì cắt văn bản trôi.
- Hợp đồng / quy chế: cắt theo điều-khoản, giữ đường dẫn phân cấp ("Chương II > Điều 7 > Khoản 2") làm metadata để trích dẫn về sau.
- Code: cắt theo hàm/lớp, không cắt giữa thân hàm.
Nguyên tắc chọn: bắt đầu từ cấu trúc tài liệu, không từ một con số token. Bảng dưới đây tóm tắt:
| Loại tài liệu | Chiến lược nên dùng |
|---|---|
| Văn bản thô, đồng nhất | Fixed-size + overlap |
| Markdown/tài liệu có heading | Structure-aware / recursive |
| Hợp đồng, quy chế, thông tư | Theo điều-khoản (structure-aware) |
| Báo cáo, bài phân tích dài | Semantic chunking |
| Bảng, hoá đơn, biểu mẫu | Document-specific |
Kỹ thuật index nâng cao
Ý tưởng xuyên suốt phần này: đơn vị tốt nhất để TÌM chưa chắc là đơn vị tốt nhất để ĐỌC. Các kỹ thuật dưới đây tách hai vai trò đó ra.
Parent-document / small-to-big
Embed chunk nhỏ (câu hoặc đoạn ngắn) để khớp thật chính xác, nhưng khi một chunk nhỏ trúng thì trả về đoạn cha lớn hơn (parent) chứa nó để LLM có đủ ngữ cảnh. Cách làm: cắt tài liệu hai tầng — parent chunk lớn (ví dụ cả một điều khoản/section) và child chunk nhỏ bên trong; chỉ index vector của child, lưu ánh xạ child → parent; lúc retrieve, tìm bằng child rồi "phóng to" lên parent trước khi đưa vào prompt. Đây là cách giải quyết trực tiếp mâu thuẫn "nhỏ để tìm, to để hiểu".
Multi-vector
Gán cho mỗi tài liệu (hoặc mỗi chunk) nhiều vector nhìn nó từ nhiều góc, tất cả cùng trỏ về một tài liệu gốc:
- Vector của các đoạn con (child chunks) — như small-to-big.
- Vector của bản tóm tắt đoạn đó.
- Vector của các câu hỏi giả định (hypothetical questions) mà đoạn đó trả lời được.
Khi truy vấn, câu hỏi người dùng có thể khớp với bất kỳ vector nào trong số này, tăng khả năng "chạm" đúng tài liệu dù cách diễn đạt khác nhau.
Summary indexing & hypothetical-questions indexing
Hai biến thể của multi-vector, đáng gọi tên riêng:
- Summary indexing: dùng LLM sinh một bản tóm tắt cho mỗi chunk/tài liệu, embed bản tóm tắt (không phải văn bản gốc). Tóm tắt cô đọng chủ đề, ít nhiễu → khớp tốt với câu hỏi mang tính khái quát. Lúc trả về vẫn dùng văn bản gốc để trả lời.
- Hypothetical-questions indexing: với mỗi chunk, dùng LLM sinh vài câu hỏi mà chunk đó trả lời được, rồi embed các câu hỏi. Vì người dùng gõ câu hỏi, việc so khớp "câu hỏi truy vấn ↔ câu hỏi giả định" thường sát hơn so khớp "câu hỏi ↔ đoạn trả lời" (thu hẹp khoảng cách ngữ nghĩa giữa câu hỏi và tài liệu).
Contextual retrieval
Vấn đề kinh điển: một chunk như "Số dư tối thiểu là 1 triệu đồng" không nói rõ sản phẩm nào, áp dụng từ khi nào. Tách khỏi tài liệu, nó tự nó không đủ nghĩa. Contextual retrieval (được Anthropic mô tả và phổ biến) giải quyết bằng cách: trước khi embed, dùng LLM sinh một câu ngữ cảnh ngắn định vị chunk trong tài liệu tổng rồi gắn vào đầu chunk, ví dụ "Đoạn này thuộc Quy chế tài khoản thanh toán 2025, mục điều kiện duy trì tài khoản gói Standard:" + nội dung gốc. Chunk sau khi bổ sung ngữ cảnh vừa được embed vừa được index từ khoá (BM25), giúp cả tìm kiếm vector lẫn lexical bám đúng. Kỹ thuật này kết hợp rất tự nhiên với hybrid search ở RAG nâng cao 3 — Hybrid search.
Metadata & filtering
Mỗi chunk nên mang metadata có cấu trúc: nguồn (tên/đường dẫn tài liệu), ngày ban hành/hiệu lực, phòng ban sở hữu, độ mật (public/internal/confidential), số hiệu điều khoản, phiên bản. Metadata phục vụ hai mục đích:
- Lọc trước/sau retrieve (filtering): chỉ tìm trong tài liệu còn hiệu lực, thuộc phòng ban người dùng có quyền, dưới mức độ mật cho phép. Đây là lớp kiểm soát truy cập tối quan trọng trong ngân hàng — xem LLMOps 4 — Bảo mật & prompt injection.
- Trích dẫn (citation): trả về "theo Điều 7 Khoản 2, Quy chế X ngày…" giúp người dùng kiểm chứng.
Metadata filtering kết hợp với hybrid search (vector + BM25) tạo thành lớp truy hồi mạnh; chi tiết ở RAG nâng cao 3.
Sơ đồ dưới đây minh hoạ small-to-big và multi-vector cùng trên một trục "một tài liệu, nhiều đại diện để tìm":
Chọn embedding model
Chất lượng retrieve phụ thuộc trực tiếp vào mô hình embedding. Các tiêu chí chọn (nền tảng khái niệm ở LLM 3 — Embeddings & vector và Vector 7 — RAG retrieval):
- Đa ngôn ngữ (multilingual): với tài liệu tiếng Việt, bắt buộc chọn model được huấn luyện tốt trên tiếng Việt/đa ngôn ngữ. Model chỉ mạnh tiếng Anh sẽ cho tương đồng kém trên văn bản tiếng Việt có dấu, từ Hán-Việt, thuật ngữ pháp lý. Nên đánh giá bằng chính tập câu hỏi thực tế của bạn thay vì tin bảng xếp hạng chung.
- Kích thước vector (dimension): nhiều chiều (1024, 1536) thường chính xác hơn nhưng tốn bộ nhớ và index chậm hơn. Một số model hỗ trợ Matryoshka — cắt bớt chiều mà vẫn giữ phần lớn chất lượng — để đánh đổi linh hoạt.
- Chuẩn hoá (normalization): nếu dùng cosine similarity, hãy chuẩn hoá vector về độ dài 1 (L2-normalize) để phép đo nhất quán; nhiều thư viện làm sẵn. Đảm bảo cùng một model và cùng cách chuẩn hoá cho cả lúc index lẫn lúc truy vấn — lệch model là lỗi âm thầm phá retrieve.
- Độ dài ngữ cảnh tối đa của model embedding: phải lớn hơn chunk lớn nhất, nếu không phần vượt sẽ bị cắt cụt.
Cập nhật, versioning và chi phí re-embed
Index không phải "làm một lần xong". Cần trả lời:
- Cập nhật tài liệu: khi một quy chế được sửa, phải re-chunk và re-embed đúng phần thay đổi, đồng thời xoá/đánh dấu hết hiệu lực các chunk cũ để tránh trả lời theo phiên bản lỗi thời. Dùng metadata
version,effective_date,is_current. - Đổi embedding model: đây là thay đổi tốn kém nhất — phải re-embed TOÀN BỘ kho vì vector cũ và mới không cùng không gian, không so sánh được. Hãy xem việc chọn model như một cam kết dài hạn; nếu buộc đổi, chạy song song (blue-green) hai index rồi cắt chuyển.
- Chi phí re-embed: chi phí ≈ (tổng số token) × (đơn giá embedding). Với kho hàng trăm nghìn trang, đây là khoản đáng kể; hãy embed theo lô, cache theo hash nội dung chunk để không embed lại chunk không đổi, và ước lượng chi phí trước khi bật lại toàn bộ pipeline.
- Versioning index: đặt tên/không gian index kèm phiên bản chunking + phiên bản model (ví dụ
rag_quyche_v3_voyage) để có thể rollback và tái lập kết quả.
Pseudocode minh hoạ (semantic chunk + parent-doc retriever)
Đoạn dưới đây chỉ để minh hoạ luồng ý tưởng, không phải API cụ thể của một thư viện. Tham số và tên hàm là giả định.
# MINH HOẠ — không phải API thật của thư viện nào
# --- 1) Semantic chunking: cắt theo ranh giới ngữ nghĩa ---
def semantic_chunk(text, threshold=0.35, max_tokens=500):
sentences = split_sentences(text)
embs = embed([s for s in sentences]) # embed từng câu
chunks, cur = [], [sentences[0]]
for i in range(1, len(sentences)):
sim = cosine(embs[i - 1], embs[i]) # tương đồng câu liền kề
too_long = token_len(cur) >= max_tokens
if sim < threshold or too_long: # chủ đề đổi -> cắt
chunks.append(" ".join(cur)); cur = []
cur.append(sentences[i])
if cur: chunks.append(" ".join(cur))
return chunks
# --- 2) Small-to-big: index child, trả parent ---
def build_index(doc):
parents = split_by_clause(doc.text) # parent = theo điều/khoản
for p in parents:
parent_id = store_parent(p, meta={
"source": doc.name, "dept": doc.dept,
"confidentiality": doc.level, "version": doc.version,
"effective_date": doc.effective_date, "is_current": True,
})
for child in semantic_chunk(p.text):
ctx = llm_context_line(doc, p) # contextual retrieval
child_text = ctx + "\n" + child
vec = embed_normalized(child_text) # chuẩn hoá L2
index.add(vector=vec, parent_id=parent_id, meta=p.meta)
def retrieve(query, user):
qvec = embed_normalized(query)
hits = index.search(qvec, top_k=20, filter={ # metadata filtering
"is_current": True,
"confidentiality__lte": user.clearance,
"dept__in": user.allowed_depts,
})
parent_ids = dedupe([h.parent_id for h in hits]) # phóng to lên parent
return [load_parent(pid) for pid in parent_ids][:5]
Luồng: cắt semantic thành child nhỏ, mỗi child được gắn dòng ngữ cảnh rồi embed; index chỉ giữ child nhưng ánh xạ về parent theo điều-khoản; lúc truy vấn lọc theo metadata phòng ban/độ mật, tìm bằng child, khử trùng và trả về parent để LLM đọc.
Use case thực tế
Bối cảnh — NCB index kho quy định & hợp đồng. Khối Pháp chế và các phòng nghiệp vụ có khoảng 1.200 văn bản (quy chế nội bộ, quy trình, hợp đồng mẫu, thông tư tham chiếu), ước tính ~9.000 trang, cần một trợ lý hỏi-đáp để cán bộ tra cứu "điều kiện cấp hạn mức gói X", "quy trình mở tài khoản doanh nghiệp"…
Quyết định thiết kế:
- Chunking theo cấu trúc điều-khoản. Vì tài liệu đã đánh số "Chương / Điều / Khoản", ta cắt structure-aware: mỗi Điều là một parent chunk; trong đó cắt tiếp theo Khoản (và recursive nếu một khoản quá dài) thành child chunk. Ước lượng ~9.000 trang × ~2 chunk-con/trang ≈ 18.000 child chunk, ~5.000 parent chunk.
- Parent-document retrieval. Embed 18.000 child (mỗi child ~250–400 token), nhưng trả về nguyên Điều làm ngữ cảnh — nhờ đó câu trả lời không bị cụt "…theo khoản trên" mà có đủ định nghĩa trong cùng Điều.
- Contextual retrieval. Mỗi child được thêm dòng "Thuộc [Tên văn bản] — [Chương/Điều]" trước khi embed, giúp phân biệt các điều khoản na ná nhau giữa nhiều sản phẩm.
- Metadata phòng ban & độ mật. Gắn
dept(Pháp chế, Bán lẻ, Doanh nghiệp…),confidentiality(internal/confidential),version,effective_date,is_current. Cán bộ Bán lẻ chỉ truy hồi được tài liệu phòng mình + tài liệu dùng chung, không thấy hợp đồng mật của Khối Doanh nghiệp. Khi một quy chế được thay thế, đánhis_current=falseđể không bị trả lời theo bản cũ. - Embedding đa ngôn ngữ cho tiếng Việt, vector 1024 chiều, chuẩn hoá cosine; index HNSW.
Chi phí ước lượng (minh hoạ). Tổng token embed ≈ 18.000 child × ~350 token ≈ 6,3 triệu token. Với đơn giá embedding cỡ vài chục nghìn đồng cho mỗi triệu token, chi phí embed toàn kho lần đầu chỉ ở mức vài trăm nghìn đến ~1 triệu đồng — rẻ so với công sức chuẩn hoá tài liệu. Chi phí thật nằm ở: (a) sinh contextual line bằng LLM cho 18.000 child (một lần), (b) re-embed khi đổi model. Nhờ cache theo hash nội dung, cập nhật hằng tháng chỉ re-embed phần thay đổi (~vài %), gần như không đáng kể.
Kết quả kỳ vọng: so với baseline fixed-size 500 token không metadata, cấu hình trên cải thiện rõ tính đúng-ngữ-cảnh của câu trả lời và loại được rò rỉ tài liệu mật nhờ lọc metadata — hai yếu tố quan trọng nhất trong môi trường ngân hàng. Đánh giá định lượng (recall@k, faithfulness) sẽ bàn ở các bài sau của series.
Ghi nhớ
- Chunking đóng khung trần chất lượng RAG: chunk quá to → nhiễu, tốn token, khớp kém; quá nhỏ → mất ngữ cảnh. Không có con số token vàng — chọn theo cấu trúc tài liệu.
- Ưu tiên cắt theo cấu trúc/điều-khoản cho hợp đồng, quy chế; recursive cho tài liệu hỗn hợp; semantic cho văn xuôi dài; document-specific cho bảng/hoá đơn.
- Tách "đơn vị để tìm" khỏi "đơn vị để đọc": parent-document (small-to-big) embed child nhỏ nhưng trả parent lớn.
- Multi-vector (đoạn con + tóm tắt + câu hỏi giả định) tăng khả năng chạm đúng tài liệu; summary/hypothetical-questions indexing thu hẹp khoảng cách ngữ nghĩa câu hỏi ↔ tài liệu.
- Contextual retrieval: thêm dòng ngữ cảnh vào chunk trước khi embed để chunk tự đủ nghĩa; kết hợp tốt với hybrid search.
- Metadata (nguồn, ngày, phòng ban, độ mật, version) phục vụ cả lọc kiểm soát truy cập lẫn trích dẫn — bắt buộc trong ngân hàng.
- Embedding: chọn model đa ngôn ngữ cho tiếng Việt, cố định model + cách chuẩn hoá cho cả index và truy vấn; đổi model = re-embed toàn bộ.
- Versioning & cache theo hash để cập nhật rẻ; ước lượng chi phí re-embed trước khi chạy lại toàn kho.
Nguồn tham khảo
- Anthropic — "Introducing Contextual Retrieval" (anthropic.com/news/contextual-retrieval)
- LangChain Documentation — "Text splitters" (khái niệm) &
RecursiveCharacterTextSplitter - LlamaIndex Documentation — "Node Parsers / Text Splitters" (bao gồm
SemanticSplitterNodeParser) và pattern "Parent Document / Auto-Merging Retriever" - Liu et al., "Lost in the Middle: How Language Models Use Long Contexts" (arXiv:2307.03172)
- Muennighoff et al., "MTEB: Massive Text Embedding Benchmark" (arXiv:2210.07316) — tham chiếu khi so sánh/đánh giá embedding model
- Kusupati et al., "Matryoshka Representation Learning" (arXiv:2205.13147)
Bài viết liên quan
Đặt nền cho chuỗi AI: phân biệt ba vòng tròn lồng nhau AI ⊃ ML ⊃ DL và khác biệt bản chất giữa lập trình truyền thống với học từ dữ liệu. Giới thiệu ba kiểu học máy (supervised, unsupervised, reinforcement), phân loại descriptive/predictive/prescriptive, quy trình ML end-to-end, chia train/validation/test, overfitting/underfitting và các thuật ngữ nền tảng, gắn với ứng dụng ngân hàng NCB.
Harness — lớp scaffolding quanh model (vòng lặp, tool, context, memory, verify, sub-agent) — mới là thứ quyết định agent chạy được hay chỉ là demo. Bài này mổ xẻ giải phẫu một harness, 7 kỹ năng cốt lõi khi xây agent, single vs multi-agent (kèm số liệu hiệu quả/chi phí), các repo nên dùng, và một quickstart Python dựng-là-chạy cho bối cảnh ngân hàng.
Hiểu LLM từ gốc: bản chất dự đoán token, ba giai đoạn huấn luyện (pretraining, fine-tuning, RLHF), token, context window và các tham số sinh (temperature, top-p). Nắm hiện tượng hallucination và kỹ thuật prompt engineering (vai trò, few-shot, chain-of-thought, ràng buộc đầu ra), kèm ví dụ gọi API model Claude mới nhất với adaptive thinking.
Vì sao dữ liệu quyết định chất lượng mô hình hơn cả thuật toán. Bài này đi qua toàn bộ pipeline chuẩn bị dữ liệu: phân loại dữ liệu, làm sạch (thiếu/ngoại lai/trùng lặp), mã hoá hạng mục, scaling, feature engineering, giảm chiều, và cách phòng data leakage — soi qua bài toán chấm điểm tín dụng.
Cảm nhận của bạn
Bình luận
Chưa có bình luận. Hãy là người đầu tiên chia sẻ!