Hầu hết các nguyên mẫu RAG đều trông giống hệt nhau về bản chất. Ai đó nạp một tệp PDF vào một pipeline, chia nhỏ văn bản thành các đoạn (chunk) 512 token gọn gàng, đẩy chúng vào một cơ sở dữ liệu vector, và coi như xong việc. Đối với một bản demo trình diễn, điều này có vẻ ấn tượng. Nhưng trong môi trường thực tế, nó sẽ sụp đổ.

Một đoạn cố định không quan tâm nó đang cắt ở đâu. Nó sẽ chia đôi một hợp đồng pháp lý ngay giữa một điều khoản bồi thường. Nó sẽ nhồi nhét năm endpoint API không liên quan vào cùng một cửa sổ ngữ cảnh và khiến mô hình bị ngập trong nhiễu. Nó sẽ buộc bạn phải truy xuất nhiều mảnh vụn hơn mức cần thiết, làm tăng độ trễ và tiêu tốn token. Kết quả là những câu trả lời nửa vời, hiện tượng ảo giác và sự thất vọng của người dùng.

Chúng tôi đã đập đi xây lại toàn bộ lớp truy xuất của mình từ đầu. Kết quả là một hệ thống đạt độ triệu hồi (recall) 95% trong khi giảm được 40% độ trễ. Đây chính xác là cách chúng tôi đã làm.

Tại sao các đoạn cố định lại thất bại trong môi trường thực tế

Con số mặc định 512 token không phải là một lựa chọn thiết kế. Nó là hệ quả tất yếu từ cửa sổ ngữ cảnh của các mô hình embedding đời đầu và các thiết lập mặc định của các thư viện. Nó dễ triển khai nhưng sẽ là thảm họa nếu bạn quá phụ thuộc vào nó.

Tài liệu không đồng nhất. Một điều khoản pháp lý có thể kéo dài tới bảy trăm token mà không có điểm ngắt rõ ràng. Nếu bạn cắt ở mức năm trăm mười hai, bạn sẽ tạo ra hai mảnh vụn rời rạc. Khi một luật sư hoặc nhân viên tuân thủ hỏi về giới hạn trách nhiệm pháp lý, hệ thống chỉ trả về một nửa nghĩa vụ. Mô hình ngôn ngữ sẽ tự "ảo giác" ra nửa còn thiếu, hoặc tệ hơn, phủ nhận sự tồn tại của giới hạn đó.

Tài liệu API lại gặp phải vấn đề ngược lại. Một đoạn 500 token có thể nuốt trọn cả một module: các header xác thực, mã lỗi, giới hạn tốc độ (rate limits) và các webhook schema. Khi một nhà phát triển hỏi cách xử lý AUTH_4027, bộ truy xuất lại đưa ra một mớ hỗn độn các hàm không liên quan. Mô hình không còn cách nào khác ngoài việc trung bình hóa chúng thành một mớ thông tin chung chung vô nghĩa.

Chia đoạn kém cũng làm tăng độ trễ. Các mảnh vụn yếu kém đồng nghĩa với việc bạn cần một giá trị top-k lớn hơn để bao quát một chủ đề. Nhiều đoạn hơn đồng nghĩa với prompt dài hơn. Prompt dài hơn đồng nghĩa với việc tạo phản hồi chậm hơn và hóa đơn chi phí cao hơn. Trải nghiệm người dùng bị hủy hoại bởi vô số lỗi nhỏ tích tụ.

Điều chỉnh đoạn (chunk) phù hợp với tài liệu

Chúng tôi đã ngừng việc đếm token và bắt đầu đọc nội dung tài liệu. Chiến lược chia đoạn phù hợp phụ thuộc vào cấu trúc của nguồn dữ liệu.

Tài liệu pháp lý cần phương pháp chia đoạn theo ký tự đệ quy (recursive character chunking) với các ranh giới nhận biết điều khoản. Bộ chia sẽ tôn trọng cấu trúc phân cấp: nó tìm kiếm các tiêu đề mục trước, sau đó đến các đoạn được đánh số, và cuối cùng là các điểm ngắt câu tự nhiên. Nó không bao giờ cắt ngang một điều khoản phụ hoặc chia tách một cụm từ mang tính nghĩa vụ giữa các đoạn. Khi bạn truy xuất một đoạn văn về bồi thường, bạn sẽ nhận được toàn bộ điều khoản, giới hạn và các trường hợp ngoại lệ.

