نشر الإنتاج والمراقبة

اختبار A/B للحواجز

3 دقيقة للقراءة

يساعد اختبار A/B لتكوينات الحواجز في تحسين التوازن بين السلامة وتجربة المستخدم. يغطي هذا الدرس تنفيذ التجارب المضبوطة لتغييرات الحواجز.

لماذا اختبار A/B للحواجز؟

  • تحسين العتبات: إيجاد عتبات السمية المثلى
  • تأثير التأخير: قياس تأخير المصنف الجديد في الإنتاج
  • تقليل الإيجابيات الخاطئة: اختبار القواعد المخففة على شرائح حركة آمنة
  • طرح نموذج جديد: نشر تدريجي لمصنفات سلامة جديدة

إطار التجربة

from dataclasses import dataclass
from typing import Optional, Callable
import hashlib
import random

@dataclass
class Experiment:
    """تكوين تجربة اختبار A/B."""
    name: str
    control_weight: float = 0.5  # % حركة للتحكم
    treatment_config: dict = None
    control_config: dict = None
    user_sticky: bool = True  # نفس المستخدم يحصل دائماً على نفس المتغير

class ExperimentManager:
    """إدارة تجارب A/B للحواجز."""

    def __init__(self):
        self.experiments: dict[str, Experiment] = {}

    def register_experiment(self, experiment: Experiment):
        self.experiments[experiment.name] = experiment

    def get_variant(
        self,
        experiment_name: str,
        user_id: str = None,
        request_id: str = None
    ) -> tuple[str, dict]:
        """
        الحصول على متغير التجربة للطلب.

        يُرجع:
            زوج من (variant_name، config)
        """
        experiment = self.experiments.get(experiment_name)
        if not experiment:
            return ("control", {})

        # تحديد التخصيص
        if experiment.user_sticky and user_id:
            # تجزئة user_id للتخصيص المتسق
            hash_input = f"{experiment_name}:{user_id}"
            hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
            assignment = (hash_value % 100) / 100
        else:
            assignment = random.random()

        if assignment < experiment.control_weight:
            return ("control", experiment.control_config or {})
        else:
            return ("treatment", experiment.treatment_config or {})

# الاستخدام
manager = ExperimentManager()

# تسجيل التجربة
manager.register_experiment(Experiment(
    name="toxicity_threshold_test",
    control_weight=0.5,
    control_config={"threshold": 0.8},
    treatment_config={"threshold": 0.6}
))

# الحصول على المتغير
variant, config = manager.get_variant(
    "toxicity_threshold_test",
    user_id="user_123"
)

مغلف الحاجز التجريبي

from typing import Dict, Any
import time

class ExperimentalGuardrail:
    """حاجز مع دعم اختبار A/B."""

    def __init__(
        self,
        base_guardrail: callable,
        experiment_manager: ExperimentManager,
        experiment_name: str
    ):
        self.base_guardrail = base_guardrail
        self.experiment_manager = experiment_manager
        self.experiment_name = experiment_name

    async def check(
        self,
        content: str,
        user_id: str = None,
        request_id: str = None
    ) -> Dict[str, Any]:
        """تشغيل الحاجز مع تكوين التجربة."""
        # الحصول على متغير التجربة
        variant, config = self.experiment_manager.get_variant(
            self.experiment_name,
            user_id=user_id,
            request_id=request_id
        )

        start_time = time.time()

        # تشغيل الحاجز مع تكوين المتغير
        result = await self.base_guardrail(content, **config)

        latency_ms = (time.time() - start_time) * 1000

        # تسجيل بيانات التجربة
        await self._log_experiment_result(
            variant=variant,
            config=config,
            result=result,
            latency_ms=latency_ms,
            user_id=user_id,
            request_id=request_id
        )

        return {
            **result,
            "experiment_variant": variant,
            "experiment_name": self.experiment_name
        }

    async def _log_experiment_result(
        self,
        variant: str,
        config: dict,
        result: dict,
        latency_ms: float,
        user_id: str,
        request_id: str
    ):
        """تسجيل نتيجة التجربة للتحليل."""
        from structlog import get_logger
        logger = get_logger()

        logger.info(
            "experiment_result",
            experiment_name=self.experiment_name,
            variant=variant,
            config=config,
            blocked=result.get("blocked", False),
            categories=result.get("categories", []),
            confidence=result.get("confidence"),
            latency_ms=latency_ms,
            user_id=user_id,
            request_id=request_id
        )

التحليل الإحصائي

from scipy import stats
import numpy as np
from dataclasses import dataclass

@dataclass
class ExperimentResults:
    control_blocked: int
    control_total: int
    treatment_blocked: int
    treatment_total: int
    control_latency: list[float]
    treatment_latency: list[float]

