Để xây dựng một hệ thống RAG hoạt động tốt ở quy mô lớn, việc chỉ cắt văn bản thô sơ theo số lượng ký tự là hoàn toàn chưa đủ. Chất lượng của dữ liệu tìm được phụ thuộc trực tiếp vào hai yếu tố cốt lõi: Chiến thuật phân mảnh thông tin (Chunking) và Khả năng lập chỉ mục (Indexing) tối ưu của cơ sở dữ liệu Vector.

Bài học này sẽ đi sâu phân tích so sánh 3 chiến thuật Chunking từ cơ bản đến nâng cao, mổ xẻ cấu trúc tìm kiếm đồ thị HNSW (Hierarchical Navigable Small World) bên trong Vector Database để đạt tốc độ tìm kiếm mili-giây, và làm chủ thuật toán tìm kiếm lai Hybrid Search kết hợp chuẩn hóa thứ hạng RRF (Reciprocal Rank Fusion).

15.1 Các kiểu cắt đoạn văn bản (Chunking Strategies)

Việc chọn lựa chiến thuật cắt văn bản quyết định trực tiếp tới tỷ lệ tìm thấy thông tin chính xác (Recall rate) và độ loãng ngữ cảnh. Nếu chunk quá lớn, LLM sẽ bị loãng thông tin. Nếu chunk quá nhỏ, ngữ cảnh bổ trợ sẽ bị đứt gãy.

Ba chiến thuật Chunking phổ biến nhất hiện nay:

  1. Fixed-size Chunking (Cắt theo kích thước cố định): Cắt văn bản cơ học đúng $N$ ký tự hoặc từ. Phương pháp này siêu nhanh nhưng thường xuyên chẻ đôi câu, cắt ngang các số liệu quan trọng hoặc tên riêng, làm hỏng hoàn toàn ngữ nghĩa của đoạn.
  2. Recursive Character Chunking (Cắt đệ quy thông minh): Chia nhỏ văn bản dựa trên danh sách các ký tự phân tách có thứ tự ưu tiên giảm dần: Ký tự xuống dòng kép \n\n (đoạn văn), ký tự xuống dòng đơn \n (dòng), dấu chấm câu . (câu), dấu phẩy ,, và cuối cùng là khoảng trắng. Nó cố gắng giữ các câu văn trọn vẹn nhất có thể trong giới hạn kích thước chunk.
  3. Semantic Chunking (Cắt theo ngữ nghĩa): Phương pháp cao cấp hơn. Ta quét qua văn bản theo từng câu, tính toán vector embedding cho từng câu và đo độ tương đồng ngữ nghĩa giữa các câu liên tiếp. Một ranh giới chunk mới sẽ tự động được tạo ra ngay tại vị trí mà độ tương đồng ngữ nghĩa giữa câu hiện tại và câu kế tiếp đột ngột giảm mạnh xuống dưới một ngưỡng (Threshold) định sẵn.
⚠️ Cạm bẫy: Chia cắt dữ liệu dạng bảng biểu hoặc số liệu
Khi cắt tài liệu có chứa bảng biểu dữ liệu (Table), việc sử dụng Fixed hoặc Recursive Chunking sẽ chẻ các hàng trong bảng thành các dòng độc lập rời rạc, làm mất đi hoàn toàn tiêu đề cột (Header Context). Khi tìm kiếm vector, một hàng số liệu lẻ loi sẽ mất đi ý nghĩa ngữ nghĩa của nó. Giải pháp: Sử dụng Markdown Table Parser chuyên dụng để chuyển đổi bảng biểu sang dạng JSON trước khi thực hiện chunking.

15.2 Cấu trúc bên trong của Vector Database: HNSW Indexing

Nếu kho tài liệu có 1 triệu chunks, việc tính toán Cosine Similarity tuyến tính (Exact Search) giữa câu hỏi và từng chunk sẽ mất $O(N)$ thời gian, làm CPU bị nghẽn hoàn toàn. Vì vậy, các Vector Database (như ChromaDB, Pinecone, Qdrant) sử dụng cơ chế tìm kiếm láng giềng gần nhất xấp xỉ Approximate Nearest Neighbors (ANN) với thuật toán đồ thị phân tầng HNSW (Hierarchical Navigable Small World).

