Từ số 0 đến hệ thống RAG: thành công và thất bại
AI/ML·Hacker News·0 lượt xem

Từ số 0 đến hệ thống RAG: thành công và thất bại

From zero to a RAG system: successes and failures

AI Summary

Một lập trình viên đã xây dựng một hệ thống RAG nội bộ để kỹ sư có thể truy vấn dữ liệu dự án của công ty. Quá trình này gặp phải các thử thách trong việc lựa chọn LLM chạy local, tạo embedding và xử lý 1TB dữ liệu phi cấu trúc. Hệ thống sử dụng Ollama để chạy LLM trên máy cá nhân và LlamaIndex làm RAG framework. Đối với việc tạo embedding, `nomic-embed-text` đã được chọn. Những bài học kinh nghiệm quan trọng cho các lập trình viên khác bao gồm: việc lọc dữ liệu mạnh mẽ là cực kỳ cần thiết để loại bỏ các loại file không liên quan, và cần quản lý tài nguyên cẩn thận để tránh tình trạng tràn bộ nhớ (memory overflow) trong quá trình indexing.

Vài tháng trước, tôi được giao nhiệm vụ tạo một công cụ nội bộ cho các kỹ sư của công ty: Trò chuyện sử dụng LLM cục bộ. Không có gì bất thường cho đến nay. Sau đó, các yêu cầu được đưa ra: nó phải có tốc độ nhanh...

Một vài tháng trước, tôi được giao nhiệm vụ tạo một công cụ nội bộ cho các kỹ sư của công ty: Trò chuyện sử dụng LLM cục bộ. Không có gì bất thường cho đến nay. Sau đó, các yêu cầu được đưa ra: nó phải có phản hồi nhanh, tôi nhấn mạnh... nhanh!, và... nó cũng phải đưa ra câu trả lời về mọi dự án mà công ty đã thực hiện trong suốt lịch sử của mình (gần một thập kỷ). Họ không muốn một công cụ tìm kiếm truyền thống mà là một công cụ mà bạn có thể đặt câu hỏi bằng ngôn ngữ tự nhiên và nhận được câu trả lời kèm theo tham chiếu đến tài liệu gốc. Với sự nhấn mạnh vào việc cung cấp thông tin từ các tập tin OrcaFlex (phần mềm mô phỏng động lực học vật nổi, dây cáp, v.v., được sử dụng rộng rãi trong ngành công nghiệp ngoài khơi). Nó vốn có vẻ phức tạp, nhưng nó đã được xác nhận khi tôi được cấp quyền truy cập vào 1 TB dự án, trộn lẫn với các tài liệu kỹ thuật, báo cáo, phân tích, quy định, CSV, v.v. Chuyến tàu lượn đầy cảm xúc đã bắt đầu.

Tôi sẽ nói trước với bạn rằng đó không phải là một quá trình nhanh chóng hay dễ dàng và đó là lý do tại sao tôi muốn chia sẻ nó. Từ những nỗ lực đầu tiên, những sai lầm cho đến kiến ​​trúc cuối cùng được đưa vào sản xuất. Tôi cũng muốn nhấn mạnh rằng tôi chưa bao giờ làm bất cứ điều gì tương tự trước đây và cũng không biết RAG hoạt động như thế nào.

Chúng ta sẽ giải quyết từng vấn đề và giải pháp tôi áp dụng cho từng vấn đề.

Vấn đề 1: chọn công nghệ phù hợp

Bước đầu tiên là xác định ngăn xếp.

Tôi cần một mô hình ngôn ngữ địa phương mà không cần dựa vào API bên ngoài vì lý do bảo mật. Ollama nổi lên như một lựa chọn hoàn thiện và dễ sử dụng nhất để chạy các mô hình LLaMA cục bộ. Tôi đã thử một số nội dung nhúng và nomic-embed-text mang lại hiệu suất và chất lượng tốt cho các tài liệu kỹ thuật.

