SEPEHR.SYS

Starting SEPEHR.SYS…

Loading

سپهر محسنی

مهندس نرم‌افزار فول‌استک و هوش مصنوعی

NOTES.TXT ×

ساخت بک‌اندهای AI-native – پایپ‌لاین‌های RAG، فراخوانی تابع (Function Calling)، نسخه‌بندی پرامپت و مشاهده‌پذیری LLM

4′ · ۸ بهمن ۱۴۰۴

دو ماه پیش، چت‌بات پایگاه دانش داخلی‌مان با اطمینان کامل به یکی از کارشناسان پشتیبانی گفت که سیاست بازگشت وجه ما «۱۴ روز، بدون هیچ سؤالی» است. سیاست واقعی ما ۳۰ روز است، آن هم با تأیید برای مبالغ بزرگ‌تر.

یک بازگشت وجه ۲۰۰۰ دلاری بر اساس همان توهم (Hallucination) پردازش شد.

همان لحظه بود که دست از نگاه‌کردن به قابلیت‌های LLM به‌چشم «جعبه‌های متنی هوشمند» برداشتیم و شروع کردیم به رفتار با آن‌ها به‌عنوان سیستم‌های توزیع‌شدهٔ غیرقابل‌اعتماد که به مهندسی واقعی نیاز دارند.

این مقاله دربارهٔ دمو نیست؛ دربارهٔ چیزی است که بعد از به‌کار افتادن دمو باید بسازید.


واقعیت بک‌اندهای هوش مصنوعی

بک‌اندهای سنتی قطعی (Deterministic) هستند.

ورودی یکسان → خروجی یکسان.

بک‌اندهای هوش مصنوعی احتمالاتی (Probabilistic) هستند.

ورودی یکسان → خروجی کمی متفاوت، بسته به کانتکست، واریانس مدل و ساختار پرامپت.

این یعنی:

  • نمی‌توانید به خروجی‌ها اعتماد کنید
  • نمی‌توانید به بازیابی (Retrieval) اعتماد کنید
  • نمی‌توانید به پرامپت‌ها اعتماد کنید
  • نمی‌توانید به فراخوانی ابزارها اعتماد کنید
  • باید همه‌چیز را زیر نظر بگیرید

یک بک‌اند هوش مصنوعی در پروداکشن در نهایت چنین شکلی پیدا می‌کند:

API │ AI Orchestrator ├─ Guardrails ├─ Router ├─ Rate limits │ ├─ RAG pipeline ├─ Function execution └─ Direct generation │ Observability + Evals

اگر هر کدام از این لایه‌ها را جا بیندازید، دیر یا زود توهمی را روانهٔ پروداکشن می‌کنید که هزینهٔ مالی دارد.


RAG فقط «چانک، Embed، کوئری» نیست

نسخهٔ آموزشیِ RAG این است:

متن را تکه‌تکه کن → Embed بگیر → جست‌وجوی برداری انجام بده → به LLM بسپار

این روش در نوت‌بوک جواب می‌دهد؛ در پروداکشن شکست می‌خورد.

یک RAG واقعی به این‌ها نیاز دارد:

  • پایپ‌لاین درست‌وحسابی برای ورود داده (Ingestion)
  • چانک‌بندی معنایی (Semantic Chunking)
  • تشخیص تغییرات
  • بازیابی ترکیبی (برداری + کلیدواژه‌ای)
  • بازمرتب‌سازی نتایج (Reranking)
  • ارزیابی مداوم کیفیت بازیابی

Ingestion در لاراول

Ingestion یک Job صف‌شده است، نه یک اسکریپت.

اسناد را مدام دوباره بررسی می‌کنید و فقط چیزی را که تغییر کرده دوباره Embed می‌کنید.

class IngestDocuments
{
    public function handle(SourceInterface $source)
    {
        $documents = $source->fetch();

        foreach ($documents as $doc) {
            $hash = sha1($doc->content);

            if (Cache::get("doc_hash_{$doc->id}") === $hash) {
                continue;
            }

            $chunks = (new SemanticChunker())->chunk($doc->content);
            $embeddings = app(EmbeddingService::class)->embed($chunks);

            app(VectorStore::class)->upsert($doc->id, $chunks, $embeddings);

            Cache::put("doc_hash_{$doc->id}", $hash, now()->addDay());
        }
    }
}

بزرگ‌ترین بهبود کیفیتی که خواهید دید، چانک‌بندی معنایی به‌جای تقسیم‌های ثابت مبتنی بر توکن است.


بازیابی ترکیبی (Hybrid Retrieval) اجباری است

جست‌وجوی برداری تطبیق‌های دقیق مثل شناسهٔ سفارش، SKU و ایمیل را از دست می‌دهد.

جست‌وجوی کلیدواژه‌ای معنا را از دست می‌دهد.

به هر دو نیاز دارید.

class HybridRetriever
{
    public function search(string $query, int $limit = 8)
    {
        $vector = app(VectorStore::class)->search($query, $limit * 2);
        $keyword = app(KeywordSearch::class)->search($query, $limit * 2);

        return $this->mergeAndRank($vector, $keyword, $limit);
    }
}

بیشتر توهم‌ها در سیستم‌های RAG در واقع شکست بازیابی هستند، نه شکست مدل.


تولید پاسخ با کانتکست مستند (Grounded)

چیزی که به مدل می‌دهید، از خود مدل مهم‌تر است.

class RagResponder
{
    public function answer(string $question, array $chunks)
    {
        $context = collect($chunks)
            ->pluck('content')
            ->join("\n\n");

        $prompt = Prompt::load('rag-answer', 'v3');

        $response = app(LLM::class)->chat([
            ['role' => 'system', 'content' => $prompt->system],
            ['role' => 'user', 'content' => $prompt->fill([
                'context' => $context,
                'question' => $question,
            ])],
        ], temperature: 0.2, json: true);

        return $response;
    }
}

دمای (Temperature) پایین. خروجی ساخت‌یافته. کانتکست صریح.

هدف شما کم‌کردن خلاقیت است، نه بیشترکردن آن.


Function Calling بدون گاردریل کار دست‌تان می‌دهد

اینکه اجازه بدهید یک LLM بدون هیچ کنترلی اکشن‌های بک‌اند را اجرا کند، معادل این است که بگذارید کاربران مستقیماً APIهای داخلی را صدا بزنند.

هر فراخوانی ابزار باید از این‌ها عبور کند:

  • احراز مجوز (Authorization)
  • محدودسازی نرخ (Rate Limiting)
  • ثبت لاگ ممیزی (Audit Log)
  • تأیید اختیاری
class ToolExecutor
{
    public function execute(string $tool, array $args, User $user)
    {
        $definition = ToolRegistry::get($tool);

        Gate::authorize($definition->ability, $user);

        if ($definition->needsApproval && !$user->isAdmin()) {
            throw new AuthorizationException();
        }

        RateLimiter::hit("tool:{$tool}", 60);

        $result = call_user_func($definition->handler, $args);

        AuditLog::create([
            'user_id' => $user->id,
            'tool' => $tool,
            'args' => $args,
            'result' => $result,
        ]);

        return $result;
    }
}

بازگشت وجه، تغییرات حساب کاربری، عملیات صورتحساب — این‌ها هرگز نباید «صرفاً یک فراخوانی تابع» باشند.


پرامپت‌ها کد هستند

پرامپت‌ها رفتار سیستم را بیشتر از خود کد تغییر می‌دهند.

پس باید:

  • نسخه‌بندی شوند
  • ذخیره شوند
  • بازبینی شوند
  • به‌تدریج منتشر شوند
class Prompt extends Model
{
    protected $casts = ['variables' => 'array'];
}

class PromptManager
{
    public static function load(string $name, string $version): Prompt
    {
        return Prompt::where(compact('name', 'version'))->firstOrFail();
    }
}

هرگز پرامپت‌ها را در فایل‌های PHP هاردکد نکنید.

بالاخره می‌خواهید بدون deploy مجدد تغییرشان بدهید.


مشاهده‌پذیری (Observability) اختیاری نیست

باید این‌ها را لاگ کنید، ردیابی (Trace) کنید و بسنجید:

  • کوئری کاربر
  • چانک‌های بازیابی‌شده
  • پرامپت نهاییِ ارسال‌شده
  • خروجی مدل
  • توکن‌ها و تأخیر (Latency)

بدون این‌ها، دیباگ‌کردن توهم‌ها ممکن نیست.

همچنین به ارزیابی‌های خودکاری (Evals) نیاز دارید که به‌صورت دوره‌ای بپرسند:

«آیا این پاسخ واقعاً به کانتکست ارائه‌شده مستند است؟»

این‌طوری است که مشکلات را پیش از کاربران پیدا می‌کنید.


کش و کنترل هزینه

فراخوانی‌های LLM گران و کند هستند.

فراخوانی‌های قطعی را با هش‌کردن ورودی‌ها cache کنید.

class CachedLLM
{
    public function chat(array $payload)
    {
        $key = hash('sha256', json_encode($payload));

        return Cache::remember($key, 3600, fn () =>
            app(LLM::class)->chat($payload)
        );
    }
}

هزینه را روزانه رصد کنید و اگر از بودجه فراتر رفتید، به‌طور قطعی متوقفش کنید (Hard Stop).


چه چیزی واقعاً جلوی حادثه‌ها را می‌گیرد

بعد از تعداد کافی حادثه در پروداکشن، می‌فهمید که محافظ‌های واقعی این‌ها هستند:

  • بازیابی ترکیبی
  • پرامپت‌های سخت‌گیرانه
  • اجرای کنترل‌شدهٔ ابزارها
  • ردیابی کامل (Tracing)
  • ارزیابی‌های خودکار (Evals)
  • کش‌کردن تهاجمی

نه انتخاب مدل. نه ایجنت‌های پرزرق‌وبرق. نه فریم‌ورک‌ها.

فقط انضباط مهندسی، اعمال‌شده بر یک سیستم احتمالاتی.


جمع‌بندی نهایی

  • یک قابلیت هوش مصنوعی در حد دمو، شبیه جادوست.
  • یک سیستم هوش مصنوعی در پروداکشن، شبیه یک بک‌اند بدبین و بیش‌ازحد مهندسی‌شده است.

و دقیقاً باید همین‌طور هم باشد.

این یادداشت ترجمهٔ فارسی نوشتهٔ خودم است — نسخهٔ اصلی (انگلیسی) در dev.to