ناوبری اصلی

نمونه‌های عملی راهکارهای سفارشی RAG

نمونه‌های واقعی نشان می‌دهند سیستم‌های سفارشی تولید تقویت‌شده با بازیابی چگونه مسائل پیچیده دانش سازمانی را حل می‌کنند.

این روزها گاهی RAG بدنام شده است؛ یا چون مردم فکر می‌کنند کاملاً پیش‌پاافتاده است—شروعش ساده است، اما توسعه‌اش نه—یا چون تصور می‌کنند «سیستم‌های عامل‌محور» جای آن را گرفته‌اند؛ سیستم‌هایی که در بسیاری از موارد با کمی بررسی خیلی زود شبیه RAG به نظر می‌رسند…

در این مطلب، با یکی دو مثال عملی نشان می‌دهیم که چگونه با برخی چالش‌های رایج زیر روبه‌رو می‌شویم:

  • کار با داده‌های ترکیبی متنی و عددی و اینکه چرا RAG ساده در اینجا شکست می‌خورد: کلیدواژه‌ها هم‌پوشانی دارند و اعداد به‌تنهایی معنای مفهومی ندارند.

  • چرا طراحی تعبیه‌سازی بر پایه خلاصه مفید است: برای هر قطعه یک خلاصه توصیفی کوتاه تولید کنید و تعبیه‌سازی و جست‌وجو را روی آن انجام دهید.

  • چگونه خلاصه‌های زمینه‌مند تولید کنیم: زمینه سند والد را اضافه کنید تا آمارهایی با ساختار مشابه از یکدیگر قابل تشخیص بمانند.

  • چه زمانی به کد و مدل‌های Pydantic تکیه کنیم: هرجا حفظ عین محتوا مهم است، برای اطمینان‌پذیری کد سفارشی و/یا مدل Pydantic را با فراخوانی الگوهای زبانی بزرگ (LLM) ترکیب کنید.

ساخت راهکارهای سفارشی RAG

مبانی

سیستم‌های RAG زیربنای ابزارهای گوناگونی‌اند؛ از ربات‌های پشتیبانی تا دستیارهای دانش سازمانی.

در پشت صحنه معمولاً این کارها را انجام می‌دهید:

  1. اسناد منبع را قطعه‌بندی می‌کنید

  2. هر قطعه را در یک فضای برداری تعبیه می‌کنید

  3. هنگام جست‌وجو، K قطعه برتر را بازیابی می‌کنید

  4. بر اساس آن قطعه‌ها پاسخی تولید می‌کنید

ابزارهای محبوبی مانند LangChain،‏ LlamaIndex و Filestore متعلق به OpenAI این مراحل را تقریباً پیش‌پاافتاده کرده‌اند. اما در فرایندهای واقعی با داده‌هایی روبه‌رو می‌شوید که فقط متن متراکم نیستند و RAG پایه ممکن است در پردازششان مشکل داشته باشد. در بخش‌های بعدی، نمونه‌های ملموسی از چالش‌های داده را نشان می‌دهیم و با افزایش تدریجی پیچیدگی، راهکار را تکمیل می‌کنیم.

وقتی کار پیچیده‌تر می‌شود

  1. وقتی داده‌های شما فقط متن نیستند—که اتفاقاً چندان هم نادر نیست

قطعه داده زیر را در فضای یک بازی در نظر بگیرید:

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

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

اگر فقط همین نوع داده را داشتیم، مشکلی پیش نمی‌آمد؛ چون همچنان می‌توانستیم با تعبیه‌سازی همان چند واژه توصیفی موجود، داده را بازیابی کنیم—یا صرفاً از تبدیل متن به SQL بهره ببریم. اما اگر این قطعه میان انبوهی از قطعه‌های متنی متراکم مدفون باشد که همین واژه‌ها در آن‌ها نیز آمده‌اند، چه؟ برای مثال:

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

حالا فرض کنید می‌خواهیم پاسخ پرسش «What is the attack range with Draconic Ascension?» را بازیابی کنیم.به‌احتمال زیاد نمی‌توانیم قطعه مرتبط را بازیابی کنیم، چون میان هیاهوی قطعه‌های دیگری که همان کلیدواژه‌ها را دارند گم شده است.

