Evaluation

بنچمارک‌های LLM: فراتر از Pass@1 تا سمت نگهداری‌پذیری

در دو سال گذشته، صنعت به‌طور وسواسی بر یک شاخص واحد برای ارزیابی مدل‌های زبانی بزرگ (LLM) در برنامه‌نویسی تمرکز کرده است: Pass@1. اگرچه Pass@1 به ما می‌گوید آیا یک مدل می‌تواند تابعی تولید کند که در تلاش اول از تست‌ها عبور کند، اما در پاسخ به سوال مهم‌تری برای توسعه سازمانی شکست می‌خورد: آیا کد نگهداری‌پذیر است؟

در حالی که از پروتوتایپ‌های آزمایشی به سمت سیستم‌های سطح تولید حرکت می‌کنیم، به چارچوب‌های ارزیابی نیاز داریم که سلامت کد، خوانایی و قابلیت تکامل را در نظر بگیرند. این پست بررسی می‌کند که چگونه می‌توانیم LLMها را نه تنها بر اساس صحت، بلکه بر اساس کیفیت کدی که تولید می‌کنند، بنچمارک کنیم.

محدودیت‌های Pass@1

Pass@1 یک شاخص باینری است. یک تابع تولید شده یا از مجموعه تست‌های پنهان عبور می‌کند یا نمی‌کند. این رویکرد چندین نقطه کور دارد:

  • بی‌توجهی به سبک: یک LLM ممکن است کدی کارآمد اما غیرقابل‌خوانش با استفاده از تک‌خطی‌های مبهم یا تکنیک‌های بهینه‌سازی تهاجمی تولید کند که برای انسان‌ها مرور آن‌ها دشوار است.
  • نبود زمینه: این شاخص اندازه‌گیری نمی‌کند که کد چقدر خوب در یک کدبیس موجود جاسازی می‌شود. آیا از قرارداد نام‌گذاری پروژه پیروی می‌کند؟ آیا از کتابخانه استاندارد استفاده می‌کند یا وابستگی‌های غیرضروری معرفی می‌کند؟
  • عدم سیگنال بازسازی: این شاخص توانایی مدل در درک و اصلاح کد موجود را آزمایش نمی‌کند، در حالی که این بخش بزرگی از جریان کاری روزانه یک توسعه‌دهنده است، در مقایسه با نوشتن از صفر.

معرفی شاخص‌های کیفیت ترکیبی

برای رفع این خلأها، ما یک سیستم امتیازدهی ترکیبی را پیشنهاد می‌دهیم که صحت عملکردی را با شاخص‌های تحلیل استاتیک وزن‌دهی می‌کند. در اینجا تفکیکی از اجزای آن آمده است:

۱. پیچیدگی کد (پیچیدگی سیکلوماتیک)

ما از ابزارهایی مانند radiate یا pylint برای اندازه‌گیری پیچیدگی سیکلوماتیک استفاده می‌کنیم. امتیازات پیچیدگی بالا نشان‌دهنده کدی است که تست و نگهداری آن دشوار است. یک بنچمارک باید راه‌حل‌هایی که از طریق تودرتویی عمیق یا منطق شرطی پیچیده به نتایج صحیح می‌رسند را تنبیه کند.

۲. پایداری Diff و تغییرات حداقلی

در وظایف بازسازی، ما «پایداری diff» را اندازه‌گیری می‌کنیم. یک مدل با کیفیت بالا باید تغییرات حداقلی و جراحی‌گونه در کد موجود ایجاد کند. اگر از یک LLM خواسته شود پارامتری به یک تابع اضافه کند اما به جای آن کل کلاس را بازنویسی کند، باید تنبیه شود. می‌توانیم این موضوع را با استفاده از فاصله Levenshtein روی diff یا صرفاً شمارش تعداد خطوط تغییر یافته در مقابل خطوط ضروری کمّی کنیم.

۳. پوشش تست راه‌حل

آیا کد تولید شده همراه با تست‌های محکم است؟ می‌توانیم کیفیت تست‌های واحد خودکار تولید شده را با اندازه‌گیری پوشش آن‌ها از موارد حاشیه‌ای (edge cases) ارزیابی کنیم. یک LLM که کد تولید می‌کند اما فقط «مسیر خوشبخت» (happy path) را تست می‌کند، باید امتیاز کمتری نسبت به مدلی دریافت کند که حالت‌های شکست بالقوه را شناسایی می‌کند.

