كيف تبني شات بوت عربي يفهم اللهجات ويجيب بذكاء باستخدام نماذج اللغة الكبيرة؟ دليل عملي خطوة بخطوة يكشف أسرار الـ Tokenization، الـ Context Window، ودمج الـ RAG مع LLMs لتجنب الهلوسة.
في آخر مرة جربت فيها بناء شات بوت عربي، وجدت نفسي أمام مشكلة غريبة: النموذج كان يفهم الجمل الرسمية مثل "ما هي عاصمة مصر؟" بشكل ممتاز، لكنه يفشل تماماً عند سؤال مثل "شو أخبارك يا عم؟" أو "كيف أروح من جدة للرياض؟". المشكلة ليست في النموذج نفسه، بل في الطريقة التي نعالجه بها. معظم الدروس تركز على الإنجليزية وتنسى أن العربية تحمل تحديات فريدة: اللهجات المتعددة، التشكيل، والكلمات المتشابهة ذات المعاني المختلفة. في هذا المقال، سنبني شات بوت عربي من الصفر، خطوة بخطوة، مع التركيز على التفاصيل التي تجعل الفرق بين بوت غبي وبوت ذكي يفهم السياق العربي حقاً.
لن نستخدم مكتبات جاهزة مثل LangChain أو LlamaIndex بشكل أعمى. بدلاً من ذلك، سنفهم ماذا يحدث خلف الكواليس: كيف يعمل الـ Tokenizer العربي؟ لماذا يفشل النموذج أحياناً في تذكر السياق؟ وكيف يمكننا تحسين الـ Context Window لتجنب الهلوسة؟ سنستخدم نموذجاً مفتوح المصدر مثل Jais أو AraT5، ونبني كل شيء من البداية حتى نتمكن من التحكم الكامل في الأداء والسلوك. في النهاية، سيكون لدينا بوت يفهم اللهجات، يتذكر السياق، ويمكنه الإجابة عن أسئلة معقدة مثل "ما هي أفضل طريقة لاستثمار 10 آلاف ريال في السوق السعودي؟" دون أن يهلوس بإجابات غير منطقية.
قبل أن نكتب سطر كود واحد، يجب أن نفهم لماذا العربية تختلف عن الإنجليزية في معالجة النماذج اللغوية الكبيرة. أولاً، العربية لغة معقدة من الناحية الصرفيّة: كلمة واحدة مثل "كَتَبَ" يمكن أن تتحول إلى أشكال متعددة مثل "يَكْتُبُ"، "كُتِبَ"، "كِتَابٌ"، وكل منها له معنى مختلف. الـ Tokenizer التقليدي مثل الذي يستخدمه GPT-3 يتعامل مع الإنجليزية بشكل جيد لأنه يفصل الكلمات بناءً على المسافات، لكن في العربية، هذا النهج يفشل لأن الكلمات غالباً ما تكون متصلة ببعضها البعض (مثل "بالقلم" = "بـ" + "القلم").
ثانياً، اللهجات العربية تمثل تحدياً كبيراً. النموذج المدرب على العربية الفصحى قد لا يفهم اللهجة المصرية أو الخليجية بشكل جيد. مثلاً، كلمة "شو" في اللهجة الشامية تعني "ماذا"، بينما في الفصحى لا معنى لها. هذا يعني أننا بحاجة إلى إما تدريب النموذج على بيانات اللهجات، أو استخدام تقنيات مثل الـ Transfer Learning لتكييف النموذج مع اللهجات المختلفة. في هذا المقال، سنستخدم نموذجاً مدرباً مسبقاً على العربية الفصحى واللهجات الشائعة مثل Jais-13b-chat، ثم سنعززه بمجموعة بيانات صغيرة من اللهجات لتحسين الأداء.
# مثال على كيفية عمل Tokenizer العربي وكيف يختلف عن الإنجليزي
from transformers import AutoTokenizer
# تحميل Tokenizer لنموذج Jais (مدرب على العربية)
tokenizer = AutoTokenizer.from_pretrained("core42/jais-13b-chat")
# مثال على Tokenization للإنجليزية
english_text = "What is the capital of Egypt?"
english_tokens = tokenizer.tokenize(english_text)
print("English tokens:", english_tokens)
# Output: ['What', 'is', 'the', 'capital', 'of', 'Egypt', '?']
# مثال على Tokenization للعربية (لاحظ كيف يتعامل مع التشكيل والاتصالات)
arabic_text = "ما هي عاصمة مصر؟"
arabic_tokens = tokenizer.tokenize(arabic_text)
print("Arabic tokens:", arabic_tokens)
# Output: ['ما', 'هي', 'عاصمة', 'مصر', '؟']
# مثال على مشكلة اللهجات
arabic_dialect = "شو أخبارك يا عم؟"
dialect_tokens = tokenizer.tokenize(arabic_dialect)
print("Dialect tokens:", dialect_tokens)
# Output قد يكون غير متوقع: ['شو', 'أ', 'خبار', 'ك', 'يا', 'عم', '؟']الآن، دعنا نجهز بيئتنا ونختار النموذج المناسب. هناك عدة نماذج عربية مفتوحة المصدر يمكن استخدامها، لكن ليس كلها مناسبة لبناء شات بوت ذكي. مثلاً، نموذج AraBERT جيد لفهم النصوص العربية، لكنه ليس مصمماً للمحادثات. من ناحية أخرى، نموذج Jais-13b-chat مصمم خصيصاً للمحادثات العربية ويدعم اللهجات بشكل أفضل. المشكلة الرئيسية مع Jais هي حجمه الكبير (13 مليار باراميتر)، مما يعني أنه يحتاج إلى GPU قوي لتشغيله بكفاءة. إذا لم يكن لديك وصول إلى GPU، يمكنك استخدام نسخة أصغر مثل Jais-1.3b، لكنها ستفقد بعض الدقة في الفهم السياقي.
سنستخدم مكتبة Transformers من Hugging Face لأنها توفر واجهة سهلة للعمل مع نماذج LLMs وتدعم اللغة العربية بشكل جيد. بالإضافة إلى ذلك، سنستخدم مكتبة PyTorch كbackend لأن معظم النماذج العربية مفتوحة المصدر مبنية عليها. إذا كنت تعمل على سيرفر بدون GPU، يمكنك استخدام مكتبة Accelerate من Hugging Face لتسريع الاستدلال باستخدام CPU، لكنها ستكون أبطأ بكثير من GPU. في هذا المقال، سنفترض أنك تعمل على بيئة تحتوي على GPU مثل Google Colab أو سيرفر محلي مع كرت شاشة قوي.
# إعداد البيئة وتثبيت المكتبات المطلوبة
# نستخدم Python 3.10 أو أحدث
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install transformers accelerate sentencepiece
pip install flask flask-cors # سنستخدم Flask لبناء API بسيط للشات بوت
# تحميل نموذج Jais-13b-chat
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_name = "core42/jais-13b-chat"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16, # لتقليل استخدام الذاكرة
device_map="auto" # لتوزيع النموذج على GPU بشكل تلقائي
)أحد أكبر التحديات في بناء شات بوت ذكي هو إدارة الـ Context Window. الـ Context Window هو عدد الـ Tokens التي يستطيع النموذج تذكرها خلال المحادثة. مثلاً، إذا كان الـ Context Window للنموذج هو 2048 token، فهذا يعني أنه يستطيع تذكر آخر 2048 token من المحادثة. المشكلة هي أن العربية تستهلك tokens أكثر من الإنجليزية بسبب تعقيداتها الصرفيّة. مثلاً، جملة "كيف حالك اليوم؟" في الإنجليزية قد تستهلك 5 tokens، بينما في العربية قد تستهلك 7 أو 8 tokens بسبب التشكيل والاتصالات بين الكلمات.
هناك عدة طرق لإدارة الـ Context Window. الطريقة الأولى هي استخدام تقنية تسمى Sliding Window، حيث نحتفظ فقط بآخر N tokens من المحادثة ونرمي الباقي. هذه الطريقة بسيطة لكنها قد تفقد السياق المهم إذا كانت المحادثة طويلة. الطريقة الثانية هي استخدام تقنية تسمى Summarization، حيث نلخص المحادثة القديمة ونحتفظ بالملخص فقط بدلاً من التفاصيل الكاملة. هذه الطريقة أكثر ذكاءً لكنها تحتاج إلى نموذج إضافي للتلخيص. في هذا المقال، سنستخدم مزيجاً من الطريقتين: سنحتفظ بآخر 1500 token من المحادثة، وإذا تجاوزت المحادثة هذا الحد، سنلخص الجزء القديم باستخدام نموذج تلخيص بسيط قبل إضافته إلى الـ Context Window الجديد.
from transformers import pipeline
# تحميل نموذج تلخيص عربي
summarizer = pipeline(
"summarization",
model="abdalrahmanshahrour/arabic-pegasus-summarization",
tokenizer="abdalrahmanshahrour/arabic-pegasus-summarization",
device=0 if torch.cuda.is_available() else -1
)
class ChatContextManager:
def __init__(self, max_tokens=1500):
self.max_tokens = max_tokens
self.c []
self.tokenizer = tokenizer
def add_message(self, role, content):
# إضافة رسالة جديدة للتاريخ
self.conversation_history.append({"role": role, "content": content})
# حساب عدد الـ tokens في التاريخ الحالي
total_tokens = sum(
len(self.tokenizer.tokenize(msg["content"]))
for msg in self.conversation_history
)
# إذا تجاوز الحد الأقصى، قم بتلخيص الجزء القديم
if total_tokens > self.max_tokens:
# جمع الرسائل القديمة حتى نصل إلى نصف الـ max_tokens
old_messages = []
old_tokens = 0
for msg in reversed(self.conversation_history):
msg_tokens = len(self.tokenizer.tokenize(msg["content"]))
if old_tokens + msg_tokens > self.max_tokens // 2:
break
old_messages.insert(0, msg)
old_tokens += msg_tokens
# تلخيص الرسائل القديمة
old_text = "\n".join([msg["content"] for msg in old_messages])
summary = summarizer(old_text, max_length=100, min_length=30, do_sample=False)[0]["summary_text"]
# استبدال الرسائل القديمة بالملخص
self.conversation_history = [
{"role": "system", "content": f"ملخص المحادثة السابقة: {summary}"}
] + self.conversation_history[len(old_messages):]
def get_context(self):
# إرجاع التاريخ كاملاً كنص
return "\n".join([
f"{msg['role']}: {msg['content']}"
for msg in self.conversation_history
])
# مثال على الاستخدام
context_manager = ChatContextManager()
context_manager.add_message("user", "مرحبا، كيف حالك؟")
context_manager.add_message("assistant", "أنا بخير، شكراً لك! كيف يمكنني مساعدتك اليوم؟")
print(context_manager.get_context())حتى الآن، الشات بوت يعتمد فقط على المعرفة المخزنة داخل النموذج نفسه. هذا جيد للأسئلة العامة، لكنه سيفشل في الأسئلة التي تحتاج إلى معلومات محدثة أو محددة. مثلاً، إذا سألت البوت "ما هو سعر سهم أرامكو اليوم؟"، فلن يعرف الإجابة لأنه لا يملك وصولاً إلى بيانات السوق الحالية. هنا يأتي دور تقنية RAG (Retrieval-Augmented Generation)، التي تدمج بين البحث عن المعلومات في قاعدة بيانات خارجية وتوليد الاستجابة باستخدام النموذج اللغوي.
في حالتنا، سنستخدم RAG لتحسين استجابات البوت للأسئلة التي تتعلق بالمعلومات المحلية أو البيانات المحدثة. مثلاً، يمكننا ربط البوت بقاعدة بيانات تحتوي على أسعار الأسهم المحلية، أو معلومات عن المطاعم في المدن السعودية، أو حتى بيانات الطقس الحالية. سنستخدم مكتبة FAISS من فيسبوك لبناء فهرس بحث سريع للبيانات، ثم ندمج النتائج مع الـ Context Window قبل توليد الاستجابة. هذه الطريقة تضمن أن البوت لن يهلوس بإجابات غير صحيحة، بل سيستخدم البيانات الحقيقية المتاحة لديه.
from langchain.vectorstores import FAISS
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.text_splitter import CharacterTextSplitter
import json
# تحميل بيانات تجريبية (مثال: معلومات عن المدن السعودية)
with open("saudi_cities.json", "r", encoding="utf-8") as f:
documents = json.load(f)
# تقسيم النصوص إلى قطع صغيرة
text_splitter = CharacterTextSplitter(chunk_size=200, chunk_overlap=50)
texts = text_splitter.split_text("\n".join([
f"مدينة {doc['name']}: {doc['description']}"
for doc in documents
]))
# إنشاء embeddings للنصوص باستخدام نموذج عربي
embeddings = HuggingFaceEmbeddings(
model_name="CAMeL-Lab/bert-base-arabic-camelbert-da"
)
# بناء فهرس FAISS
vectorstore = FAISS.from_texts(texts, embeddings)
# دالة للبحث عن المعلومات ذات الصلة
def retrieve_relevant_info(query, k=3):
docs = vectorstore.similarity_search(query, k=k)
return "\n".join([doc.page_content for doc in docs])
# مثال على الاستخدام
query = "ما هي أفضل الأماكن السياحية في الرياض؟"
relevant_info = retrieve_relevant_info(query)
print("المعلومات ذات الصلة:", relevant_info)
# دمج المعلومات مع الـ Context Window
context_manager.add_message("user", query)
full_c context_manager.get_context() + "\n\nمعلومات إضافية:\n" + relevant_info
# توليد الاستجابة باستخدام النموذج
input_ids = tokenizer.encode(full_context, return_tensors="pt").to("cuda")
output = model.generate(
input_ids,
max_new_tokens=200,
do_sample=True,
top_p=0.9,
temperature=0.7
)
response = tokenizer.decode(output[0], skip_special_tokens=True)
print("استجابة البوت:", response.split("assistant:")[-1].strip())الآن، دعنا نتعامل مع التحدي الأكبر: اللهجات العربية. كما ذكرنا سابقاً، النموذج المدرب على الفصحى قد لا يفهم اللهجات بشكل جيد. هناك طريقتان رئيسيتان لحل هذه المشكلة: الأولى هي تدريب النموذج على بيانات اللهجات، والثانية هي استخدام تقنية تسمى Dialect Normalization لتحويل اللهجات إلى شكل قريب من الفصحى قبل معالجة النموذج. الطريقة الأولى تحتاج إلى موارد ضخمة (بيانات كبيرة وقوة حوسبة عالية)، لذلك سنستخدم الطريقة الثانية لأنها أكثر عملية للمشاريع الصغيرة والمتوسطة.
سنستخدم مكتبة تسمى Camel Tools لتطبيع اللهجات العربية. هذه المكتبة قادرة على تحويل اللهجات الشامية والمصرية والخليجية إلى شكل قريب من الفصحى. مثلاً، ستحول "شو أخبارك؟" إلى "ما أخبارك؟"، و"وينك؟" إلى "أين أنت؟". هذه التقنية ليست مثالية، لكنها تحسن الفهم السياقي بشكل كبير. بعد التطبيع، سنمرر النص إلى النموذج كما هو، مما يزيد من فرص فهمه بشكل صحيح. بالإضافة إلى ذلك، سنستخدم تقنية تسمى Few-Shot Learning لإعطاء النموذج أمثلة على كيفية التعامل مع اللهجات في بداية المحادثة، مما يساعد على تحسين الاستجابات.
from camel_tools.disambig.mle import MLEDisambiguator
from camel_tools.tokenizers.word import simple_word_tokenize
from camel_tools.normalizers import Normalizer
# تحميل أدوات التطبيع
normalizer = Normalizer()
disambiguator = MLEDisambiguator.pretrained()
# دالة لتطبيع اللهجات
def normalize_dialect(text):
# تطبيع النص الأساسي (إزالة التشكيل، توحيد الهمزات، إلخ)
normalized = normalizer.normalize(text)
# تحليل صرفي للنص
tokens = simple_word_tokenize(normalized)
disambig = disambiguator.disambiguate(tokens)
# تحويل اللهجات إلى الفصحى (بسيط)
dialect_map = {
"شو": "ما",
"وين": "أين",
"كيفك": "كيف حالك",
"وينك": "أين أنت",
"وينكم": "أين أنتم",
"شغلك": "عملك",
"شلون": "كيف"
}
normalized_tokens = []
for token in tokens:
if token in dialect_map:
normalized_tokens.append(dialect_map[token])
else:
normalized_tokens.append(token)
return " ".join(normalized_tokens)
# مثال على التطبيع
dialect_text = "شو أخبارك يا عم؟ وينك هسه؟"
normalized_text = normalize_dialect(dialect_text)
print("النص الأصلي:", dialect_text)
print("النص بعد التطبيع:", normalized_text)
# استخدام التطبيع في الشات بوت
user_input = "شلون أروح من جدة للرياض؟"
normalized_input = normalize_dialect(user_input)
context_manager.add_message("user", normalized_input)
full_c context_manager.get_context()
# توليد الاستجابة
input_ids = tokenizer.encode(full_context, return_tensors="pt").to("cuda")
output = model.generate(
input_ids,
max_new_tokens=200,
do_sample=True,
top_p=0.9,
temperature=0.7
)
response = tokenizer.decode(output[0], skip_special_tokens=True)
print("استجابة البوت:", response.split("assistant:")[-1].strip())الآن بعد أن بنينا الشات بوت، حان الوقت لنشره كخدمة ويب يمكن الوصول إليها عبر API. سنستخدم Flask لبناء API بسيط، وسنضيف طبقة حماية أساسية لمنع إساءة الاستخدام. أحد أهم الأمور التي يجب مراعاتها عند نشر الشات بوت هو إدارة الـ Rate Limiting، حيث أن النماذج اللغوية الكبيرة تستهلك موارد كثيرة وقد تكلف الكثير إذا تم استخدامها بشكل مفرط. سنستخدم مكتبة Flask-Limiter لإضافة حدود للطلبات، وسنخزن الـ Context لكل مستخدم في ذاكرة مؤقتة باستخدام Redis لتجنب إعادة تحميل النموذج مع كل طلب.
بالإضافة إلى ذلك، سنضيف طبقة من الـ Caching للاستجابات المتكررة. مثلاً، إذا سأل 100 مستخدم عن "ما هي عاصمة مصر؟"، فلا داعي لتشغيل النموذج 100 مرة. بدلاً من ذلك، سنخزن الاستجابة في ذاكرة مؤقتة ونعيدها مباشرة للطلبات المتكررة. هذه التقنية تقلل من الحمل على السيرفر وتسرع الاستجابات. سنستخدم مكتبة Flask-Caching مع Redis كخادم للذاكرة المؤقتة. أخيراً، سنضيف طبقة من الـ Logging لتتبع الاستخدام والأخطاء، مما يساعد في تحسين الأداء مع مرور الوقت.
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_caching import Cache
import redis
import uuid
app = Flask(__name__)
# إعداد Redis للذاكرة المؤقتة والسياق
redis_client = redis.Redis(host='localhost', port=6379, db=0)
cache = Cache(app, c{'CACHE_TYPE': 'RedisCache', 'CACHE_REDIS_URL': 'redis://localhost:6379/0'})
# إعداد Rate Limiting
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
# تحميل النموذج مرة واحدة عند بدء التشغيل
model = AutoModelForCausalLM.from_pretrained(
"core42/jais-13b-chat",
torch_dtype=torch.bfloat16,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained("core42/jais-13b-chat")
@app.route('/chat', methods=['POST'])
@limiter.limit("5 per minute")
def chat():
data = request.get_json()
user_id = data.get('user_id', str(uuid.uuid4()))
user_input = data.get('message', '')
# تطبيع النص إذا كان باللهجة
normalized_input = normalize_dialect(user_input)
# استرجاع السياق من Redis
context_key = f"context:{user_id}"
context = redis_client.get(context_key)
if context:
context = context.decode('utf-8')
else:
context = ""
# إضافة الرسالة الجديدة للسياق
full_context = f"{context}\nuser: {normalized_input}"
# توليد الاستجابة
input_ids = tokenizer.encode(full_context, return_tensors="pt").to("cuda")
output = model.generate(
input_ids,
max_new_tokens=200,
do_sample=True,
top_p=0.9,
temperature=0.7
)
response = tokenizer.decode(output[0], skip_special_tokens=True)
bot_response = response.split("assistant:")[-1].strip()
# تحديث السياق في Redis
new_context = f"{full_context}\nassistant: {bot_response}"
redis_client.setex(context_key, 3600, new_context) # expires in 1 hour
return jsonify({"response": bot_response})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, threaded=True)بعد بناء هذا الشات بوت من الصفر، هناك عدة دروس مهمة تعلمتها من التجربة. أولاً، لا تعتمد فقط على النموذج اللغوي الكبير للقيام بكل العمل. استخدم تقنيات مثل RAG وContext Management لتحسين الدقة وتجنب الهلوسة. ثانياً، اللغة العربية تحتاج إلى معالجة خاصة: استخدم Tokenizer عربي جيد، وطبق تقنيات التطبيع للهجات، ولا تنسَ أن الـ Context Window يستهلك بسرعة بسبب تعقيد اللغة. ثالثاً، لا تهمل الجانب العملي عند النشر: أضف Rate Limiting وCaching وLogging من البداية لتجنب المشاكل لاحقاً.
إذا كنت تريد تحسين الشات بوت أكثر، فكر في تدريب نموذج صغير مخصص لحالتك الاستخدام باستخدام بياناتك الخاصة. مثلاً، إذا كنت تبني بوت لخدمة العملاء في بنك، قم بتدريب نموذج على بيانات المحادثات الحقيقية للعملاء. هذا سيحسن الفهم السياقي بشكل كبير. أيضاً، لا تنسَ اختبار الشات بوت مع مستخدمين حقيقيين: اجمع ملاحظاتهم واستخدمها لتحسين الأداء. وأخيراً، راقب استخدام الذاكرة والمعالج عن كثب: النماذج الكبيرة تستهلك موارد كثيرة، وقد تحتاج إلى تحسين الـ Batch Size أو استخدام تقنيات مثل Quantization لتقليل استهلاك الموارد.
الذكاء الاصطناعي ليس سحراً، بل هو هندسة دقيقة تجمع بين البيانات والخوارزميات والبنية التحتية. كلما فهمت التفاصيل خلف الكواليس، كلما استطعت بناء أنظمة أكثر ذكاءً وموثوقية.
— مهندس برمجيات سنيور في مجال LLMs