Đánh giá RAG của riêng bạn- Tại sao các Tốt nhất đã Thất bại với Chúng tôi

Đánh giá RAG của riêng bạn- Tại sao các Tốt nhất đã Thất bại với Chúng tôi

  • 25 min read
Đánh giá RAG của riêng bạn- Tại sao các Tốt nhất đã Thất bại với Chúng tôi
Đánh giá RAG của riêng bạn- Tại sao các Tốt nhất đã Thất bại với Chúng tôi

Đánh giá RAG của riêng bạn: Tại sao các thực tiễn tốt nhất lại thất bại với chúng tôi

Bởi Charles AZAM trên Hugging Face

Jimmy là một công ty kỹ thuật hạt nhân đang xây dựng Lò phản ứng mô-đun nhỏ (SMR) đầu tiên của Pháp. Các kỹ sư của chúng tôi cần tìm kiếm hàng nghìn tài liệu kỹ thuật phức tạp: các bài báo nghiên cứu hạt nhân, tài liệu quy định, các tệp PDF khoa học đa ngôn ngữ chứa đầy phương trình, sơ đồ và thuật ngữ chuyên ngành. Việc tìm kiếm thủ công không hiệu quả – chúng tôi cần một hệ thống RAG có thể thực sự truy xuất đúng tài liệu.

Khi chúng tôi xây dựng, chúng tôi đã làm những gì mọi đội kỹ thuật tốt làm: chúng tôi đã tuân theo các thực tiễn tốt nhất. Chunking nhận biết ngữ cảnh. Tìm kiếm lai. Tối ưu hóa kích thước chunk cẩn thận. Các mô hình từ bảng xếp hạng MTEB.

Chúng tôi đã kiểm tra 156 truy vấn trên ba ngôn ngữ. Gần như mọi “thực tiễn tốt nhất” đều sai với trường hợp sử dụng của chúng tôi.

Kết quả đã làm chúng tôi ngạc nhiên: chunking ngẫu nhiên vượt trội hơn chunking nhận biết ngữ cảnh (70,5% so với 63,8%). Kích thước chunk hầu như không quan trọng. Tìm kiếm lai thua tìm kiếm dày đặc (69,2% so với 63,5%). Mô hình nhúng chiến thắng? Thậm chí không có trong bảng xếp hạng MTEB.

Bài viết này chia sẻ toàn bộ hành trình của chúng tôi: phương pháp luận benchmark, các phát hiện hiệu suất đáng ngạc nhiên và những bài học rút ra từ việc triển khai RAG trong sản xuất. Từ việc lựa chọn giữa Mistral OCR và các giải pháp thay thế mã nguồn mở, đến việc khám phá tại sao AWS OpenSearch lại tốn kém 70 đô la mỗi ngày với kết quả tầm thường, đến tối ưu hóa cơ sở hạ tầng của chúng tôi – chúng tôi đang chia sẻ những gì thực sự hiệu quả (và những gì không).

Cách duy nhất để biết điều gì hiệu quả với bạn là đo lường. Đây là cách chúng tôi đã làm.

TL;DR (Quá dài; Không đọc)

Những phát hiện chính:

  • Nhúng AWS Titan V2 hoạt động tốt nhất trên tất cả các chỉ số (tỷ lệ hit 69,2% so với 57,7% Qwen 8B, 39,1% Mistral embed)
  • Kích thước chunk không thực sự quan trọng – không có sự khác biệt thống kê đáng kể giữa 2K và 40K ký tự cho việc truy xuất cấp tài liệu
  • Chunking ngẫu nhiên vượt trội hơn chiến lược nhận biết ngữ cảnh – đơn giản hơn thì tốt hơn (70,5% so với 63,8% tỷ lệ hit trung bình)
  • Mistral OCR là vô đối cho các tệp PDF khoa học phức tạp (tốn kém nhưng xứng đáng)
  • Không sử dụng AWS OpenSearch cho tìm kiếm vector – quá đắt đỏ ($70/ngày tối thiểu)
  • Qdrant rất tuyệt cho cơ sở dữ liệu vector – dễ sử dụng, hiệu suất ổn định, tìm kiếm lai, dễ tự host, dịch vụ được quản lý tuyệt vời
  • Tìm kiếm chỉ dày đặc đã đánh bại tìm kiếm lai trong các thử nghiệm của chúng tôi – tìm kiếm dày đặc cho kết quả tốt hơn tìm kiếm lai (tỷ lệ hit 69,2% so với 63,5% cho Titan)

Hãy nhớ rằng, những kết quả này đặc biệt đối với trường hợp sử dụng của chúng tôi (tài liệu khoa học bằng tiếng Anh, tiếng Pháp và tiếng Nhật).

Tổng quan RAG

Retrieval-Augmented Generation (RAG) kết hợp các mô hình ngôn ngữ lớn (LLM) với việc truy xuất kiến thức bên ngoài. Thay vì chỉ dựa vào dữ liệu đào tạo của LLM, RAG sẽ tìm nạp các tài liệu có liên quan và cung cấp chúng làm ngữ cảnh cho việc tạo sinh.