مثال عملی: ارزیابی یک بازسازی

فرض کنید وظیفه‌ای وجود دارد که پرامپت آن به این صورت است: «تابع زیر را برای بهبود خوانایی و افزودن مدیریت خطا بازسازی کنید.»


# کد اصلی
def process_data(data):
    if data:
        return [x for x in data if x > 0]
    return []

مدلی که با پاسخ زیر پاسخ دهد، در Pass@1 (اگر تست‌ها پاس شوند) امتیاز بالایی خواهد گرفت اما در نگهداری‌پذیری امتیاز پایینی:


# پاسخ A: کیفیت پایین
def process_data(d):
    if d is not None:
        try:
            return [i for i in d if i > 0]
        except Exception as e:
            return []
    return []

پاسخ A از نام‌های متغیر مبهم (d، i) استفاده می‌کند و تمام استثناها را دریافت می‌کند، که یک ضدالگو (anti-pattern) است. یک پاسخ با کیفیت بالا به این صورت خواهد بود:


# پاسخ B: کیفیت بالا
def process_data(data):
    """
    مقادیر غیرمثبت را از داده ورودی فیلتر می‌کند.
    
    آرگومان‌ها:
        data: یک لیست از اعداد.
        
    بازگشت:
        یک لیست از اعداد مثبت، یا یک لیست خالی اگر ورودی None باشد.
    """
    if not data:
        return []
    
    positive_values = []
    for value in data:
        if isinstance(value, (int, float)) and value > 0:
            positive_values.append(value)
            
    return positive_values

پاسخ B پرحرف است، اما خودمستند (self-documenting) است، بررسی نوع را به‌طور صریح انجام می‌دهد و از استانداردهای PEP8 پیروی می‌کند. یک بنچمارک متمرکز بر نگهداری‌پذیری به پاسخ B امتیاز به‌طور قابل‌توجهی بالاتری نسبت به پاسخ A اختصاص خواهد داد، حتی اگر هر دو از تست‌های عملکردی پایه عبور کنند.

پیاده‌سازی بنچمارک

برای پیاده‌سازی این موضوع، می‌توانیم یک پایپ‌لاین ایجاد کنیم که به جریان CI/CD چارچوب ارزیابی متصل شود:

  1. تولید کد: LLM را با وظیفه پرامپت کنید.
  2. تحلیل استاتیک: flake8، bandit و mypy را روی خروجی اجرا کنید. خروجی را برای هشدارها تجزیه کنید.
  3. تست عملکردی: کد تولید شده را در مقابل مجموعه‌ای از تست‌های واحد اجرا کنید.
  4. اعتبارسنجی بازسازی: اگر وظیفه شامل اصلاح کد موجود است، یک «بررسی diff» را اجرا کنید تا اطمینان حاصل شود که تغییرات محلی (localized) هستند.
  5. جمع‌بندی امتیاز: این سیگنال‌ها را به یک امتیاز نهایی وزن‌دار ترکیب کنید. برای مثال: Score = 0.5 * Functional_Correctness + 0.3 * Static_Analysis_Score + 0.2 * Refactor_Efficiency

نتیجه‌گیری

در حالی که LLMها به بخش جدایی‌ناپذیر جریان‌های کاری توسعه ما تبدیل می‌شوند، تکیه صرف بر Pass@1 مانند استخدام یک توسعه‌دهنده صرفاً بر اساس توانایی او در عبور از یک مصاحبه برنامه‌نویسی تکی است، در حالی که تناسب فرهنگی بلندمدت و کیفیت کد او نادیده گرفته می‌شود. با معرفی شاخص‌های نگهداری‌پذیری و بازسازی به بنچمارک‌های خود، می‌توانیم بهتر پیش‌بینی کنیم که این مدل‌ها در بافت‌های مهندسی نرم‌افزار دنیای واقعی چگونه عمل خواهند کرد. آینده ارزیابی LLMها نه تنها درباره چیزی است که امروز کار می‌کند، بلکه درباره چیزی است که شش ماه دیگر نیز همچنان کار خواهد کرد.

Share: