لارافيل, Others / 2026-08-18

بناء CRM يفكر معك: كيف تجعل Laravel يحلل العملاء تلقائيًا بالذكاء الاصطناعي؟

بناء CRM يفكر معك: كيف تجعل Laravel يحلل العملاء تلقائيًا بالذكاء الاصطناعي؟

2026-08-18 وقت القراءه : 14 دقائق

في كل شركة تعتمد على الـ Leads يوجد رقم لا يظهر في أي تقرير: عدد العملاء الذين كانوا جاهزين للشراء، ووصلوا إلى قاعدة البيانات فعلًا، لكن لم يتصل بهم أحد في الوقت المناسب. هؤلاء لا يظهرون كخسارة، بل يظهرون كصفوف عادية في جدول leads بحالة new.

المشكلة هنا ليست في التخزين. أي CRM يخزّن الـ Leads بكفاءة. المشكلة في التكلفة المزدوجة التي تنشأ بعد التخزين:

  • تكلفة التأخير: تُظهر أبحاث Lead Response Management أن احتمالية تأهيل العميل تنخفض بشكل حاد كلما طال زمن الاستجابة الأولى، والفارق بين الرد خلال دقائق والرد بعد ساعة ليس فارقًا هامشيًا بل فارق بمرتبة كاملة.
  • تكلفة سوء الترتيب: عندما يعمل الفريق بترتيب الأحدث أولًا، فإن الـ Lead الأعلى نية شرائية يجلس في الطابور خلف عشرات الاستفسارات العامة، لمجرد أنه وصل قبلهم بساعتين.

مع 20 Lead يوميًا يستطيع موظف المبيعات قراءة كل رسالة والحكم بنفسه. مع 500 Lead يوميًا من الموقع والتطبيق وWhatsApp والحملات الإعلانية وصفحات الهبوط، لم يعد السؤال «كيف نخزّن هذه البيانات؟» بل:

بمن أتصل الآن، ولماذا هو تحديدًا قبل غيره؟

هذا المقال يجيب على هذا السؤال عمليًا: نبني طبقة تحليل ذكية فوق CRM قائم باستخدام Laravel AI SDK، بحيث يصل الـ Lead فيُقيَّم ويُصنَّف ويُرفق بإجراء مقترح خلال ثوانٍ من إنشائه — دون إعادة بناء النظام، ودون أن ينتظر المستخدم النهائي.


ما الذي يميّز هذا الدليل

معظم الأمثلة المتاحة تتوقف عند «أرسل الرسالة إلى النموذج واحفظ الدرجة». هذا يعمل في العرض التوضيحي ويفشل في الإنتاج. لذلك يغطي هذا المقال إضافة إلى الجزء الأساسي:

  • منع إعادة التحليل المكرر (Idempotency) وضبط التزامن عبر الطوابير.
  • حماية البيانات: ما الذي يُرسل إلى مزوّد الذكاء الاصطناعي وما الذي لا يُرسل أبدًا.
  • ضبط التكلفة عبر تحليل على مرحلتين (Cascade) بدل استدعاء نموذج قوي لكل Lead.
  • الموثوقية: إعادة المحاولة، الـ Failover بين المزوّدين، والمهلات الزمنية.
  • التتبّع والتدقيق (Auditing) وتوثيق نسخة الـ Prompt المسؤولة عن كل درجة.
  • الاختبار الآلي، وقياس ما إذا كان النظام يعمل فعلًا بعد الإطلاق.


المتطلبات المسبقة

  • تطبيق Laravel يحتوي جدول leads ونموذج Lead.
  • حزمة الذكاء الاصطناعي الرسمية: composer require laravel/ai، مع نشر ملفات الإعداد والهجرات.
  • مفتاح مزوّد واحد على الأقل في .env (مثل ANTHROPIC_API_KEY أو OPENAI_API_KEY).
  • Queue Worker يعمل فعليًا — هذه ليست تفصيلة اختيارية، بل شرط أساسي في التصميم الذي سنتبعه.


أين يفيد الذكاء الاصطناعي هنا — وأين لا يفيد

قبل كتابة أي كود، من المهم ضبط التوقعات، لأن أغلب مشاريع Lead Scoring الفاشلة تفشل بسبب توقع خاطئ لا بسبب كود خاطئ.

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