HNSW hoạt động dựa trên cấu trúc đồ thị đa tầng, tương tự như danh sách liên kết bỏ qua (Skip Lists):

  • Tầng trên cùng (Tầng thưa): Chứa rất ít nút vector phân tán xa nhau. Khi tìm kiếm, thuật toán thực hiện các bước nhảy lớn để nhanh chóng định vị vùng lân cận tổng quan của vector câu hỏi.
  • Các tầng dưới (Tầng dày dần): Mật độ các nút vector dày hơn. Thuật toán đi sâu xuống các tầng dưới để tinh chỉnh đường đi, thực hiện các bước nhảy ngắn hơn nhằm tìm kiếm chính xác các láng giềng gần nhất.
⚡ Trade-off của HNSW: Tốc độ vs Bộ nhớ RAM
HNSW giúp đưa độ phức tạp tìm kiếm từ tuyến tính $O(N)$ xuống logarit $O(\log N)$ chỉ trong vài mili-giây. Tuy nhiên, nó đòi hỏi chi phí RAM cực kỳ lớn để lưu trữ cấu trúc các đường liên kết đồ thị đa tầng trong bộ nhớ. Ngoài ra, thời gian xây dựng chỉ mục (indexing time) ban đầu cũng lâu hơn. Do là thuật toán xấp xỉ (ANN), HNSW không đảm bảo 100% tìm thấy vector gần nhất tuyệt đối (đánh đổi một lượng nhỏ Recall để lấy tốc độ cực nhanh).
🔢 Con số thật: $O(N)$ so với $O(\log N)$ nhanh hơn bao nhiêu lần?
Với kho tài liệu có $N = 1.000.000$ vector (một con số rất bình thường ở quy mô doanh nghiệp), quét tuyến tính (Exact Search) cần khoảng $1.000.000$ bước so sánh. HNSW xấp xỉ chỉ cần khoảng $\log_2(1.000.000) \approx 19.93$ bước nhảy qua các tầng đồ thị (đã tính xác minh) — nhanh hơn xấp xỉ 50.000 lần. Đây chính là lý do các Vector Database production không bao giờ dùng Exact Search khi kho dữ liệu vượt quá vài chục nghìn vector.

15.3 Hybrid Search: Kết hợp ngữ nghĩa và từ khóa

Tìm kiếm ngữ nghĩa bằng Dense Embeddings rất giỏi nắm bắt đại ý câu hỏi nhưng lại thường xuyên thất bại khi người dùng tìm kiếm chính xác tên riêng, mã số sản phẩm viết tắt hoặc mã lỗi phần cứng chuyên biệt (ví dụ: lỗi "ERR_403_INVALID").

Giải pháp là Hybrid Search — chạy song song hai công cụ tìm kiếm độc lập:

  1. Dense Retrieval (Tìm ngữ nghĩa): Dùng mô hình Embeddings để đo khoảng cách góc Cosine Similarity.
  2. Sparse Retrieval (Tìm từ khóa): Dùng thuật toán BM25 (phát triển từ TF-IDF) để tính tần suất từ khóa chính xác trên đĩa cứng.
📐 Thuật toán chuẩn hóa kết hợp RRF (Reciprocal Rank Fusion)
Ta không thể cộng điểm số BM25 và Cosine Similarity lại với nhau vì thang điểm của chúng hoàn toàn khác nhau. Thay vào đó, ta sử dụng RRF (Reciprocal Rank Fusion) để xếp hạng lại tài liệu dựa trên thứ hạng vị trí của nó trong từng danh sách kết quả: $$RRF\_Score(d) = \sum_{m \in M} \frac{1}{k + r_m(d)}$$ Trong đó:
  • $M$ là tập hợp các phương pháp tìm kiếm (Dense và Sparse).
  • $r_m(d)$ là thứ hạng vị trí của tài liệu $d$ trong danh sách kết quả của phương pháp tìm kiếm $m$ (1-indexed).
  • $k$ là hằng số làm mượt (Smoothing constant), thường được đặt mặc định là $60$ để giảm thiểu tác động của những tài liệu có thứ hạng quá thấp.
Tài liệu nào có tổng điểm RRF cao nhất sẽ được chọn làm ngữ cảnh tối ưu cho LLM.

15.4 Dự án thực hành bài 15: Chương trình so sánh hiệu quả của các chiến thuật Chunking

Dự án thực hành của bài học này là viết chương trình Python chunking_evaluation.py thiết lập một In-Memory Vector Database hỗ trợ tìm kiếm ngữ nghĩa và từ khóa.

