Danh sách bài viết

Bài 22: Text Splitters — chia document thành chunk

Bài này đào sâu vào LangChain Text Splitter API (langchain-text-splitters 0.3.x): CharacterTextSplitter, RecursiveCharacterTextSplitter, token-based splitter, splitter cho Markdown/HTML/Code, và SemanticChunker. Tập trung vào code thực tế, trade-off khi chọn chunk_size/chunk_overlap, và các pitfall hay gặp.

27/05/2026
1 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • Hiểu ràng buộc kỹ thuật khiến chunking là bắt buộc trong RAG pipeline
  • Dùng được CharacterTextSplitterRecursiveCharacterTextSplitter, biết khi nào dùng cái nào
  • Dùng được token-based splitter khi cần chunk_size tính theo token
  • Áp dụng splitter chuyên dụng cho Markdown, HTML, và source code
  • Hiểu cách SemanticChunker hoạt động và chi phí của nó
  • Chọn chunk_size và chunk_overlap hợp lý cho từng use case
2

Tại sao cần chunking

Có hai ràng buộc độc lập buộc phải chia nhỏ document:

Ràng buộc từ embedding model

Embedding model có max token cứng. Vượt quá ngưỡng này, token thừa bị cắt bỏ hoàn toàn:

  • BERT-based model (sentence-transformers): thường 512 token
  • text-embedding-3-small / text-embedding-3-large (OpenAI): 8192 token
  • text-embedding-ada-002: 8191 token

Nếu bạn truyền đoạn văn 2000 token vào model giới hạn 512, phần từ token 513 trở đi bị bỏ qua hoàn toàn — không có lỗi, chỉ có embedding sai.

Ràng buộc từ retrieval granularity

Ngay cả khi model embedding hỗ trợ 8192 token, chunk quá lớn vẫn làm retrieval kém hơn:

  • Chunk 3000 token chứa 10 chủ đề → embedding là trung bình của 10 chủ đề → vector không đại diện chính xác cho nội dung nào
  • Chunk 100 token — quá nhỏ → thiếu ngữ cảnh, embedding noisy, recall thấp

Mục tiêu: mỗi chunk đại diện cho một ý, đủ ngữ cảnh để hiểu độc lập.

Ràng buộc từ context window LLM (ít quan trọng hơn khi indexing)

Khi retrieve top-k chunk để đưa vào prompt LLM, tổng token các chunk + system prompt + user query phải vừa context window. GPT-4o có 128k token, Claude 3.5 Sonnet có 200k — với chunk nhỏ (~500 token) bạn dễ dàng nhét 50-100 chunk mà không lo overflow.

3

Cài đặt package

Từ LangChain 0.1+, text splitter được tách ra package riêng:

pip install langchain-text-splitters   # core splitters
pip install tiktoken                    # cho TokenTextSplitter (OpenAI tokenizer)
pip install sentence-transformers       # cho SentenceTransformersTokenTextSplitter
# SemanticChunker nằm trong langchain_experimental
pip install langchain-experimental
pip install langchain-openai            # nếu dùng OpenAIEmbeddings với SemanticChunker

Kiểm tra version:

pip show langchain-text-splitters
# Name: langchain-text-splitters
# Version: 0.3.x
4

CharacterTextSplitter

CharacterTextSplitter là splitter đơn giản nhất: split theo một separator duy nhất, sau đó gom đến đủ chunk_size.

from langchain_text_splitters import CharacterTextSplitter

splitter = CharacterTextSplitter(
    separator="\n\n",   # tìm double newline để split
    chunk_size=500,     # tính theo character (KHÔNG phải token)
    chunk_overlap=50,   # 50 char cuối chunk này lặp lại ở đầu chunk sau
    length_function=len,
)

long_text = """
Đây là đoạn đầu tiên. Có hai câu.

Đây là đoạn thứ hai. Cũng có nội dung.

Đây là đoạn thứ ba. Nội dung khác.
"""

chunks = splitter.split_text(long_text)
for i, c in enumerate(chunks):
    print(f"Chunk {i}: {len(c)} chars — {c[:60]!r}")

Cơ chế hoạt động

  1. Split toàn bộ text tại mọi vị trí có separator → danh sách đoạn nhỏ
  2. Gom các đoạn liền kề cho đến khi tổng length ≥ chunk_size → đó là 1 chunk
  3. Bắt đầu chunk tiếp theo, lùi lại chunk_overlap character

Giới hạn

Nếu document không chứa separator (ví dụ văn bản liên tục không có paragraph rỗng), splitter sẽ trả về nguyên document trong 1 chunk — bất kể chunk_size bao nhiêu. Đây là lý do RecursiveCharacterTextSplitter phù hợp hơn trong hầu hết trường hợp.

# Trường hợp separator không có trong text:
text_lien_tuc = "A" * 2000  # 2000 char, không có "\n\n"
splitter = CharacterTextSplitter(separator="\n\n", chunk_size=500)
result = splitter.split_text(text_lien_tuc)
print(len(result))  # → 1 (không split được!)
print(len(result[0]))  # → 2000
5

RecursiveCharacterTextSplitter

RecursiveCharacterTextSplitter là lựa chọn mặc định cho hầu hết text. Nó thử split theo separator có độ ưu tiên cao nhất; nếu chunk vẫn lớn hơn chunk_size, nó tiếp tục split bằng separator tiếp theo trong danh sách.

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", " ", ""],
    # "" là fallback: split từng character — đảm bảo không chunk nào > chunk_size
)

chunks = splitter.split_text(long_text)
print(f"Số chunk: {len(chunks)}")
print(f"Max chunk size: {max(len(c) for c in chunks)}")  # sẽ ≤ chunk_size

Thứ tự ưu tiên separator

Với default separators ["\n\n", "\n", ". ", " ", ""]:

  1. Cố split tại \n\n (paragraph boundary) trước — giữ nguyên cấu trúc văn bản nhất
  2. Nếu chunk vẫn to → split tại \n (line break)
  3. Nếu vẫn to → split tại . (câu)
  4. Nếu vẫn to → split tại khoảng trắng (từ)
  5. Fallback cuối: split từng character — đảm bảo max chunk_size được tuân thủ

So sánh với CharacterTextSplitter

# Ví dụ: text 1500 char liên tục, không có "\n\n"
text = "Nội dung. " * 150  # 1500 char

from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter

char_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=500)
rec_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)

char_chunks = char_splitter.split_text(text)
rec_chunks = rec_splitter.split_text(text)

print(f"CharacterTextSplitter: {len(char_chunks)} chunks")  # 1 chunk (không split được)
print(f"RecursiveCharacterTextSplitter: {len(rec_chunks)} chunks")  # ~3 chunks

Vietnamese text

Default separators hoạt động tốt cho cả tiếng Việt. Tiếng Việt dùng khoảng trắng giữa âm tiết (khác tiếng Trung/Nhật), nên split tại khoảng trắng không cắt giữa từ ghép. Tuy nhiên, split tại khoảng trắng vẫn có thể cắt giữa từ ghép 2-3 âm tiết — nếu cần chính xác cao hơn, dùng token-based splitter với tokenizer tiếng Việt.

6

Token-based splitter

Các splitter ở trên đếm character, nhưng embedding model và LLM có giới hạn tính theo token. Cùng 500 character, tiếng Anh thường là ~100 token, tiếng Việt có thể là 150-200 token tùy tokenizer. Dùng token-based splitter khi chunk_size phải khớp chính xác với token limit của model.

TokenTextSplitter (dùng tiktoken)

from langchain_text_splitters import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=400,          # tính theo token
    chunk_overlap=40,
    encoding_name="cl100k_base",  # tokenizer của GPT-4 / text-embedding-3
)

chunks = splitter.split_text(long_text)

