ابنِ أداة JavaScript لاختبار تراجع LLM

Share:

شرح عملي · JavaScript · اختبار تراجع LLM

ابنِ أداة JavaScript لاختبار تراجع LLM

قد تتراجع تطبيقات نماذج اللغة عند تغيير prompts أو النماذج أو APIs. في هذا الشرح، سنبني أداة اختبار صغيرة بلغة JavaScript تقيّم الاستجابات المحفوظة وفق قواعد قبول واضحة وحتمية. وتوفر هذه الأداة أساسًا مستقلًا عن مزود الخدمة؛ إذ تؤكد المصادر الموثوقة المرفقة أهمية اختبار تراجع LLM وتحدياته، لكنها لا تتحقق من صياغة SDKs الحالية لـ OpenAI أو Anthropic، أو أسماء النماذج، أو معاملات نقاط النهاية. لذلك، لا تدّعي هذه العملية توفير موصل API مباشر وموثوق تم التحقق منه.

لماذا تهم اختبارات التراجع تطبيقات LLM؟

تقارن اختبارات البرمجيات التقليدية غالبًا مخرجات دالة بنتيجة متوقعة. أما استجابات LLM فهي أقل اتساقًا؛ فقد تُصاغ الإجابة بطريقة مختلفة مع بقائها مقبولة، أو تبدو مقنعة رغم مخالفتها قاعدة مهمة في المنتج. لذلك، تحتاج مجموعة الاختبارات إلى assertions مختارة بعناية بدل الاعتماد على فقرة مطابقة واحدة بوصفها إجابة «مرجعية».

تحدد الأبحاث المتعلقة بتراجع الأداء في التطبيقات التي تعتمد على APIs متطورة لـ LLM أن التراجع يمثل تحديًا هندسيًا حقيقيًا. كما توضح الأبحاث الخاصة بتوليد الاختبارات المعتمدة على LLM كيف يمكن للاختبارات المولدة دعم اختبار التراجع مع تغير البرمجيات. لكن التوليد ليس بديلًا عن المراجعة. وتسلط الدراسة المسحية لهندسة البرمجيات الواردة في السياق المرفق الضوء على مشكلة مرجع الاختبار: فقد يشفّر اختبار ينجح في الإصدار الحالي السلوك المتوقع بطريقة خاطئة. كما تشير إلى أن الاختبارات المولدة ليست مضمونة القابلية للتنفيذ. تعامل مع حالات الاختبار باعتبارها متطلبات منتج خضعت للمراجعة، لا مخرجات لا تقبل الشك من نظام آلي.

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

المتطلبات الأساسية وإعداد المشروع

ستحتاج إلى بيئة تشغيل JavaScript تدعم ES modules وnpm. تنشئ الأوامر التالية مشروعًا مصغرًا. لا يجري هذا الإطار أي طلبات عبر الشبكة ولا يتطلب مفاتيح API، ما يتيح لك التحقق من أداة التقييم قبل إدخال تكاليف API أو بيانات الاعتماد أو السلوكيات الخاصة بمزود الخدمة.

mkdir llm-regression-runner
cd llm-regression-runner
npm init -y
npm pkg set type=module
npm pkg set scripts.test="node --test"
npm pkg set scripts.eval="node src/run.js"
mkdir -p src test fixtures reports

أنشئ ملف .gitignore حتى لا تُضمَّن الاعتماديات والتقارير المولدة في المستودع عن طريق الخطأ. قد تحتوي التقارير على prompts الاختبار واستجابات النموذج؛ لذلك راجع محتواها ومتطلبات مؤسستك المتعلقة بالتعامل معها قبل تخزينها أو مشاركتها.

