وكيل ذكاء اصطناعي يعمل بشكل ممتاز في عرض تجريبي مدته عشر دقائق، ثم يبدأ بارتكاب أخطاء غريبة بعد ساعة: يسأل المستخدم عن تفضيل ذكره في البداية، أو يعيد خطوة أنهاها، أو يفقد قراراً اتُّخذ قبل أربعين استدعاءً للأدوات. لا شيء معطل هنا. الوكيل اصطدم بالحد الذي يواجهه كل نموذج لغوي: هو لا يعرف إلا ما يوجد في نافذة السياق الآن، وهذه النافذة محدودة.
الحل ليس نموذجاً أكبر ولا تعليمات أطول، بل أن تعطي الوكيل ذاكرة يتحكم فيها، وأن تُبقي سياق عمله نظيفاً. في هذا الدليل نشرح الأدوات الثلاث التي توفرها واجهة Claude البرمجية لهذا الغرض في 2026، ومتى تستخدم كل واحدة، ثم نبني مخزن ذاكرة حقيقياً بلغة Python لكل مستخدم على حدة، واختبرناه سطراً سطراً.
لماذا ينسى الوكيل؟ نافذة السياق ليست ذاكرة
كل ما "يعرفه" النموذج أثناء المحادثة موجود في مكان واحد: نافذة السياق. كل رسالة من المستخدم، وكل استدعاء أداة، وكل نتيجة ترجعها الأداة، تُضاف إليها. وفي تشغيل طويل للوكيل تتراكم نتائج الأدوات بسرعة: بحث على الويب يرجع صفحات، وقراءة ملف ترجع الملف كاملاً، واستعلام قاعدة بيانات يرجع مئات الصفوف. وينتج عن هذا مشكلتان:
- النافذة تمتلئ. وعندما تمتلئ يجب التخلص من المحتوى الأقدم، وما كان فيه يضيع إلا إذا حفظه شيء ما.
- الجودة تنخفض قبل الامتلاء. وثائق Anthropic نفسها تصف السياق بأنه مورد محدود تتناقص فائدته كلما زاد، وتشير إلى أن المحتوى غير المهم يشتت تركيز النموذج. الوكيل الذي يجرّ خلفه خمسين نتيجة بحث قديمة يكون أسوأ في الخطوة الحالية، وليس أغلى فقط.
إذن "الذاكرة" في الحقيقة مهمتان منفصلتان: إبقاء سياق العمل صغيراً ومفيداً، وحفظ ما يجب أن يبقى في مكان خارج هذا السياق.
ثلاث طبقات، ولكل واحدة وظيفة
واجهة Claude صار فيها أداة مخصصة لكل مهمة. كل أداة تحل مشكلة مختلفة، والخطأ الذي نراه كثيراً هو استخدام واحدة في مكان الأخرى.
- تعديل السياق (Context Editing): تخلّص من نتائج الأدوات القديمة. باستخدام الاستراتيجية
clear_tool_uses_20250919(مع ترويسة البيتاcontext-management-2025-06-27)، تحذف الواجهة أقدم نتائج الأدوات عندما تتجاوز المحادثة حداً معيناً، وتضع مكان كل نتيجة نصاً بديلاً. افتراضياً تبدأ عند 100,000 توكن إدخال وتُبقي آخر 3 استدعاءات. استخدمها عندما يستدعي وكيلك أدوات كثيرة نتائجها مفيدة للحظة فقط. - الضغط (Compaction): لخّص أدوار المحادثة القديمة. يستبدل الضغط الأدوار القديمة بملخص تكتبه الواجهة نفسها، فتبقى المحادثة الطويلة داخل النافذة دون أن تكتب أنت كود التلخيص. تستطيع تشغيله بنفسك عند الطلب (حالياً خلف ترويسة البيتا
compact-2026-09-04)، أو تتركه يعمل تلقائياً عندما تصل توكنات الإدخال إلى حد تحدده. استخدمه في المحادثات الطويلة التي يهمك فيها المضمون لا الصياغة الحرفية للدور الثاني عشر. - أداة الذاكرة (Memory Tool): حقائق يجب أن تبقى. أداة الذاكرة (
memory_20250818) تسمح لـ Claude بقراءة وكتابة ملفات داخل مجلد/memoriesموجود في تخزينك أنت، لا عند Anthropic. هذه الملفات تبقى بعد تنظيف السياق وبعد الضغط، بل وحتى في محادثة جديدة تماماً بعد أيام. استخدمها لتفضيلات المستخدم وقرارات المشروع وتقدّم المهام.
وثائق Anthropic توصي بالجمع بينها في الوكلاء الذين يعملون لفترات طويلة: الضغط يُبقي السياق النشط صغيراً، والذاكرة تحفظ المعلومات التي يجب أن تنجو من التلخيص. وتعديل السياق يذهب خطوة أبعد عندما يُستخدم مع الذاكرة: عندما تقترب المحادثة من حد التنظيف، يصل Claude تنبيه تلقائي ليحفظ المهم في ذاكرته قبل أن تختفي نتائج الأدوات.
كيف تعمل أداة الذاكرة فعلاً؟
أهم نقطة يجب فهمها: Claude لا يلمس قرصك أو قاعدة بياناتك مباشرة أبداً. أداة الذاكرة تعمل من جهة العميل: يرسل Claude أمراً، وكودك ينفّذه على التخزين الذي تختاره، ثم ترجع له النتيجة. وهناك ستة أوامر:
view: عرض محتوى مجلد أو قراءة ملف (أو جزء من أسطره فقط)create: إنشاء ملف جديدstr_replace: استبدال جزء من النص داخل ملفinsert: إدراج نص عند رقم سطر معينdelete: حذف ملف أو مجلدrename: إعادة تسمية ملف أو نقله
عندما تفعّل الأداة، تضيف الواجهة تلقائياً بروتوكولاً قصيراً إلى تعليمات النظام. يطلب هذا البروتوكول من Claude أن يفتح مجلد ذاكرته قبل أي شيء آخر، وأن يفترض أن العمل قد ينقطع في أي لحظة، فيسجّل تقدّمه أولاً بأول لأن السياق قد يُمسح فجأة. ولهذا فالوكيل المبني جيداً يراجع ملاحظاته أولاً ويكتبها باستمرار، لا في النهاية فقط.
الأداة لا تحتاج ترويسة بيتا، وتعمل مع Claude 4 والنماذج الأحدث. ومكتبة Python الرسمية فيها أداتان جاهزتان: BetaLocalFilesystemMemoryTool التي تحفظ الذاكرة كملفات في مجلد على جهازك، وBetaAbstractMemoryTool وهي فئة أساسية ترث منها لتحفظ الذاكرة في أي مكان آخر.
رأينا: الملفات البسيطة أفضل من قاعدة بيانات المتجهات لهذه المهمة
كثير من شروحات "ذاكرة الوكلاء" تقفز مباشرة إلى الـ embeddings وقواعد بيانات المتجهات. هذه هي الأداة الصحيحة للبحث في آلاف المستندات، لكنها غالباً مبالغة للذاكرة التي يحتاجها الوكيل فعلاً: ماذا يفضّل هذا المستخدم، وماذا تقرر، وأين توقفت المهمة. هذه الحقائق قليلة، وتتغير، وتحتاج تعديلاً دقيقاً لا مطابقة تقريبية. مجموعة صغيرة من الملفات المقروءة يديرها Claude بنفسه أسهل في المراجعة والتصحيح، وأسهل في الحذف عندما يطلب المستخدم ذلك. ابدأ بالملفات، وأضف البحث الدلالي فقط عندما تكبر الذاكرة أكثر مما يستطيع Claude استعراضه بالأمر view.
لنبنِ ذاكرة لكل مستخدم في SQLite
أداة المجلد المحلي تكفي على جهازك الشخصي. لكن في تطبيق حقيقي فيه مستخدمون كثيرون تحتاج شيئين لا توفرهما: أن تكون ذاكرة كل مستخدم معزولة عن غيره، وأن تكون في قاعدة بيانات تأخذ لها نسخاً احتياطية أصلاً. لذلك نرث من BetaAbstractMemoryTool ونحفظ الملفات في SQLite مع ربط كل ملف بالمستخدم. تحتاج Python 3.10 أو أحدث ومكتبة Anthropic (استخدمنا الإصدار 1.11.0):
pip install anthropic
أنشئ الملف sqlite_memory.py:
import posixpath
import re
import sqlite3
from datetime import datetime, timezone
from anthropic.lib.tools import ToolError
from anthropic.tools import BetaAbstractMemoryTool
MAX_FILE_CHARS = 20_000 # one memory file can't grow without limit
SECRET_PATTERNS = [
re.compile(r"sk-[A-Za-z0-9_-]{20,}"), # API-key style secrets
re.compile(r"AKIA[0-9A-Z]{16}"), # AWS access key IDs
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY"), # private keys
]
class SQLiteMemory(BetaAbstractMemoryTool):
"""Claude's memory tool, stored in SQLite, one isolated space per user."""
def __init__(self, db_path: str, user_id: str):
super().__init__()
self.user_id = user_id
self.db = sqlite3.connect(db_path)
self.db.execute(
"CREATE TABLE IF NOT EXISTS memories ("
" user_id TEXT, path TEXT, content TEXT, updated_at TEXT,"
" PRIMARY KEY (user_id, path))"
)
# ---- safety -------------------------------------------------------
def _path(self, raw: str) -> str:
if "%" in raw or "\\" in raw or "\x00" in raw:
raise ToolError(f"Invalid characters in path: {raw}")
clean = posixpath.normpath(raw)
if clean != "/memories" and not clean.startswith("/memories/"):
raise ToolError(f"Path must stay inside /memories: {raw}")
return clean
def _check_text(self, text: str) -> None:
if len(text) > MAX_FILE_CHARS:
raise ToolError(f"Memory file too large (max {MAX_FILE_CHARS} characters)")
if any(p.search(text) for p in SECRET_PATTERNS):
raise ToolError("Refusing to store something that looks like a secret")
# ---- storage helpers ---------------------------------------------
def _get(self, path: str) -> str | None:
row = self.db.execute(
"SELECT content FROM memories WHERE user_id=? AND path=?", (self.user_id, path)
).fetchone()
return row[0] if row else None
def _put(self, path: str, text: str) -> None:
self._check_text(text)
self.db.execute(
"INSERT INTO memories VALUES (?,?,?,?) ON CONFLICT(user_id, path) "
"DO UPDATE SET content=excluded.content, updated_at=excluded.updated_at",
(self.user_id, path, text, datetime.now(timezone.utc).isoformat()),
)
self.db.commit()
def _children(self, folder: str) -> list[str]:
rows = self.db.execute(
"SELECT path FROM memories WHERE user_id=? AND path LIKE ? ORDER BY path",
(self.user_id, folder.rstrip("/") + "/%"),
).fetchall()
return [r[0] for r in rows]
# ---- the six commands Claude can send ------------------------------
def view(self, command):
path = self._path(command.path)
text = self._get(path)
if text is None:
files = self._children(path)
if path != "/memories" and not files:
raise ToolError(f"The path {command.path} does not exist")
return "\n".join(files) if files else "(empty)"
lines = text.split("\n")
start, end = 1, len(lines)
if command.view_range:
start, end = command.view_range
end = len(lines) if end == -1 else end
return "\n".join(f"{i}\t{lines[i - 1]}" for i in range(start, end + 1))
def create(self, command):
path = self._path(command.path)
self._put(path, command.file_text)
return f"File created: {path}"
def str_replace(self, command):
path = self._path(command.path)
text = self._get(path)
if text is None:
raise ToolError(f"The path {command.path} does not exist")
count = text.count(command.old_str)
if count != 1:
raise ToolError(f"old_str must appear exactly once, found {count} times")
self._put(path, text.replace(command.old_str, command.new_str or ""))
return f"File {path} edited"
def insert(self, command):
path = self._path(command.path)
text = self._get(path)
if text is None:
raise ToolError(f"The path {command.path} does not exist")
lines = text.split("\n")
if not 0 <= command.insert_line <= len(lines):
raise ToolError(f"insert_line must be between 0 and {len(lines)}")
lines.insert(command.insert_line, command.insert_text.rstrip("\n"))
self._put(path, "\n".join(lines))
return f"Text inserted in {path}"
def delete(self, command):
path = self._path(command.path)
if path == "/memories":
raise ToolError("Refusing to delete the whole memory directory")
cur = self.db.execute(
"DELETE FROM memories WHERE user_id=? AND (path=? OR path LIKE ?)",
(self.user_id, path, path + "/%"),
)
self.db.commit()
if cur.rowcount == 0:
raise ToolError(f"The path {command.path} does not exist")
return f"Deleted {path}"
def rename(self, command):
old, new = self._path(command.old_path), self._path(command.new_path)
text = self._get(old)
if text is None:
raise ToolError(f"The path {command.old_path} does not exist")
if self._get(new) is not None:
raise ToolError(f"The path {command.new_path} already exists")
self._put(new, text)
self.db.execute("DELETE FROM memories WHERE user_id=? AND path=?", (self.user_id, old))
self.db.commit()
return f"Renamed {old} to {new}"
جزء التخزين استعلامات SQL عادية. أما الجزء الذي يستحق القراءة مرتين فهو الحمايات، وكل واحدة منها تعالج خطراً تطلب منك وثائق Anthropic أن تتعامل معه:
- اختراق المسار (Path Traversal). مسار مثل
/memories/../../etc/passwdيجب ألا يخرج أبداً من منطقة الذاكرة. الدالةposixpath.normpathتختصر مقاطع..، ثم نشترط أن تبدأ النتيجة بـ/memories/. ونرفض الرمز%تماماً، لأن الوثائق تحذّر تحديداً من الاختراق المُرمَّز مثل%2e%2e%2f. - عزل المستخدمين. كل استعلام مقيّد بـ
user_id. وكيل سارة لا يستطيع قراءة ملاحظات عمر حتى لو أقنعته تعليمات خبيثة بالمحاولة، لأن الحد موجود في كودك لا في التعليمات. - حد للحجم. ملف الذاكرة لا يتجاوز 20,000 حرف، فلا تستطيع حلقة خارجة عن السيطرة أن تملأ قاعدة بياناتك أو تُضخّم سياق الطلب التالي.
- لا أسرار. تذكر الوثائق أن Claude يرفض عادةً كتابة المعلومات الحساسة، لكنها توصي بالتحقق على أي حال. نحن نمنع حفظ أي نص يشبه مفتاح API أو مفتاحاً خاصاً.
عندما ترمي إحدى الدوال ToolError، يلتقطها منفّذ الأدوات في المكتبة ويرسل لـ Claude نتيجة معلَّمة بـ is_error ومعها رسالتك. فيقرأ Claude السبب ويعدّل تصرفه، بدل أن ينهار تطبيقك.
اختبرها دون صرف أي استدعاء API
بما أن أداة الذاكرة مجرد معالج لستة أوامر، تستطيع اختبارها وحدها بالكامل بإرسال الأوامر نفسها التي سيرسلها Claude. أنشئ test_memory.py:
from anthropic.lib.tools import ToolError
from sqlite_memory import SQLiteMemory
def send(memory, **command):
"""Run one command exactly as Claude would send it, and print the result."""
try:
print("OK ", memory.call(command))
except ToolError as e:
print("ERROR", e)
sara = SQLiteMemory("memory.db", user_id="sara")
omar = SQLiteMemory("memory.db", user_id="omar")
send(sara, command="view", path="/memories")
send(sara, command="create", path="/memories/preferences.md",
file_text="- Prefers replies in Arabic\n- Timezone: Asia/Dubai")
send(sara, command="insert", path="/memories/preferences.md", insert_line=2,
insert_text="- Wants invoices as PDF")
send(sara, command="str_replace", path="/memories/preferences.md",
old_str="as PDF", new_str="as PDF, never as Word")
send(sara, command="view", path="/memories/preferences.md")
print("--- isolation: Omar cannot see Sara's memory")
send(omar, command="view", path="/memories")
print("--- attacks the handler must refuse")
send(sara, command="view", path="/memories/../../etc/passwd")
send(sara, command="view", path="/memories/%2e%2e/secrets")
send(sara, command="create", path="/memories/keys.md",
file_text="openai key: sk-proj-abcdefghijklmnopqrstuvwx")
send(sara, command="delete", path="/memories")
وهذا الناتج الذي حصلنا عليه:
OK (empty)
OK File created: /memories/preferences.md
OK Text inserted in /memories/preferences.md
OK File /memories/preferences.md edited
OK 1 - Prefers replies in Arabic
2 - Timezone: Asia/Dubai
3 - Wants invoices as PDF, never as Word
--- isolation: Omar cannot see Sara's memory
OK (empty)
--- attacks the handler must refuse
ERROR Path must stay inside /memories: /memories/../../etc/passwd
ERROR Invalid characters in path: /memories/%2e%2e/secrets
ERROR Refusing to store something that looks like a secret
ERROR Refusing to delete the whole memory directory
كل سطر مطابق لما أردناه: الملاحظات تُنشأ وتُعدَّل بدقة، والمستخدم الثاني يرى ذاكرة فارغة، والهجمات الأربع كلها مرفوضة مع سبب واضح. شغّل هذا الاختبار بعد كل تعديل على المعالج؛ يستغرق أقل من ثانية.
اربطها بـ Claude
بعد اختبار المعالج، ربطه يحتاج أسطراً قليلة. هذا الكود يتبع النمط الموجود في وثائق أداة الذاكرة، مع وضع فئتنا مكان أداة المجلد المحلي. ويحتاج متغير بيئة اسمه ANTHROPIC_API_KEY؛ لا تكتب المفتاح داخل الكود أبداً.
import anthropic
from sqlite_memory import SQLiteMemory
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
memory = SQLiteMemory("memory.db", user_id="sara")
runner = client.beta.messages.tool_runner(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "From now on, send my invoices as PDF."}],
tools=[memory],
)
print(runner.until_done().content)
المنفّذ يتولى الدورة كاملة: يفتح Claude مجلد /memories، ويقرر كتابة ملاحظة، فتحفظها فئتك، ثم يؤكد Claude ذلك. وإذا بدأت محادثة جديدة تماماً لنفس user_id وسألت "كيف أرسل لك الفواتير؟"، سيجد Claude الإجابة في ملاحظاته بدل أن يسألك من جديد.
وللوكلاء الذين يعملون لفترات طويلة، أضف تعديل السياق بجانب أداة الذاكرة، فتُحذف نتائج الأدوات القديمة بعد أن يصل Claude تنبيه أولاً. هذا هو الجمع الموضح في وثائق تعديل السياق:
response = client.beta.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
messages=[{"role": "user", "content": "Hello"}],
tools=[{"type": "memory_20250818", "name": "memory"}],
betas=["context-management-2025-06-27"],
context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},
)
خمس قواعد لذاكرة تفيد ولا تضر
- قل للوكيل ما الذي يستحق التذكر. البروتوكول المدمج يجعل Claude يراجع ذاكرته ويحدّثها، لكنه لا يعرف منتجك. سطر واحد في تعليمات النظام، مثل "تذكّر تفضيلات المستخدم وقراراته، لا الأحاديث الجانبية"، يُبقي الملاحظات قصيرة ومفيدة.
- احفظ القرارات لا المحادثات. "العميل وافق على الشعار الأزرق يوم 3 أكتوبر" هذه ذاكرة. أما نسخة كاملة من المحادثة فليست ذاكرة، وهذا عمل الضغط.
- اسمح للمستخدم برؤية ذاكرته وحذفها. لأن الذاكرة في قاعدة بياناتك، تستطيع عرضها في صفحة الإعدادات وحذفها عند الطلب. وهذه ممارسة جيدة، بل ومتطلب قانوني في أماكن كثيرة عندما يتعلق الأمر ببيانات شخصية.
- احذف ما صار قديماً. الوثائق تقترح حذف الملفات غير المستخدمة دورياً. ذاكرة عن أسعار السنة الماضية قد تكون أسوأ من عدم وجود ذاكرة.
- تعامل مع الذاكرة كمدخل غير موثوق. الملاحظات المكتوبة في جلسة تُقرأ كسياق في الجلسة التالية. فإذا تمكّن مهاجم من إدخال نص إلى الذاكرة عبر صفحة ويب مسمومة، يستطيع توجيه الجلسات القادمة. اجعل الكتابة في الذاكرة محدودة، ولا تسمح أبداً لملاحظة بأن تمنح صلاحيات.
من أين تبدأ؟
إذا كان وكيلك يعمل لدقائق، فقد لا تحتاج شيئاً من هذا الآن. أما إذا كان يعمل لساعات، أو يعود إليه المستخدمون على مدى أيام، فأضف الطبقات الثلاث بهذا الترتيب: تعديل السياق أولاً (مُعامل واحد ووفر فوري)، ثم أداة الذاكرة مع معالج مُختبَر مثل الموجود أعلاه، ثم الضغط عندما تطول المحادثات الواحدة. واختبر المعالج وحده قبل ربطه بأي نموذج؛ فكل خطأ تكتشفه هنا هو خطأ لن تضطر لتتبعه داخل محادثة من 200 دور.
وإذا لم تبنِ خادم أدوات بعد، فدليلنا عن بناء خادم MCP في 2026 يغطي النصف الآخر من الوكيل القوي: إعطاؤه أدوات موثوقة.
المصادر: وثائق Anthropic لأداة الذاكرة وتعديل السياق والضغط (platform.claude.com)، وكود مكتبة Anthropic الرسمية لـ Python (الإصدار 1.11.0). جرى اختبار الكود بتاريخ 2026-10-05 على anthropic 1.11.0 وPython 3.13.