Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 33

بناء ذاكرة محادثة قصيرة المدى

Recommended resources

أهلاً بك في الدرس الجديد! في الدرس الماضي، قمنا بربط وكيلنا بـ "عقل" ذكي، وهو نموذج لغوي كبير (LLM). أصبح وكيلنا قادراً على التفكير وتقديم إجابات ديناميكية. لكنه يعاني من مشكلة كبيرة: ذاكرته ضعيفة جداً، فهو ينسى كل شيء بمجرد انتهاء التفاعل.

هدفنا لهذا الدرس هو تحقيق المخرج التالي: بناء نظام ذاكرة بسيط قصير المدى (Short-Term Memory) لتخزين سياق المحادثة. سنقوم بتطوير وكيلنا ليتذكر التفاعلات السابقة داخل الجلسة الواحدة، مما يجعله قادراً على خوض محادثات مترابطة وذات معنى.

بنهاية هذا الدرس، سيتحول وكيلك من مجرد مستجيب لمرة واحدة إلى محاور حقيقي يتذكر ما قلته.


1. لماذا يحتاج الوكيل إلى ذاكرة؟ مشكلة انعدام الحالة (Statelessness)

في الدرس السابق، قمنا بإنشاء قائمة conversation_history داخل وكيلنا. كانت هذه القائمة تحتفظ بسياق المحادثة أثناء الجولة الواحدة فقط. ولكن في كل مرة نتفاعل فيها مع نموذج اللغة عبر واجهة برمجة التطبيقات (API)، يكون الأمر أشبه ببدء محادثة جديدة تماماً. لا يتذكر النموذج ما قلته له في الطلب السابق. هذا ما يُعرف بأنه "عديم الحالة" (Stateless).

تخيل أنك تتحدث مع شخص ينسى هويتك وما تحدثتما عنه كل ثلاثين ثانية. سيكون الحوار معه مستحيلاً! هذا هو حال وكيلنا الآن.

أهمية الذاكرة المحادثة لوكلاء الذكاء الاصطناعي
توضح هذه الصورة الفرق الجوهري بين وكيل يمتلك ذاكرة (يسار) وآخر لا يمتلكها (يمين). الذاكرة تسمح للوكيل بفهم السياق وتقديم ردود متماسكة، وهو ما نسعى لتحقيقه اليوم.

لنفهم هذه المشكلة بشكل عملي، شاهد المقطع التالي الذي يوضح كيف أن نماذج اللغة الكبيرة، عند استخدامها عبر الـ API، لا تتذكر أي شيء من تلقاء نفسها.

LangChain: Giving Memory to LLMs

لفهم سبب أهمية الذاكرة، دعنا أولاً نرى ما يحدث عندما لا يمتلك النموذج اللغوي ذاكرة. يوضح هذا الفيديو الطبيعة 'عديمة الحالة' لاستدعاءات واجهة برمجة تطبيقات النموذج اللغوي.

شاهد من البداية حتى الدقيقة 01:43. لاحظ كيف ينسى النموذج 'الكلمة السرية' في تفاعل لاحق ومنفصل. هذا بالضبط ما نريد حله.

كما رأيت في الفيديو، كل طلب API هو عالم مستقل. الحل هو أن نقوم نحن، في برنامجنا، بتجميع سجل المحادثة وإرساله بالكامل مع كل طلب جديد. بهذه الطريقة، نحن "نُذكّر" النموذج بالسياق في كل مرة نتحدث إليه.



2. الحل الأبسط: ذاكرة التخزين المؤقت للمحادثة (Conversation Buffer Memory)

إن أبسط استراتيجية لبناء ذاكرة هي الاحتفاظ بسجل كامل لكل الرسائل المتبادلة بين المستخدم والوكيل، ثم إرسال هذا السجل بأكمله إلى النموذج اللغوي مع كل سؤال جديد. هذا يضمن أن النموذج لديه دائمًا السياق الكامل للمحادثة.

الفيديو الذي شاهدته يقدم هذا المفهوم. دعنا نلقي نظرة على الجزء الذي يشرح هذه الفكرة.

LangChain: Giving Memory to LLMs

يقدم نفس الفيديو الحل الأبسط لهذه المشكلة، والذي يسميه 'Conversation Buffer Memory'. دعنا نشاهده لنفهم المفهوم. لاحظ أن الفيديو يستخدم مكتبة تسمى LangChain، لكننا سنقوم بتطبيق الفكرة الأساسية بأنفسنا في وكيلنا الخاص.