مشکل اصلی این است که نمی‌توانیم این قطعه‌های داده را به‌خوبی از هم متمایز کنیم، هرچند درباره یک موضوع واحد، اطلاعات متفاوتی دارند. آیا می‌توانیم به‌نحوی آن را غنی‌تر و بهتر کنیم؟ البته که می‌توانیم :smile:

  1. با خلاصه‌سازی، داده‌هایتان را غنی کنید؛ بله، درست خواندید

به‌جای تعبیه مستقیم خود قطعه، ابتدا خلاصه‌ای تولید می‌کنیم که موضوع داده را توضیح دهد؛ سپس تعبیه‌سازی و بازیابی را روی آن خلاصه انجام می‌دهیم. در مرحله تولید پاسخ، همچنان از داده اصلی پیوندخورده به خلاصه استفاده می‌کنیم.

بنابراین برای دو قطعه نمونه بالا، خلاصه‌هایی شبیه این تولید می‌کنیم:

  1. آمار بُرد، سرعت و قدرت حمله در حالت پیش‌فرض و هنگام Draconic Ascension.

  2. شرح و جزئیات Draconic Ascension، شامل شرایط فعال‌سازی، جلوه‌های بصری و پیشینه داستانی.

سپس پرس‌وجو را نیز گسترش می‌دهیم تا با خلاصه «هم‌راستا» شود. برای نمونه، پرسش «What is the attack range with Draconic Ascension?» رابه «What is the statistics of attack range with Draconic Ascension?» تبدیل می‌کنیم.این موضوع به‌ویژه زمانی مهم است که پرس‌وجوی بازیابی را کاربران غیرفنی با زبان ~~«آزاد»~~ عادی انسان مطرح می‌کنند؛ چون در نهایت نه می‌دانند RAG چگونه کار می‌کند و نه دغدغه‌شان این است که چطور می‌توان دقت و فراخوانی آن را به حداکثر رساند.

نموداری درباره زمانی که کار پیچیده‌تر می‌شود.

  1. چیزها را از زمینه‌شان جدا نکنید—قاعده‌ای که عموماً در زندگی هم کاربرد دارد

سناریوی بعدی، کار با تعداد زیادی قطعه داده مشابه نمونه زیر است:

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

اگر همین رویکرد را ادامه دهیم، فرض کنید بپرسیم: «what is character X’s attack range?»با خلاصه‌هایی که به‌تازگی تولید کرده‌ایم، نتیجه به حدس‌زدنی وابسته به شانس تبدیل می‌شود؛ چون آن‌ها نیز بسیار شبیه یکدیگرند. پس چگونه می‌توانیم آن‌ها را از هم متمایز کنیم؟

پاسخ ساده است: زمینه را فراهم کنید. می‌توانیم در قطعه داده صرفاً ارجاعی به سند والد آن اضافه کنیم؛ برای نمونه در اینجا {”character”: “X”}. به این ترتیب، حتی اگر همین داده‌ها را برای شخصیت‌های Y و Z نیز داشته باشیم، می‌توانیم داده درست مربوط به شخصیت X را دقیقاً بازیابی کنیم.

بااین‌حال، رویکردی بهتر و تعمیم‌پذیرتر این است که خلاصه‌ای زمینه‌مند از قطعه تولید کنیم. یعنی به‌جای تولید خلاصه فقط از خود قطعه داده، هم سند والد و هم قطعه را برای تولید خلاصه‌ای کلی و زمینه‌مند ارائه کنیم و در آن توضیح دهیم که این قطعه چه جایگاهی در سند والد دارد؛ برای مثال:

  1. این قطعه آمار تفصیلیِ … را برای شخصیت X ارائه می‌کند. این قطعه با نشان‌دادن قدرت X در سرعت حمله، بخشی از سند کامل را تشکیل می‌دهد…

  2. این قطعه آمار تفصیلیِ … را برای شخصیت Y ارائه می‌کند. این قطعه با نشان‌دادن آمار تقویت‌شده Y هنگام استفاده از توانایی ویژه‌اش، بخشی از سند کامل را تشکیل می‌دهد…

  3. این قطعه آمار تفصیلیِ … را برای شخصیت Z ارائه می‌کند. این قطعه با نشان‌دادن آمار Z که او را برای نقش تانک در مسابقات تیمی مناسب می‌کند، بخشی از سند کامل را تشکیل می‌دهد…