Tài liệu API đòi hỏi việc chia đoạn nhận biết cấu trúc (structure-aware chunking). Chúng tôi phân tích theo định nghĩa hàm, chứ không phải theo ngân sách token. Mỗi đoạn chứa một chữ ký hàm (function signature) hoàn chỉnh, các mô tả tham số của nó và các ghi chú xử lý lỗi liền kề. Nếu một nhà phát triển tìm kiếm một phương thức cụ thể, họ sẽ nhận được toàn bộ hợp đồng, chứ không phải một mảnh vụn bị cắt bởi một ranh giới tùy tiện.

Ticket hỗ trợ thường nhiễu và phi tuyến tính. Một luồng hội thoại có thể bắt đầu bằng một báo cáo lỗi, đưa ra một giải pháp tạm thời và kết thúc bằng một ghi chú chuyển cấp nội bộ. Chia đoạn theo ngữ nghĩa (semantic chunking) sẽ phát hiện sự thay đổi chủ đề bằng cách đo lường độ tương đồng embedding giữa các câu. Chúng tôi chỉ cho phép ngắt đoạn tại các ranh giới chủ đề tự nhiên, để một cuộc hội thoại về lỗi đăng nhập không bị lẫn với một cuộc trao đổi tiếp theo về chu kỳ thanh toán.

Wiki là phần khó nhất. Chúng lan tỏa, liên kết chéo và tổ chức lỏng lẻo. Chúng tôi đã sử dụng phương pháp chia đoạn bằng agent (agentic chunking), trong đó một LLM nhẹ sẽ đọc trang và quyết định các điểm ngắt dựa trên tính mạch lạc của chủ đề. Phương pháp này tốn thêm một chút chi phí tại thời điểm nạp dữ liệu (ingestion time), nhưng các đoạn tạo ra sẽ độc lập và sẵn sàng cho việc truy xuất. Một trang về các thực hành tốt nhất khi triển khai sẽ được chia thành các đơn vị logic: kiểm tra trước khi triển khai, quy trình rollback và thiết lập giám sát, thay vì là các khối văn bản tùy tiện.

Truy xuất hỗn hợp: Kết hợp Từ khóa và Vector

Tìm kiếm vector dày đặc (dense vector search) hiểu được ý nghĩa. Nhưng nó rất tệ trong việc tìm các chuỗi ký tự chính xác. Nếu người dùng tìm kiếm một mã lỗi chính xác như AUTH_4027 hoặc tên khách hàng như "Stark Industries", các embedding vector có thể bỏ lỡ mục tiêu vì chúng tối ưu hóa cho sự gần gũi về mặt khái niệm chứ không phải độ chính xác ở cấp độ ký tự.

Tìm kiếm từ khóa thuần túy thông qua BM25 lại có nhược điểm ngược lại. Nó sẽ tìm thấy AUTH_4027 một cách hoàn hảo, nhưng nó sẽ bỏ lỡ mối liên kết về mặt khái niệm giữa "lỗi xác thực" (authorization failure) và "đăng nhập bị từ chối" (login denied).

We run both in parallel. BM25 and vector search operate independently over the same corpus. Their result lists are merged using Reciprocal Rank Fusion, which reorders candidates by balancing their positional ranks. You do not need calibrated weights. You simply get the precision of exact match and the intuition of semantic search in a single ranked list.

Then we add a cross-encoder reranker. This is a separate model that scores each passage against the original query, producing a relevance signal far finer than either retriever alone. It adds about 50 milliseconds of latency. It increases recall by 15 percent. If you care about answer quality, that trade is non-negotiable.

Query Expansion: Fix the Search Before It Starts

Bad queries are the dirty secret of every retrieval system. Users do not write like your embedding space. They type "it broke." They paste truncated stack traces. They use internal jargon your index has never seen.

We transform the query before it ever touches the index. First, we expand a single query into three to five diverse search terms. If the original is "payment failed," we also search for "transaction error," "billing declined," and "charge unsuccessful