Các thành phần cốt lõi:

  • Bộ chuyển đổi PDF - Trích xuất văn bản từ tài liệu
  • Chiến lược Chunking - Chia tài liệu thành các phần có thể truy xuất
  • Mô hình nhúng - Chuyển đổi văn bản thành biểu diễn vector
  • Cơ sở dữ liệu Vector - Lưu trữ và tìm kiếm các vector nhúng
  • Chế độ truy xuất - Tìm kiếm dày đặc, thưa thớt hoặc lai
  • LLM - Tạo phản hồi từ ngữ cảnh đã truy xuất

rag_mermaid

Phương pháp luận Benchmark của chúng tôi

Thiết lập

Benchmark của chúng tôi sử dụng các tài liệu thực tế từ cơ sở dữ liệu sản xuất của chúng tôi:

  • PDF được chuyển đổi bằng Mistral OCR - các tài liệu khoa học phức tạp với phương trình, bảng biểu và sơ đồ
  • Lưu trữ trong Qdrant - cơ sở dữ liệu vector mà chúng tôi chọn
  • Cấu hình sản xuất - thiết lập tương tự như chúng tôi sử dụng trong hệ thống RAG thực tế của mình

Bộ dữ liệu kiểm tra

Chúng tôi đã tạo các câu hỏi kiểm tra bao gồm các mức độ khó khác nhau:

  • Truy xuất đơn giản: Trích xuất trực tiếp từ văn bản (ví dụ: “Uncertainty in the specific heat capacity of helium”)
  • Câu hỏi phức tạp: Hiểu khái niệm (ví dụ: “Synchronization in a coupled fluid–structure system?”)

Bộ dữ liệu kiểm tra của chúng tôi bao gồm 156 truy vấn kiểm tra duy nhất cho mỗi mô hình nhúng:

  • 3 ngôn ngữ: Tiếng Anh, tiếng Pháp, tiếng Nhật
  • 2 dạng truy vấn: Câu hỏi (“What is…?”) và các câu khẳng định (“Characteristics of…”)

Đây là một ví dụ từ bộ kiểm tra của chúng tôi:

python MetaQuestion( source_document=“Jaiman et al. - 2023 - Mechanics of Flow-Induced Vibration Physical Mode.pdf”, related_questions=[ # Affirmatives Question( text=“Cylinder vibrations induced by fluid flow”, language=Language.ENGLISH, form=Form.affirmative, ), Question( text=“Vibrations des cylindres induites par l’écoulement des fluides”, language=Language.FRENCH, form=Form.affirmative, ), Question( text=“流体の流れによって誘発される円筒の振動”, language=Language.JAPANESE, form=Form.affirmative, ), # Interrogatives Question( text=“What can you tell me about cylinder vibrations induced by fluid flow?”, language=Language.ENGLISH, form=Form.interrogative, ), Question( text=“Que peux-tu me dire sur les vibrations des cylindres induites par l’écoulement des fluides ?”, language=Language.FRENCH, form=Form.interrogative, ), Question( text=“流体の流れによって誘発される円筒の振動について教えてください。”, language=Language.JAPANESE, form=Form.interrogative, ), ], )

Mỗi truy vấn đều có kết quả thực tế đã biết - chúng tôi biết tài liệu nào cần được truy xuất.

Các chỉ số

Lưu ý quan trọng về Mục tiêu Truy xuất của chúng tôi:

Mục tiêu: Mục tiêu chính của chúng tôi là truy xuất đúng tài liệu, không nhất thiết là đoạn văn hoặc phần chính xác. Một khi chúng tôi có tài liệu phù hợp, LLM của chúng tôi có thể trích xuất thông tin cụ thể cần thiết. Đây là một sự khác biệt quan trọng đã ảnh hưởng đến thiết kế benchmark và kết luận của chúng tôi.

Chúng tôi đã đo lường hiệu suất truy xuất bằng:

  • Top-10 Recall (Tỷ lệ Hit): % truy vấn mà tài liệu đúng xuất hiện trong top 10 kết quả
  • MRR (Mean Reciprocal Rank): Trung bình của 1/hạng của các tài liệu đúng (càng cao càng tốt) nhân với 100 để chuẩn hóa
  • Top-1 Recall: % truy vấn mà tài liệu đúng là kết quả đầu tiên

Vì chúng tôi chỉ quan tâm đến việc truy xuất cấp tài liệu, không phải độ chính xác cấp chunk, điều này có ý nghĩa quan trọng đối với việc tối ưu hóa kích thước chunk (xem các phát hiện bên dưới).

Chiến lược Chunking

Chúng tôi đã thử nghiệm hai phương pháp:

Chunking ngẫu nhiên: Phân chia dựa trên ký tự đơn giản với sự chồng lấp bằng LangChain’s RecursiveCharacterTextSplitter. Các chunk được tạo bằng cách tách tại các ranh giới tự nhiên (đoạn văn, câu) mà không hiểu cấu trúc tài liệu.

Chunking nhận biết ngữ cảnh: Phân tích cấu trúc markdown (tiêu đề, phần) thành một cây phân cấp. Mỗi chunk bao gồm các tiêu đề phần mẹ của nó làm ngữ cảnh. Ví dụ, một chunk từ “Phần 2.3 → Tiêu đề phụ 2.3.1” bao gồm cả hai cấp tiêu đề, bảo tồn cấu trúc tài liệu.

Cấu hình kiểm tra

Chúng tôi đã kiểm tra các kết hợp khác nhau:

  • Chiến lược chunking: Ngẫu nhiên so với nhận biết ngữ cảnh
  • Kích thước chunk: 2K, 4K, 6K ký tự cho Titan V2; 2K cho Mistral Embed; 2K, 10K, 40K ký tự cho Qwen 8B (tận dụng cửa sổ ngữ cảnh 32K lớn hơn của nó)
  • Mô hình nhúng: AWS Titan V2, Qwen 8B, Mistral
  • Chế độ truy xuất: Chỉ dày đặc, lai (dày đặc + thưa thớt), chỉ thưa thớt

Điều này đã cho chúng tôi nhiều cấu hình để so sánh một cách có hệ thống trên tất cả các khía cạnh.

Kết quả & Bài học chính

1. Mô hình nhúng: AWS Titan Thắng (Đáng ngạc nhiên!)

Chúng tôi đã so sánh ba mô hình nhúng trên tất cả các cấu hình, chiến lược kích thước chunk là chunking ngẫu nhiên với kích thước chunk 2000 và chồng lấp 400.

model_comparison

model_comparison_english_affirmative

Tiêu chí lựa chọn Mô hình:

Chúng tôi đặc biệt muốn các mô hình nhúng có thể truy cập qua API để tránh quản lý cơ sở hạ tầng suy luận mô hình. Điều này đã dẫn chúng tôi đến việc kiểm tra:

  • AWS Titan V2 (amazon.titan-embed-text-v2:0): Truy cập qua AWS Bedrock
  • Qwen 8B (Alibaba-NLP/gte-Qwen2-7B-instruct): Truy cập qua Hugging Face Inference API (nhà cung cấp: Nebius AI)
  • Mistral Embed (mistral-embed): Truy cập qua Mistral AI API

Cả ba đều cung cấp quyền truy cập API REST đơn giản, điều này rất cần thiết cho các yêu cầu sản xuất của chúng tôi.

Kết quả:

  • AWS Titan V2: Tỷ lệ hit 69,2%
  • Qwen 8B: Tỷ lệ hit 57,7%
  • Mistral: Tỷ lệ hit 39,1%

Sự ngạc nhiên từ bảng xếp hạng MTEB:

Nếu bạn chọn mô hình nhúng, bạn sẽ tự nhiên kiểm tra bảng xếp hạng MTEB (Massive Text Embedding Benchmark) trên Hugging Face. Đây là benchmark tiêu chuẩn để so sánh các mô hình nhúng trên các tác vụ khác nhau.

Phần đáng ngạc nhiên ở đây là: AWS Titan V2 thậm chí còn không có trên bảng xếp hạng MTEB. Tuy nhiên, nó đã vượt trội hơn cả Qwen 8B và Mistral (cả hai đều có trên bảng xếp hạng) cho trường hợp sử dụng của chúng tôi.

Tại sao Titan thắng đối với chúng tôi:

  • Rẻ hơn các giải pháp thay thế và có giới hạn tỷ lệ cao hơn
  • Hiệu suất đa ngôn ngữ tốt hơn (quan trọng đối với tài liệu EN/FR/JA của chúng tôi)
  • Mạnh mẽ hơn đối với thuật ngữ khoa học và biệt ngữ kỹ thuật
  • Rất rẻ

Cạm bẫy của các benchmark truyền thống:

Hầu hết các benchmark nhúng đều kiểm tra trên các truy vấn khẳng định chỉ bằng tiếng Anh. Đây là những gì xảy ra khi chúng tôi chỉ giới hạn phân tích của mình cho các điều kiện đó:

Hiểu biết quan trọng: Trong các điều kiện benchmark “truyền thống” (chỉ câu hỏi khẳng định bằng tiếng Anh), Mistral hoạt động gần như ngang bằng với Titan (tỷ lệ hit 76,9% so với 80,8%). Điều này có vẻ hứa hẹn! Tuy nhiên, khi chúng tôi kiểm tra trên tất cả các ngôn ngữ và dạng truy vấn, hiệu suất của Mistral giảm đáng kể (39,1% tổng thể so với 69,2% cho Titan).

Sự khác biệt chính: tính nhất quán. Titan duy trì hiệu suất mạnh mẽ trên tiếng Anh, tiếng Pháp, tiếng Nhật và cả hai dạng truy vấn. Mistral vượt trội trong các điều kiện hẹp nhưng thiếu tính mạnh mẽ trên các truy vấn thực tế đa dạng.

Đây chính xác là lý do tại sao chúng tôi chọn Titan thay vì Mistral mặc dù hiệu suất cạnh tranh của Mistral trong các điều kiện lý tưởng. Các hệ thống RAG trong sản xuất cần các mô hình hoạt động nhất quán trên các mẫu truy vấn đa dạng, không chỉ những mô hình vượt trội trong các benchmark được kiểm soát.

Bài học đã rút ra: Khi đánh giá các mô hình nhúng, hãy kiểm tra chúng trong các điều kiện đa dạng phù hợp với trường hợp sử dụng sản xuất của bạn. Một mô hình thống trị các benchmark chỉ bằng tiếng Anh có thể gặp khó khăn với nội dung đa ngôn ngữ hoặc các cách diễn đạt truy vấn khác nhau. Hãy tìm kiếm sự nhất quán, không chỉ hiệu suất đỉnh cao.

2. Kích thước Chunk: Đừng Suy Nghĩ Quá Nhiều

Chúng tôi đã kiểm tra các kích thước chunk khác nhau: 2K, 4K, 6K ký tự với Titan V2 và 2K, 10K, 40K ký tự với Qwen 8B.

titan_naive_chunk_size_comparison

qwen_naive_chunk_size_comparison

Kết quả: Kích thước chunk có ít tác động đến hiệu suất. Sự khác biệt giữa các kích thước chunk khác nhau là không đáng kể – tất cả các cấu hình đều hoạt động trong vòng vài phần trăm giống nhau.

Tại sao kích thước chunk không quan trọng lắm đối với chúng tôi:

Hãy nhớ, mục tiêu của chúng tôi là truy xuất cấp tài liệu, không phải tìm đoạn văn chính xác. Miễn là bất kỳ chunk nào từ tài liệu đúng đều được xếp hạng cao, chúng tôi sẽ thành công. Các chunk lớn hơn vẫn chứa nội dung liên quan, chỉ là có nhiều ngữ cảnh xung quanh hơn.

Khuyến nghị thực tế: Đừng tối ưu hóa quá mức

Không giống như lời khuyên RAG thông thường nhấn mạnh việc tinh chỉnh kích thước chunk cẩn thận, dữ liệu của chúng tôi cho thấy kích thước chunk đơn giản là không phải là một tham số quan trọng. Đây là những gì chúng tôi đã tìm thấy:

  • Không có phạt hiệu suất với chunk lớn hơn - 2K hoạt động tương tự như 40K
  • Chunk lớn hơn = Chi phí thấp hơn - Ít chunk hơn có nghĩa là:
    • Nhà cung cấp suy luận: ít token chồng chéo hơn (rẻ hơn và ít khả năng đạt giới hạn tỷ lệ) và nhúng tất cả tài liệu nhanh hơn
    • Cơ sở dữ liệu Vector: ít lưu trữ hơn, lập chỉ mục nhanh hơn và ít so sánh tương tự hơn tại thời điểm truy vấn Những gì chúng tôi sử dụng:
  • Titan V2: 6K ký tự (chúng tôi gặp lỗi vượt quá giới hạn này, mặc dù mô hình tuyên bố hỗ trợ 8K token - không rõ lý do)
  • Qwen 8B: 40K ký tự (hoạt động tốt với cửa sổ ngữ cảnh 32K token của nó)

Kết luận: Chúng tôi đã chuyển từ chunk 2K sang chunk lớn hơn để tiết kiệm chi phí, nhưng thành thật mà nói, bất kỳ kích thước hợp lý nào cũng sẽ hoạt động. Đừng lãng phí nhiều ngày để tối ưu hóa điều này – hãy tập trung vào lựa chọn mô hình nhúng và chế độ truy xuất.

3. Chiến lược Chunking: Ngẫu nhiên tương đương với Nhận biết ngữ cảnh

Chúng tôi đã thử nghiệm chunking ngẫu nhiên (phân chia dựa trên ký tự đơn giản) so với chunking nhận biết ngữ cảnh (bảo tồn cấu trúc tài liệu như tiêu đề và đoạn văn).

strategy_comparison_titan strategy_comparison_qwen

Kết quả: Chunking ngẫu nhiên vượt trội hơn chunking nhận biết ngữ cảnh cho các nhúng Titan (tỷ lệ hit tốt nhất 71,8% so với 67,9%, trung bình 70,5% so với 63,8% trên các kích thước chunk với chỉ tìm kiếm dày đặc).

Diễn giải:

  • Đối với tài liệu kỹ thuật, các ranh giới cấu trúc chặt chẽ (như dấu ngắt phần) có thể làm chia tách nội dung liên quan
  • Chunking ngẫu nhiên với sự chồng lấp đã nắm bắt đủ ngữ cảnh
  • Chunking nhận biết ngữ cảnh làm tăng thêm sự phức tạp mà không mang lại lợi ích rõ ràng trong trường hợp của chúng tôi

Khuyến nghị: Bắt đầu đơn giản với chunking ngẫu nhiên. Chỉ sử dụng nhận biết ngữ cảnh nếu bạn có các yêu cầu cụ thể phụ thuộc vào cấu trúc.

4. Tìm kiếm Dày đặc so với Lai trong Qdrant (Kết quả đáng ngạc nhiên)

Hiểu các Chế độ Truy xuất:

Các cơ sở dữ liệu vector như Qdrant hỗ trợ các phương pháp truy xuất khác nhau:

  • Tìm kiếm dày đặc: Sử dụng nhúng ngữ nghĩa (như Titan hoặc Qwen) để tìm tài liệu dựa trên ý nghĩa. Chuyển đổi truy vấn và tài liệu thành các vector có chiều cao và tìm các vector tương tự. Tuyệt vời cho các kết quả phù hợp về khái niệm nhưng có thể bỏ lỡ các yêu cầu từ khóa chính xác.
  • Tìm kiếm thưa thớt: Sử dụng khớp từ khóa (tương tự như BM25 hoặc TF-IDF). Qdrant thực hiện điều này thông qua các nhúng thưa thớt của FastEmbed (chúng tôi đã sử dụng prithvida/Splade_PP_en_v1). Tuyệt vời cho các kết quả khớp thuật ngữ chính xác nhưng bỏ lỡ sự tương tự về ngữ nghĩa.
  • Tìm kiếm lai: Kết hợp cả hai phương pháp – hiểu ngữ nghĩa từ nhúng dày đặc + độ chính xác từ khóa từ nhúng thưa thớt. Kết quả được hợp nhất bằng cách hợp nhất điểm số. Thông thường, người ta tin rằng phương pháp này sẽ luôn tốt hơn bất kỳ phương pháp nào đơn lẻ.

Phát hiện của chúng tôi:

Lý thuyết thông thường cho rằng tìm kiếm lai sẽ vượt trội hơn tìm kiếm dày đặc. Đây thực sự là lý do tại sao chúng tôi chọn Qdrant. Dữ liệu của chúng tôi cho thấy điều ngược lại cho trường hợp sử dụng của chúng tôi.

metrics_by_sparse_mode_titan metrics_by_sparse_mode_qwen metrics_by_sparse_mode_mistral

Phát hiện: Tìm kiếm chỉ dày đặc đạt tỷ lệ hit 69,2% so với 63,5% cho tìm kiếm lai (sử dụng nhúng Titan với chunking ngẫu nhiên 2K ký tự).

Tại sao điều này có thể xảy ra:

  • Đối với tài liệu khoa học có thuật ngữ kỹ thuật, bản thân các nhúng dày đặc đã nắm bắt hiệu quả ý nghĩa ngữ nghĩa
  • Tìm kiếm thưa thớt có thể gây ra nhiễu từ các kết quả khớp từ khóa thiếu ngữ cảnh ngữ nghĩa
  • Kết quả của bạn có thể khác nhau – điều này đặc biệt đối với các loại tài liệu của chúng tôi và việc triển khai FastEmbed của Qdrant

Bối cảnh quan trọng: Chúng tôi đã chọn Qdrant đặc biệt vì khả năng tìm kiếm lai của nó. Mặc dù tìm kiếm chỉ dày đặc hoạt động tốt hơn trong các benchmark của chúng tôi, nhưng tìm kiếm lai vẫn có giá trị đối với:

  • Yêu cầu khớp từ khóa chính xác
  • Tìm kiếm tuân thủ quy định nơi các thuật ngữ cụ thể quan trọng
  • Phương án dự phòng khi nhúng dày đặc gặp khó khăn với các thuật ngữ hiếm

Khuyến nghị: Benchmark cả hai chế độ trên corpus cụ thể của bạn. Đừng giả định rằng lai luôn tốt hơn.

5. Hiệu suất đa ngôn ngữ

Tài liệu của chúng tôi trải dài trên tiếng Anh, tiếng Pháp và tiếng Nhật. Chúng tôi cần truy xuất đa ngôn ngữ mạnh mẽ.

Kết quả:

  • Tiếng Anh: Tỷ lệ hit 73,1%
  • Tiếng Pháp: Tỷ lệ hit 48,7%
  • Tiếng Nhật: Tỷ lệ hit 44,2%

Tiếng Anh vượt trội đáng kể so với tiếng Pháp và tiếng Nhật. Điều này cho thấy hệ thống RAG của chúng tôi hoạt động tốt nhất với nội dung tiếng Anh, mặc dù tiếng Pháp và tiếng Nhật vẫn đạt được tỷ lệ truy xuất hợp lý. Khả năng đa ngôn ngữ của Titan embeddings đã được xác nhận, ngay cả khi hiệu suất thay đổi theo ngôn ngữ.

So sánh Mô hình trên các Ngôn ngữ:

Biểu đồ dưới đây so sánh cả ba mô hình nhúng (Titan, Qwen và Mistral) trên ba ngôn ngữ chúng tôi đã kiểm tra:

model_comparison_by_language

Titan hoạt động nhất quán hơn các mô hình khác trên tất cả các ngôn ngữ, với hiệu suất đặc biệt mạnh mẽ bằng tiếng Anh. Khoảng cách giữa các mô hình rõ rệt nhất ở tiếng Pháp và tiếng Nhật, nơi khả năng đa ngôn ngữ của Titan thể hiện lợi thế rõ ràng.

So sánh Mô hình trên các Dạng Truy vấn:

Chúng tôi cũng đã phân tích hiệu suất trên các dạng truy vấn khác nhau (câu hỏi so với khẳng định):

model_comparison_by_form

Thật thú vị, TITAN và Mistral hoạt động tốt hơn với các câu khẳng định, như lý thuyết dự đoán (vì hầu hết thông tin trong văn bản được trình bày dưới dạng khẳng định). Tuy nhiên, Qwen hoạt động tốt hơn với các câu hỏi, điều này thực sự không có ý nghĩa gì.

Quyết định Thực tế (Không Benchmark)

Chuyển đổi PDF: Mistral OCR

Quyết định: Chúng tôi sử dụng Mistral OCR cho tất cả các quy trình xử lý tài liệu.

Đánh giá: Chúng tôi đã thử nghiệm thủ công 3 tệp PDF phức tạp (phương trình, bảng biểu, sơ đồ, trang quét) với:

Phát hiện: Chỉ có Mistral OCR phân tích đúng các ký hiệu toán học phức tạp và bảng biểu trong các tài liệu được quét. Sự khác biệt về chất lượng đầu ra là rất lớn, thậm chí nó còn hoạt động với các phương trình hóa học.

Tại sao Chuyển đổi Markdown lại Quan trọng:

Việc chuyển đổi tất cả tài liệu (PDF, tệp Word, v.v.) sang markdown không chỉ vì hiệu suất – nó thực sự cần thiết để xây dựng một hệ thống RAG có thể gỡ lỗi:

  • Khả năng gỡ lỗi: Khi truy xuất thất bại hoặc trả về kết quả sai, bạn cần kiểm tra xem thực tế những gì đã được lập chỉ mục. Với các tệp PDF thô, bạn hoàn toàn mù tịt. Với markdown, bạn có thể mở tệp, tìm kiếm nội dung dự kiến và hiểu chính xác chiến lược chunking của bạn đã làm gì với nó.
  • Hiệu suất: Markdown nhẹ và xử lý nhanh. Chunking văn bản markdown nhanh hơn nhiều lần so với việc phân tích lại PDF sau mỗi lần cập nhật nhúng.
  • Khả năng tái lập: Markdown cung cấp cho bạn một biểu diễn ổn định, có thể kiểm soát phiên bản của tài liệu của bạn. Bạn có thể theo dõi các thay đổi, so sánh các phiên bản và đảm bảo tính nhất quán trên toàn bộ quy trình của bạn.
  • Tốc độ lặp lại: Kiểm tra các chiến lược chunking khác nhau trên markdown chỉ mất vài giây, không phải vài phút. Điều này làm cho việc thử nghiệm trở nên thực tế.

Kết luận: Nếu không có markdown làm định dạng trung gian, bạn không thể gỡ lỗi hoặc lặp lại hệ thống RAG của mình một cách hiệu quả. Nó không phải là tùy chọn – nó là nền tảng.

Sự đánh đổi: Mistral OCR rất tốn kém (1 đô la cho mỗi 1000 trang). Đối với các tài liệu khoa học mà độ chính xác là rất quan trọng, nó xứng đáng. Nếu bạn có các tệp PDF đơn giản hơn, hãy thử các giải pháp thay thế mã nguồn mở trước.

Cơ sở dữ liệu Vector: Qdrant

Quyết định: Chúng tôi sử dụng Qdrant (dịch vụ được quản lý).

Bối cảnh: Chúng tôi đã đánh giá Milvus, Qdrant, AWS OpenSearch, Pinecone và PostgreSQL với pgvector. Là một phần của AWS, ban đầu chúng tôi đã thử OpenSearch.

Phát hiện quan trọng: Đừng sử dụng AWS OpenSearch cho tìm kiếm vector. Lựa chọn rẻ nhất là $70 /ngày (~$2.100/tháng) cho một cụm một node. Điều này quá đắt đỏ cho những gì nó cung cấp.

Tại sao Qdrant:

  • Hỗ trợ tìm kiếm lai gốc với FastEmbed (động lực ban đầu của chúng tôi)
  • Thiết lập Docker dễ dàng để tự host
  • Sau đó di chuyển sang dịch vụ Qdrant được quản lý để tránh bảo trì
  • Hiệu suất và tính năng ổn định
  • Giá cả hợp lý trên Qdrant Cloud

Tại sao không Qdrant:

  • Quản trị trong Qdrant Cloud thiếu các tính năng cơ bản, như chuyển quyền sở hữu
  • Đôi khi các collection vẫn ở “chế độ xám” vì những lý do không rõ ràng (ít nhất là đối với những người mới bắt đầu như chúng tôi), tất cả những gì bạn phải làm là “bắt đầu tối ưu hóa” thủ công, nhưng nó vẫn kỳ lạ.

Tại sao không phải các giải pháp thay thế khác:

  • PostgreSQL có vẻ quá mức vì chúng tôi không sử dụng PostgreSQL (hoặc Aurora từ AWS) và chúng tôi sợ phải trả nhiều tiền như chúng tôi đã làm với opensearch cho một dịch vụ được quản lý.
  • Pinecone không phải mã nguồn mở.
  • Milvus so với Qdrant, đây là hai cơ sở dữ liệu mã nguồn mở được sao nhiều nhất. Chúng tôi phải chọn một nên một thành viên của chúng tôi chỉ thử triển khai cả hai trên một máy chủ tự host, và anh ấy đã mất nhiều thời gian hơn để làm điều đó trên Milvus, vì vậy Qdrant đã thắng. Tôi thừa nhận đây không phải là lý do tốt nhất.

Kết luận

Xây dựng hệ thống RAG trong sản xuất đòi hỏi sự cân bằng giữa hiệu suất, chi phí và sự phức tạp. Đây là điểm khởi đầu được đề xuất của chúng tôi:

Cấu hình được đề xuất:

  • Mô hình nhúng: AWS Titan V2 (tỷ lệ hit 69,2% - tốt nhất cho nội dung khoa học đa ngôn ngữ)
  • Kích thước Chunk: Đừng suy nghĩ quá nhiều – bất kỳ kích thước hợp lý nào cũng hoạt động (chúng tôi sử dụng 6K cho Titan, 40K cho Qwen để tiết kiệm chi phí)
  • Chiến lược Chunking: Chunking ngẫu nhiên (trung bình 70,5% tỷ lệ hit so với 63,8% cho nhận biết ngữ cảnh – đơn giản và tốt hơn)
  • Chế độ Truy xuất: Chỉ dày đặc cho trường hợp sử dụng của chúng tôi (69,2% so với 63,5% cho lai với Titan)
  • Cơ sở dữ liệu Vector: Qdrant hoặc Milvus (tránh AWS OpenSearch do chi phí)
  • Chuyển đổi PDF: Mistral OCR cho các tài liệu khoa học phức tạp (tốn kém nhưng cần thiết)

Bài học chính: Đừng mù quáng làm theo “thực tiễn tốt nhất” từ các bài đăng blog. Hãy benchmark trên các loại tài liệu và mẫu truy vấn cụ thể của bạn. Các phát hiện của chúng tôi đã đi ngược lại lời khuyên thông thường (chỉ dày đặc vượt trội hơn lai, ngẫu nhiên vượt trội hơn nhận biết ngữ cảnh), nhưng chúng có thể tái lập và có ý nghĩa đối với trường hợp sử dụng của chúng tôi. Những gì hiệu quả với chúng tôi có thể không hiệu quả với bạn. Cách duy nhất để biết là đo lường.

Lưu ý về Tính khả dụng của Dữ liệu

Mã benchmark và phương pháp luận của chúng tôi được trình bày chi tiết trong bài viết này để có thể tái lập. Tuy nhiên, các tài liệu khoa học mà chúng tôi đã sử dụng là độc quyền và mã nguồn đóng (tài liệu nghiên cứu và vật liệu quy định kỹ thuật hạt nhân). Mã nguồn cũng vậy, không thể chia sẻ, nó là một phần của mono-repo của chúng tôi.

Mặc dù chúng tôi không thể chia sẻ dữ liệu thô, chúng tôi sẵn lòng trả lời các câu hỏi về phương pháp luận, cách tiếp cận kiểm thử hoặc các phát hiện cụ thể của chúng tôi. Đừng ngần ngại liên hệ nếu bạn đang triển khai một cái gì đó tương tự!

Câu hỏi hoặc phản hồi? Chúng tôi rất muốn nghe về trải nghiệm RAG của bạn, đặc biệt nếu bạn tìm thấy kết quả khác!

Chi tiết Triển khai

Đây là các đoạn mã chính từ việc triển khai của chúng tôi mà bạn có thể thấy hữu ích, nó rất cơ bản:

Chiến lược Chunking

Chunking Ngẫu nhiên - Phân chia dựa trên ký tự đơn giản với sự chồng lấp bằng LangChain:

python

src/pyjimmy/rag/chunk.py:32-34

def split_markdown(s3_markdown_path: Path, max_chunk_size: int, strategy: ChunkingStrategy) -> list[str]: if strategy == ChunkingStrategy.naive: return RecursiveCharacterTextSplitter( chunk_size=max_chunk_size, chunk_overlap=400, add_start_index=True ).split_text(markdown_text)

Chunking Nhận biết Ngữ cảnh - Bảo tồn cấu trúc tài liệu bằng cách phân tích phân cấp tiêu đề markdown:

python

src/pyjimmy/rag/chunk.py:82-99

class Section: def init(self, body: str = “”, title: str | None = None, level: int = 0): self.body: str = body self.title: str | None = title self.level: int = level self.children: list[Section] = []

@classmethod
def from_markdown(cls, markdown_text: str) -> Section:
    """Parse markdown into hierarchical sections based on heading levels."""
    lines = markdown_text.split("\n")
    root = cls()
    stack = [root]
    for line in lines:
        if line.startswith("#"):
            level = len(line) - len(line.lstrip("#"))
            title = line.lstrip("#").strip()
            new_section = cls(title=title, level=level)
            while stack and stack[-1].level >= level:
                stack.pop()
            stack[-1]._add_child(new_section)
            stack.append(new_section)
        else:
            if stack:
                stack[-1].body += line + "\n"
            return root
            
def to_chunks(self, max_chunk_size: int, context: str = "") -> list[str]:
    context += self.current_context
    chunks = []
    if self.body.replace("\n", "").strip():
        if len(context) >= max_chunk_size:
            logger.warning(
                f"Context too large ({len(context)} chars) for max_chunk_size {max_chunk_size}. Skipping context."
            )
            context = ""
        chunks += [
            context + splitted_body
            for splitted_body in _split_text_into_chunks(self.body, max_chunk_size - len(context))
        ]
    for child in self.children:
        chunks += child.to_chunks(max_chunk_size, context)
    return chunks

Xử lý PDF bằng Mistral OCR

Chuyển đổi PDF sang Markdown - Xử lý các tài liệu khoa học phức tạp:

python

src/pyjimmy/rag/pdf.py:22-45

def convert_pdf_to_markdown(pdf_path: Path, output_path: Path) -> tuple[Path, Path]: logger.info(f"Start converting PDF {pdf_path} to markdown…") # Split large PDFs to stay under 50MB API limit pdfs_under_50mb = _split_pdf_to_under_50mb_improved(pdf_path, output_path)

final_markdown = ""
final_pages = []
for pdf in pdfs_under_50mb:
    ocr_response = _convert_pdf_under_50MB_to_markdown(pdf)
    markdown = _get_combined_markdown(ocr_response=ocr_response)
    pages = ocr_response.model_dump()["pages"]
    final_markdown += markdown
    final_pages.extend(pages)

markdown_path = output_path / f"{pdf_path.stem}.md"
markdown_path.write_text(final_markdown)
return markdown_path, yaml_path

Gọi API Mistral OCR:

python

src/pyjimmy/rag/pdf.py:175-192

def _convert_pdf_under_50MB_to_markdown(pdf_path: Path) -> OCRResponse: uploaded_pdf = MISTRAL_CLIENT.files.upload( file=File( file_name=pdf_path.name, content=pdf_path.read_bytes(), content_type=“application/pdf” ), purpose=“ocr”, )

signed_url = MISTRAL_CLIENT.files.get_signed_url(file_id=uploaded_pdf.id)

return MISTRAL_CLIENT.ocr.process(
    model="mistral-ocr-latest",
    document={"type": "document_url", "document_url": signed_url.url},
    include_image_base64=True,
)

Thiết lập Mô hình Nhúng

Cấu hình các Mô hình Nhúng khác nhau:

python

src/pyjimmy/rag/embedding.py:33-58

def get_dense_embedding_model(model: DenseEmbeddingModel) -> Embeddings: if model == DenseEmbeddingModel.titan: # AWS Titan V2: 8,192 max tokens, 50K max characters return BedrockEmbeddings(model_id=“amazon.titan-embed-text-v2:0”)

if model == DenseEmbeddingModel.qwen:
    # Qwen 8B: 32K token context window
    return HuggingFaceEndpointEmbeddings(
        client=InferenceClient(provider="nebius", api_key=token),
        model="Qwen/Qwen3-Embedding-8B"
    )

if model == DenseEmbeddingModel.mistral:
    return MistralAIEmbeddings(
        mistral_api_key=os.environ["PYJIMMY_MISTRAL_API_KEY"],
        model="mistral-embed"
    )

Sparse embeddings for hybrid search

SPARSE_EMBEDDING_MODEL = FastEmbedSparse(model_name=“prithvida/Splade_PP_en_v1”)

Cấu hình Cửa hàng Vector Qdrant

Tạo Collection với Tìm kiếm Lai:

python

src/pyjimmy/rag/qdrant.py:27-40

def create_vector_store(collection_name: str, model: DenseEmbeddingModel): logger.info(f"Creating Qdrant collection {collection_name}…") QdrantVectorStore.from_texts( texts=[], url=QDRANT_URL, api_key=QDRANT_API_KEY, collection_name=collection_name, embedding=get_dense_embedding_model(model), sparse_embedding=SPARSE_EMBEDDING_MODEL, # For hybrid search retrieval_mode=RetrievalMode.HYBRID, )

Thêm Tài liệu với Logic Thử lại:

python

src/pyjimmy/rag/qdrant.py:64-72, 78-104

@retry(wait=wait_fixed(60)) def _add_texts_to_vector_store( vector_store: QdrantVectorStore, texts: list[str], s3_folder: Path ) -> list[str]: “““Retry on failure due to embedding API rate limits.””” return vector_store.add_texts( texts, metadatas=[{“s3_folder”: s3_folder} for _ in texts] )

def add_markdown_to_qdrant( s3_markdown_path: Path, max_chunk_size: int, chunking_strategy: ChunkingStrategy, model: DenseEmbeddingModel ): chunks = split_markdown(s3_markdown_path, max_chunk_size, strategy=chunking_strategy) vector_store = get_vector_store(collection_name, model=model)

vector_ids = []
max_chunks_one_request = 10
# Batch processing to avoid timeouts
for start_index in range(0, len(chunks), max_chunks_one_request):
    vector_ids.extend(
        _add_texts_to_vector_store(
            vector_store,
            chunks[start_index : start_index + max_chunks_one_request],
            s3_folder=s3_folder
        )
    )

Recommended for You

LLasa Chuyển sang RL- Huấn luyện LLaSA với GRPO để cải thiện Âm điệu và Khả năng Diễn đạt

LLasa Chuyển sang RL- Huấn luyện LLaSA với GRPO để cải thiện Âm điệu và Khả năng Diễn đạt

LLasa Chuyển sang RL- Huấn luyện LLaSA với GRPO để cải thiện Âm điệu và Khả năng Diễn đạt

⚡ Điện, Nhiệt và Trí tuệ ☁️ - Giải thích Trung tâm Dữ liệu AI 🏭

⚡ Điện, Nhiệt và Trí tuệ ☁️ - Giải thích Trung tâm Dữ liệu AI 🏭

Điện, Nhiệt và Trí tuệ - Giải thích Trung tâm Dữ liệu AI