عنوان:

‫داستان مشارکت من در OpenAI.NET


نویسنده: امیر مکارچی
تاریخ: ۱۴۰۵/۰۴/۲۳ ۱۸:۰۵
آدرس: www.dntips.ir
نوع خاصی از سکوت وجود دارد که وقتی برای اولین بار یک کدبیس (Codebase) بزرگ را باز می‌کنید، شما را فرا می‌گیرد. مخزن کد (Repository) درست مقابل شماست. سیستم پیگیری مشکلات (Issue Tracker) عمومی است. هر کلاس، تست، Pull Request و بحث‌های مربوط به طراحی برای خواندن در دسترس است. از نظر فنی، هیچ چیزی مانع مشارکت شما نیست. با این حال، فاصله بین «خواندن کد» و «تغییر دادن کد» بسیار زیاد به نظر می‌رسد. این دقیقاً همان حسی بود که وقتی شروع به کار روی SDK رسمی OpenAI برای .NET کردم، داشتم. این پروژه متعلق به مایکروسافت نیست، اما عمیقاً در اکوسیستم .NET مایکروسافت جای گرفته است: قراردادهای API عمومی، تجربه تزریق وابستگی (DI)، مدل پیکربندی و معماری کلاینت آن، همگی به شدت با الگوهای مورد استفاده در سراسر .NET و Azure SDK مرتبط هستند. به عبارت دیگر، این دقیقاً همان نوع کدبیسی بود که می‌توانست «سندروم ایمپاستر» را تحریک کند. من کی بودم که بخواهم یک تجربه جدید از تزریق وابستگی را برای کتابخانه‌ای پیشنهاد دهم که توسعه‌دهندگان در سراسر جهان از آن استفاده می‌کنند؟ اگر معماری را اشتباه فهمیده بودم چه؟ اگر Pull Request من به جای ارزش‌آفرینی، فقط باعث شلوغی کد می‌شد چه؟ اگر نگهداران پروژه (Maintainers) به پیاده‌سازی من نگاه می‌کردند و فوراً ده مشکل را می‌دیدند که من نادیده گرفته بودم چه؟ این سؤالات قبل از شروع من ناپدید نشدند. اما من با وجود آن‌ها، شروع کردم. آنچه در ادامه آمد، یک داستان سریع از نوع «ارسال PR، دریافت تایید و ادغام (Merge)» نبود. این تبدیل به سفری تقریباً پنج‌ماهه شد؛ از اولین Pull Request در ۱۸ سپتامبر ۲۰۲۵ تا ادغام نهایی در ۲۵ فوریه ۲۰۲۶. کدها تغییر کردند. طراحی عوض شد. محدوده کار گسترش یافت. در حالی که یک طراحی جامع‌تر برای اکوسیستم در حال شکل‌گیری بود، PR من متوقف شد. سپس دوباره زنده شد، چندین دور بررسی (Review) را پشت سر گذاشت، موانع نهایی CI را رد کرد و در نهایت به آن آیکون بنفش کوچکی رسید که هر مشارکت‌کننده‌ای آن را به خاطر می‌سپارد. این داستان Pull Request شماره ۶۹۶ است؛ و داستانی از اینکه چگونه آموختم یک مشارکت موفق در متن‌باز، به‌ندرت فقط به نوشتن نسخه اول کد محدود می‌شود.

تجربه‌ای که می‌خواستم برای توسعه‌دهندگان بهبود ببخشم، برای تقریباً هر توسعه‌دهنده ASP.NET Core آشنا بود: ثبت (Register) کلاینت‌های OpenAI در کانتینر داخلی Dependency Injection. در یک اپلیکیشن، توسعه‌دهندگان می‌خواهند پیکربندی کلاینت طبیعی به نظر برسد. آن‌ها می‌خواهند تنظیمات را در appsettings.json قرار دهند، اسرار (Secrets) را از طریق متغیرهای محیطی بازنویسی کنند، سرویس‌ها را هنگام استارت‌آپ ثبت کنند و کلاینت‌های Typed را هر جا که نیاز است تزریق کنند. نسخه ساده‌شده‌ای از تجربه مورد نظر به این شکل بود:
builder.AddChatClient("Chat");
public sealed class MyService(ChatClient chatClient)
{
    // Use chatClient...
}
هدف، توضیح دادنش آسان بود: کلاینت‌های OpenAI را به‌گونه‌ای بسازیم که به‌طور طبیعی با قراردادهای پیکربندی و DI که توسعه‌دهندگان .NET از قبل می‌شناسند، سازگار باشند.
اما «آسان بودن در توضیح»، با «آسان بودن در طراحی» یکی نیست. اولین پیاده‌سازی من مستقیماً به سراغ مشکل رفت. اولین کامیت من شامل متدهای Extension برای IServiceCollection بود، یک نوع Options تعریف کردم، پشتیبانی از پیکربندی و متغیرهای محیطی را اضافه کردم و مستندات و تست‌های واحد (Unit Tests) را نوشتم. این راهکار برای هر کسی که یک یکپارچه‌سازی (Integration) در ASP.NET Core انجام داده بود، کاربردی و آشنا بود. من کارها را در محیط محلی انجام داده بودم. کدهای اطراف را خوانده بودم. تست‌ها را اضافه کرده بودم و مستندات را نوشته بودم. اما در یک لحظه، آماده‌سازی باید تمام شود. شما باید کارتان را در معرض دید دیگران قرار دهید. هنوز تردیدی را که قبل از کلیک روی دکمه Create Pull Request داشتم به یاد می‌آورم. خود دکمه بی‌ضرر است، اما آنچه نمایندگی می‌کند، نیست. این دکمه، استدلال‌های خصوصی شما را به یک پیشنهاد عمومی تبدیل می‌کند. می‌گوید: «من معتقدم این کد متعلق به اینجاست.» سپس روی آن کلیک کردم.

