Mục lục
- Mục tiêu bài học
- Chunking là gì
- Vì sao phải chunk
- Trade-off chunk size
- Tổng quan 5 chiến lược
- Fixed-size chunking
- Overlap chunking
- Sentence chunking
- Recursive chunking
- Semantic chunking
- Document-structure-aware chunking
- Token-based vs character-based
- Chunk metadata
- Special handling — code, markdown, table, list
- Chunk size theo task
- Pipeline loader → chunker (LangChain)
- LlamaIndex node parser
- Markdown-aware splitter
- Code splitter
- Hierarchical chunking
- Parent-Document retrieval
- Pitfalls hay gặp
- Evaluation chunk quality
- LLM-based chunking
- So sánh 3 strategy trên 1 đoạn
- Recommendation default
- Bài tập
Mục tiêu bài học
Sau bài này, bạn cần:
- Giải thích vì sao phải chunk thay vì embed cả document.
- Phân biệt 5 chiến lược chunking và biết khi nào dùng cái nào.
- Biết trade-off chunk size — quá nhỏ vs quá lớn — và sweet spot phổ biến.
- Dùng được
RecursiveCharacterTextSplitter(LangChain) vàSentenceSplitter(LlamaIndex). - Áp dụng markdown-aware và code-aware splitter cho input có cấu trúc.
- Hiểu hierarchical chunking và parent-document retrieval.
- Tránh các pitfall: cắt giữa câu, mismatch tokenizer, mất metadata.
- Có default config để start: recursive, size 500, overlap 100.
Bài này nằm trong Module 6 (RAG). Bài 36 đã nói về embedding model, Bài 37 nói về vector DB. Bài này lấp phần "chia document trước khi embed".
Chunking là gì
Chunking là bước chia một document dài (PDF 200 trang, file Markdown 10K dòng, transcript meeting 2 giờ) thành các đoạn nhỏ — chunk — trước khi gọi embedding model. Mỗi chunk sẽ:
- Được embed thành một vector.
- Được lưu vào vector DB cùng metadata (source, page, section).
- Trở thành "đơn vị retrieve" — retriever trả về chunk, không phải document.
Nói cách khác: granularity của chunk = granularity của retrieval. Bạn chunk ra sao thì retriever trả về cấp đó.
Vì sao phải chunk
Bốn lý do chính:
- Context limit của embedding model — model embedding có max input thường 512 token (sentence-transformers cũ), 8192 token (OpenAI
text-embedding-3-*, BGE-M3). Document dài hơn sẽ bị truncate, mất thông tin. - Embed cả document → vector "loãng" — vector là trung bình ngữ nghĩa toàn đoạn. Nhồi 50 chủ đề vào 1 vector thì query về 1 chủ đề khó match — gọi là semantic dilution.
- Retrieve cần độ cụ thể — user hỏi 1 câu cụ thể; trả về 1 đoạn 300 token đúng trọng tâm tốt hơn trả về cả chương 50 trang.
- Cost LLM ở phase Query — chunk nhỏ → context prompt nhỏ → ít token → rẻ hơn và nhanh hơn.
Mục tiêu chunk: vừa đủ context để chunk có ý nghĩa đứng một mình, vừa đủ specific để retrieve trúng câu hỏi.
Trade-off chunk size
Chunk size Đặc điểm Phù hợp
─────────────────────────────────────────────────────────────────────────────
Quá nhỏ Mất context, fragment, câu cụt QA siêu specific
(50-100 token) "they decided to..." không biết ai (hiếm)
Vừa Đủ context một paragraph, đủ specific Default, 80% case
(200-800 token) Sweet spot phổ biến RAG QA, search
Quá lớn Nhiều chủ đề trong 1 chunk, noise, Summarization,
(2000+ token) embedding "loãng", LLM context tăng long-form context
Số "sweet spot" 200-800 không phải tuyệt đối; nó là khoảng giá trị đã quan sát trên nhiều benchmark RAG (xem doc LangChain và LlamaIndex). Luôn cần test trên data của bạn.
Một quan sát thực tế: với corpus tiếng Việt, do trung bình token-per-character cao hơn tiếng Anh, chunk 500 ký tự thường tương đương ~150-200 token — ít hơn người ta tưởng.
Tổng quan 5 chiến lược
Strategy Ý tưởng Pros / Cons
─────────────────────────────────────────────────────────────────────────────
1. Fixed-size Cắt mỗi N ký tự / token Đơn giản, nhanh /
cắt giữa câu
2. Overlap Fixed-size + overlap k% Giữ context biên /
tăng số chunk
3. Sentence Tách theo câu (NLTK / spaCy) Bảo toàn câu /
câu dài/ngắn lệch
4. Recursive Thử các separator có thứ tự Cân bằng tốt /
default
5. Semantic Group câu có cosine cao Boundary tự nhiên /
chậm, cần embed
+ Structure-aware Theo heading, section, code block Tôn trọng cấu trúc /
cần parser riêng
Trong production, người ta thường kết hợp: structure-aware ở ngoài (chia theo heading) + recursive ở trong (chia mỗi section thành chunk vừa).
Fixed-size chunking
Đơn giản nhất: cắt mỗi N ký tự (hoặc N token).
def fixed_chunk(text: str, size: int = 500) -> list[str]:
return [text[i:i + size] for i in range(0, len(text), size)]
chunks = fixed_chunk(long_text, size=500)
Pros: nhanh, không phụ thuộc thư viện, deterministic, dễ debug.
Cons: cắt giữa câu, giữa từ. Ví dụ "...được sử dụng để truy xuất" có thể bị cắt thành "...được sử dụ" + "ng để truy xuất" — chunk đầu mất ý, chunk sau bắt đầu lửng.
Dùng được khi nội dung là log/JSON dòng đều, hoặc khi đã chấp nhận nhược điểm này để có baseline nhanh.
Overlap chunking
Cải tiến fixed-size: cho hai chunk liền nhau chồng lấn một đoạn nhỏ ở biên. Lý do: thông tin quan trọng có thể nằm đúng chỗ cắt; overlap giúp chunk thứ N+1 vẫn "thấy" cuối chunk N.
def overlap_chunk(text: str, size: int = 500, overlap: int = 100) -> list[str]:
step = size - overlap
return [text[i:i + size] for i in range(0, len(text), step)]
chunks = overlap_chunk(long_text, size=500, overlap=100)
Overlap phổ biến: 10-20% của size. Quá ít thì không giải quyết được vấn đề biên; quá nhiều thì số chunk tăng, lưu trữ và cost embed tăng tỉ lệ.
Cảnh báo: overlap không cứu được lỗi "cắt giữa từ" của fixed-size. Muốn fix mạch ngữ nghĩa, hãy dùng recursive (bước 9).
Sentence chunking
Tách văn bản theo câu (sentence boundary), rồi group N câu liên tiếp thành 1 chunk. Bảo toàn nguyên câu là điểm mạnh chính.
import nltk
nltk.download("punkt_tab", quiet=True)
from nltk.tokenize import sent_tokenize
def sentence_chunk(text: str, n: int = 5) -> list[str]:
sents = sent_tokenize(text)
return [" ".join(sents[i:i + n]) for i in range(0, len(sents), n)]
spaCy cũng có sentencizer, hoạt động tốt cho tiếng Anh. Tiếng Việt nên thử underthesea hoặc pyvi nếu tách câu bằng NLTK ra lệch.
Cons: câu trong doc kỹ thuật có thể rất dài hoặc rất ngắn — chunk không đều, có chunk 100 token, có chunk 1000.
Recursive chunking
Ý tưởng (LangChain RecursiveCharacterTextSplitter): có một danh sách separator theo thứ tự ưu tiên. Thử cắt theo separator đầu (paragraph). Nếu chunk vẫn lớn hơn chunk_size, thử separator tiếp (newline đơn). Lặp xuống tới câu, từ, ký tự.
Separators mặc định (theo thứ tự):
"\n\n" (paragraph)
"\n" (line)
". " (sentence, English)
" " (word)
"" (character)
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(text)
Recursive cân bằng tốt giữa "tôn trọng boundary tự nhiên" và "chunk đều size". Đây là default được khuyên cho 80% trường hợp.
Với tiếng Việt, có thể thêm "。", "?", "!", "; " vào danh sách separator để tách câu mềm hơn.
Semantic chunking
Ý tưởng (Kamradt 2023, sau đó LangChain SemanticChunker, LlamaIndex SemanticSplitter): tách theo câu trước, embed từng câu, đo cosine giữa câu liên tiếp. Khi similarity tụt qua một ngưỡng (ví dụ percentile 95), coi đó là chỗ chuyển chủ đề — cắt chunk ở đó.
Bước:
1. Tách câu.
2. Embed mỗi câu.
3. Tính cos_sim(sent_i, sent_{i+1}).
4. Tìm các điểm cos_sim < threshold (drop) → break point.
5. Chunk = group câu giữa hai break point.
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
splitter = SemanticChunker(
OpenAIEmbeddings(model="text-embedding-3-small"),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95,
)
chunks = splitter.split_text(text)
Pros: boundary trùng với chuyển chủ đề thật → chunk có nghĩa rõ.
Cons: chậm (phải embed từng câu lúc index), tốn tiền nếu dùng API embedding, size chunk không kiểm soát được chính xác. Đáng dùng cho corpus quan trọng, không lớn quá.
Document-structure-aware chunking
Nếu document có cấu trúc, hãy tôn trọng cấu trúc đó thay vì chia bằng ký tự thuần.
- Markdown — split theo
#,##,###; giữ heading làm metadata cho chunk con (MarkdownHeaderTextSplitter). - HTML — split theo
<section>,<article>,<h1>-<h6>(HTMLHeaderTextSplitter). - PDF — split theo page, paragraph; giữ page number trong metadata để cite.
- Code — split theo function / class boundary (bước 19).
- Notion / Confluence — split theo block / page.
Pipeline thường gặp: structure-aware ở ngoài chia thành "section" có ngữ cảnh, sau đó recursive ở trong chia tiếp những section quá dài thành chunk vừa.
Token-based vs character-based
Cách đo size Pros Cons
─────────────────────────────────────────────────────────────────────────────
Character Đơn giản, không cần tokenizer Không khớp với context
limit embedding (đo token)
Token Khớp đúng limit của model Cần tokenizer của model
(tiktoken, HF) Chính xác hơn cho cost (mismatch dễ sai)
Trong production, đo bằng token chính xác hơn vì embedding model và LLM đều tính theo token. LangChain hỗ trợ:
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=400, # 400 token, không phải ký tự
chunk_overlap=80,
)
Quan trọng: tokenizer dùng để đo chunk phải match với embedding model. Đo bằng cl100k_base (GPT) mà embed bằng BGE (sentencepiece) thì sẽ lệch — không sai nghiêm trọng nhưng size không đúng dự kiến.
Chunk metadata
Mỗi chunk lưu vào vector DB nên kèm metadata. Thiếu metadata = không cite được, không filter được, không debug được.
source— đường dẫn file hoặc URL nguyên thuỷ.page— số trang (PDF, slide).section_title— heading của section chứa chunk.chunk_index— thứ tự chunk trong document (để fetch chunk lân cận khi cần).timestamp— thời gian doc tạo / cập nhật (cho filter "chỉ doc 2026").doc_type— pdf / md / code / ticket.tenant_id— nếu multi-tenant.
col.add(
ids=[f"{doc_id}_chunk_{i}" for i in range(len(chunks))],
documents=chunks,
embeddings=embedder.encode(chunks).tolist(),
metadatas=[{
"source": "policy.pdf",
"page": page_for_chunk[i],
"section_title": section_for_chunk[i],
"chunk_index": i,
} for i in range(len(chunks))],
)
Special handling — code, markdown, table, list
- Code — split theo function / class boundary; không bao giờ cắt giữa thân hàm. LangChain
RecursiveCharacterTextSplitter.from_language(Language.PYTHON, ...)có sẵn separator AST-aware. - Markdown — giữ heading context kèm chunk con. Nếu chunk là phần dưới
## Cấu hình → ### Database, thêm hai header vào đầu chunk để query về "Database" vẫn match đúng phần đó. - Table — không split. Bảng cắt nửa mất đầu cột, vô nghĩa. Coi cả bảng là 1 chunk (kể cả vượt size), hoặc convert sang dạng row-by-row "Cột A=..., Cột B=...".
- List dài — có thể split theo bullet group (5-10 bullet / chunk), kèm tiêu đề list ở đầu mỗi chunk.
- Code block trong markdown — không cắt vào trong block; giữ
```lang ... ```nguyên vẹn. - Image / figure caption — kèm caption vào chunk text gần nó; image binary nằm metadata.
Chunk size theo task
Task Chunk size khuyên Lý do
─────────────────────────────────────────────────────────────────────────────
QA factual ("năm nào?") 200-400 token Specific, retrieve hẹp
Search ngữ nghĩa 400-800 token Cân bằng context/specific
Summarization 800-2000 token Cần context dài
Long-form Q&A 600-1200 token Vừa cụ thể vừa đủ ngữ cảnh
Code search theo function / class Không theo token
Conversation memory 1-3 message / chunk Theo turn
Đây chỉ là gợi ý ban đầu. Cách đúng là chạy benchmark trên golden set của bạn với 2-3 size khác nhau, đo accuracy và faithfulness (Bài 58 nói về metric).
Pipeline loader → chunker (LangChain)
Pattern phổ biến với LangChain: loader đọc file thành Document object có page_content + metadata; splitter nhận Document trả về list Document con (mỗi cái là 1 chunk).
pip install langchain langchain-community pypdf
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
loader = PyPDFLoader("policy.pdf")
docs = loader.load() # mỗi Document = 1 page, có metadata page
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
)
chunks = splitter.split_documents(docs)
# chunks[i].page_content -> text
# chunks[i].metadata -> {'source': 'policy.pdf', 'page': 3, ...}
print(len(docs), "pages ->", len(chunks), "chunks")
split_documents giữ nguyên metadata của doc cha; mỗi chunk con kế thừa source và page — đủ để cite.
LlamaIndex node parser
LlamaIndex gọi chunk là Node. SentenceSplitter là default — cắt theo câu rồi gom đủ token.
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
docs = SimpleDirectoryReader("./data").load_data()
splitter = SentenceSplitter(chunk_size=512, chunk_overlap=50)
nodes = splitter.get_nodes_from_documents(docs)
print(len(nodes), "nodes")
print(nodes[0].text[:200])
print(nodes[0].metadata)
Khác biệt nhỏ so với LangChain: LlamaIndex tự duy trì quan hệ prev_node / next_node để dễ "mở rộng context" lúc retrieve (xem bước 20-21).
Hai library này gần tương đương về chất lượng chunk; chọn theo style stack bạn đang dùng.
Markdown-aware splitter
Với corpus markdown (docs, blog, knowledge base): chia trước theo heading, rồi recursive trong từng section. Mỗi chunk con tự kèm chuỗi heading làm metadata — cite được "section nào trong file nào".
from langchain.text_splitter import (
MarkdownHeaderTextSplitter,
RecursiveCharacterTextSplitter,
)
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
)
sections = md_splitter.split_text(md_text)
# sections[i].page_content = body section
# sections[i].metadata = {'h1': '...', 'h2': '...'}
inner = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
chunks = inner.split_documents(sections)
Khi retrieve, query "cách cài đặt postgres" sẽ match chunk có metadata h2 = "Cài đặt" → ngữ cảnh tự rõ hơn nhiều so với chunk cùng nội dung nhưng mất heading.
Code splitter
Code không nên chia theo paragraph hay câu. LangChain cung cấp recursive splitter theo ngôn ngữ: separator đầu tiên là class, kế đến là def, rồi block, rồi line.
from langchain.text_splitter import (
RecursiveCharacterTextSplitter,
Language,
)
py_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=500,
chunk_overlap=50,
)
chunks = py_splitter.split_text(code_text)
# Tương tự cho JS, TS, GO, JAVA, CPP, RUST, ...
js_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.JS,
chunk_size=500,
)
Kết quả: mỗi chunk thường là 1 hàm hoặc 1 method, không bị cắt nửa thân hàm. Tốt cho code search / Copilot-style retrieval.
Hierarchical chunking
Một chunk size không phục vụ tốt mọi truy vấn. Hierarchical chunking tạo nhiều cấp cùng lúc: chunk nhỏ (200 token), chunk vừa (800 token), chunk lớn (2400 token), với quan hệ cha-con.
Document
└─ Large chunk (2400 token) ← context cho LLM
└─ Medium chunk (800 token)
└─ Small chunk (200 token) ← embed + retrieve
Retrieve trên chunk nhỏ (đặc trưng cao, match chính xác), nhưng đưa chunk cha vào prompt cho LLM (có ngữ cảnh đầy đủ). LlamaIndex có HierarchicalNodeParser và AutoMergingRetriever implement pattern này.
Parent-Document retrieval
Biến thể đơn giản của hierarchical: index chunk nhỏ trong vector DB, nhưng khi retrieve trả về chunk cha (section / page) chứa nó. Lợi cả hai: search chính xác + context đủ.
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
vectorstore = Chroma(
collection_name="docs",
embedding_function=HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
),
)
docstore = InMemoryStore()
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=docstore,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
retriever.add_documents(docs)
results = retriever.invoke("câu hỏi cụ thể") # trả về parent chunk
Pattern này đặc biệt hữu ích cho tài liệu pháp lý / kỹ thuật, nơi một câu trả lời cần đặt vào context của cả một điều khoản / phần.
Pitfalls hay gặp
- Cắt giữa câu / giữa từ — dùng fixed-size thuần cho corpus tự nhiên. Khắc phục: recursive splitter.
- Chunk quá nhỏ — câu cụt, không đủ ngữ cảnh đứng một mình ("Họ quyết định...").
- Chunk quá lớn — nhiều chủ đề trộn, vector "loãng", retrieve top-K kéo nhiều noise.
- Quên metadata — không cite được nguồn, không filter được theo source / page / time.
- Tokenizer mismatch — đo chunk size bằng
tiktokenrồi embed bằng BGE/E5 → size thực vs dự kiến lệch. - Cắt vào trong code block / table — markdown / pdf parser ngây thơ làm. Cần parser cấu trúc.
- Không dedup — file giống nhau ở nhiều nơi → chunk trùng, context lặp, LLM bias.
- Header bị mất khi chia con — chunk "Backup mỗi ngày" mà không có heading "Database production" thì khi retrieve mất ngữ cảnh.
- Overlap quá lớn — vượt 30% size, lưu trữ và embed cost tăng đáng kể mà chất lượng không tăng tương ứng.
- Một chiến lược cho mọi loại file — code, markdown, PDF, transcript yêu cầu strategy khác nhau.
Evaluation chunk quality
Hai cấp đánh giá:
- Eyeball — lấy ngẫu nhiên 10-20 chunk, đọc xem mỗi chunk có là một "đoạn văn có nghĩa đứng riêng" không. Nếu thấy nhiều câu cụt, chunk vô nghĩa — strategy hiện tại sai.
- End-to-end — chạy RAG hoàn chỉnh trên golden set 30-100 Q&A với 2-3 size / strategy. Đo Context Recall, Context Precision, Faithfulness (RAGAS, DeepEval). Chọn config tốt nhất.
Workflow tune chunk size:
1. Tạo golden set 30 Q&A (câu hỏi + đáp án mong đợi).
2. Set baseline: recursive, size=500, overlap=100.
3. Lần lượt thử: size=200, 800, 1500.
4. Đo Context Recall + Answer Accuracy mỗi config.
5. Chọn config tốt nhất, fine-tune overlap quanh đó.
Không có "best chunk size" tuyệt đối — câu trả lời luôn là "tuỳ data và task". Phải đo.
LLM-based chunking
Hướng mới 2024-2026: dùng chính LLM nhỏ (Haiku, GPT-4o-mini, Llama 3 8B) để đọc document và quyết định chỗ cắt — gọi là agentic hoặc LLM-based chunking. Ý tưởng: LLM hiểu boundary chủ đề tốt hơn rule-based.
Prompt mẫu:
"Chia đoạn văn sau thành các chunk theo chủ đề.
Mỗi chunk 200-600 token. Trả về JSON list."
Pros: chất lượng boundary cao nhất hiện tại.
Cons: chậm hơn 100-1000 lần, tốn tiền (LLM call cho mỗi document). Đáng dùng cho corpus quý (sách giáo khoa, đáng cite), không phải corpus lớn / cập nhật nhanh.
Xem thêm llmsherpa, Docling (IBM) — kết hợp parsing có cấu trúc + LLM gọn nhẹ.
So sánh 3 strategy trên 1 đoạn
Code mẫu chạy 3 splitter trên cùng input, in ra chunk thứ nhất để so sánh chất lượng boundary:
from langchain.text_splitter import (
CharacterTextSplitter,
RecursiveCharacterTextSplitter,
)
text = open("article.txt", encoding="utf-8").read()
# 1) Fixed-size
fixed = CharacterTextSplitter(
separator="", chunk_size=500, chunk_overlap=0
).split_text(text)
# 2) Overlap fixed
overlap = CharacterTextSplitter(
separator="", chunk_size=500, chunk_overlap=100
).split_text(text)
# 3) Recursive
recursive = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=100,
separators=["\n\n", "\n", ". ", " ", ""],
).split_text(text)
for name, chunks in [("fixed", fixed),
("overlap", overlap),
("recursive", recursive)]:
print(f"=== {name} ({len(chunks)} chunks) ===")
print(chunks[0][:200], "...")
print()
Chạy 3 lần, đọc tay 5 chunk đầu mỗi strategy: recursive thường cho chunk kết thúc bằng dấu câu, fixed thường kết thúc giữa từ. Khác biệt rõ.
Recommendation default
Nếu chưa biết chọn gì, dùng cấu hình này làm baseline rồi tune:
Splitter: RecursiveCharacterTextSplitter
chunk_size: 500 (token nếu đo bằng tiktoken; 1500-2000 ký tự nếu char)
chunk_overlap: 100 (~20%)
separators: ["\n\n", "\n", ". ", " ", ""]
Metadata: source, page, section_title, chunk_index
Đo bằng: token (tiktoken hoặc tokenizer của embedding model)
Đặc biệt: Markdown → MarkdownHeaderTextSplitter + recursive
Code → RecursiveCharacterTextSplitter.from_language
Table → keep whole, không split
Sau khi có baseline chạy được, lần lượt thử 200, 800, 1500; chọn cái cho accuracy / faithfulness cao nhất trên golden set của bạn.
Đừng tối ưu chunk size khi chưa có golden set — tất cả thay đổi sẽ là cảm tính.
Bài tập
- Chọn 1 file PDF khoảng 20-50 trang. Chunk nó với 3 strategy (fixed-size, overlap, recursive) cùng
chunk_size=500. So sánh: tổng số chunk, độ dài trung bình, số chunk kết thúc bằng dấu câu (proxy đo "boundary tốt"). Viết 5 dòng nhận xét. - Lấy 1 file Markdown có heading (ví dụ README dài). Áp dụng
MarkdownHeaderTextSplitter+RecursiveCharacterTextSplitter. In ra metadata của 5 chunk đầu, kiểm trah1/h2có đúng không. - Build pipeline mini: PDF →
PyPDFLoader→RecursiveCharacterTextSplitter(500/100) → embed bằngsentence-transformers/all-MiniLM-L6-v2→ index vào ChromaDB → hỏi 3 câu, in ra top-3 chunk retrieve được kèm metadatasource+page. - Trên cùng PDF ở câu 3, chạy 3 cấu hình chunk size: 200, 500, 1500. Tạo golden set 5 câu hỏi (bạn biết đáp án). Đo: với mỗi config, top-3 retrieve có chứa đáp án đúng không? Ghi lại tỉ lệ "có" trên 5 câu × 3 config.
- (Tuỳ chọn) Thử
SemanticChunkercủa LangChain vớiOpenAIEmbeddingshoặcHuggingFaceEmbeddingstrên 1 đoạn 5000-10000 từ. So sánh chunk count và boundary với recursive trên cùng input. Nhận xét thời gian chạy.
- LangChain — Text splitters concept
- LangChain — RecursiveCharacterTextSplitter
- LangChain — MarkdownHeaderTextSplitter
- LangChain — Splitting code
- LangChain — Semantic Chunker
- LangChain — Parent Document Retriever
- LlamaIndex — Node parsers
- LlamaIndex — Semantic Chunking
- LlamaIndex — Auto Merging Retriever (hierarchical)
- Pinecone — Chunking strategies for LLM applications
- Chroma Research — Evaluating chunking strategies
- Greg Kamradt — 5 levels of chunking (RetrievalTutorials)
- tiktoken — OpenAI tokenizer
- Docling — Document parsing (IBM Research)
- llmsherpa — LLM-assisted document parsing