Tiếp theo là công cụ RAG để điều phối quy trình lập chỉ mục tài liệu, tạo nhúng, lưu trữ cơ sở dữ liệu vectơ và truy vấn. Không có nó, dù mô hình ngôn ngữ có nhanh đến đâu, chúng ta cũng không thể lấy được thông tin liên quan từ tài liệu. Hãy coi nó giống như mục lục của một cuốn sách: không có nó, bạn sẽ phải đọc toàn bộ cuốn sách để tìm được thông tin mình cần. Và với một chỉ mục tốt, bạn có thể vào thẳng trang bên phải. Tôi sẽ gọi quá trình này là lập chỉ mục để đơn giản, mặc dù nó thực sự là một quá trình vector hóa và lập chỉ mục.

Sau một số nghiên cứu, tôi đã tìm thấy một khung nguồn mở hoàn thiện có tên là LlamaIndex.

Ngôn ngữ tôi sẽ sử dụng là Python, tôi có thể liệt kê nhiều lý do, nhưng lý do quan trọng nhất là tôi cảm thấy thoải mái và làm việc hiệu quả với nó. Ngoài ra, cả Ollama và LlamaIndex đều có SDK Python tuyệt vời.

Tôi đã sẵn sàng bắt đầu xây dựng phần mềm. Tôi đã viết các tập lệnh đầu tiên của mình để chạy thử nghiệm vectơ trên hệ thống RAG và thực hiện một số thử nghiệm truy vấn. Nó hoạt động rất tốt với rất ít mã. Tôi nghĩ đó sẽ là một dự án kéo dài vài tuần. Tôi không thể nào sai hơn được nữa.

Bước tiếp theo là làm việc với các tài liệu thực tế. Hãy bám chặt nhé, chuyến đi sẽ gập ghềnh lắm đấy!

Vấn đề 2: sự hỗn loạn của tài liệu

Nguồn tệp của tôi là một thư mục trên Azure với số lượng lớn tài liệu kỹ thuật: hàng trăm gigabyte, hàng nghìn tệp, nhiều định dạng khác nhau, không có tổ chức hoặc cấu trúc nào ngoài hệ thống phân cấp thư mục. Giấc mơ của mọi kỹ sư dữ liệu (hãy lưu ý điều trớ trêu).

Tôi bẻ khớp ngón tay, đặt đầu ra RAG để lưu vào đĩa và khởi chạy tập lệnh đầu tiên của mình. LlamaIndex cuối cùng đã làm tràn RAM máy tính xách tay của tôi trong vòng vài phút, làm nghẹt hệ điều hành của tôi cho đến khi mọi thứ đóng băng. Tôi đã thử nhiều cấu hình, hệ thống bộ nhớ đệm và các chiến lược khác nhưng đến một lúc nào đó, máy của tôi luôn chết.

Sau khi gỡ lỗi, tôi phát hiện ra rằng nó đang xử lý các tệp lớn mà không đóng góp gì: video, mô phỏng, tệp sao lưu... Các tài liệu không bổ sung gì vào hệ thống RAG, nhưng LlamaIndex đã cố xử lý như thể chúng là văn bản. Nếu một tệp nặng vài gigabyte, hệ thống sẽ cố tải toàn bộ tệp đó vào bộ nhớ để xử lý, điều này là tự sát.

Tôi đã thêm một hệ thống lọc vào quy trình loại trừ các tệp theo phần mở rộng và theo mẫu tên (tệp mô phỏng, kết quả bằng số, v.v.).

Danh mục Tiện ích mở rộng bị loại trừ
Video mp4, avi, Mov, mkv, wmv, flv, webm, m4v, mpg, mpeg, 3gp, mts...
Hình ảnh jpg, jpeg, png, gif, bmp, tiff, svg, ico, webp, heic, psd...
Các tệp thực thi exe, dll, msi, bat, sh, app, dmg, so, jar...
Đã nén zip, rar, 7z, tar, gz, bz2, xz
Mô phỏng sim, dat
Tạm thời tmp, temp, bộ đệm, nhật ký, swp, pyc, crdownload, một phần...
Sao lưu bak, 3dmbak, dwgbak, dxfbak, pdfbak, stlbak, cũ, bkp, gốc...
Email tin nhắn, pst, eml, oft