Chúng ta sẽ nạp vào một đoạn văn bản dài có cấu trúc phức tạp, sau đó thực hiện cắt nhỏ bằng 3 phương pháp: Fixed-size Chunking, Recursive Character-based Chunking, và Semantic Chunking (giả lập thông minh dựa trên độ tương đồng câu). Tiếp theo, ta đo lường độ tương đồng ngữ nghĩa (Retrieval Similarity) của đoạn tài liệu truy xuất được đối với câu hỏi kiểm thử để trực quan hóa sự khác biệt.

chunking_evaluation.py
import re
import math
 
# Văn bản thử nghiệm chứa thông tin điều khoản bảo mật nội bộ
DOCUMENT = """
Hợp đồng bảo mật thông tin (NDA) của JS-Tools quy định rõ:
[Điều 1] Mọi tài liệu thiết kế hệ thống và mã nguồn dự án đều được phân loại là Mật.
Nhân viên không được chia sẻ thông tin này ra ngoài dưới bất kỳ hình thức nào.
[Điều 2] Mức phạt vi phạm hành chính đối với trường hợp rò rỉ dữ liệu khách hàng lên tới 500,000,000 VND.
Hành vi vi phạm nghiêm trọng có thể dẫn đến việc chấm dứt hợp đồng lao động lập tức mà không bồi thường.
[Điều 3] Thời hạn hiệu lực của thỏa thuận bảo mật kéo dài 5 năm kể từ ngày chấm dứt hợp đồng làm việc tại công ty.
Mọi tranh chấp sẽ được giải quyết tại Tòa án Nhân dân Thành phố Hồ Chí Minh.
"""
 
# Bộ tách từ và tính toán Cosine Similarity thô
def tokenize(text):
    return re.findall(r'\b\w+\b', text.lower())
 
def cosine_similarity(text1, text2):
    tokens1 = tokenize(text1)
    tokens2 = tokenize(text2)
    
    vocab = list(set(tokens1 + tokens2))
    v1_dict = {}
    v2_dict = {}
    
    for t in tokens1: v1_dict[t] = v1_dict.get(t, 0) + 1
    for t in tokens2: v2_dict[t] = v2_dict.get(t, 0) + 1
    
    v1 = [v1_dict.get(word, 0) for word in vocab]
    v2 = [v2_dict.get(word, 0) for word in vocab]
    
    dot_product = sum(a * b for a, b in zip(v1, v2))
    norm1 = math.sqrt(sum(a * a for a in v1))
    norm2 = math.sqrt(sum(b * b for b in v2))
    
    if norm1 == 0 or norm2 == 0: return 0.0
    return dot_product / (norm1 * norm2)
 
# 1. Chiến thuật Fixed-size Chunking (Cắt cứng theo số lượng ký tự)
def fixed_size_chunk(text, chunk_size=120):
    chunks = []
    for i in range(0, len(text), chunk_size):
        chunks.append(text[i:i+chunk_size].strip())
    return chunks
 
# 2. Chiến thuật Recursive Character-based Chunking (Cắt thông minh theo dấu chấm câu)
def recursive_character_chunk(text, chunk_size=120):
    # Chia theo dấu chấm câu trước để giữ trọn câu
    sentences = re.split(r'(?<=[.\n])\s+', text.strip())
    chunks = []
    current_chunk = ""
    
    for sentence in sentences:
        if not sentence: continue
        # Nếu cộng dồn vượt quá size thì đẩy chunk cũ đi
        if len(current_chunk) + len(sentence) > chunk_size and current_chunk:
            chunks.append(current_chunk.strip())
            current_chunk = sentence
        else:
            current_chunk += " " + sentence
            
    if current_chunk:
        chunks.append(current_chunk.strip())
    return chunks
 
# 3. Chiến thuật Semantic Chunking (Giả lập cắt dựa theo độ tương đồng câu)
def semantic_chunk(text, similarity_threshold=0.25):
    sentences = [s.strip() for s in re.split(r'(?<=[.\n])\s+', text.strip()) if s.strip()]
    chunks = []
    
    if not sentences: return chunks
    
    current_chunk = sentences[0]
    for i in range(1, len(sentences)):
        # Đo độ tương đồng ngữ nghĩa giữa câu hiện tại và câu tiếp theo
        sim = cosine_similarity(sentences[i-1], sentences[i])
        
        # Nếu độ tương đồng giảm sâu dưới ngưỡng -> Cắt chunk mới
        if sim < similarity_threshold:
            chunks.append(current_chunk)
            current_chunk = sentences[i]
        else:
            current_chunk += " " + sentences[i]
            
    chunks.append(current_chunk)
    return chunks
 
