التخزين المؤقت الدلالي: خفّض تكلفة نموذج اللغة على الأسئلة المتكررة
المستوى: متوسط. المقال ده مناسب لو عندك تطبيق شغّال فعلاً على نموذج لغة (شات بوت دعم، مساعد داخلي، بحث بالأسئلة) وعايز تقلّل التكلفة والبطء من غير ما تغيّر النموذج ولا تنزّل جودة الردود.
لو تطبيقك بيبعت كل سؤال للـ LLM حتى لو اتسأل قبل كده بصياغة مختلفة، إنت بتدفع مرتين على نفس الإجابة. التخزين المؤقت الدلالي بيخلّي "إعادة تعيين الباسورد" و"نسيت الباسورد اعمل إيه" يوصّلوا لنفس الإجابة المخزّنة، بدل استدعاء جديد ومكلف في كل مرة.
المشكلة باختصار
الكاش التقليدي بيشتغل بالتطابق الحرفي: نفس النص بالظبط يرجّع نفس النتيجة. ده بيفشل مع نماذج اللغة، لأن المستخدمين بيسألوا نفس السؤال بمليون صياغة. النتيجة إن نسبة الإصابة (cache hit) بتكون شبه صفر، وكل طلب بيروح للنموذج. في بوت دعم عادي، حوالي 60 إلى 70% من الترافيك بيكون تكرار لنفس عدد محدود من الأسئلة. يعني إنت بتهدر ثلثين فاتورتك على إجابات اتحسبت قبل كده.
الفكرة بمثال بسيط
تخيّل موظف دعم شاطر. أول ما حد يسأل "إزاي أغيّر باقتي؟"، بيجاوب ويكتب الإجابة في دفتر. لما ييجي عميل تاني ويقول "عايز أرفّع الاشتراك بتاعي"، الموظف مبيقولش "دي جملة مختلفة، هفكّر من الأول". هو فاهم إن المعنى واحد، فبيرجع لنفس صفحة الدفتر. التخزين المؤقت الدلالي بيعمل نفس الحكاية بالظبط: بيطابق بالمعنى، مش بالحروف.
يعني إيه بالظبط: من التطابق الحرفي للتطابق بالمعنى
علميًا، بنحوّل كل سؤال لمتجه أرقام (embedding) بيمثّل معناه في فضاء متعدد الأبعاد. الأسئلة المتقاربة في المعنى بيبقى متجهاتها قريبة من بعض. بنقيس القرب ده بتشابه جيب التمام (cosine similarity)، وهو رقم بين 0 و1: كل ما قرب من 1، كل ما المعنى أقرب.
الكاش الدلالي بيشتغل كالتالي: نحوّل السؤال الجديد لمتجه، ندوّر على أقرب سؤال مخزّن، ولو التشابه عدّى عتبة معيّنة (threshold) — مثلاً 0.92 — نرجّع الإجابة المخزّنة على طول. لو مفيش حاجة قريبة كفاية، نستدعي النموذج مرة واحدة ونخزّن النتيجة للمرات الجاية.
مثال تنفيذي: كاش دلالي في أقل من 30 سطر
ده مثال شغّال يعتمد على مكتبة sentence-transformers اللي بتحسب الـ embeddings محليًا وببلاش. الفكرة كلها في دالة واحدة:
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2") # يشتغل محليًا بدون تكلفة API
cache = [] # كل عنصر: (متجه السؤال, الإجابة المخزّنة)
THRESHOLD = 0.92 # ارفعها تقلّل الإصابات الخاطئة، لكن تقلّل نسبة الإصابة
def cosine(a, b):
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
def ask(question, call_llm):
q = model.encode(question)
best_ans, best_sim = None, 0.0
for vec, ans in cache: # بحث خطّي، كفاية للكاش الصغير
sim = cosine(q, vec)
if sim > best_sim:
best_sim, best_ans = sim, ans
if best_sim >= THRESHOLD: # إصابة كاش
return best_ans, f"HIT ({best_sim:.2f})"
answer = call_llm(question) # فشل: استدعِ النموذج مرة واحدة بس
cache.append((q, answer))
return answer, "MISS"