ما لا يجيده: اختراع إشارة غير موجودة. إذا كان الـ Lead لا يحتوي سوى اسم ورقم هاتف ومصدر Facebook، فإن أي درجة يعطيها النموذج فوق المتوسط هي تخمين مغلّف برقم يبدو دقيقًا. القاعدة الحاكمة:

جودة التحليل محكومة بسقف جودة الإشارات المتاحة، لا بذكاء النموذج. لا يمكن استخراج Signal غير موجود أصلًا في البيانات.

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

المعمارية العامة

التدفق الذي سنبنيه يفصل تمامًا بين استقبال الـ Lead وتحليله:

Lead Sources (Web · App · Ads · WhatsApp)
        ↓
   POST /api/leads
        ↓
   Lead::create()            → استجابة فورية للعميل
        ↓
   LeadObserver (afterCommit)
        ↓
   AnalyzeLead Job  ──────→ Queue: ai
        ↓
   LeadSnapshot (تصفية الحقول)
        ↓
   LeadAnalyzer Agent  ──→ Provider (+ Failover)
        ↓
   Structured Output (JSON مُتحقَّق منه)
        ↓
   Hybrid Score + Business Rules
        ↓
   CRM Updated → Sales Queue مُرتَّبة


الخطوة 1: نمذجة البيانات

الخطأ الشائع هنا هو إضافة عمود ai_score وحده. عمود واحد يعني أنك بعد شهرين ستنظر إلى درجة 87 دون أن تعرف: أي نموذج أنتجها؟ بأي نسخة من الـ Prompt؟ وعلى أي بيانات؟

php artisan make:migration add_ai_analysis_to_leads_table
Schema::table('leads', function (Blueprint $table) {
    // نتيجة التحليل
    $table->unsignedTinyInteger('ai_score')->nullable();
    $table->string('ai_classification', 20)->nullable();
    $table->string('ai_intent', 20)->nullable();
    $table->string('ai_recommended_action', 40)->nullable();
    $table->text('ai_summary')->nullable();
    $table->text('ai_reason')->nullable();
    $table->json('ai_objections')->nullable();

    // جودة التحليل
    $table->unsignedTinyInteger('ai_confidence')->nullable();
    $table->string('ai_data_sufficiency', 20)->nullable();

    // الدرجة النهائية بعد دمج القواعد الحتمية
    $table->unsignedTinyInteger('final_score')->nullable();

    // التدقيق وإمكانية التتبع
    $table->string('ai_provider', 40)->nullable();
    $table->string('ai_model', 80)->nullable();
    $table->string('ai_prompt_version', 40)->nullable();
    $table->char('ai_input_hash', 64)->nullable();
    $table->timestamp('ai_analyzed_at')->nullable();

    $table->index(['final_score', 'created_at']);
    $table->index('ai_classification');
});

ثلاثة أعمدة تستحق التوقف عندها:

  • ai_prompt_version: يسمح لاحقًا بالإجابة على سؤال «أي نسخة من نظام التحليل أعطت هذا الـ Lead درجة 87؟» — وهو سؤال حتمي عندما يبدأ التحليل بالتأثير على قرارات تجارية.
  • ai_input_hash: بصمة المدخلات. إذا لم تتغير بيانات الـ Lead، فلا داعي لدفع تكلفة استدعاء جديد.
  • final_score: الدرجة التي يُرتَّب بها الطابور فعليًا، وهي ليست درجة النموذج وحدها كما سنرى في قسم الدمج الهجين.


استخدام Enums بدل السلاسل النصية الحرة

القيم المُصنَّفة تتكرر في الـ Schema وفي واجهة الـ CRM وفي منطق الأعمال. تعريفها مرة واحدة يمنع التباعد بينها:

enum LeadClassification: string
{
    case Hot = 'hot';
    case Warm = 'warm';
    case Cold = 'cold';
    case Unqualified = 'unqualified';

    public function slaMinutes(): ?int
    {
        return match ($this) {
            self::Hot => 15,
            self::Warm => 240,
            self::Cold => 1440,
            self::Unqualified => null,
        };
    }
}
// app/Models/Lead.php
protected function casts(): array
{
    return [
        'ai_classification' => LeadClassification::class,
        'ai_intent' => LeadIntent::class,
        'ai_recommended_action' => NextBestAction::class,
        'ai_objections' => 'array',
        'ai_analyzed_at' => 'datetime',
    ];
}


الخطوة 2: بناء حمولة التحليل (Payload)

