اکثر دموهای RAG روی یک لپ‌تاپ فوق‌العاده به نظر می‌رسند. یک فایل PDF بیست صفحه‌ای به اسکریپت بدهید، سوالی بپرسید و تماشا کنید که چگونه به پاراگراف درست ارجاع می‌دهد. اما انتقال همان خط لوله (pipeline) به محیط عملیاتی (production)، جایی است که آن جذابیت تمام می‌شود. اسناد حقوقی در سطح جمله، از وسط نصف می‌شوند. مراجع طولانی API، سیگنال‌های مهم را در میان نویزهای کدهای تکراری (boilerplate) غرق می‌کنند. تأخیر (latency) به شدت بالا می‌رود. کاربران منتظر می‌مانند، کلافه می‌شوند و صفحه را می‌بندند. ما با این چالش سخت روبرو شدیم. بنابراین، لایه بازیابی (retrieval layer) را از پایه کنده و آن را به عنوان یک سیستم دقیق و قابل تنظیم بازسازی کردیم. نتیجه، خط لوله‌ای بود که بدون تبدیل تجربه کاربری به یک نمایش اسلاید (کند)، به ۹۵٪ فراخوانی (recall) دست یافت.

چرا دموهای RAG در محیط عملیاتی شکست می‌خورند

پشته (stack) استاندارد در پروژه‌های تفننی و محصولات مراحل اولیه، به طرز غافلگیرکننده‌ای یکسان است: تکه‌های (chunks) ثابت بر اساس توکن، امبدینگ‌های (embeddings) آماده و یک فراخوانی جستجوی برداری (vector search) واحد. این سادگی وسوسه‌انگیز است و زمانی که مجموعه داده (corpus) شما تمیز، کوچک و از نظر نحو (syntax) قابل پیش‌بینی باشد، کار می‌کند. اما داده‌های محیط عملیاتی هیچ‌کدام از این ویژگی‌ها را ندارند. یک تکه ثابت ۵۱۲ توکنی، با خوشحالی از وسط یک بندِ غرامت (indemnification clause) در یک قرارداد SaaS عبور می‌کند. ناگهان لایه بازیابی شما، تنها نیمی از یک تعهد حقوقی را به مدل زبانی می‌دهد و از آن می‌خواهد به سوالی درباره مسئولیت (liability) پاسخ دهد. مدل دچار توهم (hallucination) می‌شود، زیرا بافتار (context) از هم گسیخته است.

اسناد فنی حجیم، این مشکل را تشدید می‌کنند. مستندات API پر از امضای توابع (function signatures)، جداول و بلوک‌های کد است. یک پنجره ثابت ممکن است وسط یک اینترفیس TypeScript را در بر بگیرد، اما نام تابع در بالای آن و مثال استفاده در پایین آن را از دست بدهد. در نهایت، بردار امبدینگ به جای نشان دادن قابلیت واقعی که کاربر درباره آن سوال می‌پرسد، قطعات نحو و نویزهای درون‌متنی را نمایش می‌دهد. ورودی بی‌کیفیت، خروجی توهم‌آمیز.

تکه‌بندی بر اساس ساختار، نه تعداد توکن

اولین تغییری که ایجاد کردیم این بود که دیگر به تکه‌ها (chunks) به عنوان کیسه‌ای از توکن‌ها نگاه نکنیم. تکه‌ها واحدهای معنایی هستند. استراتژی درست کاملاً به آنچه ایندکس می‌کنید بستگی دارد.

برای اسناد حقوقی، ما به سمت تکه‌بندی بازگشتی (recursive chunking) رفتیم که به سلسله‌مراتب سند احترام می‌گذارد. این روش، بخش‌ها، زیربخش‌ها و بندها را به عنوان مرز در نظر می‌گیرد. یک بند دست‌نخورده باقی می‌ماند، زیرا یک بند خود یک واحد معنایی است. اگر آن را از وسط برش دهید، منطق حقوقی از بین می‌رود.

برای مستندات API، تکه‌بندیِ ساختار-آگاه (structure-aware chunking)، توابع، کلاس‌ها و نقاط پایانی (endpoints) را به عنوان واحدهای اتمیک در نظر می‌گیرد. یک تکه ممکن است شامل امضای یک تابع، آرگومان‌های آن و رشته مستندات (docstring) آن باشد. این تکه صرفاً به این دلیل که شمارنده توکن از حدی گذشت، به طور خودسرانه به تابع کاربردی بعدی سرریز نمی‌شود. این کار باعث می‌شود امبدینگ بر روی یک قابلیت مشخص متمرکز بماند.