اولین بررسی، بزرگ‌تر از کد من بود

یکی از اولین درس‌ها تقریباً بلافاصله رسید: نگهداران پروژه، یک مشارکت را فقط به عنوان تکه کدی که «کار می‌کند» بررسی نمی‌کنند. آن‌ها آن را به عنوان بخشی دائمی از یک اکوسیستم می‌بینند. «استیون توب» (Stephen Toub) یک سؤال بنیادی مطرح کرد: آیا این تجربه باید در SDK اصلی باشد یا باید توسط Aspire مدیریت شود؟ «کریستوف کوالینا» (Krzysztof Cwalina) اضافه کرد که مدل پیکربندی باید با کارهای موجود در Aspire، .NET و Azure همسو باشد. نگرانی آن‌ها این نبود که DI بی‌اهمیت است؛ بلکه این بود که چندین تیم در حال حل مشکلات مشابه بودند و یک راهکار محلی (Local) ممکن بود به‌طور اتفاقی یک قرارداد جدید ایجاد کند، به جای اینکه به یک قرارداد مشترک بپیوندد. این موضوع ماهیت بحث را تغییر داد. من با یک سؤال پیاده‌سازی آمده بودم: «آیا این کد کار می‌کند؟»؛ اما نگهداران پروژه سؤالی اکوسیستمی می‌پرسیدند: «آیا این انتزاع (Abstraction) درست است، در لایه درست قرار دارد و از الگوی بلندمدت صحیحی استفاده می‌کند؟»؛ این‌ها دو سؤال بسیار متفاوت هستند. برای یک مشارکت‌کننده، این لحظه‌ای آسیب‌پذیر است. راحت است که بازخوردهای معماری را به عنوان قضاوتی شخصی ترجمه کنیم: «راهکار من رد شد، پس من به اندازه کافی خوب نبودم.» اما این تفسیر، چیزی را که نگهداران پروژه از آن محافظت می‌کنند نادیده می‌گیرد. یک SDK که به‌طور گسترده استفاده می‌شود، بهای هر انتزاع عمومی را برای سال‌ها می‌پردازد. وقتی یک API منتشر می‌شود، تغییر دادن آن بسیار دشوارتر از تغییر دادن یک Pull Request است. بازخورد این نبود که «نباید تلاش می‌کردی»، بلکه این بود که «این ایده با طراحی‌ای بزرگ‌تر از این مخزن کد در ارتباط است».
در اول اکتبر، «جسی اسکوایر» (Jesse Squire) توضیح داد که تیم هنوز در حال تعریف الگوهای مشترک برای ثبت، ساخت، پیکربندی کلاینت و همسویی با اکوسیستم گسترده‌تر .NET است. بدون یک طراحی کاملاً مدون، تیم هنوز نمی‌توانست راهنمایی‌های دقیقی برای پیشبرد پیاده‌سازی ارائه دهد. او سه گزینه پیشنهاد داد: بستن PR، بازگشت به آن در آینده با یک پیاده‌سازی جدید، یا متوقف کردن (Park) آن و همکاری پس از آماده شدن طراحی. من گزینه متوقف کردن را انتخاب کردم. این تصمیم در نهایت یکی از مهم‌ترین بخش‌های این مشارکت شد. پاسخ دادم که خوشحال می‌شوم منتظر بمانم، کد را بازبینی کنم و پیاده‌سازی را با الگوهای گسترده‌تر همسو کنم. «مایکل نش» (Michael Nash) موافقت کرد که این بهترین مسیر است و گفت وقتی طراحی آماده شد، می‌توانیم همکاری کنیم. برای چندین ماه، Pull Request باز ماند اما به‌طور عمدی متوقف شد. در ابتدا، این حسِ درجا زدن را داشت؛ اما در نگاه به گذشته، این هم بخشی از کار بود.

متن‌باز یعنی دانستن اینکه چه زمانی باید منتظر ماند