Tôi cũng đã xóa các tệp tốn nhiều chi phí xử lý và không thêm giá trị, như CSV, JSON, cùng nhiều tệp khác. Mặt khác, tôi đã chuyển đổi các tệp PDF, DOCX, XLSX, PPTX, v.v. thành văn bản thuần túy để LlamaIndex có thể xử lý chúng mà không gặp vấn đề gì.

Kết quả là số lượng tệp cần lập chỉ mục giảm 54%. Và tất nhiên, RAM của tôi đã ngừng nổ.

Cuối cùng tôi cũng có thể bắt đầu lập chỉ mục mà không còn lo sợ.

Vấn đề 3: lập chỉ mục 451GB tài liệu mà không bị chết trong nỗ lực

RAG liên quan đến việc tạo tệp chỉ mục vectơ chứa các phần nhúng tài liệu. Vectơ là biểu diễn số của các tài liệu cho phép đo lường mức độ tương tự của chúng. LlamaIndex có một hệ thống đơn giản mà bạn có thể định cấu hình chỉ bằng một vài dòng. Bạn chỉ cần trỏ nó vào thư mục và nó sẽ đảm nhiệm việc lưu trữ tất cả thông tin bên trong ở định dạng JSON. Nó thực sự tiện lợi, hoạt động tốt, trừ khi bạn đang xử lý hàng trăm gigabyte tài liệu. Hệ thống trở nên không thể quản lý được: mỗi khi dịch vụ khởi động lại, nó phải xử lý lại tất cả tài liệu từ đầu, việc này có thể mất nhiều ngày. Ngoài ra, định dạng mặc định không tối ưu cho các tìm kiếm lớn (JSON).

Tôi đã thêm hệ thống điểm kiểm tra để lưu tiến trình lập chỉ mục. Mỗi khi xảy ra sự cố, tôi sẽ không mất tất cả tiến trình mà có thể tiếp tục từ tệp được xử lý cuối cùng. Tuy nhiên, dữ liệu bị hỏng, dễ bị lỗi và rất chậm. Tôi đang phải đối mặt với một nút thắt mà tôi không thể vượt qua.

Sau nhiều thử nghiệm và sai sót cũng như đọc thêm về nó, tôi quyết định chuyển sang cơ sở dữ liệu vectơ chuyên dụng: ChromaDB. Cơ sở dữ liệu nguồn mở (giấy phép Apache-2.0) để lưu trữ và truy vấn vectơ. Đừng nhầm lẫn với trình duyệt Chrome/Chromium. ChromaDB là một lớp trừu tượng lưu trữ trên cơ sở dữ liệu truyền thống, tôi đã định cấu hình SQLite và cung cấp các chức năng cụ thể như tìm kiếm tương tự, phân cụm, v.v.

Sự thay đổi diễn ra triệt để và ngay lập tức. Lập chỉ mục đã chuyển từ một quy trình nguyên khối tải mọi thứ vào bộ nhớ sang một đường dẫn hàng loạt xử lý 150 tệp cùng một lúc, tạo các phần nhúng của chúng và lưu trữ chúng trực tiếp trong ChromaDB. Điều này cho phép lập chỉ mục 451GB tài liệu qua nhiều phiên, có điểm kiểm tra mà không làm mất tiến trình khi bị gián đoạn và không có dữ liệu bị hỏng. Ngoài ra, việc sao lưu và khôi phục chỉ mục trong trường hợp bị lỗi thực sự rất dễ dàng (chỉ cần sao chép tệp SQLite).

Hệ thống đã sẵn sàng. Với điểm chuẩn nhanh, tôi phát hiện ra rằng mình sẽ cần vài tháng để lập chỉ mục tất cả nội dung trên máy tính xách tay của mình. Bây giờ nút thắt cổ chai không phải là RAM, hệ thống lập chỉ mục, cũng không phải các tệp mà là GPU.

