انتقل للمحتوى
علي هيثم·TECH·يردّ خلال أقل من ساعة
  • الرئيسيةابدأ من هنا
  • الأعمالدراسات حالة، مشاريع
  • الخدماتما الذي سأبنيه لك
  • المدونةمقالات، سلاسل
  • التدريبدورات، مختبرات، دورات
  • عنّيالمهندس وراء هذا الموقع
تسجيل الدخولابدأ مشروع
علي هيثم · TECH
  • الرئيسية↗
  • الأعمال↗
  • الخدمات↗
  • المدونة↗
  • التدريب↗
  • عنّي↗
تسجيل الدخولابدأ مشروع
متاح
علي هيثم·TECH

استوديو هندسي. دمشق، GMT+3.

aliyosef.online

الاستوديو

  • الأعمال
  • المدونة
  • التدريب
  • عنّي
  • تواصل معي

البوابة

  • تسجيل الدخول
  • فتح تذكرة دعم
  • متابعة مشروع
  • حسابي

مصادر

  • الوثائق
  • حالة الأنظمة
  • سجلّ التغييرات
  • حزمة العلامة
  • الخصوصية
  • الشروط

النشرة

ملاحظات تقنية وأخبار، أسبوعياً. بدون حشو.

مجاني. إلغاء الاشتراك متى ما حبيت.

تابعني على
© 2026 علي هيثم يوسف. جميع الحقوق محفوظة.مبنيّ يدويًا بـ React 19.آخر نشر · 2026-05-08كل الأنظمة تعمل
  1. الرئيسيّة/
  2. تدريب/
  3. مختبرات/
  4. اضبط حزمة eval لـ LLM
مختبر٠٤

اضبط حزمة eval لـ LLM

لا تستطيع شحن ميزات مدعومة بـ LLM دون طريقة لمعرفة متى تسوء الأمور. معظم الفرق تلصق المخرجات في جدول لأسبوع، ثمّ تتوقّف. هذا المختبر يبني حزام تقييم بـ ٢٠٠ سطر يشغّل كلّ تغيير prompt مقابل مجموعة بيانات ثابتة، يُسجّل النتائج بتأكيدات قائمة على الخصائص (بدون تقييم بشريّ)، ويُصدر diff تستطيع وضعه في تعليق pull-request.

ابدأ المختبر↓افتح المستودع↖
دقيقة
90
الـ STACK
Node
المستوى
متقدّم
/02 — التهيئة

ما ستبنيه

حزام تقييم بـ ٢٠٠ سطر لأدوات غير حتميّة. مُسجِّلون قائمون على الخصائص، بدون تقييم بشريّ.

المتطلّبات

  1. 01Node 20+ ومفتاح API متوافق مع OpenAI أو Anthropic.
  2. 02أجريت نداء API إنتاجيّ واحد على الأقلّ لـ LLM.
  3. 03حفنة من أزواج prompt-input حقيقيّة من تطبيقك لبذر مجموعة البيانات.
/03 — الشرح
/04 — موارد

خذها معك.

  • ⌥
    المستودعالكود الابتدائيّ على GitHub.
  • ↓
    تحميل zipنفس الكود، بدون git.
/يُستخدم في

دورات تتزاوج مع هذا المختبر.

  • الهندسة المقترنة بالـ AI→
/05 — التالي
جرّب هذا تالياً٠١ابنِ queue في ٦٠ دقيقةمختبر آخر قصير ومجّانيّ.→
أو اذهب إلى العمق

هندسة مقترنة بـ AI

حزم الـ eval قطعة واحدة. الدورة الكاملة تغطّي حواجز الكلفة، استراتيجيّات الـ fallback، وعمليّات اليوم الثاني لتشغيل LLMs في نظام تتحمّل دعمه.

شاهد الدورة←

لماذا تتوقّف معظم الفرق عن القياس بعد الأسبوع الأوّل#