قبل الوصول إلى الـ Agent، هناك قرار تصميمي أهم من الـ Prompt نفسه: ما الذي نرسله؟ الحل السهل والخاطئ هو:

// لا تفعل هذا
$response = (new LeadAnalyzer)->prompt($lead->toJson());

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

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

نُغلّف ذلك في كائن مستقل قابل للاختبار:

<?php

namespace App\Ai\Payloads;

use App\Models\Lead;
use Illuminate\Support\Str;

final readonly class LeadSnapshot
{
    public function __construct(private Lead $lead) {}

    public function toArray(): array
    {
        return [
            'source' => $this->lead->source,
            'campaign' => $this->lead->utm_campaign,
            'product_of_interest' => $this->lead->product?->name,
            'message' => Str::limit((string) $this->lead->message, 1500),

            'behavior' => [
                'website_visits' => (int) $this->lead->website_visits,
                'pricing_page_visits' => (int) $this->lead->pricing_page_visits,
                'brochure_downloaded' => (bool) $this->lead->brochure_downloaded,
                'demo_requested' => (bool) $this->lead->demo_requested,
                'days_since_first_touch' => $this->lead->first_touch_at?->diffInDays(now()),
            ],

            'history' => [
                'previous_leads' => $this->lead->previous_leads_count,
                'is_existing_customer' => (bool) $this->lead->is_existing_customer,
                'recent_interactions' => $this->lead->interactions()
                    ->latest()
                    ->limit(10)
                    ->pluck('summary')
                    ->all(),
            ],
        ];
    }

    public function toJson(): string
    {
        return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
    }

    public function hash(): string
    {
        return hash('sha256', $this->toJson());
    }
}

لاحظ JSON_UNESCAPED_UNICODE: بدونه سيتحول النص العربي إلى تسلسلات \uXXXX، ما يستهلك عددًا أكبر من الـ Tokens دون فائدة.

لاحظ أيضًا غياب الاسم والهاتف والبريد. الـ Agent لا يحتاجها ليقرر أولوية التواصل، والـ CRM يعرفها أصلًا.


الخطوة 3: بناء الـ Agent

php artisan make:agent LeadAnalyzer --structured

الفكرة الجوهرية: هذا ليس Chatbot. وظيفته محددة ومغلقة — يستقبل لقطة بيانات ويعيد تحليلًا منظمًا. وهذا الفارق ينعكس مباشرة على الإعدادات: حرارة منخفضة لأن المطلوب اتساق التصنيف لا إبداع الصياغة.

<?php

namespace App\Ai\Agents;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Attributes\MaxTokens;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Attributes\Temperature;
use Laravel\Ai\Attributes\Timeout;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;
use Stringable;

#[Provider(Lab::Anthropic)]
#[Model('claude-sonnet-5')]
#[Temperature(0.1)]
#[MaxTokens(1500)]
#[Timeout(45)]
class LeadAnalyzer implements Agent, HasStructuredOutput
{
    use Promptable;

    public const VERSION = 'lead-analyzer@2026-08';

    public function instructions(): Stringable|string
    {
        return <<<'PROMPT'
        You are a CRM lead scoring analyst for a B2C sales team.

        Analyze the supplied lead snapshot and return a structured evaluation.

        SCORING FRAMEWORK (total 100):
        - Purchase Intent .... 0-30  explicit buying signals in the message
        - Engagement ......... 0-20  visits, downloads, repeat interactions
        - Product Fit ........ 0-20  match between stated need and product
        - Urgency ............ 0-15  time pressure or deadlines expressed
        - Contact Quality .... 0-15  completeness and specificity of the data

        CLASSIFICATION BANDS:
        80-100 = hot | 50-79 = warm | 20-49 = cold | 0-19 = unqualified

        HARD RULES:
        - Use ONLY the supplied data. Never infer demographics, income,
          nationality, or any attribute that is not explicitly present.
        - If the snapshot lacks a message and shows no behavioral signals,
          set data_sufficiency to "insufficient", keep the score at or below
          40, and lower the confidence accordingly.
        - Absence of a signal is neutral evidence, not negative evidence.
        - Write "summary" and "reason" in the same language as the lead's
          message. Default to Arabic when no message is present.
        - "summary" must be at most three sentences and must not repeat
          data the CRM already displays.
        PROMPT;
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'lead_score' => $schema->integer()->min(0)->max(100)->required()
                ->description('Total score computed from the five framework dimensions.'),

            'classification' => $schema->string()
                ->enum(['hot', 'warm', 'cold', 'unqualified'])
                ->required(),

            'intent' => $schema->string()
                ->enum(['high', 'medium', 'low', 'unknown'])
                ->required(),

            'data_sufficiency' => $schema->string()
                ->enum(['sufficient', 'partial', 'insufficient'])
                ->required()
                ->description('How much of the score is evidence-based rather than inferred.'),

            'confidence' => $schema->integer()->min(0)->max(100)->required(),

            'summary' => $schema->string()->required(),

            'recommended_action' => $schema->string()
                ->enum([
                    'call_customer',
                    'send_whatsapp',
                    'send_information',
                    'schedule_demo',
                    'follow_up_later',
                    'no_action',
                ])
                ->required(),

            'reason' => $schema->string()->required()
                ->description('Why this action, referencing concrete signals from the snapshot.'),

            'objections' => $schema->array()->items(
                $schema->object(fn ($schema) => [
                    'type' => $schema->string()
                        ->enum(['price', 'payment_terms', 'timing', 'trust', 'features', 'other'])
                        ->required(),
                    'description' => $schema->string()->required(),
                    'suggested_response' => $schema->string()->required(),
                ])
            )->required(),
        ];
    }
}


لماذا Structured Output وليس «أرجع JSON من فضلك»

الفرق ليس تجميليًا. عند الاعتماد على تعليمات نصية، يبقى احتمال أن يعيد النموذج فقرة سردية، أو JSON داخل سياج Markdown، أو قيمة "Hot Lead 🔥" بدل hot — وكل حالة من هذه تكسر الكود في الإنتاج بعد أسبوعين من العمل السليم.

تعريف schema() ينقل هذا الالتزام إلى مستوى المزوّد نفسه: القيم المُعدّدة مقيّدة، والنطاقات الرقمية محروسة، والاستجابة تُستهلك مباشرة كمصفوفة:

$response = (new LeadAnalyzer)->prompt($snapshot->toJson());

$response['lead_score'];       // 87
$response['classification'];   // 'hot'
$response['objections'][0];    // ['type' => 'payment_terms', ...]

ملاحظة مهمة: الـ Structured Output ممتاز للتصنيف والاستخراج، لكنه يُضعف جودة النص السردي الحر. إن احتجت لاحقًا مساعدًا حواريًا لموظف المبيعات، اجعله Agent منفصلًا بلا Schema.


الخطوة 4: التنفيذ في الخلفية

لا يجوز أن ينتظر مُرسِل النموذج على صفحة الهبوط انتهاء استدعاء الذكاء الاصطناعي. المطلوب: إنشاء الـ Lead، إرجاع الاستجابة فورًا، ثم التحليل في الطابور.

الإطلاق من Observer لا من Controller

وضع dispatch داخل الـ Controller يعني نسيانه في كل مسار آخر ينشئ Leads — الاستيراد من ملف، Webhook إعلاني، أو أمر Artisan. الـ Observer يضمن التغطية الكاملة:

<?php

namespace App\Observers;

use App\Jobs\AnalyzeLead;
use App\Models\Lead;

class LeadObserver
{
    public function created(Lead $lead): void
    {
        AnalyzeLead::dispatch($lead->id)->afterCommit();
    }
}

afterCommit() ليست تفصيلة شكلية: بدونها قد يبدأ الـ Worker بمعالجة الـ Job قبل اكتمال المعاملة (Transaction)، فيبحث عن Lead غير موجود بعد.

الـ Job

<?php

namespace App\Jobs;

use App\Ai\Agents\LeadAnalyzer;
use App\Ai\Payloads\LeadSnapshot;
use App\Models\Lead;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Support\Facades\Log;
use Laravel\Ai\Enums\Lab;
use Throwable;

class AnalyzeLead implements ShouldQueue, ShouldBeUnique
{
    use Queueable;

    public int $tries = 3;
    public int $timeout = 90;
    public int $uniqueFor = 600;

    public function __construct(public int $leadId)
    {
        $this->onQueue('ai');
    }

    public function uniqueId(): string
    {
        return (string) $this->leadId;
    }

    public function backoff(): array
    {
        return [10, 60, 300];
    }

    public function middleware(): array
    {
        return [new RateLimited('ai-analysis')];
    }