Vấn đề 4: card đồ họa của tôi không phải là tên lửa

Máy tính xách tay của tôi có card đồ họa tích hợp. Việc xử lý 500 MB tài liệu bằng CPU mất 4-5 giờ, con số không phải là tốt. Tôi thực sự cần một GPU mạnh mẽ. Trong một cuộc họp tiếp theo, tôi quyết định thuê cho tôi một máy ảo có NVIDIA RTX 4000 SFF Ada, có 20GB VRAM. Những loại cho thuê này không hẳn là rẻ. Bây giờ tôi phải làm việc dưới nhiều áp lực hơn.

Tôi đã sửa đổi vùng chứa của mình và hệ thống đã được tối ưu hóa để tận dụng GPU. Tôi đã đưa ra kịch bản của mình. Sau vài tuần, từ thứ 2 đến thứ 3, quá trình lập chỉ mục đã kết thúc mà không gặp lỗi nào. 738.470 vectơ, 54GB chỉ mục trong ChromaDB và hệ thống RAG sẵn sàng trả lời các câu hỏi. Tôi đã sao chép cơ sở dữ liệu ChromaDB, một tệp SQLite, vào máy cục bộ của mình và thế là xong. Trước sự nhẹ nhõm của quản trị viên hệ thống và Người quản lý dự án, cuối cùng chúng tôi cũng có thể tắt máy ảo. Chi phí trên Hetzner là 184 euro, không hề rẻ.

Đã đến lúc xây dựng phần phụ trợ và giao diện người dùng.

Vấn đề 5: trải nghiệm người dùng

Với Flask, tôi đã xây dựng một API đơn giản để truy cập LlamaIndex, từ đó truy vấn ChromaDB và Ollama.

Tôi khá yêu thích Streamlit trong việc xây dựng các loại dự án nội bộ, vì vậy đây sẽ là giao diện người dùng của tôi (và tôi sẽ tiếp tục sử dụng Python). Nó cũng có một tiện ích gốc dành cho Hỏi đáp, theo kiểu bất kỳ cuộc trò chuyện hiện tại nào để tương tác với AI.

Trong vài giờ, tôi đã hoàn thành toàn bộ phần hình ảnh. Phần còn lại là các chi tiết cần trau chuốt: hiển thị logo công ty, một vòng quay trong khi xử lý truy vấn, lưu phiên, v.v.

Sơ đồ về cách các thành phần hệ thống khác nhau giao tiếp như sau:

flowchart TD
    U["👤 User"]:::user --> E["Streamlit (Web UI)"]:::web
    E <-->|HTTP| D["Flask API"]:::api
    D --> F["Python Backend"]:::backend
    F <--> C["Ollama (LLM + Embeddings)"]:::llm
    C <--> B["RAG (LlamaIndex)"]:::rag
    B <--> G["ChromaDB"]:::chroma
    classDef user fill:#37474F,stroke:#263238,stroke-width:2px,color:#fff
    classDef web fill:#8E24AA,stroke:#6A1B9A,stroke-width:2px,color:#fff
    classDef api fill:#D32F2F,stroke:#B71C1C,stroke-width:2px,color:#fff
    classDef backend fill:#00897B,stroke:#00695C,stroke-width:2px,color:#fff
    classDef rag fill:#7CB342,stroke:#558B2F,stroke-width:2px,color:#fff
    classDef chroma fill:#4CAF50,stroke:#388E3C,stroke-width:2px,color:#fff
    classDef llm fill:#FF6F00,stroke:#E65100,stroke-width:2px,color:#fff

Tôi đã điều chỉnh mẫu về cách định dạng phản hồi LLM. Đối với mỗi câu trả lời, hệ thống phải hiển thị các nguồn thông tin, tức là các tài liệu được sử dụng để tạo ra câu trả lời.