Các encoding phổ biến của tiktoken:

  • cl100k_base: GPT-4, GPT-3.5-turbo, text-embedding-3-small/large, text-embedding-ada-002
  • o200k_base: GPT-4o
  • p50k_base: Codex, text-davinci-003

SentenceTransformersTokenTextSplitter

Khi dùng sentence-transformers model làm embedding, dùng splitter tương ứng để đảm bảo chunk không vượt max_seq_length của model đó:

from langchain_text_splitters import SentenceTransformersTokenTextSplitter

splitter = SentenceTransformersTokenTextSplitter(
    model_name="sentence-transformers/all-MiniLM-L6-v2",
    chunk_size=256,     # all-MiniLM-L6-v2 max là 256 token
    chunk_overlap=25,
)

chunks = splitter.split_text(long_text)

Splitter này tự load tokenizer của model để đếm token chính xác. Lần đầu chạy sẽ download model tokenizer (~100MB).

7

Splitter cho Markdown, HTML, Code

MarkdownHeaderTextSplitter

Split theo header level và giữ lại thông tin header như metadata của mỗi chunk. Phù hợp khi document là tài liệu kỹ thuật, wiki có cấu trúc rõ ràng.

from langchain_text_splitters import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ("#", "h1"),
    ("##", "h2"),
    ("###", "h3"),
]

splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

markdown_text = """
# Hướng dẫn cài đặt

## Yêu cầu hệ thống

Cần Python 3.10 trở lên.

## Cài đặt package

Chạy lệnh sau:

### Với pip

pip install langchain
"""

docs = splitter.split_text(markdown_text)
for doc in docs:
    print(doc.metadata)  # {'h1': 'Hướng dẫn cài đặt', 'h2': 'Cài đặt package', 'h3': 'Với pip'}
    print(doc.page_content[:80])

Kết quả trả về là list[Document], mỗi Document có metadata chứa header path. Đây là điểm khác biệt với RecursiveCharacterTextSplitter — metadata rõ ràng giúp filtering sau này.

Nếu section vẫn dài hơn chunk_size mong muốn, chain thêm RecursiveCharacterTextSplitter:

from langchain_text_splitters import RecursiveCharacterTextSplitter

md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
char_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)

# Bước 1: split theo header
header_docs = md_splitter.split_text(markdown_text)

# Bước 2: split lại nếu section quá dài
final_chunks = char_splitter.split_documents(header_docs)

HTMLHeaderTextSplitter

from langchain_text_splitters import HTMLHeaderTextSplitter

headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
]

splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
docs = splitter.split_text(html_string)

RecursiveCharacterTextSplitter.from_language — cho source code

Split code theo boundary phù hợp với ngôn ngữ (function, class, comment block). Dùng from_language() class method:

from langchain_text_splitters import RecursiveCharacterTextSplitter, Language

# Python: ưu tiên split tại class, function, blank line
python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=1000,
    chunk_overlap=100,
)

python_code = """
class DataProcessor:
    def __init__(self, config):
        self.config = config

    def process(self, data):
        results = []
        for item in data:
            results.append(self._transform(item))
        return results

    def _transform(self, item):
        return item.strip().lower()
"""

chunks = python_splitter.split_text(python_code)

Các ngôn ngữ được hỗ trợ: Language.PYTHON, Language.JS, Language.TS, Language.GO, Language.JAVA, Language.RUST, Language.CPP, Language.MARKDOWN, Language.LATEX, Language.HTML, và một số ngôn ngữ khác.

Để xem separator list của một ngôn ngữ cụ thể:

print(RecursiveCharacterTextSplitter.get_separators_for_language(Language.PYTHON))
# ['\nclass ', '\ndef ', '\n\tdef ', '\n\n', '\n', ' ', '']
8

SemanticChunker

SemanticChunker (trong langchain_experimental) dùng embedding model để phát hiện chỗ ngữ nghĩa thay đổi, rồi đặt ranh giới chunk tại đó — thay vì dựa vào character hay token count.

Cơ chế

  1. Split text thành câu (dùng . hoặc sentencizer)
  2. Embed từng câu
  3. Tính cosine distance giữa các câu liền kề
  4. Đặt boundary tại các điểm distance vượt ngưỡng (breakpoint)

Cách dùng

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

splitter = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="percentile",  # hoặc "standard_deviation", "interquartile"
    breakpoint_threshold_amount=95,           # split tại 5% điểm có distance cao nhất
)

chunks = splitter.split_text(long_text)

Các breakpoint_threshold_type

  • "percentile": split tại những điểm distance nằm trong top X% cao nhất. breakpoint_threshold_amount=95 nghĩa là chỉ split tại 5% điểm có khoảng cách lớn nhất.
  • "standard_deviation": split khi distance vượt mean + X * std. breakpoint_threshold_amount=3 là conservative (ít boundary hơn).
  • "interquartile": split khi distance vượt Q3 + X * IQR.

Chi phí và khi nào dùng

SemanticChunker embed mỗi câu trong document — một tài liệu 1000 câu cần 1000 API call (hoặc 1000 local inference). Chi phí và latency cao hơn đáng kể so với RecursiveCharacterTextSplitter.

Phù hợp khi:

  • Document có nhiều chủ đề xen kẽ không có dấu phân cách rõ ràng
  • Đang offline batch-process document (không phải realtime)
  • Bạn dùng local embedding model (không tốn API cost)

Không phù hợp khi:

  • Cần index nhanh lượng lớn document
  • Document đã có cấu trúc rõ (Markdown header, HTML heading) — dùng splitter chuyên dụng thay
9

Chọn chunk_size và chunk_overlap

chunk_size

Không có con số tuyệt đối — phụ thuộc vào loại document và model embedding:

Loại document chunk_size khuyến nghị Ghi chú
Văn bản thông thường (FAQ, blog) 400–600 token Mỗi chunk ≈ 1 đoạn văn
Tài liệu kỹ thuật dài 600–900 token Cần đủ ngữ cảnh xung quanh
Source code 1000–1500 token Tránh cắt giữa function
Câu hỏi - đáp ngắn (Q&A pairs) 100–200 token Mỗi Q&A là 1 chunk

Khi dùng character-based splitter, quy đổi token ↔ character ước tính: 1 token ≈ 4 character (tiếng Anh), 1 token ≈ 2–3 character (tiếng Việt với BPE tokenizer).

Quy tắc giới hạn:

  • chunk_size không vượt max_seq_length của embedding model
  • chunk_size không nhỏ hơn 50 token — embedding sẽ không đủ ngữ nghĩa

chunk_overlap

Overlap giúp tránh mất thông tin ở biên chunk — câu vắt qua hai chunk sẽ xuất hiện ở cả hai, đảm bảo retrieval vẫn tìm thấy.

Khuyến nghị: overlap = 10–20% của chunk_size.

  • chunk_size=500 → chunk_overlap=50–100
  • chunk_size=1000 → chunk_overlap=100–200

Overlap = 0: phù hợp khi document có ranh giới rõ (mỗi Q&A, mỗi ticket support là 1 đơn vị độc lập).

Overlap quá lớn (> 30%): tốn storage và làm vector DB có nhiều vector trùng lặp nội dung.

10

split_text vs split_documents

Tất cả splitter đều có hai method chính:

from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)

# Method 1: split_text — input string, output list[str]
text = "Nội dung văn bản dài..."
chunks_str: list[str] = splitter.split_text(text)

# Method 2: split_documents — input list[Document], output list[Document]
docs = [
    Document(
        page_content="Nội dung trang 1...",
        metadata={"source": "report.pdf", "page": 1}
    ),
    Document(
        page_content="Nội dung trang 2...",
        metadata={"source": "report.pdf", "page": 2}
    ),
]
chunks_doc: list[Document] = splitter.split_documents(docs)

# Metadata được propagate xuống từng chunk:
print(chunks_doc[0].metadata)
# {'source': 'report.pdf', 'page': 1}