# Bộ đánh giá tìm kiếm thử nghiệm
def evaluate_retrieval(query, chunks, chunking_name):
    print(f"\n--- Đánh giá kết quả của chiến thuật: {chunking_name} ---")
    print(f"Tổng số chunks tạo ra: {len(chunks)}")
    
    best_similarity = -1.0
    best_chunk = ""
    
    # Tìm kiếm đoạn văn có Cosine Similarity cao nhất đối với câu hỏi
    for idx, chunk in enumerate(chunks):
        sim = cosine_similarity(query, chunk)
        print(f"  * Chunk [{idx}] ({len(chunk)} ký tự): \"{chunk[:60]}...\" -> Độ khớp: {sim:.4f}")
        if sim > best_similarity:
            best_similarity = sim
            best_chunk = chunk
            
    print(f"🏆 Kết quả khớp nhất: \"{best_chunk}\" (Độ khớp: {best_similarity:.4f})")
 
if __name__ == "__main__":
    query_test = "Mức phạt tiền vi phạm rò rỉ dữ liệu là bao nhiêu?"
    
    print("=== ĐÁNH GIÁ CHẤT LƯỢNG CHUNKING TRÊN TÀI LIỆU NDA ===")
    print(f"Câu hỏi kiểm thử: '{query_test}'")
    
    # Thực hiện cắt
    chunks_fixed = fixed_size_chunk(DOCUMENT, chunk_size=120)
    chunks_recursive = recursive_character_chunk(DOCUMENT, chunk_size=120)
    chunks_semantic = semantic_chunk(DOCUMENT, similarity_threshold=0.20)
    
    # Đánh giá so sánh trực quan
    evaluate_retrieval(query_test, chunks_fixed, "Fixed-size Chunking (120 ký tự)")
    evaluate_retrieval(query_test, chunks_recursive, "Recursive Character Chunking")
    evaluate_retrieval(query_test, chunks_semantic, "Semantic Chunking (Similarity Threshold 0.20)")
⚠️ Chạy thử thật phát hiện: trên văn bản NDA ngắn này, Semantic = Recursive Chunking
Chạy chính xác đoạn code trên (đã kiểm chứng bằng Python) cho thấy Recursive Character ChunkingSemantic Chunking tạo ra 6 chunks giống hệt nhau về nội dung và độ khớp, cùng chọn ra chunk "[Điều 2] Mức phạt vi phạm..." với độ khớp $0.4529$ — cao hơn hẳn Fixed-size Chunking (chỉ đạt $0.4108$ vì bị cắt cụt giữa câu). Điều này không có nghĩa Semantic Chunking "vô dụng": văn bản NDA mẫu này đã có ranh giới câu/đoạn rất rõ ràng (mỗi điều khoản là một đoạn hoàn chỉnh), nên Recursive Chunking dựa trên dấu câu vô tình trùng khớp hoàn toàn với ranh giới ngữ nghĩa thật. Lợi thế thực sự của Semantic Chunking chỉ bộc lộ rõ trên các văn bản có cấu trúc lộn xộn hơn — ví dụ transcript hội thoại, ghi chú không dấu câu chuẩn, hoặc văn bản dịch máy — nơi các ký tự phân tách cơ học (dấu chấm, xuống dòng) không còn phản ánh đúng ranh giới chủ đề nữa.

Tóm tắt bài học & Cầu nối kiến thức

🔑 Bài học đạt được:
  • Đạt được: Làm chủ các kỹ thuật phân đoạn nâng cao như Recursive Character và Semantic Chunking dựa trên mật độ ngữ nghĩa.
  • Đạt được: Hiểu cơ chế chỉ mục đồ thị HNSW giúp tìm kiếm hàng triệu vector trong mili-giây.

Cầu nối bài tiếp theo: Khi câu hỏi của người dùng mơ hồ hoặc tài liệu dài bị loãng, RAG cơ bản sẽ tìm sai tài liệu. Chúng ta cần các kỹ thuật tối ưu Query Rewrite và Reranking ở Bài số 16.

Tải file code thực hành minh họa bài học

File Python chunking_evaluation.py — mã nguồn triển khai các bộ phân mảnh văn bản Fixed, Recursive, Semantic và bộ máy tìm kiếm vector so sánh Recall (chạy python chunking_evaluation.py):

Tải về chunking_evaluation.py

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 14: Hệ thống RAG Cơ Bản: Hỏi đáp tài liệu Bài 16: RAG Nâng Cao: Query Rewrite & Rerank Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận