حفظ واسترجاع ذاكرة المحادثات من قاعدة بيانات (SQLite / Redis)
حفظ واسترجاع ذاكرة المحادثات من قاعدة بيانات SQLite / Redis
عند بناء تطبيق محادثة يعتمد على نماذج اللغة الكبيرة LLMs، فإن النموذج نفسه لا “يتذكر” شيئاً بين الطلبات إلا إذا قمت أنت بتمرير السياق السابق في كل استدعاء. هذه الفكرة ناقشناها سابقاً في مقال الذاكرة في الذكاء الاصطناعي: لماذا ينسى الـ API المحادثات السابقة وكيف نعالج ذلك؟، لكن على المستوى الهندسي العملي نحتاج إلى طبقة تخزين خارجية تحفظ الرسائل، ثم تعيد تحميلها قبل إرسال الطلب التالي. هنا تظهر فائدة SQLite وRedis كخيارين مختلفين في السلوك والتكلفة والأداء.
الفكرة الأساسية بسيطة: كل رسالة من المستخدم أو المساعد تُخزَّن مع session_id ووقت الإرسال ونوع الرسالة. وعند وصول رسالة جديدة، يقوم التطبيق باسترجاع آخر جزء من السجل، ثم يبني prompt جديداً يحتوي على المحادثة السابقة والطلب الحالي. بهذا الشكل تتحول الذاكرة من مفهوم منطقي إلى بنية بيانات قابلة للإدارة، الفحص، الحذف، والتوسعة.
المعمارية العامة لتدفق الذاكرة داخل تطبيق المحادثة
في التطبيقات الاحترافية، لا يُفضّل ربط واجهة المستخدم مباشرة بالنموذج. الأفضل أن يمر الطلب عبر خط معالجة واضح:
- استقبال رسالة المستخدم من الواجهة الأمامية.
- تحديد هوية الجلسة عبر
session_id. - استرجاع الرسائل السابقة من قاعدة البيانات.
- اختصار أو تقليم السجل إذا كان طويلاً لتقليل استهلاك الرموز
Tokens. - تجميع الرسائل في بنية مفهومة للنموذج أو لإطار LangChain.
- إرسال الطلب إلى النموذج واستقبال الرد.
- حفظ رسالة المستخدم ورد المساعد فوراً داخل مخزن الذاكرة.
هذا الفصل بين طبقة النموذج وطبقة التخزين يجعل النظام أكثر مرونة. يمكنك لاحقاً استبدال SQLite بـ Redis دون تغيير كبير في المنطق العام، فقط في طبقة التخزين.
متى تختار SQLite ومتى تختار Redis؟
استخدام SQLite
يصلح عندما تريد تخزيناً دائماً Persistent داخل ملف محلي، مع سهولة النسخ الاحتياطي والاستعلام باستخدام SQL. وهو ممتاز في النماذج الأولية، لوحات التحكم الصغيرة، وتطبيقات الدعم الداخلي.
استخدام Redis
يصلح عندما تريد سرعة عالية جداً وذاكرة مؤقتة In-Memory، خصوصاً للتطبيقات ذات الجلسات الكثيفة أو الردود الفورية. كما أنه مناسب عندما ترغب في وضع صلاحية انتهاء TTL لكل محادثة بحيث تُحذف تلقائياً بعد مدة.
تصميم جدول ذاكرة المحادثة في SQLite
في الحد الأدنى، يكفي أن يحتوي الجدول على معرف، ومعرف الجلسة، والدور role، والنص، والطابع الزمني. هذا التصميم مناسب قبل إدخال طبقات أكثر تقدماً مثل ذاكرة الملخصات Summary Memory.
import sqlite3
conn = sqlite3.connect("chat_memory.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
بعد إنشاء الجدول، نحتاج إلى دالتين: واحدة للحفظ، وأخرى للاسترجاع. هذا النمط يعزل منطق التخزين عن بقية التطبيق.
import sqlite3
DB_PATH = "chat_memory.db"
def save_message(session_id, role, content):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)",
(session_id, role, content)
)
conn.commit()
conn.close()
def load_messages(session_id, limit=10):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"""
SELECT role, content
FROM messages
WHERE session_id = ?
ORDER BY id DESC
LIMIT ?
""",
(session_id, limit)
)
rows = cursor.fetchall()
conn.close()
return list(reversed(rows))
تخزين الذاكرة السريعة باستخدام Redis
في Redis غالباً نخزن الرسائل داخل قائمة List مرتبطة بمفتاح مثل chat:session_123. كل عنصر يمكن أن يكون JSON بسيطاً يضم الدور والمحتوى.
import redis
import json
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
def save_message_redis(session_id, role, content, ttl=3600):
key = f"chat:{session_id}"
payload = json.dumps({"role": role, "content": content})
r.rpush(key, payload)
r.expire(key, ttl)
def load_messages_redis(session_id, limit=10):
key = f"chat:{session_id}"
items = r.lrange(key, -limit, -1)
return [json.loads(item) for item in items]
ميزة هذا الأسلوب أنه سريع جداً، لكنه ليس الخيار الأفضل كسجل تاريخي طويل المدى إلا إذا كنت قد فعّلت استراتيجيات ديمومة مناسبة أو كنت تستخدمه كطبقة مؤقتة فوق قاعدة أبطأ وأكثر ثباتاً.
دمج الذاكرة مع LangChain
إذا كنت قد قرأت مقال إنشاء أول سلسلة Chain باستخدام LangChain، فستفهم أن الذاكرة هنا لا تعني مجرد قائمة رسائل، بل كائن يضبط تدفق السياق. يمكننا استخدام الرسائل المسترجعة لبناء سجل محادثة يدخله النموذج قبل الرسالة الجديدة.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
def build_chat_messages(session_id, user_input):
history = load_messages(session_id, limit=8)
messages = [
SystemMessage(content="You are a helpful AI assistant.")
]
for role, content in history:
if role == "user":
messages.append(HumanMessage(content=content))
elif role == "assistant":
messages.append(AIMessage(content=content))
messages.append(HumanMessage(content=user_input))
return messages
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
def chat(session_id, user_input):
messages = build_chat_messages(session_id, user_input)
response = llm.invoke(messages)
save_message(session_id, "user", user_input)
save_message(session_id, "assistant", response.content)
return response.content
لاحظ هنا أن التحكم في الإبداع عبر معامل Temperature مهم جداً، لأن الذاكرة الطويلة مع درجة عشوائية مرتفعة قد تؤدي إلى انجراف في أسلوب الإجابة أو تكرار غير منضبط.
بناء Prompt احترافي يعتمد على السجل المخزن
عند تحويل الرسائل السابقة إلى توجيه فعلي للنموذج، يجب أن تكون الصياغة منضبطة. إذا كنت تستخدم قوالب ديناميكية، فراجع مقال هندسة الأوامر داخل الكود: قوالب النصوص المتغيرة.
أنت مساعد تقني. استخدم سجل المحادثة السابق فقط لفهم السياق، ولا تخترع معلومات غير موجودة. إذا كانت الرسالة الحالية تشير إلى ضمير أو مرجع سابق، ففسره من الذاكرة المحفوظة. وإذا كان السياق غير كافٍ، اطلب توضيحاً قصيراً من المستخدم.
هذا النوع من التعليمات يقلل من ظاهرة الاستنتاج المفرط، خصوصاً عندما تكون الذاكرة مخزنة جزئياً أو مقلمة بسبب حدود context window.
أفضل الممارسات الهندسية والإنتاجية
- استخدم
SQLiteللتخزين الدائم المحلي، وRedisكطبقة تسريع للجلسات النشطة. - لا ترسل كل السجل دائماً؛ حدّد آخر
Nرسائل أو استخدم التلخيص. - خزّن الرسائل بصيغة منظمة لتسهيل التحويل لاحقاً إلى
JSONمنضبط أو إلى تحليلات جودة. - أضف فهارس
Indexesعلىsession_idفيSQLiteعند ارتفاع عدد الجلسات. - إذا كان التطبيق يتوسع نحو البحث الدلالي أو RAG، فافصل بين ذاكرة المحادثة القصيرة وبين مخزن المعرفة طويل المدى القائم على
Embeddingsوقواعد البيانات المتجهة.
الخلاصة الهندسية
حفظ ذاكرة المحادثات ليس ميزة تجميلية، بل مكوّن معماري أساسي في أي تطبيق محادثة احترافي. SQLite يمنحك بساطة وثباتاً ممتازين، بينما يوفر Redis سرعة مناسبة للأنظمة التفاعلية السريعة. القرار الصحيح لا يتعلق فقط بسرعة القراءة والكتابة، بل أيضاً بعمر الجلسة، التكلفة، الحاجة إلى الاسترجاع التاريخي، وكيفية إدارة السياق ضمن حدود النموذج. وكلما صممت طبقة الذاكرة بصورة مفصولة وقابلة للتبديل، أصبح تطبيقك التوليدي أكثر نضجاً وقابلية للتوسع.
3 comments