Khi nào dùng method nào

  • split_text: khi bạn chỉ cần danh sách string để embed, không cần track source
  • split_documents: khi output của Document Loader (bài 21) cần được giữ nguyên metadata — source, page, author, url... Metadata này rất quan trọng cho metadata filtering ở vector DB (xem bài 18) và cho LLM biết chunk đến từ đâu khi cite nguồn

Trong thực tế RAG pipeline, hầu như luôn dùng split_documents vì bạn cần truy nguyên nguồn của chunk.

11

Pipeline thực tế với Document Loaders

Kết hợp Document Loader (bài 21) với Text Splitter:

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# Bước 1: Load document
loader = PyPDFLoader("./annual_report.pdf")
docs = loader.load()  # list[Document], mỗi Document = 1 trang PDF

print(f"Loaded: {len(docs)} pages")
# Loaded: 45 pages

# Bước 2: Split
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
)
chunks = splitter.split_documents(docs)

print(f"Split: {len(chunks)} chunks")
# Split: ~200 chunks (tùy nội dung)

# Bước 3: Inspect
for chunk in chunks[:3]:
    print(f"  source={chunk.metadata.get('source')}, page={chunk.metadata.get('page')}")
    print(f"  content ({len(chunk.page_content)} chars): {chunk.page_content[:80]!r}")
    print()

Pipeline với nhiều loại document

from pathlib import Path
from langchain_community.document_loaders import PyPDFLoader, TextLoader, WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)

all_docs = []

# PDF
for pdf_path in Path("./docs").glob("*.pdf"):
    docs = PyPDFLoader(str(pdf_path)).load()
    all_docs.extend(docs)

# Text files
for txt_path in Path("./docs").glob("*.txt"):
    docs = TextLoader(str(txt_path), encoding="utf-8").load()
    all_docs.extend(docs)

all_chunks = splitter.split_documents(all_docs)
print(f"Tổng: {len(all_docs)} documents → {len(all_chunks)} chunks")

Lazy loading với split (tiết kiệm memory)

# Với document rất lớn, dùng lazy_load() để không load toàn bộ vào memory
loader = PyPDFLoader("./large_book.pdf")

all_chunks = []
for doc in loader.lazy_load():
    chunks = splitter.split_documents([doc])
    all_chunks.extend(chunks)
    # Có thể embed và store ngay, rồi discard khỏi memory
12

Đo lường và kiểm tra chất lượng chunk

Không nên chọn chunk_size rồi dùng mà không kiểm tra. Có ba cách đánh giá nhanh:

Phân bố kích thước chunk

import statistics

sizes = [len(c.page_content) for c in all_chunks]

print(f"Count : {len(sizes)}")
print(f"Min   : {min(sizes)}")
print(f"Max   : {max(sizes)}")
print(f"Mean  : {statistics.mean(sizes):.0f}")
print(f"Median: {statistics.median(sizes):.0f}")
print(f"Stdev : {statistics.stdev(sizes):.0f}")

# Đếm chunk quá nhỏ (< 100 chars):
too_small = sum(1 for s in sizes if s < 100)
print(f"Chunk < 100 chars: {too_small} ({100 * too_small / len(sizes):.1f}%)")

Nếu có nhiều chunk quá nhỏ (dưới 100 char), thường là do document có nhiều dòng trống, tiêu đề đứng một mình, hoặc bảng bị split lẻ. Cần xem xét preprocess document trước khi split.

Manual inspection

import random

sample = random.sample(all_chunks, min(20, len(all_chunks)))
for i, chunk in enumerate(sample):
    print(f"--- Chunk {i} ---")
    print(f"Metadata: {chunk.metadata}")
    print(chunk.page_content)
    print()

Kiểm tra thủ công 20–30 chunk ngẫu nhiên để phát hiện: chunk bị cắt giữa câu, chunk chỉ là header mà không có nội dung, bảng bị vỡ, code bị chia giữa function.

RAG retrieval eval