شاهد من الدقيقة 02:05 إلى 04:10. ركّز على الفكرة المتمثلة في كيفية تضمين المحادثة السابقة ('hi remember the secret word is cat') في الطلب الجديد الذي يحتوي على السؤال التالي ('what is the capital of France'). هذا هو جوهر ذاكرة التخزين المؤقت.

الفكرة بسيطة وقوية: الذاكرة ليست شيئًا سحريًا داخل النموذج اللغوي، بل هي قائمة من الرسائل التي نديرها ونحافظ عليها في برنامجنا.



3. بناء مكون الذاكرة في وكيلنا

للحفاظ على تنظيم الكود، سننشئ "فئة" (Class) مخصصة للذاكرة. هذا يتماشى مع مبدأ بناء الوكلاء من مكونات منفصلة وقابلة لإعادة الاستخدام. بعد ذلك، سنجعل وكيلنا يستخدم هذه الفكرة للسماح بمحادثة مستمرة.

سنقوم بتحديث ملف agent_v2.py ونحفظه باسم agent_v3.py.

# agent_v3.py

import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

class Memory:
    """
    فئة بسيطة لإدارة ذاكرة المحادثة قصيرة المدى.
    """
    def __init__(self, system_prompt):
        # يبدأ سجل المحادثة دائماً بتعليمات النظام (شخصية الوكيل)
        self.system_prompt = {"role": "system", "content": system_prompt}
        self.history = [self.system_prompt]

    def add_message(self, role, content):
        """
        تضيف رسالة جديدة (من المستخدم أو المساعد) إلى السجل.
        """
        self.history.append({"role": role, "content": content})

    def get_history(self):
        """
        تُرجع سجل المحادثة الكامل.
        """
        return self.history

class Agent:
    """
    الوكيل الآن يستخدم فئة Memory لإدارة سياق المحادثة.
    """
    def __init__(self, goal):
        print(f"تم إنشاء وكيل جديد بهدف: '{goal}'")
        self.goal = goal
        self.llm_client = OpenAI()
        
        # إعداد الذاكرة مع تعليمات النظام الأولية
        system_prompt = "أنت مساعد ذكي ومفيد. مهمتك هي تحليل الملاحظات واتخاذ قرار حول الخطوة التالية بناءً على سياق المحادثة."
        self.memory = Memory(system_prompt=system_prompt)

    def think_and_act(self, observation):
        """
        يدمج خطوتي التفكير والتصرف.
        يأخذ ملاحظة، يضيفها للذاكرة، يستدعي الـ LLM، ويضيف الرد للذاكرة.
        """
        print(f"🤔 المستخدم قال: '{observation}'")

        # 1. إضافة ملاحظة المستخدم إلى الذاكرة
        self.memory.add_message("user", observation)
        
        # 2. استدعاء الـ LLM مع سجل المحادثة الكامل
        chat_completion = self.llm_client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=self.memory.get_history()
        )
        
        decision = chat_completion.choices[0].message.content
        print(f"🧠 قرار الـ LLM: '{decision}'")
        
        # 3. إضافة رد المساعد (القرار) إلى الذاكرة للحفاظ على السياق
        self.memory.add_message("assistant", decision)
        
        # 4. في هذا الإصدار، التصرف هو ببساطة إرجاع القرار
        return decision

# --- الجزء الرئيسي من البرنامج (الآن في وضع المحادثة) ---
if __name__ == "__main__":
    my_agent = Agent("إجراء محادثة مفيدة مع المستخدم وتذكر السياق")
    
    print("الوكيل جاهز للمحادثة. اكتب 'خروج' لإنهاء الحوار.")

    while True:
        user_input = input("أنت: ")
        
        if user_input.lower() == 'خروج':
            print("الوكيل: إلى اللقاء!")
            break
            
        # الوكيل يفكر ويتصرف بناءً على مدخلاتك
        response = my_agent.think_and_act(user_input)
        
        print(f"الوكيل: {response}")