node_modules/
reports/*.json

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

الخطوة ١: تحديد الحالات التي خضعت للمراجعة والاستجابات المحفوظة

أنشئ الملف src/cases.js. يتضمن كل اختبار معرّفًا ثابتًا ووصفًا يساعد في تفسير حالات الفشل وفحوصًا واضحة. توضح الحالات النموذجية التالية حقائق تتعلق بالسياسات، ولغة محظورة، وطول الاستجابة، وعقدًا صغيرًا بصيغة JSON. استبدل السياسة الافتراضية الوهمية بقواعد راجعها مالكو التطبيق. أما أجزاء النص المتوقعة فهي أمثلة وليست إرشادات قانونية أو إرشادات لخدمة العملاء.

export const testCases = [
  {
    id: "refund-window",
    description: "Includes the reviewed refund window and offers support.",
    checks: {
      requiredSubstrings: ["30 days", "support"],
      forbiddenSubstrings: ["guaranteed refund"],
      maxCharacters: 900
    }
  },
  {
    id: "account-security",
    description: "Directs the user to a password reset and does not request a password.",
    checks: {
      requiredSubstrings: ["password reset"],
      forbiddenSubstrings: ["send me your password", "share your password"],
      maxCharacters: 700
    }
  },
  {
    id: "shipping-json",
    description: "Returns the expected fields and a numeric delivery estimate.",
    checks: {
      requiredJsonKeys: [
        "status",
        "estimated_delivery_days",
        "customer_message"
      ],
      maxCharacters: 600
    }
  }
];

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

أنشئ الملف fixtures/responses.json مع استجابات مسجلة لكل حالة. هذه البيانات أمثلة لاختبار الإطار، وليست ادعاءات بشأن ما سيعيده نموذج معين. وفي سير العمل المدعوم بـ API، خزّن هنا مخرجات نموذجية خضعت للمراجعة والموافقة، أو حمّلها من مصدر مضبوط لبيانات الاختبار.

{
  "refund-window": "The stated refund window is 30 days from delivery. Please contact support to discuss your situation.",
  "account-security": "Do not share your password. Use the password reset flow to regain access to your account.",
  "shipping-json": "{"status":"shipped","estimated_delivery_days":3,"customer_message":"Your order is on the way."}"
}

الخطوة ٢: تنفيذ فحوص حتمية للاستجابات

أنشئ الملف src/evaluate.js. أداة التقييم دالة محضة: تستقبل حالة واختبارًا ونص الاستجابة، ثم تعيد ما إذا كانت الفحوص قد نجحت وقائمة بأسباب الفشل. ولا تستدعي نموذجًا أو تعدّل ملفات أو تفترض شيئًا بشأن صيغ استجابات مزود الخدمة. ويسهّل هذا الفصل اختبار حالات الفشل بسرعة وتشخيصها.

function parseJsonObject(text) {
  try {
    const value = JSON.parse(text);
    if (value === null || Array.isArray(value) || typeof value !== "object") {
      return null;
    }
    return value;
  } catch {
    return null;
  }
}

export function evaluateResponse(testCase, text) {
  if (typeof text !== "string") {
    throw new TypeError("Response text must be a string.");
  }

  const failures = [];
  const checks = testCase.checks ?? {};
  const normalized = text.toLocaleLowerCase("en-US");

  for (const phrase of checks.requiredSubstrings ?? []) {
    if (!normalized.includes(phrase.toLocaleLowerCase("en-US"))) {
      failures.push(`Missing required text: ${JSON.stringify(phrase)}`);
    }
  }

  for (const phrase of checks.forbiddenSubstrings ?? []) {
    if (normalized.includes(phrase.toLocaleLowerCase("en-US"))) {
      failures.push(`Contains forbidden text: ${JSON.stringify(phrase)}`);
    }
  }

  if (Number.isInteger(checks.maxCharacters) && text.length > checks.maxCharacters) {
    failures.push(
      `Response has ${text.length} characters; maximum is ${checks.maxCharacters}.`
    );
  }

  if (Array.isArray(checks.requiredJsonKeys) && checks.requiredJsonKeys.length > 0) {
    const parsed = parseJsonObject(text);

    if (parsed === null) {
      failures.push("Response is not a valid JSON object.");
    } else {
      const actualKeys = Object.keys(parsed).sort();
      const expectedKeys = [...checks.requiredJsonKeys].sort();

      for (const key of expectedKeys) {
        if (!Object.hasOwn(parsed, key)) {
          failures.push(`JSON response is missing required key: ${JSON.stringify(key)}`);
        }
      }

      if (
        actualKeys.length !== expectedKeys.length ||
        actualKeys.some((key, index) => key !== expectedKeys[index])
      ) {
        failures.push(`JSON keys must be exactly: ${expectedKeys.join(", ")}.`);
      }

      if (
        Object.hasOwn(parsed, "estimated_delivery_days") &&
        typeof parsed.estimated_delivery_days !== "number"
      ) {
        failures.push('JSON key "estimated_delivery_days" must contain a number.');
      }
    }
  }

  return { passed: failures.length === 0, failures };
}

يقبل فحص JSON هذا صيغة JSON خامًا فقط، وليس كتلة Markdown البرمجية. وهذا الاختيار مقصود للبيانات القابلة للقراءة آليًا، إذ يمنع اعتبار النص المزخرف حمولة صالحة. ويتطلب فحص المفاتيح وجود المفاتيح المدرجة بالضبط، وهو مناسب فقط عندما يكون العقد دقيقًا فعلًا. وإذا كانت الخصائص الإضافية مسموحة، فعدّل الفحص ليتطلب مجموعة فرعية بدلًا من التطابق التام. وبالنسبة إلى البيانات الحساسة للأعمال، تحقّق من كل حقل والقيم المسموح بها باستخدام schema مصمم لهذا التطبيق.

الخطوة ٣: إضافة أداة تشغيل عبر سطر الأوامر وتقرير JSON

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

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { testCases } from "./cases.js";
import { evaluateResponse } from "./evaluate.js";

async function main() {
  const fixtureText = await readFile("fixtures/responses.json", "utf8");
  const fixtures = JSON.parse(fixtureText);
  const results = [];

  for (const testCase of testCases) {
    const responseText = fixtures[testCase.id];

    if (typeof responseText !== "string") {
      results.push({
        testCaseId: testCase.id,
        description: testCase.description,
        passed: false,
        failures: ["No string response fixture was found for this test case."]
      });
      continue;
    }

    const evaluation = evaluateResponse(testCase, responseText);
    results.push({
      testCaseId: testCase.id,
      description: testCase.description,
      passed: evaluation.passed,
      failures: evaluation.failures
    });
  }

  for (const result of results) {
    console.log(`[${result.passed ? "PASS" : "FAIL"}] ${result.testCaseId}`);
    for (const failure of result.failures) {
      console.log(`  - ${failure}`);
    }
  }

  const passed = results.filter((result) => result.passed).length;
  const report = {
    generatedAt: new Date().toISOString(),
    total: results.length,
    passed,
    failed: results.length - passed,
    results
  };

  await mkdir("reports", { recursive: true });
  const filename = `report-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
  const reportPath = join("reports", filename);
  await writeFile(reportPath, `${JSON.stringify(report, null, 2)}n`, "utf8");

  console.log(`Completed ${report.total} evaluations: ${report.passed} passed, ${report.failed} failed.`);
  console.log(`Report written to ${reportPath}`);

  if (report.failed > 0) {
    process.exitCode = 1;
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack : String(error));
  process.exitCode = 1;
});

يحتوي التقرير على معرّفات الاختبارات وإخفاقات assertions، لكن هذا المثال المصغر لا يتضمن نص الاستجابة. ويقلل ذلك افتراضيًا احتمال نسخ مخرجات حساسة إلى أحد artifacts الخاصة بالبناء. وإذا احتجت إلى نص الاستجابة لتصحيح الأخطاء، فأضِفه عمدًا وتحكم في الوصول إليه والاحتفاظ به وإخفاء البيانات الحساسة منه. يظل تقرير الاختبار artifact هندسيًا للبيانات، وينبغي التعامل معه وفقًا لذلك.

الخطوة ٤: اختبار أداة التقييم من دون استدعاءات API

أنشئ الملف test/evaluate.test.js. تختبر هذه الحالات منطق assertions مباشرة، وتعمل من دون بيانات اعتماد للمزود أو اتصال بالشبكة. وهي تعرض استجابة ناجحة ونوعين مختلفين من حالات الفشل.

import test from "node:test";
import assert from "node:assert/strict";
import { evaluateResponse } from "../src/evaluate.js";

test("passes when required and prohibited-text checks are satisfied", () => {
  const result = evaluateResponse(
    {
      checks: {
        requiredSubstrings: ["30 days", "support"],
        forbiddenSubstrings: ["guaranteed refund"],
        maxCharacters: 300
      }
    },
    "The policy states 30 days. Please contact support for help."
  );

  assert.deepEqual(result, { passed: true, failures: [] });
});

test("reports missing required text and a prohibited phrase", () => {
  const result = evaluateResponse(
    {
      checks: {
        requiredSubstrings: ["password reset"],
        forbiddenSubstrings: ["share your password"]
      }
    },
    "Please share your password with support."
  );

  assert.equal(result.passed, false);
  assert.equal(result.failures.length, 2);
});

test("rejects invalid JSON and detects wrong JSON field types", () => {
  const checks = {
    requiredJsonKeys: ["status", "estimated_delivery_days", "customer_message"]
  };

  const invalidJson = evaluateResponse({ checks }, "not JSON");
  assert.equal(invalidJson.passed, false);
  assert.ok(invalidJson.failures.includes("Response is not a valid JSON object."));

  const wrongType = evaluateResponse(
    { checks },
    JSON.stringify({
      status: "shipped",
      estimated_delivery_days: "three",
      customer_message: "On the way"
    })
  );
  assert.equal(wrongType.passed, false);
  assert.ok(
    wrongType.failures.includes(
      'JSON key "estimated_delivery_days" must contain a number.'
    )
  );
});

شغّل الفحوص المحلية:

npm test
npm run eval

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

الخطوة ٥: ربط نموذج مباشر بعد التحقق من API الخاص به

تتمثل الخطوة التالية في إنشاء موصل لمزود الخدمة يستقبل prompt الحالة ويعيد نص الاستجابة. لا يقدم هذا الشرح موصلًا من هذا النوع عمدًا. فالسياق الموثوق لا يحدد أسماء أساليب API الحالية لدى OpenAI أو Anthropic، أو إصدارات SDK، أو معرّفات النماذج المدعومة، أو حقول الطلب، أو صيغ الاستجابة. وسيؤدي نسخ شيفرة تكامل غير موثوقة إلى الشرح إلى ادعاء مضلل بالتوافق.

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

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

تصميم مجموعة تراجع مفيدة

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

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

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

القيود والخطوات التالية

  • يقيّم هذا المثال بيانات تجريبية محفوظة، ولا يجري استدعاءات إلى OpenAI أو Anthropic.
  • لا تستطيع فحوص الأجزاء النصية وJSON الأساسية إثبات الصحة الواقعية أو الجودة الشاملة.
  • تغطي مجموعة الاختبارات الناجحة السيناريوهات الممثلة في حالاتها فقط، ولا تضمن السلوك المستقبلي.
  • تحتاج الاختبارات المولدة إلى مراجعة. وتشير الأبحاث المرفقة إلى أن الاختبارات المولدة قد لا تكون قابلة للتنفيذ، وتناقش الصعوبة الأوسع في تحديد ما إذا كانت النتيجة المتوقعة للاختبار صحيحة.

تشمل الإضافات المفيدة schemas أكثر صرامة ومخصصة للتطبيق، وحالات تراجع خضعت للمراجعة واستُمدت من الحوادث، وتشغيلات مضبوطة مدعومة بـ API، وتنسيقًا للتقارير يلتقط البيانات الوصفية التي يحتاج إليها فريقك فقط. أضف التزامن أو إعادة المحاولة أو احتساب التكلفة أو بوابات زمن الاستجابة بعد تحديد متطلباتها والتحقق منها لمزودي الخدمة المختارين وبيئة النشر لديك.

المصادر والسياق البحثي

النسبة التحريرية: فريقا تحرير وهندسة بوابة الذكاء الاصطناعي، GateOfAI، LLC.

Share:
تدريب النماذج السيادية المخصصة استضافة وتدريب محلي آمن

هل تبحث عن ضبط دقيق أو استضافة آمنة لنماذجك اللغوية؟

احصل على نماذج مفتوحة المصدر (Llama, DeepSeek, Mistral) معدّلة ومدرّبة خصيصاً على بيانات شركتك ومنشورة على خوادمك السحابية الخاصة بأعلى معايير الأمان والامتثال.

اطرح مشروعك واحصل على عروض مخصصة
تبحث عن حلول أتمتة ووكلاء جاهزين للتشغيل فوراً؟ استكشف حلول الأتمتة الجاهزة ←
حماية اتفاقية عدم الإفصاح (NDA)
عروض معتمدة خلال 24-48 ساعة
استشارة وتحديد نطاق مجاناً (0$ مقدماً)

هل كان هذا الشرح مفيداً؟

هل لديك سؤال حول هذا الشرح؟

مساعدنا الذكي قرأ هذا الشرح وهو جاهز للإجابة على استفساراتك المعمارية والبرمجية فوراً.

🤖
مرحباً بك! يمكنك سؤالي عن أي كود أو مفهوم تقني ورد في هذا الشرح.
مساعد الذكاء الاصطناعي متاح مجاناً للمهندسين والمشتركين.
تسجيل دخول مجاني لبدء المحادثة
🛡️ سوق حلول الإنتاج المؤسسية (0% عمولة رموز BYOK)

أنظمة وأكواد جاهزة للتشغيل مرتبطة بهذا الموضوع

تصفح سوق الحلول بالكامل →
أدوات المطورين • BYOK 🛡️ من $14/شهرياً

معماري أوامر المطورين والوكلاء (BYOK)

بيئة هندسية لبناء أوامر النظام (System Prompts)، قوالب LangGraph و CrewAI، مع دعم مفاتيح BYOK الخاصة بك.

مشروع مخصص • ضمان Escrow 🛡️ عرض سعر مخصص

بناء أنظمة ذكاء اصطناعي مخصصة (Escrow)

تعاقد مع مهندسي ذكاء اصطناعي معتمدين لبناء أو تخصيص أنظمة RAG ووكلاء بمفاتيح BYOK تحت حماية الضمان المالي.

Custom Chatbots • BYOK $1,187

SkillForge AI – AI-Powered Learning Management System

An AI-powered learning platform for discovering courses, generating learning content, managing students and instructors, and delivering a complete…

مرشد بوابة الذكاء الاصطناعي
نشط للخدمة
مرحباً بك في بوابة الذكاء الاصطناعي! أنا مرشدك هنا لمساعدتك في فهم خدمات المنصة، باقات التشغيل وخطط الضمان المالي. كيف يمكنني إرشادك اليوم؟