تیکت‌های پشتیبانی نامنظم‌تر هستند. آن‌ها گفتگومحور، رشته‌ای و غیرخطی هستند. تکه‌های ثابت، یک به‌روزرسانی وضعیت از یک مهندس و یک شکایت مشتری را از همان رشته برداشته و وانمود می‌کنند که آن‌ها یک واحد منسجم را تشکیل می‌دهند. ما به تکه‌بندی معنایی (semantic chunking) روی آوردیم؛ یعنی زمانی که موضوع یا گوینده تغییر می‌کند، تقسیم‌بندی انجام می‌شود، نه زمانی که بودجه توکن تمام می‌شود.

ویکی‌های داخلی اغلب بی‌نظم‌ترین داده‌ها در یک سازمان هستند. قالب‌بندی‌ها ناهماهنگ است، سرتیترها (headers) وجود ندارند و بخش‌ها در هم می‌آمیزند. برای این موارد، ما از تکه‌بندی مبتنی بر LLM استفاده می‌کنیم. یک مدل کوچک، متن را پیش‌خوانی کرده و مرزهای منطقی را قبل از تولید امبدینگ شناسایی می‌کند. این کار هزینه اولیه بیشتری نسبت به تقسیم‌بندی بر اساس کاراکتر دارد، اما کیفیت بازیابی بلافاصله هزینه خود را جبران می‌کند.

بازیابی ترکیبی: ترکیب سیگنال‌ها

جستجوی برداری قدرتمند است اما نقاط کور دارد. اگر یک کد خطای دقیق مانند ERR_CONNECTION_REFUSED_0x800 را وارد کنید، جستجوی شباهت ممکن است راهنمای عیب‌یابی یک ماژول بی‌ربط را برگرداند، زیرا فضای امبدینگ آن‌ها را در نزدیکی هم خوشه‌بندی کرده است. تطابق‌های دقیق (exact matches) اهمیت دارند و جستجوی برداری به تنهایی می‌تواند اثر آن‌ها را خنثی کند.

جستجوی کلمات کلیدی با BM25 مشکل تطابق دقیق را به زیبایی حل می‌کند. اما در برابر فاصله مفهومی (conceptual distance) ناتوان است. اگر کاربری درباره "کاهش عملکرد تحت بار سنگین" سوال کند، BM25 یادداشت تشخیصی را که "کاهش نرخ عبور در زمان اوج ترافیک" را توصیف می‌کند، از دست می‌دهد، زیرا هم‌پوشانی کلمات کلیدی کافی وجود ندارد.

ما از انتخاب بین این دو دست کشیدیم و شروع به اجرای موازی هر دو کردیم. جستجوهای برداری و کلمات کلیدی هر کدام لیست‌های رتبه‌بندی شده خود را برمی‌گردانند. ما آن‌ها را با استفاده از Reciprocal Rank Fusion (RRF) ادغام می‌کنیم. RRF در اثربخشی خود ساده و بی‌رحمانه است. این روش به هر سند بر اساس جایگاه آن در هر لیست امتیاز می‌دهد. اسنادی که در هر دو سیستم در نزدیکی رتبه‌های بالا قرار دارند، جهش بزرگی دریافت می‌کنند. اسنادی که فقط مورد پسند یکی از موتورها هستند، همچنان جایگاه خود را در مجموعه کاندیداهای نهایی حفظ می‌کنند.

پس از ادغام (fusion)، کاندیداهای برتر را از یک reranker از نوع cross-encoder عبور می‌دهیم. این کار رایگان نیست و حدود ۵۰ میلی‌ثانیه به زمان محاسبات اضافه می‌کند، اما باعث افزایش ۱۵ درصدی recall می‌شود. cross-encoder کل پرس‌وجو و هر چانک (chunk) کاندیدا را با هم ارزیابی می‌کند و امتیاز مرتبطی تولید می‌کند که بسیار دقیق‌تر و ظریف‌تر از آنچه یک bi-encoder embedding می‌تواند باشد، است. آن ۵۰ میلی‌ثانیه اضافی بسیار به‌صرفه است؛ زیرا از ارسال یک context window بی‌ارزش به LLM و صرف دو ثانیه وقت برای انتظار جهت دریافت پاسخی گیج‌کننده یا توهم‌آمیز جلوگیری می‌کند.

پیش از جستجو، پرس‌وجو را اصلاح کنید

کاربران مانند مهندسان جستجو پرس‌وجو نمی‌نویسند. آن‌ها تایپ می‌کنند "app broken". آن‌ها قطعات رمزآلود لاگ را کپی می‌کنند. آن‌ها سوالات مبهم و دوپهلو می‌پرسند. اگر این رشته‌های خام را مستقیماً به ایندکس بفرستید، خروجی بی‌ارزش دریافت خواهید کرد.

ما هر پرس‌وجو را پیش از آنکه با موتور بازیابی (retrieval engine) برخورد کند، تغییر می‌دهیم.