أوّل مرّة تشحن ميزة مدعومة بـ LLM، تنسخ بضعة مخرجات إلى Google Sheet، تسجّلها يدوياً، وتشعر بالمسؤوليّة. الخامسة التي تغيّر فيها الـ prompt، الجدول فيه ٤٠٠ صفّ، لا أحد يتذكّر اصطلاحات الأعمدة، وتتوقّف عن فتحه. من تلك النقطة فصاعداً، كلّ تغيير prompt قرار قائم على الإحساس.

العلاج ليس "راجع بعناية أكثر". العلاج حزام صغير يشغّل كلّ تغيير prompt مقابل مجموعة بيانات ثابتة، يسجّل النتائج بالكود، ويطبع diff. مئتا سطر. خمسة ملفّات. لا تقييم بشريّ.

الخطوة ١: مجموعة البيانات#

مجموعة البيانات ملفّ JSON. أصغرها مفيدة بـ ٢٠ مدخلاً، كلّ منها بثلاثة مفاتيح: id، input، و expected. حقل expected ليس الإجابة الصحيحة بالضبط؛ هو مجموعة خصائص يجب أن تستوفيها الإجابة.

[
  {
    "id": "1",
    "input": "Summarise this support ticket: ...",
    "expected": {
      "must_contain_any": ["refund", "return", "exchange"],
      "max_words": 80,
      "must_be_in_language": "en"
    }
  },
  {
    "id": "2",
    "input": "ترجم وصف هذه الفاتورة العربيّة: ...",
    "expected": {
      "must_be_in_language": "ar",
      "must_not_contain": ["TODO", "[INSERT", "<", ">"]
    }
  }
]

الانضباط هو بناء مجموعة البيانات من تتبّعات إنتاجيّة حقيقيّة، ليس أمثلة اصطناعيّة. اسحب ٢٠ مدخلاً حقيقيّاً من سجلّاتك، اكتب ما يجعل الإجابة "جيّدة بما يكفي"، ولديك خطّ أساس يمسك التراجعات في مساحة السطح الفعليّة التي يضربها المستخدمون.

الخطوة ٢: المُشغّل#

المُشغّل حلقة async صغيرة تنادي عميل LLM لكلّ مدخل وتُنتج Result:

import { OpenAI } from 'openai';

interface DatasetEntry {
  id: string;
  input: string;
  expected: Record<string, unknown>;
}

interface Result {
  id: string;
  output: string;
  expected: DatasetEntry['expected'];
}

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function runOne(entry: DatasetEntry, prompt: string): Promise<Result> {
  const resp = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: prompt },
      { role: 'user', content: entry.input },
    ],
  });
  return {
    id: entry.id,
    output: resp.choices[0].message.content ?? '',
    expected: entry.expected,
  };
}

export async function runAll(
  dataset: DatasetEntry[],
  prompt: string,
  concurrency = 4,
): Promise<Result[]> {
  const results: Result[] = [];
  let cursor = 0;
  await Promise.all(
    Array(concurrency).fill(0).map(async () => {
      while (cursor < dataset.length) {
        const idx = cursor++;
        results[idx] = await runOne(dataset[idx], prompt);
      }
    }),
  );
  return results;
}

التوازي المحدود قرار هادئ: قليل جدّاً ويستغرق التشغيل ٢٠ دقيقة، عالٍ جدّاً وتُسقِط حدود المعدّل أو ميزانيّة الطلب. أربعة افتراضيّ جيّد لمعظم الـ APIs.

الخطوة ٣: المُسجِّلون القائمون على الخصائص#

المُسجِّل دالّة نقيّة من (output, expected) إلى { ok: boolean; reason?: string }. خمسة منهم يغطّون معظم ما تهتمّ به فعلاً:

type Score = { ok: boolean; reason?: string };

