بهینه‌سازی

prompt caching — کم کردن هزینه بدون افت کیفیت

prompt caching چطور با تطبیق پیشوند هزینه‌ی مدل زبانی را کم می‌کند، چرا یک بایت تغییر همه‌چیز را باطل می‌کند، و چطور بفهمیم واقعاً کار می‌کند.

· ۷ دقیقه مطالعه

پرداختن دوباره برای متنی که عوض نشده

یک دستیار داخلی دارید که در هر درخواست یک system prompt سه‌هزارتوکنی، تعریف بیست ابزار و یک سند مرجع ده‌هزارتوکنی می‌فرستد. کاربر یک جمله می‌نویسد و شما بابت سیزده هزار توکنی که از دیروز یک بایتش هم عوض نشده، دوباره پول می‌دهید. در روز چند هزار درخواست، همین تکرار می‌تواند بخش عمده‌ی صورت‌حساب باشد.

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

یک نکته را از اول روشن کنیم: کش کیفیت را عوض نمی‌کند. همان توکن‌ها به همان مدل داده می‌شوند و خروجی از نظر آماری همان است. این یک بهینه‌سازی هزینه و تأخیر است، نه یک سازش کیفی.

تطبیق پیشوند: تنها قاعده‌ای که باید بلد باشید

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

سه پیامد مستقیم دارد:

  • تغییر در جای k، همه‌چیز از k به بعد را باطل می‌کند. عوض‌کردن یک کلمه در سطر دوم system prompt یعنی کل ده هزار توکن بعدی دوباره حساب می‌شوند.
  • ترتیب اهمیت دارد، نه محتوا. دو درخواست با محتوای یکسان ولی ترتیب متفاوت، هیچ اشتراکی از نظر کش ندارند.
  • افزودن به انتها بی‌خطر است. به همین دلیل تاریخچه‌ی گفت‌وگو که فقط رشد می‌کند، الگوی ایده‌آل کش است.

دو محدودیت عملی هم هست که باید بدانید: کش عمر کوتاهی دارد و اگر مدتی استفاده نشود از بین می‌رود، و برای پیشوندهای خیلی کوتاه اصلاً فعال نمی‌شود. عددهای دقیق (طول حداقل و طول عمر) در مستندات ارائه‌دهنده است و بین مدل‌ها فرق دارد. نتیجه‌ی عملی‌اش این است که کش برای ترافیک پیوسته و پیشوند بزرگ می‌ارزد؛ برای یک درخواست تکی در ساعت، احتمالاً نه.

چه چیزی اول، چه چیزی آخر

کل کار طراحی این است: ورودی را از ثابت‌ترین به متغیرترین مرتب کنید.

1. system prompt (قواعد ثابت)
2. تعریف ابزارها — با ترتیب قطعی
3. اسناد مرجع / دانش پایه‌ی ثابت
4. مثال‌های few-shot — با ترتیب ثابت
--- مرز کش ---
5. تاریخچه‌ی گفت‌وگو (فقط append)
6. تکه‌های بازیابی‌شده‌ی مخصوص این پرسش
7. پیام جاری کاربر

نکته‌های مهم در همین ترتیب:

  • اسناد بازیابی‌شده در RAG بعد از بخش ثابت بیایند، نه قبلش. این‌ها با هر پرسش عوض می‌شوند. اگر بالای زمینه بگذاریدشان، هر درخواست کل کش را از بین می‌برد. اگر پیکره‌ی شما کوچک و ثابت است، حالت دیگری هم هست: کل پیکره را در بخش ثابت بگذارید و اصلاً بازیابی نکنید.
  • تعریف ابزارها باید سریال‌سازی قطعی داشته باشد. اگر فهرست ابزارها از یک dict یا set ساخته می‌شود، ترتیب ممکن است بین اجراها عوض شود. sort_keys=True و ترتیب ثابت فهرست را تضمین کنید.
  • پیام کاربر همیشه آخر. بدیهی به نظر می‌رسد، ولی وقتی کسی «برای تأکید» پرسش کاربر را بالای system prompt هم کپی می‌کند، کش تمام می‌شود.

کش‌شکن‌های رایج

این‌ها را در همین ترتیب بررسی کنید؛ تقریباً همیشه یکی از این پنج مورد است:

  1. timestamp یا تاریخ در بالای system prompt. «امروز ۱۶ شهریور، ساعت ۱۴:۳۰ است» در ابتدای prompt یعنی هر دقیقه کش از صفر ساخته می‌شود. اگر مدل واقعاً به زمان نیاز دارد، ببریدش انتها — و اگر دقت روز کافی است، ساعت را ننویسید.
  2. شناسه‌ی کاربر یا نشست در ابتدا. هر کاربر کش خودش را می‌سازد و هیچ‌کدام به‌صرفه نمی‌شود. اطلاعات کاربر متعلق به انتهای زمینه است.
  3. مونتاژ پویا و بی‌نظم prompt. اگر بخش‌های prompt با حلقه روی یک ساختار بی‌ترتیب ساخته می‌شوند، یا مثال‌های few-shot در هر اجرا shuffle می‌شوند، خروجی هر بار متفاوت است.
  4. بریدن تاریخچه از ابتدا. وقتی برای جا شدن در پنجره، قدیمی‌ترین پیام‌ها را حذف می‌کنید، پیشوند عوض می‌شود و کش می‌سوزد. به‌جای برش در هر نوبت، به‌صورت دوره‌ای فشرده‌سازی کنید — کش را کمتر و قابل پیش‌بینی‌تر باطل می‌کنید. تعادل بین این دو در پنجره‌ی زمینه بحث شده.
  5. آزمایش A/B روی system prompt. اگر ترافیک بین دو نسخه‌ی prompt تقسیم شود، دو کش جدا خواهید داشت و هرکدام نصف ترافیک را می‌گیرد. این لزوماً بد نیست، ولی باید بدانید که هزینه‌اش را می‌پردازید.

از کجا بفهمیم واقعاً کار می‌کند

فرض نکنید؛ اندازه بگیرید. پاسخ API فیلدهای مصرف را برمی‌گرداند و در آن، توکن‌های نوشته‌شده در کش (cache_creation_input_tokens) از توکن‌های خوانده‌شده از کش (cache_read_input_tokens) جدا گزارش می‌شوند. سه حالت را ببینید:

  • خواندن از کش همیشه صفر است → پیشوند اصلاً تطبیق نمی‌خورد. مشکل ساختاری است.
  • نوشتن در کش تقریباً به‌اندازه‌ی هر درخواست است → هر بار کش از نو ساخته می‌شود؛ یکی از کش‌شکن‌های بالا فعال است.
  • نسبت خواندن به کل ورودی بالا و پایدار است → درست کار می‌کند.

برای پیدا کردن دقیق نقطه‌ی شکست، پیشوند ثابت هر درخواست را هش کنید و لاگ بگیرید:

import hashlib, json

def prefix_hash(system, tools):
    blob = json.dumps({"system": system, "tools": tools},
                      ensure_ascii=False, sort_keys=True)
    return hashlib.sha256(blob.encode("utf-8")).hexdigest()[:12]

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

کجا کش کمکی نمی‌کند

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

کش را در ذهنتان یک اهرم معماری بدانید نه یک سوییچ. اگر ورودی را از همان اول «ثابت به متغیر» مرتب کنید و ترتیبش را قطعی نگه دارید، فعال‌کردنش یک تغییر کوچک است. اگر prompt در هر درخواست از نو و به‌شکل دلخواه ساخته شود، هیچ تنظیمی نجاتش نمی‌دهد.