Chuyển đến nội dung chính

Chia nhỏ tài liệu cho truy hồi: quyết định âm thầm chặn trần chất lượng RAG

Một hệ thống truy hồi trả lời đúng câu “thời hạn hoàn tiền của chúng ta là bao lâu?” nhưng thất bại với câu “thời hạn hoàn tiền có khác với khách doanh nghiệp không?” thường không có vấn đề về mô hình. Nó có một đoạn chứa chính sách chung và một đoạn khác, không bao giờ được truy hồi, chứa ngoại lệ.

Việc chia đoạn quyết định một đơn vị truy hồi là gì. Làm sai và không lượng rerank, kỹ thuật prompt hay nâng cấp mô hình nào cứu lại được thông tin bạn đã cắt rời.

Chia theo kích thước cố định là mốc so sánh, không phải chiến lược

Cách mặc định ai cũng bắt đầu — cắt mỗi N ký tự với M ký tự chồng lấn — đáng được hiểu chính xác, vì đó là thứ bạn sẽ đem ra so sánh.

def fixed_chunks(text, size=1000, overlap=200):
    chunks, start = [], 0
    while start < len(text):
        chunks.append(text[start:start + size])
        start += size - overlap
    return chunks

Nó làm đúng chỗ nào: đoạn đều nhau, chi phí đoán được, không giả định gì về định dạng tài liệu.

Nó làm sai chỗ nào: nó cắt ngang câu, bảng, khối mã và — tệ nhất — cắt đứt quan hệ giữa một tiêu đề với phần văn bản bên dưới. Một đoạn bắt đầu giữa câu mà không có dấu hiệu nó đến từ mục nào là một đoạn nhúng kém và đọc cũng dở khi bộ sinh nhìn thấy nó.

Hai cải tiến tức thì, đều rẻ:

  • Chia theo cấu trúc trước, kích thước sau. Cắt ở tiêu đề, rồi đoạn văn, rồi câu, và chỉ lùi về đếm ký tự khi một đơn vị vượt ngân sách. Đa số thư viện splitter gọi đây là chia đệ quy và nó nên là sàn của bạn chứ không phải trần.
  • Đo bằng token, không phải ký tự. Giới hạn 1.000 ký tự tương đương khoảng 250 token tiếng Anh và ít hơn khá nhiều với tiếng Việt hoặc với mã nguồn. Nếu bạn đo bằng ký tự thì bạn đang đo bằng đơn vị mà cả bộ nhúng lẫn bộ sinh đều không dùng.

Chồng lấn mang lại ít hơn người ta tưởng

Chồng lấn tồn tại để tránh việc một câu trả lời bị cắt đứt ở ranh giới. Nó có tác dụng, nhưng là công cụ thô: 200 token chồng lấn trên đoạn 1.000 token nghĩa là nhiều hơn 20% vector, 20% lưu trữ, 20% chi phí tìm kiếm, và những kết quả gần trùng nhau chen chỗ trong top k của bạn.

Tôi dùng 10-15% chồng lấn cho văn xuôi và bằng không cho nội dung chia theo cấu trúc, nơi ranh giới vốn đã có ý nghĩa. Nếu chồng lấn đang gánh nhiều việc trong hệ thống của bạn, đó là bằng chứng rằng cách chia đang cắt nhầm chỗ, và cách sửa là ranh giới tốt hơn chứ không phải nhiều dư thừa hơn.

Thay đổi giá trị nhất: cho mỗi đoạn ngữ cảnh của nó

Một đoạn bị tách rời sẽ mất hết những gì tài liệu xung quanh đã thiết lập. “Điều này chỉ áp dụng cho tài khoản tạo trước đợt di chuyển dữ liệu” chẳng có nghĩa gì nếu không biết “điều này” là điều gì.

Cách sửa là gắn ngữ cảnh vào trước văn bản mà bạn đem đi nhúng:

def contextualise(chunk, doc_title, section_path, doc_date):
    header = f"Tài liệu: {doc_title}\nMục: {' > '.join(section_path)}\nCập nhật: {doc_date}\n\n"
    return header + chunk

