FDE PulseViệc làm FDE đang mở 316Mới đăng 7 ngày qua 10Chủ đề nổi bật: Đào tạo kỹ năng FDE tại Đông Nam Á

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

RAG trên tài liệu thật của khách: xử lý PDF, bảng biểu và bản scan tiếng Việt

Demo RAG chạy mượt trên file mẫu sẽ vỡ ngay khi gặp ổ đĩa của khách, và chỗ vỡ thường không nằm ở model mà ở bước đọc tài liệu.

Đồ hoạPipeline RAG cho tài liệu thật của khách
  1. 1Phân loại fileChia thành PDF có text, PDF nhiều bảng và bản scan, mỗi nhóm một nhánh xử lý
  2. 2Đọc theo nhánh của từng nhómBảng: Unstructured hoặc Docling. Scan: OCR tiếng Việt, đếm lỗi dấu, hoặc ColPali
  3. 3Thêm ngữ cảnh cho chunkGắn 50-100 token mô tả chunk thuộc tài liệu nào, phần nào
  4. 4Truy xuất hybridIndex chunk có ngữ cảnh vào cả embedding lẫn BM25, sau đó rerank
  5. 5Đo bằng câu hỏi thậtĐo tỷ lệ chunk đúng lọt top-20 sau mỗi lần đổi một biến

Lỗi thường bắt đầu từ hai bước đầu, nên hãy kiểm tra chúng trước khi chỉnh truy xuất.

Đồ hoạ: FDE Times

Tóm tắt nhanh

  • Lỗi RAG trên tài liệu thật thường bắt đầu từ khâu parse và OCR, trước cả embedding.
  • Bảng phải được tách riêng và chunk theo tiêu đề. Bản scan tiếng Việt cần một bước kiểm tra dấu thanh.
  • Thêm ngữ cảnh vào chunk, ghép BM25 với embedding và rerank. Anthropic tự đo được mức giảm truy xuất thất bại lên tới 67%.
Chia sẻLinkedInFacebookX

Thử hình dung tuần đầu ở khách hàng. Bạn được cấp quyền vào một thư mục chung với vài trăm file: hợp đồng PDF xuất từ Word, báo cáo quý chi chít bảng, và một chồng hóa đơn scan nghiêng, có con dấu đỏ đè lên chữ.

Bản demo RAG chạy rất mượt trên file mẫu, nhưng đến đây nó trả lời sai ngay câu đầu tiên: “Hạn thanh toán của hợp đồng số 0457 là ngày nào?”

Phản xạ đầu tiên của nhiều kỹ sư là đổi model embedding hoặc tăng top-k. Nhưng chỗ hỏng thường không nằm ở đó. Câu trả lời không có trong top-20 vì hệ thống chưa bao giờ đọc đúng trang đó: bảng bị cắt làm đôi, dấu thanh bị OCR làm rơi, còn mã “0457” bị embedding coi như nhiễu.

Vì thế, cách làm đúng là lần theo dữ liệu từ file thô đến kết quả truy xuất, từng khâu một. Với một FDE, kỹ năng đáng tiền nhất là chỉ ra lỗi nằm ở khâu nào trước khi bắt tay sửa.

Bước đầu tiên: phân loại file trước khi parse

Đừng đẩy cả thư mục qua một parser duy nhất. Hãy mở ngẫu nhiên vài chục file và chia chúng thành ba nhóm, vì mỗi nhóm hỏng theo một kiểu khác nhau và cần một nhánh xử lý riêng.

Loại tài liệu Rủi ro chính Cách xử lý gợi ý
PDF có lớp text (xuất từ Word) Mất cấu trúc tiêu đề, header/footer lặp lại Parser có phân tích layout, chunk theo tiêu đề
PDF nhiều bảng Hàng bảng bị cắt, cột bị trộn thành một dòng chữ Tách bảng riêng, giữ bảng cùng tiêu đề của nó
Bản scan, ảnh chụp OCR sai dấu thanh, nhiễu, con dấu OCR tiếng Việt chuyên biệt hoặc truy xuất thẳng từ ảnh trang

Cách kiểm tra rất đơn giản: thử bôi đen chữ trong file. Bôi được thì file có lớp text. Không bôi được thì đó là ảnh và bạn cần OCR.

Chỉ riêng bước này đã cho bạn một con số để báo với khách ngay trong tuần đầu, chẳng hạn bao nhiêu phần trăm kho tài liệu là scan. Con số đó quyết định phần lớn công sức phía sau.

Bảng biểu: đừng để parser cắt đôi một hàng

