نگه داشتن نوبتهای طولانی عامل در پنجرهی متن
یک مدل فقط میتواند مقدار مشخصی را همزمان بخواند — پنجرهی متن آن. عامل Mythex کارهای طولانی انجام میدهد: خواندن فایلهای زیاد، اجرای دستورها، و واگذاری کارهای محدود به زیرعاملها. سه راه پیدا کردیم که کارهای طولانی به آن خراب میشدند: زیرعاملها آنقدر بزرگ میشدند که مدل هر درخواست را رد میکرد، یک زیرعامل زودتر متوقف میشد چون فکر میکرد جا ندارد، و شمارش توکن ما برای متن روسی، چینی و ژاپنی بسیار اشتباه بود. آنچه تغییر دادیم این است.
ابتدا چند اصطلاح
- توکنها تکههایی هستند که مدل میخواند. محدودیتها و هزینهها بر حسب توکن شمرده میشوند، نه نویسه یا کلمه.
- پنجرهی متن بیشترین چیزی است که مدل میتواند در یک درخواست بخواند. ما هم برای هر درخواست یک بودجه پایینتر خودمان داریم، تا هزینه و سرعت معقول بمانند.
- نتایج ابزار چیزی است که عامل از ابزارهایش برمیگرداند: محتوای فایلی که خوانده، خروجی دستوری که اجرا کرده. در کارهای طولانی بیشترِ آنچه فرستاده میشود همین است.
- زیرعامل یک یاریگر است که عامل اصلی برای یک کار مستقل شروع میکند، مثلاً «این فصلها را با یادداشتها بسنج». یک دستور میگیرد، سپس خودش از طریق تعداد زیادی فراخوانی ابزار کار میکند.
مشکل ۱: زیرعاملها آنقدر بزرگ شدند که مدل رد کرد
عامل در هر گام مکالمهی تا آن لحظه را به مدل میفرستد. وقتی این بزرگ شد، کد ما آن را کوتاه میکرد — اما فقط در ابتدای یک پیام کاربر، تا یک فراخوانی ابزار و نتیجهاش هرگز از هم جدا نشوند. پیام اول را نگه میداشت، یادداشتی اضافه میکرد که گفتگوی قبلی کوتاه شده، و جدیدترین نوبتهای کامل را که جا میشدند نگه میداشت.
یک زیرعامل دقیقاً یک پیام کاربر دارد: دستورش. جایی برای برش نبود. پس در هر گام کل تاریخچهاش را میفرستاد، و هر بار بزرگتر میشد، تا وقتی درخواست بزرگتر از چیزی شد که مدل اصلاً میپذیرد — حدود یک میلیون توکن. در یک آزمایش با ۸ زیرعامل، همهی ۸ رد شدند.
راهحل: اول نتایج قدیمی ابزار را پاک کن
حالا درخواستها در سه گام در بودجه جا میشوند؛ هر گام فقط وقتی امتحان میشود که گام قبلی کافی نبوده باشد:
- قدیمیترین نتایج ابزار را پاک کن. محتوایشان با یک یادداشت کوتاه جایگزین میشود: نتیجه پاک شد تا گفتگو در پنجرهی مدل بماند، و عامل اگر لازم داشت میتواند ابزار را دوباره اجرا کند. فایلی که یک ساعت پیش خوانده شده معمولاً بزرگترین و کمفایدهترین چیز در درخواست است، و فایل هنوز هست که دوباره خوانده شود. تازهترین نتایج کامل میمانند.
- نوبتهای کامل کاربر را نگه دار. مثل قبل: پیام اول، یک یادداشت، و جدیدترین نوبتهای کامل که جا میشوند.
- دورهای کامل ابزار را نگه دار. درون یک نوبت طولانی — تنها نوعی که یک زیرعامل دارد — پیام اول، یک یادداشت، و جدیدترین دورهای کامل «یک ابزار را فراخوانی کن، نتیجهاش را بگیر» را نگه دار.
این فقط شکل آنچه فرستاده میشود را تعیین میکند. تاریخچهی ذخیرهشدهی عامل همهچیز را نگه میدارد. در همان آزمایش، ۰ از ۸ رد شدند و هر ۸ تمام کردند.
پاکسازی دستهای، تا کش کار کند
ارائهدهندگان مدل میتوانند کار ابتدای یک درخواست را دوباره استفاده کنند، اگر با درخواست قبلی یکسان باشد — یک کش پیشوند. اگر هر بار یک نتیجهی دیگر را پاک میکردیم، ابتدای درخواست در هر فراخوانی عوض میشد و کش هرگز کمک نمیکرد.
پس قدیمیترین نتایج را چهارتا چهارتا پاک میکنیم، نه یکییکی. بخش پاکشده آنگاه برای چند فراخوانی ثابت میماند. نتیجهای که خیلی بزرگتر از یادداشت نیست دستنخورده میماند: پاک کردنش تقریباً چیزی صرفهجویی نمیکرد و باز هم ابتدای درخواست را عوض میکرد. اینکه چه مقدار کامل بماند با اندازه تعیین میشود، نه تعداد: تازهترین نتایجی که در یکچهارم بودجه جا میشوند میمانند، و همیشه حداقل یکی. نسخهی قبلی شش نتیجهی ثابت کامل نگه میداشت، که برای خروجی دستورهای کوتاه درست بود، اما با خواندن فایلهای بلند، بیشتر بودجه را برای نتایج قدیمی باقی میگذاشت.
رد شدنها حالا خودشان را توضیح میدهند
درخواستی که مدل آن را نامعتبر رد کند دیگر دوباره امتحان نمیشود؛ فرستادن دوبارهی همان درخواست همان جواب را میگیرد. اگر گفتگو برای مدل خیلی بلند باشد، شخص این را میبیند: «این گفتگو برای اینکه مدل بتواند یکجا بخواندش خیلی طولانی شد. برای گام بعدی یک چت تازه شروع کنید — هرچه تاکنون ساخته شده باقی میماند.» این چیزی است که خود شخص باید برایش اقدام کند، نه یک قطعی، پس ما را خبردار نمیکند.
مشکل ۲: عامل برای «جا باز کردن» متوقف میشد
یک زیرعامل که خواسته بود ۴۰ فصل بلند را بخواند، بعد از ۲ فصل متوقف شد و گفت میخواهد جا باز کند. حدود ۱۰۰٬۰۰۰ توکن از بودجهی ۴۰۰٬۰۰۰ توکنیاش را مصرف کرده بود.
هیچ راهی برای دانستن این نداشت که نتایج قدیمی حالا برایش پاک خواهند شد. پس به آن گفتیم. دستورالعملهای عامل حالا میگویند وقتی گفتگو از آنچه مدل میتواند بپذیرد بزرگتر شود، قدیمیترین نتایج ابزار پاک میشوند و تازهترینها کامل میمانند — پس برای جا باز کردن یک کار طولانی را متوقف نکن؛ ادامه بده، و اگر چیزی که پاک شده لازم بود، فایل را دوباره بخوان.
راهحلی که مدل نمیبیند، از دید مدل راهحل نیست. این مکانیزم فقط وقتی کمک میکند که عامل به آن اعتماد کند.
مشکل ۳: شمردن توکنها انگار همهچیز انگلیسی است
توکنها را بدون اجرای یک توکنایزر تخمین میزدیم: نویسه تقسیم بر چهار. این قاعدهی سرانگشتی برای انگلیسی معقول است، اما برای خطهای دیگر نه: روسی حدود ۲.۲ نویسه برای هر توکن است و چینی یا ژاپنی تقریباً یک.
پس یک زیرعامل که فصلهای روسی را میخواند درخواستی حدود ۶۴۰٬۰۰۰ توکنی فرستاد، در حالی که تخمین ما میگفت زیر بودجهی ۴۰۰٬۰۰۰ است. این تخمین به همان اندازه میتوانست درخواستی را از حد سخت مدل رد کند.
حالا تخمین بر اساس خط حساب میشود: ASCII ساده با چهار نویسه برای هر توکن، چینی، ژاپنی و کرهای با یکی، و بقیه — سیریلیک، یونانی، عربی، حروف با نشانه و ایموجی — با دو. قرار است عمداً بالا تخمین بزند. روی یک فصل روسی حالا ۴۳٬۴۲۳ توکن تخمین میزند در برابر حدود ۴۴٬۶۰۰ اندازهگیریشده؛ پیشتر ۲۵٬۰۲۳ میگفت. از آنجا که یک پیام پس از نوشته شدن هرگز تغییر نمیکند، تخمین آن یکبار محاسبه و ذخیره میشود، نه در هر گام.
آنچه از آن یاد گرفتیم
- برش را جایی بزنید که حجم واقعاً هست. در کار عامل، آن نتایج ابزار هستند، نه پیامهای کاربر.
- به عامل دربارهی تورهای ایمنی خودش بگویید. وگرنه بر اساس محدودیتهایی برنامهریزی میکند که دیگر وجود ندارند.
- «نویسه ÷ ۴» یک فرض انگلیسی است. هر چیزی که برای کاربران چندزبانه توکن میشمارد باید بر اساس خط بشمارد، یا از یک توکنایزر واقعی استفاده کند.
- مراقب کش باشید. درخواستی که در ابتدایش در هر فراخوانی عوض شود گرانتر و کندتر است.
اگر ویژگیهای هوش مصنوعی را به برنامهی خودتان اضافه میکنید، راهنماهای ما دربارهٔ مدل زبانی بزرگ چیست و چطور از APIهای LLM استفاده کنیم پنجرهی متن و توکنها را از همان ابتدا پوشش میدهند. توضیح بیشتر دربارهٔ کار زیرعاملها در Mythex در مستندات زیرعاملها آمده است.