Câu hỏi: Công ty đã thực hiện những dự án trang trại gió nào? Trả lời: Một số dự án liên quan đến trang trại gió đã được thực hiện, bao gồm:
- Dự án A: Phân tích khả thi cho một trang trại gió ngoài khơi. Tham khảo: https://.../project_a_wind_farm_report.pdf
- Dự án B: Mô phỏng tuabin gió sử dụng OrcaFlex. Tham khảo: https://.../project_b_wind_farm_simulation.sim

Nhưng tất nhiên, tôi cần lưu trữ tất cả dữ liệu gốc trên đĩa, cùng với cơ sở dữ liệu vectơ, LLM và phần phụ trợ. Và tôi không có chỗ cho nó. Môi trường sản xuất của tôi là một máy ảo rất hạn chế về tài nguyên và thậm chí còn hơn thế nữa về dung lượng ổ đĩa (100 GB). Tôi không đủ khả năng để có nửa terabyte tài liệu trên máy chủ.

Vấn đề 6: phân phối tài liệu mà không làm đầy đĩa

Chúng ta phải nhớ rằng việc có cơ sở dữ liệu vectơ không có nghĩa là chúng ta có thể làm được nếu không có tài liệu gốc. Tuy nhiên, tôi có thể có chỉ mục vectơ với phần nhúng tài liệu ở một bên và tài liệu gốc ở nơi khác (dù là trên cùng một máy chủ hay trên đám mây).

Giải pháp là cung cấp tài liệu gốc trực tiếp từ Bộ lưu trữ Azure Blob (điều này có thể thực hiện được với bất kỳ hệ thống nào khác). Đối với mỗi tài liệu được trích dẫn trong phản hồi, hệ thống sẽ tạo liên kết tải xuống có mã thông báo SAS cho phép người dùng tải xuống trực tiếp từ đám mây.

