این روزها گاهی RAG بدنام شده است؛ یا چون مردم فکر میکنند کاملاً پیشپاافتاده است—شروعش ساده است، اما توسعهاش نه—یا چون تصور میکنند «سیستمهای عاملمحور» جای آن را گرفتهاند؛ سیستمهایی که در بسیاری از موارد با کمی بررسی خیلی زود شبیه RAG به نظر میرسند…
در این مطلب، با یکی دو مثال عملی نشان میدهیم که چگونه با برخی چالشهای رایج زیر روبهرو میشویم:
کار با دادههای ترکیبی متنی و عددی و اینکه چرا RAG ساده در اینجا شکست میخورد: کلیدواژهها همپوشانی دارند و اعداد بهتنهایی معنای مفهومی ندارند.
چرا طراحی تعبیهسازی بر پایه خلاصه مفید است: برای هر قطعه یک خلاصه توصیفی کوتاه تولید کنید و تعبیهسازی و جستوجو را روی آن انجام دهید.
چگونه خلاصههای زمینهمند تولید کنیم: زمینه سند والد را اضافه کنید تا آمارهایی با ساختار مشابه از یکدیگر قابل تشخیص بمانند.
چه زمانی به کد و مدلهای Pydantic تکیه کنیم: هرجا حفظ عین محتوا مهم است، برای اطمینانپذیری کد سفارشی و/یا مدل Pydantic را با فراخوانی الگوهای زبانی بزرگ (LLM) ترکیب کنید.
مبانی
سیستمهای RAG زیربنای ابزارهای گوناگونیاند؛ از رباتهای پشتیبانی تا دستیارهای دانش سازمانی.
در پشت صحنه معمولاً این کارها را انجام میدهید:
اسناد منبع را قطعهبندی میکنید
هر قطعه را در یک فضای برداری تعبیه میکنید
هنگام جستوجو، K قطعه برتر را بازیابی میکنید
بر اساس آن قطعهها پاسخی تولید میکنید
ابزارهای محبوبی مانند LangChain، LlamaIndex و Filestore متعلق به OpenAI این مراحل را تقریباً پیشپاافتاده کردهاند. اما در فرایندهای واقعی با دادههایی روبهرو میشوید که فقط متن متراکم نیستند و RAG پایه ممکن است در پردازششان مشکل داشته باشد. در بخشهای بعدی، نمونههای ملموسی از چالشهای داده را نشان میدهیم و با افزایش تدریجی پیچیدگی، راهکار را تکمیل میکنیم.
وقتی دادههای شما فقط متن نیستند—که اتفاقاً چندان هم نادر نیست
قطعه داده زیر را در فضای یک بازی در نظر بگیرید:
JSON
تعبیهسازی بهواسطه روابط آموختهشده میان واژهها بر اساس معنا و دستور زبان کار میکند. داده بالا ترکیبی از متن و عدد است و خارج از همین زمینه، اعداد هیچ ارتباطی با واژهها ندارند. بنابراین میتوان گفت این قطعه داده تقریباً ترکیبی است از چند واژه تاحدی توصیفی که تعدادی عدد تصادفی پس از آنها آمده است.
اگر فقط همین نوع داده را داشتیم، مشکلی پیش نمیآمد؛ چون همچنان میتوانستیم با تعبیهسازی همان چند واژه توصیفی موجود، داده را بازیابی کنیم—یا صرفاً از تبدیل متن به SQL بهره ببریم. اما اگر این قطعه میان انبوهی از قطعههای متنی متراکم مدفون باشد که همین واژهها در آنها نیز آمدهاند، چه؟ برای مثال:
JSON
حالا فرض کنید میخواهیم پاسخ پرسش «What is the attack range with Draconic Ascension?» را بازیابی کنیم.بهاحتمال زیاد نمیتوانیم قطعه مرتبط را بازیابی کنیم، چون میان هیاهوی قطعههای دیگری که همان کلیدواژهها را دارند گم شده است.
مشکل اصلی این است که نمیتوانیم این قطعههای داده را بهخوبی از هم متمایز کنیم، هرچند درباره یک موضوع واحد، اطلاعات متفاوتی دارند. آیا میتوانیم بهنحوی آن را غنیتر و بهتر کنیم؟ البته که میتوانیم :smile:
با خلاصهسازی، دادههایتان را غنی کنید؛ بله، درست خواندید
بهجای تعبیه مستقیم خود قطعه، ابتدا خلاصهای تولید میکنیم که موضوع داده را توضیح دهد؛ سپس تعبیهسازی و بازیابی را روی آن خلاصه انجام میدهیم. در مرحله تولید پاسخ، همچنان از داده اصلی پیوندخورده به خلاصه استفاده میکنیم.
بنابراین برای دو قطعه نمونه بالا، خلاصههایی شبیه این تولید میکنیم:
آمار بُرد، سرعت و قدرت حمله در حالت پیشفرض و هنگام Draconic Ascension.
شرح و جزئیات Draconic Ascension، شامل شرایط فعالسازی، جلوههای بصری و پیشینه داستانی.
سپس پرسوجو را نیز گسترش میدهیم تا با خلاصه «همراستا» شود. برای نمونه، پرسش «What is the attack range with Draconic Ascension?» رابه «What is the statistics of attack range with Draconic Ascension?» تبدیل میکنیم.این موضوع بهویژه زمانی مهم است که پرسوجوی بازیابی را کاربران غیرفنی با زبان ~~«آزاد»~~ عادی انسان مطرح میکنند؛ چون در نهایت نه میدانند RAG چگونه کار میکند و نه دغدغهشان این است که چطور میتوان دقت و فراخوانی آن را به حداکثر رساند.


چیزها را از زمینهشان جدا نکنید—قاعدهای که عموماً در زندگی هم کاربرد دارد
سناریوی بعدی، کار با تعداد زیادی قطعه داده مشابه نمونه زیر است:
Plain Text
اگر همین رویکرد را ادامه دهیم، فرض کنید بپرسیم: «what is character X’s attack range?»با خلاصههایی که بهتازگی تولید کردهایم، نتیجه به حدسزدنی وابسته به شانس تبدیل میشود؛ چون آنها نیز بسیار شبیه یکدیگرند. پس چگونه میتوانیم آنها را از هم متمایز کنیم؟
پاسخ ساده است: زمینه را فراهم کنید. میتوانیم در قطعه داده صرفاً ارجاعی به سند والد آن اضافه کنیم؛ برای نمونه در اینجا {”character”: “X”}. به این ترتیب، حتی اگر همین دادهها را برای شخصیتهای Y و Z نیز داشته باشیم، میتوانیم داده درست مربوط به شخصیت X را دقیقاً بازیابی کنیم.
بااینحال، رویکردی بهتر و تعمیمپذیرتر این است که خلاصهای زمینهمند از قطعه تولید کنیم. یعنی بهجای تولید خلاصه فقط از خود قطعه داده، هم سند والد و هم قطعه را برای تولید خلاصهای کلی و زمینهمند ارائه کنیم و در آن توضیح دهیم که این قطعه چه جایگاهی در سند والد دارد؛ برای مثال:
این قطعه آمار تفصیلیِ … را برای شخصیت X ارائه میکند. این قطعه با نشاندادن قدرت X در سرعت حمله، بخشی از سند کامل را تشکیل میدهد…
این قطعه آمار تفصیلیِ … را برای شخصیت Y ارائه میکند. این قطعه با نشاندادن آمار تقویتشده Y هنگام استفاده از توانایی ویژهاش، بخشی از سند کامل را تشکیل میدهد…
این قطعه آمار تفصیلیِ … را برای شخصیت Z ارائه میکند. این قطعه با نشاندادن آمار Z که او را برای نقش تانک در مسابقات تیمی مناسب میکند، بخشی از سند کامل را تشکیل میدهد…
این روش (که تاحدی از Anthropic الهام گرفته است) شاید برای مثال بالا زیادهروی به نظر برسد، اما برای قطعههایی که ممکن است «خارج از زمینه» بد تعبیر شوند بسیار مؤثر است. همچنین رویکردی یکپارچه برای همه قطعهها فراهم میکند و فرایند مهندسی را منظم نگه میدارد.


