الوحدة 6 — ذاكرة المحادثة
النماذج اللغويّة بلا حالة بطبيعتها: كلّ استدعاء مستقلّ تمامًا عن سابقه. لجعل مساعد النفقات يتذكّر ما قاله المستخدم قبل رسالتين، علينا أن نمرّر تاريخ المحادثة يدويًّا في كلّ استدعاء. هنا يظهر مفهوم الذاكرة.
سجلّ الرسائل: البنية الأساسيّة
يُعرِّف LangChain واجهة BaseChatMessageHistory بعمليّتين: add_message وmessages. أبسط تنفيذ يحفظ في الذاكرة الحيّة:
from langchain_community.chat_message_histories import ChatMessageHistory
historique = ChatMessageHistory()
historique.add_user_message("ما سقف عشاء العمل؟")
historique.add_ai_message("السقف 50 يورو للشخص.")
الآن يُمكن حقن هذه الرسائل في كلّ استدعاء تحت مفتاح history عبر MessagesPlaceholder:
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
tpl = ChatPromptTemplate.from_messages([
("system", "أنت مساعد إدارة نفقات."),
MessagesPlaceholder("history"),
("human", "{question}"),
])
RunnableWithMessageHistory: التركيب الحديث
قديمًا كانت هناك فئات ConversationChain وConversationBufferMemory؛ لم تعد مستحسنة. البديل المعياريّ اليوم يُغلِّف أي Runnable بحقن الذاكرة تلقائيًّا:
from langchain_core.runnables.history import RunnableWithMessageHistory
sessions: dict[str, ChatMessageHistory] = {}
def obtenir_historique(id_session: str) -> ChatMessageHistory:
if id_session not in sessions:
sessions[id_session] = ChatMessageHistory()
return sessions[id_session]
chaine_avec_memoire = RunnableWithMessageHistory(
tpl | ChatOpenAI(model="gpt-4o-mini", temperature=0),
obtenir_historique,
input_messages_key="question",
history_messages_key="history",
)
chaine_avec_memoire.invoke(
{"question": "ما سقف الغداء؟"},
config={"configurable": {"session_id": "user-42"}},
)
قاعدة: كلّ مستخدم (أو محادثة) له session_id مختلف؛ الخلط بين الجلسات كارثة سرّيّة.
المشكلة الكبرى: نافذة السياق
بعد عشرين تبادلًا، تصبح قائمة الرسائل طويلة جدًّا. نافذة السياق ليست فقط ضيقة، بل مكلفة: كلّ رمز في التاريخ يُدفَع في كلّ استفسار. وأسوأ من ذلك، النموذج يفقد تركيزه على النقاط المهمّة حين يُغرَق بتفاصيل قديمة.
ثلاث استراتيجيّات:
1. ذاكرة النافذة
نحتفظ فقط بآخر K تبادلات:
from langchain_core.messages import trim_messages
trimmer = trim_messages(max_tokens=1000, strategy="last", token_counter=modele)
بسيط، فعّال، لكن يفقد المعلومات القديمة. مناسب لمحادثات قصيرة أو حين لا تُهمّ العودة إلى ما قبل عشر رسائل.
2. ذاكرة الملخّص
نطلب من نموذج لغويّ ثانوي أن يُلخّص التاريخ القديم في فقرة نصّيّة، ونحتفظ بها بدل الرسائل الأصليّة:
from langchain.memory import ConversationSummaryBufferMemory
# نمرّر ملخّصًا يتحدَّث كلّ N رسائل
مفيد للمحادثات الطويلة حيث تُهمّ المعلومات القديمة (اسم المستخدم، تفضيلاته). الكلفة: نداء لخّص إضافيّ دوريًّا.
3. الذاكرة الهجينة
الأشيع في الإنتاج: آخر عشر رسائل حرفيًّا + ملخّص لما قبلها. يحفظ الحوار الحيّ ويحفظ التاريخ في شكل مكثّف.
الإدامة: من الذاكرة الحيّة إلى قاعدة بيانات
الذاكرة الحيّة تختفي مع إعادة تشغيل الخادم. LangChain يوفّر تنفيذات مُدامة:
SQLChatMessageHistory— يحفظ في SQLite أو PostgreSQL.RedisChatMessageHistory— للتوسّع الأفقيّ ومحادثات عالية التزامن.FileChatMessageHistory— ملفّ JSON لكلّ جلسة، مناسب لأنماط بسيطة.
from langchain_community.chat_message_histories import SQLChatMessageHistory
def obtenir_historique(id_session: str) -> SQLChatMessageHistory:
return SQLChatMessageHistory(session_id=id_session, connection_string="sqlite:///memoire.db")
الفخّ: الذاكرة تُشبع السياق
سيناريو ملحوظ: بعد ساعة من الاستخدام، يُرجع المساعد إجابات غريبة، وتتضخّم فاتورة API. السبب النموذجيّ: التاريخ يُرسَل بكامله في كلّ استدعاء. مساعد يستقبل 30 رسالة عن نفقات مختلفة يرسل نافذة 5000 رمز في كلّ سؤال، فيَبعُد النموذج عن التركيز.
علامات إنذار:
- بطء متزايد مع طول الجلسة.
- استجابات تُشير إلى نفقات قديمة لا صلة لها بالسؤال الحاليّ.
- زيادة كلفة كلّ استدعاء رغم قصر السؤال.
الحلّ: مراقبة عدد رموز التاريخ في التتبّع (الوحدة 9)، وضبط trim_messages بحدّ أعلى أو تفعيل الملخّص.
الخيط الأحمر
مساعد النفقات الآن يحتفظ بالسياق: يسأل المستخدم «هل يمكنني استرداد 47 يورو من عشاء عمل بتاريخ 3 مارس؟»، ثم «ما إن كنت مسافرًا؟». السؤال الثاني بلا سياق يقع، لكن مع الذاكرة يفهم المساعد أنّ «المسافر» يعود إلى وضع العشاء المذكور. الجلسة مُعرَّفة بـsession_id مربوط بمعرّف الموظّف، وتُحفَظ في PostgreSQL لتنجو من إعادة التشغيل.
ابدأ بذاكرة نافذة بسيطة (آخر 10 رسائل). أضِف الملخّص فقط حين يشتكي المستخدمون من فقدان معلومات قديمة. أضِف الإدامة قبل الإطلاق العلنيّ.
الخلاصة
ChatMessageHistoryبنية الذاكرة الأساسيّة؛ وRunnableWithMessageHistoryهو التركيب الحديث المستحسَن.- كلّ جلسة تحتاج
session_idفريدًا؛ الخلط بين الجلسات خطأ سرّيّة كبير. - ثلاث استراتيجيّات: نافذة، ملخّص، هجين؛ اختَر حسب طول المحادثات وأهمّيّة المعلومات القديمة.
- الإدامة عبر SQL أو Redis ضروريّة قبل الإنتاج؛ ومراقبة عدد رموز التاريخ تمنع تضخّم السياق الصامت.