DSPy: جایگزینی مهندسی پرامپت شکننده با خطلولههای کامپایلشده مدل زبانی بزرگ (LLM)
هنگام فکر کردن به خطلولههای هوش مصنوعی مالی، همیشه با همان دیوار برخورد میکنم: شما میتوانید چیزی بسازید که روی موارد آزمایشی شما به زیبایی کار کند، اما وقتی یک فروشنده فرمت فاکتور خود را تغییر میدهد یا یک نوع تراکنش جدید ظاهر میشود، شاهد فروپاشی تدریجی آن باشید. این شکنندگی تقریباً همیشه در پرامپتها نهفته است — رشتههای متنی دستسازی که هیچکس تمایلی به دستکاری آنها ندارد. DSPy که توسط خطاب و همکاران در استنفورد معرفی و در ICLR 2024 منتشر شد، روشی بنیادین و متفاوت برای ساخت خطلولههای LLM پیشنهاد میکند که شایسته توجه جدی هر کسی است که سعی در اتوماسیون امور حسابداری دارد.
مقاله
مقاله DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines (خطاب، سینگوی، ماهشواری و همکاران، ICLR 2024) ساخت خطلولههای LLM را به جای یک مشکل مهندسی پرامپت، به عنوان یک مسئله برنامهنویسی بازتعریف میکند. مشاهده اصلی این است که برنامههای مدرن LLM معمولاً به عنوان مجموعهای از رشتههای پرامپت هارد-کد شده — که نویسندگان آنها را «الگوهای پرامپت» مینامند — ساخته میشوند که با جریان کنترل پایتون به هم چسبیدهاند. وقتی مدل تغییر میکند یا توزیع وظایف جابجا میشود، کسی باید برود و آن رشتهها را دستی بازنویسی کند.
DSPy الگوهای پرامپت را با دو انتزاع جایگزین میکند: امضاها (signatures) و ماژولها (modules). امضا یک مشخصات تایپشده و اخباری از کاری است که یک فراخوانی LM باید انجام دهد، که به صورت فشرده به شکل question -> answer یا با توضیحات فیلد صریح در یک کلاس پایتون نوشته میشود. یک ماژول، یک امضا را با یک استراتژی استدلال — مانند ChainOfThought ،ReAct ،ProgramOfThought ،MultiChainComparison و غیره — بستهبندی میکند. بخش حیاتی، یک کامپایلر است (مقاله آن را teleprompter مینامد) که یک برنامه DSPy، یک مجموعه داده برچسبدار کوچک و یک معیار اعتبارسنجی را میگیرد، سپس به طور خودکار نمایشهای چند-نمونهای (few-shot demonstrations) ایجاد میکند، از میان آنها انتخاب میکند و پرامپتهایی تولید میکند که برای آن معیار بهینه شدهاند. کامپایلر در هر مرحله میانی به برچسب نیاز ندارد — میتواند با اجرای یک برنامه معلم روی ورودیهای بدون برچسب و فیلتر کردن مسیرهایی که منجر به خروجیهای نهایی صحیح میشوند، نمایشها را بوتاسترپ کند.
ایدههای کلیدی
- امضاها نیت را از پیادهسازی جدا میکنند. نوشتن
question, context -> answerبرای DSPy کافی است تا بداند چگونه فراخوانی LM زیرین را بسازد، فراخوانی کند و بهینه سازد. توسعهدهنده هرگز یک رشته پرامپت نمینویسد. - کامپایل کردن یک بوتاسترپینگ مبتنی بر معیار است. بهینهساز
BootstrapFewShotبرنامه را روی ورودیهای آموزشی اجرا میکند، ردپاهای ورودی-خروجی را که در آنها خطلوله موفق بود ه جمعآوری میکند و از آنها به عنوان نمایش (demonstration) استفاده میکند — بدون نیاز به حاشیهنویسی انسانی برای مراحل استدلال میانی. - کامپایلر پتانسیل مدلهای کوچک را آزاد میکند. در GSM8K (مسائل ریاضی متنی)، مدل خام Llama2-13b با پرامپتینگ zero-shot امتیاز ۹.۴٪ را کسب میکند. پس از کامپایل DSPy با ماژولهای reflection و ensemble، این رقم به ۴۶.۹٪ میرسد. T5-Large (با ۷۷۰ میلیون پارامتر)، مدلی که اکثر مردم برای استدلالهای پیچیده کنار گذاشته بودند، با استفاده از تنها ۲۰۰ نمونه برچسبدار، به ۳۹.۳٪ مطابقت دقیق پاسخ در HotPotQA دست مییابد.
- پرامپتهای خبره سقف کارایی نیستند. در GSM8K، مدل GPT-3.5 با پرامپتینگ چند-نمونهای معمولی به ۲۵.۲٪ میرسد. روش chain-of-thought دستساز متخصصان، این رقم را به حدود ۷۲–۷۳٪ میرساند. خطلوله کامپایلشده reflection-and-ensemble در DSPy، آن را به ۸۱.۶٪ میرساند — بدون اینکه هیچ انسانی پرامپتی نوشته باشد.
- برنامهها ترکیبپذیر هستند. یک خطلوله پرسشوپاسخ بازیابی چند-مرحلهای (multi-hop) در DSPy حدود ۱۲ خط کد پایتون است. نویسندگان اشاره میکنند که معادل آن در LangChain شامل ۵۰ رشته متنی است که از ۱۰۰۰ کاراکتر محتوای پرامپت دستساز فراتر میرود.
- سه مرحله کامپایل. بهینهساز در این مراحل عمل میکند: تولید کاندیدا (بوتاسترپ کردن ردپاها)، بهینهسازی پارامترها (جستجوی تصادفی یا Optuna روی هایپرپارامترها) و بهینهسازی مرتبه بالاتر (انسمبلها، جریان کنترل پویا).
چه چیزی ثابت میماند — و چه چیزی نه
نتایج تجربی واقعی و قابل توجه هستند. رفتن از ۹.۴٪ به ۴۶.۹٪ در GSM8K با Llama2-13b، در حالی که تنها از تعداد کمی نمونه آموزشی برچسبدار استفاده شده، یک پیشرفت جزئی نیست؛ این همان شکافی است که مدلهای کوچک و ارزان را برای کارهایی که قبلاً به GPT-4 نیاز داشتند، قابل استفاده میکند. معماری نیز واقعاً ظریف است: امضاها به راحتی خوانده میشوند، ماژولها ترکیبپذیر هستند و انتزاع برای وظایف نمایشدادهشده، دچار نشتی (leaky) نمیشود.
با این حال، محدودیتها واقعی هستند، اگرچه مقاله در بخش اختصاصی به آنها نمیپردازد. مهمترین آنها: کامپایلر فقط به اندازه معیار (metric) شما خوب عمل میکند. اگر معیار اعتبارسنجی شما نادقیق باشد یا با کیفیت واقعی وظیفه همسو نباشد — که در عمل بسیار رایج است — بهینهساز راههای هوشمندانهای برای به حداکثر رساندن آن پیدا میکند در حالی که در آنچه واقعاً برای شما مهم است شکست میخورد. در حوزهای ساختاریافته مانند حسابداری، ممکن است معیاری مانند "تراز بودن سند حسابداری" تعریف کنید، اما یک سند تراز میتواند همچنان کدهای حساب کاملاً اشتباهی داشته باشد. نویسندگان میدانند که این مشکل وجود دارد، اما مسئولیت آن را بر عهده توسعهدهنده میگذارند.
محدودیت دوم: کامپایل کردن همچنان به مقداری داده برچسبدار نیاز دارد. مقاله ادعا میکند که میتوانید با BootstrapFewShot از حداقل ۱۰ نمونه استفاده کنید و فقط به مقادیر ورودی نیاز است (نه برچسبهای میانی). این در بهترین حالت درست است، اما در عمل، قابلیت اطمینان بوتاسترپینگ زمانی که برنامه اولیه نمیتواند هیچکدام از نمونههای آموزشی را حل کند، کاهش مییابد. اگر خطلوله ایجنت مالی شما دقت پایه نزدیک به صفر داشته باشد — که هنگام ساختن چیزی جدید رایج است — کامپایلر ممکن است بیهوده درجا بزند.
سوم و ظریفتر: DSPy پرامپتها و نمایشها را بهینه میکند، اما خودِ ساختار برنامه را بهینه نمیکند. اگر ماژولها را به شکلی به هم متصل کردهاید که اساساً برای آن وظیفه اشتباه است، کامپایلر به شما کمکی نخواهد کرد. طراحی برنامه همچنان بر عهده توسعهدهنده است.
چرا این موضوع برای هوش مصنوعی مالی اهمیت دارد
مشکل شکنندگی پرامپت شاید بزرگترین مانع عملی برای استقرار ایجنتهای هوش مصنوعی مالی در محیط عملیاتی باشد. خطلولهای که تراکنشها را با تطبیق شرح تراکنش با کدهای حساب دستهبندی میکند، هر زمان که دادههای فروشنده تغییر فرمت دهد، هر زمان که دستهبندی هزینه جدیدی ظاهر شود، یا هر زمان که سرفصل حسابها (chart of accounts) بهروز شود، دچار افت کیفیت میشود. با DSPy، شما وظیفه را به صورت انتزاعی تعریف میکنید (transaction_description, chart_of_accounts -> account_code, confidence) و اجازه میدهید کامپایلر هر بار که توزیع دادهها تغییر میکند، نمایشهای بهینه را پیدا کند.
به طور خاص برای Beancount، من میتوانم خطلولهای را تصور کنم که به صورت سه ماژول زنجیرهای DSPy ساختار یافته است: یکی که دادههای تراکنش ساختاریافته را از خروجیهای خام بانکی استخر اج میکند، یکی که بهترین حساب مطابق را در سرفصل حسابهای موجود در دفتر کل جستجو میکند، و دیگری که سند حسابداری حاصل را با محدودیتهای حسابداری دوطرفه اعتبارسنجی میکند. هر ماژول امضای خود را میگیرد؛ کل برنامه بر اساس معیاری کامپایل میشود که هم صحت حسابداری و هم رعایت فرمت را بررسی میکند. مشکل کیفیتِ معیار اینجا بیشترین نمود را دارد — شما به معیاری نیاز دارید که کدهای حساب اشتباه را تشخیص دهد، نه فقط اسناد ناتراز — اما این یک مشکل مهندسی قابل حل است.
پیامد عمیقتر این است که DSPy تمرکز کار را از "نوشتن پرامپتهای بهتر" به "نوشتن معیارهای بهتر و جمعآوری مجموعهدادههای برچسبدار کوچک" تغییر میدهد. این یک شیوه مهندسی بسیار پایدارتر برای یک سیستم مالی عملیاتی است که باید با تغییر مقررات، ساختار سرفصل حسابها و فرمتهای تراکنش در طول زمان تکامل یابد.
چه چیزی را در مرحله بعد بخوانیم
- OPRO: Large Language Models as Optimizers (یانگ و همکاران، arXiv:2309.03409) — رویکرد گوگل دیپمایند برای بهینهسازی پرامپت از طریق اصلاح مکرر توسط LM؛ یک تقابل مفید با رویکرد بوتاسترپینگ DSPy.
- TextGrad: Automatic "Differentiation" via Text (یوکسکگونول و همکاران، arXiv:2406.07496) — بهینهسازی را به عنوان انتشار معکوس (backpropagation) از طریق بازخورد متنی به جای بوتاسترپینگ مبتنی بر معیار قالببندی میکند؛ نتایج قوی در وظایف کدنویسی و علمی نشان میدهد که در آنها رویکرد DSPy ضعیفتر است.
- DSPy Assertions: Computational Constraints for Self-Refining Language Model Pipelines (سینگوی و همکاران، arXiv:2312.13382) — محدودیتهای سخت و نرم را به برنامههای DSPy اضافه میکند و به خطلولهها اجازه میدهد زمانی که خروجیها قوانین دامنه را نقض میکنند، خود را اصلاح کنند؛ این موضوع مستقیماً با اجرای قوانین تغییرناپذیر حسابداری مرتبط است.