توسعه‌دهندگان آموزش دیده‌اند که برای «حرکت» ارزش قائل شوند. ما تیکت‌ها را می‌بندیم، کامیت‌ها را Push می‌کنیم، تست‌های شکست‌خورده را کم می‌کنیم و پیشرفت را با تغییرات قابل مشاهده می‌سنجیم. انتظار کشیدن، غیربهره‌ور به نظر می‌رسد. اما پروژه‌های بالغ متن‌باز در مقیاس متفاوتی عمل می‌کنند. یک مشارکت ممکن است به هماهنگی بین چندین مخزن کد، تیم‌های فریم‌ورک، مالکان طراحی، زمان‌بندی‌های انتشار و فرآیندهای بررسی API وابسته باشد. بهترین اقدام بعدی گاهی یک کامیت دیگر نیست؛ بلکه حفظ گفتگو تا زمانی است که پروژه آماده اتخاذ تصمیم درست باشد.
متوقف کردن PR سه چیز به من آموخت:
۱. تأخیر نگهدارنده پروژه لزوماً به معنای بی‌علاقگی نیست؛ بلکه ممکن است بازتابی از هماهنگی‌هایی باشد که از بیرون دیده نمی‌شوند.
۲. زنده نگه داشتن یک مشارکت به معنای درخواست مکرر برای توجه نیست؛ بلکه به معنای در دسترس بودن، ارتباط محترمانه و آمادگی در لحظه‌ای است که طراحی قابل اجرا شود.
۳. مالکیت کد و مالکیت ایده متفاوت است. من مشارکت را شروع کرده بودم، اما طراحی نهایی باید متعلق به پروژه و کاربرانش می‌بود، نه متعلق به اولین پیاده‌سازی من.
نکته آخر از همه مهم‌تر بود. اگر من از نظر عاطفی به کد اولیه دلبستگی پیدا می‌کردم، از چیز اشتباهی دفاع می‌کردم. هدف این نبود که «دقیقاً پیاده‌سازی من» ادغام شود؛ هدف این بود که تجربه توسعه‌دهنده .NET به‌گونه‌ای بهبود یابد که نگهداران پروژه بتوانند در بلندمدت از آن پشتیبانی کنند.

بازگشت Pull Request به زندگی

نقطه عطف با [Issue شماره ۹۳۲: "OpenAI: Add Configuration and DI Support"] رسید. این Issue، کارهای SDK OpenAI را به طراحی پیکربندی و DI در System.ClientModel متصل کرد. مستندات طراحی Azure SDK، یک مدل قابل استفاده مجدد برای تنظیمات کلاینت متصل به پیکربندی (Configuration-bound)، ثبت‌های DI معمولی و Keyed، حل اعتبارنامه‌ها (Credential Resolution)، گزینه‌های کلاینت تودرتو و مراجع پیکربندی مشترک را توصیف می‌کرد. این همان لحظه‌ای بود که معماری در ذهنم جا افتاد. وظیفه دیگر «اضافه کردن چند متد کمکی برای ثبت سرویس‌های خاص OpenAI» نبود؛ بلکه «ترجمه یک الگوی مشترک Client-Model به ساختار عینی SDK OpenAI» بود.
این تفاوت شاید ظریف به نظر برسد، اما تقریباً همه چیز را تغییر داد.
پیاده‌سازی اولیه من بر ثبت مستقیم در IServiceCollection و یک شیء Options مخصوص OpenAI متمرکز بود. طراحی بازنگری‌شده بر مفاهیم زیر متمرکز شد:
  • هر کلاینت تخصصی، کلاس ClientSettings با تایپ قوی (Strongly Typed) مخصوص به خود را دریافت می‌کند.
  • ویژگی‌های آن کلاس تنظیمات، بازتاب‌دهنده پارامترهای سازنده (Constructor) اصلی کلاینت است.
  • رفتار اعتبارنامه‌ها از مدل تنظیمات پایه مشترک می‌آید، به جای اینکه در هر تایپ مشتق شده دوباره اختراع شود.
  • هر کلاینت سازنده‌ای می‌گیرد که شیء تنظیمات آن را می‌پذیرد.
  • ثبت سرویس‌ها از IHostApplicationBuilder و زیرساخت عمومی System.ClientModel استفاده می‌کند.
  • هر دو نوع ثبت معمولی و Keyed پشتیبانی می‌شوند تا امکان داشتن چندین کلاینت از یک نوع با پیکربندی‌های متفاوت فراهم شود.
به عنوان مثال، ChatClient سازنده‌ای داشت که حول مدل، سیاست احراز هویت و گزینه‌های کلاینت شکل گرفته بود. بنابراین، تایپ تنظیمات آن باید بخش‌های قابل پیکربندی آن سازنده را نمایندگی می‌کرد:

[Experimental("SCME0002")]
public sealed class ChatClientSettings : ClientSettings
{
 public string? Model { get; set; }

