التخزين المؤقت للموجّهات في LangGraph: ترتيب الحالة لضمان التخزين المؤقت للبادئة
يقلل التخزين المؤقت للموجّهات من التكلفة ووقت الانتظار لإعادة قراءة نفس التعليمات في كل دور للوكيل، ولكن فقط إذا كان ترتيب رسائلك يحافظ على تطابق البادئة بالبايت. إليك كود التجميع، ونقاط توقف التخزين المؤقت، والأخطاء التي تعطله بصمت.
في كل مرة تقوم فيها عقدة LangGraph باستدعاء النموذج، فإنها ترسل الحمولة (payload) بالكامل مرة أخرى. وهذا يعني موجّه النظام (system prompt)، ومخططات الأدوات، والمستندات المرجعية، والسجل حتى الآن. النموذج لا يحتفظ بأي ذاكرة بين الاستدعاءات. وكيل البحث (research agent) الذي يكرر العملية أربع مرات ثم يسلمها إلى وكيل التنسيق يمكن أن يدفع تكلفة نفس كتلة التعليمات المكونة من 10,000 رمز (token) خمس أو ست مرات في طلب مستخدم واحد.
التخزين المؤقت للموجّهات (Prompt caching) يحل معظم هذه المشكلة. اعتماده غير مكلف، ولكنه يفشل بصمت إذا أخطأت في تنفيذه. الكود سيستمر في العمل والإجابات ستبدو صحيحة، لكن الفاتورة وزمن الاستجابة لن ينخفضا أبداً.
ما الذي يقوم المزود بتخزينه مؤقتاً
قبل إنشاء أي كلمة، يقوم النموذج بمعالجة مدخلاتك بالكامل. هذه الخطوة تسمى التعبئة المسبقة (prefill). تكلفتها ومدتها تزداد مع طول المدخلات. التعبئة المسبقة هي السبب الرئيسي الذي يجعل استدعاء السياق الطويل يستغرق ثوانٍ قبل ظهور أول رمز. هذا التأخير يسمى وقت أول رمز (time to first token أو TTFT).
مع التخزين المؤقت للموجّهات، يحتفظ المزود بالشكل المعالج لبادئة الموجّه (prompt prefix) لفترة قصيرة. إذا بدأ طلبك التالي بنفس الرموز بالضبط، يتم قراءة هذا الجزء بدلاً من إعادة حسابه. أنت تدفع سعراً مخفضاً مقابل ذلك، ويبدأ النموذج في الإنشاء بشكل أسرع.
الكلمة المفتاحية هنا هي البادئة (prefix). تبدأ المطابقة من الرمز الأول وتتوقف عند أول اختلاف. كل شيء بعد الرمز المتغير تتم معالجته بالسعر الكامل. إذا كان هناك حرف واحد متغير في أعلى موجّه النظام، فلن يتم تخزين أي شيء تحته مؤقتاً.
يتعامل مزودا واجهة برمجة التطبيقات (API) الرئيسيان مع هذا الأمر بشكل مختلف.
تستخدم Anthropic (Claude) التخزين المؤقت الصريح. تقوم بتحديد نقاط التوقف (breakpoints) باستخدام cache_control، وكل شيء حتى كل نقطة توقف يصبح بادئة قابلة للتخزين المؤقت. وفقاً لـ وثائق التخزين المؤقت للموجّهات من Anthropic:
- ▸العمر الافتراضي هو 5 دقائق، ويتم تجديده في كل مرة يتم فيها قراءة الذاكرة المؤقتة. يتوفر عمر لمدة ساعة واحدة باستخدام
"ttl": "1h". - ▸الكتابة في الذاكرة المؤقتة لمدة 5 دقائق تكلف 1.25 ضعف السعر الأساسي للمدخلات. الكتابة لمدة ساعة تكلف ضعفين. القراءة تكلف 0.1 ضعف.
- ▸يمكنك الحصول على ما يصل إلى أربع نقاط توقف لكل طلب.
- ▸الموجّهات التي يقل طولها عن الحد الأدنى الخاص بالنموذج لا يتم تخزينها مؤقتاً. يتراوح الحد الأدنى من حوالي 1,024 إلى 4,096 رمزاً حسب النموذج.
- ▸يتم تجميع البادئة بترتيب ثابت: الأدوات (tools)، ثم النظام (system)، ثم الرسائل (messages). تغيير تعريف الأداة يبطل كل شيء بعدها.
تقوم OpenAI (GPT) بالتخزين المؤقت تلقائياً للموجّهات التي تبلغ 1,024 رمزاً أو أكثر، دون الحاجة إلى أي علامات (markup). وفقاً لـ دليل التخزين المؤقت للموجّهات من OpenAI، تظل البادئة المخزنة مؤقتاً مؤهلة لإعادة الاستخدام لمدة 30 دقيقة بعد أحدث كتابة أو إعادة استخدام لها. تدعم بعض النماذج خيار احتفاظ ممتد يصل إلى 24 ساعة. يختلف الخصم على المدخلات المخزنة مؤقتاً حسب النموذج، لذا تحقق من صفحة التسعير الحالية. يمكنك أيضاً تمرير prompt_cache_key بحيث يتم توجيه الطلبات التي تشترك في بادئة إلى نفس الذاكرة المؤقتة.
رقم "5 إلى 15 دقيقة" المتداول في المقالات القديمة أصبح قديماً. خطط لحوالي 5 دقائق لـ Anthropic و 30 دقيقة لـ OpenAI كإعداد افتراضي. ادفع مقابل الاحتفاظ الأطول في Anthropic فقط عندما يكون هناك فجوات في حركة المرور (traffic) الخاصة بك.
الثابت أولاً، والمتغير أخيراً
لأغراض التخزين المؤقت، تنقسم حالة LangGraph إلى مجموعتين.
ثابتة بالبايت عبر الجلسة: مخططات الأدوات، موجّه النظام، السياسات، أمثلة few-shot، والمستندات التي تم استرجاعها (retrieval) مرة واحدة عند بدء الجلسة.
تتغير في كل دور: رسائل المستخدم الجديدة، نتائج الأدوات، الطوابع الزمنية، السياق الخاص بالمستخدم، ومعرفات التتبع أو الطلب (trace/request IDs).
تتبع القاعدة آلية البادئة. المحتوى الثابت يأتي أولاً ويجب أن يكون متطابقاً حتى مستوى البايت. المحتوى المتغير يأتي أخيراً. السجل (History) هو للإلحاق فقط (append-only)، لذا فهو يعمل كبادئة ثابتة متنامية طالما أنك لا تعيد كتابة الأدوار السابقة أبداً.
الفشل الأكثر شيوعاً هو الإعداد الافتراضي لإطار العمل أو أداة مساعدة تضع Current time: {now} أو معرف التتبع في موجّه النظام. هذا السطر الواحد يفرض دفع السعر الكامل لموجّه النظام في كل استدعاء.
كود تجميع الرسائل
تستدعي هذه العقدة Anthropic SDK مباشرة، لذا فإن كل بايت من الحمولة يكون مرئياً. نفس الهيكل يعمل من خلال نموذج الدردشة Anthropic في LangChain، والذي يقبل cache_control في كتل المحتوى.
</>View technical implementation · عرض التفاصيل التقنية
import json
from typing import TypedDict, Annotated
import operator
import anthropic
client = anthropic.Anthropic()
# Loaded once at startup from versioned files. Never formatted per request.
STATIC_SYSTEM = open("prompts/support_agent.v12.md", encoding="utf-8").read()
def load_tools() -> list[dict]:
tools = json.load(open("tools/support_tools.v12.json", encoding="utf-8"))
# Deterministic order and key layout, so the serialized bytes never drift.
tools = sorted(tools, key=lambda t: t["name"])
return json.loads(json.dumps(tools, sort_keys=True))
TOOLS = load_tools()
# Breakpoint 1: caches the tool block on its own.
TOOLS[-1] = {**TOOLS[-1], "cache_control": {"type": "ephemeral"}}
class AgentState(TypedDict):
# Anthropic-format turns. Append-only: earlier turns are never edited.
messages: Annotated[list[dict], operator.add]
session: dict # tier, locale, trace_id: NOT sent in the system prompt
def ingest_user_turn(text: str, session: dict, now_iso: str) -> dict:
"""Freeze volatile context into the user turn once, when it arrives.
The header is persisted with the turn, so it is identical on every
later call. Re-generating it at send time would break the history cache.
"""
header = (
f"<context>\nlocale: {session['locale']}\n"
f"tier: {session['tier']}\nreceived_at: {now_iso}\n</context>\n\n"
)
return {"role": "user", "content": [{"type": "text", "text": header + text}]}
def build_request(state: AgentState) -> dict:
system = [
# Breakpoint 2: tools + system prompt.
{"type": "text", "text": STATIC_SYSTEM,
"cache_control": {"type": "ephemeral"}},
]
# Copy, never mutate state: stale breakpoints would pile up past the limit of 4.
messages = [{**m, "content": [dict(b) for b in m["content"]]}
for m in state["messages"]]
# Breakpoint 3: rolling marker on the newest block caches the history so far.
messages[-1]["content"][-1]["cache_control"] = {"type": "ephemeral"}
return {"system": system, "tools": TOOLS, "messages": messages}
def agent_node(state: AgentState) -> dict:
req = build_request(state)
resp = client.messages.create(
model=MODEL_ID, # pinned per environment
max_tokens=1024,
metadata={"user_id": state["session"]["user_hash"]}, # outside the prompt
**req,
)
log_cache_usage(resp.usage, trace_id=state["session"]["trace_id"])
return {"messages": [{"role": "assistant", "content": resp.content}]}
ثلاثة تفاصيل تحمل معظم الأهمية.
معرف التتبع لا يدخل أبداً في الموجّه. يذهب إلى طبقة التتبع الخاصة بك، مثل Langfuse callback، أو إعدادات تشغيل LangGraph، أو المسجل (logger) الخاص بك. النموذج لا يحتاجه للإجابة.
يتم تجميد الطابع الزمني في دور المستخدم عند وصول ذلك الدور. كل استدعاء لاحق يرى نفس البايتات. إذا قمت بإعادة بناء الترويسة (header) في وقت الإرسال بدلاً من ذلك، سيتغير كل دور سابق ولن يتم استخدام الذاكرة المؤقتة للسجل أبداً.
نقطة التوقف المتحركة تتقدم في كل دور. تتحقق Anthropic للخلف من نقطة التوقف بحثاً عن أطول بادئة تم تخزينها مؤقتاً مسبقاً. لا يزال يتم العثور على الذاكرة المؤقتة للدور السابق عندما تتحرك العلامة، ضمن نافذة بحث محدودة لكتل المحتوى.
أثبت أنها تعمل (hitting)
لا تثق بمخطط البنية (architecture diagram). اقرأ حقول الاستخدام.
</>View technical implementation · عرض التفاصيل التقنية
def log_cache_usage(usage, trace_id: str) -> None:
logger.info(
"llm_usage",
trace_id=trace_id,
input=usage.input_tokens, # uncached, full price
cache_write=usage.cache_creation_input_tokens, # written this call
cache_read=usage.cache_read_input_tokens, # served from cache
)
في OpenAI، الحقل المكافئ هو usage.prompt_tokens_details.cached_tokens.
ضع تأكيداً (assertion) في حزمة الاختبار الخاصة بك. قم بتشغيل نفس الجلسة مرتين خلال دقيقة واحدة، وتحقق من أن cache_read في الاستدعاء الثاني يغطي على الأقل كتلة الأدوات والنظام. هذا الاختبار الواحد يكتشف معظم التراجعات (regressions) قبل أن تصل إلى الفاتورة. يجب أن يكون جنباً إلى جنب مع فحوصات إنتاج الوكيل الأخرى الخاصة بك.
معادلة التكلفة، بأرقامك
بكلمات بسيطة: عندما تشترك عشرة استدعاءات في جلسة واحدة في نفس التعليمات، فإن التخزين المؤقت يعني أنك تدفع مقابل تلك التعليمات حوالي مرتين بدلاً من عشر مرات. إليك من أين يأتي ذلك.
استخدم هذه المتغيرات للكتلة الثابتة:
- ▸S = الرموز الثابتة (الأدوات، موجّه النظام، المستندات الثابتة)
- ▸N = استدعاءات النموذج لكل جلسة والتي تعيد استخدامها، داخل عمر الذاكرة المؤقتة
- ▸p = السعر الأساسي للمدخلات لكل رمز
بدون التخزين المؤقت، التكلفة الثابتة لكل جلسة هي N × S × p.
مع الذاكرة المؤقتة لمدة 5 دقائق من Anthropic، تكون S × 1.25p + (N − 1) × S × 0.1p.
بالنسبة لـ N = 10، هذا يعادل 2.15 × S × p مقابل 10 × S × p. أنت تدفع حوالي 21.5% من التكلفة الثابتة غير المخزنة مؤقتاً. هذا الرقم يتبع مباشرة المضاعفات المنشورة من Anthropic. يعتمد توفيرك الحقيقي على مقدار الجزء الثابت من كل طلب، حيث أن السجل ومخرجات الأدوات لا تزال تنمو بالسعر الكامل حتى يتم تخزينها مؤقتاً.
نقطة التعادل (Breakeven) سهلة. الكتابة لمدة 5 دقائق تدفع ثمنها بعد قراءة واحدة: 1.35 مقابل 2.0. الكتابة لمدة ساعة بضعفين تحتاج إلى قراءتين على الأقل للتغلب على عدم التخزين المؤقت. اختر عمر الساعة الواحدة فقط عندما تكون الاستدعاءات في نفس الجلسة غالباً متباعدة بأكثر من 5 دقائق.
يتبع زمن الاستجابة (Latency) نفس المنطق، لكن لا تأخذ نسبة أي شخص كأمر مسلم به. ينمو المكسب مع حصة الموجّه التي يتم تخزينها مؤقتاً. قم بقياس TTFT على حركة المرور الخاصة بك قبل وبعد التغيير.
التخزين المؤقت لا يفيد في عمليات إعادة المحاولة (retries) أو في النفقات الإضافية للتقييم (evaluation overhead). نموذج التكلفة الكامل بما في ذلك إعادة المحاولة والتقييم يجمع الثلاثة معاً.
ما الذي يعطل الذاكرة المؤقتة بصمت
هذا الجدول هو السبب الرئيسي لهذا المقال. كل صف هو نمط نتحقق منه عند مراجعة قاعدة كود الوكيل.
| معطل الذاكرة المؤقتة | ما تراه | الحل |
|---|---|---|
| طابع زمني، تاريخ، أو معرف تتبع في موجّه النظام | cache_read يكون صفراً في كل استدعاء | انقل البيانات المتغيرة إلى دور المستخدم، أو خارج الموجّه بالكامل |
| مخططات الأدوات متسلسلة بترتيب غير مستقر | تختلف الإصابات (Hits) بين عمليات النشر أو العمال (workers) | قم بفرز الأدوات بالاسم وتفريغ JSON بمفاتيح مفروزة |
| المستندات المسترجعة (Retrieved documents) محقونة بترتيب الصلة | إخفاقات في الذاكرة المؤقتة كلما تغير التصنيف قليلاً | قم بفرز مستندات الجلسة الثابتة حسب المعرف (ID) قبل الحقن |
trim_messages تسقط أقدم الأدوار | إخفاق في ذاكرة السجل المؤقتة بعد امتلاء النافذة | قم بتلخيص السجل في مقاطع (chunks) ثابتة على فترات محددة، وليس رسالة منزلقة واحدة في كل مرة |
| ترويسة متغيرة يعاد إنشاؤها في وقت الإرسال | موجّه النظام يصيب، السجل لا يصيب أبداً | احتفظ بالترويسة مع الدور عند وصوله |
| مجموعات أدوات مختلفة لكل وكيل | يتشارك الوكلاء في موجّه نظام ولكن لا يتشاركون أبداً في ذاكرة مؤقتة | الأدوات تأتي أولاً في البادئة. شارك قائمة أدوات واحدة، أو اقبل بذاكرات مؤقتة منفصلة لكل وكيل |
| تفرع متوازي (Parallel fan-out) قبل وجود الذاكرة المؤقتة | N من الوكلاء الفرعيين المتزامنين يدفعون جميعاً سعر الكتابة | أرسل استدعاءً واحداً أولاً، ثم تفرع بمجرد بدء استجابته |
| كتلة ثابتة أقل من الحد الأدنى للمزود | لا يتم ملء أي حقول للذاكرة المؤقتة على الإطلاق | تحقق من الحد الأدنى لنموذجك. دونه، لا ينطبق التخزين المؤقت |
| تبديل النموذج في منتصف الجلسة (على سبيل المثال، بواسطة موجه router) | كل تبديل هو بداية باردة (cold start) | الذاكرات المؤقتة تكون لكل نموذج. قم بالتوجيه عند بدء الجلسة، وليس لكل دور |
تغيير tool_choice، أو وجود الصورة، أو إعدادات التفكير بين الاستدعاءات | إخفاق ذاكرة السجل المؤقتة في Anthropic | حافظ على ثبات هذه الإعدادات داخل الجلسة |
صف التفرع (fan-out) هو الأكثر أهمية لرسوم الوكلاء المتعددين (multi-agent graphs). المشرف الذي يرسل خمسة عمال في وقت واحد، كل منهم بنفس الديباجة المكونة من 8,000 رمز، يكتب تلك الديباجة خمس مرات. استدعاء إحماء واحد، يليه الدفعة المتوازية، يحول أربعاً من تلك الكتابات إلى قراءات. يقلل التخزين المؤقت من سعر كل خطوة؛ لكنه لا يحد من عدد الخطوات التي يستغرقها التشغيل. لذلك لا يزال يتعين عليك استخدام سقوف الرموز وحدود التكلفة كضوابط.
أين يتناسب هذا في إعادة هيكلة أكبر
التخزين المؤقت هو أرخص عمل لتحسين الأداء متاح في نظام الوكيل. لا يغير أي إجابات ولا يحتاج إلى نماذج جديدة. إذا كان الرسم البياني الخاص بك يبني حالياً سلسلة نصية واحدة كبيرة باستخدام f-strings ويرسلها كرسالة مستخدم واحدة، فإن إعادة الهيكلة (refactor) تتكون من ثلاثة أجزاء. انتقل إلى مصفوفة رسائل منظمة. افصل بين الطبقات الثابتة والمتغيرة. أضف تسجيل الاستخدام المذكور أعلاه.
تغطي أنماط الإنتاج لـ LangGraph تصميم الحالة والمخفض (reducer) الذي يعتمد عليه هذا. اضبط شكل الحالة بشكل صحيح أولاً، وسيأتي التخزين المؤقت كنتيجة طبيعية.
→ تكلفة واجهات برمجة تطبيقات النماذج اللغوية الكبيرة (LLM APIs) على نطاق واسع: السياق، وإعادة المحاولة، وعبء التقييم → التوجيه الدلالي باستخدام LiteLLM: توجيه كل استعلام لأرخص نموذج قادر على معالجته → LangGraph في بيئة الإنتاج: خمسة أنماط تحافظ على أمان الوكلاءالأسئلة الشائعة
كم تدوم البادئة المخزنة مؤقتاً؟ في Anthropic، 5 دقائق كإعداد افتراضي، يتم تجديدها في كل قراءة، مع عمر اختياري لمدة ساعة واحدة بسعر كتابة أعلى. في OpenAI، تظل البادئة المخزنة مؤقتاً مؤهلة لإعادة الاستخدام لمدة 30 دقيقة بعد أحدث كتابة أو إعادة استخدام لها. تقدم بعض النماذج احتفاظاً ممتداً يصل إلى 24 ساعة. يجب أن تتوقع الأنظمة ذات حركة المرور المنخفضة أن يكون الاستدعاء الأول بعد فترة هدوء بمثابة إخفاق (miss) بالسعر الكامل.
هل ينطبق هذا على النماذج المستضافة ذاتياً؟ نعم. تعيش الآلية في خادم الاستنتاج (inference server)، وليس في أوزان النموذج. يدعم كل من vLLM و SGLang التخزين المؤقت التلقائي للبادئة، لذا فإن نفس قواعد الترتيب تحدد معدل الإصابة (hit rate) الخاص بك. لا يوجد خصم لكل رمز على أجهزتك الخاصة. العائد هو TTFT أقل والمزيد من الجلسات المتزامنة لكل وحدة معالجة رسومات (GPU). يغطي دليل التقديم المحلي الخاص بنا الإعدادات ذات الصلة.
هل يمكن أن يسرب التخزين المؤقت بيانات عميل إلى آخر؟ يتم تحديد نطاق الذاكرات المؤقتة للمزود لمؤسستك، لذا لا يمكن للعملاء الآخرين قراءتها. الخطر الحقيقي يكمن في الكود الخاص بك. إذا كانت البادئة الثابتة المشتركة تحتوي على بيانات مستخدم واحد، فإنها لم تعد ثابتة، ولم يكن ينبغي أن تكون هناك أبداً. احتفظ بالسياسة والأدوات المشتركة في البادئة، واحتفظ بأي شيء خاص بالمستخدم في أدوار ذلك المستخدم الخاصة.
هذا الأسبوع: أثبت أن الذاكرة المؤقتة تعمل
أضف تأكيد cache_read أعلاه إلى حزمة الاختبار الخاصة بك: قم بتشغيل جلسة واحدة مرتين خلال دقيقة وتأكد من أن cache_read في الاستدعاء الثاني يغطي كتلة الأدوات والنظام. ثم افتح تتبع إنتاج (production trace) واحد واقرأ حقول الاستخدام الثلاثة. إذا كان cache_read صفراً، فإن أحد الصفوف في الجدول موجود في الكود الخاص بك، والعثور عليه عادة ما يكون عمل فترة ما بعد الظهيرة.
