كيف تبني شات بوت عربي يفهم اللهجات ويتعامل مع الـ Context بطول 4K token؟ دليل عملي يشرح الـ Prompt Engineering، الـ Memory Management، ودمج الـ APIs مع أمثلة كود حقيقية قابلة للتطبيق مباشرة.
في آخر مرة جربت فيها بناء شات بوت بالعربية باستخدام نموذج لغة كبير، وجدت نفسي أمام مفاجأة غير متوقعة: النموذج الذي كان يتفوق على نظيره الإنجليزي في اختبارات الـ Benchmark فشل فشلاً ذريعاً في فهم جملة بسيطة مثل "شفتك فين؟". المشكلة لم تكن في النموذج نفسه، بل في الطريقة التي نتعامل بها مع اللغة العربية كلغة منخفضة الموارد Low-Resource Language. عندما نتحدث عن بناء شات بوت عربي، نحن لا نتحدث فقط عن استدعاء API وإرسال رسالة واستقبال رد، بل عن هندسة كاملة تبدأ من معالجة الـ Input النصي وتنتهي بإدارة حالة المحادثة عبر جلسات طويلة دون أن يفقد البوت ذاكرته أو ينهار تحت ضغط الـ Rate Limits. في هذا الدليل، سأريك كيف تبني شات بوتاً عربياً ذكياً من الصفر، خطوة بخطوة، مع التركيز على التفاصيل التي لا تذكرها الوثائق الرسمية.
البداية ليست مع الـ LLM نفسه، بل مع فهم كيف يتعامل النموذج مع اللغة العربية على مستوى الـ Tokenization. معظم النماذج الكبيرة مثل GPT-4 أو Llama تستخدم ما يسمى بـ Byte Pair Encoding (BPE) لتقسيم النص إلى وحدات أصغر تسمى Tokens. المشكلة هنا أن اللغة العربية، بعكس الإنجليزية، تعتمد بشكل كبير على السياق والتركيب النحوي، مما يجعل عملية الـ Tokenization أكثر تعقيداً. مثلاً، كلمة "البيت" قد تُقسم إلى ثلاثة tokens: "ال"، "بي"، "ت" بدلاً من token واحد كما هو الحال في الإنجليزية. هذا التقسيم غير الفعال يؤدي إلى استهلاك أكبر للـ Context Window، مما يعني أن البوت قد "ينسى" بداية المحادثة أسرع مما تتوقع. الحل؟ استخدام نماذج مدربة خصيصاً على العربية مثل AraBERT أو CAMeL، أو على الأقل استخدام الـ Tokenizer المخصص للنموذج لضبط عملية المعالجة مسبقاً.
الـ Prompt Engineering ليس مجرد كتابة تعليمات للنموذج، بل هو علم كامل يعتمد على فهم كيف يفكر النموذج وكيف يعالج اللغة. عندما تبني شات بوتاً عربياً، عليك أن تأخذ في الاعتبار عدة عوامل: اللهجات المختلفة، الأخطاء الإملائية الشائعة، وحتى طريقة كتابة الأرقام (هل هي عربية أم هندية؟). مثلاً، إذا طلبت من النموذج كتابة رقم هاتف، قد يعطيك النتيجة بالصيغة الإنجليزية "0123456789" بدلاً من "٠١٢٣٤٥٦٧٨٩". الحل هنا هو استخدام ما يسمى بـ "Few-Shot Prompting"، حيث تقدم للنموذج أمثلة متعددة توضح له بالضبط ما تتوقعه. لكن احذر: الأمثلة السيئة أسوأ من عدم وجود أمثلة على الإطلاق. مثلاً، إذا قدمت مثالاً واحداً فقط لكلمة "كتب" في سياق الماضي، قد يفشل النموذج في فهم نفس الكلمة في سياق المضارع أو الأمر.
هناك أيضاً مشكلة الـ Context Drift، حيث يبدأ النموذج في نسيان التعليمات الأولية بعد عدة رسائل. الحل؟ استخدام ما يسمى بـ "System Message" أو "Instruction Priming" في بداية كل محادثة. هذه الرسالة تكون ثابتة وتشرح للنموذج دوره بالضبط. مثلاً:
# System Message ثابت يحدد شخصية البوت وسلوكه
system_message = """
أنت مساعد ذكي يتحدث اللغة العربية بطلاقة ويفهم اللهجات المختلفة.
- أجب بإجابات قصيرة ومباشرة ما لم يطلب المستخدم التفصيل.
- إذا سألك المستخدم عن معلومات شخصية، رد بأنك بوت مساعد ولا تملك هوية شخصية.
- استخدم الأرقام العربية (١، ٢، ٣) وليس الهندية (1, 2, 3) إلا إذا طلب المستخدم غير ذلك.
- إذا لم تفهم السؤال، اطلب من المستخدم إعادة صياغته باللهجة التي يفضلها.
"""
# عند بداية كل جلسة، نرسل هذا الـ System Message مع أول رسالة
messages = [
{"role": "system", "content": system_message},
{"role": "user", "content": "شفتك فين؟"}
]لاحظ كيف قمنا بتحديد سلوك البوت بدقة في الـ System Message. هذا ليس ترفاً، بل ضرورة. بدون هذه التعليمات، قد يبدأ البوت في الرد باللغة الإنجليزية أو يستخدم اللهجة الخاطئة. أيضاً، لاحظ أننا استخدمنا أمثلة محددة للأرقام العربية بدلاً من الهندية. هذا النوع من التفاصيل هو ما يميز البوت الجيد عن البوت العادي.
أحد أكبر التحديات في بناء شات بوت طويل الأمد هو إدارة الـ Context Window. معظم النماذج اليوم تدعم ما بين 4K إلى 32K token كحد أقصى للسياق. لكن في المحادثات الحقيقية، قد تصل الرسائل إلى مئات الـ Tokens بسرعة، خاصة إذا كان المستخدم يتحدث باللهجة أو يستخدم جملاً طويلة. المشكلة هنا أن النموذج يبدأ في "نسيان" الرسائل الأولى عندما يصل السياق إلى الحد الأقصى. الحل التقليدي هو استخدام ما يسمى بـ "Sliding Window"، حيث نحتفظ بآخر N رسالة فقط. لكن هذا الحل غير فعال في المحادثات التي تعتمد على السياق الطويل، مثل الاستشارات أو الدعم الفني.
الحل الأفضل هو استخدام ما يسمى بـ "Context Summarization". الفكرة هنا هي تلخيص الرسائل القديمة بشكل دوري وإرسال التلخيص فقط مع الرسائل الجديدة. مثلاً، إذا كانت المحادثة تحتوي على 20 رسالة، يمكننا تلخيص أول 15 رسالة في فقرة واحدة وإرسالها مع آخر 5 رسائل. لكن هناك مشكلة: التلخيص نفسه يستهلك tokens، وقد يفقد بعض التفاصيل المهمة. الحل؟ استخدام نموذج تلخيص مخصص مثل BART أو T5، أو حتى استخدام نفس الـ LLM للتلخيص مع تعليمات دقيقة. إليك مثال عملي:
from transformers import pipeline
# نستخدم نموذج تلخيص مخصص للعربية
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
def summarize_context(messages, max_length=150):
# نجمع كل الرسائل في نص واحد
full_text = "\n".join([msg["content"] for msg in messages])
# نقوم بالتلخيص
summary = summarizer(full_text, max_length=max_length, min_length=30, do_sample=False)
return summary[0]["summary_text"]
# عند وصول السياق إلى حد معين، نقوم بالتلخيص
if len(messages) > 10:
old_messages = messages[:-5] # نحتفظ بآخر 5 رسائل
summary = summarize_context(old_messages)
# نستبدل الرسائل القديمة بالتلخيص
messages = [
{"role": "system", "content": f"تلخيص المحادثة السابقة: {summary}"}
] + messages[-5:]هذا الكود يقوم بتلخيص الرسائل القديمة ويحتفظ بآخر 5 رسائل كاملة. لكن هناك مشكلة محتملة: التلخيص قد يكون غير دقيق، خاصة إذا كانت المحادثة تحتوي على تفاصيل دقيقة مثل أرقام أو أسماء. الحل؟ استخدام ما يسمى بـ "Memory Slots"، حيث نحتفظ ببعض المعلومات المهمة بشكل منفصل. مثلاً، إذا كان المستخدم قد ذكر اسمه أو عنوانه في بداية المحادثة، يمكننا حفظ هذه المعلومات في متغير منفصل وإضافتها إلى السياق عند الحاجة، بدلاً من الاعتماد على التلخيص فقط.
اللغة العربية ليست لغة واحدة، بل مجموعة من اللهجات التي تختلف بشكل كبير بين بلد وآخر، وأحياناً بين مدينة وأخرى. المشكلة هنا أن معظم النماذج الكبيرة مدربة على اللغة العربية الفصحى، مما يجعلها تفشل في فهم اللهجات العامية. مثلاً، جملة مثل "وينك؟" قد لا يفهمها النموذج إذا كان مدرباً على الفصحى فقط. الحل؟ استخدام ما يسمى بـ "Dialect Normalization"، حيث نقوم بتحويل اللهجات العامية إلى صيغة أقرب إلى الفصحى قبل إرسالها إلى النموذج. هناك مكتبات مثل ADID أو CamelTools التي تقوم بهذه المهمة، لكنها ليست مثالية وتحتاج إلى ضبط دقيق.
هناك أيضاً مشكلة الأخطاء الإملائية الشائعة في الكتابة العربية، مثل استخدام "ة" بدلاً من "ه" في نهاية الكلمة، أو كتابة "ال" بدلاً من "الـ". هذه الأخطاء قد تبدو بسيطة، لكنها قد تؤدي إلى فشل النموذج في فهم السياق. الحل؟ استخدام ما يسمى بـ "Spell Checking" قبل إرسال الرسالة إلى النموذج. لكن احذر: التصحيح الإملائي قد يغير معنى الكلمة تماماً في بعض الحالات، خاصة في اللهجات العامية. مثلاً، كلمة "مش" في اللهجة المصرية قد تُصَحح إلى "ما" في الفصحى، مما يغير معنى الجملة تماماً. الحل الأفضل هو استخدام نموذج تصحيح إملائي مخصص للهجات، أو على الأقل السماح للمستخدم بتعديل الرسالة بعد التصحيح.
from camel_tools.disambig.mle import MLEDisambiguator
from camel_tools.tokenizers.word import simple_word_tokenize
# نستخدم مكتبة CamelTools لتصحيح الأخطاء الإملائية
mle = MLEDisambiguator.pretrained()
def normalize_text(text):
# نقوم بتقطيع النص إلى كلمات
tokens = simple_word_tokenize(text)
# نقوم بتصحيح كل كلمة
disambig = mle.disambiguate(tokens)
# نعيد تجميع النص بعد التصحيح
normalized_text = " ".join([d.analyses[0].analysis["lex"] for d in disambig])
return normalized_text
# مثال
user_input = "شفتك فين يا عم؟"
normalized_input = normalize_text(user_input)
print(normalized_input) # قد يطبع: "أين أنت يا عم؟"هذا الكود يستخدم مكتبة CamelTools لتصحيح الأخطاء الإملائية وتحويل اللهجات العامية إلى صيغة أقرب إلى الفصحى. لكن كما ذكرت سابقاً، هذا الحل ليس مثالياً وقد يحتاج إلى ضبط دقيق. مثلاً، قد يحول كلمة "مش" إلى "ما" مما يغير معنى الجملة. الحل الأفضل هو استخدام نموذج مخصص للهجات، أو على الأقل السماح للمستخدم بتعديل الرسالة بعد التصحيح.
الشات بوت الجيد لا يعيش في فراغ، بل يحتاج إلى التفاعل مع العالم الخارجي عبر الـ APIs. مثلاً، إذا كنت تبني بوتاً لحجز الفنادق، قد تحتاج إلى الاتصال بـ API لحجز الغرف. أو إذا كنت تبني بوتاً للدعم الفني، قد تحتاج إلى الاتصال بقاعدة بيانات العملاء. المشكلة هنا أن دمج الـ APIs مع الـ LLMs ليس بهذه البساطة. أولاً، عليك أن تتعامل مع مشكلة الـ Latency: استدعاء API قد يستغرق عدة ثوانٍ، وخلال هذا الوقت، قد يفقد المستخدم صبره أو يعتقد أن البوت قد توقف عن العمل. ثانياً، عليك أن تتعامل مع مشكلة الـ Rate Limits: معظم الـ APIs تفرض حدوداً على عدد الاستدعاءات في الدقيقة، وإذا تجاوزت هذه الحدود، قد يتم حظرك مؤقتاً.
الحل؟ استخدام ما يسمى بـ "Asynchronous Processing". بدلاً من انتظار رد الـ API قبل الرد على المستخدم، يمكنك إرسال رد فوري مثل "جاري البحث عن أفضل الخيارات..." ثم تحديث الرسالة لاحقاً عند وصول الرد. هذا يتطلب استخدام WebSockets أو Server-Sent Events (SSE) للحفاظ على الاتصال مفتوحاً مع العميل. إليك مثال عملي باستخدام FastAPI وWebSockets:
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
import httpx
import asyncio
app = FastAPI()
async def fetch_hotel_data(location: str):
# محاكاة استدعاء API بطيء
await asyncio.sleep(2)
return {"hotels": ["فندق الأهرام", "فندق النيل"]}
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
# إرسال رد فوري
await websocket.send_text("جاري البحث عن فنادق في منطقتك...")
# استدعاء API بشكل غير متزامن
hotels = await fetch_hotel_data(data)
# تحديث الرسالة بالنتائج
await websocket.send_text(f"وجدنا هذه الفنادق: {', '.join(hotels['hotels'])}")هذا الكود يرسل رداً فورياً للمستخدم ثم يقوم باستدعاء الـ API بشكل غير متزامن. عندما تصل النتائج، يرسلها للمستخدم عبر نفس الاتصال. هذه الطريقة تحسن تجربة المستخدم بشكل كبير، خاصة في التطبيقات التي تعتمد على استدعاءات بطيئة. لكن هناك مشكلة محتملة: إذا انقطع الاتصال بين العميل والخادم، قد يفقد المستخدم الرد النهائي. الحل؟ استخدام آلية لإعادة المحاولة أو تخزين الرد مؤقتاً في قاعدة بيانات حتى يعود المستخدم.
عندما تبني شات بوتاً يعتمد على الـ LLMs، عليك أن تتوقع أن الـ Latency سيكون مشكلة كبيرة. معظم النماذج الكبيرة تحتاج إلى عدة ثوانٍ لمعالجة الطلب، خاصة إذا كان السياق طويلاً. المشكلة هنا أن المستخدمين اليوم يتوقعون ردوداً فورية، وإذا استغرق البوت أكثر من ثانيتين للرد، قد يفقدون الاهتمام. الحل؟ استخدام ما يسمى بـ "Streaming Responses". بدلاً من انتظار النموذج لإنهاء معالجة الطلب بالكامل، يمكنك إرسال الرد بشكل تدريجي كلما أصبح جزءاً منه جاهزاً. هذا يعطي انطباعاً بأن البوت أسرع مما هو عليه في الواقع.
هناك أيضاً مشكلة الـ Cold Starts، حيث يستغرق النموذج وقتاً أطول للرد على أول طلب بعد فترة من عدم الاستخدام. الحل؟ استخدام ما يسمى بـ "Warm-Up Requests"، حيث ترسل طلبات وهمية للنموذج بشكل دوري للحفاظ على "حرارته". إليك مثال عملي باستخدام مكتبة LangChain لتبسيط عملية الـ Streaming:
from langchain.llms import OpenAI
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
# نستخدم OpenAI مع خاصية الـ Streaming
llm = OpenAI(
streaming=True,
callbacks=[StreamingStdOutCallbackHandler()],
temperature=0
)
# عند استلام رسالة من المستخدم
resp llm("ما هي أفضل الأماكن السياحية في القاهرة؟", stop=["\n"])
# الرد سيتم إرساله بشكل تدريجي كلما أصبح جزءاً منه جاهزاًهذا الكود يرسل الرد بشكل تدريجي بدلاً من انتظار اكتماله. لكن هناك مشكلة: إذا كان المستخدم يستخدم واجهة تعتمد على الـ WebSocket، قد تحتاج إلى تعديل الكود لإرسال كل جزء من الرد عبر الاتصال. أيضاً، عليك أن تتعامل مع مشكلة الـ Timeout: إذا استغرق النموذج وقتاً أطول من المتوقع، قد تحتاج إلى إرسال رسالة مثل "لا يزال جاري التفكير..." للحفاظ على اهتمام المستخدم.
هناك أيضاً مشكلة الـ Cost: استخدام الـ LLMs ليس رخيصاً، خاصة إذا كنت تتعامل مع عدد كبير من المستخدمين. الحل؟ استخدام ما يسمى بـ "Caching". يمكنك تخزين الردود الشائعة في قاعدة بيانات واسترجاعها مباشرة عند تكرار نفس السؤال. مثلاً، إذا سأل 100 مستخدم عن سعر الدولار مقابل الجنيه، يمكنك تخزين الرد الأول واستخدامه للـ 99 مستخدم التاليين. لكن احذر: التخزين قد يؤدي إلى ردود قديمة أو غير دقيقة. الحل؟ استخدام آلية لتحديث الردود المخزنة بشكل دوري، أو على الأقل التحقق من تاريخ الرد قبل إرساله.
بعد بناء أكثر من خمسة شات بوتات عربية تعتمد على الـ LLMs، تعلمت درساً مهماً: التفاصيل الصغيرة هي ما تصنع الفرق بين البوت الجيد والبوت العادي. أولاً، لا تعتمد أبداً على نموذج واحد فقط. استخدم نماذج متعددة واختبرها مع اللهجات المختلفة قبل اتخاذ القرار. ثانياً، لا تهمل جانب الـ UX: المستخدم العربي يتوقع ردوداً سريعة ومباشرة، لذا استخدم الـ Streaming و الـ Asynchronous Processing لتحسين التجربة. ثالثاً، راقب الأداء باستمرار: استخدم أدوات مثل Prometheus و Grafana لقياس الـ Latency و الـ Error Rates، وكن مستعداً لتوسيع البنية التحتية عند الحاجة.
وأخيراً، لا تنسى الجانب الأخلاقي: الشات بوت العربي قد يواجه أسئلة حساسة تتعلق بالدين أو السياسة أو الثقافة. ضع حدوداً واضحة لما يمكن للبوت الإجابة عليه وما لا يمكن، واستخدم آليات لتصفية المحتوى غير المناسب. في النهاية، بناء شات بوت عربي ذكي ليس مجرد تحدٍ تقني، بل هو تحدٍ ثقافي ولغوي أيضاً. إذا نجحت في التعامل مع هذه التحديات، ستكون قد بنيت شيئاً يفيد ملايين المستخدمين العرب الذين يبحثون عن تجربة تفاعلية باللغة التي يفهمونها حقاً.