وقتی باید ~~وسواس کنترل داشته باشید~~ دقیق و سختگیر باشید
معمولاً داده را بهصورت کامل دریافت میکنیم و برای یک سیستم RAG به قطعههای کوچکتر تقسیم میکنیم. در این مثال با وضعیت کمی متفاوتی روبهرو هستیم: داده به قطعههای نامناسب تقسیم شده است. این قطعهها بخشهایی تصادفی از یک قطعه منطقیاند و باید دوباره کنار هم قرار گیرند. قطعه منطقی یعنی بخشی از محتوا که ذاتاً باید یکجا باشد؛ مانند زیربخشی از یک سند یا یک بند منسجم.


در نخستین تلاش، همه دادهها را به یک فراخوانی الگوهای زبانی بزرگ (LLM) میدهیم، از آن میخواهیم به تشخیص خود گروهبندیشان کند و سپس محتوای گروهبندیشده را برگرداند. الگوهای زبانی بزرگ (LLM) باید در این کار عملکرد خوبی داشته باشند، درست است؟ خب، هم بله و هم نه.
در چند مورد دیگر نیز دریافتهایم که الگوهای زبانی بزرگ معمولاً کمکاری میکنند و وقتی تمام محتوا را دقیقاً و بهطور کامل میخواهید، بهویژه در زمینههای طولانی، قابل اتکا نیستند. که البته کاملاً منطقی است. اما این موضوع برای کاربرد خاص ما قابلقبول نبود، چون محتوا را دقیقاً واژهبهواژه لازم داشتیم؛ بدون خلاصهسازی و بدون حذف هیچ بخشی از محتوای اصلی. نباید هیچ جزئیاتی را از دست بدهیم.
البته بخش «بله» ماجرا این بود که معنا و ساختار قطعههای شکسته را بسیار خوب درک میکرد. البته فقط به شرط آنکه از بازگرداندن عین محتوا سر باز نزند. لعنتی! :/
پس چگونه میتوانیم از توانایی الگوهای زبانی بزرگ (LLM) بهره ببریم، اما از بخشهای غیرقابلاتکای آن دوری کنیم؟ سراغ دوست قدیمی و قابلاعتمادمان رفتیم: کد—بخوانید تابع سفارشی Python. و یک مدل Pydantic که «از این سادهتر نمیشود». راهکار این است:
درحالیکه قطعه منطقی جاری را نگه میدارید، بخشها را یکییکی پیمایش کنید
برای هر بخش از الگوهای زبانی بزرگ (LLM) بپرسید: آیا این بخش به قطعه منطقی جاری تعلق دارد؟ مطابق مدل Pydantic با بله یا خیر پاسخ بده.
اگر پاسخ بله بود، بخش را به قطعه اضافه کنید؛ اگر خیر بود، قطعه منطقی جاری را که کامل شده است خروجی دهید و با این بخش قطعهای جدید بسازید.


البته در مقایسه با پردازش یکباره کل محتوا، کمی توکن بیشتری مصرف میکنیم؛ اما در این کاربرد خاص که حفظ دقیق محتوا بالاترین اولویت را داشت، هزینه اضافی اندک کاملاً ارزشش را داشت.
این راهکار بسیار ساده است، اما از اصلی مهم پیروی میکند: هرجا دقت و سختگیری لازم است، نباید فقط به الگوهای زبانی بزرگ تکیه کنیم، چون در نهایت احتمالاتیاند.
با استفاده از کد و توابع سفارشی و مدلهای Pydantic میتوان به نتیجهای پیشبینیپذیر و قابلاتکا رسید و همزمان از تواناییهای الگوهای زبانی بزرگ بهره برد.
ساخت یک راهکار هوش مصنوعی مولد، همانقدر که چالشی در حوزه هوش مصنوعی است، چالشی مهندسی نیز به شمار میرود. امیدواریم این نمونهها الهامبخش شما برای روبهروشدن با چالشهای منحصربهفرد خودتان باشند. برای مطالعه بیشتر درباره راهکارهای مهندسیمحور هوش مصنوعی مولد، مطلب ما درباره طراحی سیستمهای عاملمحور مبتنی بر مسیریاب را بخوانید.