    public function handle(): void
    {
        $lead = Lead::find($this->leadId);

        if (! $lead) {
            return;
        }

        $snapshot = new LeadSnapshot($lead);

        // لا تُعد تحليل بيانات لم تتغير
        if ($lead->ai_input_hash === $snapshot->hash()) {
            return;
        }

        $response = (new LeadAnalyzer)->prompt(
            $snapshot->toJson(),
            provider: [Lab::Anthropic, Lab::OpenAI],
        );

        $lead->forceFill([
            'ai_score' => $response['lead_score'],
            'ai_classification' => $response['classification'],
            'ai_intent' => $response['intent'],
            'ai_summary' => $response['summary'],
            'ai_recommended_action' => $response['recommended_action'],
            'ai_reason' => $response['reason'],
            'ai_objections' => $response['objections'],
            'ai_confidence' => $response['confidence'],
            'ai_data_sufficiency' => $response['data_sufficiency'],
            'ai_prompt_version' => LeadAnalyzer::VERSION,
            'ai_input_hash' => $snapshot->hash(),
            'ai_analyzed_at' => now(),
        ])->save();

        LeadScore::apply($lead);
    }

    public function failed(Throwable $e): void
    {
        Log::error('Lead analysis failed', [
            'lead_id' => $this->leadId,
            'exception' => $e->getMessage(),
        ]);
    }
}

أربعة قرارات تستحق الشرح:

  • ShouldBeUnique: يمنع تكدّس تحليلات متزامنة لنفس الـ Lead عند وصول عدة تحديثات متتالية.
  • فحص الـ hash: يجعل الـ Job قابلًا لإعادة التشغيل بأمان (Idempotent) ويقلّل الفاتورة.
  • RateLimited: يحمي حصة المزوّد عند وصول دفعة كبيرة من الـ Leads دفعة واحدة، ويُعرَّف في AppServiceProvider:
    RateLimiter::for('ai-analysis', fn () => Limit::perMinute(120));
  • provider: [Lab::Anthropic, Lab::OpenAI]: عند انقطاع أو تجاوز حد المعدل لدى المزوّد الأساسي، تنتقل المحاولة تلقائيًا إلى الثاني. لاحظ أن الـ Failover لا يُفعَّل إلا لأخطاء قابلة للتجاوز (Rate limit، تحميل زائد، رصيد غير كافٍ) ولا يُفعَّل لأخطاء الطلب نفسه.


الخطوة 5: التقييم الهجين — لا تسلّم الترتيب للنموذج وحده

بعض الإشارات لا تحتاج تفسيرًا لغويًا على الإطلاق. «طلب Demo» إشارة حتمية بقيمة معروفة، ولا معنى لدفع تكلفة نموذج لغوي كي يقرر وزنها ثم يقرره بشكل مختلف قليلًا في المرة التالية.

التصميم الأقوى في الإنتاج يفصل بين ما هو قابل للحساب وما يحتاج فهمًا دلاليًا:

توزيع المسؤوليات بين الطبقتين
الطبقةتتولىلماذا
قواعد حتميةطلب Demo، زيارات صفحة الأسعار، عميل حالي، قيمة الباقةثابتة، قابلة للتدقيق، بلا تكلفة ولا تباين
تحليل الذكاء الاصطناعينية الرسالة، الاعتراضات، الاستعجال، النبرة، التلخيصيتطلب فهم نص حر بلهجات مختلفة
<?php

namespace App\Services;

use App\Models\Lead;

final class LeadScore
{
    private const MAX_DETERMINISTIC = 45;

    public static function apply(Lead $lead): void
    {
        $lead->forceFill([
            'final_score' => self::compute($lead),
        ])->save();
    }

    private static function compute(Lead $lead): int
    {
        $rules = 0;
        $rules += $lead->demo_requested ? 20 : 0;
        $rules += $lead->pricing_page_visits >= 3 ? 15 : 0;
        $rules += $lead->is_existing_customer ? 10 : 0;

        $rulesNormalized = min($rules, self::MAX_DETERMINISTIC)
            / self::MAX_DETERMINISTIC * 100;

        // خفّض وزن التحليل عندما تكون البيانات غير كافية
        $aiWeight = $lead->ai_data_sufficiency === 'insufficient' ? 0.3 : 0.6;

        return (int) round(
            ($aiWeight * $lead->ai_score) + ((1 - $aiWeight) * $rulesNormalized)
        );
    }
}

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