Một bảng giá hay bảng tiến độ thanh toán, khi bị đọc như văn bản thường, sẽ thành một chuỗi số không còn biết số nào thuộc cột nào. Tệ hơn, chunker cắt theo số token có thể đặt tiêu đề cột vào một chunk và các hàng dữ liệu vào chunk kế tiếp.

Tài liệu của Cohere về RAG trên dữ liệu hỗn hợp khuyến nghị Unstructured để parse PDF, vì công cụ này tách được bảng ra khỏi văn bản. Nó còn chunk bảng và văn bản theo tiêu đề ngay trong bước parse, để các phần tử liên quan nằm cùng nhóm.

Docling của IBM đi theo hướng tương tự: dùng các mô hình chuyên biệt để phân tích layout (huấn luyện trên DocLayNet) và nhận dạng cấu trúc bảng. Một đoạn code tối thiểu để bắt đầu:

from docling.document_converter import DocumentConverter

converter = DocumentConverter()
result = converter.convert("bao_cao_quy3.pdf")
markdown = result.document.export_to_markdown()
# Mở markdown ra đọc bằng mắt: bảng còn đủ hàng, đủ cột không?

Dòng comment cuối mới là phần quan trọng. Trước khi index, hãy mở ra đọc lại vài bảng khó nhất, như bảng có ô gộp hay bảng kéo dài qua hai trang. Nếu bảng đã vỡ ở đây thì không reranker nào cứu được.

Bản scan tiếng Việt: dấu thanh là chỗ vỡ đầu tiên

Tiếng Việt dùng chữ Latin nhưng có rất nhiều dấu để đánh dấu thanh điệu và phân biệt nguyên âm. Với OCR, mỗi dấu là một chi tiết nhỏ mà nhiễu, nét mờ hay con dấu đỏ đều có thể xóa mất.

Một bài khảo sát năm 2025 về nhận dạng tài liệu tiếng Việt nhận xét rằng Tesseract có hỗ trợ tiếng Việt, nhưng độ chính xác vẫn hạn chế vì khâu xử lý dấu và nhiễu tài liệu.

“Hạn thanh toán” thành “Han thanh toan” thì BM25 không còn khớp từ với câu hỏi của người dùng. Còn “bảo hành” bị đọc thành “bào hành” thì embedding có thể kéo về một chủ đề khác hẳn.

Vì vậy, hãy đo lỗi dấu trước khi index. Gõ tay lại một trang làm bản chuẩn, chạy Tesseract với lang="vie", rồi đếm những từ đúng mặt chữ nhưng sai dấu:

import difflib, unicodedata
import pytesseract
from PIL import Image

def bo_dau(s):
    s = s.replace("đ", "d").replace("Đ", "D")
    return "".join(c for c in unicodedata.normalize("NFD", s)
                   if unicodedata.category(c) != "Mn")

ocr = pytesseract.image_to_string(Image.open("hoa_don_01.png"), lang="vie")
goc = open("hoa_don_01_goc.txt", encoding="utf-8").read()  # bản gõ tay

ocr_w = unicodedata.normalize("NFC", ocr).split()
goc_w = unicodedata.normalize("NFC", goc).split()

sai_dau = 0
sm = difflib.SequenceMatcher(a=goc_w, b=ocr_w, autojunk=False)
for tag, i1, i2, j1, j2 in sm.get_opcodes():
    if tag == "replace":
        for g, o in zip(goc_w[i1:i2], ocr_w[j1:j2]):
            if bo_dau(g) == bo_dau(o):  # cùng chữ, khác dấu
                sai_dau += 1

print(f"Từ sai dấu: {sai_dau}/{len(goc_w)}")

Bước chuẩn hóa NFC rất quan trọng. Cùng một chữ “ệ” có thể được lưu bằng hai chuỗi Unicode khác nhau, và nếu không chuẩn hóa, bạn sẽ đếm nhầm cả những từ thực ra đúng.

Cùng bài khảo sát đó mô tả một pipeline điển hình từ cuộc thi MC-OCR 2021 về hóa đơn: CRAFT phát hiện vùng chữ, VietOCR nhận dạng chữ, sau đó trích xuất thông tin bằng luật. Bài học cho bạn là tách OCR thành hai khâu, phát hiện và nhận dạng, để biết lỗi nằm ở khâu nào.

Còn một hướng khác là bỏ hẳn OCR. ColPali dùng một Vision Language Model tạo multi-vector embedding thẳng từ ảnh trang tài liệu, tránh được bước trích xuất text vốn dễ vỡ. Với kho scan chất lượng kém, đáng thử ColPali song song với OCR rồi so kết quả trên cùng một bộ câu hỏi.

Chunk cần biết nó thuộc về đâu

Giả sử parse và OCR đã sạch. Vẫn còn một vấn đề: chunk “Bên B thanh toán trong vòng 30 ngày kể từ ngày nghiệm thu” không cho biết đây là hợp đồng nào, với khách nào.