 public OpenAIClientOptions? Options { get; set; }

 protected override void BindCore(IConfigurationSection section)
 {
 // Bind client-specific configuration.
 }
}
پیاده‌سازی دقیق در طول بررسی تکامل یافت، اما اصل معماری اکنون روشن بود: تنظیمات (Settings) باید کلاینت را توصیف کنند، در حالی که مدل مشترک کلاینت (Shared Client Model) باید مکانیسم‌های مشترک پیکربندی و DI را فراهم کند.
[Experimental("SCME0002")]
public ChatClient(ChatClientSettings settings)
 : this(
 settings.Model,
 AuthenticationPolicy.Create(settings),
 settings.Options)
{
}
این لحظه «یافتم!» (Aha!) من بود. دیگر مجموعه‌ای از متدهای Extension ایزوله را نمی‌دیدم، بلکه SDK را به عنوان یکی از شرکت‌کنندگان در یک اکوسیستم بزرگ‌تر کلاینت‌های .NET می‌دیدم.

حرکت سریع و کشف اینکه طراحی هنوز در حال حرکت است

وقتی مسیر مشخص شد، سریع حرکت کردم. شاید بیش از حد سریع. در ۵ فوریه، PR را بر اساس Issue شماره ۹۳۲ به‌روز کردم و درخواست بررسی دادم. مایکل پاسخ داد که من سریع‌تر از آمادگی او شروع کرده‌ام چون آن Issue هنوز تمام جزئیات پیاده‌سازی را شامل نمی‌شد. او راهنمایی‌های ناقص را به‌روز کرد. این تعامل چیزی را نشان داد که بعدها در این همکاری قدر بدانم: نگهدارنده پروژه صرفاً آنچه ناقص بود را رد نکرد، بلکه طراحی را شفاف کرد و من پیاده‌سازی را اصلاح کردم. رابطه ما کمتر شبیه «ارسال‌کننده در مقابل دروازه‌بان» و بیشتر شبیه دو توسعه‌دهنده شد که برای رسیدن به یک نتیجه مشترک تلاش می‌کنند. تا ۷ فوریه، جزئیات تکمیلی را اعمال کردم و PR را برای بررسی فرستادم. سپس تکرارهای (Iterations) واقعی شروع شد.

آزمونی سخت: یادگیری از طریق بررسی (Review)

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

یک منبع واحد برای حقیقتِ اعتبارسنجی (Validation Truth)

در سازنده یکی از کلاینت‌ها، من چک کردن پارامترها و رفتارهای جایگزینی (Fallback) را تکرار کرده بودم که قبلاً در سازنده اصلی وجود داشت. مایکل اشاره کرد که سازنده اصلی مالک این اعتبارسنجی است و سازنده جدیدِ مبتنی بر Settings باید اجازه دهد سازنده اصلی کارش را انجام دهد. درس بزرگتر از حذف چند خط کد بود: اورلودهای سازنده باید به یک مسیر اعتبارسنجی واحد ختم شوند، به جای اینکه تعاریف موازی از وضعیت معتبر (Valid State) ایجاد کنند. اعتبارسنجی تکراری هنگام نوشتن حس امنیت می‌دهد، اما در طول زمان باعث ایجاد «انحراف» (Drift) می‌شود. یک سازنده مقداری را می‌پذیرد که دیگری رد می‌کند. یکی مقدار پیش‌فرض را می‌دهد و دیگری فراموش می‌کند. متمرکز کردن قوانین ساخت، استدلال درباره رفتار کد و نگهداری آن را آسان‌تر می‌کند.

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

من نام‌های پیش‌فرض برای بخش‌های پیکربندی (Configuration Sections) در متدهای ثبت قرار داده بودم. بررسی‌ها این فرض را به چالش کشید. یک توسعه‌دهنده ممکن است به دو کلاینت چت نیاز داشته باشد که هر کدام مدل، Endpoint یا اعتبارنامه متفاوتی دارند. نام بخش‌ها می‌تواند PrimaryChat یا PremiumChat یا هر چیز دیگری باشد که برای آن اپلیکیشن معنا دارد. حذف پیش‌فرض‌ها صرفاً یک ترجیح در سبک API نبود؛ بلکه انتخاب کاربر را حفظ می‌کرد و سناریوهای چندکلاینتی را به‌طور طبیعی پشتیبانی می‌کرد. این بینش مستقیماً به ثبت Keyed وصل شد:
builder.AddKeyedChatClient("primary", "PrimaryChat");
builder.AddKeyedChatClient("premium", "PremiumChat");
طراحی فقط برای این نبود که ثبت یک کلاینت آسان شود، بلکه برای این بود که کل خانواده‌ای از توپولوژی‌های استقرار واقعی امکان‌پذیر شوند.

اجتناب از جفت‌شدگی اتفاقی (Accidental Coupling)