تحليل التغييرات الرئيسية:

  1. class Memory: أنشأنا فئة جديدة تمامًا لإدارة الذاكرة.

    • __init__: عند إنشائها، تأخذ "تعليمات النظام" (system prompt) وتجعلها أول عنصر في قائمة history. هذا يعطي الوكيل "شخصيته" الدائمة.
    • add_message: طريقة واضحة لإضافة رسائل جديدة.
    • get_history: طريقة للحصول على السجل الكامل لإرساله إلى الـ API.
  2. class Agent:

    • في __init__، لم نعد ننشئ قائمة conversation_history مباشرة، بل ننشئ كائنًا من فئة Memory. هذا يفصل مسؤولية إدارة الذاكرة عن الوكيل نفسه.
    • دمجنا think و act في تابع واحد think_and_act لتبسيط الحلقة الرئيسية. هذا التابع الآن يستخدم self.memory لجلب السجل وتحديثه.
  3. الحلقة الرئيسية (if __name__ == "__main__":):

    • تغيرت بالكامل لتصبح حلقة محادثة تفاعلية (while True).
    • تأخذ مدخلاً من المستخدم في كل دورة.
    • تستدعي my_agent.think_and_act الذي يقوم بكل العمل (التفكير، استدعاء الـ LLM، تحديث الذاكرة).
    • تطبع رد الوكيل وتنتظر المدخل التالي.

جرب بنفسك!

قم بتشغيل الكود (python agent_v3.py). الآن، يمكنك إجراء محادثة حقيقية مع وكيلك. جرب هذا الحوار:

  1. أنت: مرحباً، اسمي خالد.
  2. الوكيل: (سيرد بترحيب).
  3. أنت: ما هو اسمي؟

إذا سار كل شيء على ما يرام، يجب أن يتذكر الوكيل اسمك ويجيب "اسمك خالد". لقد نجحت في بناء وكيل بذاكرة!

اختبر فهمك!

كيف يمكنك تعديل فئة Memory لإضافة طريقة clear() التي تمسح سجل المحادثة وتعيده إلى الحالة الأولية (يحتوي فقط على تعليمات النظام)؟

أظهر الإجابة

يمكنك إضافة التابع التالي إلى فئة Memory:

# داخل class Memory:

def clear(self):
    """
    يمسح الذاكرة ويعيدها إلى الحالة الأولية.
    """
    print("... يتم مسح الذاكرة ...")
    self.history = [self.system_prompt]

بعد ذلك، يمكنك استدعاء my_agent.memory.clear() في الحلقة الرئيسية لمسح ذاكرة الوكيل وبدء محادثة جديدة من الصفر دون الحاجة إلى إعادة تشغيل البرنامج.



خلاصة الدرس

في هذا الدرس، عالجنا إحدى أكبر نقاط ضعف وكيلنا: انعدام الذاكرة. لقد حولناه من مجرد منفذ للمهام لمرة واحدة إلى شريك حواري قادر على متابعة السياق.

أهم النقاط التي تعلمناها:

  • نماذج اللغة الكبيرة بطبيعتها "عديمة الحالة" (Stateless)، وكل استدعاء API هو تفاعل مستقل.
  • لإعطاء الوكيل ذاكرة، يجب أن نحتفظ بسجل المحادثة في برنامجنا ونرسله مع كل طلب جديد.
  • قمنا ببناء أبسط أشكال الذاكرة قصيرة المدى، وهي "ذاكرة التخزين المؤقت للمحادثة" (Conversation Buffer Memory)، التي تحتفظ بكل شيء.
  • فصلنا منطق الذاكرة في فئة Memory خاصة به، مما يجعل الكود أكثر تنظيماً وقابلية للتطوير.
  • قمنا بتعديل الحلقة الرئيسية للسماح بمحادثة مستمرة، مما يثبت فعالية نظام الذاكرة الجديد.

ماذا بعد؟

نظام الذاكرة الذي بنيناه فعال، لكنه ساذج بعض الشيء. فهو يجمع كل كلمة قيلت في المحادثة. ماذا سيحدث لو استمرت المحادثة لساعات؟ سيصبح سجل المحادثة ضخمًا جدًا.

نماذج اللغة الكبيرة لها حد أقصى لكمية النص التي يمكنها معالجتها في المرة الواحدة، وهو ما يسمى "نافذة السياق" (Context Window). في الدرس القادم، سنتعمق في فهم هذه النافذة، وكيف تؤثر على وكيلنا، وسنستكشف استراتيجيات أولية للتعامل مع هذا التحدي.