Điểm yếu chí mạng của RAG tiêu chuẩn
Hầu hết các pipeline RAG bắt đầu với một giả định đơn giản: chia văn bản thành các đoạn (chunk) 500 ký tự và hy vọng mọi thứ sẽ ổn. Cách này hiệu quả với các bài blog cơ bản. Tuy nhiên, ngay khi bạn đưa vào một báo cáo tài chính 10-K với các bảng lồng nhau hoặc tài liệu kỹ thuật đầy sơ đồ mạch điện, hệ thống sẽ gặp trục trặc.
Phương pháp chunking tiêu chuẩn thường cắt đôi một bảng ngay giữa, tách rời một hàng khỏi tiêu đề của nó. Khi retriever lấy ra hàng bị cô lập đó, LLM chỉ thấy một chuỗi các con số ngẫu nhiên mà không có ngữ cảnh. Nó không chỉ thất bại mà còn đưa ra các phản hồi sai lệch (hallucination) một cách đầy tự tin.
Tôi đã dành nhiều tháng để tìm hiểu lý do tại sao một hệ thống RAG thực tế lại thất bại trong các truy vấn PDF cơ bản. Thủ phạm không phải là khả năng suy luận của LLM mà là chiến lược truy xuất (retrieval strategy). Bằng cách chuyển sang phương pháp Multi-vector, cụ thể là chiến lược Parent Document, chúng tôi đã tăng độ chính xác truy xuất trên dữ liệu dạng bảng từ mức 55% không ổn định lên hơn 92%. Kể từ đó, tôi đã triển khai kiến trúc này cho nhiều dự án doanh nghiệp yêu cầu độ chính xác dữ liệu tuyệt đối.
So sánh các chiến lược truy xuất
Trước khi viết code, bạn cần hiểu tại sao cách tiếp cận “Naive” lại thất bại trong khi Multi-vector lại thành công. Điều này nằm ở cách chúng ta cân bằng giữa khả năng tìm kiếm và ngữ cảnh.
Cách tiếp cận Naive (RAG tiêu chuẩn)
Trong một thiết lập điển hình, bạn chia tài liệu thành các chunk nhỏ có kích thước bằng nhau, tạo embedding và lưu trữ chúng. Khi có truy vấn, hệ thống sẽ tìm các chunk top-K. Các chunk nhỏ rất tốt để xác định chính xác từ khóa nhưng lại thiếu ngữ cảnh xung quanh. Ngược lại, các chunk lớn chứa quá nhiều “nhiễu”, làm loãng vector embedding và khiến việc tìm kiếm kém chính xác hơn. Bạn bị mắc kẹt trong sự đánh đổi: hoặc có độ chính xác mà không có ngữ cảnh, hoặc có ngữ cảnh mà không có độ chính xác.
Chiến lược Multi-vector (Parent-Child)
Multi-vector Retriever tách biệt dữ liệu chúng ta tìm kiếm khỏi dữ liệu chúng ta gửi cho LLM. Chúng ta đánh chỉ mục (index) các “child chunk” nhỏ hoặc các bản tóm tắt ngắn gọn cho quá trình tìm kiếm.
Chúng được liên kết với một tài liệu “parent” lớn hơn trong một kho lưu trữ riêng biệt. Khi một child chunk khớp với truy vấn, retriever sẽ lấy toàn bộ tài liệu parent—có thể là cả một chương 2.000 từ hoặc một bảng đầy đủ 15 hàng—và chuyển nó cho LLM. Bạn sẽ có được sự chính xác cực cao của việc tìm kiếm chunk nhỏ kết hợp với ngữ cảnh phong phú của một tài liệu đầy đủ, điều này rất hữu ích khi Xây dựng Private RAG cho doanh nghiệp.
Sự đánh đổi: Có xứng đáng với công sức bỏ ra không?
Phương pháp này rất mạnh mẽ, nhưng không có gì là miễn phí. Bạn cần cân nhắc giữa hiệu quả đạt được và chi phí hạ tầng.
Lợi ích
- Tính toàn vẹn của tài liệu: Các bảng được giữ nguyên vẹn. LLM có thể thấy tiêu đề, đơn vị và chú thích cùng lúc.
- Sự rõ ràng về ngữ nghĩa: Bằng cách đánh chỉ mục các bản tóm tắt của hình ảnh hoặc bảng, bạn giúp dữ liệu phi văn bản có thể tìm kiếm được.
- Tỷ lệ tín hiệu trên nhiễu cao hơn: Tìm kiếm trên một bản tóm tắt 100 từ thường chính xác hơn so với tìm kiếm trên một khối văn bản thô 1.000 từ.
Chi phí
- Nhu cầu lưu trữ: Về cơ bản, bạn đang lưu trữ dữ liệu hai lần—một lần dưới dạng embedding và một lần dưới dạng văn bản/hình ảnh thô.
- Độ trễ nạp dữ liệu (Ingestion): Việc phân tích một tệp PDF 100 trang bằng các công cụ độ phân giải cao có thể mất vài phút thay vì vài giây.
- Chi phí API: Việc tạo tóm tắt cho 50 bảng trong một báo cáo yêu cầu thêm 50 lượt gọi LLM trong giai đoạn tải lên.
Tech Stack hiện đại
Để xây dựng hệ thống này, bạn cần những công cụ có thể thực sự “nhìn” thấy cấu trúc tài liệu thay vì chỉ đọc các chuỗi ký tự, đảm bảo nguyên tắc Dữ liệu Sạch, RAG Tốt hơn.
- Unstructured.io: Tiêu chuẩn vàng để phân tách PDF thành các thành phần riêng biệt như bảng và văn bản tự sự.
- LangChain: Cung cấp lớp
MultiVectorRetrieverđể quản lý liên kết giữa bản tóm tắt và dữ liệu thô. - ChromaDB hoặc Pinecone: Kho lưu trữ hiệu suất cao cho các vector của bạn.
- Redis: Một lựa chọn tuyệt vời cho
DocStoređể giữ các tài liệu parent sẵn sàng cho việc truy xuất dưới một mili giây. - GPT-4o hoặc Claude 3.5 Sonnet: Những mô hình này vượt trội trong việc tóm tắt các bảng phức tạp thành các mô tả có thể tìm kiếm được.
Triển khai từng bước
Hãy cùng xây dựng một pipeline xử lý báo cáo tài chính chứa cả văn bản và các bảng phức tạp.
1. Trích xuất các thành phần cấu trúc
Chúng ta sử dụng unstructured để xác định ranh giới của bảng. Điều này ngăn chặn vấn đề “bảng bị cắt đôi” ngay từ đầu.
from unstructured.partition.pdf import partition_pdf
# Trích xuất các thành phần với tính năng phát hiện bảng độ chính xác cao
raw_pdf_elements = partition_pdf(
filename="q3_report.pdf",
extract_images_in_pdf=False,
infer_table_structure=True, # "Công thức bí mật" dành cho bảng
chunking_strategy="by_title",
max_characters=4000,
)
# Tách riêng các bảng để xử lý chuyên biệt
tables = [el for el in raw_pdf_elements if el.category == "Table"]
texts = [el for el in raw_pdf_elements if el.category == "CompositeElement"]
2. Tạo các bản tóm tắt có thể tìm kiếm được
Các bảng HTML hoặc Markdown thô thường quá dày đặc đối với tìm kiếm vector. Việc tóm tắt hoặc trích xuất dữ liệu có cấu trúc từ chúng tạo ra một “cầu nối có thể tìm kiếm được”.
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o", temperature=0)
table_summaries = []
for table in tables:
prompt = f"Tóm tắt bảng này để tìm kiếm ngữ nghĩa. Bao gồm các chỉ số chính và tiêu đề hàng/cột: {table.text}"
summary = model.invoke(prompt)
table_summaries.append(summary.content)
3. Thiết lập Multi-vector Retriever
Đây là nơi chúng ta liên kết các bản tóm tắt có thể tìm kiếm trong vector store với dữ liệu thô ban đầu trong document store.
import uuid
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryStore
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
# Vector store cho các bản tóm tắt
vectorstore = Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings())
store = InMemoryStore()
id_key = "doc_id"
retriever = MultiVectorRetriever(
vectorstore=vectorstore,
docstore=store,
id_key=id_key,
)
def add_data(summaries, raw_contents):
doc_ids = [str(uuid.uuid4()) for _ in summaries]
# Thêm các bản tóm tắt vào tìm kiếm vector
summary_docs = [Document(page_content=s, metadata={id_key: doc_ids[i]}) for i, s in enumerate(summaries)]
retriever.vectorstore.add_documents(summary_docs)
# Thêm dữ liệu thô vào kho lưu trữ parent
retriever.docstore.mset(list(zip(doc_ids, raw_contents)))
add_data(table_summaries, [t.text for t in tables])
add_data([t.text[:1000] for t in texts], [t.text for t in texts])
4. Kết quả
Khi người dùng hỏi: “Tăng trưởng doanh thu quý 3 của chúng ta là bao nhiêu?”, hệ thống sẽ tìm thấy bản tóm tắt của bảng doanh thu. Sau đó, nó truy xuất toàn bộ bảng thô cho LLM. Giờ đây, mô hình có mọi con số cần thiết để tính toán câu trả lời một cách chính xác.
Mở rộng cho hình ảnh và biểu đồ
Bạn có thể áp dụng mô hình tương tự này cho dữ liệu hình ảnh. Sử dụng một mô hình đa phương thức (multimodal) như GPT-4o để viết mô tả khoảng 200 từ cho một biểu đồ đường. Đánh chỉ mục mô tả đó. Liên kết nó với hình ảnh gốc. Khi người dùng hỏi về các xu hướng, phần mô tả sẽ kích hoạt việc truy xuất và LLM sẽ nhận được hình ảnh thực tế để phân tích. Đây là một cách mạnh mẽ để xử lý dữ liệu mà các mô hình chỉ hỗ trợ văn bản không thể “thấy” được.
Lời kết
Vượt xa khỏi RAG cơ bản là điều cần thiết cho bất kỳ ứng dụng doanh nghiệp nghiêm túc nào, đặc biệt khi cần xây dựng AI Research Agent thời gian thực. Chunking tiêu chuẩn phù hợp cho các bản thử nghiệm (prototype), nhưng nó sẽ thất bại ngay khi gặp các bố cục tài liệu trong thực tế. Mặc dù kiến trúc Multi-vector yêu cầu nhiều khâu điều phối hơn và chi phí nạp dữ liệu cao hơn, nhưng thành quả đạt được là một hệ thống mà người dùng thực sự có thể tin tưởng. Nếu tệp PDF của bạn chứa nhiều thứ hơn là chỉ các đoạn văn bản thuần túy, thì đây chính là kiến trúc mà bạn nên xây dựng ngay hôm nay.