const scorers = {
  must_contain_any: (out: string, expected: string[]): Score =>
    expected.some((s) => out.toLowerCase().includes(s.toLowerCase()))
      ? { ok: true }
      : { ok: false, reason: `none of: ${expected.join(', ')}` },

  must_not_contain: (out: string, expected: string[]): Score => {
    const hit = expected.find((s) => out.includes(s));
    return hit ? { ok: false, reason: `contains: ${hit}` } : { ok: true };
  },

  max_words: (out: string, expected: number): Score => {
    const n = out.trim().split(/\s+/).length;
    return n <= expected
      ? { ok: true }
      : { ok: false, reason: `${n} > ${expected} words` };
  },

  min_chars: (out: string, expected: number): Score =>
    out.length >= expected
      ? { ok: true }
      : { ok: false, reason: `${out.length} < ${expected} chars` },

  must_be_in_language: (out: string, expected: 'ar' | 'en'): Score => {
    const arabic = /[؀-ۿ]/.test(out);
    const isAr = arabic;
    return (expected === 'ar') === isAr
      ? { ok: true }
      : { ok: false, reason: `expected ${expected}, looks ${isAr ? 'ar' : 'en'}` };
  },
};

كشف اللغة فظّ عمداً. التسجيل القائم على الخصائص ليس عن تصنيف مثاليّ؛ هو عن مسك الحالة التي يعيد فيها النموذج إنجليزيّة حين طلبت عربيّة، وتحدث أكثر ممّا تعتقد.

الخطوة ٤: التسجيل + الـ diff#

طبّق كلّ مُسجِّل يطابق مفتاحاً في expected، اجمع الإخفاقات، وأصدر نتيجة منظّمة. أدناه المنسّق:

function scoreOne(result: Result): { id: string; pass: boolean; failures: string[] } {
  const failures: string[] = [];
  for (const [key, expected] of Object.entries(result.expected)) {
    const fn = scorers[key as keyof typeof scorers];
    if (!fn) continue;
    const s = fn(result.output as never, expected as never);
    if (!s.ok) failures.push(`${key}: ${s.reason}`);
  }
  return { id: result.id, pass: failures.length === 0, failures };
}

export function summarise(results: Result[]) {
  const scored = results.map(scoreOne);
  const passed = scored.filter((r) => r.pass).length;
  const total = scored.length;
  return {
    passRate: passed / total,
    failed: scored.filter((r) => !r.pass),
  };
}

شغّله مرّتَين (مرّة بالـ prompt القديم، مرّة بالجديد)، ثمّ JSON-diff الملخّصَين. ذلك الـ diff هو ما يذهب في تعليق pull-request.

الخطوة ٥: خطّاف الـ CI#

سطران من GitHub Action يحوّلان الحزام إلى البوّابة التي تمنع تراجعات الـ prompt من الشحن:

- run: npm run eval -- --prompt prompts/v3.txt --threshold 0.85
- run: npm run eval -- --prompt prompts/v3.txt --diff prompts/v2.txt

علامة --threshold تُفشل الوظيفة إن نزل معدّل النجاح تحت ٨٥٪. علامة --diff تشغّل كلا الـ prompts وتطبع deltas لكلّ id. معاً يمنعان أكثر تراجع شائع لـ LLM: الـ prompt يبدو أفضل في ثلاث فحوصات سريعة، يُشحن، ويفشل على الذيل الطويل.

ما ليس عليه هذا المختبر#

هذا المُشغّل، المُسجِّلون، والـ diff. ليس حاجز الكلفة (محاسبة tokens لكلّ نداء وتنبيهات الميزانيّة)، ليس استراتيجيّة الـ fallback (متى تتوقّف عن نداء النموذج وتُقدّم إجابة مخزّنة)، وليس عمليّات اليوم الثاني (كيف تُحدّث مجموعة البيانات أسبوعيّاً دون كسر خطّ الأساس). دورة هندسة مقترنة بـ AI الكاملة تغطّي الثلاثة فوق نفس الحزام.