Phần đầu đề đó được nhúng cùng với đoạn, nên một truy vấn có nhắc tên sản phẩm hay chủ đề của mục giờ khớp được với những đoạn không hề viết ra điều đó. Theo kinh nghiệm của tôi, đây là cải tiến lớn nhất từ một thay đổi đơn lẻ mà đa số hệ thống RAG có thể thực hiện, và nó tốn đúng một phép nối chuỗi lúc dựng chỉ mục.

Phiên bản kỹ hơn sẽ sinh một câu tóm tắt về việc mỗi đoạn nằm ở đâu trong tài liệu rồi gắn vào trước. Nó tốn một lời gọi mô hình cho mỗi đoạn lúc dựng chỉ mục, tức là tiền thật với kho lớn, nhưng nó xử lý được những ca mà một đầu đề tĩnh không xử lý nổi — tham chiếu ngầm, đại từ, sự nối tiếp từ mục trước.

Dù theo cách nào, hãy giữ đoạn thô tách biệt với văn bản đem nhúng, để bộ sinh nhận được nội dung sạch chứ không phải phần đầu đề soạn sẵn của bạn.

Đoạn nhỏ để truy hồi, đoạn lớn để trả lời

Hai mục tiêu này xung đột trực tiếp. Đoạn nhỏ nhúng chính xác và khớp với câu hỏi cụ thể; đoạn lớn cho bộ sinh đủ chất liệu xung quanh để thật sự trả lời được.

Truy hồi tài liệu cha hoá giải xung đột: đánh chỉ mục cái nhỏ, trả về cái lớn.

# Dựng chỉ mục: chia thành các đoạn con ~200 token, mỗi cái trỏ về cha của nó
for parent in documents:
    for child in split(parent, size=200):
        index.add(embed(contextualise(child)), metadata={"parent_id": parent.id})

# Truy vấn: tìm trên đoạn con, khử trùng lặp, lấy về tài liệu cha
hits = index.search(embed(query), k=10)
parent_ids = dict.fromkeys(h.metadata["parent_id"] for h in hits)  # giữ thứ tự, không trùng
context = [store.get(pid) for pid in list(parent_ids)[:3]]

Việc khử trùng lặp rất quan trọng: nhiều đoạn con của cùng một cha sẽ thường cùng khớp, và thiếu nó thì ba “kết quả” của bạn hoá ra là một tài liệu lặp lại ba lần.

Một mẹo họ hàng, rẻ hơn: truy hồi đoạn nhỏ rồi mở rộng mỗi đoạn để bao gồm các đoạn liền kề trước khi đưa cho bộ sinh. Kém chính xác hơn truy hồi tài liệu cha thật sự, nhưng không cần kho thứ hai.

Nội dung không chia được

Một số nội dung phá vỡ mọi splitter tổng quát:

  • Bảng biểu. Cắt một bảng là các dòng mất tiêu đề cột. Hãy giữ nguyên bảng khi nó vừa, và khi không vừa thì lặp lại dòng tiêu đề trong từng mảnh và thêm một chú thích mô tả chủ đề của bảng.
  • Mã nguồn. Chia theo hàm hoặc lớp, không bao giờ theo số dòng. Một mảnh thân hàm gần như vô dụng trong truy hồi.
  • Hội thoại và ticket. Chia theo ranh giới lượt nói, và giữ phần giải quyết dính với phần mô tả vấn đề — một đoạn chỉ chứa lời phàn nàn sẽ được truy hồi đúng truy vấn nhưng không trả lời được câu nào.
  • Danh mục tham chiếu và bảng thuật ngữ dài. Mỗi mục là một đoạn riêng. Đây là trường hợp duy nhất mà đoạn rất nhỏ là hoàn toàn đúng.
  • PDF nhiều cột hoặc trang quét. Hãy sửa khâu trích xuất trước khi nghĩ tới chia đoạn; văn bản sai thứ tự đọc thì không splitter nào cứu được.

Đo xem thay đổi có hiệu quả không

Thay đổi cách chia đoạn thì dễ làm và cũng dễ tự lừa mình. Kỷ luật cần có:

  1. Giữ một bộ đánh giá cố định gồm các truy vấn với tài liệu nguồn đúng đã biết.
  2. Đổi một thứ.
  3. Đo recall@k trên cùng bộ đó, với cùng mô hình nhúng và cùng k.
  4. Đọc từng cái trong mười thất bại tệ nhất. Chỉ số tổng hợp cho bạn biết nó có nhúc nhích không; chỉ việc đọc thất bại mới cho biết vì sao.

