پرش به محتوای اصلی
پرش به محتوای مقاله

«توهم مسیر موفق»؛ ریسک پنهان در کدنویسی با Claude Code

·۲۹ شهریور ۱۴۰۵۲۶ دقیقه مطالعه
راهنما
لاگ مهندسی ساخت کتابخانه findmypylibrary با برنامه‌نویس هوشمند Claude Code
لاگ مهندسی ساخت کتابخانه findmypylibrary با برنامه‌نویس هوشمند Claude Code
اشتراک‌گذاری
واقعاً چه چیز جدید است؟

افشای مکانیسم «تله تست‌های سبز» در توسعه با Claude Code؛ جایی که پوشش کد ۹۸ درصدی نتوانست از کرش‌های سیستمی جلوگیری کند چون تست‌ها روی پراکسی‌ها متمرکز بودند نه نتایج.

تصور کنید کدی می‌نویسید که تمام تست‌هایش سبز هستند، اما محصول نهایی در دستان کاربر به‌طور کامل از کار می‌افتد. این همان «توهم مسیر موفق» (Happy Path Fallacy) است که در توسعه‌ی findmypylibrary — یک ابزار سبک برای جست‌وجوی کتابخانه‌های PyPI — به وضوح نمایان شد. توسعه‌ی این ابزار، از یک جایگاه‌دار (Placeholder) ساده در نسخه‌ی 0.0.1 در PyPI — که تنها یک رزرو نام بود و README آن دستوراتی را توصیف می‌کرد که هنوز وجود نداشتند — تا تبدیل شدن به یک ابزار منتشر شده، نقشه‌ی راهی هشداردهنده از تنش میان برنامه‌نویس و هوش مصنوعی است و ثابت می‌کند که ساخت یک ابزار آماده برای تولید با کمک یک جفت‌برنامه‌نویس AI، به‌ندرت مسیری مستقیم از پرامپت به انتشار است.

این پروژه در زمانی پیش می‌رود که برنامه‌نویسان به‌طور فزاینده‌ای سپردن کل ماژول‌ها را به عامل‌های هوش مصنوعی تجربه می‌کنند. همان‌طور که در تحلیل‌های قبلی ما درباره‌ی عملکرد مدل‌های مختلف در محیط‌های کاری واقعی اشاره کردیم، این گزارش ثابت می‌کند که اگرچه Claude Code می‌تواند سرعت نوشتن کدهای تکراری (Boilerplate) را بالا ببرد، اما مهندسی‌های حیاتی مانند منطق رتبه‌بندی، مدیریت هم‌روندی (Concurrency) و پایداری در پلتفرم‌های مختلف، همچنان به بازبینی سخت‌گیرانه‌ی انسانی نیاز دارند. این چالش‌ها یادآور برخی باگ‌های بحرانی در اتصال عامل‌های هوش مصنوعی به APIهاست که نشان می‌دهد حتی پیشرفته‌ترین مدل‌ها در مواجهه با پیچیدگی‌های زیرساختی دچار لغزش می‌شوند.

محدودیت‌های بنیادین

توسعه‌ی ابزار بر سه شرط غیرقابل‌مذاکره استوار بود که از روز اول تعیین شدند:

  • مبنی‌سازی بر داده‌های واقعی (Grounding): هر نتیجه باید یک بسته‌ی واقعی با تعداد دانلود واقعی و تاریخ انتشار واقعی باشد، نه بر اساس حافظه‌ی یک مدل زبانی.
  • آفلاین بودن پس از نخستین دانلود: پرس‌وجوها هرگز نباید از ماشین کاربر خارج شوند.
  • صفر کردن اصطکاک: بدون نیاز به API Key، بدون حساب کاربری و بدون وابستگی‌های سنگین.

برای دستیابی به این هدف، تیم توسعه دو بار پیشنهاد «بدیهی» هوش مصنوعی برای استفاده از بردار معنایی (Embeddings) و دانلود مدل‌ها را رد کرد؛ زیرا این کار باعث حجیم شدن ردپای ابزار و نقض فلسفه‌ی «سبک بودن» می‌شد.

اکتساب داده‌ها و مقیاس‌بندی