این روش (که تاحدی از Anthropic الهام گرفته است) شاید برای مثال بالا زیاده‌روی به نظر برسد، اما برای قطعه‌هایی که ممکن است «خارج از زمینه» بد تعبیر شوند بسیار مؤثر است. همچنین رویکردی یکپارچه برای همه قطعه‌ها فراهم می‌کند و فرایند مهندسی را منظم نگه می‌دارد.

نموداری درباره زمانی که کار پیچیده‌تر می‌شود.

  1. وقتی باید ~~وسواس کنترل داشته باشید~~ دقیق و سخت‌گیر باشید

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

نموداری درباره زمانی که کار پیچیده‌تر می‌شود.

در نخستین تلاش، همه داده‌ها را به یک فراخوانی الگوهای زبانی بزرگ (LLM) می‌دهیم، از آن می‌خواهیم به تشخیص خود گروه‌بندی‌شان کند و سپس محتوای گروه‌بندی‌شده را برگرداند. الگوهای زبانی بزرگ (LLM) باید در این کار عملکرد خوبی داشته باشند، درست است؟ خب، هم بله و هم نه.

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

البته بخش «بله» ماجرا این بود که معنا و ساختار قطعه‌های شکسته را بسیار خوب درک می‌کرد. البته فقط به شرط آنکه از بازگرداندن عین محتوا سر باز نزند. لعنتی! :/

پس چگونه می‌توانیم از توانایی الگوهای زبانی بزرگ (LLM) بهره ببریم، اما از بخش‌های غیرقابل‌اتکای آن دوری کنیم؟ سراغ دوست قدیمی و قابل‌اعتمادمان رفتیم: کد—بخوانید تابع سفارشی Python. و یک مدل Pydantic که «از این ساده‌تر نمی‌شود». راهکار این است:

  • درحالی‌که قطعه منطقی جاری را نگه می‌دارید، بخش‌ها را یکی‌یکی پیمایش کنید

  • برای هر بخش از الگوهای زبانی بزرگ (LLM) بپرسید: آیا این بخش به قطعه منطقی جاری تعلق دارد؟ مطابق مدل Pydantic با بله یا خیر پاسخ بده.

  • اگر پاسخ بله بود، بخش را به قطعه اضافه کنید؛ اگر خیر بود، قطعه منطقی جاری را که کامل شده است خروجی دهید و با این بخش قطعه‌ای جدید بسازید.

نموداری درباره زمانی که کار پیچیده‌تر می‌شود.

البته در مقایسه با پردازش یک‌باره کل محتوا، کمی توکن بیشتری مصرف می‌کنیم؛ اما در این کاربرد خاص که حفظ دقیق محتوا بالاترین اولویت را داشت، هزینه اضافی اندک کاملاً ارزشش را داشت.

این راهکار بسیار ساده است، اما از اصلی مهم پیروی می‌کند: هرجا دقت و سخت‌گیری لازم است، نباید فقط به الگوهای زبانی بزرگ تکیه کنیم، چون در نهایت احتمالاتی‌اند.

با استفاده از کد و توابع سفارشی و مدل‌های Pydantic می‌توان به نتیجه‌ای پیش‌بینی‌پذیر و قابل‌اتکا رسید و هم‌زمان از توانایی‌های الگوهای زبانی بزرگ بهره برد.

جمع‌بندی

ساخت یک راهکار هوش مصنوعی مولد، همان‌قدر که چالشی در حوزه هوش مصنوعی است، چالشی مهندسی نیز به شمار می‌رود. امیدواریم این نمونه‌ها الهام‌بخش شما برای روبه‌روشدن با چالش‌های منحصربه‌فرد خودتان باشند. برای مطالعه بیشتر درباره راهکارهای مهندسی‌محور هوش مصنوعی مولد، مطلب ما درباره طراحی سیستم‌های عامل‌محور مبتنی بر مسیریاب را بخوانید.

نویسنده

Cynthia Yu