در پیاده‌سازی بازنگری‌شده، ابتدا یک لایه مشترک OpenAISpecializedClientSettings معرفی کردم. به نظر می‌رسید راهی منطقی برای کاهش تکرار باشد. مایکل پیشنهاد داد که هر کلاس تنظیمات مستقیماً از ClientSettings ارث‌بری کند، زیرا لایه اضافی مخصوص OpenAI، کلاینت‌های تخصصی را بیش از حد نیاز به هم جفت (Couple) می‌کند. این یک یادآوری ارزشمند بود: «حذف تکرار» لزوماً به معنای «طراحی خوب» نیست. گاهی اوقات مقدار کمی ساختار تکراری، ارزان‌تر از یک انتزاع مشترک است که تایپ‌های غیرمرتبط را مجبور می‌کند با هم تکامل یابند.

منطق Bind کردن را جایی قرار دهید که داده‌ها درک شوند

منطق Bind کردن پیکربندی برای OpenAIClientOptions ابتدا خارج از نوع Options بود. در بررسی‌ها، این منطق به یک سازنده داخلی در OpenAIClientOptions منتقل شد که یک IConfigurationSection می‌پذیرفت. باز هم، این فقط سازماندهی فایل نبود. این کار باعث شد تفسیر داده‌ها در کنار شیئی قرار بگیرد که معنا و اعتبارسنجی آن مقادیر را درک می‌کند.

کامل بودن، بخشی از سازگاری است

در دور اول، من کلاینت‌های تخصصی بدیهی را پوشش دادم: چت، embeddings، صدا، تصویر و نظارت (Moderation). سپس در بررسی متوجه شدم که SDK انواع بسیار بیشتری از کلاینت‌ها را دارد: assistants، batches، containers و غیره. اگر این ویژگی فقط برای زیرمجموعه‌ای از کلاینت‌ها منتشر می‌شد، توسعه‌دهندگان با یک SDK ناسازگار مواجه می‌شدند: یک کلاینت را می‌شد با الگوی جدید پیکربندی کرد، اما دیگری هنوز نیاز به ساخت دستی داشت. بنابراین محدوده کار گسترش یافت. در نسخه نهایی، Pull Request بیش از چهل فایل را تغییر داده بود. آنچه به عنوان چند متد کمکی DI شروع شده بود، به یک قابلیت سیستماتیک در تمام خانواده کلاینت‌ها تبدیل شد. این گسترش، «خزش محدوده» (Scope Creep) نبود؛ بلکه نتیجه طبیعی درک درست معماری بود.

لحظه‌ای که لحن بازخوردها تغییر کرد

در بررسی‌های طولانی، لحظه ظریف اما مشخصی وجود دارد که گفتگو تغییر می‌کند. در ابتدا، کامنت‌ها شکل طراحی را به چالش می‌کشند. بعداً، جزئیات را اصلاح می‌کنند. سؤالات از «آیا این انتزاع باید وجود داشته باشد؟» به «آیا می‌توانیم این عبارت را صریح‌تر کنیم؟» یا «آیا این متدها می‌توانند به ترتیب الفبایی باشند؟» تغییر می‌کنند. در ۱۰ فوریه، پس از اینکه تغییرات معماری اعمال شد، مایکل نوشت که فقط یک به‌روزرسانی جزئی باقی مانده و کارها خوب به نظر می‌رسند. دو روز بعد، در بررسی‌اش نوشت: "Changes LGTM, thanks again." (تغییرات از نظر من عالی است، باز هم ممنون). برای یک مشارکت‌کننده، این چند کلمه وزن زیادی دارند. به این معنی نیست که کار تمام شده، اما یعنی ایده اصلی از یک مرز مهم عبور کرده است. کد دیگر به عنوان یک پیشنهاد خارجی ارزیابی نمی‌شود، بلکه شروع به شبیه شدن به بخشی از پروژه می‌کند. سپس بررسی دیگری رسید. Copilot نبودِ چک کردن‌های null را در سازنده‌های جدید شناسایی کرد و درخواست پوشش تستی برای Bind کردن پیکربندی و ثبت‌های معمولی و Keyed داد. José Harriaga این یافته‌ها را بررسی کرد و پیشنهاد داد در جاهایی که اعتبارسنجی و مقداردهی صریح منجر به مسیر ساخت شفاف‌تر و امن‌تری می‌شود، زنجیره‌سازی سازنده‌ها (Constructor Chaining) حذف شود. او همچنین خواست متدهای ثبت به صورت سازگار مرتب شوند تا نگهداران آینده راحت‌تر کلاینت‌های جا مانده را شناسایی کنند. Pull Request دوباره به وضعیت "Changes Requested" بازگشت. در ابتدای این مسیر، شاید این حس عقب‌نشینی می‌داد. اما در آن زمان، من متفاوت فکر می‌کردم. تایید نهایی، خط پایان نیست اگر بررسی‌کننده دیگری یک شکاف واقعی پیدا کند. کد آنقدر بهبود یافته بود که سطح عمیق‌تری از بررسی را جذب کند. من تغییرات را اعمال کردم و از خوزه برای بررسی دقیقش تشکر کردم.

کیلومتر آخر، باشکوه نبود

بخش نهایی یک مشارکت متن‌باز اغلب کمتر از کارهای معماری، سینمایی است. لیست‌های API تولیدشده (Generated) باید به‌روز می‌شدند. یک اسکریپت مخزن (Export-Api.ps1) بود که باید اجرا می‌شد. حتی بعد از اینکه فکر می‌کردم فایل‌های API به‌روز هستند، CI هنوز شکست می‌خورد. برنچ‌های تکمیلی و PRهای نگهداران پروژه برای هماهنگی وجود داشت. دوباره درخواست شد که API Surface بازتولید شود. اسکریپت را دوباره اجرا کردم اما تغییری ایجاد نشد. مایکل با PRهای کوچک روی فورک (Fork) من، به حل مسائل مربوط به وضعیت مخزن کمک کرد. این بخش از داستان مهم است چون مشارکت در سطح کیفیت تولیدی (Production-quality)، فقط بحث‌های ظریف معماری نیست. بلکه شامل این موارد است:
  • درک اسکریپت‌های Build خاص پروژه؛
  • همگام نگه داشتن فایل‌های API عمومی تولید شده؛
  • پاسخ شفاف وقتی نتایج محلی و نتایج CI با هم نمی‌خوانند؛
  • پذیرفتن کمک وقتی نگهداران پروژه می‌توانند سریع‌تر مشکلات خاص مخزن را حل کنند؛
  • و درگیر ماندن حتی بعد از اینکه کارهای «جالب» کدنویسی تمام شده است.
در ۲۵ فوریه، مایک PR را تایید کرد و از خوزه خواست برای آخرین بار نگاهی بیندازد. خوزه در ساعت ۰۹:۰۱ UTC آن را تایید کرد. یک دقیقه بعد، Pull Request شماره ۶۹۶ ادغام شد .

نشان بنفش

گیت‌هاب یک Pull Request ادغام شده را با یک آیکون بنفش کوچک نشان می‌دهد. از نظر بصری، تقریباً هیچ است. اما از نظر عاطفی، کل این سفر را در خود جای داده است. شامل اضطراب قبل از اولین ارسال. سؤالات اولیه معماری. لحظه‌ای که PR برای جلوگیری از ادغام اتفاقی متوقف شد. انتخابِ متوقف کردن کار به جای رهای کردن آن. ماه‌ها انتظار. Issue طراحی جدید. بازنویسی کد. بررسی‌های دقیق مایکل. بررسی‌های گسترده‌تر خوزه. شکست‌های CI. فایل‌های API تولید شده. تاییدات نهایی و از همه مهم‌تر، شامل تغییر در دیدگاه من است. وقتی PR را باز کردم، فکر می‌کردم موفقیت یعنی ثابت کنم راهکار اولیه من «به اندازه کافی خوب» است. وقتی ادغام شد، فهمیدم موفقیت یعنی کمک به پروژه برای رسیدن به «راهکار درست» حتی اگر این کار مستلزم جایگزینی بخش بزرگی از رویکرد اول من باشد.

اثر موجی: مشارکتی فراتر از یک متد کمکی

ویژگی نهایی، یک میان‌بر DI ایزوله و مخصوص OpenAI نبود. بلکه الگویی را پیاده کرد که به System.ClientModel (زیرساخت مشترک کلاینت‌ها در Azure SDK) متصل بود.
این طراحی گسترده‌تر قابلیت‌هایی چون موارد زیر را پشتیبانی می‌کند:
  • تنظیمات با تایپ قوی که از پیکربندی بارگذاری می‌شوند؛
  • گزینه‌های تودرتو برای Pipeline، Retry، Endpoint، Logging و کلاینت؛
  • اعتبارنامه‌هایی که از طریق پیکربندی یا Resolvers خارجی تامین می‌شوند؛
  • ثبت‌های معمولی و Keyed کلاینت؛
  • چندین نمونه از یک نوع کلاینت؛
  • و سینتکس مرجع برای به اشتراک گذاشتن پیکربندی بین کلاینت‌ها.
بنابراین اثر موجی در هر دو جهت بود. طراحی گسترده‌تر .NET و Azure SDK، PR من را شکل داد و SDK دات‌نت OpenAI به یک پذیرنده عینی (Concrete Adopter) برای آن طراحی در تمام کلاینت‌های تخصصی‌اش تبدیل شد. این نتیجه‌ای بسیار معنادارتر از اضافه کردن چند متد کمکی است. این کار «سازگاری» (Consistency) ایجاد می‌کند. توسعه‌دهنده دات‌نتی که این الگو را در یک کتابخانه کلاینت یاد می‌گیرد، می‌تواند آن را در جای دیگر هم شناسایی کند. نگهداران پروژه یک مدل مشترک به دست می‌آورند. انواع کلاینت‌های آینده، مسیر گسترش شفافی دارند. این تجربه طرز فکر من را درباره معماری تغییر داد: گاهی اوقات باارزش‌ترین مشارکت، اختراع یک انتزاع منحصربه‌فرد نیست، بلکه شناسایی یک انتزاع مشترک و پیاده‌سازی وفادارانه آن در یک زمینه جدید است.

دستاوردهای فنی که با خود خواهم داشت

این بررسی‌ها درس‌هایی را به من داد که اکنون آن‌ها را به عنوان اصول مهندسی قابل استفاده می‌بینم:
  • API عمومی را برای اکوسیستم طراحی کنید، نه فقط برای یک مثال فوری. متد ثبت کلاینتی که برای یک کلاینت پیش‌فرض کار می‌کند، ممکن است زمانی که اپلیکیشن به چندین نمونه با پیکربندی متفاوت نیاز دارد، شکست بخورد.
  • اورلودها را از طریق یک مدل ساخت و اعتبارسنجی واحد هدایت کنید. تکرار اعتبارسنجی در سازنده‌ها باعث ایجاد ناسازگاری‌های ظریف می‌شود.
  • فقط برای حذف تکرار، از ارث‌بری استفاده نکنید. یک تایپ پایه مشترک می‌تواند تبدیل به یک جفت‌شدگی (Coupling) ناخواسته بین کلاینت‌هایی شود که در حالت عادی مستقل هستند.
  • منطق Bind کردن را نزدیک به تایپی قرار دهید که معنای مقادیر را درک می‌کند. پارس کردن پیکربندی، مقادیر پیش‌فرض و اعتبارسنجی نباید در کلاس‌های Extension غیرمرتبط پخش شوند.
  • سازگاری مستلزم پوشش کامل است. اگر یک ویژگی از نظر مفهومی برای خانواده‌ای از کلاینت‌ها کاربرد دارد، پیاده‌سازی جزئی می‌تواند یک ظاهر عمومی گیج‌کننده ایجاد کند.
  • تزریق وابستگی Keyed یک مورد استثنایی (Edge Case) نیست. داشتن چندین مدل، Endpoint، Tenant یا اعتبارنامه، از الزامات عادی محیط Production است.
  • تست‌ها باید تجربه توسعه‌دهنده را تأیید کنند، نه صرفاً ثبت سرویس را. دیدن یک Service Descriptor در کانتینر کافی نیست؛ Bind شدن پیکربندی و کلاینت‌های Resolve شده باید درست عمل کنند.
  • آرتیفکت‌های تولید شده API بخشی از قرارداد هستند. خطوط پایه API عمومی و اسکریپت‌های Export لایق همان توجهی هستند که به کدهای دست‌نویس می‌شود.

آنچه درباره ارتباطات آموختم

درس‌های فنی فقط نیمی از ماجرا بود. این مشارکت به من آموخت که «ارتباطات» بخشی از «پیاده‌سازی» است. یاد گرفتم که وضعیت کار را بپرسم بدون اینکه تصور کنم نادیده گرفته شده‌ام. یاد گرفتم تفاوت بین «طراحی متوقف شده» و «مشارکت‌کننده رد شده» را تشخیص دهم. یاد گرفتم وقتی تغییرات آماده است به وضوح بیان کنم، آنچه را که تست کرده‌ام خلاصه کنم و آنچه را که هنوز خراب است توصیف کنم. یاد گرفتم که یک پاسخ کوتاه مانند «خوشحال می‌شوم وقتی طراحی آماده شد، کد را بازبینی کنم» می‌تواند ماه‌ها همکاری آینده را حفظ کند. ارتباطات خوب در متن‌باز نه منفعل است و نه مطالبه‌گر؛ بلکه دقیق، صبور و متمرکز بر «مالکیت مشترک» است. بهترین تبادلات در PR، تراکنش‌هایی نبود که در آن نگهدارنده دستور می‌داد و من اجرا می‌کردم؛ بلکه لحظاتی از استدلال مشترک بود:
«کلاس پایه مشترک ممکن است تکرار را کم کند، اما آیا کلاینت‌ها را به هم جفت می‌کند؟»
«سازنده می‌تواند اینجا اعتبارسنجی کند، اما آیا این مسئولیت سازنده اصلی نیست؟»
«نام پیش‌فرض بخش پیکربندی راحت است، اما وقتی کاربر به دو کلاینت چت نیاز دارد چه می‌شود؟»
«کلاینت‌های بدیهی پوشش داده شده‌اند، اما وقتی توسعه‌دهنده به یک کلاینت کمتر رایج می‌رسد، SDK چه حسی خواهد داشت؟»
این سؤالات پیاده‌سازی را بهتر کرد، اما مرا هم به مهندس بهتری تبدیل کرد.

توصیه‌ای به توسعه‌دهندگانی که اولین مشارکت بزرگ خود را انجام می‌دهند

اگر به یک مخزن کد بزرگ خیره شده‌اید و فکر می‌کنید آیا جای شما آنجاست یا نه، این چیزی است که اکنون به شما می‌گویم:
۱. قبل از اینکه سندروم ایمپاستر ناپدید شود، شروع کنید. اعتمادبه‌نفس اغلب بعد از اقدام می‌آید، نه قبل از آن. کد را بخوانید، مشکل را محدود کنید، تست اضافه کنید و پیشنهادتان را بدهید.
۲. اولین پیاده‌سازی خود را به عنوان «شروع یک گفتگو» ببینید. کد به نگهداران پروژه چیزی عینی برای ارزیابی می‌دهد. حتی اگر معماری تغییر کند، ارزش این کد از بین نمی‌رود.
۳. بازخوردهای مربوط به طراحی را از قضاوت درباره خودتان جدا کنید. یک بررسی‌کننده سخت‌گیر ممکن است اساس رویکرد شما را به چالش بکشد. این به معنای به چالش کشیدن حق شما برای مشارکت نیست.
۴. برای «راهکار ادغام شده» بهینه کنید، نه برای «حفظ کد اصلی خودتان». حذف یا بازنویسی کار خودتان شکست نیست؛ بلکه اغلب گواه این است که شما مشکل را عمیق‌تر از لحظه شروع درک کرده‌اید.
۵. آیین‌های عملیاتی مخزن کد را یاد بگیرید. فرمت‌بندی، فایل‌های تولید شده، خطوط پایه API، قراردادهای تست و اسکریپت‌های CI بخشی از فرهنگ مهندسی پروژه هستند.
۶. صبور باشید اما غیب نشوید. اگر طراحی متوقف شده است، روی گام بعدی توافق کنید، در دسترس بمانید و زمانی برگردید که پروژه شفافیت کافی برای پیشروی داشته باشد.
۷. متوجه لحظه‌ای شوید که «همکاری» آغاز می‌شود. لحظه‌ای که یک نگهدارنده شروع به توضیح بده بستان‌ها (Trade-offs) می‌کند، جایگزین‌های عینی پیشنهاد می‌دهد یا در حل آخرین مشکل CI کمک می‌کند، شما دیگر صرفاً درخواست پذیرش نمی‌کنید؛ شما در حال ساختن چیزی به صورت مشترک هستید.

تأمل نهایی

Pull Request من با یک ایده کاربردی شروع شد: پیکربندی و تزریق کلاینت‌های OpenAI در اپلیکیشن‌های .NET را آسان‌تر کنم و به چیزی بسیار ارزشمندتر ختم شد. یاد گرفتم که یک SDK بالغ چگونه طراحی API عمومی را ارزیابی می‌کند. یاد گرفتم چرا همسویی با اکوسیستم می‌تواند مهم‌تر از یک راهکار محلی ظریف باشد. یاد گرفتم که انتظار کشیدن می‌تواند یک تصمیم مهندسی باشد. یاد گرفتم که کامنت‌های بررسی (Review) اغلب درس‌های فشرده معماری هستند. یاد گرفتم که کامل بودن، تست‌ها، فایل‌های API تولید شده و CI همگی بخشی از «منتج کردن» (Shipping) هستند، نه کارهای اداری پیرامون آن و یاد گرفتم که متن‌باز صحنه‌ای نیست که در آن یک مشارکت‌کننده نبوغ فردی خود را ثابت کند. در بهترین حالت، متن‌باز جایی است که افرادی با زمینه‌های مختلف، به‌تدریج یک درک مشترک می‌سازند. مشارکت‌کننده انرژی، زمان و یک پیشنهاد عینی می‌آورد. نگهداران پروژه دانش تاریخی، محدودیت‌های اکوسیستم و مسئولیت آینده را می‌آورند. بررسی‌ها (Review)، این ورودی‌ها را به چیزی تبدیل می‌کند که هیچ‌کدام از طرفین به تنهایی نمی‌توانستند تولید کنند.
نشان بنفش Merged رضایت‌بخش بود. البته که بود. اما پیروزی عمیق‌تر، درک این بود که من توانستم وارد کدبیسی شوم که در ابتدا مرا می‌ترساند، در میان عدم قطعیت و بازنگری‌ها بمانم، از افرادی که اکوسیستم را در عمق متفاوتی درک می‌کردند بیاموزم و کمک کنم تا یک ویژگی تا مراحل نهایی در پروژه جای بگیرد. اولین کامیت مهم بود چون سفر را آغاز کرد. ادغام (Merge) مهم بود چون من اصرار نکردم که سفر دقیقاً همان‌جایی تمام شود که در ابتدا تصور می‌کردم.