Cách chính xác nhất: tạo tập câu hỏi-đáp từ document, embed chunks với cấu hình khác nhau, đo recall@k (tỉ lệ câu hỏi có chunk đúng trong top-k kết quả). Xem chi tiết ở bài 58 (Series 4 — RAG evaluation).

13

Common pitfalls

Dùng CharacterTextSplitter với separator không có trong document

Kết quả: toàn bộ document thành 1 chunk, vượt token limit embedding model. Fix: dùng RecursiveCharacterTextSplitter.

chunk_size tính theo character nhưng model limit tính theo token

Ví dụ: chunk_size=2000 (character). Tiếng Việt với tiktoken khoảng 1 token/2 char → chunk ~1000 token. Với text-embedding-3-small (max 8192 token) không sao, nhưng với all-MiniLM-L6-v2 (max 256 token) thì bị cắt. Fix: dùng TokenTextSplitter hoặc SentenceTransformersTokenTextSplitter.

Quên chunk_overlap

Câu "A liên quan đến B" vắt qua biên giữa chunk N và N+1. Không có overlap → chunk N không có "B", chunk N+1 không có "A". Query hỏi về quan hệ A-B không retrieve được. Fix: overlap = 10–20% chunk_size.

Markdown table bị split

RecursiveCharacterTextSplitter không hiểu cấu trúc Markdown table — có thể split ngang giữa bảng. Fix: dùng MarkdownHeaderTextSplitter trước, sau đó xử lý table riêng (ví dụ chuyển table thành JSON/CSV trước khi split), hoặc set chunk_size đủ lớn để mỗi table vừa trong 1 chunk.

Document quá ngắn bị split thành chunk rỗng

Document 50 char với chunk_size=500 → 1 chunk bình thường. Nhưng nếu dùng MarkdownHeaderTextSplitter trên document không có header → trả về 1 Document với metadata trống. Kiểm tra len(chunk.page_content) == 0 trước khi embed:

chunks = [c for c in raw_chunks if c.page_content.strip()]

Dùng code splitter sai ngôn ngữ

Language.PYTHON separators bao gồm "\nclass ""\ndef ". Nếu dùng cho TypeScript, các pattern này sẽ không match — kết quả tệ như dùng text splitter thông thường. Luôn dùng đúng Language.* enum.

SemanticChunker với document cực ngắn

Nếu document có 3 câu, SemanticChunker vẫn embed 3 câu nhưng kết quả không split có nghĩa. Không cần SemanticChunker cho document ngắn.

14

Tóm tắt

Splitter Dùng khi nào Hạn chế
RecursiveCharacterTextSplitter Default cho hầu hết text Đếm character, không token
CharacterTextSplitter Document có separator cụ thể nhất quán Không fallback nếu thiếu separator
TokenTextSplitter Cần chunk_size chính xác theo token OpenAI Chỉ hỗ trợ tiktoken encoding
SentenceTransformersTokenTextSplitter Cần khớp với BERT-based embedding model Download tokenizer lần đầu
MarkdownHeaderTextSplitter Tài liệu Markdown có cấu trúc header Không giới hạn chunk size tự động
from_language(Language.X) Source code Phải chọn đúng Language enum
SemanticChunker Document nhiều chủ đề, không cấu trúc rõ Chậm, tốn API call embed

Quy trình đề xuất:

  1. Bắt đầu với RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
  2. Kiểm tra phân bố kích thước chunk
  3. Sample 20–30 chunk xem có bị cắt sai không
  4. Nếu document có cấu trúc rõ (Markdown, HTML, code) → chuyển sang splitter chuyên dụng
  5. Nếu cần chính xác theo token → chuyển sang token-based splitter
  6. Đo recall trên eval set nếu cần tối ưu tiếp
15

Bài tiếp theo

Bài 23: Retrievers — abstract layer trên vector DB — Sau khi đã có chunks và embed vào vector DB, bài tiếp theo giải thích Retriever interface trong LangChain: VectorStoreRetriever, MultiQueryRetriever, ContextualCompressionRetriever, và cách kết nối vào LCEL chain.