اول، گسترش پرس‌وجو (query expansion). سیستم از یک سوال کوتاه، چندین عبارت جستجو تولید می‌کند. مثلاً کاربر می‌پرسد: "چگونه timeout را رفع کنم؟". موتور جستجو آن را گسترش می‌دهد تا مواردی مثل connection timeouts، read timeouts، gateway timeouts و منطق تلاش مجدد (retry logic) را پوشش دهد. این رویکرد به تنهایی recall ما را از ۷۸٪ به ۹۶٪ رساند.

دوم، تجزیه پرس‌وجو (query decomposition). سوالات پیچیده به زیرسوال‌های کوچک‌تر تقسیم می‌شوند. پرس‌وجویی مانند "سیاست بازگشت وجه برای مشتریان سازمانی پس از ۹۰ روز چیست و چه تفاوتی با طرح‌های ماهانه دارد؟" به جای یک جستجوی سنگینِ embedding، به دو جستجوی متمرکز تبدیل می‌شود. هر زیرسوال به طور مستقل به ایندکس مراجعه می‌کند. نتایج در مراحل بعدی دوباره به هم متصل می‌شوند. این کار باعث می‌شود بازیابی (retrieval) محدود و دقیق باقی بماند و از رقیق شدن نتایج (dilution) که هنگام تلاش یک embedding واحد برای تطبیق با ده‌ها مفهوم به طور همزمان رخ می‌دهد، جلوگیری کند.

اگر هنوز در حال تنظیم دستی اندازه چانک (chunk size)، نسبت‌های هم‌پوشانی (overlap ratios) و وزن‌های بازیابی هستید، دارید از عملکرد بهینه خود چشم‌پوشی می‌کنید. ما حدس زدن را متوقف کردیم.

ما یک فضای جستجو تعریف کردیم که در آن اندازه چانک، درصد هم‌پوشانی، وزن‌های vector در مقابل BM25 و آستانه‌های reranker همگی متغیر هستند. سپس از بهینه‌سازی بیزی (Bayesian optimization) استفاده کردیم. جستجوی بیزی به جای جستجوی شبکه‌ای (grid-searching) در میان صدها پیکربندی تصادفی، یک مدل احتمالی از آنچه کارآمد است می‌سازد. این روش یک پیکربندی را پیشنهاد می‌دهد، recall و تأخیر (latency) را مشاهده می‌کند، باورهای خود را به‌روز می‌کند و پیکربندی بعدی را پیشنهاد می‌دهد. با گذشت زمان، این روش به تعادلی دست می‌یابد که یک انسان هرگز نمی‌تواند به صورت دستی به آن برسد.

این روش ترکیب‌هایی را پیدا کرد که ما هرگز امتحان نمی‌کردیم؛ مثلاً چانک‌های کوچک‌تر با هم‌پوشانی بیشتر، یا وزن کمی کمتر برای جستجوی dense vector در کنار یک آستانه reranker تهاجمی‌تر. این سبک‌سنگین کردن‌های (tradeoffs) غیربدیهی، هم recall بالاتر و هم latency کمتری را برای ما به ارمغان آورد.

این یک کار تنظیمات یک‌باره نیست. ما به صورت ماهانه بهینه‌سازی ابرپارامترها (hyperparameter optimization) را دوباره اجرا می‌کنیم. مجموعه داده (corpus) شما تغییر می‌کند (drift می‌کند) و رفتار کاربران جابجا می‌شود. خط لوله (pipeline) شما باید به جای ثابت ماندن و از کار افتادن، خود را تطبیق دهد.

نتیجه کار

خروجی خام این بازسازی غیرقابل انکار است.

Recall در جایگاه دهم از ۷۸٪ به ۹۵٪ رسید. وقتی پاسخ صحیح در پایگاه دانش ما وجود داشته باشد، در ۱۹ مورد از هر ۲۰ مورد آن را نمایش می‌دهیم. تأخیر در صدک ۹۵ام (95th percentile) از ۸۵۰ میلی‌ثانیه به ۳۲۰ میلی‌ثانیه کاهش یافت. چت به جای کند بودن، بسیار سریع و آنی به نظر می‌رسد.

بازیابی بهتر، زمینه‌سازی (grounding) بهتری برای مدل زبانی فراهم کرد. نرخ توهم (hallucination) از ۱۲٪ به ۳٪ کاهش یافت. وقتی مدل بافت (context) درستی در مقابل خود داشته باشد، از ساختن حقایق کاذب دست می‌کشد. هزینه به ازای هر پرس‌وجو ۳۸٪ کاهش یافت. بازیابی سریع‌تر و دقیق‌تر به معنای هدر رفتن توکن‌های کمتر برای بافت‌های بی‌ربط، حلقه‌های تلاش مجدد و پرامپت‌های طولانی اما بی‌فایده است.

آن را مانند زیرساخت بسازید

اگر در حال انتقال از مرحله نمونه اولیه (prototype) به مرحله تولید (production) هستید، با بازیابی (retrieval) مانند کد زیرساخت برخورد کنید، نه صرفاً یک پیکربندی.