الخطوة 6: ضبط التكلفة عبر التحليل المتدرّج

عند 500 Lead يوميًا، استدعاء نموذج قوي لكل Lead يعني دفع تكلفة كاملة على استفسارات لا تستحقها («كم السعر؟» بلا أي سلوك مسبق). الحل نمط Cascade:

  1. فرز أولي بنموذج رخيص يستبعد الـ Spam والاستفسارات غير المؤهلة.
  2. تحليل كامل بنموذج أقوى لما يتجاوز عتبة معينة فقط.
use Laravel\Ai\Attributes\UseCheapestModel;

#[UseCheapestModel]
class LeadTriage implements Agent, HasStructuredOutput
{
    use Promptable;

    public function schema(JsonSchema $schema): array
    {
        return [
            'is_spam' => $schema->boolean()->required(),
            'worth_full_analysis' => $schema->boolean()->required(),
        ];
    }
}

في التجربة العملية، هذا النمط يخفّض تكلفة التحليل بشكل ملموس دون التأثير على الـ Leads التي تهم فعلًا. انتبه فقط إلى أن UseCheapestModel قد يتغيّر النموذج الذي يختاره بين إصدارات الحزمة — إن أردت تكلفة وسلوكًا ثابتين تمامًا، حدّد النموذج صراحةً عبر #[Model(...)].


الخطوة 7: من درجة إلى إجراء

الدرجة وحدها لا تُغيّر سلوك فريق المبيعات. ما يُغيّره فعلًا هو الإجابة على «ماذا أفعل الآن؟». لذلك يجب أن يمر مخرج النموذج عبر طبقة قواعد عمل قبل أن يصل إلى الشاشة:

final class NextAction
{
    public static function for(Lead $lead): NextBestAction
    {
        // القواعد التنظيمية تتقدّم على اقتراح النموذج
        if ($lead->do_not_call) {
            return NextBestAction::SendWhatsapp;
        }

        if ($lead->outsideBusinessHours() && $lead->ai_recommended_action === NextBestAction::CallCustomer) {
            return NextBestAction::FollowUpLater;
        }

        return $lead->ai_recommended_action;
    }
}

وعندها يتغيّر ترتيب شاشة الـ CRM من «الأحدث أولًا» إلى «الأعلى احتمالًا للتحويل أولًا»:

Lead::query()
    ->whereNull('closed_at')
    ->orderByDesc('final_score')
    ->orderBy('created_at')
    ->paginate(25);

وما يراه الموظف لم يعد سطرًا في جدول، بل بطاقة قرار:

🔥  Ahmad — 91/100 · Hot · SLA: 15 min

Intent: High          Confidence: 84%
Action: Call now

الملخص:
مهتم بالاشتراك في الباقة المميزة، زار صفحة الأسعار 4 مرات
خلال أسبوع، واعتراضه الوحيد يتعلق بطريقة الدفع.

الاعتراض الرئيسي: شروط الدفع
الرد المقترح: اشرح خيارات التقسيط المتاحة.


الخطوة 8: جعل الدرجة ديناميكية

الـ Lead Score ليس رقمًا يُحسب مرة واحدة عند الإنشاء. كل تفاعل جديد يحمل إشارة أقوى بكثير من نموذج التسجيل الأول.

تحليل المكالمات

الحزمة توفّر تفريغًا صوتيًا مع فصل المتحدثين، وهو ما يجعل تحليل المكالمة أدق بكثير من نص مدموج:

use Laravel\Ai\Transcription;

$transcript = Transcription::fromStorage("calls/{$call->id}.mp3")
    ->diarize()
    ->generate();

$analysis = (new CallAnalyzer)->prompt((string) $transcript);

ومن التفريغ يمكن استخراج: نية العميل، الاعتراضات، النبرة العامة، المنافسون المذكورون، والالتزامات المتفق عليها — ثم تحديث الـ Lead تلقائيًا دون أن يكتب الموظف ملاحظات يدوية.


محادثات WhatsApp

هنا يجب تحليل المحادثة كوحدة واحدة لا رسالة منفردة، لأن الفرق بين «هل يسأل فقط؟» و«هل يقارن الأسعار؟» و«هل حسم قراره وينتظر رابط الدفع؟» يظهر في تسلسل الرسائل لا في آخرها.