Contextual Retrieval của Anthropic xử lý đúng chỗ này. Trước khi index, họ dùng model viết một đoạn ngữ cảnh ngắn, thường từ 50 đến 100 token, rồi gắn vào đầu mỗi chunk. Ví dụ: “Trích điều 5, hợp đồng số 0457 giữa công ty A và công ty B, phần điều khoản thanh toán.”

Đoạn ngữ cảnh này không chỉ đi vào embedding. Với Contextual BM25, chính chunk đã được gắn ngữ cảnh cũng được đưa vào index BM25, nên một câu hỏi chứa “0457” khớp được với chunk điều khoản dù bản thân điều khoản không hề nhắc mã đó.

Đó là lý do BM25 đáng ghép vào. Nó là hàm xếp hạng dựa trên khớp từ chính xác, nên bắt được đúng những thứ embedding hay bỏ qua: mã hợp đồng, số hiệu, tên riêng, vốn dày đặc trong tài liệu của khách.

Theo số liệu Anthropic tự đo, Contextual Embeddings giảm 35% tỷ lệ truy xuất thất bại ở top-20 chunk. Thêm Contextual BM25 thì mức giảm là 49%, và thêm reranking thì lên 67%. Thử quy ra một bộ test giả định có 100 lần truy xuất thất bại: sau từng bước, con số đó lần lượt còn 65, rồi 51, rồi 33.

Đừng hứa với khách những con số này. Chúng được đo trên dữ liệu của Anthropic, không phải trên kho hóa đơn của khách bạn. Thứ nên mang theo là phương pháp: đo tỷ lệ thất bại của chính kho tài liệu đó, sau mỗi lần thay đổi một thứ.

Tự làm lại theo thứ tự

  1. Thu 20 đến 30 câu hỏi thật từ người dùng của khách, mỗi câu kèm trang chứa đáp án. Bộ câu hỏi này là thước đo cho mọi quyết định phía sau.
  2. Phân loại file thành ba nhóm: PDF có text, PDF nhiều bảng, bản scan.

Chọn nhánh cho từng nhóm: parser hiểu layout (Unstructured, Docling) cho nhóm có bảng, OCR tiếng Việt hoặc ColPali cho nhóm scan. 4. Kiểm tra bằng mắt một mẫu nhỏ của mỗi nhóm, và chạy script đếm từ sai dấu cho nhóm scan. 5.

Chunk theo tiêu đề, rồi gắn đoạn ngữ cảnh vào đầu mỗi chunk. 6. Index chunk đã gắn ngữ cảnh vào cả embedding lẫn BM25, thêm reranker. 7. Đo lại tỷ lệ chunk đúng lọt top-20 trên bộ câu hỏi. Mỗi lần chỉ đổi một biến, nếu không bạn sẽ không biết cải thiện đến từ đâu.

Những lỗi hay gặp

Lỗi phổ biến nhất là đánh giá bằng câu trả lời cuối cùng mà không nhìn top-20 chunk. Khi câu trả lời sai, bạn cần biết đó là lỗi truy xuất hay lỗi sinh câu trả lời. Hai loại lỗi này sửa theo hai cách hoàn toàn khác nhau.

Lỗi thứ hai là bỏ dấu tiếng Việt ở cả index lẫn câu hỏi cho “đồng bộ”. Cách này làm mất phân biệt giữa những từ khác nghĩa. Nếu cần chuẩn hóa, hãy giữ song song một bản có dấu.

Lỗi thứ ba là chỉ test trên file đẹp. Bộ câu hỏi phải có câu nằm trong bảng gộp ô, câu nằm trên trang scan mờ và câu chứa mã số. Người dùng thật sẽ hỏi đúng những câu đó.

Khi đọc JD tuyển FDE, hãy để ý các cụm như “document ingestion”, “OCR”, “unstructured data”. Trong CV, một dòng kiểu “tăng tỷ lệ truy xuất đúng trên kho hóa đơn scan tiếng Việt từ X lên Y” có sức nặng hơn nhiều so với “xây dựng hệ thống RAG”.

Khách hàng không bao giờ đưa bạn dữ liệu sạch. Người làm được việc là người mở PDF ra đọc trước khi mở notebook.

5 nguồn
Đọc tiếp trên lộ trình · Chặng 3: AI ứng dụngStream LLM tới giao diện: SSE trước, WebSocket khi thật cầnPhần lớn tính năng chat với LLM chỉ cần server đẩy token xuống trình duyệt. Chọn WebSocket theo thói quen có thể khiến bạn phải gánh thêm những đánh đổi hạ tầng mà khách hàng không cần.