از آنجایی که PyPI فاقد یک نقطه اتصال (Endpoint) رایگان برای دریافت انبوه آمار دانلودهاست، تیم مجبور شد از یک رویکرد ترکیبی استفاده کند. آن‌ها دو منبع عمومی را که نیاز به کلید دسترسی نداشتند ترکیب کردند:

۱. hugovk/top-pypi-packages: یک فایل JSON از پردانلودترین بسته‌ها در ۳۰ روز گذشته که توسط نگهدارنده‌اش به‌طور دوره‌ای از BigQuery بازسازی می‌شود.
۲. PyPI JSON API: درخواست‌های مجزا به آدرس https://pypi.org/pypi/<name>/json برای دریافت خلاصه‌ها و تاریخ‌های انتشار.

در همین مسیر، یک باگ اولیه رخ داد؛ URL مستندات به‌جای برگرداندن JSON، یک ریدایرکت HTML 301 برگرداند که دستور curl اولیه نتوانست آن را دنبال کند. این اتفاق منجر به یک درس دائمی شد: هرگز پاسخ ۲۰۰ OK را پیش‌فرض نگیرید و همیشه ریدایرکت‌ها را دنبال کنید.

تعریف بسته‌های «فعال»

تیم بر سر این موضوع بحث کرد که آیا باید ۲۰۰، ۲,۰۰۰ یا ۸,۰۰۰ بسته را ایندکس کند. مالک محصول درخواست کرد «۱۵,۰۰۰ بسته، یا هر تعدادی که تمام کتابخانه‌های فعال را پوشش دهد». پس از بررسی، مشخص شد که مجموعه‌ی داده دقیقاً ۱۵,۰۰۰ ردیف دارد که بسته‌هایی با حداقل حدود ۶۸,۰۰۰ دانلود در ماه را پوشش می‌دهد. این عدد به عنوان تعریف رسمی «بسته‌های فعال» پذیرفته شد.

مقیاس‌بندی خزش (Crawl)

دریافت ۱۵,۰۰۰ بسته به‌صورت متوالی بیش از یک ساعت زمان می‌برد. تیم برای کاهش این زمان به چند دقیقه، httpx.AsyncClient را با یک سمه‌فور (Semaphore) برای ۲۵ درخواست هم‌زمان پیاده‌سازی کرد. برای جلوگیری از فشار بیش از حد به سرورهای PyPI در طول توسعه، آن‌ها از دستور refresh --limit 40 برای اکثر تست‌ها استفاده کردند. اولین اجرای کامل با موفقیت ۱۴,۹۹۹ بسته از ۱۵,۰۰۰ مورد را دریافت کرد (یک مورد واقعاً ۴۰۴ بود و از لیست حذف شده بود).

نخستین معماری عملیاتی

معماری اولیه به‌طور تعمدی «ساده و خسته‌کننده» طراحی شد تا پیچیدگی کاهش یابد:

  • fetch.py: مدیریت دو منبع داده برای ایجاد لیستی از دیکشنری‌های بسته‌ها با استفاده از async، هم‌روندی محدود و مکانیزم‌های تلاش مجدد (Retries).
  • cache.py: مدیریت یک فایل SQLite در مسیر ~/.cache/findmypylibrary/.
  • rank.py: پردازش پرس‌وجوها برای بازگرداندن بسته‌های رتبه‌بندی شده.
  • cli.py: پیاده‌سازی دستورات Click برای refresh و search.

برای پشتیبانی از وعده‌ی README مبنی بر امکان اجرای پرس‌وجوی ساده (مثلاً findmypylibrary "parse messy pdfs")، تیم مجبور شد click.Group را به یک DefaultGroup تبدیل کند. این کار به CLI اجازه داد تا نام‌های دستور ناشناخته را شناسایی کرده و به‌طور خودکار آن‌ها را به زیردستور search هدایت کند.

نبرد رتبه‌بندی: BM25 در برابر واقعیت

رتبه‌بندی اولیه از یک ترکیب BM25 خالص پایتونی استفاده می‌کرد: score = 0.60 * relevance + 0.25 * popularity + 0.15 * recency (که هر کدام با روش min-max نرمال شده بودند). این رویکرد منجر به یک شکست بحرانی شد: بسته‌های تخصصی (Niche) با خلاصه‌های کوتاه و متراکم از نظر کلمات کلیدی، رتبه‌ی بالاتری نسبت به استانداردهای صنعت می‌گرفتند. برای مثال، در جست‌وجوی «read and write excel spreadsheets»، بسته‌ی numbers-parser (با ۹۲۴ هزار دانلود) بر openpyxl (با ۳۳۹ میلیون دانلود) پیروز می‌شد، زیرا خلاصه‌ی شش‌کلمه‌ای آن تراکم بسیار بالایی از کلمات پرس‌وجو داشت.

تشخیص مشکل نشان داد که نرمال‌سازی طول در BM25، به اسناد کوتاه با تراکم بالا پاداش می‌دهد. علاوه بر این، چون هر فاکتور به‌طور جداگانه نرمال شده بود، یک شکاف بزرگ در ارتباط (با وزن ۰.۶) نمی‌توانست توسط فاکتور محبوبیت (با وزن ۰.۲۵) جبران شود، فارغ از اینکه اختلاف تعداد دانلودها چقدر زیاد باشد.

برای رفع این مشکل، تیم از «ترکیب» (Blending) به «گیتینگ» (Gating) تغییر مسیر داد. اکنون «ارتباط» به عنوان یک فیلتر عمل می‌کند — یعنی هر تطابقی که در محدوده ۵۰٪ بهترین تطابق باشد را نگه می‌دارد — و سپس «محبوبیت» ترتیب نهایی را تعیین می‌کند. این روش دقیقاً مشابه عملکرد موتورهای جست‌وجوی حرفه‌ای است: تطابق متنی برای بازیابی (Recall) و اعتبار برای ترتیب‌بندی (Ordering). آن‌ها همچنین یک لیست مترادف (مانند excel/xlsx/spreadsheet) اضافه کردند و به تطابق‌های موجود در «نام بسته» وزن بیشتری نسبت به «خلاصه» دادند.

مهاجرت به SQLite FTS5

با افزایش نیاز به تحلیل ریشه‌ی کلمات (Stemming) و ایندکس کردن فایل‌های README، مدل BM25 خالص پایتونی بیش از حد کند شد. تیم به SQLite FTS5 مهاجرت کرد و از یک Porter stemmer و یک جدول مجازی بدون محتوا (content='') استفاده نمود. این تغییر حجم دیسک را به ۱۹.۸ مگابایت (۱۰.۹ مگابایت در حالت gzipped) کاهش داد و زمان پاسخ‌دهی به پرس‌وجوها را به ده‌ها میلی‌ثانیه رساند.

مدیریت نویز در README

ایندکس کردن READMEها باعث ورود «نویز» شد. برای مثال، boto3 برای جست‌وجوی «unit testing» رتبه می‌گرفت، صرفاً چون در README آن بخشی درباره‌ی نحوه اجرای تست‌های واحد وجود داشت. برای مبارزه با این مشکل، تیم یک سیستم پرس‌وجوی دوگانه پیاده کرد:

  • فیلدهای اصلی: نام، خلاصه، کلمات کلیدی و موضوعات به‌طور عادی امتیاز می‌گیرند.
  • توضیحات: بخش‌های README در یک پرس‌وجوی جداگانه امتیازدهی شده و با تخفیف شدیدی به امتیاز کلی اضافه می‌شوند.

برای تضمین کیفیت، آن‌ها پیش از تغییر ثابت‌های رتبه‌بندی، ۴۰ «پرس‌وجوی طلایی» (Golden Queries) با پاسخ‌های صحیح مورد انتظار نوشتند. یک بررسی پارامتری نشان داد که وزن ۰.۵ برای README، نرخ موفقیت مجموعه طلایی را به حداکثر (۴۰/۴۰) می‌رساند و در عین حال boto3 را از نتایج «unit testing» حذف می‌کند.

تله‌ی «تست‌های سبز»

یکی از مهم‌ترین یافته‌ها، شکست سیستم تست بود. با وجود پوشش کد ۹۸ درصدی، چندین باگ جدی باقی مانده بودند زیرا تست‌ها به‌جای بررسی «نتایج نهایی»، «پراکسی‌ها» (شاخص‌های جایگزین) را تایید می‌کردند.

ترجمه گیت‌های کیفی

تیم یک متدولوژی بازبینی سخت‌گیرانه را از یک پروژه TypeScript دیگر اقتباس کرد و آن را به محیط Python CLI منتقل نمود:

  • TDD: نوشتن تست‌های رگرسیون Pytest پیش از هر اصلاح باگ.
  • پوشش (Coverage): اجبار به پوشش >= ۹۰٪ در CI توسط pytest-cov.
  • Linting: استفاده از Ruff و Mypy با هدف رسیدن به صفر خطا.
  • E2E: تست‌های Subprocess که باینری واقعی نصب شده را اجرا می‌کنند.
  • شعاع تخریب (Blast-radius): جست‌وجوی تمام واردکنندگان (Importers) کدی که تغییر کرده است.
  • نتیجه‌محور (Outcome-based): تایید آنچه کاربر می‌بیند، نه یک پراکسی برای آن.

شکست‌های بحرانی در تست

با وجود این گیت‌ها، چندین شکست بحرانی رخ داد:

  • تاییدهای پوچ (Vacuous Assertions): تست‌ها بررسی می‌کردند که کلمه‌ی "Traceback" در خروجی نباشد. اما Runner تست‌های Click، استثناها را در result.exception ذخیره می‌کند و آن‌ها را در خروجی نمی‌نویسد. در نتیجه، تست‌ها سبز می‌شدند در حالی که برنامه در حال کرش کردن بود.
  • فیکسچرهای دوری (Circular Fixtures): مجموعه ارزیابی از نتایج برترِ خودِ رتبه‌بند ساخته شده بود، که یک حلقه بازخورد ایجاد می‌کرد و رقبای جدید را پنهان می‌نمود. این مجموعه بازسازی شد تا شامل پردانلودترین بسته‌هایی باشد که با کلمات پرس‌وجو تطابق دارند، فارغ از امتیازشان.
  • کش بایت‌کد: فایل‌های .pyc پایتون گاهی نسخه‌های قدیمی ثابت‌ها را لود می‌کردند. توسعه‌دهندگان شاهد شکست تست بودند، کد را برگرداندند، اما مفسر همچنان بایت‌کد قدیمی را اجرا می‌کرد چون اندازه فایل و زمان تغییر آن تغییر نکرده بود.
  • تاییدهای پراکسی: یک تست هم‌روندی پاس شد چون فقط رشته‌ی خواننده (Reader thread) را بررسی می‌کرد و خطای sqlite3.OperationalError: disk I/O error (به‌طور خاص SQLITE_IOERR_LOCK) در رشته‌ی نویسنده را نادیده می‌گرفت. این مشکل به دلیل استفاده از URI فقط-خواندنی (mode=ro) در macOS بود.

بازبینی متقاطع-پلتفرمی و خصمانه

تست روی یک ماتریس CI متقاطع (لینوکس، مک، ویندوز با پایتون ۳.۱۰ تا ۳.۱۴) یک باگ خاص ویندوز را آشکار کرد. هنگام لوله‌کشی (Piping) نتایج به دستور head در ویندوز، به‌جای BrokenPipeError (EPIPE)، خطای OSError با کد EINVAL صادر می‌شد. تیم این مورد را با مدیریت هر دو کد خطا در نقطه ورود کنسول اصلاح کرد.

یک بازبین AI مستقل که هیچ حق ویرایشی نداشت و مأموریت داشت «اشتباهات نویسنده را ثابت کند»، حفره‌های بیشتری را پیدا کرد:

  • تکرارهای خالی: یک پرس‌وجوی سه کلمه‌ای بدون هیچ تطابقی، خطای ValueError: min() iterable argument is empty ایجاد می‌کرد.
  • باگ حد صفر: دستور refresh --limit 0 باعث پاک شدن اسنپ‌شات می‌شد، زیرا گارد حفاظتی ۹۵٪، عبارت 0 < 0.95 * 0 را غلط تشخیص داده و یک لیست خالی را ذخیره می‌کرد.
  • برش منفی (Negative Slicing): استفاده از -n -1 باعث چاپ ۳۸۹ نتیجه می‌شد (به دلیل نحوه عملکرد برش‌های منفی در پایتون).
  • توکن‌بندی: عبارت "résumé parser" به صورت r, sum, parser توکن‌بندی می‌شد زیرا توکن‌بند فقط ASCII بود.
  • پایداری خزش: پیش از این، یک پاسخ ۴۰۳ با بدنه HTML باعث کرش کردن کل خزش ۱۵,۰۰۰ بسته می‌شد؛ اکنون این اتفاق فقط باعث از دست رفتن یک بسته می‌شود.

عملکرد و دقت نهایی

پس از تنظیمات نهایی، ابزار به نرخ موفقیت ۹۰٪ در مجموعه طلایی ۹۵ پرس‌وجویی رسید. با این حال، تیم صادقانه گزارش می‌دهد که نرخ صحت در مجموعه اعتبارسنجی (که کاربران واقعاً تجربه می‌کنند) ۸۹٪ است. ۱۱٪ شکست‌های باقی‌مانده معمولاً مربوط به بسته‌های «میلیارد-دانلودی» است که تنها با یک کلمه رایج در پرس‌وجو تطابق دارند (مثلاً ظاهر شدن idna برای «gui application» به دلیل کلمه «application»).

برای کاهش این مورد، آن‌ها یک فاکتور اعتبار محبوبیت بر اساس نسبت کلمات اطلاعاتی تطبیق‌یافته اعمال کردند. این کار MRR (میانگین رتبه متقابل) را از ۰.۷۱۵ به ۰.۸۵۸ بهبود داد. همچنین لیستی از چهار ترکیب واقعی (unit test, time zone, data frame, web socket) پیاده شد که تنها زمانی اعمال می‌شود که هر دو کلمه در پرس‌وجو حضور داشته باشند.

مشخصات نهایی

  • سرعت جست‌وجو: حدود ۰.۱۵ ثانیه برای هر اجرا (که با وارد کردن تنبل یا Lazy Import پشته HTTP به نصف کاهش یافت).
  • اسنپ‌شات: ۱۴,۹۹۹ بسته، ۱۰.۸ مگابایت دانلود، بازسازی ماهانه از طریق GitHub Actions. گردش کار شامل یک گیت کیفی است که اگر نرخ موفقیت زیر ۸۵٪ بیاید، از انتشار جلوگیری می‌کند.
  • توزیع: توزیع سورس ۱۸ کیلوبایتی (پس از حذف مجموعه تست).
  • CI: شامل ۱۵ جاب (Job) در سه سیستم‌عامل و پنج نسخه پایتون.

درس‌های مهندسی

پروژه با این نتیجه به پایان می‌رسد که «دستورالعمل‌ها، محیط ایزوله (Sandbox) نیستند». وقتی بازبین به‌طور تصادفی کش واقعی را بازنویسی کرد (با وجود اینکه به او گفته شده بود این کار را نکند)، ثابت شد که اگر فرآیندی باید محافظت شود، باید از طریق متغیرهای محیطی (مانند XDG_CACHE_HOME) یا Read-only Mounts غیرقابل دسترس شود.

علاوه بر این، تیم آموخت که از لوله‌کشی بین یک گیت و عملیاتی که آن را محافظت می‌کند (مثلاً pytest | tail && git push) اجتناب کند، زیرا موفقیت لوله (tail) می‌تواند شکست گیت (pytest) را بپوشاند. قانون نهایی: آنچه کاربر می‌بیند را تایید کنید، نه پراکسی آن را. این رویکرد سخت‌گیرانه در رفع باگ‌ها مشابه متدولوژی پروژه Benzi است که هزینه رفع باگ‌های واقعی را به شدت کاهش داد و نشان داد که دقت در تست‌ها کلید بهره‌وری است.

خلاصه تصمیمات کلیدی

داده‌ها و توزیع:

  • منابع: انتخاب دو منبع عمومی بدون کلید به‌جای BigQuery برای جلوگیری از نیاز به حساب کاربری.
  • اسنپ‌شات: انتقال از خزش‌های سمت کاربر به اسنپ‌شات مرکزی ماهانه برای جلوگیری از محدودیت نرخ (Rate-limiting) در PyPI.
  • استراتژی URL: استفاده از تگ انتشار ثابت snapshot-latest به‌جای releases/latest برای اطمینان از اینکه انتشار کدها، URL اسنپ‌شات را خراب نمی‌کند.

رتبه‌بندی و جست‌وجو:

  • موتور: انتخاب SQLite FTS5 به‌جای BM25 خالص پایتونی برای تحلیل ریشه و سرعت.
  • منطق: پیاده‌سازی سیستم «ابتدا فیلتر، سپس رتبه‌بندی» برای جلوگیری از پیشی گرفتن بسته‌های تخصصی کوچک از کتابخانه‌های محبوب.
  • دقت: اولویت دادن به دقت (Precision) بر بازیابی (Recall) در مواردی مانند "dataframes"؛ پذیرفتن اینکه pandas ممکن است ظاهر نشود اگر متادیتای آن پراکنده باشد، به‌جای اجازه دادن به نویزهایی مانند boto3 برای «unit testing».

پایداری و امنیت:

  • اعتبارسنجی: پیاده‌سازی الگوی «ابتدا تایید، سپس جایگزینی» که در آن دانلودها در یک فایل موقت باز شده و با PRAGMA quick_check بررسی می‌شوند و سپس جایگزین اسنپ‌شات واقعی می‌گردند.
  • امنیت: استفاده از Trusted Publishing از طریق OpenID Connect برای انتشار در PyPI جهت حذف توکن‌های ذخیره شده.
  • پاک‌سازی: حذف کاراکترهای کنترلی از خلاصه‌های PyPI در زمان خزش برای جلوگیری از حملات توالی‌های Escape در ترمینال.
چرا این موضوع مهم است؟

این تجربه نشان می‌دهد که ابزارهای AI-Pair Programmer می‌توانند سرعت توسعه را افزایش دهند اما ریسک «شکست‌های پنهان» را بالا می‌برند. تکیه بر تجربه مهندسی برای اعتبارسنجی خروجی‌های AI، تنها راه جلوگیری از انتشار محصولات معیوب با تست‌های سبز است.

تأثیر برای ایران

برای برنامه‌نویسان ایرانی که به‌طور گسترده از Claude و ChatGPT برای تسریع توسعه استفاده می‌کنند، این گزارش یک نقشه راه برای جلوگیری از باگ‌های پنهان در محیط‌های Production است.

·نگاه ما
تحریریه دات‌هوش

این گزارش یک هشدار جدی درباره‌ی «اعتماد بیش از حد به پوشش کد» (Code Coverage) در عصر AI است. وقتی AI کد می‌زند، احتمال نوشتن تست‌هایی که فقط مسیرهای موفق را تایید می‌کنند (و باگ‌ها را نادیده می‌گیرند) به‌شدت بالا می‌رود. در واقع، ما از عصر «نوشتن کد» به عصر «تایید نتایج» حرکت کرده‌ایم و مهارت اصلی اکنون در طراحی تست‌های خصمانه است، نه صرفاً نوشتن توابع.

منابع

این گزارش با خط‌لولهٔ خودکار دات‌هوش از منابع معتبر جهانی تدوین و زیر نظر تحریریه منتشر شده است. روش کار ما

گفتگو

پنج‌شنبه‌های هوش‌محور

بسته‌ی هفتگی دات‌هوش

۵ خبر، ۲ ابزار، ۱ پرامپت در هر شماره. به‌زودی راه‌اندازی می‌شود — هر پنج‌شنبه صبح.

خبر کلیدی
ابزار کاربردی
پرامپت حرفه‌ای
تحلیل پژوهش
به‌زودی
زاویه‌ی ایرانی
به‌زودی
تمرین این هفته
به‌زودی

راهنماهای دات‌هوش

راهنماهای کاربردیِ دات‌هوش برای کار با هوش مصنوعی — از همین‌جا شروع کنید:

دات‌هوش

راهنمای فارسی هوش مصنوعی — با نگاه به ایران

اخبار روزانه، معرفی ابزارها و مدل‌ها، و آموزشِ کار با هوش مصنوعی؛ همیشه با این پرسش که از ایران چه چیزی کار می‌کند و چه چیزی نه.