Chia lại đoạn nghĩa là nhúng lại, nên hãy dựng đường ống với giả định rằng bạn sẽ làm việc đó nhiều lần: giữ văn bản gốc trong kho của chính bạn, đánh phiên bản cấu hình chia đoạn cạnh các vector, và biến việc dựng lại toàn bộ thành một câu lệnh duy nhất. Một nhóm không thể chia lại đoạn với chi phí thấp sẽ ngừng thử nghiệm, và ngừng cải thiện.

Câu hỏi thường gặp

Nên bắt đầu với kích thước đoạn bao nhiêu?

Khoảng 200-400 token cho đoạn con trong thiết lập tài liệu cha, hoặc 500-800 token cho chỉ mục phẳng. Rồi đo — câu trả lời đúng phụ thuộc vào tài liệu và câu hỏi của bạn, không phụ thuộc vào một khuyến nghị chung.

Chia đoạn ngữ nghĩa — cắt ở chỗ độ tương đồng embedding tụt xuống — có đáng không?

Đôi khi, với văn xuôi không cấu trúc và không có tiêu đề. Với tài liệu có cấu trúc thật, chia theo cấu trúc rẻ hơn và thường tốt hơn.

Siêu dữ liệu của đoạn nên nhúng hay lưu riêng?

Cả hai. Hãy nhúng phần đầu đề ngữ cảnh để nó ảnh hưởng tới việc khớp; lưu các trường có cấu trúc riêng để bạn lọc được trước khi tìm bằng vector.

Xử lý tài liệu hay thay đổi thế nào?

Hãy chia đoạn một cách tất định để những mục không đổi sinh ra ID đoạn giống hệt, rồi chỉ nhúng lại phần đã đổi. Băm nội dung từng đoạn khiến việc này đơn giản.

Mô hình ngữ cảnh dài có xoá bỏ nhu cầu chia đoạn không?

Không. Bạn vẫn phải chọn cái gì đi vào cửa sổ, và chất lượng truy hồi — chứ không phải kích thước cửa sổ — mới là thứ quyết định đoạn đúng có ở đó hay không.


Hành vi của splitter, cách chia đệ quy và chia theo cấu trúc, cùng kỹ thuật truy hồi theo ngữ cảnh được mô tả trong các tài liệu framework và bài kỹ thuật liên kết bên trên. Các tỉ lệ chồng lấn, kích thước cho truy hồi tài liệu cha, hướng dẫn theo loại nội dung và kỷ luật đo đạc là đánh giá riêng của tôi từ việc xây các hệ thống truy hồi; con số đúng cho kho tài liệu của bạn chỉ có thể đến từ việc đo trên chính nó.


Bài viết gốc đăng tại FlutterCook. Bản trên đó là bản được cập nhật mới nhất.

Nhận xét

Bài đăng phổ biến từ blog này

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

Announcing Flutter 2

  Phụ lục: Flutter on the web Flutter 2 on desktops, foldables, and embedded devices The growing Flutter ecosystem Dart: The secret sauce behind Flutter Flutter 2: Available now Hôm nay, chúng tôi sẽ công bố Flutter 2: một bản nâng cấp lớn cho Flutter cho phép các nhà phát triển tạo các ứng dụng đẹp, nhanh chóng và di động cho bất kỳ nền tảng nào. Với Flutter 2, bạn có thể sử dụng cùng một cơ sở mã để gửi các ứng dụng gốc cho năm hệ điều hành: IOS, Android, Windows, macOS và Linux; cũng như trải nghiệm web nhắm mục tiêu các trình duyệt như Chrome, Firefox, Safari hoặc Edge. Flutter thậm chí có thể được nhúng vào ô tô, TV và thiết bị gia dụng thông minh, mang đến trải nghiệm di động và lan tỏa nhất cho thế giới điện toán xung quanh. Mục tiêu của chúng tôi là thay đổi cơ bản cách các nhà phát triển nghĩ về việc xây dựng ứng dụng, bắt đầu không phải với nền tảng bạn đang nhắm mục tiêu mà là với trải nghiệm bạn muốn tạo. Flutter cho phép bạn tạo ra những trải nghiệm tuyệt đẹp trong đó ...