خلاصة القول
يستغرق بناء نموذج لغوي بسيط من الصفر أقل من 300 سطر من بايثون. تكشف العملية بالضبط كيف تعمل عملية ترميز النص (tokenization)، والانتباه (attention)، والاستدلال (inference)، مما يجعلك مستهلكًا أفضل بكثير لواجهات برمجة التطبيقات (API) عند دمج نماذج اللغة الكبيرة (LLMs) في تطبيقاتك.
مقدمة
يتعامل معظم المطورين مع النماذج اللغوية كصناديق سوداء. ترسل نصًا إليها، وتخرج رموزًا (tokens)، وبين هذا وذاك، يحدث السحر. يعمل هذا النموذج الذهني بشكل جيد حتى تحتاج إلى تصحيح خطأ في دمج واجهة برمجة تطبيقات معطلة، أو ضبط معلمات أخذ العينات، أو معرفة سبب استمرار نموذجك في هلوسة البيانات المهيكلة.
مشروع GuppyLM، الذي تصدر مؤخرًا الصفحة الرئيسية لموقع HackerNews بـ 842 نقطة، يجعل التفاصيل الداخلية مرئية. إنه محول (transformer) يحتوي على 8.7 مليون معلمة مكتوب من الصفر بلغة بايثون. يتدرب في أقل من ساعة على وحدة معالجة رسوميات استهلاكية (GPU). الرمز البرمجي يتناسب مع ملف واحد. الهدف ليس التنافس مع GPT-4؛ بل هو إزالة الغموض عن كيفية عمل نماذج اللغة الكبيرة (LLMs) بالفعل.
تتناول هذه المقالة كيفية بناء نموذج لغوي كبير (LLM) صغير، وما تفعله كل مكونة، وما الذي يعلمك إياه فهم التفاصيل الداخلية عند العمل مع واجهات برمجة تطبيقات الذكاء الاصطناعي (AI APIs) بشكل احترافي.
ما الذي يجعل النموذج اللغوي "صغيرًا"؟
يمتلك نموذج لغوي كبير (LLM) إنتاجي مثل GPT-4 مئات المليارات من المعلمات. يقع النموذج اللغوي "الصغير" في نطاق 1 مليون إلى 25 مليون معلمة. تندرج مشاريع مثل GuppyLM (8.7 مليون)، وnanoGPT من Karpathy (124 مليون)، وMicroLM (1-2 مليون) ضمن هذه الفئة.
يمكن للنماذج اللغوية الصغيرة (Tiny LLMs) أن: - تتدرب على جهاز كمبيوتر محمول أو Google Colab - تتناسب بالكامل في ذاكرة وحدة المعالجة المركزية (CPU) - يمكن فحصها وتعديلها وتصحيح أخطائها على مستوى الأوزان
لا تستطيع أن: - تتعامل مع الاستدلال المعقد - توليد نصوص طويلة متماسكة بشكل موثوق - تتطابق مع العمق الواقعي للنماذج الإنتاجية
القيمة ليست في المخرجات. بل في الفهم الذي تحصل عليه من بناء نموذج.
المكونات الأساسية: كيف يعمل النموذج اللغوي (LLM) بالفعل
قبل كتابة أي رمز برمجي، تحتاج إلى معرفة ما تفعله الأجزاء الأربعة الرئيسية.
مُرمّز النص (Tokenizer)
يقوم مرمّز النص (tokenizer) بتحويل النص الخام إلى معرفات عددية. "Hello, world!" يصبح شيئًا مثل [15496, 11, 995, 0]. كل عدد صحيح يرتبط بوحدة فرعية من الكلمة من مفردات ثابتة.
لماذا يهم هذا لعمل واجهة برمجة التطبيقات (API): تؤثر أعداد الرموز (token counts) بشكل مباشر على زمن الاستجابة والتكلفة. يساعدك فهم كيفية تقسيم مرمّزات النص للنص على كتابة تعليمات (prompts) تتناسب مع نوافذ السياق وتجنب الاقتطاع غير المتوقع.
يستخدم GuppyLM مرمّز نص بسيط على مستوى الحرف. تستخدم نماذج الإنتاج مثل GPT-4 ترميز BPE (ترميز زوج البايت) مع مفردات تتراوح من 50 ألف إلى 100 ألف رمز.
طبقة التضمين (Embedding layer)
تقوم طبقة التضمين بتحويل معرفات الرموز إلى متجهات كثيفة. يحصل كل رمز على متجه متعلم (مثل 384 بعدًا في GuppyLM). تحمل هذه المتجهات معنى دلاليًا: تنتهي الرموز المتشابهة بالقرب من بعضها البعض في فضاء المتجهات.
تُضاف تضمينات الموضع (position embeddings) فوقها، ليعرف النموذج ترتيب الرموز.
كتل المحول (Transformer blocks)
هذه هي الحوسبة الأساسية. تتكون كل كتلة من جزأين:
الانتباه الذاتي (Self-attention): يتيح لكل رمز النظر إلى جميع الرموز الأخرى في التسلسل وتحديد أي منها يهم للتنبؤ بالرمز التالي. يستخدم GuppyLM 6 رؤوس انتباه عبر 6 طبقات.
شبكة التغذية الأمامية (Feed-forward network): شبكة MLP من طبقتين تُطبق على تمثيل كل رمز بعد الانتباه. يستخدم GuppyLM دالة تفعيل ReLU، وهي أبسط من SwiGLU المستخدمة في البنى الأحدث.
رأس الإخراج (Output head)
بعد كتلة المحول النهائية، تقوم طبقة خطية بتحويل تمثيل كل رمز إلى متجه بحجم يساوي حجم المفردات. يتم تطبيق دالة Softmax للحصول على الاحتمالات، ثم يتم اختيار الرمز التالي الأكثر احتمالاً (أو أخذ عينة)، وتتكرر العملية.
بناء نموذج لغوي صغير (LLM) في بايثون
إليك نموذج لغوي صغير عامل يعتمد على منهج GuppyLM. يعمل هذا في PyTorch القياسي.
import torch
import torch.nn as nn
import torch.nn.functional as F
# Hyperparameters
VOCAB_SIZE = 256 # character-level: one slot per ASCII char
D_MODEL = 128 # embedding dimension
N_HEADS = 4 # attention heads
N_LAYERS = 3 # transformer blocks
SEQ_LEN = 64 # context window
DROPOUT = 0.1
class SelfAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
self.proj = nn.Linear(d_model, d_model, bias=False)
self.dropout = nn.Dropout(DROPOUT)
def forward(self, x):
B, T, C = x.shape
qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim)
q, k, v = qkv.unbind(dim=2)
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
# Causal mask: each token can only attend to previous tokens
scale = self.head_dim ** -0.5
attn = (q @ k.transpose(-2, -1)) * scale
mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
attn = attn.masked_fill(mask, float('-inf'))
attn = F.softmax(attn, dim=-1)
attn = self.dropout(attn)
out = (attn @ v).transpose(1, 2).reshape(B, T, C)
return self.proj(out)
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
self.attn = SelfAttention(d_model, n_heads)
self.ff = nn.Sequential(
nn.Linear(d_model, 4 * d_model),
nn.ReLU(),
nn.Linear(4 * d_model, d_model),
nn.Dropout(DROPOUT),
)
self.ln1 = nn.LayerNorm(d_model)
self.ln2 = nn.LayerNorm(d_model)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.ff(self.ln2(x))
return x
class TinyLLM(nn.Module):
def __init__(self):
super().__init__()
self.embed = nn.Embedding(VOCAB_SIZE, D_MODEL)
self.pos_embed = nn.Embedding(SEQ_LEN, D_MODEL)
self.blocks = nn.ModuleList([
TransformerBlock(D_MODEL, N_HEADS) for _ in range(N_LAYERS)
])
self.ln_f = nn.LayerNorm(D_MODEL)
self.head = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)
def forward(self, idx):
B, T = idx.shape
tok_emb = self.embed(idx)
pos = torch.arange(T, device=idx.device)
pos_emb = self.pos_embed(pos)
x = tok_emb + pos_emb
for block in self.blocks:
x = block(x)
x = self.ln_f(x)
logits = self.head(x)
return logits
# Initialize and count parameters
model = TinyLLM()
total_params = sum(p.numel() for p in model.parameters())
print(f"Model size: {total_params:,} parameters") # ~1.2M
حلقة التدريب
import torch.optim as optim
def train(model, data, epochs=100, lr=3e-4):
optimizer = optim.AdamW(model.parameters(), lr=lr)
model.train()
for epoch in range(epochs):
# data: tensor of token IDs, shape [batch, seq_len+1]
x = data[:, :-1] # input: all tokens except last
y = data[:, 1:] # target: all tokens shifted by 1
logits = model(x)
loss = F.cross_entropy(logits.reshape(-1, VOCAB_SIZE), y.reshape(-1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}, loss: {loss.item():.4f}")
الاستدلال (توليد النص)
@torch.no_grad()
def generate(model, prompt_ids, max_new_tokens=50, temperature=1.0, top_k=10):
model.eval()
ids = torch.tensor([prompt_ids])
for _ in range(max_new_tokens):
idx_cond = ids[:, -SEQ_LEN:] # crop to context window
logits = model(idx_cond)
logits = logits[:, -1, :] / temperature # last token only
# top-k sampling
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float('-inf')
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, num_samples=1)
ids = torch.cat([ids, next_id], dim=1)
return ids[0].tolist()
ما الذي يعلمك إياه هذا عن سلوك واجهات برمجة تطبيقات الذكاء الاصطناعي
يكشف بناء هذا النموذج عدة أمور تجعلك مستهلكًا أفضل لواجهة برمجة التطبيقات.
درجة الحرارة (Temperature) وأخذ العينات (sampling) ميكانيكية وليست سحرية
تقسم درجة الحرارة اللوجيتات (logits) قبل دالة Softmax. درجة حرارة أعلى = توزيع أكثر تسطحًا = مخرجات أكثر عشوائية. درجة حرارة أقل = توزيع أكثر حدة = مخرجات أكثر تحديدًا. عندما تُرجع واجهة برمجة تطبيقات الإنتاج الخاصة بك نتائج غير متسقة مع temperature=0.0، فهذا ليس خطأً. درجة الحرارة الصفرية الحقيقية هي argmax جشعة، والعديد من واجهات برمجة التطبيقات تخفضها قليلاً لتجنب المخرجات المتدهورة.
نوافذ السياق هي حدود صارمة وليست اقتراحات مرنة
يُظهر السطر idx_cond = ids[:, -SEQ_LEN:] في حلقة الاستدلال بالضبط ما يحدث عند حد السياق. يسقط النموذج الرموز الأقدم بصمت. إذا افترض دمج واجهة برمجة التطبيقات الخاصة بك أن النموذج يتذكر سجل المحادثة بالكامل، فإنه لا يفعل ذلك بعد نقطة معينة. انظر [internal: how-ai-agent-memory-works] لمعرفة كيفية تعامل الوكلاء مع هذه المشكلة.
الرموز المتدفقة هي مجرد خطوات استدلال مرئية
لا تفعل واجهات برمجة التطبيقات المتدفقة أي شيء مختلف من الناحية المعمارية. إنها تشغل حلقة الاستدلال وتدفع كل رمز إلى تيار الاستجابة فور توليده. يساعد هذا الفهم عندما تكتب منطق إعادة المحاولة: لا يمكن استئناف تيار متوقف في منتصف التوليد، بل يجب أن يبدأ من جديد.
اللوجيتات تشرح لماذا يصعب إنتاج مخرجات مهيكلة
يخصص النموذج احتمالية لكل رمز في المفردات في كل خطوة. يتطلب توليد JSON صالح أن يفوز الرمز الصحيح في كل موضع. تحد المكتبات مثل Outlines و Guidance توزيع اللوجيتات لفرض القواعد النحوية في وقت الاستدلال. عندما ترى واجهات برمجة تطبيقات الذكاء الاصطناعي تقدم أوضاع "الإخراج المهيكل"، فهذا ما تفعله داخليًا.
كيفية اختبار دمج واجهات برمجة تطبيقات الذكاء الاصطناعي باستخدام Apidog
بمجرد أن تفهم كيفية عمل استدلال نماذج اللغة الكبيرة (LLM)، يمكنك كتابة اختبارات واجهة برمجة تطبيقات أفضل بكثير. تتيح لك "سيناريوهات الاختبار" (Test Scenarios) في Apidog ربط استدعاءات واجهة برمجة التطبيقات والتأكيد على بنية استجابات الذكاء الاصطناعي.
على سبيل المثال، عند اختبار واجهة برمجة تطبيقات للدردشة المتدفقة:
- أنشئ سيناريو اختبار في Apidog باستخدام نقطة النهاية
/v1/chat/completions - اضبط التأكيدات للتحقق من بنية الاستجابة:
response.choices[0].finish_reason == "stop"،response.usage.total_tokens < 4096 - أضف خطوة متابعة ترسل الاستجابة كسياق للدور التالي، محاكية محادثة متعددة الأدوار
- استخدم Smart Mock من Apidog لوضع نقطة نهاية الذكاء الاصطناعي واختبار معالجة الأخطاء في تطبيقك: محاكاة
finish_reason: "length"(مخرجات مقتطعة)،finish_reason: "content_filter"، ومهلة الشبكة في منتصف التدفق
هذه هي الطريقة التي تختبر بها عمليات دمج الذكاء الاصطناعي دون استهلاك أرصدة واجهة برمجة التطبيقات في كل تشغيل CI. انظر [internal: api-testing-tutorial] للحصول على نظرة أوسع على أساليب اختبار واجهة برمجة التطبيقات.
اختبار تأكيدات عدد الرموز
{
"assertions": [
{
"field": "response.usage.completion_tokens",
"operator": "less_than",
"value": 512
},
{
"field": "response.choices[0].finish_reason",
"operator": "equals",
"value": "stop"
},
{
"field": "response.choices[0].message.content",
"operator": "not_empty"
}
]
}
شغل هذا عبر نماذج متعددة (GPT-4o، Claude 3.5 Sonnet، Gemini 1.5 Pro) في سيناريو اختبار واحد لالتقاط اختلافات مخطط واجهة برمجة التطبيقات قبل أن تصل إلى مرحلة الإنتاج.
متقدم: التكميم (Quantization) وتحسين الاستدلال (Inference Optimization)
بمجرد أن يكون لديك نموذج لغوي صغير (LLM) عامل، تستحق تقنيتان الفهم لأنهما تنطبقان مباشرة على كيفية تقديم نماذج الإنتاج.
التكميم (Quantization)
الأوزان في نموذجنا هي أرقام عشرية بحجم 32 بت افتراضيًا. يقلل التكميم حجمها إلى أعداد صحيحة بحجم 8 بت (INT8) أو حتى 4 بت (INT4). هذا يقلل من استخدام الذاكرة بمقدار 4-8 مرات مع خسارة متواضعة في الدقة.
# Example: dynamic INT8 quantization in PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
تشغل واجهات برمجة تطبيقات الإنتاج نماذج مكممة. عندما ترى جودة مخرجات مختلفة في "إصدارات" مختلفة من نفس النموذج، غالبًا ما يكون التكميم متضمنًا.
ذاكرة التخزين المؤقت KV (KV cache)
في حلقة الاستدلال لدينا، نعيد حساب الانتباه عبر التسلسل الكامل في كل خطوة. تقوم أنظمة الإنتاج بتخزين أزواج المفتاح-القيمة (key-value pairs) من الرموز السابقة (ذاكرة التخزين المؤقت KV) بحيث يحتاج كل رمز جديد إلى عملية حساب انتباه جديدة واحدة فقط. هذا هو السبب في أن الرمز الأول في استجابة التدفق يستغرق وقتًا أطول من الرموز اللاحقة.
نموذج لغوي صغير (Tiny LLM) مقابل واجهة برمجة تطبيقات إنتاجية: متى تستخدم كل منهما
| حالة الاستخدام | نموذج لغوي صغير (Tiny LLM) | واجهة برمجة تطبيقات إنتاجية |
|---|---|---|
| تعلم التفاصيل الداخلية للنموذج | الأفضل لـ | مبالغ فيه |
| إنشاء نموذج أولي لتطبيق جديد | جودة غير كافية | الأفضل لـ |
| بيانات خاصة/حساسة | خيار جيد | يعتمد على المزود |
| النشر دون اتصال/على الحافة | قابل للتطبيق | غير ممكن |
| حساس للتكلفة، حجم كبير | ممكن مع مقايضات | مكلف على نطاق واسع |
| مهام تتطلب استدلالًا كبيرًا | غير قابل للتطبيق | مطلوب |
الإجابة الحقيقية لمعظم المطورين: استخدم واجهة برمجة تطبيقات الإنتاج لتطبيقك، ولكن قم بتشغيل نموذج صغير لفهم ما يحدث تحت الغطاء. الاثنان لا يتنافسان. تتناول مقالة [internal: open-source-coding-assistants-2026] الأدوات التي تطمس هذا الخط مع إعدادات "أحضر نموذجك الخاص" (bring-your-own-model).
خاتمة
يستغرق بناء نموذج لغوي صغير (LLM) من الصفر عطلة نهاية أسبوع. ما تحصل عليه ليس نظام إنتاج؛ بل هو نموذج ذهني عملي لكيفية عمل كل نموذج لغوي، من GuppyLM إلى GPT-4o، بالفعل. هذا الفهم يؤتي ثماره في كل مرة تقوم فيها بتصحيح خطأ في دمج تدفق، أو ضبط معلمات أخذ العينات، أو تصميم تأكيدات لاختبارات واجهة برمجة تطبيقات الذكاء الاصطناعي الخاصة بك.
مشروع GuppyLM هو نقطة بداية جيدة. استنسخه، دربه على أي مجموعة بيانات نصية، واقضِ فترة ما بعد الظهر في قراءة حلقة الاستدلال. ثم عد إلى عمليات دمج واجهات برمجة تطبيقات الإنتاج الخاصة بك وسترى الأمور بشكل مختلف.
جرب سيناريوهات اختبار Apidog لإضفاء نفس الصرامة على اختبار واجهة برمجة تطبيقات الذكاء الاصطناعي الخاصة بك التي قد تطبقها على أي نظام خلفي آخر.
الأسئلة الشائعة
كم عدد المعلمات التي يحتاجها نموذج لغوي "صغير" لتوليد نص متماسك؟ حوالي 10 ملايين إلى 50 مليون معلمة مع مجموعة بيانات تدريب جيدة يمكن أن تنتج جملًا متماسكة محليًا. أقل من مليون، تحصل على كلام غير مفهوم في معظم المهام. GuppyLM بـ 8.7 مليون يعمل للمحادثات القصيرة في مجال تدريبه (60 موضوعًا).
هل يمكنني تشغيل نموذج لغوي صغير (LLM) بدون وحدة معالجة رسوميات (GPU)؟ نعم. تعمل النماذج التي تقل عن 100 مليون معلمة بشكل جيد على وحدة المعالجة المركزية (CPU)، على الرغم من أن الاستدلال يكون أبطأ. النموذج المذكور أعلاه (1.2 مليون معلمة) يولد رموزًا في غضون أجزاء من الثانية على وحدة المعالجة المركزية لجهاز كمبيوتر محمول.
ما هي مجموعة البيانات التي يجب أن أتدرب عليها؟ تعمل النماذج على مستوى الحرف بشكل جيد مع نصوص مشروع غوتنبرغ (Project Gutenberg)، أو مجموعات فرعية من ويكيبيديا، أو أي مجموعة نصوص عادية. يستخدم GuppyLM مجموعة بيانات محادثات تحتوي على 60 ألف إدخال على HuggingFace (arman-bd/guppylm-60k-generic). لتوليد الرمز البرمجي، استخدم The Stack أو CodeParrot.
ما الفرق بين درجة الحرارة (temperature) وأخذ العينات top-k؟ درجة الحرارة تعدّل توزيع اللوجيتات (تتحكم في العشوائية العامة). بينما يقيد top-k مجموعة العينات إلى الرموز الأكثر احتمالًا (k) قبل تطبيق درجة الحرارة. يتم تطبيقهما معًا: يقوم top-k أولاً بتصفية المرشحين، ثم تشكل درجة الحرارة الاحتمالات داخل تلك المجموعة.
لماذا يكرر نموذج LLM نفسه أحيانًا؟ التكرار هو نمط فشل حيث يخصص النموذج احتمالية عالية للرموز التي ولدها للتو لأنها ظهرت في السياق. تستخدم واجهات برمجة التطبيقات الإنتاجية عقوبات التكرار (تعديل اللوجيت الذي يخصم الرموز المولدة حديثًا). أضف repetition_penalty=1.1 في استدعاء واجهة برمجة التطبيقات الخاصة بك لتقليل ذلك.
كم يستغرق تدريب نموذج LLM صغير؟ يتدرب النموذج المذكور أعلاه لإنتاج مخرجات متماسكة في أقل من ساعتين على وحدة معالجة رسوميات واحدة (RTX 3060 أو ما يعادلها). يتدرب GuppyLM في Colab في نفس الوقت تقريبًا. تتطلب النماذج الأكبر (أكثر من 100 مليون معلمة) إعدادات متعددة لوحدات معالجة الرسوميات وأيامًا من التدريب.
ما هي أسرع طريقة للانتقال من نموذج LLM صغير إلى نقطة نهاية API حقيقية؟ قم بالتصدير إلى تنسيق GGUF باستخدام نص تحويل llama.cpp، ثم قم بتقديمه باستخدام llama-server. سيوفر لك هذا نقطة نهاية API متوافقة مع OpenAI تعمل محليًا. يمكنك بعد ذلك توجيه Apidog إليها للاختبار، انظر [internal: rest-api-best-practices].
كيف تتعامل نماذج LLM الإنتاجية مع السياقات الأطول من نافذة تدريبها؟ تعمل تقنيات مثل RoPE (تضمين الموضع الدوراني) مع التوسع الموسع، وانتباه النافذة المنزلقة (sliding window attention)، والتوليد المعزز بالاسترجاع (retrieval-augmented generation) على توسيع السياق الفعال. لا تتغير بنية المحول الأساسية؛ هذه تعديلات على كيفية ترميز معلومات الموضع وكيفية تطبيق نافذة الانتباه.