%%{init: {'theme':'default'}}%%
sơ đồ LR
    U["👤 Người dùng"]:::người dùng -->|Câu hỏi| S["Máy chủ (VM)"]:::máy chủ
    S -->|Phản hồi + liên kết| bạn
    U -->|Tải xuống trực tiếp bằng mã thông báo SAS| A["Bộ lưu trữ Azure Blob
(451 GB tài liệu)"]:::azure Người dùng classDef điền:#37474F,đột quỵ:#263238,độ rộng nét:2px,màu:#fff Máy chủ classDef điền:#00897B,đột quỵ:#00695C,độ rộng nét:2px,màu:#fff classDef azure fill:#0078D4,đột quỵ:#005A9E,độ rộng nét:2px,color:#fff

Dung lượng ổ đĩa thực sự cần thiết là chỉ mục vectơ ChromaDB, ở mức 54 GB hoàn toàn có thể quản lý được trên đĩa cục bộ, LLM, chiếm khoảng 10 GB, phần phụ trợ (vài megabyte) và giao diện người dùng (vài megabyte khác). Các tài liệu còn lại vẫn ở Azure, có thể truy cập theo yêu cầu.

Kiến trúc cuối cùng

Cấu trúc hệ thống cuối cùng trông như thế này:

flowchart LR
    A["Azure Blob Storage"]:::azure -- Documents --> B["RAG (LlamaIndex)"]:::rag
    B <--> G["ChromaDB"]:::chroma
    B <--> C["Ollama (LLM + Embeddings)"]:::llm
    D["Flask API"]:::api <-- HTTP --> E["Streamlit (Web UI)"]:::web
    C <--> F["Python Backend"]:::backend
    D -- Call --> F
    classDef azure fill:#0078D4,stroke:#005A9E,stroke-width:2px,color:#fff
    classDef rag fill:#7CB342,stroke:#558B2F,stroke-width:2px,color:#fff
    classDef chroma fill:#4CAF50,stroke:#388E3C,stroke-width:2px,color:#fff
    classDef llm fill:#FF6F00,stroke:#E65100,stroke-width:2px,color:#fff
    classDef api fill:#D32F2F,stroke:#B71C1C,stroke-width:2px,color:#fff
    classDef web fill:#8E24AA,stroke:#6A1B9A,stroke-width:2px,color:#fff
    classDef backend fill:#00897B,stroke:#00695C,stroke-width:2px,color:#fff
Lớp Công nghệ Mục đích
LLM Ollama + llama3.2:3b Tạo phản hồi cục bộ
Phần nhúng văn bản nhúng-nomic Vectơ hóa tài liệu
Cơ sở dữ liệu vectơ ChromaDB (HNSW) Lưu trữ và tìm kiếm tương tự
Khung RAG Chỉ mục Llama Điều phối đường ống RAG
API Bình + Gunicorn Dịch vụ HTTP REST
Giao diện người dùng web Sáng sủa Giao diện hội thoại
Vùng chứa Soạn Docker Điều phối dịch vụ
GPU Bộ công cụ bộ chứa NVIDIA Tăng tốc phần cứng
Dịch vụ lưu trữ Bộ nhớ Azure Blob Tính bền bỉ của đám mây

Bài học kinh nghiệm

  • Quản lý bộ nhớ: Việc tải tất cả tài liệu vào bộ nhớ và xử lý chúng cùng một lúc là rất nguy hiểm. Triển khai xử lý hàng loạt, tôi đã sử dụng 150 mỗi lần lặp, với các lệnh gọi trình thu gom rác rõ ràng giữa các lô. Mỗi lô được xử lý, nhúng, lưu trữ trong ChromaDB và bộ nhớ được giải phóng trước khi tiếp tục.
  • Các tệp có vấn đề: Ngay cả sau các lớp lọc, một số tệp vẫn vượt qua được nhưng không thành công trong quá trình phân tích cú pháp, như các tệp PDF bị hỏng, tài liệu Word có macro bị hỏng, bảng tính có định dạng không mong muốn... Chiến lược này là khả năng chịu lỗi. Nếu một tệp bị lỗi, nó sẽ được ghi lại và chúng tôi chuyển sang tệp tiếp theo. Tệp có vấn đề không bao giờ dừng toàn bộ lô. Sau đó, tôi có thể xem xét, tải xuống hoặc gán nó cho đợt khác theo cách thủ công.
  • Điểm kiểm tra: Mỗi lô có thể mất hàng giờ, mất điện hoặc khởi động lại không có nghĩa là bắt đầu từ con số 0. Triển khai hệ thống điểm kiểm tra để lưu lô hoàn thành cuối cùng, tổng số nút được xử lý và dấu thời gian. Khi khởi động lại, việc lập chỉ mục sẽ tiếp tục chính xác ở nơi nó đã dừng lại.
  • Giám sát: Thêm tập lệnh giám sát và thông tin cho các trạng thái khác nhau: index-progress, index-watch, index-speed, index-checkpoint, index-failed... Khi RAG hoạt động hàng giờ, bạn cần biết chuyện gì đang xảy ra.

Kết luận

Đây không phải là một hệ thống hoàn hảo nhưng cũng đủ. Sẽ thật tuyệt nếu tôi có thể khởi chạy một phiên bản OrcaFlex để LLM có thể chạy các dự án hoặc thực hiện các mô phỏng của riêng nó theo yêu cầu. Nhưng điều đó đòi hỏi nhiều thời gian và nguồn lực hơn mức có thể được cung cấp. Tuy nhiên, tôi rất hài lòng với kết quả cuối cùng. Hệ thống này nhanh, đáng tin cậy và trên hết là hữu ích cho đồng nghiệp của tôi.

Lời khuyên khiêm tốn của tôi, nếu bạn đang cân nhắc xây dựng thứ gì đó tương tự: hãy dành thời gian xây dựng dữ liệu tốt nhất có thể. Nếu nguồn không đủ liên quan thì LLM sẽ không thể tạo ra câu trả lời hay.

Tác giả: andros

#discussion