class ConversationObserver
{
    public function created(Message $message): void
    {
        AnalyzeLead::dispatch($message->lead_id)
            ->delay(now()->addMinutes(2))   // انتظر انتهاء تدفق الرسائل
            ->afterCommit();
    }
}

التأخير المقصود هنا (Debounce) مع ShouldBeUnique يمنع إطلاق تحليل لكل رسالة في محادثة سريعة.


الحوكمة: لا تجعل النموذج صاحب القرار النهائي

من السهل الانزلاق إلى أتمتة كاملة من نوع:

AI score < 20  →  حذف الـ Lead
AI = unqualified  →  عدم التواصل نهائيًا

هذا خطر لسببين: الأول أن النموذج قد يخطئ في قراءة رسالة قصيرة أو لهجة غير مألوفة، والثاني أن القرار يصبح غير قابل للمراجعة لأن الـ Lead المحذوف لا يترك أثرًا يمكن قياسه لاحقًا. التصميم الآمن:

AI Recommendation  +  Business Rules  +  Human Decision  →  الإجراء

عمليًا: اجعل التصنيف المنخفض يؤثر على الترتيب ومستوى الجهد المخصص، لا على حق العميل في التواصل.


التتبّع والتدقيق

الحزمة تُطلق أحداثًا مثل PromptingAgent وAgentPrompted، ويمكن الاستماع إليها لتسجيل الاستخدام والتكلفة دون تلويث منطق الأعمال:

class RecordAiUsage
{
    public function handle(AgentPrompted $event): void
    {
        AiUsage::create([
            'agent' => $event->agent::class,
            'provider' => $event->provider,
            'model' => $event->model,
            'usage' => $event->response->usage,
        ]);
    }
}

لماذا يهم هذا؟ لأنك بعد شهر ستغيّر الـ Prompt أو النموذج أو إطار التقييم، وستلاحظ أن الدرجات تحرّكت. وجود ai_prompt_version إلى جانب سجل الاستخدام هو ما يحوّل هذه الملاحظة من لغز إلى تفسير.


الاختبار: تثبيت السلوك قبل الإطلاق

الميزة الأهم عمليًا في الـ SDK هي إمكانية تزييف الاستجابات، ما يجعل اختبار منطق التسجيل ممكنًا دون استدعاء أي مزوّد:

it('promotes a lead with clear purchase intent', function () {
    LeadAnalyzer::fake([
        [
            'lead_score' => 87,
            'classification' => 'hot',
            'intent' => 'high',
            'data_sufficiency' => 'sufficient',
            'confidence' => 84,
            'summary' => 'مهتم بالاشتراك ويسأل عن التقسيط.',
            'recommended_action' => 'call_customer',
            'reason' => 'نية شراء واضحة مع اعتراض على طريقة الدفع.',
            'objections' => [],
        ],
    ]);

    $lead = Lead::factory()->create(['demo_requested' => true]);

    AnalyzeLead::dispatchSync($lead->id);

    expect($lead->fresh()->ai_classification)->toBe(LeadClassification::Hot)
        ->and($lead->fresh()->final_score)->toBeGreaterThan(80);

    LeadAnalyzer::assertPrompted(
        fn (AgentPrompt $prompt) => $prompt->contains('pricing_page_visits')
    );
});

أضف LeadAnalyzer::fake()->preventStrayPrompts() في إعداد الاختبارات لتضمن أن أي استدعاء غير متوقع يُسقط الاختبار بدل أن يمر بصمت.

مجموعة اختبار مرجعية (Golden Set)

الاختبارات السابقة تتحقق من الأنابيب لا من جودة الحكم. لقياس الجودة، جهّز 50–100 Lead تاريخيًا تعرف نتيجتها الفعلية، ومرّرها على الـ Agent عند كل تغيير في الـ Prompt، وقارن التوزيع قبل وبعد. هذا يحوّل تعديل الـ Prompt من تخمين إلى تغيير مُقاس.


القياس بعد الإطلاق: هل النظام يعمل فعلًا؟

لا تفترض أن درجة 90 تعني Lead ممتاز. أثبت ذلك. بعد شهر من التشغيل، ابنِ جدول معايرة يقارن الدرجة بمعدل التحويل الحقيقي:

مثال على جدول معايرة صحي
الدرجةعدد الـ Leadsمعدل التحويل
90–10021042%
80–8934031%
70–7951219%
50–698808%
0–491,3402%

