أبلغ أحد المستخدمين أن المكون الإضافي للترجمة (translation plugin) الخاص بنا كان يُرجع نفس النتيجة المخزنة مؤقتًا (cached result) لكل طلب، بغض النظر عن المدخلات. قمنا بالتحقيق ووجدنا ما هو أسوأ: 95% من جميع حالات الـ semantic cache hits عبر منصتنا كانت نتائج إيجابية خاطئة (false positives). 199 طلب ترجمة مختلف، 198 محتوى طلب فريد، واستجابة واحدة مخزنة مؤقتًا تم تقديمها للجميع.
إذا كنت تهتم بحالة الـ agent طويلة الأمد ومعالجة طلبات الإنتاج (production requests)، فإن هذا المنشور يتماشى جيدًا مع لماذا يستمر وكيل الذكاء الاصطناعي الخاص بك في فقدان ذاكرته، و دليل بناء chatbot بمفتاح واحد، و دليل تحديد معدل الـ AI API.
تقرير الخطأ (The Bug Report)
كان التقرير بسيطًا: "لقد قمت بتعطيل الـ semantic cache، ولكن كل ترجمة تُرجع نفس النتيجة."
ثلاثة معرفات طلبات (request IDs)، ثلاثة أجزاء ترجمة مختلفة، واستجابات مخزنة مؤقتًا متطابقة. تراوحت أحجام محتوى الطلبات من 1,564 إلى 8,676 bytes. كان معرف الاستجابة المخزنة مؤقتًا هو نفسه في جميع الطلبات: chatcmpl-DG6J03nhdvcF7Ek0C8rJkjh7lN9pF.
الشك الأول: لم تكن إعدادات الـ cache الخاصة بالمستخدم تُطبق. تبين أن ذلك كان خطأ منفصلاً في مزامنة مصدر البيانات (لوحة الإدارة كانت تكتب في جدول، و الـ API gateway يقرأ من جدول آخر). لكن إصلاح ذلك لم يحل سوى نصف المشكلة. فحتى مع تمكين الـ cache وعمله بشكل صحيح، كانت الـ semantic cache تطابق طلبات لا ينبغي أن تتطابق أبدًا.
بيانات الإنتاج (The Production Data)
قمنا بسحب بيانات الـ cache hit لمدة 24 ساعة من ClickHouse. كانت الأرقام سيئة.
| النموذج (Model) | إجمالي الطلبات | الـ Cache Hits | الطلبات الفريدة | الاستجابات الفريدة | معدل الـ Hit |
|---|---|---|---|---|---|
| gpt-4.1-nano | 200 | 199 | 198 | 1 | 99.5% |
| glm-4.6-thinking | 100 | 38 | 13 | 1 | 38% |
| gpt-5-nano | 31 | 29 | 28 | 2 | 93.5% |
| gpt-oss-120b | 18 | 17 | 17 | 1 | 94.4% |
| qwen3-vl-flash | 17 | 16 | 16 | 1 | 94.1% |
198 طلب ترجمة فريد، جميعها تُرجع نفس الاستجابة الوحيدة المخزنة مؤقتًا. هذا ليس cache. هذه وظيفة معطلة تُرجع قيمة ثابتة.
كل نموذج متأثر شارك صفتين: جميع الطلبات جاءت من مستخدم واحد، وجميعها استخدمت قالب prompt نظام ثابت مع محتوى مستخدم متغير.
لماذا تفشل الـ Embeddings في المدخلات المهيكلة
يرسل المكون الإضافي للترجمة طلبات مثل هذه:
System: "Act as a translation API. Output a single raw JSON object only.
Input: {"targetLanguage":"<lang>","title":"...","segments":[...]}"
User: {"targetLanguage":"zh","title":"Product Page",
"description":"Translate product descriptions",
"tone":"formal",
"segments":[{"text":"actual varying content here"}]}
الـ system prompt متطابق في جميع الطلبات. رسالة المستخدم هي كائن JSON حيث تكون قيم targetLanguage و title و description و tone ثابتة. فقط segments[].text هو ما يتغير.
عندما تقوم الـ semantic cache الخاصة بنا باستخراج النص لعمل الـ embedding، فإنها تدمج الـ system prompt ورسالة المستخدم. يمثل القالب الثابت حوالي 80% من النص. يقوم نموذج الـ embedding (وهو all-mpnet-base-v2، بـ 768 بُعدًا) بضغط هذا في vector حيث يهيمن هيكل القالب. محتوى الترجمة الفعلي بالكاد يؤثر على النتيجة.
النتيجة: الـ cosine similarity بين "ترجم 'Hello world'" و "ترجم 'The quarterly financial report shows a 15% increase in revenue'" تتجاوز 0.95. الحد الأدنى (threshold) لدينا هو 0.95. كل طلب ترجمة يطابق أول إدخال مخزن مؤقتًا.
بالبحث في السجلات، وجدنا ثلاث طرق يؤدي بها ذلك إلى حدوث خلل:
المكون الإضافي للترجمة هو الأسوأ. مفاتيح وقيم JSON الثابتة تطغى على أجزاء الترجمة الفعلية. كل من gpt-4.1-nano و gpt-5-nano واجها هذه المشكلة.
واجه مساعد تلخيص السياق (context summarization assistant) نوعًا مختلفًا من نفس المشكلة. كان الـ system prompt الخاص به طويلاً جدًا لدرجة أن محتوى المستخدم (الذي يتراوح من 5KB إلى 47KB) بالكاد يظهر في الـ embedding. هكذا انتهى الأمر بـ glm-4.6-thinking بإرجاع نفس الملخص لكل محادثة.
كان النمط الثالث أكثر دقة. بالنسبة لـ gpt-oss-120b و qwen3-vl-flash، كانت أول 500 حرف من كل طلب متطابقة تمامًا. جاء المحتوى المتغير بعد ذلك، لكن الـ embedding كان قد هيمن عليه بالفعل البادئة المشتركة.
ماذا تقول الأبحاث
هذه ليست مشكلة جديدة. لقد قامت أوراق بحثية حديثة بقياسها.
وجد مشروع vCache التابع لجامعة UC Berkeley أن حالات الـ cache hits الصحيحة وغير الصحيحة لها "توزيعات تشابه متداخلة للغاية". يختلف الـ threshold الأمثل من 0.71 إلى 1.0 عبر المدخلات المختلفة المخزنة مؤقتًا. لا يوجد رقم واحد يصلح للجميع. حلهم: تعلم threshold منفصل لكل إدخال في الـ cache، مما قلل معدلات الخطأ بمقدار 6 مرات مع مضاعفة معدلات الـ hit. (vCache, 2025)
ويزداد الأمر سوءًا عند خلط أنواع الاستعلامات. أظهرت دراسة حول الـ caching المدرك للفئات (category-aware caching) أن threshold بقيمة 0.80 ينتج 15% من المطابقات الخاطئة في استعلامات الكود (sort_ascending مقابل sort_descending)، بينما يفشل نفس الـ threshold في العثور على إعادة صياغة صالحة في الاستعلامات الحوارية. عتبة واحدة، ونمطان من الفشل. (Category-Aware Semantic Caching, 2025)
البنوك واجهت هذا أيضًا. وثقت دراسة حالة من InfoQ نظام RAG حيث تطابق سؤال "هل يمكنني تخطي دفعة القرض هذا الشهر" مع "ماذا يحدث إذا فاتني دفع القرض" بنسبة تشابه 88.7%. نية مختلفة، نفس الإجابة المخزنة مؤقتًا. بدأوا بمعدل نتائج إيجابية خاطئة بنسبة 99% واحتاجوا إلى أربع جولات من التحسين للوصول إلى 3.8%. (InfoQ Banking Case Study, 2025)
المشكلة الأعمق: تقيس الـ embeddings ما إذا كان الـ prompts متشابهين دلاليًا، وليس ما إذا كانت نفس الاستجابة يمكن أن تجيب على كليهما. هذه الفجوة هي المكان الذي تعيش فيه حالات الـ cache hits الخاطئة. (Efficient Prompt Caching via Embedding Similarity, 2024)
كل ورقة بحثية وجدناها تتفق على شيء واحد: تشابه الـ embedding وحده لا يكفي. أنت بحاجة إلى طبقة تحقق (verification layer).
الإصلاح ذو الطبقتين
قمنا ببناء وسيلتي دفاع. الأولى تزيل ضجيج القوالب (template noise) قبل عملية الـ embedding. والثانية تتحقق من الـ hits بعد المطابقة.
الطبقة 2: استخراج المحتوى للـ Embeddings
قبل إنشاء الـ embedding، نقوم الآن باكتشاف المدخلات المهيكلة (JSON) واستخراج المحتوى المتغير ذو المعنى فقط.
المنطق:
- التحقق مما إذا كان محتوى الرسالة يبدأ بـ
{أو[ - إذا تم تحليله كـ JSON، يتم جمع كل قيم النصوص النهائية (string leaf values) بشكل متكرر
- تصفية القيم القصيرة (20 حرفًا أو أقل) لأنها عادة ما تكون حقول إعدادات مثل
"zh"أو"formal"أو"Product Page" - إذا كان النص المستخرج قصيرًا جدًا أو فارغًا، يتم الرجوع إلى النص الأصلي
function extractContentForEmbedding(text: string): string {
const extracted = tryExtractJsonContent(text);
return extracted && extracted.length > 20 ? extracted : text;
}
ينطبق هذا على كل من الـ system prompt ورسالة المستخدم. بالنسبة للمكون الإضافي للترجمة، يمثل الـ embedding الآن "Hello world" بدلاً من JSON blob بحجم 2KB. بالنسبة لمساعد التلخيص، فإنه يسحب المحادثة الفعلية من غلاف القالب.
تم اختيار حد 20 حرفًا بناءً على التجربة:
"zh"(حرفان): تمت تصفيته. قيمة إعدادات."formal"(6 أحرف): تمت تصفيته. قيمة إعدادات."Product Page"(12 حرفًا): تمت تصفيته. حقل قالب."Translate product descriptions"(31 حرفًا): تم الاحتفاظ به. محتوى ذو معنى."The quarterly financial report..."(أكثر من 40 حرفًا): تم الاحتفاظ به. محتوى الترجمة الفعلي.
الطبقة 3: التحقق من البصمة (Fingerprint Verification)
بعد حدوث semantic cache hit، نقوم بمقارنة hash للنص المستخرج من الطلب الحالي مع الـ hash المخزن في الإدخال المخزن مؤقتًا. إذا لم يتطابقا، يتم رفض الـ hit.
// On cache write
entry.metadata.textHash = fnv1aHash(extractedText);
// On cache read, after finding a similarity match
if (entry.metadata.textHash !== undefined) {
if (entry.metadata.textHash !== fnv1aHash(currentExtractedText)) {
// False positive: semantically similar but different content
metrics.recordFingerprintRejection();
return null;
}
}
يستخدم الـ hash النص المستخرج (بعد الطبقة 2)، وليس المدخلات الخام. الطلبان اللذان يحتويان على أغلفة قوالب مختلفة ولكن محتوى فعلي متطابق سيظلان متطابقين. محتوى مختلف، hash مختلف، يتم الرفض.
تتخطى إدخالات الـ cache القديمة التي لا تحتوي على textHash عملية التحقق (متوافقة مع الإصدارات السابقة). ستنتهي صلاحيتها بشكل طبيعي عبر الـ TTL.
نحن نستخدم FNV-1a (32-bit) للـ hash. فهو سريع، وحتمي، ومعدل تصادم يبلغ حوالي 1 في 4 مليارات وهو أمر جيد للتحقق من cache hit واحد.
لماذا لا نكتفي برفع الـ Threshold؟
الـ threshold لدينا هو بالفعل 0.95. رفعه لا يساعد. المشكلة هي أن المدخلات المتشابهة هيكليًا تنتج درجات تشابه أعلى من 0.95 بغض النظر عما يقوله المحتوى الفعلي.
تدعم بيانات vCache هذا: توزيعات التشابه للـ hits الصحيحة والخاطئة تتداخل لدرجة أنه لا يوجد حد فاصل واحد يفصل بينهما. ارفع الـ threshold إلى 0.99 وستقضي على حالات الـ cache hits المشروعة لإعادة الصياغة دون القضاء على النتائج الإيجابية الخاطئة من الطلبات كثيفة القوالب.
أصلح المدخلات، وتحقق من المخرجات. لا تتلاعب بالـ threshold.
النتائج
مع نشر كلتا الطبقتين:
| المقياس | قبل | بعد |
|---|---|---|
| gpt-4.1-nano نتائج إيجابية خاطئة | 198/199 | 0 |
| حصة النتائج الإيجابية الخاطئة من جميع الـ cache hits | ~95% | <5% |
| معدل الـ cache hit المشروع | لم يتغير | لم يتغير |
| زمن الاستجابة المضاف لكل طلب | 0 | <1ms (JSON parse + FNV hash) |
الطبقة 2 وحدها كانت ستصلح المكون الإضافي للترجمة. الطبقة 3 هي شبكة الأمان للحالات التي لا يفصل فيها استخراج JSON المحتوى تمامًا، أو للمدخلات المهيكلة التي ليست بتنسيق JSON.
الخلاصات
إذا كنت تقوم بتشغيل semantic cache في بيئة الإنتاج:
راقب تنوع الاستجابات. إذا كان للنموذج معدل cache hit بنسبة 100% واستجابة فريدة واحدة، فلديك مشكلة نتائج إيجابية خاطئة. الاستعلام:
SELECT model, uniqExact(substring(response_body, 1, 200)) as unique_responses, count() as total FROM request_logs WHERE cache_hit = true GROUP BY model.المدخلات المهيكلة تقتل الـ embedding البسيط. أي طلب بقالب ثابت (JSON APIs، أغلفة system prompt، مهام ملء النماذج) سينتج درجات تشابه عالية بشكل مصطنع. قم بالمعالجة المسبقة قبل الـ embedding.
طبقة التحقق ليست اختيارية. كل semantic cache للإنتاج في الأدبيات البحثية تحتوي على واحدة. السؤال هو ما إذا كنت تستخدم فحص hash خفيف الوزن، أو cross-encoder reranker، أو مكالمة تحقق كاملة من LLM. اختر بناءً على ميزانية زمن الاستجابة (latency budget) لديك.
الـ thresholds العالمية هي حل وسط، وليست حلاً. تحتاج أنواع الاستعلامات المختلفة إلى thresholds مختلفة. إذا لم تتمكن من عمل thresholds لكل فئة أو لكل إدخال، فأضف على الأقل معالجة مسبقة للمدخلات لتوحيد جودة الـ embedding عبر الفئات.
يمكن للـ semantic caching أن يقلل من 30-70% من تكاليف LLM API. ولكن بدون المعالجة المسبقة للمدخلات والتحقق من الـ hits، فأنت تقدم إجابات قديمة وتسميها فوزًا في الأداء.
توفر LemonData وصولاً موحدًا إلى أكثر من 300 نموذج ذكاء اصطناعي مع caching مدمج، وتوجيه (routing)، وتحسين التكلفة. جربها مجانًا برصيد 1 دولار.