class ExperimentAnalyzer:
    """تحليل نتائج اختبار A/B."""

    def analyze(self, results: ExperimentResults) -> dict:
        """إجراء تحليل إحصائي على نتائج التجربة."""
        # مقارنة معدل الحظر
        control_rate = results.control_blocked / results.control_total
        treatment_rate = results.treatment_blocked / results.treatment_total

        # اختبار مربع كاي لمعدلات الحظر
        contingency = [
            [results.control_blocked, results.control_total - results.control_blocked],
            [results.treatment_blocked, results.treatment_total - results.treatment_blocked]
        ]
        chi2, p_value_blocks, _, _ = stats.chi2_contingency(contingency)

        # اختبار T للتأخير
        t_stat, p_value_latency = stats.ttest_ind(
            results.control_latency,
            results.treatment_latency
        )

        # حجم الأثر (Cohen's d للتأخير)
        cohens_d = self._cohens_d(
            results.control_latency,
            results.treatment_latency
        )

        return {
            "block_rate": {
                "control": control_rate,
                "treatment": treatment_rate,
                "relative_change": (treatment_rate - control_rate) / control_rate * 100,
                "p_value": p_value_blocks,
                "significant": p_value_blocks < 0.05
            },
            "latency": {
                "control_mean": np.mean(results.control_latency),
                "treatment_mean": np.mean(results.treatment_latency),
                "control_p99": np.percentile(results.control_latency, 99),
                "treatment_p99": np.percentile(results.treatment_latency, 99),
                "p_value": p_value_latency,
                "cohens_d": cohens_d,
                "significant": p_value_latency < 0.05
            },
            "recommendation": self._get_recommendation(
                control_rate, treatment_rate,
                np.mean(results.control_latency),
                np.mean(results.treatment_latency),
                p_value_blocks
            )
        }

    def _cohens_d(self, group1: list, group2: list) -> float:
        """حساب حجم أثر Cohen's d."""
        n1, n2 = len(group1), len(group2)
        var1, var2 = np.var(group1), np.var(group2)
        pooled_std = np.sqrt(((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2))
        return (np.mean(group1) - np.mean(group2)) / pooled_std

    def _get_recommendation(
        self,
        control_rate: float,
        treatment_rate: float,
        control_latency: float,
        treatment_latency: float,
        p_value: float
    ) -> str:
        """توليد توصية التجربة."""
        if p_value >= 0.05:
            return "لا يوجد فرق كبير. استمر في التجربة أو حافظ على التحكم."

        rate_change = (treatment_rate - control_rate) / control_rate * 100
        latency_change = (treatment_latency - control_latency) / control_latency * 100

        if rate_change < -10 and latency_change < 10:
            return "انشر: العلاج يقلل الحظر دون تأثير على التأخير."
        elif rate_change > 20:
            return "ارفض: العلاج يحظر محتوى كثير جداً."
        elif latency_change > 20:
            return "ارفض: العلاج يضيف تأخيراً كثيراً."
        else:
            return "تحقق: نتائج مختلطة، راجع البيانات النوعية."

الطرح التدريجي

from datetime import datetime, timedelta
from typing import Callable

class GradualRollout:
    """طرح تغييرات الحواجز تدريجياً."""

    def __init__(
        self,
        start_date: datetime,
        end_date: datetime,
        start_percentage: float = 5.0,
        end_percentage: float = 100.0
    ):
        self.start_date = start_date
        self.end_date = end_date
        self.start_percentage = start_percentage
        self.end_percentage = end_percentage

    def get_rollout_percentage(self) -> float:
        """الحصول على نسبة الطرح الحالية."""
        now = datetime.now()

        if now < self.start_date:
            return 0.0
        if now > self.end_date:
            return self.end_percentage

        total_duration = (self.end_date - self.start_date).total_seconds()
        elapsed = (now - self.start_date).total_seconds()
        progress = elapsed / total_duration

        return self.start_percentage + (
            self.end_percentage - self.start_percentage
        ) * progress

    def should_use_new_guardrail(self, user_id: str) -> bool:
        """تحديد إذا كان المستخدم يجب أن يحصل على الحاجز الجديد."""
        percentage = self.get_rollout_percentage()

        # تجزئة متسقة للمستخدم
        hash_value = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
        user_bucket = hash_value % 100

        return user_bucket < percentage

# الاستخدام
rollout = GradualRollout(
    start_date=datetime(2026, 1, 15),
    end_date=datetime(2026, 1, 22),
    start_percentage=5.0,
    end_percentage=100.0
)

# التحقق إذا كان المستخدم يجب أن يحصل على الحاجز الجديد
if rollout.should_use_new_guardrail(user_id="user_123"):
    result = await new_guardrail.check(content)
else:
    result = await old_guardrail.check(content)

نصيحة اختبار A/B: دائماً لديك خطة تراجع. إذا أظهر العلاج سلامة أسوأ بشكل ملحوظ (سلبيات خاطئة أعلى)، أوقف التجربة فوراً وارجع للتحكم.

التالي: التسجيل والتدقيق والامتثال. :::

اختبار

الوحدة 6: نشر الإنتاج والمراقبة

خذ الاختبار
نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.