العلاقة التصاعدية الواضحة تعني أن النظام يقدّم إشارة مفيدة. أما توزيع مسطّح (10% ثم 12% ثم 11%) فيعني أن الدرجة ضجيج مُنسَّق بشكل جميل، ويجب مراجعة إطار التقييم أو الإشارات المتاحة.


تحذير منهجي: التنبؤ الذي يُحقق نفسه

هذه النقطة تُغفل غالبًا وهي جوهرية: إذا اتصل الفريق أولًا وبأسرع وقت بالـ Hot Leads فقط، فمن الطبيعي أن تتحول أكثر — ليس بالضرورة لأن التقييم دقيق، بل لأنها حظيت باهتمام أكبر. لتفادي خداع النفس، احتفظ بمجموعة ضابطة صغيرة (5–10%) تُعالَج بالترتيب الزمني التقليدي، وقارن النتائج. هذه هي الطريقة الوحيدة لعزل أثر النظام عن أثر الاهتمام.


المقاييس التي تهم فعلًا

  • زمن الاستجابة الأولى للـ Leads عالية الدرجة (المؤشر الأسرع استجابة للتحسّن).
  • معدل التحويل لكل شريحة درجات.
  • إنتاجية الموظف: عدد المحادثات المُجدية مقابل إجمالي المحاولات.
  • الوقت الموفَّر في قراءة السجلات وكتابة الملاحظات.
  • تكلفة التحليل لكل Lead مقابل قيمة الصفقة المتوسطة.


أخطاء شائعة تستحق التجنّب

  • إرسال النموذج كاملًا إلى المزوّد بدل لقطة مُصفّاة.
  • تشغيل التحليل داخل دورة الطلب، فيتحوّل بطء المزوّد إلى بطء في الموقع.
  • حرارة مرتفعة في مهمة تصنيف، فتحصل على درجات مختلفة لنفس الـ Lead.
  • غياب نسخة الـ Prompt، فتصبح مقارنة الأداء عبر الزمن مستحيلة.
  • الاعتماد على مزوّد واحد بلا Failover ولا إعادة محاولة.
  • معاملة غياب الإشارة كإشارة سلبية، فتُظلم الـ Leads القادمة من قنوات لا تجمع بيانات سلوكية.
  • القياس بالدقة (Accuracy) وحدها بدل معدل التحويل وزمن الاستجابة.


الخلاصة

ما بنيناه ليس ميزة ذكاء اصطناعي مضافة إلى الواجهة، بل طبقة قرار مدمجة في مسار البيانات: الـ Lead يصل، فتُبنى منه لقطة مُصفّاة، تُحلَّل في الخلفية، وتُدمَج نتيجتها مع قواعد حتمية، ثم تظهر لموظف المبيعات كأولوية وإجراء مبرَّر — مع أثر كامل يسمح بالمراجعة والقياس.

الفارق الجوهري أن الـ CRM التقليدي يقول: «هذه بيانات العميل». بينما النظام الذي بنيناه يقول: «هذه بيانات العميل، درجة استعداده للشراء 91، اعتراضه الأساسي طريقة الدفع، اتصل به خلال 15 دقيقة، وابدأ بخيارات التقسيط».

والفكرة الأهم ليست أن يستبدل النموذج حكم فريق المبيعات، بل أن يقرأ آلاف الإشارات التي يستحيل على الموظف مراجعتها يدويًا كل صباح، ثم يقدّم له إجابة واحدة قابلة للتنفيذ فورًا: بمن أتصل الآن، ولماذا.

قائمة تحقق قبل الإطلاق

  1. حمولة التحليل تعتمد قائمة بيضاء صريحة للحقول، ولا تتضمن أي بيانات هوية غير ضرورية.
  2. التحليل يعمل في طابور مخصص مع Worker مراقَب.
  3. الـ Job قابل لإعادة التشغيل بأمان، مع حد معدل وإعادة محاولة وfailed().
  4. مزوّد احتياطي مُعرَّف عبر الـ Failover.
  5. كل تحليل يحمل نسخة الـ Prompt والنموذج والمزوّد وبصمة المدخلات.
  6. اختبارات آلية بالـ fake() تغطي منطق التسجيل الهجين.
  7. مجموعة ضابطة جاهزة لقياس الأثر الحقيقي بعد شهر.
  8. لا يوجد أي مسار يحذف Lead أو يمنع التواصل معه اعتمادًا على درجة تلقائية وحدها.
إضافة تعليق
Loading...